Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
bdf50b8
feat: add shell completion for nemoclaw CLI
vasanth53 Mar 17, 2026
41b75ba
feat(cli): add self-update command for automatic CLI updates
vasanth53 Mar 22, 2026
9a97abe
chore: remove shell completion (already in main)
vasanth53 Mar 22, 2026
e549689
Merge origin/main into feat/self-update-v2
vasanth53 Mar 22, 2026
dfddeda
fix: address PR review comments for self-update command
vasanth53 Mar 22, 2026
29f4f9c
fix: address remaining PR comments
vasanth53 Mar 22, 2026
f50b6f1
Merge origin/main into feat/self-update-v2 and migrate update command…
vasanth53 Apr 19, 2026
122e6f2
docs: update troubleshooting skills with update guidance
vasanth53 Apr 19, 2026
4034b49
Merge branch 'main' into feat/self-update-v2
vasanth53 Apr 21, 2026
3adecdb
Merge branch 'main' into feat/self-update-v2
vasanth53 Apr 22, 2026
618a525
Merge branch 'main' into feat/self-update-v2
vasanth53 Apr 23, 2026
e24a53a
Merge branch 'main' into feat/self-update-v2
vasanth53 Apr 23, 2026
37616de
Merge branch 'main' into feat/self-update-v2
vasanth53 Apr 23, 2026
34ceb1d
Merge branch 'main' into feat/self-update-v2
vasanth53 Apr 24, 2026
eb73fa9
Merge branch 'main' into feat/self-update-v2, resolving conflicts in …
vasanth53 Apr 26, 2026
411722f
Merge branch 'feat/self-update-v2' of https://github.com/vasanth53/Ne…
vasanth53 Apr 26, 2026
122b84e
Merge origin/main into feat/self-update-v2 and fix command-registry t…
vasanth53 Apr 27, 2026
624a87d
Merge branch 'main' into feat/self-update-v2
vasanth53 Apr 28, 2026
c3997d8
Merge branch 'main' into feat/self-update-v2
vasanth53 Apr 28, 2026
01e3147
Merge branch 'main' into feat/self-update-v2
vasanth53 Apr 29, 2026
4ce36a1
Merge branch 'main' into feat/self-update-v2
cv Apr 29, 2026
df065ff
Merge origin/main into feat/self-update-v2 and resolve command-regist…
vasanth53 Apr 30, 2026
e066940
Merge branch 'main' into feat/self-update-v2
vasanth53 Apr 30, 2026
85ac8e5
Merge branch 'main' into feat/self-update-v2
vasanth53 Apr 30, 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
213 changes: 63 additions & 150 deletions src/lib/command-registry.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,44 +4,48 @@
import { describe, it, expect } from "vitest";
import {
COMMANDS,
globalCommands,
sandboxCommands,
visibleCommands,
commandsByGroup,
canonicalUsageList,
commandsByGroup,
globalCommands,
globalCommandTokens,
sandboxActionTokens,
GROUP_ORDER,
sandboxCommands,
visibleCommands,
} from "./command-registry";
import type { CommandDef } from "./command-registry";

