Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
79 commits
Select commit Hold shift + click to select a range
5ac7144
refactor(cli): tighten oclif routing and help metadata
cv May 15, 2026
e98d1ea
refactor(cli): extract public dispatch adapter
cv May 15, 2026
df2e5b4
refactor(cli): derive parent usage from metadata
cv May 15, 2026
a0a7081
refactor(cli): share common oclif flag builders
cv May 15, 2026
20972c8
refactor(cli): render parent usage through help adapter
cv May 16, 2026
9f61aac
refactor(cli): return config failures to oclif
cv May 16, 2026
8c63541
test(cli): guard public help examples against native grammar
cv May 16, 2026
d87bee5
refactor(cli): let oclif require credential provider args
cv May 16, 2026
4dcc675
refactor(cli): derive global parent usage from metadata
cv May 16, 2026
f2142f0
refactor(cli): lazy-load sandbox dispatch dependencies
cv May 16, 2026
8ff41dc
refactor(cli): colocate service display metadata
cv May 16, 2026
74334d2
refactor(cli): use oclif json handling for inference get
cv May 16, 2026
dd6ae5b
refactor(cli): validate inference set values in parser
cv May 16, 2026
566b81a
refactor(cli): return share failures to oclif
cv May 16, 2026
919319a
test(cli): guard native oclif taxonomy settings
cv May 16, 2026
02a56fd
refactor(cli): colocate global display metadata
cv May 16, 2026
98a0f7e
refactor(cli): use oclif json handling for doctor
cv May 16, 2026
2fb62b0
refactor(cli): return host alias failures to oclif
cv May 16, 2026
e15947a
refactor(cli): colocate sandbox display metadata
cv May 16, 2026
4d2835e
Merge branch 'main' into refactor/oclif-router-cleanup
cv May 16, 2026
80c9040
test(cli): guard status json oclif handling
cv May 16, 2026
2bc81af
refactor(cli): return gateway token failures to oclif
cv May 16, 2026
160f23f
refactor(cli): reuse common quiet and json flags
cv May 16, 2026
b58b780
refactor(cli): derive version alias from metadata
cv May 16, 2026
a58e5d4
refactor(cli): return snapshot failures to oclif
cv May 16, 2026
c3727b7
refactor(cli): use oclif deprecation metadata for service aliases
cv May 16, 2026
ce79962
test(cli): guard display metadata colocation
cv May 16, 2026
e4208a7
fix(cli): address oclif review comments
cv May 16, 2026
8e2bf37
Merge remote-tracking branch 'origin/main' into refactor/oclif-router…
cv May 16, 2026
78bca5d
Merge remote-tracking branch 'origin' into refactor/oclif-router-cleanup
cv May 16, 2026
e6dd2d4
refactor(cli): move oclif commands to discovery tree
cv May 16, 2026
e21f6b9
Merge branch 'main' into refactor/oclif-router-cleanup
cv May 17, 2026
75609f6
refactor(cli): inline sandbox recover command
cv May 17, 2026
bc174a3
test(cli): cover oclif public display metadata
cv May 17, 2026
43b6e9c
refactor(cli): rename public display metadata
cv May 17, 2026
c05a73c
refactor(cli): load oclif metadata from manifest
cv May 17, 2026
a34e052
fix(cli): preserve uninstall exit semantics
cv May 17, 2026
a4795a5
fix(cli): align public oclif metadata
cv May 17, 2026
dad3684
Merge branch 'main' into refactor/oclif-router-cleanup
cv May 17, 2026
3c5e912
Merge branch 'main' into refactor/oclif-router-cleanup
cv May 17, 2026
b6e04f2
Merge branch 'main' into refactor/oclif-router-cleanup
cv May 17, 2026
0d0b033
refactor(cli): move command support helpers
cv May 17, 2026
55babc2
test(cli): tolerate group-writable build dirs
cv May 18, 2026
0ea7aaa
Merge branch 'main' into refactor/oclif-router-cleanup
cv May 18, 2026
f26d5df
refactor(cli): make public routing metadata explicit
cv May 18, 2026
959b741
refactor(cli): expose native argv translation
cv May 18, 2026
dbe8a15
refactor(cli): separate sandbox recovery preflight
cv May 18, 2026
81f1a39
refactor(cli): derive parent dispatch from oclif metadata
cv May 18, 2026
535f704
refactor(cli): isolate public help rendering
cv May 18, 2026
0217bcc
test(cli): guard public display routing separation
cv May 18, 2026
3ccde74
test(cli): bound explicit compatibility route aliases
cv May 18, 2026
35a9044
refactor(cli): derive public dispatch from oclif metadata
cv May 18, 2026
c79e092
refactor(cli): execute public routes through native oclif argv
cv May 18, 2026
2a11eac
refactor(cli): defer native argv errors to oclif
cv May 18, 2026
e62c417
Merge branch 'main' into refactor/oclif-router-cleanup
cv May 18, 2026
49aecd9
refactor(cli): return native argv from public translators
cv May 18, 2026
b0b4c42
refactor(cli): execute nested global routes through native argv
cv May 18, 2026
8fd7d73
test(cli): document direct global dispatch fallback
cv May 18, 2026
2ed19ae
refactor(cli): rename public dispatch helpers as argv translators
cv May 18, 2026
f22deaa
refactor(cli): mark direct command execution as compatibility fallback
cv May 18, 2026
482fb14
test(cli): document public help compatibility cases
cv May 18, 2026
6454710
refactor(cli): isolate public connect compatibility handling
cv May 18, 2026
65d07ec
refactor(cli): rename public argv translation module
cv May 18, 2026
02f1e9f
refactor(cli): rename public translation result types
cv May 18, 2026
02e9878
refactor(cli): bound direct command compatibility fallback
cv May 18, 2026
3023466
test(cli): guard public translation boundaries
cv May 18, 2026
3a822b2
refactor(cli): remove deprecated display metadata helpers
cv May 18, 2026
6a02bdb
refactor(cli): accept native oclif help for public routes
cv May 18, 2026
fdff9e2
refactor(cli): centralize public display metadata
cv May 18, 2026
09e22a4
Merge branch 'main' into refactor/oclif-router-cleanup
cv May 19, 2026
5e8e708
refactor(cli): derive public display usage from route metadata
cv May 19, 2026
dc24f49
Merge branch 'main' into refactor/oclif-router-cleanup
cv May 19, 2026
fd82259
Merge remote-tracking branch 'origin/main' into refactor/oclif-router…
cv May 19, 2026
70a90f4
Merge remote-tracking branch 'origin/refactor/oclif-router-cleanup' i…
cv May 19, 2026
1c30222
Merge branch 'main' into refactor/oclif-router-cleanup
cv May 19, 2026
14697b7
Merge branch 'main' into refactor/oclif-router-cleanup
cv May 19, 2026
40dd215
Merge branch 'main' into refactor/oclif-router-cleanup
cv May 19, 2026
8682bd7
Merge branch 'main' into refactor/oclif-router-cleanup
cv May 19, 2026
9a0ac23
Merge branch 'main' into refactor/oclif-router-cleanup
ericksoa May 19, 2026
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
435 changes: 435 additions & 0 deletions .coderabbit.yaml

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@
"format:ts": "cd nemoclaw && npm run lint:fix && npm run format",
"check:installer-hash": "bash scripts/check-installer-hash.sh",
"typecheck": "tsc -p jsconfig.json",
"build:cli": "tsc -p tsconfig.src.json && if find nemoclaw-blueprint/scripts -name '*.ts' -print -quit | grep -q .; then tsc -p nemoclaw-blueprint/tsconfig.json; fi",
"build:cli": "tsc -p tsconfig.src.json && node dist/lib/cli/generate-oclif-metadata-manifest.js && if find nemoclaw-blueprint/scripts -name '*.ts' -print -quit | grep -q .; then tsc -p nemoclaw-blueprint/tsconfig.json; fi",
"typecheck:cli": "tsc -p tsconfig.cli.json",
"validate:configs": "tsx scripts/validate-configs.ts",
"type-safety:hotspots": "tsx scripts/type-safety-hotspots.ts",
Expand Down
15 changes: 5 additions & 10 deletions scripts/checks/layer-import-boundaries.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ type ImportRef = {
};

