@mycadet/cli 0.12.9 → 0.12.11

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 (55) hide show
  1. package/node_modules/@mycadet/assistant-api/node_modules/@mycadet/service-contracts/src/__tests__/ingress.test.ts +8 -8
  2. package/node_modules/@mycadet/assistant-api/node_modules/@mycadet/service-contracts/src/ingress.ts +1 -1
  3. package/node_modules/@mycadet/local-mode/node_modules/@mycadet/service-contracts/src/__tests__/ingress.test.ts +8 -8
  4. package/node_modules/@mycadet/local-mode/node_modules/@mycadet/service-contracts/src/ingress.ts +1 -1
  5. package/node_modules/@mycadet/service-contracts/src/__tests__/ingress.test.ts +8 -8
  6. package/node_modules/@mycadet/service-contracts/src/ingress.ts +1 -1
  7. package/package.json +4 -3
  8. package/src/__tests__/client-web-config.test.ts +0 -1
  9. package/src/__tests__/cloud-login-poll.test.ts +69 -0
  10. package/src/__tests__/devices.test.ts +62 -0
  11. package/src/__tests__/ensure-binary.test.ts +52 -0
  12. package/src/__tests__/hatch-next-steps.test.ts +38 -0
  13. package/src/__tests__/multi-local.test.ts +3 -3
  14. package/src/__tests__/nginx-ingress.test.ts +58 -26
  15. package/src/__tests__/pair.test.ts +149 -78
  16. package/src/__tests__/ps-cloud-status.test.ts +6 -4
  17. package/src/__tests__/setup.test.ts +14 -4
  18. package/src/__tests__/teleport.test.ts +0 -15
  19. package/src/__tests__/tunnel-startup-summary.test.ts +110 -0
  20. package/src/__tests__/tunnel-stop.test.ts +226 -0
  21. package/src/__tests__/tunnel.test.ts +265 -70
  22. package/src/__tests__/uninstall.test.ts +140 -8
  23. package/src/commands/client.ts +4 -6
  24. package/src/commands/devices.ts +66 -30
  25. package/src/commands/hatch.ts +14 -53
  26. package/src/commands/pair.ts +147 -38
  27. package/src/commands/ps.ts +10 -20
  28. package/src/commands/teleport.ts +2 -21
  29. package/src/commands/tunnel.ts +67 -28
  30. package/src/commands/uninstall.ts +106 -71
  31. package/src/commands/upgrade.ts +6 -1
  32. package/src/lib/__tests__/local-gateway-cli.test.ts +42 -0
  33. package/src/lib/assistant-config.ts +20 -5
  34. package/src/lib/cloud-client.ts +0 -3
  35. package/src/lib/cloud-login-poll.ts +75 -0
  36. package/src/lib/cloud-login.ts +3 -2
  37. package/src/lib/color.ts +4 -0
  38. package/src/lib/docker.ts +46 -70
  39. package/src/lib/ensure-binary.ts +65 -6
  40. package/src/lib/environments/__tests__/paths.test.ts +27 -1
  41. package/src/lib/environments/paths.ts +16 -8
  42. package/src/lib/environments/resolve.ts +2 -4
  43. package/src/lib/hatch-local.ts +16 -59
  44. package/src/lib/hatch-next-steps.ts +18 -9
  45. package/src/lib/live-voice/tunnel.ts +2 -2
  46. package/src/lib/local.ts +42 -9
  47. package/src/lib/nginx-ingress.ts +124 -24
  48. package/src/lib/step-runner.ts +6 -1
  49. package/src/lib/tunnel.ts +86 -34
  50. package/src/lib/tunnels/cadet.ts +12 -1
  51. package/src/lib/tunnels/cloudflare.ts +36 -1
  52. package/src/lib/tunnels/ngrok.ts +16 -2
  53. package/src/lib/tunnels/process-lifecycle.ts +26 -0
  54. package/src/lib/tunnels/tailscale.ts +79 -1
  55. package/src/lib/xdg-log.ts +11 -0
