@mycadet/cli 0.12.8 → 0.12.9-dev.202610131620.d833e01

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (67) hide show
  1. package/node_modules/@mycadet/assistant-api/events/show-cloud-login.ts +2 -2
  2. package/node_modules/@mycadet/assistant-api/node_modules/@mycadet/service-contracts/package.json +1 -1
  3. package/node_modules/@mycadet/local-mode/node_modules/@mycadet/service-contracts/package.json +1 -1
  4. package/node_modules/@mycadet/service-contracts/package.json +1 -1
  5. package/package.json +4 -3
  6. package/src/__tests__/client-web-config.test.ts +0 -1
  7. package/src/__tests__/cloud-releases.test.ts +23 -13
  8. package/src/__tests__/ensure-binary.test.ts +182 -0
  9. package/src/__tests__/nginx-ingress.test.ts +6 -11
  10. package/src/__tests__/ngrok-spawn.test.ts +49 -0
  11. package/src/__tests__/pair.test.ts +149 -78
  12. package/src/__tests__/ps-cloud-status.test.ts +4 -3
  13. package/src/__tests__/ps-table.test.ts +79 -0
  14. package/src/__tests__/tailscale.test.ts +1 -1
  15. package/src/__tests__/teleport.test.ts +1 -16
  16. package/src/__tests__/tunnel.test.ts +274 -82
  17. package/src/__tests__/uninstall.test.ts +141 -9
  18. package/src/__tests__/wake.test.ts +2 -2
  19. package/src/commands/backup.ts +3 -3
  20. package/src/commands/client.ts +4 -6
  21. package/src/commands/completions.test.ts +104 -0
  22. package/src/commands/completions.ts +87 -0
  23. package/src/commands/exec.ts +7 -7
  24. package/src/commands/flags.ts +2 -2
  25. package/src/commands/hatch.ts +5 -2
  26. package/src/commands/logs.ts +7 -7
  27. package/src/commands/pair.ts +140 -37
  28. package/src/commands/ps.ts +63 -26
  29. package/src/commands/restore.ts +2 -2
  30. package/src/commands/retire.ts +2 -2
  31. package/src/commands/rollback.ts +2 -2
  32. package/src/commands/sleep.ts +2 -2
  33. package/src/commands/teleport.ts +18 -37
  34. package/src/commands/terminal.ts +1 -1
  35. package/src/commands/tunnel.ts +101 -78
  36. package/src/commands/uninstall.ts +108 -73
  37. package/src/commands/upgrade.ts +8 -8
  38. package/src/commands/use.ts +1 -1
  39. package/src/commands/wake.ts +6 -6
  40. package/src/index.ts +15 -0
  41. package/src/lib/__tests__/docker.test.ts +168 -20
  42. package/src/lib/__tests__/job-polling.test.ts +1 -1
  43. package/src/lib/__tests__/local-gateway-cli.test.ts +42 -0
  44. package/src/lib/cloud-client.ts +0 -3
  45. package/src/lib/cloud-login.ts +0 -1
  46. package/src/lib/cloud-releases.ts +6 -4
  47. package/src/lib/color.ts +4 -0
  48. package/src/lib/docker.ts +190 -102
  49. package/src/lib/ensure-binary.ts +171 -0
  50. package/src/lib/environments/__tests__/paths.test.ts +27 -1
  51. package/src/lib/environments/paths.ts +16 -8
  52. package/src/lib/environments/resolve.ts +2 -4
  53. package/src/lib/flag-args.test.ts +1 -1
  54. package/src/lib/flag-args.ts +1 -1
  55. package/src/lib/guardian-token.ts +1 -1
  56. package/src/lib/hatch-local.ts +15 -8
  57. package/src/lib/hatch-next-steps.ts +5 -9
  58. package/src/lib/job-polling.ts +1 -1
  59. package/src/lib/local.ts +37 -4
  60. package/src/lib/nginx-ingress.ts +3 -5
  61. package/src/lib/pair.ts +1 -1
  62. package/src/lib/step-runner.ts +6 -1
  63. package/src/lib/sync-cloud-assistants.ts +2 -2
  64. package/src/lib/tunnels/cloudflare.ts +21 -11
  65. package/src/lib/tunnels/ngrok.ts +198 -108
  66. package/src/lib/tunnels/tailscale.ts +34 -4
  67. package/src/lib/upgrade-lifecycle.ts +5 -4