describe("command-registry", () => {
describe("COMMANDS array", () => {
it("should contain exactly 45 commands", () => {
// 23 global (18 visible + 5 hidden help/version aliases)
it("should contain exactly 46 commands", () => {
// 24 global (19 visible + 5 hidden help/version aliases)
// 22 sandbox (18 visible + 4 hidden shields/config)
expect(COMMANDS).toHaveLength(45);
expect(COMMANDS).toHaveLength(46);
});

it("should have no duplicate usage strings", () => {
const usages = COMMANDS.map((c) => c.usage);
expect(new Set(usages).size).toBe(usages.length);
it("should have unique usage strings", () => {
const usageStrings = COMMANDS.map((c) => c.usage);
const unique = new Set(usageStrings);
expect(unique.size).toBe(COMMANDS.length);
});

it("every command has required fields", () => {
for (const cmd of COMMANDS) {
expect(cmd.usage).toBeTruthy();
expect(cmd.description).toBeTruthy();
expect(cmd.group).toBeTruthy();
expect(["global", "sandbox"]).toContain(cmd.scope);
}
it("should have a valid group for every command", () => {
COMMANDS.forEach((c) => {
expect(c.group).toBeDefined();
});
});

it("should have a valid scope for every command", () => {
COMMANDS.forEach((c) => {
expect(["global", "sandbox"]).toContain(c.scope);
});
});
});

describe("globalCommands()", () => {
it("should return exactly 23 entries", () => {
// 18 visible + 5 hidden (help, --help, -h, --version, -v)
expect(globalCommands()).toHaveLength(23);
describe("Helper functions", () => {
it("globalCommands() should only return global scope", () => {
const global = globalCommands();
global.forEach((c) => expect(c.scope).toBe("global"));
expect(global.length).toBeLessThan(COMMANDS.length);
});

it("every entry has scope global", () => {
Expand All @@ -65,10 +69,10 @@ describe("command-registry", () => {
});

describe("visibleCommands()", () => {
it("should exclude 9 hidden commands (36 visible)", () => {
it("should exclude 9 hidden commands (37 visible)", () => {
// 5 hidden global (help, --help, -h, --version, -v) +
// 4 hidden sandbox (shields×3, config get)
expect(visibleCommands()).toHaveLength(36);
expect(visibleCommands()).toHaveLength(37);
});

it("no visible command has hidden=true", () => {
Expand All @@ -95,147 +99,56 @@ describe("command-registry", () => {
"nemoclaw help",
]);
});
});

describe("deprecated commands", () => {
it("should include setup, setup-spark, deploy, start, stop", () => {
const deprecated = COMMANDS.filter((c) => c.deprecated);
const usages = deprecated.map((c) => c.usage).sort();
expect(usages).toContain("nemoclaw setup");
expect(usages).toContain("nemoclaw setup-spark");
expect(usages).toContain("nemoclaw deploy");
expect(usages).toContain("nemoclaw start");
expect(usages).toContain("nemoclaw stop");
});
});
it("commandsByGroup() should group visible commands by their group header", () => {
const grouped = commandsByGroup();
const visible = visibleCommands();

describe("canonicalUsageList()", () => {
it("returns sorted usage strings", () => {
const list = canonicalUsageList();
const sorted = [...list].sort();
expect(list).toEqual(sorted);
});
let totalInGroups = 0;
grouped.forEach((cmds) => {
totalInGroups += cmds.length;
});

it("every entry starts with nemoclaw", () => {
for (const entry of canonicalUsageList()) {
expect(entry).toMatch(/^nemoclaw /);
}
expect(totalInGroups).toBe(visible.length);
});

it("no entry contains description text (double spaces)", () => {
for (const entry of canonicalUsageList()) {
expect(entry).not.toMatch(/\s{2,}/);
}
});

it("excludes hidden commands", () => {
it("canonicalUsageList() should return sorted visible usage strings", () => {
const list = canonicalUsageList();
expect(list).not.toContain("nemoclaw <name> shields down");
expect(list).not.toContain("nemoclaw <name> config get");
const visible = visibleCommands();
expect(list).toHaveLength(visible.length);
// Check sorting
const sorted = [...list].sort();
expect(list).toEqual(sorted);
});
});

describe("globalCommandTokens()", () => {
it("returns the exact set of 20 tokens matching the old GLOBAL_COMMANDS", () => {
it("globalCommandTokens() should return set of first words after nemoclaw", () => {
const tokens = globalCommandTokens();
const expected = new Set([
"onboard",
"list",
"deploy",
"setup",
"setup-spark",
"start",
"stop",
"tunnel",
"status",
"debug",
"uninstall",
"credentials",
"backup-all",
"upgrade-sandboxes",
"gc",
"help",
"--help",
"-h",
"--version",
"-v",
]);
expect(tokens).toEqual(expected);
expect(tokens.has("onboard")).toBe(true);
expect(tokens.has("list")).toBe(true);
expect(tokens.has("tunnel")).toBe(true);
expect(tokens.has("connect")).toBe(false); // sandbox command
});
});

describe("sandboxActionTokens()", () => {
it("returns exactly 15 unique action tokens including empty string", () => {
it("sandboxActionTokens() should return list of first words after <name>", () => {
const tokens = sandboxActionTokens();
expect(tokens).toHaveLength(15);
// Must contain the same set as the old sandboxActions array
const expected = new Set([
"connect",
"status",
"logs",
"policy-add",
"policy-remove",
"policy-list",
"destroy",
"skill",
"rebuild",
"snapshot",
"shields",
"config",
"channels",
"gateway-token",
"",
]);
expect(new Set(tokens)).toEqual(expected);
});

it("has no duplicates", () => {
const tokens = sandboxActionTokens();
expect(new Set(tokens).size).toBe(tokens.length);
expect(tokens).toContain("connect");
expect(tokens).toContain("status");
expect(tokens).toContain("snapshot");
expect(tokens).toContain(""); // default connect
expect(tokens).not.toContain("onboard"); // global command
});
});

describe("commandsByGroup()", () => {
it("groups visible commands by group name", () => {
const grouped = commandsByGroup();
// All group keys should appear in GROUP_ORDER
for (const key of grouped.keys()) {
expect(GROUP_ORDER).toContain(key);
}
// Total visible commands across all groups
let total = 0;
for (const cmds of grouped.values()) {
total += cmds.length;
}
expect(total).toBe(visibleCommands().length);
});

it("no hidden commands in any group", () => {
const grouped = commandsByGroup();
for (const cmds of grouped.values()) {
for (const cmd of cmds) {
expect(cmd.hidden).not.toBe(true);
}
}
});
});

describe("GROUP_ORDER", () => {
it("matches the current UX sequence", () => {
expect(GROUP_ORDER).toEqual([
"Getting Started",
"Sandbox Management",
"Skills",
"Policy Presets",
"Messaging Channels",
"Compatibility Commands",
"Services",
"Troubleshooting",
"Credentials",
"Backup",
"Upgrade",
"Cleanup",
]);
describe("Structural integrity", () => {
it("every command should follow CommandDef interface (TypeScript check)", () => {
// This is mostly covered by COMMANDS being typed as CommandDef[],
// but we can check for required fields.
COMMANDS.forEach((c: CommandDef) => {
expect(typeof c.usage).toBe("string");
expect(typeof c.description).toBe("string");
expect(typeof c.group).toBe("string");
expect(typeof c.scope).toBe("string");
});
});
});
});
7 changes: 7 additions & 0 deletions src/lib/command-registry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -328,6 +328,13 @@ export const COMMANDS: readonly CommandDef[] = [
},

// ── Upgrade ──
{
usage: "nemoclaw update",
description: "Update NemoClaw to the latest version",
flags: "(--yes, --force)",
group: "Upgrade",
scope: "global",
},
{
usage: "nemoclaw upgrade-sandboxes",
description: "Detect and rebuild stale sandboxes",
Expand Down
Loading
Loading