@@ -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"),
@@ -105,7 +137,7 @@ describe("cadet uninstall", () => {
105
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
  });
@@ -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,
@@ -13,6 +13,7 @@
13
13
  * that runs the assistant (server side).
14
14
  */
15
15
 
16
+ import { z } from "zod";
16
17
  import { createHash } from "node:crypto";
17
18
 
18
19
  import type { DeviceRecord } from "@mycadet/local-mode";
@@ -33,8 +34,16 @@ import {
33
34
  canPromptForConfirmation,
34
35
  confirmAction,
35
36
  } from "../lib/confirm-action.js";
37
+ import { GATEWAY_INTERNAL_PORT } from "../lib/environments/paths.js";
38
+ import { execOutput } from "../lib/step-runner.js";
39
+ import { dockerResourceNames } from "../lib/docker.js";
36
40
  import { loopbackSafeFetch } from "../lib/loopback-fetch.js";
37
41
 
42
+ const dockerGatewayResponseSchema = z.object({
43
+ status: z.number().int().min(200).max(599),
44
+ body: z.string(),
45
+ });
46
+
38
47
  function printUsage(): void {
39
48
  console.log(`cadet devices [beta] - List and revoke devices paired to a local assistant
40
49
 
@@ -81,6 +90,9 @@ function resolveLoopbackBase(entry: AssistantEntry): string {
81
90
  process.exit(1);
82
91
  }
83
92
 
93
+ if (entry.cloud === "docker")
94
+ return `http://127.0.0.1:${GATEWAY_INTERNAL_PORT}`;
95
+
84
96
  const base =
85
97
  entry.localUrl ||
86
98
  (entry.resources?.gatewayPort
@@ -96,6 +108,31 @@ function resolveLoopbackBase(entry: AssistantEntry): string {
96
108
  return base.replace(/\/+$/, "");
97
109
  }
98
110
 
111
+ /** Docker host management enters the gateway network namespace before calling loopback-only routes. */
112
+ async function deviceRequest(
113
+ entry: AssistantEntry,
114
+ url: string,
115
+ init: RequestInit,
116
+ ): Promise<Response> {
117
+ if (entry.cloud !== "docker") return loopbackSafeFetch(url, init);
118
+ const script = `const r = await fetch(process.argv[1], {...JSON.parse(process.argv[2]), signal: AbortSignal.timeout(10000)}); console.log(JSON.stringify({status:r.status, body:await r.text()}));`;
119
+ const output = await execOutput(
120
+ "docker",
121
+ [
122
+ "exec",
123
+ dockerResourceNames(entry.assistantId).gatewayContainer,
124
+ "bun",
125
+ "-e",
126
+ script,
127
+ url,
128
+ JSON.stringify(init),
129
+ ],
130
+ { timeoutMs: 15000 },
131
+ );
132
+ const result = dockerGatewayResponseSchema.parse(JSON.parse(output));
133
+ return new Response(result.body, { status: result.status });
134
+ }
135
+
99
136
  /**
100
137
  * The hashed device id the gateway stores for THIS machine's own guardian
101
138
  * credential (`leaseGuardianToken` registers it device-bound as
@@ -122,7 +159,7 @@ async function listDevices(
122
159
 
123
160
  let response: Response;
124
161
  try {
125
- response = await loopbackSafeFetch(`${base}/v1/devices`, {
162
+ response = await deviceRequest(entry, `${base}/v1/devices`, {
126
163
  method: "GET",
127
164
  headers: getClientRegistrationHeaders(CLI_INTERFACE_ID),
128
165
  });
@@ -239,7 +276,7 @@ async function revokeDevice(
239
276
 
240
277
  let response: Response;
241
278
  try {
242
- response = await loopbackSafeFetch(`${base}/v1/devices/revoke`, {
279
+ response = await deviceRequest(entry, `${base}/v1/devices/revoke`, {
243
280
  method: "POST",
244
281
  headers: {
245
282
  "Content-Type": "application/json",
@@ -281,38 +318,37 @@ async function revokeDevice(
281
318
 
282
319
  export const devices = Object.assign(
283
320
  async function devices(): Promise<void> {
284
- const rawArgs = process.argv.slice(3);
285
-
286
- if (rawArgs.includes("--help") || rawArgs.includes("-h")) {
287
- printUsage();
288
- return;
289
- }
321
+ const rawArgs = process.argv.slice(3);
290
322
 
291
- const jsonOutput = rawArgs.includes("--json");
292
- const args = rawArgs.filter((a) => a !== "--json");
293
-
294
- if (args[0] === "revoke") {
295
- const rest = args.slice(1);
296
- const yes = rest.includes("--yes");
297
- const positionals = rest.filter((a) => !a.startsWith("-"));
298
- const hashedDeviceId = positionals[0];
299
- if (!hashedDeviceId) {
300
- console.error("Error: a hashedDeviceId is required to revoke.");
323
+ if (rawArgs.includes("--help") || rawArgs.includes("-h")) {
301
324
  printUsage();
302
- process.exit(1);
325
+ return;
303
326
  }
304
- const nameArg = positionals.slice(1).join(" ") || undefined;
327
+
328
+ const jsonOutput = rawArgs.includes("--json");
329
+ const args = rawArgs.filter((a) => a !== "--json");
330
+
331
+ if (args[0] === "revoke") {
332
+ const rest = args.slice(1);
333
+ const yes = rest.includes("--yes");
334
+ const positionals = rest.filter((a) => !a.startsWith("-"));
335
+ const hashedDeviceId = positionals[0];
336
+ if (!hashedDeviceId) {
337
+ console.error("Error: a hashedDeviceId is required to revoke.");
338
+ printUsage();
339
+ process.exit(1);
340
+ }
341
+ const nameArg = positionals.slice(1).join(" ") || undefined;
342
+ const entry = resolveTargetAssistant(nameArg);
343
+ const base = resolveLoopbackBase(entry);
344
+ await revokeDevice(entry, base, hashedDeviceId, yes, jsonOutput);
345
+ return;
346
+ }
347
+
348
+ const nameArg = parseAssistantTargetArg(args, []);
305
349
  const entry = resolveTargetAssistant(nameArg);
306
350
  const base = resolveLoopbackBase(entry);
307
- await revokeDevice(entry, base, hashedDeviceId, yes, jsonOutput);
308
- return;
309
- }
310
-
311
- const nameArg = parseAssistantTargetArg(args, []);
312
- const entry = resolveTargetAssistant(nameArg);
313
- const base = resolveLoopbackBase(entry);
314
- await listDevices(entry, base, jsonOutput);
315
- },
351
+ await listDevices(entry, base, jsonOutput);
352
+ },
316
353
  { description: "List or revoke devices paired to a local assistant [beta]" },
317
354
  );
318
-
@@ -6,10 +6,11 @@ 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";
12
- import { parseFeatureFlagArgs, readAmbientFlagEnvVars } from "../lib/flag-args";
13
+ import { readAmbientFlagEnvVars } from "../lib/flag-args";
13
14
  import { hatchLocal } from "../lib/hatch-local";
14
15
  import {
15
16
  getCloudUrl,
@@ -22,37 +23,26 @@ const DEFAULT_REMOTE: RemoteHost = "local";
22
23
 
23
24
  interface HatchArgs {
24
25
  verbose: boolean;
25
- keepAlive: boolean;
26
26
  name: string | null;
27
27
  remote: RemoteHost;
28
28
  watch: boolean;
29
29
  sourcePath: string | null;
30
30
  configValues: Record<string, string>;
31
31
  flagEnvVars: Record<string, string>;
32
- analyze: boolean;
33
- disableCloud: boolean;
34
32
  netnsContainer: string | null;
35
33
  gatewayPort: number | null;
36
34
  assistantCaCert: string | null;
37
35
  }
38
36
 
39
37
  function parseArgs(): HatchArgs {
40
- const { envVars: cliFlagVars, remaining: args } = parseFeatureFlagArgs(
41
- process.argv.slice(3),
42
- );
43
- const flagEnvVars = { ...readAmbientFlagEnvVars(), ...cliFlagVars };
44
- const disableCloudAmbient =
45
- process.env.CADET_DISABLE_CLOUD?.trim().toLowerCase();
46
- let disableCloud =
47
- disableCloudAmbient === "true" || disableCloudAmbient === "1";
38
+ const args = process.argv.slice(3);
39
+ const flagEnvVars = readAmbientFlagEnvVars();
48
40
  let verbose = false;
49
- let keepAlive = false;
50
41
  let name: string | null = null;
51
42
  let remote: RemoteHost = DEFAULT_REMOTE;
52
43
  let watch = false;
53
44
  let sourcePath: string | null = null;
54
45
  const configValues: Record<string, string> = {};
55
- let analyze = false;
56
46
  let netnsContainer: string | null = null;
57
47
  let gatewayPort: number | null = null;
58
48
  let assistantCaCert: string | null = null;
@@ -68,47 +58,33 @@ function parseArgs(): HatchArgs {
68
58
  console.log(
69
59
  " -v, --verbose Show service startup logs and extended next steps",
70
60
  );
71
- console.log(" --name <name> Custom instance name");
61
+ console.log(" --name <name> Custom name for this assistant");
72
62
  console.log(
73
63
  " --remote <host> Remote host (local, docker, custom, cloud)",
74
64
  );
75
65
  console.log(
76
- " --watch Run assistant and gateway in watch mode (hot reload on source changes)",
77
- );
78
- console.log(
79
- " --source <path> Build images from a local source tree at <path>. Useful for callers that want each run to pick up local CLI changes.",
66
+ " --watch Run Cadet in watch mode with hot reload on source changes",
80
67
  );
81
68
  console.log(
82
- " --keep-alive Stay alive after hatch, exit when gateway stops",
69
+ " --source <path> Build images from a local source tree at <path>",
83
70
  );
84
71
  console.log(
85
72
  " --config <key=value> Set a workspace config value (repeatable)",
86
73
  );
87
74
  console.log(
88
- " --flag <key=value> Set a feature flag override as CADET_FLAG_<KEY> env var (repeatable)",
89
- );
90
- console.log(
91
- " --analyze Emit a structured hatch-timing log line on stdout",
92
- );
93
- console.log(
94
- " --disable-cloud Suppress all outbound platform API calls",
95
- );
96
- console.log(
97
- " --netns-container <name> Join an existing container's network namespace (docker target only) instead of creating a per-instance network. The namespace owner publishes host ports, so --gateway-port is required.",
75
+ " --netns-container <name> Join an existing container's network namespace instead of creating a per-instance network.",
98
76
  );
99
77
  console.log(
100
- " --gateway-port <port> Use an explicit host port for the gateway runtime URL instead of auto-allocating. Required with --netns-container.",
78
+ " --gateway-port <port> Use an explicit host port for accessing the assistant.",
101
79
  );
102
80
  console.log(
103
- " --assistant-ca-cert <path> Trust an extra PEM CA bundle in the assistant container (NODE_EXTRA_CA_CERTS) from process start. Useful behind a TLS-terminating egress proxy.",
81
+ " --assistant-ca-cert <path> Trust an extra PEM CA bundle in the assistant container from process start.",
104
82
  );
105
83
  process.exit(0);
106
84
  } else if (arg === "-v" || arg == "--verbose") {
107
85
  verbose = true;
108
86
  } else if (arg === "--watch") {
109
87
  watch = true;
110
- } else if (arg === "--analyze") {
111
- analyze = true;
112
88
  } else if (arg === "--source") {
113
89
  const next = args[i + 1];
114
90
  if (!next || next.startsWith("-")) {
@@ -117,8 +93,6 @@ function parseArgs(): HatchArgs {
117
93
  }
118
94
  sourcePath = next;
119
95
  i++;
120
- } else if (arg === "--keep-alive") {
121
- keepAlive = true;
122
96
  } else if (arg === "--name") {
123
97
  const next = args[i + 1];
124
98
  if (!next || next.startsWith("-")) {
@@ -162,8 +136,6 @@ function parseArgs(): HatchArgs {
162
136
  const value = next.slice(eqIndex + 1);
163
137
  configValues[key] = value;
164
138
  i++;
165
- } else if (arg === "--disable-cloud") {
166
- disableCloud = true;
167
139
  } else if (arg === "--netns-container") {
168
140
  const next = args[i + 1];
169
141
  if (!next || next.startsWith("-")) {
@@ -193,7 +165,7 @@ function parseArgs(): HatchArgs {
193
165
  i++;
194
166
  } else {
195
167
  console.error(
196
- `Error: Unknown argument '${arg}'. Valid options: -v, --verbose, --watch, --source <path>, --keep-alive, --name <name>, --remote <${VALID_REMOTE_HOSTS.join("|")}>, --config <key=value>, --flag <key=value>, --analyze, --disable-cloud, --netns-container <name>, --gateway-port <port>, --assistant-ca-cert <path>`,
168
+ `Error: Unknown argument '${arg}'. Valid options: -v, --verbose, --watch, --source <path>, --name <name>, --remote <${VALID_REMOTE_HOSTS.join("|")}>, --config <key=value>, --netns-container <name>, --gateway-port <port>, --assistant-ca-cert <path>`,
197
169
  );
198
170
  process.exit(1);
199
171
  }
@@ -201,15 +173,12 @@ function parseArgs(): HatchArgs {
201
173
 
202
174
  return {
203
175
  verbose,
204
- keepAlive,
205
176
  name,
206
177
  remote,
207
178
  watch,
208
179
  sourcePath,
209
180
  configValues,
210
181
  flagEnvVars,
211
- analyze,
212
- disableCloud,
213
182
  netnsContainer,
214
183
  gatewayPort,
215
184
  assistantCaCert,
@@ -229,25 +198,17 @@ export const hatch = Object.assign(
229
198
 
230
199
  const {
231
200
  verbose,
232
- keepAlive,
233
201
  name,
234
202
  remote,
235
203
  watch,
236
204
  sourcePath,
237
205
  configValues,
238
206
  flagEnvVars,
239
- analyze,
240
- disableCloud,
241
207
  netnsContainer,
242
208
  gatewayPort,
243
209
  assistantCaCert,
244
210
  } = parseArgs();
245
211
 
246
- if (disableCloud) {
247
- process.env.CADET_DISABLE_CLOUD = "true";
248
- flagEnvVars.CADET_DISABLE_CLOUD = "true";
249
- }
250
-
251
212
  if (watch && remote !== "local" && remote !== "docker") {
252
213
  console.error(
253
214
  "Error: --watch is only supported for local and docker hatch targets.",
@@ -285,7 +246,6 @@ export const hatch = Object.assign(
285
246
  await hatchLocal({
286
247
  name,
287
248
  watch,
288
- keepAlive,
289
249
  configValues,
290
250
  flagEnvVars,
291
251
  verbose,
@@ -300,7 +260,6 @@ export const hatch = Object.assign(
300
260
  configValues,
301
261
  flagEnvVars,
302
262
  sourcePath,
303
- analyze,
304
263
  netnsContainer: netnsContainer ?? undefined,
305
264
  gatewayPort: gatewayPort ?? undefined,
306
265
  assistantCaCertPath: assistantCaCert ?? undefined,
@@ -347,7 +306,9 @@ async function hatchCadetPlatform(): Promise<void> {
347
306
  });
348
307
  setActiveAssistant(result.id);
349
308
 
350
- console.log(` ${HATCHED_EMOJI} Your assistant has hatched!`);
309
+ console.log(
310
+ " " + green(`${HATCHED_EMOJI} Your assistant has hatched!`, process.stdout),
311
+ );
351
312
  console.log("");
352
313
  console.log(` ID: ${result.id}`);
353
314
  console.log(` Name: ${result.name}`);