Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
27 changes: 24 additions & 3 deletions lib/wrapper.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -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",
Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
@@ -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"
Expand Down
20 changes: 13 additions & 7 deletions test/wrapper.test.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -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, '');

Expand All @@ -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']
Expand Down
Loading