Skip to content
Merged
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
15 changes: 8 additions & 7 deletions docs/manage-sandboxes/lifecycle.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -250,17 +250,18 @@ To remove NemoClaw and all resources created during setup, run the CLI's built-i
$$nemoclaw uninstall
```

| Flag | Effect |
|--------------------|------------------------------------------------------|
| `--yes` | Skip the confirmation prompt. |
| `--keep-openshell` | Leave OpenShell binaries installed. |
| `--delete-models` | Also remove NemoClaw-pulled Ollama models. |
| Flag | Effect |
|-----------------------|---------------------------------------------------------------------------------------|
| `--yes` | Skip the confirmation prompt. |
| `--keep-openshell` | Leave OpenShell binaries installed. |
| `--delete-models` | Also remove NemoClaw-pulled Ollama models. |
| `--destroy-user-data` | Also remove preserved user data (`rebuild-backups/`, `backups/`, `sandboxes.json`). |
Comment thread
coderabbitai[bot] marked this conversation as resolved.

<Note>
The uninstall command preserves `~/.nemoclaw/rebuild-backups/` (host-side snapshots that snapshot and `backup-all` commands write), `~/.nemoclaw/backups/` (workspace backups that `scripts/backup-workspace.sh` writes), and `~/.nemoclaw/sandboxes.json` (the sandbox registry) by default.
Uninstall removes every other entry under `~/.nemoclaw/`.
Interactive runs prompt before they remove the preserved entries; the default answer keeps them.
For non-interactive runs (`--yes`, `NEMOCLAW_NON_INTERACTIVE=1`, or a non-TTY shell), set `NEMOCLAW_UNINSTALL_DESTROY_USER_DATA=1` to acknowledge data loss and remove the preserved entries as well.
For non-interactive runs (`--yes`, `NEMOCLAW_NON_INTERACTIVE=1`, or a non-TTY shell), pass `--destroy-user-data` (or set `NEMOCLAW_UNINSTALL_DESTROY_USER_DATA=1`) to acknowledge data loss and remove the preserved entries as well.
See the [Commands reference](../reference/commands#$$nemoclaw-uninstall) for the full preservation contract.
</Note>

Expand All @@ -272,7 +273,7 @@ If the CLI is missing or broken, fall back to the hosted script:
curl -fsSL https://raw.githubusercontent.com/NVIDIA/NemoClaw/refs/heads/main/uninstall.sh | bash
```

The same `--yes`, `--keep-openshell`, and `--delete-models` flags listed above also apply to the hosted script. Pass them after `bash -s --`.
The same `--yes`, `--keep-openshell`, `--delete-models`, and `--destroy-user-data` flags listed above also apply to the hosted script. Pass them after `bash -s --`.

