From f9763398a887c59e6141f1d41e90d69f7ac7df30 Mon Sep 17 00:00:00 2001 From: cafitac Date: Tue, 28 Apr 2026 21:16:06 +0900 Subject: [PATCH] chore: release 0.3.28 --- CHANGELOG.md | 5 +++++ lib/wrapper.cjs | 27 ++++++++++++++++++++++++--- package.json | 2 +- pyproject.toml | 2 +- test/wrapper.test.cjs | 20 +++++++++++++------- 5 files changed, 44 insertions(+), 12 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e677a6c..3cd1b03 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,11 @@ The format is inspired by Keep a Changelog and is intentionally lightweight whil ## [Unreleased] +## [0.3.28] - 2026-04-28 + +### Fixed +- `agent-learner update` now prefers the npm executable next to the installed wrapper script itself before falling back to the active `node` or plain `npm`, so login/non-interactive shells that resolve `node` from `/usr/local/bin` no longer redirect updates into the wrong global prefix. + ## [0.3.27] - 2026-04-28 ### Fixed diff --git a/lib/wrapper.cjs b/lib/wrapper.cjs index 6e61e65..44518cb 100644 --- a/lib/wrapper.cjs +++ b/lib/wrapper.cjs @@ -658,9 +658,30 @@ function printDoctor(report, jsonMode = false) { } } -function runWrapperUpdate(stdio = 'inherit', runner = spawnSync, nodeExecutable = process.execPath) { - const siblingNpm = path.join(path.dirname(nodeExecutable), process.platform === 'win32' ? 'npm.cmd' : 'npm'); - const tool = fs.existsSync(siblingNpm) ? siblingNpm : 'npm'; +function runWrapperUpdate( + stdio = 'inherit', + runner = spawnSync, + nodeExecutable = process.execPath, + wrapperExecutable = process.argv[1] +) { + const npmBasename = process.platform === 'win32' ? 'npm.cmd' : 'npm'; + const candidateDirs = []; + for (const executable of [wrapperExecutable, nodeExecutable]) { + if (!executable) { + continue; + } + const executableDir = path.dirname(executable); + candidateDirs.push(executableDir); + try { + candidateDirs.push(path.dirname(fs.realpathSync(executable))); + } catch { + // Best-effort only; fall back to the non-resolved path. + } + } + const siblingNpm = candidateDirs + .map((dir) => path.join(dir, npmBasename)) + .find((candidate) => fs.existsSync(candidate)); + const tool = siblingNpm || 'npm'; const result = runner(tool, ['install', '-g', '@cafitac/agent-learner@latest'], { stdio, encoding: 'utf-8' diff --git a/package.json b/package.json index 2a240c8..2486122 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@cafitac/agent-learner", - "version": "0.3.27", + "version": "0.3.28", "description": "npm delivery wrapper for the agent-learner Python core", "license": "MIT", "type": "commonjs", diff --git a/pyproject.toml b/pyproject.toml index f1d7566..0d902a8 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "agent-learner" -version = "0.3.27" +version = "0.3.28" description = "Reusable self-learning engine for agent workflows" readme = "README.md" requires-python = ">=3.11" diff --git a/test/wrapper.test.cjs b/test/wrapper.test.cjs index b95a089..ef516c4 100644 --- a/test/wrapper.test.cjs +++ b/test/wrapper.test.cjs @@ -302,12 +302,17 @@ test('printHelp advertises bootstrap as the install path', () => { assert.doesNotMatch(output, /install-claude/); }); -test('runWrapperUpdate prefers the npm next to the active node executable', () => { - const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'agent-learner-wrapper-update-')); - const binDir = path.join(tempRoot, 'bin'); - fs.mkdirSync(binDir, { recursive: true }); - const nodeExecutable = path.join(binDir, process.platform === 'win32' ? 'node.exe' : 'node'); - const npmExecutable = path.join(binDir, process.platform === 'win32' ? 'npm.cmd' : 'npm'); +test('runWrapperUpdate prefers the npm next to the wrapper script when node comes from a different PATH entry', () => { + const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'agent-learner-wrapper-script-update-')); + const wrapperBinDir = path.join(tempRoot, 'nvm-bin'); + const systemBinDir = path.join(tempRoot, 'system-bin'); + fs.mkdirSync(wrapperBinDir, { recursive: true }); + fs.mkdirSync(systemBinDir, { recursive: true }); + + const wrapperExecutable = path.join(wrapperBinDir, process.platform === 'win32' ? 'agent-learner.cmd' : 'agent-learner'); + const nodeExecutable = path.join(systemBinDir, process.platform === 'win32' ? 'node.exe' : 'node'); + const npmExecutable = path.join(wrapperBinDir, process.platform === 'win32' ? 'npm.cmd' : 'npm'); + fs.writeFileSync(wrapperExecutable, ''); fs.writeFileSync(nodeExecutable, ''); fs.writeFileSync(npmExecutable, ''); @@ -316,7 +321,8 @@ test('runWrapperUpdate prefers the npm next to the active node executable', () = calls.push({ tool, args }); return { status: 0, stdout: '', stderr: '' }; }; - assert.equal(runWrapperUpdate('pipe', fakeRunner, nodeExecutable), 0); + + assert.equal(runWrapperUpdate('pipe', fakeRunner, nodeExecutable, wrapperExecutable), 0); assert.deepEqual(calls[0], { tool: npmExecutable, args: ['install', '-g', '@cafitac/agent-learner@latest']