diff --git a/docs/reference/commands-nemohermes.mdx b/docs/reference/commands-nemohermes.mdx index 4c45ce04371..6a062b67636 100644 --- a/docs/reference/commands-nemohermes.mdx +++ b/docs/reference/commands-nemohermes.mdx @@ -1212,6 +1212,15 @@ nemohermes tunnel stop `nemohermes stop` remains as a deprecated alias that prints a warning and delegates to `tunnel stop`. +### `nemohermes tunnel status` + +Show the current cloudflared public-URL tunnel status for the default sandbox dashboard. +The output reports whether cloudflared is running, stopped, or stale, and includes the same recovery hint used by `nemohermes status`. + +```console +nemohermes tunnel status +``` + ### `nemohermes start` diff --git a/docs/reference/commands.mdx b/docs/reference/commands.mdx index fc59382b4fb..99333ceedfa 100644 --- a/docs/reference/commands.mdx +++ b/docs/reference/commands.mdx @@ -1398,6 +1398,15 @@ $$nemoclaw tunnel stop `$$nemoclaw stop` remains as a deprecated alias that prints a warning and delegates to `tunnel stop`. +### `$$nemoclaw tunnel status` + +Show the current cloudflared public-URL tunnel status for the default sandbox dashboard. +The output reports whether cloudflared is running, stopped, or stale, and includes the same recovery hint used by `$$nemoclaw status`. + +```console +$$nemoclaw tunnel status +``` + ### `$$nemoclaw start` diff --git a/src/commands/tunnel.ts b/src/commands/tunnel.ts index 993cd08afde..42d7ae1b9b8 100644 --- a/src/commands/tunnel.ts +++ b/src/commands/tunnel.ts @@ -2,17 +2,19 @@ // SPDX-License-Identifier: Apache-2.0 import { NemoClawCommand } from "../lib/cli/nemoclaw-oclif-command"; - import { printTunnelUsage } from "../lib/tunnel/command-support"; export default class TunnelCommand extends NemoClawCommand { static id = "tunnel"; static strict = true; static summary = "Manage the cloudflared public-URL tunnel"; - static description = - "Start or stop the cloudflared public-URL tunnel for the default sandbox dashboard."; - static usage = ["tunnel "]; - static examples = ["<%= config.bin %> tunnel start", "<%= config.bin %> tunnel stop"]; + static description = "Manage the cloudflared public-URL tunnel for the default sandbox dashboard."; + static usage = ["tunnel "]; + static examples = [ + "<%= config.bin %> tunnel start", + "<%= config.bin %> tunnel stop", + "<%= config.bin %> tunnel status", + ]; static flags = {}; public async run(): Promise { diff --git a/src/commands/tunnel/status.ts b/src/commands/tunnel/status.ts new file mode 100644 index 00000000000..9253c3d4b6d --- /dev/null +++ b/src/commands/tunnel/status.ts @@ -0,0 +1,21 @@ +// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +import { NemoClawCommand } from "../../lib/cli/nemoclaw-oclif-command"; + +import { showStatus } from "../../lib/tunnel/services"; + +export default class TunnelStatusCommand extends NemoClawCommand { + static id = "tunnel:status"; + static strict = true; + static summary = "Show cloudflared public-URL tunnel status"; + static description = "Show the cloudflared public-URL tunnel status for the default sandbox dashboard."; + static usage = ["tunnel status"]; + static examples = ["<%= config.bin %> tunnel status"]; + static flags = {}; + + public async run(): Promise { + await this.parse(TunnelStatusCommand); + showStatus(); + } +} diff --git a/src/lib/cli/command-registry.test.ts b/src/lib/cli/command-registry.test.ts index c72c4e40174..92d9764ab1e 100644 --- a/src/lib/cli/command-registry.test.ts +++ b/src/lib/cli/command-registry.test.ts @@ -17,12 +17,10 @@ import { getRegisteredOclifCommandsMetadata } from "./oclif-metadata"; describe("command-registry", () => { describe("COMMANDS array", () => { - it("should contain exactly 70 commands", () => { - // 28 global (22 visible + 6 hidden help/version aliases) - // 42 sandbox (36 visible + 6 hidden shields/config), including the - // sandbox:sessions group (root + list + reset + delete) and the - // sandbox:agents passthrough pair (add + delete). - expect(COMMANDS).toHaveLength(70); + it("partitions commands into global and sandbox scopes", () => { + const partitioned = [...globalCommands(), ...sandboxCommands()]; + expect(partitioned).toHaveLength(COMMANDS.length); + expect(new Set(partitioned).size).toBe(COMMANDS.length); }); it("should have no duplicate usage strings", () => { @@ -41,9 +39,12 @@ describe("command-registry", () => { }); describe("globalCommands()", () => { - it("should return exactly 28 entries", () => { - // 22 visible + 6 hidden (help, --help, -h, version, --version, -v) - expect(globalCommands()).toHaveLength(28); + it("includes public global service commands", () => { + const usages = globalCommands().map((cmd) => cmd.usage); + expect(usages).toContain("nemoclaw tunnel start"); + expect(usages).toContain("nemoclaw tunnel stop"); + expect(usages).toContain("nemoclaw tunnel status"); + expect(usages).toContain("nemoclaw status"); }); it("every entry has scope global", () => { @@ -69,11 +70,8 @@ describe("command-registry", () => { }); describe("visibleCommands()", () => { - it("should exclude 12 hidden commands (58 visible)", () => { - // 6 hidden global (help, --help, -h, version, --version, -v) + - // 6 hidden sandbox (shields×3, config get/set/rotate-token); visible - // totals include the sessions group (4) and the agents pair (2). - expect(visibleCommands()).toHaveLength(58); + it("returns exactly the non-hidden commands", () => { + expect(visibleCommands()).toEqual(COMMANDS.filter((cmd) => !cmd.hidden)); }); it("no visible command has hidden=true", () => { diff --git a/src/lib/cli/public-display-defaults.ts b/src/lib/cli/public-display-defaults.ts index 7f086444c32..e0d6e5bf657 100644 --- a/src/lib/cli/public-display-defaults.ts +++ b/src/lib/cli/public-display-defaults.ts @@ -445,6 +445,13 @@ const PUBLIC_DISPLAY_LAYOUT: Record = { "order": 33 } ], + "tunnel:status": [ + { + "group": "Services", + "order": 33.5, + "description": "Show cloudflared public-URL tunnel status" + } + ], "uninstall": [ { "group": "Cleanup", diff --git a/src/lib/tunnel/command-support.ts b/src/lib/tunnel/command-support.ts index e5d74de8b03..0c61b4c4fdb 100644 --- a/src/lib/tunnel/command-support.ts +++ b/src/lib/tunnel/command-support.ts @@ -17,5 +17,6 @@ export function printTunnelUsage(log: (message?: string) => void = console.log): log(" Subcommands:"); log(" start Start the cloudflared public-URL tunnel"); log(" stop Stop the cloudflared public-URL tunnel"); + log(" status Show cloudflared public-URL tunnel status"); log(""); } diff --git a/test/cli.test.ts b/test/cli.test.ts index 092160672e7..4f5a85e8a17 100644 --- a/test/cli.test.ts +++ b/test/cli.test.ts @@ -2028,6 +2028,23 @@ describe("CLI dispatch", () => { expect(r.out).toContain("Unexpected argument: bogus"); }); + it("tunnel --help exits 0 and shows tunnel subcommands", () => { + const r = run("tunnel --help"); + expect(r.code).toBe(0); + expect(r.out).toContain("tunnel "); + expect(r.out).toContain("tunnel start"); + expect(r.out).toContain("tunnel stop"); + expect(r.out).toContain("tunnel status"); + }); + + it("root help shows tunnel status with tunnel start and stop", () => { + const r = run("--help"); + expect(r.code).toBe(0); + expect(r.out).toContain("nemoclaw tunnel start"); + expect(r.out).toContain("nemoclaw tunnel stop"); + expect(r.out).toContain("nemoclaw tunnel status"); + }); + it("tunnel start --help exits 0 and shows tunnel usage", () => { const r = run("tunnel start --help"); expect(r.code).toBe(0); @@ -2049,20 +2066,26 @@ describe("CLI dispatch", () => { expect(r.out).toContain("Stop the cloudflared public-URL tunnel"); }); - it("tunnel --help exits 0 and shows tunnel subcommands", () => { - const r = run("tunnel --help"); + it("tunnel status --help exits 0 and shows tunnel status usage", () => { + const r = run("tunnel status --help"); expect(r.code).toBe(0); - expect(r.out).toContain("USAGE"); - expect(r.out).toContain("$ nemoclaw tunnel "); - expect(r.out).toContain("tunnel start"); - expect(r.out).toContain("tunnel stop"); + expect(r.out).toContain("tunnel status"); + expect(r.out).toContain("Show cloudflared public-URL tunnel status"); + }); + + it("tunnel status exits 0 and prints cloudflared status", () => { + const r = run("tunnel status"); + expect(r.code).toBe(0); + expect(r.out).toContain("cloudflared"); }); it("bare tunnel exits 0 and shows tunnel subcommands", () => { const r = run("tunnel"); expect(r.code).toBe(0); + expect(r.out).toContain("tunnel "); expect(r.out).toContain("tunnel start"); expect(r.out).toContain("tunnel stop"); + expect(r.out).toContain("tunnel status"); }); it("deprecated stop --help exits 0 and shows alias usage", () => {