@mycadet/cli 0.12.9 → 0.12.10

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.
@@ -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,
@@ -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";
@@ -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}`);
@@ -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
@@ -660,35 +660,23 @@ async function getRootManagedRows(): Promise<RootManagedRow[]> {
660
660
  }
661
661
 
662
662
  /**
663
- * Fold the CLI-managed process rows into ONE table with the assistant rows:
664
- * a process fronts an assistant when it recorded that assistant's id, and
665
- * those rows nest under it; anything unattributed (or fronting an assistant
666
- * 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.
667
667
  */
668
668
  async function assistantRowsWithRootProcesses(
669
669
  assistants: AssistantEntry[],
670
670
  healths: AssistantHealth[],
671
671
  activeAssistantId: string | null,
672
672
  ): Promise<TableRow[]> {
673
- const nestedByAssistant = new Map<string, TableRow[]>();
674
- const unattributed: TableRow[] = [];
675
- const knownIds = new Set(assistants.map((entry) => entry.assistantId));
676
- for (const managed of await getRootManagedRows()) {
677
- if (managed.assistantId && knownIds.has(managed.assistantId)) {
678
- const nested = nestedByAssistant.get(managed.assistantId) ?? [];
679
- nested.push({ ...managed.row, name: ` └ ${managed.row.name}` });
680
- nestedByAssistant.set(managed.assistantId, nested);
681
- } else {
682
- unattributed.push(managed.row);
683
- }
684
- }
673
+ const rootRows = (await getRootManagedRows()).map((managed) => managed.row);
685
674
 
686
675
  const rows: TableRow[] = [];
687
676
  assistants.forEach((entry, i) => {
688
677
  rows.push(formatAssistantListRow(entry, activeAssistantId, healths[i]!));
689
- rows.push(...(nestedByAssistant.get(entry.assistantId) ?? []));
690
678
  });
691
- rows.push(...unattributed);
679
+ rows.push(...rootRows);
692
680
  return rows;
693
681
  }
694
682
 
@@ -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
  // ---------------------------------------------------------------------------
@@ -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);