@@ -1,4 +1,12 @@
1
- import { afterAll, afterEach, describe, expect, spyOn, test } from "bun:test";
1
+ import {
2
+ afterAll,
3
+ afterEach,
4
+ describe,
5
+ expect,
6
+ mock,
7
+ spyOn,
8
+ test,
9
+ } from "bun:test";
2
10
  import { existsSync, mkdirSync, mkdtempSync, rmSync, writeFileSync } from "fs";
3
11
  import { tmpdir } from "os";
4
12
  import { dirname, join } from "path";
@@ -8,6 +16,7 @@ import {
8
16
  getConfigDirs,
9
17
  getInputHistoryPath,
10
18
  getLogDir,
19
+ getSharedStateDir,
11
20
  getStateDir,
12
21
  } from "../lib/environments/paths.js";
13
22
  import { getCurrentEnvironment } from "../lib/environments/resolve.js";
@@ -16,6 +25,20 @@ import type { AssistantEntry } from "../lib/assistant-config.js";
16
25
  // The command reads its argv at call time.
17
26
  const ORIGINAL_ARGV = process.argv;
18
27
 
28
+ // retireDocker is the one retirement path with observable side effects
29
+ // (docker teardown), and uninstall is the only importer in its graph.
30
+ // Mocked module-wide so tests pin WHEN it runs — never before the
31
+ // confirmation gate — as much as whether it runs.
32
+ const retireDockerCalls: string[] = [];
33
+ let onRetireDocker: ((name: string) => void) | undefined;
34
+
35
+ mock.module("../lib/docker.js", () => ({
36
+ retireDocker: (name: string) => {
37
+ retireDockerCalls.push(name);
38
+ onRetireDocker?.(name);
39
+ },
40
+ }));
41
+
19
42
  // Every XDG location the command touches MUST be redirected: the homedir
20
43
  // fallback would otherwise point at the real user's data dirs.
