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
9 changes: 9 additions & 0 deletions docs/reference/commands-nemohermes.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -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`

<Warning>
Expand Down
9 changes: 9 additions & 0 deletions docs/reference/commands.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -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`

<Warning>
Expand Down
12 changes: 7 additions & 5 deletions src/commands/tunnel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 <start|stop>"];
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 <start|stop|status>"];
static examples = [
"<%= config.bin %> tunnel start",
"<%= config.bin %> tunnel stop",
"<%= config.bin %> tunnel status",
];
static flags = {};

public async run(): Promise<void> {
Expand Down
21 changes: 21 additions & 0 deletions src/commands/tunnel/status.ts
Original file line number Diff line number Diff line change
@@ -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<void> {
await this.parse(TunnelStatusCommand);
showStatus();
}
}
26 changes: 12 additions & 14 deletions src/lib/cli/command-registry.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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", () => {
Expand All @@ -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", () => {
Expand All @@ -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", () => {
Expand Down
7 changes: 7 additions & 0 deletions src/lib/cli/public-display-defaults.ts
Original file line number Diff line number Diff line change
Expand Up @@ -445,6 +445,13 @@ const PUBLIC_DISPLAY_LAYOUT: Record<string, readonly PublicDisplayLayout[]> = {
"order": 33
}
],
"tunnel:status": [
{
"group": "Services",
"order": 33.5,
"description": "Show cloudflared public-URL tunnel status"
}
],
"uninstall": [
{
"group": "Cleanup",
Expand Down
1 change: 1 addition & 0 deletions src/lib/tunnel/command-support.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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("");
}
35 changes: 29 additions & 6 deletions test/cli.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 <start|stop|status>");
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);
Expand All @@ -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 <start|stop>");
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 <start|stop|status>");
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", () => {
Expand Down
Loading