@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
@@ -3,9 +3,12 @@
3
3
  *
4
4
  * Print the pairing link for another device, and the same link rendered as a
5
5
  * QR code. Runs on the machine hosting the assistant: it mints a remote-web
6
- * pairing challenge over the loopback gateway and approves it immediately, so
7
- * running this command IS the local-presence proof and one scan (or one paste
8
- * into `cadet connect import`) completes the pairing.
6
+ * pairing challenge through the gateway CLI (`gateway pairing mint <url>` —
7
+ * locally, or docker-exec'd into the gateway container) which approves it
8
+ * immediately, so running this command IS the local-presence proof and one
9
+ * scan (or one paste into `cadet connect import`) completes the pairing.
10
+ * local and docker assistants mint without any HTTP; other topologies keep
11
+ * the loopback HTTP mint.
9
12
  *
10
13
  * `--web-approve` is the reverse direction: approve a code a device already
11
14
  * shows, for pairings that device started on its own.
@@ -38,8 +41,11 @@ import {
38
41
  import { GATEWAY_PORT } from "../lib/constants.js";
39
42
  import { getCurrentEnvironment } from "../lib/environments/resolve.js";
40
43
  import { isLoopbackUrl, loopbackSafeFetch } from "../lib/loopback-fetch.js";
44
+ import { dockerResourceNames } from "../lib/docker.js";
45
+ import { gatewayCliInvocation } from "../lib/local.js";
41
46
  import { formatWebApproveFailure, parseGatewayErrorCode } from "../lib/pair.js";
42
47
  import { STALE_CLI_UPDATE_HINT } from "../lib/stale-cli-hint.js";
48
+ import { execOutput } from "../lib/step-runner.js";
43
49
 
44
50
  function assistantDisplayName(entry: AssistantEntry): string {
45
51
  return entry.name || entry.assistantName || entry.assistantId;
@@ -89,9 +95,10 @@ function printUsage(): void {
89
95
  USAGE:
90
96
  cadet pair [assistant] [options]
91
97
 
92
- Mints a pairing challenge on the assistant's gateway over loopback and
93
- approves it on the spot (running this command on the host machine is the
94
- approval), then prints the pairing link and the same link as a QR code.
98
+ Mints a pairing challenge on the assistant's gateway (via the gateway CLI
99
+ for local and docker assistants, over loopback HTTP otherwise) and approves
100
+ it on the spot (running this command on the host machine is the approval),
101
+ then prints the pairing link and the same link as a QR code.
95
102
  Scanning the QR, opening the link, or passing it to 'cadet connect import'
96
103
  on the other device completes the pairing in one step.
97
104
 
@@ -214,26 +221,122 @@ async function exitOnHttpError(response: Response): Promise<Response> {
214
221
  return response;
215
222
  }
216
223
 
217
- /** Create a remote-web pairing challenge (RFC 8628 device-code flow). */
218
- async function createRemoteWebPairingChallenge(
219
- gatewayUrl: string,
224
+ /**
225
+ * Mint (and approve) a remote-web pairing challenge. `local` and `docker`
226
+ * remotes mint through the gateway CLI — `gateway pairing mint <url>` run on
227
+ * the assistant's host (locally, or docker-exec'd into the gateway container)
228
+ * — replacing the old loopback HTTP mint whose loopback-peer gate
229
+ * containerized gateways could never satisfy (published-port NAT rewrites
230
+ * host-originated peers to the docker bridge address). The mint executes the
231
+ * pairing logic directly against the file-backed challenge store, so the
232
+ * running gateway's verification routes see it with no IPC round-trip.
233
+ * Process execution is the local-presence proof; the mint command approves
234
+ * on the spot, so the returned challenge is already paired-ready. Other
235
+ * topologies keep the loopback HTTP mint they used before.
236
+ */
237
+ async function mintPairingChallengeOrExit(
238
+ entry: AssistantEntry,
239
+ publicBaseUrl: string,
240
+ ): Promise<RemoteWebPairingChallengeResponse> {
241
+ if (entry.cloud !== "local" && entry.cloud !== "docker") {
242
+ return mintPairingChallengeOverLoopbackOrExit(entry, publicBaseUrl);
243
+ }
244
+
245
+ let stdout: string;
246
+ try {
247
+ if (entry.cloud === "docker") {
248
+ stdout = await execOutput(
249
+ "docker",
250
+ [
251
+ "exec",
252
+ dockerResourceNames(entry.assistantId).gatewayContainer,
253
+ "bun",
254
+ "run",
255
+ "src/cli/index.ts",
256
+ "pairing",
257
+ "mint",
258
+ publicBaseUrl,
259
+ ],
260
+ { timeoutMs: 30_000 },
261
+ );
262
+ } else {
263
+ const invocation = gatewayCliInvocation(entry.resources);
264
+ stdout = await execOutput(
265
+ invocation.command,
266
+ [...invocation.args, "pairing", "mint", publicBaseUrl],
267
+ {
268
+ cwd: invocation.cwd,
269
+ env: invocation.env,
270
+ timeoutMs: 30_000,
271
+ },
272
+ );
273
+ }
274
+ } catch (err) {
275
+ console.error(
276
+ `Error: could not mint a pairing challenge on the gateway ` +
277
+ `(${err instanceof Error ? err.message : String(err)}).`,
278
+ );
279
+ console.error("Is the assistant running? Try `cadet wake`.");
280
+ process.exit(1);
281
+ }
282
+
283
+ const lastLine = stdout.split("\n").filter(Boolean).pop() ?? "";
284
+ let challenge: RemoteWebPairingChallengeResponse;
285
+ try {
286
+ challenge = JSON.parse(lastLine) as RemoteWebPairingChallengeResponse;
287
+ } catch {
288
+ console.error(
289
+ "Error: could not parse the gateway's pairing mint output.",
290
+ );
291
+ process.exit(1);
292
+ }
293
+ if (!challenge.deviceCode || !challenge.userCode) {
294
+ console.error(
295
+ "Error: the gateway's pairing mint output is missing the challenge codes.",
296
+ );
297
+ process.exit(1);
298
+ }
299
+ return challenge;
300
+ }
301
+
302
+ /**
303
+ * Mint a challenge over the gateway's loopback HTTP route — the path every
304
+ * topology took before the gateway-CLI mint existed. Kept for remotes that
305
+ * are neither `local` nor `docker` (apple containers, SSH-managed hosts,
306
+ * cloud-managed), where no gateway-CLI execution surface is defined.
307
+ */
308
+ async function mintPairingChallengeOverLoopbackOrExit(
309
+ entry: AssistantEntry,
220
310
  publicBaseUrl: string,
221
311
  ): Promise<RemoteWebPairingChallengeResponse> {
312
+ // Mint over loopback (localUrl avoids mDNS for same-machine calls), then
313
+ // approve immediately: running this CLI on the host IS the local-presence
314
+ // proof, so the other device completes pairing in one step from the link.
315
+ const mintUrl = (
316
+ entry.localUrl ||
317
+ entry.runtimeUrl ||
318
+ `http://127.0.0.1:${GATEWAY_PORT}`
319
+ ).replace(/\/+$/, "");
222
320
  const response = await gatewayPostOrExit(
223
- gatewayUrl,
321
+ mintUrl,
224
322
  "/v1/remote-web/pairing-challenge",
225
323
  { publicBaseUrl } satisfies RemoteWebPairingChallengeRequest,
226
324
  );
227
- return (await response.json()) as RemoteWebPairingChallengeResponse;
325
+ const challenge: RemoteWebPairingChallengeResponse =
326
+ await response.json();
327
+ await gatewayPostOrExit(mintUrl, "/v1/remote-web/pairing-verification", {
328
+ userCode: challenge.userCode,
329
+ } satisfies RemoteWebPairingVerificationRequest);
330
+ return challenge;
228
331
  }
229
332
 
230
333
  /**
231
334
  * Approve a pending pairing challenge by its user code, the local-presence
232
335
  * proof for the device-code flow. Single owner of the pairing-verification
233
- * route and request body: the link flow approves via
234
- * {@link approveRemoteWebPairing} (generic exit on non-2xx), while
235
- * `--web-approve` calls this directly to inspect rejections and print mismatch
236
- * diagnostics (see {@link formatWebApproveFailure}).
336
+ * route and request body: `--web-approve` calls this directly to inspect
337
+ * rejections and print mismatch diagnostics (see
338
+ * {@link formatWebApproveFailure}); the link flow needs no separate
339
+ * verification call — the gateway-CLI mint approves on the spot.
237
340
  */
238
341
  async function postPairingVerification(
239
342
  gatewayUrl: string,
@@ -244,20 +347,6 @@ async function postPairingVerification(
244
347
  } satisfies RemoteWebPairingVerificationRequest);
245
348
  }
246
349
 
247
- /**
248
- * Approve the challenge this command just minted so one scan completes
249
- * pairing, exiting with a generic message on non-2xx.
250
- */
251
- async function approveRemoteWebPairing(
252
- gatewayUrl: string,
253
- userCode: string,
254
- ): Promise<RemoteWebPairingVerificationResponse> {
255
- const response = await exitOnHttpError(
256
- await postPairingVerification(gatewayUrl, userCode),
257
- );
258
- return (await response.json()) as RemoteWebPairingVerificationResponse;
259
- }
260
-
261
350
  export const pair = Object.assign(
262
351
  async function pair(): Promise<void> {
263
352
  const rawArgs = process.argv.slice(3);
@@ -355,6 +444,24 @@ export const pair = Object.assign(
355
444
  }
356
445
  }
357
446
 
447
+ // Minting requires host presence on this machine. Native and docker
448
+ // topologies qualify (the latter via docker exec); an assistant this
449
+ // machine only knows through `connect import` has no host presence here
450
+ // at all, and `cadet pair` never worked against it (its loopback gate
451
+ // denied remote mints). Refuse clearly instead of a confusing mint
452
+ // failure.
453
+ if (entry.cloud === "paired") {
454
+ console.error(
455
+ `Error: ${formatAssistantReference(entry)} was paired as a remote ` +
456
+ "assistant (cadet connect import), so this machine does not host it.",
457
+ );
458
+ console.error(
459
+ "Run `cadet pair` on the machine hosting the assistant, or approve " +
460
+ "a code it shows with `cadet pair --web-approve <code>`.",
461
+ );
462
+ process.exit(1);
463
+ }
464
+
358
465
  // Mint over loopback (localUrl avoids mDNS for same-machine calls), but
359
466
  // advertise a REACHABLE url in the link, since the loopback url would point
360
467
  // the other device at its own localhost. Prefer an explicit --url, then the
@@ -437,14 +544,10 @@ export const pair = Object.assign(
437
544
  }
438
545
  const publicBaseUrl = resolved.url;
439
546
 
440
- // Mint a challenge and immediately approve it: running this CLI on the host
441
- // IS the local-presence proof, so the other device completes pairing in one
442
- // step from the link alone.
443
- const challenge = await createRemoteWebPairingChallenge(
444
- mintUrl,
445
- publicBaseUrl,
446
- );
447
- await approveRemoteWebPairing(mintUrl, challenge.userCode);
547
+ // Mint a challenge (approved on the spot by the gateway-CLI invocation):
548
+ // running this CLI on the host IS the local-presence proof, so the other
549
+ // device completes pairing in one step from the link alone.
550
+ const challenge = await mintPairingChallengeOrExit(entry, publicBaseUrl);
448
551
  const pairUrl = buildRemoteWebPairingUrl(challenge);
449
552
  const displayName = assistantDisplayName(entry);
450
553
  const appUrl = appVariant
@@ -64,8 +64,54 @@ interface ColWidths {
64
64
  info: number;
65
65
  }
66
66
 
67
+ /**
68
+ * Terminal display width for table alignment: emoji render two cells wide
69
+ * (including ZWJ sequences like 🧑‍🚀 and VS16 pairs like ⚠️), while the ZWJ
70
+ * and variation selectors themselves render zero. `.length` counts UTF-16
71
+ * units and none of that, which is what broke the STATUS and INFO columns
72
+ * whenever an emoji appeared in an earlier one.
73
+ */
74
+ export function displayWidth(s: string): number {
75
+ const chars = [...s];
76
+ let width = 0;
77
+ // A ZWJ joins the previous and next glyphs into ONE grapheme (🧑‍🚀 renders
78
+ // as a single 2-cell astronaut), so the glyph after a ZWJ adds no width.
79
+ let joinedByZwj = false;
80
+ for (let i = 0; i < chars.length; i++) {
81
+ const cp = chars[i]?.codePointAt(0) ?? 0;
82
+ // Zero-width: the joiner, the variation selectors, skin-tone modifiers.
83
+ if (cp === 0x200d) {
84
+ joinedByZwj = true;
85
+ continue;
86
+ }
87
+ if (cp === 0xfe0e || cp === 0xfe0f) continue;
88
+ if (cp >= 0x1f3fb && cp <= 0x1f3ff) continue;
89
+ const next = chars[i + 1]?.codePointAt(0);
90
+ if (!joinedByZwj) {
91
+ // A VS16 after a narrow symbol forces emoji presentation — two cells.
92
+ width += isWideCodePoint(cp) || next === 0xfe0f ? 2 : 1;
93
+ }
94
+ joinedByZwj = false;
95
+ }
96
+ return width;
97
+ }
98
+
99
+ function isWideCodePoint(cp: number): boolean {
100
+ return (
101
+ (cp >= 0x1100 && cp <= 0x115f) || // Hangul Jamo
102
+ (cp >= 0x2e80 && cp <= 0x303e) || // CJK radicals and punctuation
103
+ (cp >= 0x3040 && cp <= 0x318f) || // Hiragana, Katakana, bopomofo
104
+ (cp >= 0xac00 && cp <= 0xd7a3) || // Hangul syllables
105
+ (cp >= 0xf900 && cp <= 0xfaff) || // CJK compatibility ideographs
106
+ (cp >= 0xfe30 && cp <= 0xfe6f) || // CJK compatibility forms
107
+ (cp >= 0xff00 && cp <= 0xff60) || // fullwidth forms
108
+ (cp >= 0xffe0 && cp <= 0xffe6) ||
109
+ cp >= 0x1f000 // emoji and symbol blocks (🟢, 🚀, 🧑, …)
110
+ );
111
+ }
112
+
67
113
  function pad(s: string, w: number): string {
68
- return s + " ".repeat(Math.max(0, w - s.length));
114
+ return s + " ".repeat(Math.max(0, w - displayWidth(s)));
69
115
  }
70
116
 
71
117
  function computeColWidths(
@@ -79,14 +125,17 @@ function computeColWidths(
79
125
  ): ColWidths {
80
126
  const all = [headers, ...rows];
81
127
  return {
82
- name: Math.max(...all.map((r) => r.name.length)),
83
- type: Math.max(...all.map((r) => r.type.length)),
84
- status: Math.max(...all.map((r) => r.status.length), "checking...".length),
85
- info: Math.max(...all.map((r) => r.info.length)),
128
+ name: Math.max(...all.map((r) => displayWidth(r.name))),
129
+ type: Math.max(...all.map((r) => displayWidth(r.type))),
130
+ status: Math.max(
131
+ ...all.map((r) => displayWidth(r.status)),
132
+ "checking...".length,
133
+ ),
134
+ info: Math.max(...all.map((r) => displayWidth(r.info))),
86
135
  };
87
136
  }
88
137
 
89
- function formatRow(r: TableRow, colWidths: ColWidths): string {
138
+ export function formatRow(r: TableRow, colWidths: ColWidths): string {
90
139
  return ` ${pad(r.name, colWidths.name)} ${pad(r.type, colWidths.type)} ${pad(r.status, colWidths.status)} ${r.info}`;
91
140
  }
92
141
 
@@ -459,7 +508,7 @@ async function getAssistantProcessRows(
459
508
  ): Promise<TableRow[]> {
460
509
  const cloud = entry.cloud;
461
510
 
462
- console.log(`Processes for ${formatAssistantReference(entry)} (${cloud}):`);
511
+ console.log(`Processes for ${formatAssistantReference(entry)} — ${cloud}:`);
463
512
  console.log(`Location: ${locationFor(entry)}\n`);
464
513
 
465
514
  if (cloud === "local") {
@@ -611,35 +660,23 @@ async function getRootManagedRows(): Promise<RootManagedRow[]> {
611
660
  }
612
661
 
613
662
  /**
614
- * Fold the CLI-managed process rows into ONE table with the assistant rows:
615
- * a process fronts an assistant when it recorded that assistant's id, and
616
- * those rows nest under it; anything unattributed (or fronting an assistant
617
- * this environment no longer lists) renders at the bottom, machine-level.
663
+ * Fold the CLI-managed process rows into ONE table with the assistant rows.
664
+ * Tunnel agents and the nginx edge are root resources of the machine, not
665
+ * children of whichever assistant they front — especially as the web bundle
666
+ * splits into /app and /ui — so every CLI-managed row renders machine-level.
618
667
  */
619
668
  async function assistantRowsWithRootProcesses(
620
669
  assistants: AssistantEntry[],
621
670
  healths: AssistantHealth[],
622
671
  activeAssistantId: string | null,
623
672
  ): Promise<TableRow[]> {
624
- const nestedByAssistant = new Map<string, TableRow[]>();
625
- const unattributed: TableRow[] = [];
626
- const knownIds = new Set(assistants.map((entry) => entry.assistantId));
627
- for (const managed of await getRootManagedRows()) {
628
- if (managed.assistantId && knownIds.has(managed.assistantId)) {
629
- const nested = nestedByAssistant.get(managed.assistantId) ?? [];
630
- nested.push({ ...managed.row, name: ` └ ${managed.row.name}` });
631
- nestedByAssistant.set(managed.assistantId, nested);
632
- } else {
633
- unattributed.push(managed.row);
634
- }
635
- }
673
+ const rootRows = (await getRootManagedRows()).map((managed) => managed.row);
636
674
 
637
675
  const rows: TableRow[] = [];
638
676
  assistants.forEach((entry, i) => {
639
677
  rows.push(formatAssistantListRow(entry, activeAssistantId, healths[i]!));
640
- rows.push(...(nestedByAssistant.get(entry.assistantId) ?? []));
641
678
  });
642
- rows.push(...unattributed);
679
+ rows.push(...rootRows);
643
680
  return rows;
644
681
  }
645
682
 
@@ -830,5 +867,5 @@ export const ps = Object.assign(
830
867
  }
831
868
  printTable(rows);
832
869
  },
833
- { description: "List assistants (or processes for a specific assistant)" },
870
+ { description: "List assistants" },
834
871
  );
@@ -474,7 +474,7 @@ async function restorePlatform(
474
474
  }
475
475
  }
476
476
  // Transient error (5xx, network) — retry
477
- console.warn(`Polling failed, retrying... (${msg})`);
477
+ console.warn(`Polling failed: ${msg}. Retrying...`);
478
478
  continue;
479
479
  }
480
480
 
@@ -680,7 +680,7 @@ export const restore = Object.assign(
680
680
  }
681
681
 
682
682
  const sizeMB = formatBundleSizeMb(bundleFileSizeBytes(fromPath));
683
- console.log(`Reading ${fromPath} (${sizeMB} MB)...`);
683
+ console.log(`Reading ${fromPath} — ${sizeMB} MB...`);
684
684
 
685
685
  // Detect topology and route platform assistants through Django import
686
686
  const cloud =
@@ -287,12 +287,12 @@ async function retireInner(): Promise<void> {
287
287
  console.log("");
288
288
  console.log("Delete an assistant instance and archive its data.");
289
289
  console.log(
290
- "With no name or ID, retires the active assistant (see `cadet use`). By default, retire prints the assistant name, ID, cloud, and runtime before asking for confirmation.",
290
+ "With no name or ID, retires the active assistant — see `cadet use`. By default, retire prints the assistant name, ID, cloud, and runtime before asking for confirmation.",
291
291
  );
292
292
  console.log("");
293
293
  console.log("Arguments:");
294
294
  console.log(
295
- " <name-or-id> Assistant display name or ID to retire (defaults to the active assistant)",
295
+ " <name-or-id> Assistant display name or ID to retire, defaults to the active assistant",
296
296
  );
297
297
  console.log("");
298
298
  console.log("Options:");
@@ -51,12 +51,12 @@ function parseArgs(): { name: string | null; version: string | null } {
51
51
  console.log("");
52
52
  console.log("Arguments:");
53
53
  console.log(
54
- " <name> Name of the assistant (default: active or only assistant)",
54
+ " <name> Name of the assistant, defaults to active or only assistant",
55
55
  );
56
56
  console.log("");
57
57
  console.log("Options:");
58
58
  console.log(
59
- " --version <version> Target version (optional for managed — omit to roll back to previous)",
59
+ " --version <version> Target version, optional for managed — omit to roll back to previous",
60
60
  );
61
61
  console.log("");
62
62
  console.log("Examples:");
@@ -71,7 +71,7 @@ export const sleep = Object.assign(
71
71
  console.log("");
72
72
  console.log("Arguments:");
73
73
  console.log(
74
- " <name> Name of the assistant to stop (default: active or only local)",
74
+ " <name> Name of the assistant to stop, defaults to active or only local",
75
75
  );
76
76
  console.log("");
77
77
  console.log("Options:");
@@ -88,7 +88,7 @@ export const sleep = Object.assign(
88
88
  " schedules) to finish before stopping. Waits as long as it",
89
89
  );
90
90
  console.log(
91
- " takes by default; pass a duration (e.g. --wait 90s, --wait 10m)",
91
+ " takes by default; pass a duration, e.g. --wait 90s or --wait 10m",
92
92
  );
93
93
  console.log(
94
94
  " to bound the wait. Ctrl-C cancels and leaves the assistant running.",
@@ -100,7 +100,7 @@ function printHelp(): void {
100
100
  console.log(
101
101
  "The source and target must be different environments. Same-environment",
102
102
  );
103
- console.log("transfers (e.g. local to local) are not supported.");
103
+ console.log("transfers such as local to local are not supported.");
104
104
  console.log("");
105
105
  console.log(
106
106
  "Before exporting, the source assistant is backed up. Platform sources",
@@ -159,7 +159,7 @@ function printHelp(): void {
159
159
  " Import data from my-local into existing docker assistant my-docker",
160
160
  );
161
161
  console.log(
162
- " (or hatch a new docker assistant named my-docker if it doesn't exist)",
162
+ " or hatch a new docker assistant named my-docker if it doesn't exist",
163
163
  );
164
164
  console.log("");
165
165
  console.log(" cadet teleport --from my-local --platform");
@@ -185,7 +185,6 @@ function printHelp(): void {
185
185
 
186
186
  export function parseArgs(argv: string[]): {
187
187
  from: string | undefined;
188
- to: string | undefined;
189
188
  targetEnv: "local" | "docker" | "platform" | undefined;
190
189
  targetName: string | undefined;
191
190
  keepSource: boolean;
@@ -193,7 +192,6 @@ export function parseArgs(argv: string[]): {
193
192
  help: boolean;
194
193
  } {
195
194
  let from: string | undefined;
196
- let to: string | undefined;
197
195
  let targetEnv: "local" | "docker" | "platform" | undefined;
198
196
  let targetName: string | undefined;
199
197
  let keepSource = false;
@@ -209,11 +207,6 @@ export function parseArgs(argv: string[]): {
209
207
  continue;
210
208
  }
211
209
  from = argv[++i];
212
- } else if (arg === "--to" && i + 1 < argv.length) {
213
- if (argv[i + 1].startsWith("--")) {
214
- continue;
215
- }
216
- to = argv[++i];
217
210
  } else if (
218
211
  arg === "--local" ||
219
212
  arg === "--docker" ||
@@ -242,7 +235,7 @@ export function parseArgs(argv: string[]): {
242
235
  process.exit(1);
243
236
  }
244
237
 
245
- return { from, to, targetEnv, targetName, keepSource, dryRun, help };
238
+ return { from, targetEnv, targetName, keepSource, dryRun, help };
246
239
  }
247
240
 
248
241
  // ---------------------------------------------------------------------------
@@ -415,7 +408,7 @@ async function backupSourceBeforeTeleport(
415
408
  cloud: string,
416
409
  displayName: string,
417
410
  ): Promise<void> {
418
- console.log(`Backing up ${displayName} (${cloud})...`);
411
+ console.log(`Backing up ${displayName} on ${cloud}...`);
419
412
  try {
420
413
  if (isCloudHome(cloud)) {
421
414
  const cloudToken = readCloudToken();
@@ -443,7 +436,7 @@ async function backupSourceBeforeTeleport(
443
436
  timeoutMs: LOCAL_SOURCE_BACKUP_TIMEOUT_MS,
444
437
  });
445
438
  if (!backupPath) {
446
- throw new Error("backup export failed (see warning above)");
439
+ throw new Error("backup export failed — see the warning above");
447
440
  }
448
441
  pruneOldBackups(entry.assistantId, 3, "pre-teleport");
449
442
  console.log(`Backup saved: ${backupPath}`);
@@ -470,7 +463,7 @@ async function exportFromAssistant(
470
463
  const cloudToken = readCloudToken();
471
464
  if (!cloudToken) {
472
465
  console.error(
473
- "Not logged in. Run 'cadet login' first (required for GCS-based teleport).",
466
+ "Not logged in. Run 'cadet login' first.",
474
467
  );
475
468
  process.exit(1);
476
469
  }
@@ -688,7 +681,7 @@ async function importToAssistant(
688
681
  const cloudToken = readCloudToken();
689
682
  if (!cloudToken) {
690
683
  console.error(
691
- "Not logged in. Run 'cadet login' first (required for GCS-based teleport).",
684
+ "Not logged in. Run 'cadet login' first.",
692
685
  );
693
686
  process.exit(1);
694
687
  }
@@ -990,7 +983,7 @@ export async function resolveOrHatchTarget(
990
983
  );
991
984
  process.exit(1);
992
985
  }
993
- console.log(`Target: ${targetName} (${targetEnv})`);
986
+ console.log(`Target: ${targetName} — ${targetEnv}`);
994
987
  return existing;
995
988
  }
996
989
 
@@ -1204,7 +1197,7 @@ async function tryInjectCloudCredentials(
1204
1197
  ): Promise<void> {
1205
1198
  const token = readCloudToken();
1206
1199
  if (!token) {
1207
- console.log(" Skipped cloud credential injection (not logged in).");
1200
+ console.log(" Skipped cloud credential injection — not logged in.");
1208
1201
  return;
1209
1202
  }
1210
1203
 
@@ -1253,7 +1246,6 @@ async function tryInjectCloudCredentials(
1253
1246
  bearerToken: entry.bearerToken,
1254
1247
  assistantApiKey,
1255
1248
  cloudAssistantId: registration.assistant.id,
1256
- cloudBaseUrl: cloudUrl,
1257
1249
  organizationId: orgId,
1258
1250
  userId: user.id,
1259
1251
  webhookSecret: registration.webhook_secret,
@@ -1277,7 +1269,7 @@ async function tryInjectCloudCredentials(
1277
1269
  export const teleport = Object.assign(
1278
1270
  async function teleport(): Promise<void> {
1279
1271
  const args = process.argv.slice(3);
1280
- const { from, to, targetEnv, targetName, keepSource, dryRun, help } =
1272
+ const { from, targetEnv, targetName, keepSource, dryRun, help } =
1281
1273
  parseArgs(args);
1282
1274
 
1283
1275
  if (help) {
@@ -1285,17 +1277,6 @@ export const teleport = Object.assign(
1285
1277
  process.exit(0);
1286
1278
  }
1287
1279
 
1288
- // Legacy --to flag deprecation
1289
- if (to) {
1290
- console.error("Error: --to is deprecated. Use environment flags instead:");
1291
- console.error(
1292
- " cadet teleport --from <source> --local|--docker|--platform [name]",
1293
- );
1294
- console.error("");
1295
- console.error("Run 'cadet teleport --help' for details.");
1296
- process.exit(1);
1297
- }
1298
-
1299
1280
  if (!from) {
1300
1281
  printHelp();
1301
1282
  process.exit(1);
@@ -1393,13 +1374,13 @@ export const teleport = Object.assign(
1393
1374
  ? fromEntry.runtimeUrl
1394
1375
  : undefined;
1395
1376
 
1396
- console.log(`Exporting from ${from} (${fromCloud})...`);
1377
+ console.log(`Exporting from ${from} on ${fromCloud}...`);
1397
1378
  const { bundleKey } = await exportFromAssistant(
1398
1379
  fromEntry,
1399
1380
  fromCloud,
1400
1381
  bundleCloudUrl,
1401
1382
  );
1402
- console.log(`Importing to ${existingTarget.assistantId} (${toCloud})...`);
1383
+ console.log(`Importing to ${existingTarget.assistantId} on ${toCloud}...`);
1403
1384
  await importToAssistant(
1404
1385
  existingTarget,
1405
1386
  toCloud,
@@ -1410,7 +1391,7 @@ export const teleport = Object.assign(
1410
1391
  } else {
1411
1392
  // No existing target — just describe what would happen
1412
1393
  console.log("Dry run summary:");
1413
- console.log(` Would export data from: ${from} (${fromCloud})`);
1394
+ console.log(` Would export data from: ${from} on ${fromCloud}`);
1414
1395
  console.log(` Would upload bundle via signed URL`);
1415
1396
  console.log(
1416
1397
  ` Would hatch a new ${targetEnv} assistant${targetName ? ` named '${targetName}'` : ""}`,
@@ -1482,7 +1463,7 @@ export const teleport = Object.assign(
1482
1463
  // resolveOrHatchTarget writes to the new entry).
1483
1464
  await backupSourceBeforeTeleport(fromEntry, fromCloud, from);
1484
1465
 
1485
- console.log(`Exporting from ${from} (${fromCloud})...`);
1466
+ console.log(`Exporting from ${from} on ${fromCloud}...`);
1486
1467
  const bundleCloudUrl = targetCloudUrl ?? getCloudUrl();
1487
1468
  const { bundleKey } = await exportFromAssistant(
1488
1469
  fromEntry,
@@ -1495,7 +1476,7 @@ export const teleport = Object.assign(
1495
1476
  const toCloud = toEntry.cloud;
1496
1477
 
1497
1478
  // Import from GCS
1498
- console.log(`Importing to ${toEntry.assistantId} (${toCloud})...`);
1479
+ console.log(`Importing to ${toEntry.assistantId} on ${toCloud}...`);
1499
1480
  await importToAssistant(
1500
1481
  toEntry,
1501
1482
  toCloud,
@@ -1555,7 +1536,7 @@ export const teleport = Object.assign(
1555
1536
  await backupSourceBeforeTeleport(fromEntry, fromCloud, from);
1556
1537
 
1557
1538
  // Export from source (bundle lives in GCS after this returns).
1558
- console.log(`Exporting from ${from} (${fromCloud})...`);
1539
+ console.log(`Exporting from ${from} on ${fromCloud}...`);
1559
1540
  const { bundleKey } = await exportFromAssistant(
1560
1541
  fromEntry,
1561
1542
  fromCloud,
@@ -1632,7 +1613,7 @@ export const teleport = Object.assign(
1632
1613
  }
1633
1614
 
1634
1615
  // Import to target (also GCS-driven)
1635
- console.log(`Importing to ${toEntry.assistantId} (${toCloud})...`);
1616
+ console.log(`Importing to ${toEntry.assistantId} on ${toCloud}...`);
1636
1617
  await importToAssistant(
1637
1618
  toEntry,
1638
1619
  toCloud,
@@ -1660,7 +1641,7 @@ export const teleport = Object.assign(
1660
1641
  removeAssistantEntry(fromEntry.assistantId);
1661
1642
  console.log(`Source assistant '${from}' retired.`);
1662
1643
  } else {
1663
- console.log(`Source assistant '${from}' kept (--keep-source).`);
1644
+ console.log(`Source assistant '${from}' kept via --keep-source.`);
1664
1645
  }
1665
1646
  }
1666
1647
 
@@ -71,7 +71,7 @@ function printHelp(): void {
71
71
  console.log("");
72
72
  console.log("Options:");
73
73
  console.log(
74
- " <name> Name of the assistant (defaults to active)",
74
+ " <name> Name of the assistant, defaults to active",
75
75
  );
76
76
  console.log(
77
77
  " --assistant <name> Explicit assistant name (alternative to positional)",