```bash
curl -fsSL https://raw.githubusercontent.com/NVIDIA/NemoClaw/refs/heads/main/uninstall.sh | bash -s -- --yes --delete-models
Expand Down
4 changes: 3 additions & 1 deletion docs/reference/commands-nemohermes.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -1611,10 +1611,11 @@ On Linux, uninstall removes `~/.local/state/nemoclaw`, which contains Docker-dri
| `--yes` | Skip the confirmation prompt |
| `--keep-openshell` | Leave OpenShell binaries installed |
| `--delete-models` | Also remove NemoClaw-pulled Ollama models |
| `--destroy-user-data` | Also remove preserved user data (`rebuild-backups/`, `backups/`, `sandboxes.json`) |
| `--gateway <name>` | Override the gateway name to remove (default: `nemohermes`) |

```bash
nemohermes uninstall [--yes] [--keep-openshell] [--delete-models] [--gateway <name>]
nemohermes uninstall [--yes] [--keep-openshell] [--delete-models] [--destroy-user-data] [--gateway <name>]
```

##### User-data preservation under `~/.nemoclaw/`
Expand All @@ -1636,6 +1637,7 @@ Decision matrix:
| Interactive TTY, preserved entries present, no env override | Prompts `Also remove them? [y/N]`. Default `N` keeps the entries. |
| Interactive TTY, user answers `y` | Removes everything under `~/.nemoclaw/` (the previous full-removal behaviour). |
| Non-interactive (`--yes`, `NEMOCLAW_NON_INTERACTIVE=1`, or non-TTY shell) | Preserves the entries and prints a one-line notice. |
| Any context with `--destroy-user-data` | Skips the prompt and removes everything under `~/.nemoclaw/`. Takes precedence over the env var. |
| Any context with `NEMOCLAW_UNINSTALL_DESTROY_USER_DATA=1` | Skips the prompt and removes everything under `~/.nemoclaw/`. |

The preserved entries survive uninstall as inert files on disk.
Expand Down
4 changes: 3 additions & 1 deletion docs/reference/commands.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -1929,10 +1929,11 @@ On Linux, uninstall removes `~/.local/state/nemoclaw`, which contains Docker-dri
| `--yes` | Skip the confirmation prompt |
| `--keep-openshell` | Leave OpenShell binaries installed |
| `--delete-models` | Also remove NemoClaw-pulled Ollama models |
| `--destroy-user-data` | Also remove preserved user data (`rebuild-backups/`, `backups/`, `sandboxes.json`) |
| `--gateway <name>` | Override the gateway name to remove (default: `$$nemoclaw`) |

```bash
$$nemoclaw uninstall [--yes] [--keep-openshell] [--delete-models] [--gateway <name>]
$$nemoclaw uninstall [--yes] [--keep-openshell] [--delete-models] [--destroy-user-data] [--gateway <name>]
```

##### User-data preservation under `~/.nemoclaw/`
Expand All @@ -1954,6 +1955,7 @@ Decision matrix:
| Interactive TTY, preserved entries present, no env override | Prompts `Also remove them? [y/N]`. Default `N` keeps the entries. |
| Interactive TTY, user answers `y` | Removes everything under `~/.nemoclaw/` (the previous full-removal behaviour). |
| Non-interactive (`--yes`, `NEMOCLAW_NON_INTERACTIVE=1`, or non-TTY shell) | Preserves the entries and prints a one-line notice. |
| Any context with `--destroy-user-data` | Skips the prompt and removes everything under `~/.nemoclaw/`. Takes precedence over the env var. |
| Any context with `NEMOCLAW_UNINSTALL_DESTROY_USER_DATA=1` | Skips the prompt and removes everything under `~/.nemoclaw/`. |

The preserved entries survive uninstall as inert files on disk.
Expand Down
9 changes: 8 additions & 1 deletion src/commands/internal/uninstall/run-plan.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,14 +12,20 @@ export default class InternalUninstallRunPlanCommand extends NemoClawCommand {
static strict = true;
static summary = `${CLI_DISPLAY_NAME} Uninstaller`;
static description = `Remove host-side ${CLI_DISPLAY_NAME} resources.`;
static usage = ["internal uninstall run-plan [--yes] [--keep-openshell] [--delete-models]"];
static usage = [
"internal uninstall run-plan [--yes] [--keep-openshell] [--delete-models] [--destroy-user-data]",
];
static examples = [`${CLI_NAME} internal uninstall run-plan --yes`];
static flags = {
yes: Flags.boolean({ description: "Skip the confirmation prompt" }),
"keep-openshell": Flags.boolean({ description: "Leave the openshell binary installed" }),
"delete-models": Flags.boolean({
description: `Remove ${CLI_DISPLAY_NAME}-pulled Ollama models`,
}),
"destroy-user-data": Flags.boolean({
description:
"Also remove preserved user data under ~/.nemoclaw/ (rebuild-backups/, backups/, sandboxes.json)",
}),
gateway: Flags.string({ description: "Gateway name", default: "nemoclaw" }),
};

Expand All @@ -28,6 +34,7 @@ export default class InternalUninstallRunPlanCommand extends NemoClawCommand {
const result = runUninstallPlan({
assumeYes: flags.yes ?? false,
deleteModels: flags["delete-models"] ?? false,
destroyUserData: flags["destroy-user-data"] ?? false,
gatewayName: flags.gateway,
keepOpenShell: flags["keep-openshell"] ?? false,
});
Expand Down
133 changes: 133 additions & 0 deletions src/lib/actions/uninstall/run-plan.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -977,6 +977,139 @@ describe("uninstall run plan", () => {
}
});

it("purges the whole state dir when destroyUserData is set, even with --yes on a non-TTY", () => {
const { tmpHome, stateDir } = setupStateDir();
try {
const logs: string[] = [];
const result = runUninstallPlan(
{ assumeYes: true, deleteModels: false, destroyUserData: true, keepOpenShell: true },
{
commandExists: () => false,
env: {
HOME: tmpHome,
NEMOCLAW_UNINSTALL_DESTROY_USER_DATA: "",
} as NodeJS.ProcessEnv,
existsSync: tempScopedExistsSync(tmpHome),
isTty: false,
log: (line) => logs.push(line),
run: vi.fn(() => ok()),
runDocker: () => ok(""),
},
);

expect(result.exitCode).toBe(0);
expect(fs.existsSync(stateDir)).toBe(false);
expect(logs).toContain(`Removed ${stateDir}`);
expect(logs).toContain("--destroy-user-data set; purging user data under ~/.nemoclaw/.");
expect(logs.every((line) => !line.includes("preserved:"))).toBe(true);
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated
} finally {
fs.rmSync(tmpHome, { recursive: true, force: true });
}
});

it("destroyUserData purges on a TTY without prompting", () => {
const { tmpHome, stateDir } = setupStateDir();
const readLine = vi.fn(() => "y");
try {
const logs: string[] = [];
const result = runUninstallPlan(
{ assumeYes: true, deleteModels: false, destroyUserData: true, keepOpenShell: true },
{
commandExists: () => false,
env: {
HOME: tmpHome,
NEMOCLAW_UNINSTALL_DESTROY_USER_DATA: "",
} as NodeJS.ProcessEnv,
existsSync: tempScopedExistsSync(tmpHome),
isTty: true,
log: (line) => logs.push(line),
readLine,
run: vi.fn(() => ok()),
runDocker: () => ok(""),
},
);

expect(result.exitCode).toBe(0);
expect(fs.existsSync(stateDir)).toBe(false);
expect(logs).toContain("--destroy-user-data set; purging user data under ~/.nemoclaw/.");
expect(logs.every((line) => line !== "Also remove them? [y/N]")).toBe(true);
expect(readLine).not.toHaveBeenCalled();
} finally {
fs.rmSync(tmpHome, { recursive: true, force: true });
}
});

it("destroyUserData takes precedence over NEMOCLAW_UNINSTALL_DESTROY_USER_DATA env var", () => {
const { tmpHome, stateDir } = setupStateDir();
try {
const logs: string[] = [];
const result = runUninstallPlan(
{ assumeYes: true, deleteModels: false, destroyUserData: true, keepOpenShell: true },
{
commandExists: () => false,
env: {
HOME: tmpHome,
NEMOCLAW_UNINSTALL_DESTROY_USER_DATA: "1",
} as NodeJS.ProcessEnv,
existsSync: tempScopedExistsSync(tmpHome),
isTty: false,
log: (line) => logs.push(line),
run: vi.fn(() => ok()),
runDocker: () => ok(""),
},
);

expect(result.exitCode).toBe(0);
expect(fs.existsSync(stateDir)).toBe(false);
expect(logs).toContain("--destroy-user-data set; purging user data under ~/.nemoclaw/.");
expect(
logs.every(
(line) =>
line !==
"NEMOCLAW_UNINSTALL_DESTROY_USER_DATA=1 set; purging user data under ~/.nemoclaw/.",
),
).toBe(true);
} finally {
fs.rmSync(tmpHome, { recursive: true, force: true });
}
});

it("non-interactive hint mentions --destroy-user-data alongside the env var", () => {
const { tmpHome, stateDir } = setupStateDir();
try {
const logs: string[] = [];
const result = runUninstallPlan(
{ assumeYes: true, deleteModels: false, keepOpenShell: true },
{
commandExists: () => false,
env: {
HOME: tmpHome,
NEMOCLAW_UNINSTALL_DESTROY_USER_DATA: "",
} as NodeJS.ProcessEnv,
existsSync: tempScopedExistsSync(tmpHome),
isTty: false,
log: (line) => logs.push(line),
run: vi.fn(() => ok()),
runDocker: () => ok(""),
},
);

expect(result.exitCode).toBe(0);
expect(
fs.existsSync(path.join(stateDir, "rebuild-backups", "sb1", "20260101", "manifest.json")),
).toBe(true);
expect(
logs.some(
(line) =>
line.includes("--destroy-user-data") &&
line.includes("NEMOCLAW_UNINSTALL_DESTROY_USER_DATA=1"),
),
).toBe(true);
} finally {
fs.rmSync(tmpHome, { recursive: true, force: true });
}
});

it("purges via interactive y/N prompt when user answers yes", () => {
const { tmpHome, stateDir } = setupStateDir();
try {
Expand Down
12 changes: 10 additions & 2 deletions src/lib/actions/uninstall/run-plan.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ export interface RunResult {
export interface UninstallRunOptions {
assumeYes: boolean;
deleteModels: boolean;
destroyUserData?: boolean;
gatewayName?: string;
keepOpenShell: boolean;
}
Expand Down Expand Up @@ -797,6 +798,11 @@ function resolvePreserveSet(
options: UninstallRunOptions,
runtime: UninstallRuntime,
): readonly string[] {
// Explicit acknowledgement flag → full purge.
if (options.destroyUserData) {
runtime.log("--destroy-user-data set; purging user data under ~/.nemoclaw/.");
return [];
}
// Explicit acknowledgement env var → full purge, matches today's behaviour.
if (runtime.env.NEMOCLAW_UNINSTALL_DESTROY_USER_DATA === "1") {
runtime.log(
Expand All @@ -810,12 +816,14 @@ function resolvePreserveSet(
if (preservable.length === 0) return PRESERVED_USER_DATA_ENTRIES;
// Non-interactive (no TTY, --yes, or NEMOCLAW_NON_INTERACTIVE=1) → preserve
// silently with a one-line notice. Default behaviour is safe; users who want
// a destructive uninstall in CI must set the env var.
// a destructive uninstall in CI must pass --destroy-user-data or set the env var.
const nonInteractive =
!runtime.isTty || options.assumeYes || runtime.env.NEMOCLAW_NON_INTERACTIVE === "1";
if (nonInteractive) {
runtime.log(`Preserving ${preservable.join(", ")} under ${paths.nemoclawStateDir}.`);
runtime.log(" Set NEMOCLAW_UNINSTALL_DESTROY_USER_DATA=1 to purge user data on uninstall.");
runtime.log(
" Pass --destroy-user-data (or set NEMOCLAW_UNINSTALL_DESTROY_USER_DATA=1) to purge user data on uninstall.",
);
return PRESERVED_USER_DATA_ENTRIES;
}
runtime.log(`The following user data under ${paths.nemoclawStateDir} is preserved by default:`);
Expand Down
2 changes: 1 addition & 1 deletion uninstall.sh
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
#
# Compatibility wrapper for the TypeScript NemoClaw uninstaller.
#
# Usage: ./uninstall.sh [--yes] [--keep-openshell] [--delete-models]
# Usage: ./uninstall.sh [--yes] [--keep-openshell] [--delete-models] [--destroy-user-data]

set -euo pipefail

Expand Down
Loading