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
7 changes: 4 additions & 3 deletions docs/reference/commands-nemohermes.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -1352,13 +1352,14 @@ curl -fsSL https://www.nvidia.com/nemoclaw.sh | bash
```

```bash
nemohermes update [--check] [--yes|-y]
nemohermes update [--check] [--fresh] [--yes|-y]
```

| Flag | Description |
|------|-------------|
| `--check` | Show the current version, latest maintained version, install type, and maintained update command without changing anything |
| `--yes`, `-y` | Skip the confirmation prompt and run the maintained installer flow |
| `--check` | Show the current version, latest maintained version, install type, and maintained update command without changing anything. |
| `--fresh` | Reinstall the maintained build even when already up to date (clean re-clone of `~/.nemoclaw/source`); useful to repair a broken-but-current install. Does not reset onboarding state. |
| `--yes`, `-y` | Skip the confirmation prompt and run the maintained installer flow. |

`nemohermes update` updates the host-side NemoClaw installation.
The maintained installer flow follows the admin-promoted `lkg` release tag by default, so it may trail the newest semver or `latest` tag while validation completes.
Expand Down
7 changes: 4 additions & 3 deletions docs/reference/commands.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -1721,13 +1721,14 @@ curl -fsSL https://www.nvidia.com/nemoclaw.sh | bash
```

```bash
$$nemoclaw update [--check] [--yes|-y]
$$nemoclaw update [--check] [--fresh] [--yes|-y]
```

| Flag | Description |
|------|-------------|
| `--check` | Show the current version, latest maintained version, install type, and maintained update command without changing anything |
| `--yes`, `-y` | Skip the confirmation prompt and run the maintained installer flow |
| `--check` | Show the current version, latest maintained version, install type, and maintained update command without changing anything. |
| `--fresh` | Reinstall the maintained build even when already up to date (clean re-clone of `~/.nemoclaw/source`); useful to repair a broken-but-current install. Does not reset onboarding state. |
| `--yes`, `-y` | Skip the confirmation prompt and run the maintained installer flow. |