21
44
  function redirectXdg(): string {
@@ -70,7 +93,15 @@ describe("cadet uninstall", () => {
70
93
  const errorSpy = spyOn(console, "error").mockImplementation(() => {});
71
94
  const warnSpy = spyOn(console, "warn").mockImplementation(() => {});
72
95
 
96
+ function printedOutput(): string {
97
+ return logSpy.mock.calls
98
+ .map((args) => args.map(String).join(" "))
99
+ .join("\n");
100
+ }
101
+
73
102
  afterEach(() => {
103
+ retireDockerCalls.length = 0;
104
+ onRetireDocker = undefined;
74
105
  logSpy.mockClear();
75
106
  errorSpy.mockClear();
76
107
  warnSpy.mockClear();
@@ -82,11 +113,12 @@ describe("cadet uninstall", () => {
82
113
  warnSpy.mockRestore();
83
114
  });
84
115
 
85
- test("warns and retires remaining assistants, then proceeds to the confirm gate", async () => {
116
+ test("warns that remaining assistants will be retired, then proceeds to the confirm gate", async () => {
86
117
  await withRestoredEnv(async () => {
87
118
  const root = redirectXdg();
88
119
  // A remaining assistant is retired — with its data deleted — as part
89
- // of the uninstall (v0.0.10 run feedback: the old hard refusal just
120
+ // of the uninstall AFTER the user confirms; the pre-confirm summary
121
+ // says so up front (v0.0.10 run feedback: the hard refusal just
90
122
  // blocked the uninstall; the fs sweep deletes the archives anyway).
91
123
  writeFileSync(
92
124
  join(process.env.CADET_LOCKFILE_DIR!, "lockfile.json"),
@@ -102,10 +134,10 @@ describe("cadet uninstall", () => {
102
134
  // not an assistant guard.
103
135
  await expect(uninstall()).rejects.toThrow("process.exit:1");
104
136
  expect(warnSpy).toHaveBeenCalledWith(
105
- expect.stringContaining("1 assistant(s) still exist"),
137
+ expect.stringContaining("1 assistant still exists"),
106
138
  );
107
139
  expect(warnSpy).toHaveBeenCalledWith(
108
- expect.stringContaining("will be retired now"),
140
+ expect.stringContaining("will be retired as part of this uninstall"),
109
141
  );
110
142
  expect(errorSpy).not.toHaveBeenCalledWith(
111
143
  expect.stringContaining("Retire them first"),
@@ -138,6 +170,67 @@ describe("cadet uninstall", () => {
138
170
  });
139
171
  });
140
172
 
173
+ test("does not retire assistants when the confirm gate refuses", async () => {
174
+ await withRestoredEnv(async () => {
175
+ const root = redirectXdg();
176
+ writeFileSync(
177
+ join(process.env.CADET_LOCKFILE_DIR!, "lockfile.json"),
178
+ JSON.stringify({
179
+ assistants: [{ ...makeEntry("docker-assistant"), cloud: "docker" }],
180
+ }) + "\n",
181
+ );
182
+
183
+ const exitMock = spyOn(process, "exit");
184
+ exitMock.mockImplementation((code?: number | string | undefined): never => {
185
+ throw new Error(`process.exit:${code}`);
186
+ });
187
+ try {
188
+ process.argv = ["bun", "cadet", "uninstall"];
189
+ await expect(uninstall()).rejects.toThrow("process.exit:1");
190
+ } finally {
191
+ exitMock.mockRestore();
192
+ }
193
+ // Retirement is part of the confirmed flow — a refusal must leave the
194
+ // assistant's docker resources untouched.
195
+ expect(retireDockerCalls).toEqual([]);
196
+
197
+ rmSync(root, { recursive: true, force: true });
198
+ });
199
+ });
200
+
201
+ test("--yes retires remaining assistants after the deletion list is shown", async () => {
202
+ await withRestoredEnv(async () => {
203
+ const root = redirectXdg();
204
+ writeFileSync(
205
+ join(process.env.CADET_LOCKFILE_DIR!, "lockfile.json"),
206
+ JSON.stringify({
207
+ assistants: [{ ...makeEntry("docker-assistant"), cloud: "docker" }],
208
+ }) + "\n",
209
+ );
210
+ mkdirSync(getConfigDirs(getCurrentEnvironment())[0]!, {
211
+ recursive: true,
212
+ });
213
+
214
+ // Capture what had been printed by the time retirement ran — the list
215
+ // the user confirms against must already be on screen.
216
+ let outputAtRetire = "";
217
+ onRetireDocker = () => {
218
+ outputAtRetire = printedOutput();
219
+ };
220
+
221
+ process.argv = ["bun", "cadet", "uninstall", "--yes"];
222
+ await uninstall();
223
+
224
+ expect(retireDockerCalls).toEqual(["docker-assistant"]);
225
+ expect(outputAtRetire).toContain("It will permanently delete:");
226
+ expect(warnSpy).toHaveBeenCalledWith(
227
+ expect.stringContaining("will be retired as part of this uninstall"),
228
+ );
229
+
230
+ rmSync(root, { recursive: true, force: true });
231
+ });
232
+ });
233
+
141
234
  test("--yes deletes the environment's XDG locations and leaves the source checkout alone", async () => {
142
235
  await withRestoredEnv(async () => {
143
236
  const root = redirectXdg();
@@ -166,15 +259,54 @@ describe("cadet uninstall", () => {
166
259
  expect(existsSync(logDir)).toBe(false);
167
260
  expect(existsSync(historyPath)).toBe(false);
168
261
 
262
+ // The list shows the shared state DIR (never the input-history FILE
263
+ // alone) and never names a nested path twice — the log dir goes with
264
+ // its parent's recursive delete.
265
+ const out = printedOutput();
266
+ expect(out).toContain(
267
+ ` - ${getSharedStateDir()} — shared state, including the terminal input history`,
268
+ );
269
+ expect(out).not.toContain(logDir);
270
+ expect(out).not.toContain(historyPath);
271
+ expect(out).not.toContain("It will then remove the Cadet executable");
272
+
169
273
  // Running under bun from a git checkout: self-removal must print the
170
274
  // note and delete NOTHING outside the XDG targets.
171
- const note = logSpy.mock.calls
172
- .map((args) => args.map(String).join(" "))
173
- .join("\n");
174
- expect(note).toContain("source checkout");
275
+ expect(printedOutput()).toContain("source checkout");
175
276
  expect(existsSync(process.execPath)).toBe(true);
176
277
 
177
278
  rmSync(root, { recursive: true, force: true });
178
279
  });
179
280
  });
281
+
282
+ test("--yes lists and removes the Cadet executable itself", async () => {
283
+ await withRestoredEnv(async () => {
284
+ const root = redirectXdg();
285
+ const configDir = getConfigDirs(getCurrentEnvironment())[0]!;
286
+ mkdirSync(configDir, { recursive: true });
287
+
288
+ // A compiled-binary install: process.execPath IS the install, so the
289
+ // uninstall list must name it alongside the data dirs.
290
+ const fakeBinDir = join(root, "bin");
291
+ mkdirSync(fakeBinDir, { recursive: true });
292
+ const fakeBin = join(fakeBinDir, "cadet");
293
+ writeFileSync(fakeBin, "#!/bin/sh\n");
294
+
295
+ const realExecPath = process.execPath;
296
+ process.execPath = fakeBin;
297
+ try {
298
+ process.argv = ["bun", "cadet", "uninstall", "--yes"];
299
+ await uninstall();
300
+ } finally {
301
+ process.execPath = realExecPath;
302
+ }
303
+
304
+ expect(printedOutput()).toContain(
305
+ ` - ${fakeBin} — the Cadet executable`,
306
+ );
307
+ expect(existsSync(fakeBin)).toBe(false);
308
+
309
+ rmSync(root, { recursive: true, force: true });
310
+ });
311
+ });
180
312
  });
@@ -1503,14 +1503,14 @@ describe("cadet wake — tunnel edge restore", () => {
1503
1503
  loadRawConfigMock.mockReturnValue(webhookConfig);
1504
1504
  ensureTunnelEdgeMock.mockRejectedValue(
1505
1505
  new Error(
1506
- "nginx is not installed, so the tunnel edge cannot start. Install it (macOS: `brew install nginx`, Linux: `sudo apt install nginx`) or point NGINX_BIN at an existing binary.",
1506
+ "nginx is not installed, so the tunnel edge cannot start. See https://nginx.org/en/docs/install.html for how to install, or point NGINX_BIN at an existing binary.",
1507
1507
  ),
1508
1508
  );
1509
1509
 
1510
1510
  await wake();
1511
1511
 
1512
1512
  expect(warnSpy).toHaveBeenCalledWith(
1513
- expect.stringContaining("brew install nginx"),
1513
+ expect.stringContaining("https://nginx.org/en/docs/install.html"),
1514
1514
  );
1515
1515
  // No tunnel is recorded here, so no provider can be named and the advice
1516
1516
  // points at the help that lists them.
@@ -42,10 +42,10 @@ export const backup = Object.assign(
42
42
  console.log("Options:");
43
43
  console.log(" --output <path> Path to save the .vbundle file");
44
44
  console.log(
45
- " (default: ~/.local/share/cadet/backups/<name>-<timestamp>.vbundle)",
45
+ " defaults to ~/.local/share/cadet/backups/<name>-<timestamp>.vbundle",
46
46
  );
47
47
  console.log(
48
- ` --export-timeout <secs> Export request timeout in seconds (default: ${DEFAULT_EXPORT_TIMEOUT_MS / 1000})`,
48
+ ` --export-timeout <secs> Export request timeout in seconds, defaults to ${DEFAULT_EXPORT_TIMEOUT_MS / 1000}`,
49
49
  );
50
50
  console.log("");
51
51
  console.log("Examples:");
@@ -254,7 +254,7 @@ async function backupPlatform(
254
254
  const cloudToken = readCloudToken();
255
255
  if (!cloudToken) {
256
256
  console.error(
257
- "Not logged in. Run 'cadet login' first (required for cloud-managed backup).",
257
+ "Not logged in. Run 'cadet login' first.",
258
258
  );
259
259
  process.exit(1);
260
260
  }
@@ -1304,10 +1304,9 @@ async function spawnBackgroundWebInterface(
1304
1304
  export function buildWebInterfaceConfig(opts: {
1305
1305
  webUrl: string;
1306
1306
  cloudUrl: string;
1307
- disableCloud: boolean;
1308
1307
  }): Record<string, unknown> {
1309
- const { webUrl, cloudUrl, disableCloud } = opts;
1310
- return { webUrl, cloudUrl, disableCloud };
1308
+ const { webUrl, cloudUrl } = opts;
1309
+ return { webUrl, cloudUrl };
1311
1310
  }
1312
1311
 
1313
1312
  async function runWebInterface(
@@ -1349,9 +1348,7 @@ async function runWebInterface(
1349
1348
  const webUrl = getWebUrl();
1350
1349
  const safeJson = (v: unknown) =>
1351
1350
  JSON.stringify(v).replace(/</g, "\\u003c").replace(/>/g, "\\u003e");
1352
- const configJson = safeJson(
1353
- buildWebInterfaceConfig({ webUrl, cloudUrl, disableCloud }),
1354
- );
1351
+ const configJson = safeJson(buildWebInterfaceConfig({ webUrl, cloudUrl }));
1355
1352
  const hasOverrides = Object.keys(parsedFlagOverrides).length > 0;
1356
1353
  const flagOverridesSnippet = hasOverrides
1357
1354
  ? `;window.__CADET_FLAG_OVERRIDES__=${safeJson(parsedFlagOverrides)}`
@@ -1557,6 +1554,7 @@ async function runViteDevServer(
1557
1554
  ...viteFlagVars,
1558
1555
  ...(disableCloud ? { VITE_CADET_DISABLE_CLOUD: "true" } : {}),
1559
1556
  VITE_CLOUD_MODE: "false",
1557
+ VITE_WEB_MODE: "full",
1560
1558
  API_PROXY_TARGET: cloudUrl,
1561
1559
  CADET_WEB_URL: getWebUrl(),
1562
1560
  CADET_CLOUD_URL: cloudUrl,
@@ -0,0 +1,104 @@
1
+ import { afterEach, beforeEach, describe, expect, test } from "bun:test";
2
+ import { mkdtempSync, rmSync } from "fs";
3
+ import { tmpdir } from "os";
4
+ import { join } from "path";
5
+
6
+ import { saveAssistantEntry } from "../lib/assistant-config.js";
7
+ import { completeCadet } from "./completions.js";
8
+
9
+ const COMMAND_NAMES = [
10
+ "backup",
11
+ "clean",
12
+ "client",
13
+ "completions",
14
+ "confirm",
15
+ "connect",
16
+ "hatch",
17
+ "ps",
18
+ "retire",
19
+ "sleep",
20
+ "use",
21
+ "wake",
22
+ ];
23
+
24
+ describe("completeCadet", () => {
25
+ let lockfileDir: string;
26
+ let prevLockfileDir: string | undefined;
27
+
28
+ beforeEach(() => {
29
+ lockfileDir = mkdtempSync(join(tmpdir(), "cadet-completions-"));
30
+ prevLockfileDir = process.env.CADET_LOCKFILE_DIR;
31
+ process.env.CADET_LOCKFILE_DIR = lockfileDir;
32
+ });
33
+
34
+ afterEach(() => {
35
+ if (prevLockfileDir === undefined) delete process.env.CADET_LOCKFILE_DIR;
36
+ else process.env.CADET_LOCKFILE_DIR = prevLockfileDir;
37
+ rmSync(lockfileDir, { recursive: true, force: true });
38
+ });
39
+
40
+ function seed(id: string): void {
41
+ saveAssistantEntry({
42
+ assistantId: id,
43
+ cloud: "docker",
44
+ runtimeUrl: "http://localhost:7830",
45
+ hatchedAt: new Date().toISOString(),
46
+ });
47
+ }
48
+
49
+ test("no words yet offers every command name, sorted", () => {
50
+ const input: string[] = [];
51
+ expect(completeCadet(input, COMMAND_NAMES)).toEqual(
52
+ [...COMMAND_NAMES].sort(),
53
+ );
54
+ // The word under the cursor is popped from a COPY — the caller's array
55
+ // (bash's COMP_WORDS) must come back untouched.
56
+ expect(input).toEqual([]);
57
+ });
58
+
59
+ test("first positional of an assistant-name command offers known assistant names", () => {
60
+ seed("quill-owl-d4e5f6");
61
+ seed("fable-fox-a1b2c3");
62
+ // `cadet retire <TAB>` — the empty trailing word is the word being completed.
63
+ expect(completeCadet(["retire", ""], COMMAND_NAMES)).toEqual([
64
+ "fable-fox-a1b2c3",
65
+ "quill-owl-d4e5f6",
66
+ ]);
67
+ });
68
+
69
+ test("partial words are returned unfiltered — the shell does prefix filtering", () => {
70
+ seed("quill-owl-d4e5f6");
71
+ seed("fable-fox-a1b2c3");
72
+ expect(completeCadet(["retire", "fable"], COMMAND_NAMES)).toEqual([
73
+ "fable-fox-a1b2c3",
74
+ "quill-owl-d4e5f6",
75
+ ]);
76
+ });
77
+
78
+ test("flag prefixes fall through to the shell default (no candidates)", () => {
79
+ seed("fable-fox-a1b2c3");
80
+ expect(completeCadet(["retire", "--"], COMMAND_NAMES)).toEqual([]);
81
+ expect(completeCadet(["retire", "--name"], COMMAND_NAMES)).toEqual([]);
82
+ });
83
+
84
+ test("deeper positionals of an assistant-name command offer nothing", () => {
85
+ seed("fable-fox-a1b2c3");
86
+ expect(
87
+ completeCadet(["exec", "fable-fox-a1b2c3", ""], COMMAND_NAMES),
88
+ ).toEqual([]);
89
+ expect(
90
+ completeCadet(["logs", "fable-fox-a1b2c3", "tail", ""], COMMAND_NAMES),
91
+ ).toEqual([]);
92
+ });
93
+
94
+ test("commands that take no assistant name offer nothing after the command word", () => {
95
+ seed("fable-fox-a1b2c3");
96
+ expect(completeCadet(["hatch", ""], COMMAND_NAMES)).toEqual([]);
97
+ expect(completeCadet(["ps", ""], COMMAND_NAMES)).toEqual([]);
98
+ });
99
+
100
+ test("an unreadable/empty lockfile completes quietly with no names", () => {
101
+ // No entries seeded — the lockfile dir exists but has no lockfile.json.
102
+ expect(completeCadet(["retire", ""], COMMAND_NAMES)).toEqual([]);
103
+ });
104
+ });
@@ -0,0 +1,87 @@
1
+ import { loadAllAssistants } from "../lib/assistant-config.js";
2
+
3
+ /**
4
+ * Commands whose first positional argument is an assistant name — completing
5
+ * their first word offers the names of known assistants (from the lockfile)
6
+ * instead of falling through to shell file completion.
7
+ */
8
+ const ASSISTANT_NAME_COMMANDS = new Set([
9
+ "exec",
10
+ "logs",
11
+ "message",
12
+ "restart",
13
+ "restore",
14
+ "retire",
15
+ "rollback",
16
+ "sleep",
17
+ "teleport",
18
+ "terminal",
19
+ "tunnel",
20
+ "unpair",
21
+ "upgrade",
22
+ "use",
23
+ "wake",
24
+ ]);
25
+
26
+ /** Assistant names known to this CLI's lockfile; [] when the lockfile is unreadable. */
27
+ function assistantNames(): string[] {
28
+ try {
29
+ return loadAllAssistants()
30
+ .map((entry) => entry.assistantId)
31
+ .sort();
32
+ } catch {
33
+ return [];
34
+ }
35
+ }
36
+
37
+ /**
38
+ * Resolve completion candidates for the word the cursor is on.
39
+ *
40
+ * `words` carries every command-line word after `cadet` up to the cursor,
41
+ * INCLUDING the (possibly empty) word being completed — the same shape bash
42
+ * passes via `COMP_WORDS[@]:1`. The returned list is UNFILTERED: the shell
43
+ * (`compgen -W`) does the prefix filtering, so the CLI only decides WHAT
44
+ * KIND of thing completes here.
45
+ */
46
+ export function completeCadet(
47
+ input: string[],
48
+ commandNames: readonly string[],
49
+ ): string[] {
50
+ const words = [...input];
51
+ const toComplete = words.pop() ?? "";
52
+ const preceding = words;
53
+
54
+ // Completing the command word itself (or nothing typed yet).
55
+ if (preceding.length === 0) {
56
+ return [...commandNames].sort();
57
+ }
58
+
59
+ // First positional of an assistant-name command: offer instance names.
60
+ // Flag prefixes fall through to shell default (no flag completion yet).
61
+ if (
62
+ preceding.length === 1 &&
63
+ ASSISTANT_NAME_COMMANDS.has(preceding[0]) &&
64
+ !toComplete.startsWith("-")
65
+ ) {
66
+ return assistantNames();
67
+ }
68
+
69
+ return [];
70
+ }
71
+
72
+ const BASH_COMPLETION_SCRIPT = `# bash completion for the cadet CLI — load with: eval "$(cadet completions)"
73
+ _cadet_completions() {
74
+ local candidates
75
+ candidates=$(cadet __complete "\${COMP_WORDS[@]:1}" 2>/dev/null)
76
+ COMPREPLY=($(compgen -W "$candidates" -- "\${COMP_WORDS[COMP_CWORD]}"))
77
+ }
78
+ complete -F _cadet_completions cadet`;
79
+
80
+ // ── Entry point ─────────────────────────────────────────────────
81
+
82
+ export const completions = Object.assign(
83
+ async function completions(): Promise<void> {
84
+ console.log(BASH_COMPLETION_SCRIPT);
85
+ },
86
+ { description: `Print bash completion setup (eval "$(cadet completions)")` },
87
+ );
@@ -75,28 +75,28 @@ export const exec = Object.assign(
75
75
  console.log("");
76
76
  console.log("Arguments:");
77
77
  console.log(
78
- " <name> Name of the assistant (defaults to active)",
78
+ " <name> Name of the assistant, defaults to active",
79
79
  );
80
80
  console.log(
81
81
  " <command...> Command and arguments to run (after --)",
82
82
  );
83
83
  console.log("");
84
84
  console.log("Options:");
85
- console.log(" --service <svc> Target service (default: assistant)");
85
+ console.log(" --service <svc> Target service, defaults to assistant");
86
86
  console.log(
87
87
  " -it Interactive mode with TTY (like docker exec -it)",
88
88
  );
89
89
  console.log(
90
- " --timeout <secs> Timeout in seconds (default: 30, 0 = no timeout)",
90
+ " --timeout <secs> Timeout in seconds — default 30, 0 = no timeout",
91
91
  );
92
92
  console.log(
93
- " --verbose Show debug output (SSE events, sentinel parsing)",
93
+ " --verbose Show debug output",
94
94
  );
95
95
  console.log("");
96
96
  console.log("Services:");
97
- console.log(" assistant (or mycadet)");
98
- console.log(" gateway (or cadet-gateway)");
99
- console.log(" credential-executor (or cadet-credential-executor)");
97
+ console.log(" assistant");
98
+ console.log(" gateway");
99
+ console.log(" credential-executor");
100
100
  console.log("");
101
101
  console.log("Examples:");
102
102
  console.log(" cadet exec -- ls -la /workspace");
@@ -59,7 +59,7 @@ function printFlagTable(flags: FeatureFlagEntry[]): void {
59
59
  console.log(formatRow(row));
60
60
  }
61
61
  console.log("");
62
- console.log("* = overridden (differs from default)");
62
+ console.log("* = overridden");
63
63
  }
64
64
 
65
65
  function printHelp(): void {
@@ -67,7 +67,7 @@ function printHelp(): void {
67
67
  console.log("");
68
68
  console.log("Show and toggle feature flags for the active assistant.");
69
69
  console.log(
70
- "Reads from the gateway's merged flag state (persisted overrides > remote > defaults).",
70
+ "Reads from the gateway's merged flag state.",
71
71
  );
72
72
  console.log("");
73
73
  console.log("Subcommands:");
@@ -6,6 +6,7 @@ import {
6
6
  saveAssistantEntry,
7
7
  setActiveAssistant,
8
8
  } from "../lib/assistant-config";
9
+ import { green } from "../lib/color";
9
10
  import { HATCH_ART, HATCHED_EMOJI, VALID_REMOTE_HOSTS } from "../lib/constants";
10
11
  import type { RemoteHost } from "../lib/constants";
11
12
  import { hatchDocker } from "../lib/docker";
@@ -76,7 +77,7 @@ function parseArgs(): HatchArgs {
76
77
  " --watch Run assistant and gateway in watch mode (hot reload on source changes)",
77
78
  );
78
79
  console.log(
79
- " --source <path> Build images from a local source tree at <path> (no watcher). Useful for callers (e.g. evals) that want each run to pick up local CLI changes.",
80
+ " --source <path> Build images from a local source tree at <path>. Useful for callers that want each run to pick up local CLI changes.",
80
81
  );
81
82
  console.log(
82
83
  " --keep-alive Stay alive after hatch, exit when gateway stops",
@@ -347,7 +348,9 @@ async function hatchCadetPlatform(): Promise<void> {
347
348
  });
348
349
  setActiveAssistant(result.id);
349
350
 
350
- console.log(` ${HATCHED_EMOJI} Your assistant has hatched!`);
351
+ console.log(
352
+ " " + green(`${HATCHED_EMOJI} Your assistant has hatched!`, process.stdout),
353
+ );
351
354
  console.log("");
352
355
  console.log(` ID: ${result.id}`);
353
356
  console.log(` Name: ${result.name}`);
@@ -32,19 +32,19 @@ function printHelp(): void {
32
32
  console.log("");
33
33
  console.log("Arguments:");
34
34
  console.log(
35
- " <name> Name of the assistant (defaults to latest)",
35
+ " <name> Name of the assistant, defaults to latest",
36
36
  );
37
37
  console.log("");
38
38
  console.log("Options:");
39
- console.log(" -f, --follow Follow log output (stream new lines)");
40
- console.log(" -n, --tail <N> Show last N lines (default: all)");
39
+ console.log(" -f, --follow Follow log output");
40
+ console.log(" -n, --tail <N> Show last N lines");
41
41
  console.log(" -t, --timestamps Show timestamps on each line");
42
42
  console.log(
43
- " --since <time> Show logs since timestamp or relative (e.g. 10m, 2h)",
43
+ " --since <time> Show logs since timestamp or relative, e.g. 10m or 2h",
44
44
  );
45
45
  console.log(" --until <time> Show logs until timestamp or relative");
46
46
  console.log(
47
- " -s, --service <name> Filter to a specific service (e.g. assistant, gateway)",
47
+ " -s, --service <name> Filter to a specific service",
48
48
  );
49
49
  console.log(" -h, --help Show this help");
50
50
  }
@@ -186,13 +186,13 @@ async function showLocalLogs(
186
186
 
187
187
  if (opts.since && !sinceDate) {
188
188
  console.error(
189
- `Error: Could not parse --since value '${opts.since}'. Use relative (e.g. 10m, 2h) or ISO format.`,
189
+ `Error: Could not parse --since value '${opts.since}'. Use a relative value such as 10m or 2h, or ISO format.`,
190
190
  );
191
191
  process.exit(1);
192
192
  }
193
193
  if (opts.until && !untilDate) {
194
194
  console.error(
195
- `Error: Could not parse --until value '${opts.until}'. Use relative (e.g. 10m, 2h) or ISO format.`,
195
+ `Error: Could not parse --until value '${opts.until}'. Use a relative value such as 10m or 2h, or ISO format.`,
196
196
  );
197
197
  process.exit(1);
198
198
  }