const REPO_ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../..");
const SRC_LIB_ROOT = path.join(REPO_ROOT, "src", "lib");
const SRC_ROOT = path.join(REPO_ROOT, "src");
const SKIP_DIRS = new Set([".git", "coverage", "dist", "node_modules"]);

function toRepoPath(absPath: string): string {
Expand Down Expand Up @@ -119,18 +119,14 @@ function isAdapterFile(repoPath: string): boolean {
}

function isCommandFile(repoPath: string): boolean {
return repoPath.startsWith("src/lib/commands/");
return repoPath.startsWith("src/commands/");
}

function isActionFile(repoPath: string): boolean {
if (repoPath.startsWith("src/lib/actions/")) return true;
return /(^|\/)[^/]+-actions?\.ts$/.test(repoPath);
}

function isGeneratedOrRegistryCommandFile(repoPath: string): boolean {
return repoPath.endsWith("/common.ts");
}

function importTargetsForbiddenLayer(
fromAbsPath: string,
ref: ImportRef,
Expand Down Expand Up @@ -181,7 +177,7 @@ function checkDomainFile(absPath: string, repoPath: string, violations: Violatio
const target = importTargetsForbiddenLayer(
absPath,
ref,
["src/lib/adapters/", "src/lib/commands/", "src/lib/cli/"],
["src/lib/adapters/", "src/commands/", "src/lib/cli/"],
true,
);
if (target) {
Expand Down Expand Up @@ -236,7 +232,7 @@ function checkActionFile(absPath: string, repoPath: string, violations: Violatio

function checkAdapterFile(absPath: string, repoPath: string, violations: Violation[]): void {
for (const ref of collectImportRefs(absPath)) {
const target = importTargetsForbiddenLayer(absPath, ref, ["src/lib/commands/"], true);
const target = importTargetsForbiddenLayer(absPath, ref, ["src/commands/"], true);
if (target) {
addViolation(
violations,
Expand All @@ -251,7 +247,6 @@ function checkAdapterFile(absPath: string, repoPath: string, violations: Violati
}

function checkCommandFile(absPath: string, repoPath: string, violations: Violation[]): void {
if (isGeneratedOrRegistryCommandFile(repoPath)) return;
const sourceFile = sourceFileFor(absPath);
let commandClassCount = 0;

Expand Down Expand Up @@ -286,7 +281,7 @@ function checkCommandFile(absPath: string, repoPath: string, violations: Violati
}
}

export function findLayerImportBoundaryViolations(root = SRC_LIB_ROOT): Violation[] {
export function findLayerImportBoundaryViolations(root = SRC_ROOT): Violation[] {
const violations: Violation[] = [];
for (const absPath of walk(root)) {
const repoPath = toRepoPath(absPath);
Expand Down
16 changes: 9 additions & 7 deletions src/commands/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,18 +4,20 @@
# `src/commands`

This tree is the oclif discovery surface for the packaged `nemoclaw` CLI.
Each file is intentionally thin: it exports a command class from `src/lib/commands/**`
and attaches NemoClaw's public display metadata.
Command entrypoint files define the oclif command class directly. Do not add new
public command shims that only re-export from `src/lib/**`, and do not recreate a
parallel command layer under `src/lib`. Prefer `src/lib/<feature>/**` for shared
parser helpers.

```text
src/commands/<public command path>.ts
-> import command implementation from src/lib/commands/**
-> wrap with src/lib/cli/command-display.ts metadata
-> parse flags/args
-> call src/lib/actions/** or small src/lib/<feature> command-support helpers
```

Keep behavior out of this tree. Product behavior belongs in `src/lib/actions/**`, pure
planning and classification belongs in `src/lib/domain/**`, and host/runtime boundaries
belong in `src/lib/adapters/**`.
Keep product behavior out of this tree. Command classes should stay thin: product
behavior belongs in `src/lib/actions/**`, pure planning and classification belongs in
`src/lib/domain/**`, and host/runtime boundaries belong in `src/lib/adapters/**`.

Hidden `nemoclaw internal ...` entrypoints live under `src/commands/internal/**`; see
`src/commands/internal/README.md` for their narrower compatibility contract.
27 changes: 16 additions & 11 deletions src/commands/backup-all.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,20 @@
// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
// SPDX-License-Identifier: Apache-2.0

import Command from "../lib/commands/maintenance/backup-all";
import { withCommandDisplay } from "../lib/cli/command-display";
import { runBackupAllAction } from "../lib/actions/global";
import { NemoClawCommand } from "../lib/cli/nemoclaw-oclif-command";

export default withCommandDisplay(Command, [
{
usage: "nemoclaw backup-all",
description: "Back up all sandbox state before upgrade",
group: "Backup",
scope: "global",
order: 40,
},
]);
export default class BackupAllCommand extends NemoClawCommand {
static id = "backup-all";
static strict = true;
static summary = "Back up all sandbox state before upgrade";
static description = "Back up registered, running sandbox state before upgrading.";
static usage = ["backup-all"];
static examples = ["<%= config.bin %> backup-all"];
static flags = {};

public async run(): Promise<void> {
await this.parse(BackupAllCommand);
runBackupAllAction();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,8 @@ const mocks = vi.hoisted(() => ({
runOpenshellProviderCommand: vi.fn(),
}));

vi.mock("../credentials/store", () => ({ prompt: mocks.prompt }));
vi.mock("../actions/global", () => ({
vi.mock("../lib/credentials/store", () => ({ prompt: mocks.prompt }));
vi.mock("../lib/actions/global", () => ({
recoverNamedGatewayRuntime: mocks.recoverNamedGatewayRuntime,
runOpenshellProviderCommand: mocks.runOpenshellProviderCommand,
}));
Expand Down
24 changes: 23 additions & 1 deletion src/commands/credentials.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,26 @@
// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
// SPDX-License-Identifier: Apache-2.0

export { default } from "../lib/commands/credentials";
import { NemoClawCommand } from "../lib/cli/nemoclaw-oclif-command";

import { printCredentialsUsage } from "../lib/credentials/command-support";

export default class CredentialsCommand extends NemoClawCommand {
static id = "credentials";
static strict = true;
static summary = "Manage provider credentials";
static description =
"List or reset provider credentials registered with the OpenShell gateway.";
static usage = ["credentials <list|reset>"];
static examples = [
"<%= config.bin %> credentials list",
"<%= config.bin %> credentials reset nvidia-prod --yes",
];
static flags = {
};

public async run(): Promise<void> {
await this.parse(CredentialsCommand);
printCredentialsUsage(this.log.bind(this));
}
}
68 changes: 56 additions & 12 deletions src/commands/credentials/list.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,59 @@
// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
// SPDX-License-Identifier: Apache-2.0

import Command from "../../lib/commands/credentials/list";
import { withCommandDisplay } from "../../lib/cli/command-display";

export default withCommandDisplay(Command, [
{
usage: "nemoclaw credentials list",
description: "List stored credential keys",
group: "Credentials",
scope: "global",
order: 38,
},
]);
import { CLI_NAME } from "../../lib/cli/branding";
import { NemoClawCommand } from "../../lib/cli/nemoclaw-oclif-command";

import { runOpenshellProviderCommand } from "../../lib/actions/global";
import { OPENSHELL_OPERATION_TIMEOUT_MS } from "../../lib/adapters/openshell/timeouts";
import { isBridgeProviderName, recoverGatewayOrExit } from "../../lib/credentials/command-support";

export default class CredentialsListCommand extends NemoClawCommand {
static id = "credentials:list";
static strict = true;
static summary = "List stored credential providers";
static description = "List provider credentials registered with the OpenShell gateway.";
static usage = ["credentials list"];
static examples = ["<%= config.bin %> credentials list"];
static flags = {
};

public async run(): Promise<void> {
await this.parse(CredentialsListCommand);
if (!(await recoverGatewayOrExit("query", (lines) => this.failWithLines(lines)))) return;

const result = runOpenshellProviderCommand(["provider", "list", "--names"], {
ignoreError: true,
stdio: ["ignore", "pipe", "pipe"],
timeout: OPENSHELL_OPERATION_TIMEOUT_MS,
});
if (result.status !== 0) {
this.failWithLines([
" Could not query OpenShell gateway. Is it running?",
` Run 'openshell gateway start --name nemoclaw' or '${CLI_NAME} onboard' first.`,
]);
return;
}

const allNames = String(result.stdout || "")
.split("\n")
.map((name) => name.trim())
.filter((name) => name.length > 0);
const credentialNames = allNames.filter((name) => !isBridgeProviderName(name)).sort();
const bridgeNames = allNames.filter((name) => isBridgeProviderName(name));

if (credentialNames.length === 0) {
this.log(" No provider credentials registered.");
} else {
this.log(" Providers registered with the OpenShell gateway:");
for (const name of credentialNames) {
this.log(` ${name}`);
}
}
if (bridgeNames.length > 0) {
this.log("");
this.log(` ${String(bridgeNames.length)} per-sandbox messaging bridge(s) are also registered.`);
this.log(` Manage those with \`${CLI_NAME} <sandbox> channels list/remove/stop\` — not this command.`);
}
}
}
95 changes: 83 additions & 12 deletions src/commands/credentials/reset.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,86 @@
// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
// SPDX-License-Identifier: Apache-2.0

import Command from "../../lib/commands/credentials/reset";
import { withCommandDisplay } from "../../lib/cli/command-display";

export default withCommandDisplay(Command, [
{
usage: "nemoclaw credentials reset",
description: "Remove a stored credential so onboard re-prompts",
group: "Credentials",
scope: "global",
order: 39,
},
]);
import { Args } from "@oclif/core";
import { CLI_NAME } from "../../lib/cli/branding";
import { yesFlag } from "../../lib/cli/common-flags";
import { NemoClawCommand } from "../../lib/cli/nemoclaw-oclif-command";

import { runOpenshellProviderCommand } from "../../lib/actions/global";
import { OPENSHELL_OPERATION_TIMEOUT_MS } from "../../lib/adapters/openshell/timeouts";
import { isBridgeProviderName, recoverGatewayOrExit } from "../../lib/credentials/command-support";
import { prompt as askPrompt } from "../../lib/credentials/store";

export default class CredentialsResetCommand extends NemoClawCommand {
static id = "credentials:reset";
static strict = true;
static summary = "Remove a provider credential";
static description = "Remove a provider credential so onboard re-prompts for it.";
static usage = ["credentials reset <PROVIDER> [--yes]"];
static examples = [
"<%= config.bin %> credentials reset nvidia-prod",
"<%= config.bin %> credentials reset nvidia-prod --yes",
];
static args = {
provider: Args.string({
name: "PROVIDER",
description: "OpenShell provider name",
required: true,
}),
};
static flags = {
yes: yesFlag(),
};

public async run(): Promise<void> {
const { args, flags } = await this.parse(CredentialsResetCommand);
const key = args.provider;

if (isBridgeProviderName(key)) {
this.failWithLines([
` '${key}' is a per-sandbox messaging bridge, not a credential.`,
` Use \`${CLI_NAME} <sandbox> channels remove <telegram|discord|slack>\` to retire`,
" the integration (it tears down the bridge provider and rebuilds the sandbox),",
` or \`${CLI_NAME} <sandbox> channels stop <…>\` to pause it without clearing tokens.`,
]);
return;
}

if (!flags.yes) {
const answer = (await askPrompt(` Remove provider '${key}' from the OpenShell gateway? [y/N]: `))
.trim()
.toLowerCase();
if (answer !== "y" && answer !== "yes") {
this.log(" Cancelled.");
return;
}
}

if (!(await recoverGatewayOrExit("reach", (lines) => this.failWithLines(lines)))) return;

const result = runOpenshellProviderCommand(["provider", "delete", key], {
ignoreError: true,
stdio: ["ignore", "pipe", "pipe"],
timeout: OPENSHELL_OPERATION_TIMEOUT_MS,
});
if (result.status === 0) {
this.log(` Removed provider '${key}' from the OpenShell gateway.`);
this.log(` Re-run '${CLI_NAME} onboard' to enter a new value.`);
return;
}

const lines = [` Could not remove provider '${key}'.`];
if (/^[A-Z][A-Z0-9_]+$/.test(key)) {
lines.push(
"",
` '${key}' looks like a credential env variable name.`,
" As of this release, 'credentials reset' takes an OpenShell",
` provider name. Run '${CLI_NAME} credentials list' to see the`,
" registered providers, then retry with one of those names.",
);
}
const stderr = String(result.stderr || "").trim();
if (stderr) lines.push(` ${stderr}`);
this.failWithLines(lines);
}
}
Loading
Loading