`$$nemoclaw update` updates the host-side NemoClaw installation.
The maintained installer flow follows the admin-promoted `lkg` release tag by default, so it may trail the newest semver or `latest` tag while validation completes.
Expand Down
8 changes: 7 additions & 1 deletion src/commands/update.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,16 +16,21 @@ export default class UpdateCommand extends NemoClawCommand {
static summary = `Run the maintained ${CLI_DISPLAY_NAME} installer update flow`;
static description =
`Check for a ${CLI_DISPLAY_NAME} CLI update and run the maintained installer flow.`;
static usage = ["update [--check] [--yes|-y]"];
static usage = ["update [--check] [--fresh] [--yes|-y]"];
static examples = [
"<%= config.bin %> update --check",
"<%= config.bin %> update",
"<%= config.bin %> update --fresh",
"<%= config.bin %> update --yes",
];
static flags = {
check: Flags.boolean({
description: "Check update availability without running the installer",
}),
fresh: Flags.boolean({
description:
"Reinstall the maintained build even when already up to date (clean re-clone; useful to repair a broken install)",
}),
yes: yesFlag(),
};

Expand All @@ -34,6 +39,7 @@ export default class UpdateCommand extends NemoClawCommand {
const result = await runUpdateAction(
{
check: flags.check === true,
fresh: flags.fresh === true,
yes: flags.yes === true,
},
{
Expand Down
93 changes: 93 additions & 0 deletions src/lib/actions/update.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -143,6 +143,97 @@ describe("runUpdateAction", () => {
}
});

it("does not run the installer when already up to date without --fresh", async () => {
const spawnSyncImpl = vi.fn();
const log = vi.fn();

const result = await runUpdateAction(
{ yes: true },
{
currentVersion: () => "0.2.0",
getLatestVersion: () => "0.2.0",
isSourceCheckout: () => false,
log,
spawnSyncImpl,
},
);

expect(result.updateAvailable).toBe(false);
expect(result.ranInstaller).toBe(false);
expect(spawnSyncImpl).not.toHaveBeenCalled();
expect(log).toHaveBeenCalledWith(expect.stringContaining("already up to date"));
});

it("reinstalls even when already up to date with --fresh (#5960)", async () => {
const spawnSyncImpl = vi.fn(
() => ({ status: 0, stdout: "", stderr: "", signal: null }) as never,
);
const log = vi.fn();

const result = await runUpdateAction(
{ fresh: true, yes: true },
{
currentVersion: () => "0.2.0",
getLatestVersion: () => "0.2.0",
isSourceCheckout: () => false,
log,
spawnSyncImpl,
},
);

expect(result.updateAvailable).toBe(false);
expect(result.ranInstaller).toBe(true);
expect(spawnSyncImpl).toHaveBeenCalledWith(
"bash",
["-o", "pipefail", "-lc", NEMOCLAW_UPDATE_COMMAND],
expect.objectContaining({ stdio: "inherit" }),
);
expect(log).toHaveBeenCalledWith(expect.stringContaining("reinstalling anyway (--fresh)"));
});

it("does not announce a --fresh reinstall when the user declines the prompt (#5960)", async () => {
const spawnSyncImpl = vi.fn();
const log = vi.fn();
const prompt = vi.fn(async () => "n");

const result = await runUpdateAction(
{ fresh: true },
{
currentVersion: () => "0.2.0",
getLatestVersion: () => "0.2.0",
isSourceCheckout: () => false,
log,
prompt,
spawnSyncImpl,
},
);

expect(result.ranInstaller).toBe(false);
expect(spawnSyncImpl).not.toHaveBeenCalled();
// The reinstall claim must not print before/without confirmation.
expect(log).not.toHaveBeenCalledWith(expect.stringContaining("reinstalling anyway"));
expect(log).toHaveBeenCalledWith(expect.stringContaining("Update cancelled"));
});

it("still refuses --fresh from a developer source checkout (no reinstall)", async () => {
const spawnSyncImpl = vi.fn();

const result = await runUpdateAction(
{ fresh: true, yes: true },
{
currentVersion: () => "0.2.0",
error: vi.fn(),
getLatestVersion: () => "0.2.0",
isSourceCheckout: () => true,
log: vi.fn(),
spawnSyncImpl,
},
);

expect(result.ranInstaller).toBe(false);
expect(spawnSyncImpl).not.toHaveBeenCalled();
});

it("prompts before running the maintained installer", async () => {
const prompt = vi.fn(async () => "yes");
const spawnSyncImpl = vi.fn(
Expand Down Expand Up @@ -232,6 +323,7 @@ describe("runUpdateAction", () => {
...process.env,
BASH_ENV: "/tmp/review-bash-env",
ENV: "/tmp/review-env",
NEMOCLAW_FRESH: "1",
NEMOCLAW_INSTALL_REF: "refs/heads/not-maintained",
NEMOCLAW_INSTALL_TAG: "not-maintained",
},
Expand All @@ -248,6 +340,7 @@ describe("runUpdateAction", () => {
const options = calls[0]?.[2];
expect(options?.env?.BASH_ENV).toBeUndefined();
expect(options?.env?.ENV).toBeUndefined();
expect(options?.env?.NEMOCLAW_FRESH).toBeUndefined();
expect(options?.env?.NEMOCLAW_INSTALL_REF).toBeUndefined();
expect(options?.env?.NEMOCLAW_INSTALL_TAG).toBeUndefined();
});
Expand Down
21 changes: 19 additions & 2 deletions src/lib/actions/update.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
// SPDX-License-Identifier: Apache-2.0

import { spawnSync, type SpawnSyncReturns } from "node:child_process";
import { type SpawnSyncReturns, spawnSync } from "node:child_process";
import fs from "node:fs";
import os from "node:os";
import path from "node:path";
Expand All @@ -24,6 +24,13 @@ type SpawnSyncFn = (

export interface RunUpdateOptions {
check?: boolean;
/**
* Reinstall the maintained build even when already up to date. The installer
* re-clones `~/.nemoclaw/source`, so this repairs a broken-but-current
* install. Does not reset onboarding state (distinct from the installer's
* onboard-scoped `--fresh`/`NEMOCLAW_FRESH`).
*/
fresh?: boolean;
yes?: boolean;
}

Expand Down Expand Up @@ -198,6 +205,7 @@ function updateInstallerEnv(env: NodeJS.ProcessEnv): NodeJS.ProcessEnv {
const next = { ...env };
delete next.BASH_ENV;
delete next.ENV;
delete next.NEMOCLAW_FRESH;
delete next.NEMOCLAW_INSTALL_REF;
delete next.NEMOCLAW_INSTALL_TAG;
return next;
Expand Down Expand Up @@ -256,7 +264,7 @@ export async function runUpdateAction(
};
}

if (available === false) {
if (available === false && !options.fresh) {
log(` ${branding.displayName} is already up to date.`);
return {
currentVersion,
Expand Down Expand Up @@ -312,6 +320,15 @@ export async function runUpdateAction(
}
}

// Only announce the --fresh reinstall once the user has actually confirmed
// (or passed --yes): before this point the run could still be declined, and
// claiming a reinstall was happening would be untrue (CodeRabbit review #5963).
if (available === false && options.fresh) {
log(
` ${branding.displayName} is already up to date; reinstalling anyway (--fresh) for a clean re-clone.`,
);
}

log(` Running maintained ${branding.displayName} installer...`);
const result = (deps.spawnSyncImpl ?? spawnSync)(
"bash",
Expand Down
2 changes: 1 addition & 1 deletion src/lib/cli/public-display-defaults.ts
Original file line number Diff line number Diff line change
Expand Up @@ -543,7 +543,7 @@ const PUBLIC_DISPLAY_LAYOUT: Record<string, readonly PublicDisplayLayout[]> = {
{
group: "Upgrade",
order: 40,
flags: "(--check, --yes|-y)",
flags: "(--check, --fresh, --yes|-y)",
},
],
"upgrade-sandboxes": [
Expand Down
2 changes: 1 addition & 1 deletion test/cli/dispatch-basics.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -130,7 +130,7 @@ describe("CLI dispatch", () => {
expect(r.out).toContain("nemoclaw upgrade-sandboxes");
expect(r.out).toContain("(--check, --auto, --yes|-y)");
expect(r.out).toContain("nemoclaw update");
expect(r.out).toContain("(--check, --yes|-y)");
expect(r.out).toContain("(--check, --fresh, --yes|-y)");
expect(r.out).toContain("nemoclaw gc");
expect(r.out).toContain("(--yes|-y|--force, --dry-run)");
expect(r.out).toContain("nemoclaw onboard");
Expand Down
12 changes: 6 additions & 6 deletions test/update.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,25 +25,25 @@ describe("nemoclaw update command", () => {
const output = execSync(`node "${CLI}" help`, { encoding: "utf-8" });
expect(output).toContain("Upgrade");
expect(output).toMatch(
/nemoclaw update\s+Run the maintained NemoClaw installer update flow\s+\(--check, --yes\|-y\)/,
/nemoclaw update\s+Run the maintained NemoClaw installer update flow\s+\(--check, --fresh, --yes\|-y\)/,
);
});

it("prints oclif help for update-specific flags", () => {
const output = execSync(`node "${CLI}" update --help`, { encoding: "utf-8" });
expect(output).toContain("update [--check] [--yes|-y]");
expect(output).toContain("update [--check] [--fresh] [--yes|-y]");
expect(output).toContain("--check");
expect(output).toContain("--yes");
});

it("renders NemoHermes command names and product copy for the Hermes alias", () => {
const rootHelp = execSync(`node "${HERMES_CLI}" help`, { encoding: "utf-8" });
expect(rootHelp).toMatch(
/nemohermes update\s+Run the maintained NemoHermes installer update flow\s+\(--check, --yes\|-y\)/,
/nemohermes update\s+Run the maintained NemoHermes installer update flow\s+\(--check, --fresh, --yes\|-y\)/,
);

const updateHelp = execSync(`node "${HERMES_CLI}" update --help`, { encoding: "utf-8" });
expect(updateHelp).toContain("$ nemohermes update [--check] [--yes|-y]");
expect(updateHelp).toContain("$ nemohermes update [--check] [--fresh] [--yes|-y]");
expect(updateHelp).toContain("Run the maintained NemoHermes installer update flow");
expect(updateHelp).toContain("Check for a NemoHermes CLI update");
expect(updateHelp).not.toContain("NemoClaw CLI update");
Expand All @@ -52,13 +52,13 @@ describe("nemoclaw update command", () => {
it("renders NemoDeepAgents command names and product copy for the Deep Agents alias", () => {
const rootHelp = execSync(`"${DEEPAGENTS_CLI}" help`, { encoding: "utf-8" });
expect(rootHelp).toMatch(
/nemo-deepagents update\s+Run the maintained NemoDeepAgents installer update flow\s+\(--check, --yes\|-y\)/,
/nemo-deepagents update\s+Run the maintained NemoDeepAgents installer update flow\s+\(--check, --fresh, --yes\|-y\)/,
);

const updateHelp = execSync(`"${DEEPAGENTS_CLI}" update --help`, {
encoding: "utf-8",
});
expect(updateHelp).toContain("$ nemo-deepagents update [--check] [--yes|-y]");
expect(updateHelp).toContain("$ nemo-deepagents update [--check] [--fresh] [--yes|-y]");
expect(updateHelp).toContain("Run the maintained NemoDeepAgents installer update flow");
expect(updateHelp).toContain("Check for a NemoDeepAgents CLI update");
expect(updateHelp).not.toContain("NemoClaw CLI update");
Expand Down
Loading