@mycadet/cli 0.12.8 → 0.12.9

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 (53) 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 +2 -2
  6. package/src/__tests__/cloud-releases.test.ts +23 -13
  7. package/src/__tests__/ensure-binary.test.ts +182 -0
  8. package/src/__tests__/nginx-ingress.test.ts +3 -2
  9. package/src/__tests__/ngrok-spawn.test.ts +49 -0
  10. package/src/__tests__/ps-table.test.ts +79 -0
  11. package/src/__tests__/tailscale.test.ts +1 -1
  12. package/src/__tests__/teleport.test.ts +1 -1
  13. package/src/__tests__/tunnel.test.ts +21 -14
  14. package/src/__tests__/uninstall.test.ts +1 -1
  15. package/src/__tests__/wake.test.ts +2 -2
  16. package/src/commands/backup.ts +3 -3
  17. package/src/commands/completions.test.ts +104 -0
  18. package/src/commands/completions.ts +87 -0
  19. package/src/commands/exec.ts +7 -7
  20. package/src/commands/flags.ts +2 -2
  21. package/src/commands/hatch.ts +1 -1
  22. package/src/commands/logs.ts +7 -7
  23. package/src/commands/ps.ts +57 -8
  24. package/src/commands/restore.ts +2 -2
  25. package/src/commands/retire.ts +2 -2
  26. package/src/commands/rollback.ts +2 -2
  27. package/src/commands/sleep.ts +2 -2
  28. package/src/commands/teleport.ts +16 -16
  29. package/src/commands/terminal.ts +1 -1
  30. package/src/commands/tunnel.ts +78 -55
  31. package/src/commands/uninstall.ts +4 -4
  32. package/src/commands/upgrade.ts +8 -8
  33. package/src/commands/use.ts +1 -1
  34. package/src/commands/wake.ts +6 -6
  35. package/src/index.ts +15 -0
  36. package/src/lib/__tests__/docker.test.ts +168 -20
  37. package/src/lib/__tests__/job-polling.test.ts +1 -1
  38. package/src/lib/cloud-releases.ts +6 -4
  39. package/src/lib/docker.ts +150 -44
  40. package/src/lib/ensure-binary.ts +171 -0
  41. package/src/lib/flag-args.test.ts +1 -1
  42. package/src/lib/flag-args.ts +1 -1
  43. package/src/lib/guardian-token.ts +1 -1
  44. package/src/lib/hatch-local.ts +1 -1
  45. package/src/lib/job-polling.ts +1 -1
  46. package/src/lib/local.ts +1 -1
  47. package/src/lib/nginx-ingress.ts +2 -2
  48. package/src/lib/pair.ts +1 -1
  49. package/src/lib/sync-cloud-assistants.ts +2 -2
  50. package/src/lib/tunnels/cloudflare.ts +21 -11
  51. package/src/lib/tunnels/ngrok.ts +198 -108
  52. package/src/lib/tunnels/tailscale.ts +34 -4
  53. package/src/lib/upgrade-lifecycle.ts +5 -4
@@ -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:");
@@ -76,7 +76,7 @@ function parseArgs(): HatchArgs {
76
76
  " --watch Run assistant and gateway in watch mode (hot reload on source changes)",
77
77
  );
78
78
  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.",
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.",
80
80
  );
81
81
  console.log(
82
82
  " --keep-alive Stay alive after hatch, exit when gateway stops",
@@ -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
  }
@@ -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") {
@@ -830,5 +879,5 @@ export const ps = Object.assign(
830
879
  }
831
880
  printTable(rows);
832
881
  },
833
- { description: "List assistants (or processes for a specific assistant)" },
882
+ { description: "List assistants" },
834
883
  );
