Skip to content
Closed
Show file tree
Hide file tree
Changes from 1 commit
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
34 changes: 34 additions & 0 deletions scripts/install.sh
Original file line number Diff line number Diff line change
Expand Up @@ -2241,6 +2241,40 @@ verify_nemoclaw() {
fi
fi

# The npm prefix active now can differ from the prefix the CLI was installed
# under — a Homebrew npm shadowing the installer's nvm npm, for example — which
# leaves the user-local shim as the only working entry point. Probe it here
# rather than only listing it in the diagnostic below, so a working install is
# not reported as a failure (#8311).
#
# NEMOCLAW_READY_NOW deliberately stays false for the same reason as the
# npm-prefix branch above: this shell still cannot resolve $_CLI_BIN by name,
# so print_done() must keep emitting the PATH-refresh hint.
if [[ -x "$NEMOCLAW_SHIM_DIR/$_CLI_BIN" ]] \
&& is_real_nemoclaw_cli "$NEMOCLAW_SHIM_DIR/$_CLI_BIN" "$_CLI_BIN"; then
_CLI_PATH="$NEMOCLAW_SHIM_DIR/$_CLI_BIN"
record_cli_resolution_state "$_CLI_PATH" "$npm_bin"

# record_cli_resolution_state clears the refresh flag as soon as the shim
# directory sits on the initial PATH, which is the whole story only when the
# name resolves to this shim. A rejected binary earlier on PATH still
# shadows it, so re-check and keep the hint when it does — otherwise
# print_done() reports a clean install for a shell where $_CLI_BIN still
# runs the binary this function just refused.
if [[ "$(command -v "$_CLI_BIN" 2>/dev/null)" -ef "$_CLI_PATH" ]]; then
NEMOCLAW_READY_NOW=true
info "Verified: ${_CLI_BIN} is available at $_CLI_PATH"
return 0
fi

NEMOCLAW_CURRENT_SHELL_NEEDS_PATH_REFRESH=true
NEMOCLAW_RECOVERY_EXPORT_DIR="${NEMOCLAW_RECOVERY_EXPORT_DIR:-$NEMOCLAW_SHIM_DIR}"
NEMOCLAW_RECOVERY_PROFILE="${NEMOCLAW_RECOVERY_PROFILE:-$(detect_shell_profile)}"
warn "Found ${_CLI_BIN} at $_CLI_PATH but this shell's PATH does not yet resolve it."
warn "Running onboarding via the absolute path; refresh your shell PATH afterwards (commands below)."
return 0
Comment thread
coderabbitai[bot] marked this conversation as resolved.
fi

# Single warn header, then plain printf for each bullet. warn() prefixes
# every line with "[warn]" + colour codes, which would render the bulleted
# diagnostic table as six separate warnings rather than one structured block.
Expand Down
122 changes: 122 additions & 0 deletions test/install-npm-resolution.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,69 @@ function isLinuxRoot(): boolean {
);
}

/**
* Builds an install tree where the active npm prefix bin has no CLI but the
* user-local shim is present and working, and the shim directory is absent
* from PATH. Callers that need the no-shim control remove `shimPath`.
*/
function createStaleNpmPrefixTree(): {
tmp: string;
fakeBin: string;
prefix: string;
shimPath: string;
} {
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), "nemoclaw-install-shim-probe-"));
const fakeBin = path.join(tmp, "bin");
const prefix = path.join(tmp, "prefix");
const shimPath = path.join(tmp, ".local", "bin", "nemoclaw");

fs.mkdirSync(fakeBin);
fs.mkdirSync(path.join(prefix, "bin"), { recursive: true });
fs.mkdirSync(path.dirname(shimPath), { recursive: true });

writeExecutable(
path.join(fakeBin, "node"),
`#!/usr/bin/env bash
exit 0
`,
);
writeExecutable(
path.join(fakeBin, "npm"),
`#!/usr/bin/env bash
if [ "$1" = "config" ] && [ "$2" = "get" ] && [ "$3" = "prefix" ]; then
echo "$ACTIVE_NPM_PREFIX"
exit 0
fi
exit 99
`,
);
writeExecutable(
shimPath,
`#!/usr/bin/env bash
echo "nemoclaw v0.1.0"
`,
);

return { tmp, fakeBin, prefix, shimPath };
}

function runVerifyNemoclaw(
tree: ReturnType<typeof createStaleNpmPrefixTree>,
extraEnv: Record<string, string> = {},
) {
return runInstallerFunction(
"_CLI_BIN=nemoclaw; verify_nemoclaw; " +
"printf 'CLI_PATH=%s\\nREFRESH=%s\\nREADY=%s\\n' " +
'"$_CLI_PATH" "$NEMOCLAW_CURRENT_SHELL_NEEDS_PATH_REFRESH" "$NEMOCLAW_READY_NOW"',
tree.fakeBin,
{
ACTIVE_NPM_PREFIX: tree.prefix,
HOME: tree.tmp,
...extraEnv,
},
);
}

describe("installer npm resolution", () => {
it("creates user-local shims for every packaged CLI alias during the default install path", () => {
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), "nemoclaw-install-package-shims-"));
Expand Down Expand Up @@ -334,4 +397,63 @@ exit 98
expect(result.status).toBe(0);
expect(result.stdout.trim()).toBe("WRITABLE");
});

it("verifies through the user-local shim when the active npm prefix has no CLI (#8311)", () => {
const tree = createStaleNpmPrefixTree();

const result = runVerifyNemoclaw(tree);

expect(result.status, `${result.stdout}${result.stderr}`).toBe(0);
expect(normalizeShellPathForAssert(result.stdout)).toContain(
`CLI_PATH=${normalizeShellPathForAssert(tree.shimPath)}`,
);
expect(result.stdout).toContain("REFRESH=true");
expect(result.stdout).toContain("READY=false");
});

it("keeps the PATH-refresh hint when a rejected binary still shadows the shim (#8311)", () => {
const tree = createStaleNpmPrefixTree();
writeExecutable(
path.join(tree.fakeBin, "nemoclaw"),
`#!/usr/bin/env bash
echo "placeholder package"
`,
);

const result = runVerifyNemoclaw(tree, {
PATH: [tree.fakeBin, path.dirname(tree.shimPath), TEST_SYSTEM_PATH].join(path.delimiter),
});

expect(result.status, `${result.stdout}${result.stderr}`).toBe(0);
expect(normalizeShellPathForAssert(result.stdout)).toContain(
`CLI_PATH=${normalizeShellPathForAssert(tree.shimPath)}`,
);
expect(result.stdout).toContain("REFRESH=true");
expect(result.stdout).toContain("READY=false");
});

it("reports the CLI as ready when the shim itself resolves on PATH (#8311)", () => {
const tree = createStaleNpmPrefixTree();

const result = runVerifyNemoclaw(tree, {
PATH: [path.dirname(tree.shimPath), tree.fakeBin, TEST_SYSTEM_PATH].join(path.delimiter),
});

expect(result.status, `${result.stdout}${result.stderr}`).toBe(0);
expect(normalizeShellPathForAssert(result.stdout)).toContain(
`CLI_PATH=${normalizeShellPathForAssert(tree.shimPath)}`,
);
expect(result.stdout).toContain("READY=true");
expect(result.stdout).toContain("REFRESH=false");
});

it("still fails the install when neither the npm prefix nor the shim has a CLI (#8311)", () => {
const tree = createStaleNpmPrefixTree();
fs.rmSync(tree.shimPath);

const result = runVerifyNemoclaw(tree);

expect(result.status).toBe(1);
expect(result.stderr).toContain("nemoclaw binary not found");
});
});
Loading