@@ -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");
@@ -415,7 +415,7 @@ async function backupSourceBeforeTeleport(
415
415
  cloud: string,
416
416
  displayName: string,
417
417
  ): Promise<void> {
418
- console.log(`Backing up ${displayName} (${cloud})...`);
418
+ console.log(`Backing up ${displayName} on ${cloud}...`);
419
419
  try {
420
420
  if (isCloudHome(cloud)) {
421
421
  const cloudToken = readCloudToken();
@@ -443,7 +443,7 @@ async function backupSourceBeforeTeleport(
443
443
  timeoutMs: LOCAL_SOURCE_BACKUP_TIMEOUT_MS,
444
444
  });
445
445
  if (!backupPath) {
446
- throw new Error("backup export failed (see warning above)");
446
+ throw new Error("backup export failed — see the warning above");
447
447
  }
448
448
  pruneOldBackups(entry.assistantId, 3, "pre-teleport");
449
449
  console.log(`Backup saved: ${backupPath}`);
@@ -470,7 +470,7 @@ async function exportFromAssistant(
470
470
  const cloudToken = readCloudToken();
471
471
  if (!cloudToken) {
472
472
  console.error(
473
- "Not logged in. Run 'cadet login' first (required for GCS-based teleport).",
473
+ "Not logged in. Run 'cadet login' first.",
474
474
  );
475
475
  process.exit(1);
476
476
  }
@@ -688,7 +688,7 @@ async function importToAssistant(
688
688
  const cloudToken = readCloudToken();
689
689
  if (!cloudToken) {
690
690
  console.error(
691
- "Not logged in. Run 'cadet login' first (required for GCS-based teleport).",
691
+ "Not logged in. Run 'cadet login' first.",
692
692
  );
693
693
  process.exit(1);
694
694
  }
@@ -990,7 +990,7 @@ export async function resolveOrHatchTarget(
990
990
  );
991
991
  process.exit(1);
992
992
  }
993
- console.log(`Target: ${targetName} (${targetEnv})`);
993
+ console.log(`Target: ${targetName} — ${targetEnv}`);
994
994
  return existing;
995
995
  }
996
996
 
@@ -1204,7 +1204,7 @@ async function tryInjectCloudCredentials(
1204
1204
  ): Promise<void> {
1205
1205
  const token = readCloudToken();
1206
1206
  if (!token) {
1207
- console.log(" Skipped cloud credential injection (not logged in).");
1207
+ console.log(" Skipped cloud credential injection — not logged in.");
1208
1208
  return;
1209
1209
  }
1210
1210
 
@@ -1393,13 +1393,13 @@ export const teleport = Object.assign(
1393
1393
  ? fromEntry.runtimeUrl
1394
1394
  : undefined;
1395
1395
 
1396
- console.log(`Exporting from ${from} (${fromCloud})...`);
1396
+ console.log(`Exporting from ${from} on ${fromCloud}...`);
1397
1397
  const { bundleKey } = await exportFromAssistant(
1398
1398
  fromEntry,
1399
1399
  fromCloud,
1400
1400
  bundleCloudUrl,
1401
1401
  );
1402
- console.log(`Importing to ${existingTarget.assistantId} (${toCloud})...`);
1402
+ console.log(`Importing to ${existingTarget.assistantId} on ${toCloud}...`);
1403
1403
  await importToAssistant(
1404
1404
  existingTarget,
1405
1405
  toCloud,
@@ -1410,7 +1410,7 @@ export const teleport = Object.assign(
1410
1410
  } else {
1411
1411
  // No existing target — just describe what would happen
1412
1412
  console.log("Dry run summary:");
1413
- console.log(` Would export data from: ${from} (${fromCloud})`);
1413
+ console.log(` Would export data from: ${from} on ${fromCloud}`);
1414
1414
  console.log(` Would upload bundle via signed URL`);
1415
1415
  console.log(
1416
1416
  ` Would hatch a new ${targetEnv} assistant${targetName ? ` named '${targetName}'` : ""}`,
@@ -1482,7 +1482,7 @@ export const teleport = Object.assign(
1482
1482
  // resolveOrHatchTarget writes to the new entry).
1483
1483
  await backupSourceBeforeTeleport(fromEntry, fromCloud, from);
1484
1484
 
1485
- console.log(`Exporting from ${from} (${fromCloud})...`);
1485
+ console.log(`Exporting from ${from} on ${fromCloud}...`);
1486
1486
  const bundleCloudUrl = targetCloudUrl ?? getCloudUrl();
1487
1487
  const { bundleKey } = await exportFromAssistant(
1488
1488
  fromEntry,
@@ -1495,7 +1495,7 @@ export const teleport = Object.assign(
1495
1495
  const toCloud = toEntry.cloud;
1496
1496
 
1497
1497
  // Import from GCS
1498
- console.log(`Importing to ${toEntry.assistantId} (${toCloud})...`);
1498
+ console.log(`Importing to ${toEntry.assistantId} on ${toCloud}...`);
1499
1499
  await importToAssistant(
1500
1500
  toEntry,
1501
1501
  toCloud,
@@ -1555,7 +1555,7 @@ export const teleport = Object.assign(
1555
1555
  await backupSourceBeforeTeleport(fromEntry, fromCloud, from);
1556
1556
 
1557
1557
  // Export from source (bundle lives in GCS after this returns).
1558
- console.log(`Exporting from ${from} (${fromCloud})...`);
1558
+ console.log(`Exporting from ${from} on ${fromCloud}...`);
1559
1559
  const { bundleKey } = await exportFromAssistant(
1560
1560
  fromEntry,
1561
1561
  fromCloud,
@@ -1632,7 +1632,7 @@ export const teleport = Object.assign(
1632
1632
  }
1633
1633
 
1634
1634
  // Import to target (also GCS-driven)
1635
- console.log(`Importing to ${toEntry.assistantId} (${toCloud})...`);
1635
+ console.log(`Importing to ${toEntry.assistantId} on ${toCloud}...`);
1636
1636
  await importToAssistant(
1637
1637
  toEntry,
1638
1638
  toCloud,
@@ -1660,7 +1660,7 @@ export const teleport = Object.assign(
1660
1660
  removeAssistantEntry(fromEntry.assistantId);
1661
1661
  console.log(`Source assistant '${from}' retired.`);
1662
1662
  } else {
1663
- console.log(`Source assistant '${from}' kept (--keep-source).`);
1663
+ console.log(`Source assistant '${from}' kept via --keep-source.`);
1664
1664
  }
1665
1665
  }
1666
1666
 
@@ -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)",