@crvouga/personal 0.0.0-stage → 0.1.0

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.
package/cli.mjs ADDED
@@ -0,0 +1,2327 @@
1
+ #!/usr/bin/env node
2
+ var __esm = (fn, res, err) => () => {
3
+ if (fn)
4
+ try {
5
+ res = fn(fn = 0);
6
+ } catch (e) {
7
+ err = [e];
8
+ }
9
+ if (err)
10
+ throw err[0];
11
+ return res;
12
+ };
13
+
14
+ // packages/personal-cli/src/search.ts
15
+ import Fuse from "fuse.js";
16
+ function searchCommands(commands, term) {
17
+ const query = term.trim().toLowerCase();
18
+ if (!query)
19
+ return commands;
20
+ const words = query.split(/\s+/);
21
+ const exact = commands.filter((command) => {
22
+ const text = [
23
+ command.id,
24
+ command.title,
25
+ command.description,
26
+ command.group,
27
+ ...command.keywords
28
+ ].join(" ").toLowerCase();
29
+ return words.every((word) => text.includes(word));
30
+ });
31
+ const fuzzy = new Fuse(commands, {
32
+ keys: ["id", "title", "keywords", "description"],
33
+ threshold: 0.35,
34
+ ignoreLocation: true
35
+ }).search(query).map((result) => result.item);
36
+ return [
37
+ ...new Map([...exact, ...fuzzy].map((command) => [command.id, command])).values()
38
+ ];
39
+ }
40
+ var init_search = () => {};
41
+
42
+ // packages/personal-cli/src/ui-data.ts
43
+ function record2(value) {
44
+ return typeof value === "object" && value !== null && !Array.isArray(value);
45
+ }
46
+ function clean(value, limit = 2000) {
47
+ return value.replace(/\x1b\][^\x07]*(?:\x07|\x1b\\)/g, "").replace(/\x1b\[[0-?]*[ -/]*[@-~]/g, "").replace(/[\x00-\x1f\x7f-\x9f\u202a-\u202e\u2066-\u2069]/g, " ").slice(0, limit);
48
+ }
49
+ function text(value, fallback = "") {
50
+ return typeof value === "string" ? clean(value) : fallback;
51
+ }
52
+ function quote(value) {
53
+ return /^[a-zA-Z0-9_./:=,@-]+$/.test(value) ? value : `'${value.replaceAll("'", "'\\''")}'`;
54
+ }
55
+ function callArgs(session, args) {
56
+ return [
57
+ ...args,
58
+ ...session.workspace ? ["--workspace", session.workspace] : [],
59
+ ...session.config ? ["--config", session.config] : []
60
+ ];
61
+ }
62
+ function reproduction(session, args) {
63
+ return [
64
+ "personal",
65
+ ...args,
66
+ "--workspace",
67
+ session.workspace ?? session.options.workspace,
68
+ "--config",
69
+ session.config ?? session.options.config,
70
+ "--json"
71
+ ].map(quote).join(" ");
72
+ }
73
+ function commandChoices(commands, term) {
74
+ return searchCommands(commands, term).map((command) => ({
75
+ name: `[${clean(command.group)}] ${clean(command.title)}`,
76
+ value: `command:${command.id}`,
77
+ description: `${clean(command.description)}${command.mutation ? " · confirmation required" : ""}`
78
+ }));
79
+ }
80
+ function reportChecks(data) {
81
+ if (!record2(data) || !Array.isArray(data["nodes"]))
82
+ return [];
83
+ return data["nodes"].flatMap(nodeChecks);
84
+ }
85
+ function nodeChecks(value) {
86
+ if (!record2(value) || !Array.isArray(value["checks"]))
87
+ return [];
88
+ const node = text(value["node"], "controller");
89
+ return value["checks"].flatMap((check) => {
90
+ if (!record2(check) || !isStatus(check["status"]))
91
+ return [];
92
+ const remedy = record2(check["remedy"]) ? check["remedy"] : {};
93
+ return [
94
+ {
95
+ node,
96
+ id: text(check["id"], "check"),
97
+ status: check["status"],
98
+ summary: text(check["summary"]),
99
+ remedy: text(remedy["action"]),
100
+ command: text(remedy["command"])
101
+ }
102
+ ];
103
+ });
104
+ }
105
+ function isStatus(value) {
106
+ return ["pass", "warn", "fail", "skip"].includes(String(value));
107
+ }
108
+ function isCancellation(error) {
109
+ if (!record2(error))
110
+ return false;
111
+ return ["ExitPromptError", "AbortPromptError", "AbortError"].includes(text(error["name"]));
112
+ }
113
+ function absolutePath2(value) {
114
+ return value.startsWith("/") && value.length <= 4096 && !/[\x00-\x1f\x7f]/.test(value) || "Enter an absolute POSIX path (no ~ or control characters).";
115
+ }
116
+ var init_ui_data = __esm(() => {
117
+ init_search();
118
+ });
119
+
120
+ // packages/personal-cli/src/ui-execute.ts
121
+ async function execute2(session, args, title) {
122
+ session.display.write(`
123
+ $ ${reproduction(session, args)}`);
124
+ const progress = session.display.progress(title);
125
+ try {
126
+ return await session.options.execute(callArgs(session, args));
127
+ } catch (error) {
128
+ if (isCancellation(error))
129
+ throw error;
130
+ return { exitCode: 1, data: { ok: false, code: "operation_failed" } };
131
+ } finally {
132
+ progress.stop();
133
+ }
134
+ }
135
+ function successful(outcome) {
136
+ return outcome.exitCode === 0 && !(record2(outcome.data) && outcome.data["ok"] === false);
137
+ }
138
+ function showOutcome(session, outcome) {
139
+ const checks = reportChecks(outcome.data);
140
+ const incomplete = checks.filter((check) => ["warn", "skip"].includes(check.status)).length;
141
+ const state = successful(outcome) ? incomplete ? "WARNING" : "SUCCESS" : "FAILED";
142
+ const counts = ["pass", "warn", "fail", "skip"].map((status) => `${checks.filter((check) => check.status === status).length} ${status}`);
143
+ session.display.panel(state, checks.length ? counts.join(" · ") : outcomeMessage(outcome));
144
+ if (!successful(outcome))
145
+ session.display.write("No automatic repair was attempted. Review the command, context and setup.");
146
+ }
147
+ function outcomeMessage(outcome) {
148
+ if (!successful(outcome))
149
+ return failureMessage(outcome.data);
150
+ if (record2(outcome.data) && outcome.data["applied"] === false) {
151
+ return "Preview only — no changes applied. Review before explicitly authorizing a write.";
152
+ }
153
+ return "Command completed. Warnings and skipped checks are not passes.";
154
+ }
155
+ function failureMessage(data) {
156
+ if (!record2(data))
157
+ return "Operation failed. Raw errors and subprocess output are intentionally hidden.";
158
+ const messages = {
159
+ invalid_usage: "Configuration or arguments need attention. Use Fleet setup, validate, or session context.",
160
+ config_missing: "No roster found. Add a node in Fleet setup, or run controller-only diagnostics.",
161
+ operation_failed: "Operation failed safely. Check connectivity and configuration before retrying."
162
+ };
163
+ return messages[String(data["code"])] ?? "Operation did not complete. Review setup and the noninteractive command.";
164
+ }
165
+ var init_ui_execute = __esm(() => {
166
+ init_ui_data();
167
+ });
168
+
169
+ // packages/personal-cli/src/ui-reports.ts
170
+ async function inspectChecks(session, data) {
171
+ const checks = reportChecks(data);
172
+ let node = "*";
173
+ let status = "*";
174
+ while (true) {
175
+ const visible = checks.filter((check) => (node === "*" || check.node === node) && (status === "*" || check.status === status));
176
+ const choice = await session.prompts.search(`Report · ${visible.length}/${checks.length} checks · node ${node} · status ${status}`, (term) => checkChoices(visible, term));
177
+ if (choice === "back")
178
+ return;
179
+ if (choice === "node")
180
+ node = await nodeFilter(session, checks);
181
+ else if (choice === "status")
182
+ status = await statusFilter(session);
183
+ else {
184
+ const check = visible[Number(choice)];
185
+ if (check)
186
+ showCheck(session, check);
187
+ }
188
+ }
189
+ }
190
+ function checkChoices(checks, term) {
191
+ const choices = checks.flatMap((check, index) => {
192
+ const name = `${check.status.toUpperCase()} · ${check.node} · ${check.id}`;
193
+ return `${name} ${check.summary}`.toLowerCase().includes(term.toLowerCase()) ? [{ name, value: String(index), description: check.summary }] : [];
194
+ });
195
+ return [
196
+ ...choices.length ? choices : [
197
+ {
198
+ name: "No matching checks — change search or filters",
199
+ value: "none"
200
+ }
201
+ ],
202
+ { name: "Filter by node", value: "node" },
203
+ { name: "Filter by status", value: "status" },
204
+ { name: "Back to result", value: "back" }
205
+ ];
206
+ }
207
+ function nodeFilter(session, checks) {
208
+ return session.prompts.select("Node filter", [
209
+ { name: "All nodes", value: "*" },
210
+ ...[...new Set(checks.map((check) => check.node))].map((node) => ({
211
+ name: node,
212
+ value: node
213
+ }))
214
+ ]);
215
+ }
216
+ function statusFilter(session) {
217
+ return session.prompts.select("Status filter", [
218
+ { name: "All statuses (including warnings and skips)", value: "*" },
219
+ ...["fail", "warn", "skip", "pass"].map((status) => ({
220
+ name: status.toUpperCase(),
221
+ value: status
222
+ }))
223
+ ]);
224
+ }
225
+ function showCheck(session, check) {
226
+ session.display.panel(`${check.status.toUpperCase()} · ${check.node} · ${check.id}`, check.summary);
227
+ session.display.write(`Remedy: ${check.remedy || "No automated remedy. Review this check manually."}`);
228
+ if (check.command)
229
+ session.display.write(`Suggested operator command (not executed):
230
+ ${check.command}`);
231
+ session.display.write("Remedies are guidance only. Credentials, SSH and host-key configuration stay operator-owned.");
232
+ }
233
+ async function inspectDetails(session, data) {
234
+ const sections = detailSections(data);
235
+ if (!sections.length) {
236
+ session.display.write("No additional safe display fields. Use the reproducible command for the structured document.");
237
+ return;
238
+ }
239
+ let next = true;
240
+ while (next) {
241
+ const selection = await session.prompts.select("Details", [
242
+ ...sections.map((section, index) => ({
243
+ name: section.title,
244
+ value: String(index)
245
+ })),
246
+ { name: "Back to result", value: "back" }
247
+ ]);
248
+ const section = sections[Number(selection)];
249
+ if (selection === "back")
250
+ next = false;
251
+ else if (section)
252
+ session.display.panel(section.title, section.body);
253
+ }
254
+ }
255
+ function detailSections(data) {
256
+ if (!record2(data))
257
+ return [];
258
+ const sections = [
259
+ "help",
260
+ "guide",
261
+ "next",
262
+ "authorize",
263
+ "path",
264
+ "sshTemplate",
265
+ "workspaceCommand",
266
+ "verifyCommand",
267
+ "invocation",
268
+ "discovery"
269
+ ].flatMap((key) => typeof data[key] === "string" ? [{ title: key, body: text(data[key]) }] : []);
270
+ for (const key of ["steps", "targets", "safety", "examples"]) {
271
+ if (Array.isArray(data[key]))
272
+ sections.push(...data[key].flatMap((value, index) => typeof value === "string" ? [{ title: `${key} · ${index + 1}`, body: text(value) }] : []));
273
+ }
274
+ if (Array.isArray(data["nodes"]))
275
+ sections.push(...nodeSections(data["nodes"]));
276
+ if (Array.isArray(data["commands"]))
277
+ sections.push(...commandSections(data["commands"]));
278
+ for (const key of ["configuration", "output", "coverage"]) {
279
+ if (record2(data[key]))
280
+ sections.push({ title: key, body: safeFields(data[key]) });
281
+ }
282
+ if (typeof data["$schema"] === "string")
283
+ sections.push({
284
+ title: "JSON schema",
285
+ body: JSON.stringify(data, null, 2)
286
+ });
287
+ return sections;
288
+ }
289
+ function commandSections(commands) {
290
+ return commands.flatMap((command) => {
291
+ if (!record2(command))
292
+ return [];
293
+ return [
294
+ {
295
+ title: text(command["id"], "Command"),
296
+ body: [
297
+ text(command["title"]),
298
+ text(command["description"]),
299
+ ...Array.isArray(command["examples"]) ? command["examples"].map((value) => text(value)) : []
300
+ ].filter(Boolean).join(`
301
+ `)
302
+ }
303
+ ];
304
+ });
305
+ }
306
+ function safeFields(value) {
307
+ return Object.entries(value).flatMap(([key, field]) => typeof field === "string" ? [`${key}: ${text(field)}`] : []).join(`
308
+ `);
309
+ }
310
+ function nodeSections(nodes) {
311
+ return nodes.flatMap((node) => {
312
+ if (!record2(node))
313
+ return [];
314
+ const id = text(node["id"], text(node["node"], "Node"));
315
+ const body = ["id", "ssh", "workspace", "runtime"].flatMap((key) => typeof node[key] === "string" ? [`${key}: ${text(node[key])}`] : []);
316
+ if (Array.isArray(node["required"]))
317
+ body.push(`required: ${node["required"].map((value) => text(value)).join(", ") || "none"}`);
318
+ return [
319
+ ...body.length ? [{ title: id, body: body.join(`
320
+ `) }] : [],
321
+ ...detailSections({
322
+ steps: node["steps"],
323
+ sshTemplate: node["sshTemplate"],
324
+ verifyCommand: node["verifyCommand"]
325
+ }).map((section) => ({
326
+ title: `${id} · ${section.title}`,
327
+ body: section.body
328
+ }))
329
+ ];
330
+ });
331
+ }
332
+ var init_ui_reports = __esm(() => {
333
+ init_ui_data();
334
+ });
335
+
336
+ // packages/personal-cli/src/ui-targets.ts
337
+ async function chooseTargets(session, action) {
338
+ const selection = await session.prompts.select("Choose target scope", [
339
+ ...action === "onboard" ? [] : [
340
+ {
341
+ name: "Controller only / local",
342
+ value: "local",
343
+ description: "No roster or SSH required; uses this session workspace."
344
+ },
345
+ {
346
+ name: "All roster nodes",
347
+ value: "all",
348
+ description: action === "doctor" ? "Controller + every configured node; read-only." : "Every configured remote checkout."
349
+ }
350
+ ],
351
+ { name: "Select roster nodes…", value: "selected" },
352
+ { name: "Back to palette", value: "back" }
353
+ ]);
354
+ if (selection === "back")
355
+ return;
356
+ if (selection === "local")
357
+ return ["--local"];
358
+ const nodes = await availableNodes(session);
359
+ if (!nodes.length)
360
+ return missingRoster2(session, action);
361
+ if (selection === "all")
362
+ return [];
363
+ const selected = await session.prompts.checkbox("Select nodes · space to toggle, enter to continue", nodes);
364
+ if (!selected.length) {
365
+ session.display.write("No nodes selected. Nothing ran; returning to the palette.");
366
+ return;
367
+ }
368
+ return selected.flatMap((id) => ["--node", id]);
369
+ }
370
+ async function availableNodes(session) {
371
+ const outcome = await execute2(session, ["fleet", "nodes"], "Reading local roster (no SSH)");
372
+ if (!successful(outcome)) {
373
+ showOutcome(session, outcome);
374
+ return [];
375
+ }
376
+ if (!record2(outcome.data) || !Array.isArray(outcome.data["nodes"]))
377
+ return [];
378
+ return outcome.data["nodes"].flatMap((node) => {
379
+ if (!record2(node) || typeof node["id"] !== "string")
380
+ return [];
381
+ return [
382
+ {
383
+ name: text(node["id"]),
384
+ value: node["id"],
385
+ description: `${text(node["ssh"])} · ${text(node["workspace"])}`
386
+ }
387
+ ];
388
+ });
389
+ }
390
+ async function missingRoster2(session, action) {
391
+ session.display.panel("Fleet setup needed", [
392
+ "No usable roster nodes. Return to Fleet setup / add node to register an existing computer.",
393
+ "Fleet init creates an EXAMPLE only; saving it requires --apply --yes. Edit its aliases and paths before use.",
394
+ "Controller-only doctor works without a roster or SSH. No login, host-key change or repair is automatic."
395
+ ].join(`
396
+ `));
397
+ if (action !== "doctor")
398
+ return;
399
+ const choice = await session.prompts.select("Continue without a roster?", [
400
+ { name: "Run controller-only diagnostics", value: "local" },
401
+ { name: "Back to palette / Fleet setup", value: "back" }
402
+ ]);
403
+ return choice === "local" ? ["--local"] : undefined;
404
+ }
405
+ var init_ui_targets = __esm(() => {
406
+ init_ui_execute();
407
+ init_ui_data();
408
+ });
409
+
410
+ // packages/personal-cli/src/ui-actions.ts
411
+ async function runCommand2(session, command) {
412
+ session.display.panel(clean(command.title), clean(command.description));
413
+ const args = await actionArgs(session, command);
414
+ if (!args)
415
+ return;
416
+ const preview = args[0] === "fleet" && ["sync", "init"].includes(args[1] ?? "");
417
+ if (args[1] === "init")
418
+ session.display.write("EXAMPLE ONLY: init previews a starter roster, not a configured fleet. Saving requires --apply --yes.");
419
+ const mutation = !preview && (command.mutation || args.includes("--apply"));
420
+ if (mutation && !await confirmMutation(session, args))
421
+ return;
422
+ await runWithResults(session, args, command.title, { preview, mutation });
423
+ }
424
+ async function actionArgs(session, command) {
425
+ const args = [...command.args];
426
+ if (args[0] !== "fleet")
427
+ return args;
428
+ if (args.includes("--local"))
429
+ return args;
430
+ const action = args[1] ?? "";
431
+ if (["sync", "init"].includes(action)) {
432
+ const preview = args.filter((arg) => !["--apply", "--yes"].includes(arg));
433
+ if (action === "init")
434
+ return preview;
435
+ const targets = await chooseTargets(session, action);
436
+ return targets ? [...preview, ...targets] : undefined;
437
+ }
438
+ if (!["doctor", "onboard"].includes(action))
439
+ return args;
440
+ const targets = await chooseTargets(session, action);
441
+ return targets ? [...args, ...targets] : undefined;
442
+ }
443
+ async function runWithResults(session, initialArgs, title, settings = {}) {
444
+ let args = initialArgs;
445
+ let outcome = await execute2(session, args, title);
446
+ let preview = settings.preview === true;
447
+ let mutation = settings.mutation === true;
448
+ while (true) {
449
+ showOutcome(session, outcome);
450
+ const selection = await session.prompts.select("Next step", resultChoices(outcome, preview));
451
+ if (selection === "back")
452
+ return;
453
+ await inspectSelection(session, outcome, selection);
454
+ if (selection === "apply") {
455
+ const appliedArgs = [...args, "--apply", "--yes"];
456
+ if (!await confirmMutation(session, appliedArgs))
457
+ continue;
458
+ args = appliedArgs;
459
+ preview = false;
460
+ mutation = true;
461
+ outcome = await execute2(session, args, title);
462
+ }
463
+ if (selection === "retry") {
464
+ if (mutation && !await confirmMutation(session, args))
465
+ continue;
466
+ outcome = await execute2(session, args, title);
467
+ }
468
+ }
469
+ }
470
+ async function inspectSelection(session, outcome, selection) {
471
+ if (selection === "checks")
472
+ await inspectChecks(session, outcome.data);
473
+ if (selection === "details")
474
+ await inspectDetails(session, outcome.data);
475
+ }
476
+ function resultChoices(outcome, preview) {
477
+ return [
478
+ ...reportChecks(outcome.data).length ? [{ name: "Explore checks and remedies…", value: "checks" }] : [],
479
+ { name: "Inspect structured details / plan…", value: "details" },
480
+ ...preview && successful(outcome) ? [{ name: "Review and explicitly apply…", value: "apply" }] : [],
481
+ ...!successful(outcome) ? [{ name: "Retry command…", value: "retry" }] : [],
482
+ { name: "Back to palette", value: "back" }
483
+ ];
484
+ }
485
+ async function confirmMutation(session, args) {
486
+ const action = args[1];
487
+ const warning = action === "sync" ? "Sync runs trusted checkout lifecycle scripts and installs tools/dependencies. Failure can leave partial changes; remote installers may continue after disconnect. Inspect the node before retrying. No automatic repairs or credentials." : "This writes the local roster only. No cloud resources, SSH configuration, credentials or host keys are changed.";
488
+ session.display.panel("Explicit approval required", [
489
+ action === "init" ? "Create an EXAMPLE roster only. Replace sample aliases and paths before diagnostics." : warning,
490
+ `Workspace: ${clean(session.workspace ?? session.options.workspace)}`,
491
+ `Config: ${clean(session.config ?? session.options.config)}`,
492
+ `$ ${reproduction(session, args)}`
493
+ ].join(`
494
+ `));
495
+ return session.prompts.confirm("Apply this exact command? (default: no)");
496
+ }
497
+ var init_ui_actions = __esm(() => {
498
+ init_ui_execute();
499
+ init_ui_data();
500
+ init_ui_reports();
501
+ init_ui_targets();
502
+ });
503
+
504
+ // packages/personal-cli/src/ui-wizard.ts
505
+ async function addNode2(session) {
506
+ showIntro(session);
507
+ await registerNode(session);
508
+ }
509
+ function showIntro(session) {
510
+ session.display.panel("Fleet setup / add node", [
511
+ "Register an EXISTING computer in your local secret-free roster.",
512
+ "Use a previously verified SSH alias. This wizard never connects, provisions, logs in or changes host keys.",
513
+ "Cancel with Ctrl+C. No write occurs until you approve the preview."
514
+ ].join(`
515
+ `));
516
+ }
517
+ async function registerNode(session) {
518
+ const id = await session.prompts.input("Node ID (lowercase, e.g. dev-linux)", "", validId);
519
+ const alias = await session.prompts.input("Existing SSH alias / hostname", id, validAlias);
520
+ const workspace = await session.prompts.input("Absolute POSIX workspace on the node", "", absolutePath2);
521
+ const runtime = await session.prompts.input("Optional absolute Node/Bun executable (blank = node on SSH PATH)", "", (value) => value === "" || absolutePath2(value));
522
+ const capabilities = await session.prompts.checkbox("Required capabilities · space to toggle", CAPABILITIES2.map((capability) => ({
523
+ name: capability,
524
+ value: capability,
525
+ checked: capability !== "vscode"
526
+ })));
527
+ const args = [
528
+ "fleet",
529
+ "add",
530
+ "--id",
531
+ id,
532
+ "--ssh",
533
+ alias,
534
+ "--path",
535
+ workspace,
536
+ ...runtime ? ["--runtime", runtime] : [],
537
+ "--require",
538
+ capabilities.join(",") || "none",
539
+ "--apply",
540
+ "--yes"
541
+ ];
542
+ session.display.table(["Field", "Preview"], [
543
+ ["ID", clean(id)],
544
+ ["SSH", clean(alias)],
545
+ ["Workspace", clean(workspace)],
546
+ ["Runtime", clean(runtime) || "node (remote non-login PATH)"],
547
+ ["Required", capabilities.join(", ") || "none"]
548
+ ]);
549
+ session.display.write(`$ ${reproduction(session, args)}`);
550
+ if (!await confirmMutation(session, args)) {
551
+ session.display.write("Cancelled — no roster was written.");
552
+ return;
553
+ }
554
+ await runWithResults(session, args, "Saving roster node (no SSH)", {
555
+ mutation: true
556
+ });
557
+ session.display.write("Next: fleet validate, then fleet onboard for operator-owned SSH / authentication guidance.");
558
+ }
559
+ function validId(value) {
560
+ return /^[a-z][a-z0-9-]{0,62}$/.test(value) || "Use 1–63 lowercase letters, digits or hyphens; start with a letter.";
561
+ }
562
+ function validAlias(value) {
563
+ return /^[a-zA-Z0-9][a-zA-Z0-9._-]{0,252}$/.test(value) || "Use an existing plain SSH alias/hostname; no user@host, options or ports.";
564
+ }
565
+ var CAPABILITIES2;
566
+ var init_ui_wizard = __esm(() => {
567
+ init_ui_actions();
568
+ init_ui_data();
569
+ CAPABILITIES2 = ["github", "vault", "t3", "vscode", "toolchain"];
570
+ });
571
+
572
+ // packages/personal-cli/src/ui-controller.ts
573
+ async function runInteractive(options, prompts, display) {
574
+ const session = { options, prompts, display };
575
+ showHeader(session);
576
+ let running = true;
577
+ while (running) {
578
+ try {
579
+ const choice = await prompts.search("Command palette · type to search · arrows + enter", (term) => paletteChoices(session, term));
580
+ running = await dispatch(session, choice);
581
+ } catch (error) {
582
+ if (isCancellation(error)) {
583
+ display.write(`
584
+ Goodbye. No further actions will run.`);
585
+ throw error;
586
+ }
587
+ display.write("The interactive session could not continue safely. Use personal commands --json for noninteractive help.");
588
+ throw new Error("Interactive session failed safely.");
589
+ }
590
+ }
591
+ display.write("Goodbye. Session context was not saved.");
592
+ }
593
+ function showHeader(session) {
594
+ session.display.panel(`personal · ${clean(session.options.version)} · operator console`, [
595
+ "Your developer fleet, without the guesswork.",
596
+ `Workspace: ${clean(session.workspace ?? session.options.workspace)}`,
597
+ `Config: ${clean(session.config ?? session.options.config)}`,
598
+ "Read-only first · explicit writes · no automatic repairs",
599
+ "Search IDs, keywords or descriptions. Ctrl+C exits safely. Nothing is cleared."
600
+ ].join(`
601
+ `));
602
+ }
603
+ function paletteChoices(session, term) {
604
+ const commands = commandChoices(session.options.commands, term);
605
+ const navigation = [
606
+ {
607
+ name: "Fleet setup / add node…",
608
+ value: "setup",
609
+ description: "Register an existing node; preview + explicit approval before saving."
610
+ },
611
+ {
612
+ name: "Session workspace / config…",
613
+ value: "context",
614
+ description: "Session-only overrides; no hidden configuration writes."
615
+ }
616
+ ].filter((choice) => `${choice.name} ${choice.description}`.toLowerCase().includes(term.toLowerCase().trim()));
617
+ const groups = term.trim() ? [] : [
618
+ ...new Set(session.options.commands.map((command) => command.group))
619
+ ].map((group) => ({
620
+ name: `Browse ${clean(group)}…`,
621
+ value: `group:${group}`
622
+ }));
623
+ return [
624
+ ...groups,
625
+ ...navigation,
626
+ ...commands,
627
+ ...!commands.length && !navigation.length && term.trim() ? [
628
+ {
629
+ name: "No matching commands — change search or go back",
630
+ value: "none"
631
+ }
632
+ ] : [],
633
+ { name: "Exit", value: "exit" }
634
+ ];
635
+ }
636
+ async function dispatch(session, choice) {
637
+ if (choice === "exit")
638
+ return false;
639
+ if (choice === "setup")
640
+ await addNode2(session);
641
+ if (choice === "context")
642
+ await changeContext(session);
643
+ if (choice.startsWith("group:"))
644
+ await browseGroup(session, choice.slice(6));
645
+ if (choice.startsWith("command:"))
646
+ await openCommand(session, choice.slice(8));
647
+ return true;
648
+ }
649
+ async function browseGroup(session, group) {
650
+ const commands = session.options.commands.filter((command) => command.group === group);
651
+ while (true) {
652
+ const choice = await session.prompts.search(`${clean(group)} · searchable submenu`, (term) => {
653
+ const choices = commandChoices(commands, term);
654
+ return [
655
+ ...choices.length ? choices : [{ name: "No matches — try another keyword", value: "none" }],
656
+ { name: "Back to all commands", value: "back" }
657
+ ];
658
+ });
659
+ if (choice === "back")
660
+ return;
661
+ if (choice.startsWith("command:"))
662
+ await openCommand(session, choice.slice(8));
663
+ }
664
+ }
665
+ async function openCommand(session, id) {
666
+ const command = session.options.commands.find((entry) => entry.id === id);
667
+ if (!command)
668
+ return;
669
+ if (command.args[0] === "fleet" && command.args[1] === "add")
670
+ await addNode2(session);
671
+ else
672
+ await runCommand2(session, command);
673
+ }
674
+ async function changeContext(session) {
675
+ session.display.write("Session only. Blank inputs reset to startup defaults; no file is written.");
676
+ const workspace = await session.prompts.input("Workspace override", session.workspace ?? "", validContext);
677
+ const config = await session.prompts.input("Config override", session.config ?? "", validContext);
678
+ if (workspace)
679
+ session.workspace = workspace;
680
+ else
681
+ delete session.workspace;
682
+ if (config)
683
+ session.config = config;
684
+ else
685
+ delete session.config;
686
+ showHeader(session);
687
+ }
688
+ function validContext(value) {
689
+ return !/[\x00-\x1f\x7f]/.test(value) || "Control characters are not allowed in paths.";
690
+ }
691
+ var init_ui_controller = __esm(() => {
692
+ init_ui_actions();
693
+ init_ui_data();
694
+ init_ui_wizard();
695
+ });
696
+
697
+ // packages/personal-cli/src/ui-terminal.ts
698
+ import { checkbox, confirm, input, search, select } from "@inquirer/prompts";
699
+ import { Chalk } from "chalk";
700
+ import ora from "ora";
701
+ import boxen from "boxen";
702
+ import Table from "cli-table3";
703
+ function plainTerminal() {
704
+ return process.env["NO_COLOR"] !== undefined || process.env["TERM"] === "dumb";
705
+ }
706
+ function pageSize() {
707
+ return Math.max(3, Math.min(10, (process.stderr.rows || 24) - 10));
708
+ }
709
+ function terminalPrompts() {
710
+ const context = { output: process.stderr, clearPromptOnDone: false };
711
+ return {
712
+ select: (message, choices) => select({ message, choices, pageSize: pageSize(), loop: false }, context),
713
+ search: (message, source) => search({
714
+ message,
715
+ source: (term) => source(term ?? ""),
716
+ pageSize: pageSize(),
717
+ theme: { style: { highlight: (value) => value } }
718
+ }, context),
719
+ checkbox: (message, choices) => checkbox({ message, choices, pageSize: pageSize(), loop: false }, context),
720
+ input: (message, initial = "", validate = () => true) => input({ message, default: initial, validate }, context),
721
+ confirm: (message) => confirm({ message, default: false }, context)
722
+ };
723
+ }
724
+ function terminalDisplay() {
725
+ const color = new Chalk({
726
+ level: !plainTerminal() && process.stderr.isTTY ? 1 : 0
727
+ });
728
+ const write = (message) => {
729
+ process.stderr.write(`${message}
730
+ `);
731
+ };
732
+ return {
733
+ write,
734
+ panel: (title, body) => {
735
+ const content = `${color.bold(clean(title))}
736
+ ${body}`;
737
+ if ((process.stderr.columns || 80) < 48 || plainTerminal()) {
738
+ write(`
739
+ ${content}
740
+ `);
741
+ return;
742
+ }
743
+ write(boxen(content, {
744
+ padding: 1,
745
+ borderStyle: "round",
746
+ ...color.level ? { borderColor: "cyan" } : {},
747
+ width: Math.min(process.stderr.columns || 80, 96)
748
+ }));
749
+ },
750
+ table: (headers, rows) => drawTable(write, headers, rows),
751
+ progress: (message) => {
752
+ const spinner = ora({
753
+ text: message,
754
+ stream: process.stderr,
755
+ isEnabled: Boolean(process.stderr.isTTY) && !plainTerminal(),
756
+ isSilent: !process.stderr.isTTY || plainTerminal(),
757
+ discardStdin: false
758
+ });
759
+ if (spinner.isEnabled)
760
+ spinner.start();
761
+ else
762
+ write(`… ${message}`);
763
+ return {
764
+ stop: () => {
765
+ spinner.stop();
766
+ }
767
+ };
768
+ }
769
+ };
770
+ }
771
+ function drawTable(write, headers, rows) {
772
+ if ((process.stderr.columns || 80) < 64 || plainTerminal()) {
773
+ rows.forEach((row) => write(row.map((cell) => clean(cell)).join(" · ")));
774
+ return;
775
+ }
776
+ const width = Math.floor(((process.stderr.columns || 80) - headers.length * 3 - 1) / headers.length);
777
+ const table = new Table({
778
+ head: headers,
779
+ colWidths: headers.map(() => Math.max(8, width)),
780
+ wordWrap: true,
781
+ style: { head: [], border: [] }
782
+ });
783
+ table.push(...rows.map((row) => row.map((cell) => clean(cell))));
784
+ write(table.toString());
785
+ }
786
+ var init_ui_terminal = __esm(() => {
787
+ init_ui_data();
788
+ });
789
+
790
+ // packages/personal-cli/src/ui.ts
791
+ async function startInteractive(options) {
792
+ await runInteractive(options, terminalPrompts(), terminalDisplay());
793
+ }
794
+ var init_ui = __esm(() => {
795
+ init_ui_controller();
796
+ init_ui_terminal();
797
+ });
798
+
799
+ // packages/personal-cli/src/settings.ts
800
+ import { access } from "node:fs/promises";
801
+ import { dirname, join, resolve } from "node:path";
802
+ import { homedir } from "node:os";
803
+ async function exists(path) {
804
+ try {
805
+ await access(path);
806
+ return true;
807
+ } catch {
808
+ return false;
809
+ }
810
+ }
811
+ async function findUp(start, name) {
812
+ let current = resolve(start);
813
+ while (true) {
814
+ if (await exists(join(current, name)))
815
+ return join(current, name);
816
+ const parent = dirname(current);
817
+ if (parent === current)
818
+ return null;
819
+ current = parent;
820
+ }
821
+ }
822
+ async function resolveSettings(overrides = {}, cwd = process.cwd(), env = process.env) {
823
+ const workspace = resolve(overrides.workspace ?? env["PERSONAL_WORKSPACE"] ?? dirname(await findUp(cwd, "mise.toml") ?? join(cwd, "mise.toml")));
824
+ const config = resolve(overrides.config ?? env["PERSONAL_FLEET_CONFIG"] ?? await findUp(cwd, ".fleet.local.json") ?? join(homedir(), ".config/personal/fleet.json"));
825
+ return { workspace, config };
826
+ }
827
+
828
+ // packages/personal-cli/src/registry.ts
829
+ init_search();
830
+ function fleet(command, title, description, keywords) {
831
+ return {
832
+ id: `fleet ${command}`,
833
+ title,
834
+ description,
835
+ group: "Fleet",
836
+ keywords,
837
+ args: ["fleet", command],
838
+ examples: [`personal fleet ${command} --json`],
839
+ mutation: ["add", "sync", "init"].includes(command)
840
+ };
841
+ }
842
+ var COMMANDS = [
843
+ fleet("doctor", "Diagnose the entire fleet", "Read-only SSH, authentication, T3, VS Code and dependency checks for this controller and every roster node.", [
844
+ "health",
845
+ "ssh",
846
+ "auth",
847
+ "github",
848
+ "vscode",
849
+ "t3",
850
+ "dependencies",
851
+ "diagnose"
852
+ ]),
853
+ {
854
+ ...fleet("doctor", "Diagnose this computer", "Check this process context without a roster or remote connections.", ["local", "controller", "health"]),
855
+ id: "fleet doctor local",
856
+ args: ["fleet", "doctor", "--local"],
857
+ examples: ["personal fleet doctor --local --json"]
858
+ },
859
+ fleet("nodes", "Explore fleet nodes", "List registered developer computers and their required capabilities without contacting them.", ["roster", "list", "machines", "computers"]),
860
+ fleet("add", "Register a developer computer", "Preview or explicitly save an existing SSH alias, checkout path and per-machine capabilities. No SSH or cloud setup.", ["onboarding", "register", "new", "setup"]),
861
+ fleet("validate", "Validate the roster", "Check local configuration before making any network requests.", ["config", "schema", "verify"]),
862
+ fleet("onboard", "Explore onboarding instructions", "Get per-node SSH templates, VS Code guidance, authentication steps and safe verification commands.", ["setup", "guide", "instructions", "vscode", "connect"]),
863
+ fleet("sync", "Plan toolchain and dependency sync", "Preview convergence through the existing mise/workstation owners. Applying requires --apply --yes.", ["drift", "versions", "mise", "install", "dependencies"]),
864
+ fleet("init", "Preview an example roster", "Inspect a starter configuration; only --apply --yes creates it. Prefer Register for real nodes.", ["setup", "example", "config"]),
865
+ fleet("schema", "Explore the fleet JSON schema", "Read the versioned roster schema for editors and agents.", ["agent", "config", "json"]),
866
+ {
867
+ id: "commands",
868
+ title: "Explore the command catalog",
869
+ description: "Search command names, descriptions and keywords; emit the complete machine-readable interface.",
870
+ group: "Reference",
871
+ keywords: ["search", "help", "llm", "agent", "api"],
872
+ args: ["commands"],
873
+ examples: ["personal commands --json --search ssh"],
874
+ mutation: false
875
+ },
876
+ {
877
+ id: "agent",
878
+ title: "Agent integration guide",
879
+ description: "Noninteractive usage, JSON contracts, exit codes, safety boundaries and reproducible examples.",
880
+ group: "Reference",
881
+ keywords: ["llm", "automation", "help", "safety"],
882
+ args: ["agent"],
883
+ examples: ["personal agent --json"],
884
+ mutation: false
885
+ }
886
+ ];
887
+ function commandCatalog(search = "", group) {
888
+ return searchCommands(COMMANDS, search).filter((command) => !group || command.group.toLowerCase() === group.toLowerCase());
889
+ }
890
+
891
+ // packages/personal-cli/src/program.ts
892
+ import { Command, CommanderError } from "commander";
893
+
894
+ // packages/fleet/src/errors.ts
895
+ class FleetUsageError extends Error {
896
+ }
897
+
898
+ // packages/personal-cli/src/agent-guide.ts
899
+ var AGENT_GUIDE = {
900
+ version: 1,
901
+ name: "@crvouga/personal",
902
+ invocation: "npx --yes @crvouga/personal@<exact-version> <command> --json --non-interactive",
903
+ discovery: "personal commands --json",
904
+ configuration: {
905
+ workspace: "--workspace PATH, PERSONAL_WORKSPACE, closest ancestor mise.toml, then current directory",
906
+ roster: "--config PATH, PERSONAL_FLEET_CONFIG, closest ancestor .fleet.local.json, then ~/.config/personal/fleet.json"
907
+ },
908
+ output: {
909
+ stdout: "One versioned JSON document with --json. Prompts and progress never contaminate stdout.",
910
+ exitCodes: {
911
+ "0": "Success or preview",
912
+ "1": "Failed check or operation; strict also rejects warnings/skips",
913
+ "2": "Invalid usage or configuration",
914
+ "130": "Cancelled"
915
+ },
916
+ errors: "{version:1,ok:false,code,error}; raw subprocess output and exception details are withheld",
917
+ reports: "{version:1,command,ok,generatedAt,nodes:[{node,checks:[{id,status,summary,remedy?}]}],summary,coverage}",
918
+ statuses: ["pass", "warn", "fail", "skip"],
919
+ coverage: "Default doctor tests the controller and all configured nodes. --local tests only this controller. A missing roster fails explicitly; configured nodes are not every computer that exists. T3/VS Code end-to-end checks remain manual."
920
+ },
921
+ safety: [
922
+ "No arguments opens a menu only on an interactive terminal. --json and --non-interactive never prompt.",
923
+ "Diagnostics, onboarding instructions, schema, catalog and sync previews are read-only.",
924
+ "Roster writes and sync require both --apply and --yes. npx --yes only approves downloading the CLI.",
925
+ "Do not copy credentials, forward SSH agents, bypass host keys, provision infrastructure or execute unreviewed remedies.",
926
+ "A healthy controller does not prove another machine can connect to it. Run doctor from each intended controller.",
927
+ "Use exact npm versions for automation. The application never self-updates."
928
+ ],
929
+ examples: [
930
+ "personal fleet doctor --local --workspace /path/to/personal --json",
931
+ "personal fleet doctor --all --json --strict --timeout 120000",
932
+ "personal fleet add --id dev-linux --ssh dev-linux --path /home/dev/workspace --require github,t3,toolchain --json",
933
+ "personal fleet add --id dev-linux --ssh dev-linux --path /home/dev/workspace --apply --yes --json",
934
+ "personal fleet doctor --node dev-linux --json",
935
+ "personal fleet sync --node dev-linux --json"
936
+ ]
937
+ };
938
+
939
+ // packages/personal-cli/src/fleet.ts
940
+ import { writeFile as writeFile2, mkdir as mkdir3 } from "node:fs/promises";
941
+ import { dirname as dirname3 } from "node:path";
942
+
943
+ // packages/fleet/src/config.ts
944
+ var CAPABILITIES = [
945
+ "github",
946
+ "vault",
947
+ "t3",
948
+ "vscode",
949
+ "toolchain"
950
+ ];
951
+ function record(value) {
952
+ return typeof value === "object" && value !== null && !Array.isArray(value);
953
+ }
954
+ function fields(value, allowed) {
955
+ if (Object.keys(value).some((key) => !allowed.includes(key))) {
956
+ throw new FleetUsageError("Unknown configuration field. Run fleet schema.");
957
+ }
958
+ }
959
+ function absolutePath(value) {
960
+ return typeof value === "string" && value.startsWith("/") && value.length <= 4096 && !/[\x00-\x1f\x7f]/.test(value);
961
+ }
962
+ function parseNode(value) {
963
+ if (!record(value))
964
+ throw new FleetUsageError("Each node must be an object.");
965
+ fields(value, ["id", "ssh", "workspace", "runtime", "required"]);
966
+ const names = nodeNames(value);
967
+ if (!absolutePath(value["workspace"]))
968
+ throw new FleetUsageError("workspace must be an absolute POSIX path.");
969
+ if (value["runtime"] !== undefined && !absolutePath(value["runtime"])) {
970
+ throw new FleetUsageError("runtime must be an absolute POSIX path to Node or Bun.");
971
+ }
972
+ return {
973
+ ...names,
974
+ workspace: value["workspace"],
975
+ ...typeof value["runtime"] === "string" ? { runtime: value["runtime"] } : {},
976
+ required: requiredCapabilities(value["required"])
977
+ };
978
+ }
979
+ function nodeNames(value) {
980
+ if (typeof value["id"] !== "string" || !/^[a-z][a-z0-9-]{0,62}$/.test(value["id"])) {
981
+ throw new FleetUsageError("Node IDs must be lowercase names (1–63 characters).");
982
+ }
983
+ if (typeof value["ssh"] !== "string" || !/^[a-zA-Z0-9][a-zA-Z0-9._-]{0,252}$/.test(value["ssh"])) {
984
+ throw new FleetUsageError("ssh must be a plain SSH alias or hostname, not options or user@host.");
985
+ }
986
+ return { id: value["id"], ssh: value["ssh"] };
987
+ }
988
+ function requiredCapabilities(value) {
989
+ const required = value === undefined ? ["github", "vault", "t3", "toolchain"] : value;
990
+ if (!Array.isArray(required) || required.some((item) => !CAPABILITIES.includes(item))) {
991
+ throw new FleetUsageError("required must contain only capability names from fleet schema.");
992
+ }
993
+ if (new Set(required).size !== required.length)
994
+ throw new FleetUsageError("required capabilities must be unique.");
995
+ return required;
996
+ }
997
+ function parseConfig(value) {
998
+ if (!record(value))
999
+ throw new FleetUsageError("Fleet configuration must be an object.");
1000
+ fields(value, ["$schema", "version", "nodes"]);
1001
+ if (value["$schema"] !== undefined && typeof value["$schema"] !== "string")
1002
+ throw new FleetUsageError("$schema must be a string.");
1003
+ if (value["version"] !== 1 || !Array.isArray(value["nodes"])) {
1004
+ throw new FleetUsageError("Expected version 1 and a nodes array.");
1005
+ }
1006
+ if (value["nodes"].length < 1 || value["nodes"].length > 64) {
1007
+ throw new FleetUsageError("Configure between 1 and 64 developer nodes.");
1008
+ }
1009
+ const nodes = value["nodes"].map(parseNode);
1010
+ if (new Set(nodes.map((node) => node.id)).size !== nodes.length) {
1011
+ throw new FleetUsageError("Node IDs must be unique.");
1012
+ }
1013
+ return { version: 1, nodes };
1014
+ }
1015
+ function selectNodes(config, ids) {
1016
+ if (ids.some((id) => !config.nodes.some((node) => node.id === id))) {
1017
+ throw new FleetUsageError("Unknown node selection. Run fleet nodes --json.");
1018
+ }
1019
+ return ids.length === 0 ? config.nodes : config.nodes.filter((node) => ids.includes(node.id));
1020
+ }
1021
+
1022
+ // packages/fleet/src/fingerprint.ts
1023
+ import { readFile as readFile2, mkdir, writeFile } from "node:fs/promises";
1024
+ import { join as join3 } from "node:path";
1025
+
1026
+ // packages/fleet/src/input-hashes.ts
1027
+ import { createHash } from "node:crypto";
1028
+ import { readFile, readdir } from "node:fs/promises";
1029
+ import { join as join2 } from "node:path";
1030
+ var INPUT_FILES = [
1031
+ "package.json",
1032
+ "bun.lock",
1033
+ "mise.toml",
1034
+ "mise.lock"
1035
+ ];
1036
+ var FINGERPRINT_FILES = [
1037
+ ...INPUT_FILES,
1038
+ "workspace-manifests",
1039
+ "mise-sidecars"
1040
+ ];
1041
+ async function fileHash(path) {
1042
+ try {
1043
+ return digest(await readFile(path));
1044
+ } catch {
1045
+ return null;
1046
+ }
1047
+ }
1048
+ async function manifestHash(root) {
1049
+ try {
1050
+ const entries = await readdir(join2(root, "packages"), {
1051
+ withFileTypes: true
1052
+ });
1053
+ const hashes = [];
1054
+ for (const entry of entries.filter((item) => item.isDirectory()).sort((left, right) => left.name.localeCompare(right.name, "en"))) {
1055
+ hashes.push(`${entry.name}:${await fileHash(join2(root, "packages", entry.name, "package.json"))}`);
1056
+ }
1057
+ return digest(hashes.join(`
1058
+ `));
1059
+ } catch {
1060
+ return null;
1061
+ }
1062
+ }
1063
+ async function sidecarHash(root) {
1064
+ try {
1065
+ const files = await readdir(join2(root, ".mise/locks"), {
1066
+ recursive: true,
1067
+ withFileTypes: true
1068
+ });
1069
+ if (files.length > 4096 || files.some((entry) => entry.isSymbolicLink()))
1070
+ return null;
1071
+ const hashes = [];
1072
+ const paths = files.filter((entry) => entry.isFile()).map((entry) => join2(entry.parentPath, entry.name)).sort();
1073
+ for (const path of paths) {
1074
+ const hash = await fileHash(path);
1075
+ if (hash === null)
1076
+ return null;
1077
+ const relative = path.slice(join2(root, ".mise/locks").length).replaceAll("\\", "/");
1078
+ hashes.push(`${relative}:${hash}`);
1079
+ }
1080
+ return digest(hashes.join(`
1081
+ `));
1082
+ } catch {
1083
+ return null;
1084
+ }
1085
+ }
1086
+ function digest(value) {
1087
+ return createHash("sha256").update(value).digest("hex");
1088
+ }
1089
+
1090
+ // packages/fleet/src/fingerprint.ts
1091
+ var RECEIPT = "node_modules/.fleet-sync.json";
1092
+ async function fingerprint(root, run) {
1093
+ const files = {};
1094
+ for (const name of INPUT_FILES)
1095
+ files[name] = await fileHash(join3(root, name));
1096
+ files["workspace-manifests"] = await manifestHash(root);
1097
+ files["mise-sidecars"] = await sidecarHash(root);
1098
+ const head = await run(["git", "rev-parse", "HEAD"], { cwd: root });
1099
+ return {
1100
+ head: head.ok && /^[a-f0-9]{40,64}\s*$/.test(head.stdout) ? head.stdout.trim() : null,
1101
+ files
1102
+ };
1103
+ }
1104
+ async function dependencyCheck(root, current) {
1105
+ const remedy = {
1106
+ action: "Review fleet sync, then explicitly apply a frozen toolchain/dependency sync."
1107
+ };
1108
+ try {
1109
+ const receipt = JSON.parse(await readFile2(join3(root, RECEIPT), "utf8"));
1110
+ const matches = record(receipt) && receipt["version"] === 1 && JSON.stringify(receipt["files"]) === JSON.stringify(current.files);
1111
+ return {
1112
+ id: "dependencies",
1113
+ status: matches ? "pass" : "fail",
1114
+ summary: matches ? "Last successful frozen sync matches input hashes; not a full integrity audit." : "Dependency sync receipt is stale or invalid.",
1115
+ ...matches ? {} : { remedy }
1116
+ };
1117
+ } catch {
1118
+ return {
1119
+ id: "dependencies",
1120
+ status: "warn",
1121
+ summary: "No frozen-sync receipt; installed dependencies are unverified.",
1122
+ remedy
1123
+ };
1124
+ }
1125
+ }
1126
+ async function writeReceipt(root, current) {
1127
+ await mkdir(join3(root, "node_modules"), { recursive: true });
1128
+ await writeFile(join3(root, RECEIPT), JSON.stringify({ version: 1, files: current.files }), { mode: 384 });
1129
+ }
1130
+ function compareFingerprints(expected, actual) {
1131
+ return [
1132
+ compare("revision", expected.head, actual.head),
1133
+ ...FINGERPRINT_FILES.map((name) => compare(`sync:${name}`, expected.files[name] ?? null, actual.files[name] ?? null))
1134
+ ];
1135
+ }
1136
+ function compare(id, expected, actual) {
1137
+ const known = expected !== null && actual !== null;
1138
+ const matches = known && expected === actual;
1139
+ return {
1140
+ id,
1141
+ status: matches ? "pass" : "fail",
1142
+ summary: matches ? "Matches the controller checkout." : known ? "Differs from the controller checkout." : "Cannot verify; reference or node input is missing.",
1143
+ ...matches ? {} : {
1144
+ remedy: {
1145
+ action: "Reconcile branches/lockfiles; review fleet sync --pull. Never reset a working tree."
1146
+ }
1147
+ }
1148
+ };
1149
+ }
1150
+
1151
+ // packages/fleet/src/auth-checks.ts
1152
+ import { join as join4 } from "node:path";
1153
+ async function authCheck(capability, run, root) {
1154
+ if (capability === "t3")
1155
+ return t3Checks(run);
1156
+ const spec = {
1157
+ github: {
1158
+ command: ["gh", "auth", "status", "--active"],
1159
+ action: "Authenticate on this node with gh auth login, then gh auth setup-git."
1160
+ },
1161
+ vault: {
1162
+ command: ["vault", "token", "lookup", "-format=json"],
1163
+ action: "Authenticate to the configured Vault on this node. Do not copy a token from another computer."
1164
+ },
1165
+ vscode: {
1166
+ command: ["code", "--list-extensions"],
1167
+ action: "Install the VS Code CLI on PATH and ms-vscode-remote.remote-ssh in the local editor profile."
1168
+ },
1169
+ toolchain: {
1170
+ command: ["bun", join4(root, "scripts/dev/doctor.ts"), "--json"],
1171
+ action: "Run ws tools check --json; use the shared mise setup to reconcile tools."
1172
+ }
1173
+ }[capability];
1174
+ const result = await run(spec.command, {
1175
+ cwd: root,
1176
+ timeoutMs: capability === "toolchain" ? 90000 : 15000
1177
+ });
1178
+ const extensions = result.stdout.toLowerCase().split(/\s+/);
1179
+ const ok = result.ok && (capability !== "vscode" || extensions.includes("ms-vscode-remote.remote-ssh"));
1180
+ return [
1181
+ {
1182
+ id: `auth:${capability}`,
1183
+ status: ok ? "pass" : "fail",
1184
+ summary: ok ? `${capability} check succeeded in this process context.` : `${capability} check failed (${result.reason}); no credential output retained.`,
1185
+ ...ok ? {} : { remedy: { action: spec.action } }
1186
+ }
1187
+ ];
1188
+ }
1189
+ async function t3Checks(run) {
1190
+ const config = await run(["t3", "status", "--json"]);
1191
+ const checks = [t3Settings(config)];
1192
+ const live = await run(["t3", "environment", "list", "--json"]);
1193
+ checks.push({
1194
+ id: "t3:api",
1195
+ status: live.ok ? "pass" : "fail",
1196
+ summary: live.ok ? "Local T3 API answered a read-only environment listing; peer connectivity is not inferred." : "Local T3 API did not answer the environment listing.",
1197
+ ...!live.ok ? {
1198
+ remedy: {
1199
+ action: "Check t3 service status and t3 status. Inspect linked peers in T3 Settings → Connections; relink only with operator approval."
1200
+ }
1201
+ } : {}
1202
+ });
1203
+ checks.push({
1204
+ id: "t3:end-to-end",
1205
+ status: "skip",
1206
+ summary: "Relay traversal and remote peer authorization require an operator connection test; local settings are not proof."
1207
+ });
1208
+ return checks;
1209
+ }
1210
+ function t3Settings(config) {
1211
+ const fallback = {
1212
+ id: "t3:settings",
1213
+ status: "fail",
1214
+ summary: "T3 settings unavailable or unsupported CLI response.",
1215
+ remedy: {
1216
+ action: "Run t3 status --json on this node. Complete t3 up --connect as its operator if needed."
1217
+ }
1218
+ };
1219
+ try {
1220
+ const data = JSON.parse(config.stdout);
1221
+ if (config.ok && record(data) && record(data["connect"])) {
1222
+ const enabled = data["connect"]["enabled"] === true && data["connect"]["revoked"] === false;
1223
+ return {
1224
+ id: "t3:settings",
1225
+ status: enabled ? "pass" : "fail",
1226
+ summary: enabled ? "T3 Connect enabled and not marked revoked (settings only)." : "T3 Connect disabled, revoked, or unknown.",
1227
+ ...!enabled ? {
1228
+ remedy: {
1229
+ action: "Review t3 status, then complete t3 up --connect as the operator. Never share pairing tokens."
1230
+ }
1231
+ } : {}
1232
+ };
1233
+ }
1234
+ } catch {}
1235
+ return fallback;
1236
+ }
1237
+
1238
+ // packages/fleet/src/probe.ts
1239
+ async function probe(root, required, run) {
1240
+ const current = await fingerprint(root, run);
1241
+ const checks = [
1242
+ {
1243
+ id: "workspace",
1244
+ status: Object.values(current.files).every(Boolean) && current.head ? "pass" : "fail",
1245
+ summary: "Checkout revision and shared dependency/toolchain input availability."
1246
+ }
1247
+ ];
1248
+ const dirty = await run(["git", "status", "--porcelain"], { cwd: root });
1249
+ checks.push({
1250
+ id: "git:clean",
1251
+ status: dirty.ok ? dirty.stdout.trim() ? "warn" : "pass" : "fail",
1252
+ summary: dirty.ok && !dirty.stdout.trim() ? "Working tree is clean." : "Working tree is dirty or unreadable; sync --pull will refuse."
1253
+ });
1254
+ checks.push(await dependencyCheck(root, current));
1255
+ for (const capability of [
1256
+ "github",
1257
+ "vault",
1258
+ "t3",
1259
+ "vscode",
1260
+ "toolchain"
1261
+ ]) {
1262
+ if (required.includes(capability))
1263
+ checks.push(...await authCheck(capability, run, root));
1264
+ else
1265
+ checks.push({
1266
+ id: `auth:${capability}`,
1267
+ status: "skip",
1268
+ summary: "Not required by this node configuration."
1269
+ });
1270
+ }
1271
+ return {
1272
+ version: 1,
1273
+ platform: process.platform,
1274
+ fingerprint: current,
1275
+ checks
1276
+ };
1277
+ }
1278
+
1279
+ // packages/fleet/src/report.ts
1280
+ function makeReport(command, nodes) {
1281
+ const summary = {
1282
+ pass: 0,
1283
+ warn: 0,
1284
+ fail: 0,
1285
+ skip: 0
1286
+ };
1287
+ for (const node of nodes)
1288
+ for (const check of node.checks)
1289
+ summary[check.status] += 1;
1290
+ return {
1291
+ version: 1,
1292
+ command,
1293
+ ok: summary.fail === 0,
1294
+ generatedAt: new Date().toISOString(),
1295
+ nodes,
1296
+ summary
1297
+ };
1298
+ }
1299
+ function reportExitCode(report, strict) {
1300
+ return !report.ok || strict && (report.summary.warn > 0 || report.summary.skip > 0) ? 1 : 0;
1301
+ }
1302
+ function renderReport(report) {
1303
+ const lines = [
1304
+ `Fleet ${report.command}: ${report.ok ? "no failing checks" : "attention needed"}`
1305
+ ];
1306
+ for (const node of report.nodes) {
1307
+ lines.push(`
1308
+ ${node.node}`);
1309
+ for (const check of node.checks) {
1310
+ lines.push(` ${check.status.toUpperCase()} ${check.id}: ${check.summary}`);
1311
+ if (check.remedy)
1312
+ lines.push(` Next: ${check.remedy.action}`);
1313
+ }
1314
+ }
1315
+ lines.push(`
1316
+ ${report.summary.pass} pass, ${report.summary.warn} warn, ${report.summary.fail} fail, ${report.summary.skip} skip`);
1317
+ return lines.join(`
1318
+ `);
1319
+ }
1320
+ async function mapBounded(items, worker, concurrency = 4) {
1321
+ const results = new Array(items.length);
1322
+ let cursor = 0;
1323
+ const consume = async () => {
1324
+ while (cursor < items.length) {
1325
+ const index = cursor++;
1326
+ results[index] = await worker(items[index]);
1327
+ }
1328
+ };
1329
+ await Promise.all(Array.from({ length: Math.min(concurrency, items.length) }, consume));
1330
+ return results;
1331
+ }
1332
+
1333
+ // packages/fleet/src/ssh.ts
1334
+ function shellQuote(value) {
1335
+ return `'${value.replaceAll("'", "'\\''")}'`;
1336
+ }
1337
+ function sshArgs(node) {
1338
+ return [
1339
+ "ssh",
1340
+ "-T",
1341
+ "-a",
1342
+ "-o",
1343
+ "BatchMode=yes",
1344
+ "-o",
1345
+ "StrictHostKeyChecking=yes",
1346
+ "-o",
1347
+ "UpdateHostKeys=no",
1348
+ "-o",
1349
+ "ConnectTimeout=8",
1350
+ "-o",
1351
+ "ConnectionAttempts=1",
1352
+ "-o",
1353
+ "ServerAliveInterval=5",
1354
+ "-o",
1355
+ "ServerAliveCountMax=2",
1356
+ "-o",
1357
+ "ClearAllForwardings=yes",
1358
+ "-o",
1359
+ "PermitLocalCommand=no",
1360
+ "-o",
1361
+ "ControlMaster=no",
1362
+ "-o",
1363
+ "ControlPath=none",
1364
+ node.ssh
1365
+ ];
1366
+ }
1367
+ function remoteCommand(node, request) {
1368
+ const encoded = Buffer.from(JSON.stringify(request)).toString("base64");
1369
+ const command = `cd -- ${shellQuote(node.workspace)} && exec ${shellQuote(node.runtime ?? "node")} --input-type=module - ${shellQuote(encoded)}`;
1370
+ return [...sshArgs(node), command];
1371
+ }
1372
+ var FAILURES = [
1373
+ {
1374
+ pattern: /REMOTE HOST IDENTIFICATION HAS CHANGED|Host key verification failed|No .* host key is known/i,
1375
+ id: "ssh:host-key",
1376
+ summary: "SSH host key is unknown or changed.",
1377
+ action: "Verify the host fingerprint out of band, then update known_hosts manually. Never disable host-key checking."
1378
+ },
1379
+ {
1380
+ pattern: /Could not resolve hostname|Name or service not known|nodename nor servname/i,
1381
+ id: "ssh:dns",
1382
+ summary: "SSH hostname or jump-host resolution failed.",
1383
+ action: "Check the alias in ~/.ssh/config, DNS/VPN/Tailscale connectivity, and ProxyJump host resolution."
1384
+ },
1385
+ {
1386
+ pattern: /Permission denied|sign_and_send_pubkey|agent refused|Too many authentication failures/i,
1387
+ id: "ssh:auth",
1388
+ summary: "SSH key/agent authentication failed.",
1389
+ action: "Use ssh-add -l and ssh -vvv ALIAS locally; check User, IdentityFile, IdentitiesOnly, agent/keychain access and authorized_keys. Do not share debug logs without review."
1390
+ },
1391
+ {
1392
+ pattern: /Connection refused|No route to host|Network is unreachable/i,
1393
+ id: "ssh:network",
1394
+ summary: "SSH endpoint or jump host is unreachable/refused.",
1395
+ action: "Verify the node is awake, VPN is connected, sshd is enabled and the configured port is reachable. Do not open a public SSH port automatically."
1396
+ },
1397
+ {
1398
+ pattern: /(?:bun|node).*not found|No such file or directory|cannot cd|can.t cd/i,
1399
+ id: "ssh:runtime",
1400
+ summary: "Remote workspace or Node/Bun runtime is missing from the non-login SSH environment.",
1401
+ action: "Follow fleet onboard; configure absolute workspace/runtime paths. GUI terminals and SSH shells may have different PATHs."
1402
+ }
1403
+ ];
1404
+ function sshFailure(result) {
1405
+ if (result.reason === "timeout")
1406
+ return {
1407
+ id: "ssh:timeout",
1408
+ status: "fail",
1409
+ summary: "SSH or remote probe exceeded its deadline.",
1410
+ remedy: {
1411
+ action: "Check sleeping nodes, VPN, ProxyJump, network reachability and noninteractive shell startup. Increase --timeout only after diagnosis."
1412
+ }
1413
+ };
1414
+ if (result.reason === "missing")
1415
+ return {
1416
+ id: "ssh:missing",
1417
+ status: "fail",
1418
+ summary: "Cannot launch the local SSH client.",
1419
+ remedy: {
1420
+ action: "Install/enable the OS OpenSSH client and expose it to the terminal/editor PATH."
1421
+ }
1422
+ };
1423
+ const match = FAILURES.find((failure) => failure.pattern.test(result.stderr));
1424
+ return {
1425
+ id: match?.id ?? "ssh:probe",
1426
+ status: "fail",
1427
+ summary: match?.summary ?? `Remote probe failed (${result.reason}); raw output withheld.`,
1428
+ remedy: {
1429
+ action: match?.action ?? "Run ssh ALIAS manually; verify workspace/runtime, shell startup output and CLI compatibility. Never paste credentials or full logs into agent context."
1430
+ }
1431
+ };
1432
+ }
1433
+
1434
+ // packages/fleet/src/protocol.ts
1435
+ var STATUSES = ["pass", "warn", "fail", "skip"];
1436
+ function validCheck(value) {
1437
+ if (!record(value) || typeof value["id"] !== "string" || !/^[a-z0-9:._-]{1,100}$/.test(value["id"]) || !STATUSES.includes(String(value["status"])) || typeof value["summary"] !== "string" || value["summary"].length > 2000)
1438
+ return false;
1439
+ const remedy = value["remedy"];
1440
+ return remedy === undefined || validRemedy(remedy);
1441
+ }
1442
+ function validRemedy(value) {
1443
+ return record(value) && typeof value["action"] === "string" && value["action"].length < 2000 && (value["command"] === undefined || typeof value["command"] === "string");
1444
+ }
1445
+ function validSnapshot(value) {
1446
+ if (!record(value) || value["version"] !== 1 || typeof value["platform"] !== "string" || !Array.isArray(value["checks"]) || value["checks"].length > 200 || !value["checks"].every(validCheck))
1447
+ return false;
1448
+ return validFingerprint(value["fingerprint"]);
1449
+ }
1450
+ function validFingerprint(value) {
1451
+ if (!record(value) || !record(value["files"]))
1452
+ return false;
1453
+ if (!Object.keys(value["files"]).every((key) => FINGERPRINT_FILES.includes(key)))
1454
+ return false;
1455
+ const head = value["head"];
1456
+ return (head === null || typeof head === "string" && /^[a-f0-9]{40,64}$/.test(head)) && Object.values(value["files"]).every(validHash);
1457
+ }
1458
+ function validHash(value) {
1459
+ return value === null || typeof value === "string" && /^[a-f0-9]{64}$/.test(value);
1460
+ }
1461
+ function decodeSnapshot(output) {
1462
+ try {
1463
+ const data = JSON.parse(output);
1464
+ return validSnapshot(data) ? cleanSnapshot(data) : null;
1465
+ } catch {
1466
+ return null;
1467
+ }
1468
+ }
1469
+ function cleanCheck(check) {
1470
+ return {
1471
+ id: check.id,
1472
+ status: check.status,
1473
+ summary: check.summary,
1474
+ ...check.remedy ? {
1475
+ remedy: {
1476
+ action: check.remedy.action,
1477
+ ...check.remedy.command ? { command: check.remedy.command } : {}
1478
+ }
1479
+ } : {}
1480
+ };
1481
+ }
1482
+ function cleanSnapshot(snapshot) {
1483
+ return {
1484
+ version: 1,
1485
+ platform: snapshot.platform,
1486
+ fingerprint: {
1487
+ head: snapshot.fingerprint.head,
1488
+ files: snapshot.fingerprint.files
1489
+ },
1490
+ checks: snapshot.checks.map(cleanCheck)
1491
+ };
1492
+ }
1493
+
1494
+ // packages/fleet/src/transport.ts
1495
+ import { fileURLToPath } from "node:url";
1496
+ async function bundleProbe() {
1497
+ const result = await Bun.build({
1498
+ entrypoints: [fileURLToPath(new URL("./remote-entry.ts", import.meta.url))],
1499
+ target: "node",
1500
+ minify: true
1501
+ });
1502
+ if (!result.success || !result.outputs[0])
1503
+ throw new Error("Cannot bundle the fleet probe.");
1504
+ return result.outputs[0].text();
1505
+ }
1506
+ async function inspectRemote(node, options, run) {
1507
+ const command = remoteCommand(node, {
1508
+ action: options.action,
1509
+ node,
1510
+ apply: options.action === "sync",
1511
+ pull: options.pull === true
1512
+ });
1513
+ const result = await run(command, {
1514
+ input: options.source,
1515
+ timeoutMs: options.timeoutMs
1516
+ });
1517
+ if (!result.ok)
1518
+ return { node: node.id, checks: [sshFailure(result)] };
1519
+ if (options.action === "sync")
1520
+ return decodeSync(node.id, result.stdout);
1521
+ const snapshot = decodeSnapshot(result.stdout);
1522
+ if (!snapshot)
1523
+ return protocolFailure(node.id);
1524
+ return {
1525
+ node: node.id,
1526
+ snapshot,
1527
+ checks: [
1528
+ {
1529
+ id: "ssh:session",
1530
+ status: "pass",
1531
+ summary: "Fresh noninteractive SSH session and Node-compatible probe succeeded with verified host keys and no agent forwarding."
1532
+ },
1533
+ ...snapshot.checks,
1534
+ {
1535
+ id: "vscode:forwarding",
1536
+ status: "skip",
1537
+ summary: "VS Code Remote-SSH tunnel/server installation is not tested by a shell session. Use the onboarding forwarding test."
1538
+ }
1539
+ ]
1540
+ };
1541
+ }
1542
+ function decodeSync(node, output) {
1543
+ try {
1544
+ const data = JSON.parse(output);
1545
+ if (record(data) && data["version"] === 1 && Array.isArray(data["checks"]) && data["checks"].length <= 200 && data["checks"].every(validCheck)) {
1546
+ return { node, checks: data["checks"].map(cleanCheck) };
1547
+ }
1548
+ } catch {
1549
+ return protocolFailure(node);
1550
+ }
1551
+ return protocolFailure(node);
1552
+ }
1553
+ function protocolFailure(node) {
1554
+ return {
1555
+ node,
1556
+ checks: [
1557
+ {
1558
+ id: "ssh:protocol",
1559
+ status: "fail",
1560
+ summary: "Remote response is not a supported fleet JSON document.",
1561
+ remedy: {
1562
+ action: "Remove stdout banners from noninteractive shell startup; check the Node/Bun runtime. No raw remote output is retained."
1563
+ }
1564
+ }
1565
+ ]
1566
+ };
1567
+ }
1568
+
1569
+ // packages/fleet/src/controller.ts
1570
+ async function doctor(nodes, options, run) {
1571
+ const local = await probe(options.root, options.required, run);
1572
+ const agent = await run(["ssh-add", "-l"]);
1573
+ const reports = [
1574
+ {
1575
+ node: "controller",
1576
+ snapshot: local,
1577
+ checks: [
1578
+ ...local.checks,
1579
+ {
1580
+ id: "ssh:agent",
1581
+ status: agent.ok ? "pass" : "warn",
1582
+ summary: agent.ok ? "SSH agent exposes at least one identity." : "No accessible SSH agent identities; explicit IdentityFile keys may still work.",
1583
+ ...!agent.ok ? {
1584
+ remedy: {
1585
+ action: "Unlock/load the intended key in the controller session. Check SSH_AUTH_SOCK and GUI/editor agent visibility; do not copy private keys."
1586
+ }
1587
+ } : {}
1588
+ }
1589
+ ]
1590
+ }
1591
+ ];
1592
+ if (options.local || nodes.length === 0)
1593
+ return reports;
1594
+ const source = options.source ?? await bundleProbe();
1595
+ const reference = await fingerprint(options.root, run);
1596
+ const remote = await mapBounded(nodes, async (node) => {
1597
+ const report = await inspectRemote(node, { action: "probe", source, timeoutMs: options.timeoutMs }, run);
1598
+ if (report.snapshot)
1599
+ report.checks.push(...compareFingerprints(reference, report.snapshot.fingerprint));
1600
+ return report;
1601
+ });
1602
+ return [...reports, ...remote];
1603
+ }
1604
+
1605
+ // packages/fleet/src/sync.ts
1606
+ import { unlink } from "node:fs/promises";
1607
+ import { join as join5 } from "node:path";
1608
+ function syncPlan(pull) {
1609
+ return [
1610
+ "Validate the checkout and shared input files; invalidate any prior sync receipt.",
1611
+ ...pull ? [
1612
+ "Refuse dirty or detached checkouts; git pull --ff-only (no reset, stash, force or rebase)."
1613
+ ] : [],
1614
+ "Run mise exec --locked bun -- bun scripts/dev/setup.ts --non-interactive (shared tools, user mise/PATH, frozen dependencies).",
1615
+ "Record input hashes only after every step succeeds.",
1616
+ "Run fleet doctor afterward; workstation/provider configuration and logins remain explicit ws/operator actions."
1617
+ ];
1618
+ }
1619
+ async function syncWorkspace(root, pull, run) {
1620
+ const before = await fingerprint(root, run);
1621
+ if (!completeFingerprint(before)) {
1622
+ return failed("Not a complete workspace checkout. Follow fleet onboard first.");
1623
+ }
1624
+ if (pull) {
1625
+ const dirty = await run(["git", "status", "--porcelain"], { cwd: root });
1626
+ const branch = await run(["git", "symbolic-ref", "--quiet", "HEAD"], {
1627
+ cwd: root
1628
+ });
1629
+ if (!dirty.ok || dirty.stdout.trim() || !branch.ok)
1630
+ return failed("Refusing pull: dirty, detached, or unreadable checkout. Reconcile manually.");
1631
+ }
1632
+ await unlink(join5(root, "node_modules/.fleet-sync.json")).catch(() => {
1633
+ return;
1634
+ });
1635
+ if (pull) {
1636
+ const result = await run(["git", "pull", "--ff-only"], {
1637
+ cwd: root,
1638
+ timeoutMs: 120000
1639
+ });
1640
+ if (!result.ok)
1641
+ return failed("Fast-forward pull failed. Inspect upstream/network/auth manually; nothing was reset.");
1642
+ }
1643
+ const expected = await fingerprint(root, run);
1644
+ const result = await run([
1645
+ "mise",
1646
+ "exec",
1647
+ "--locked",
1648
+ "bun",
1649
+ "--",
1650
+ "bun",
1651
+ "scripts/dev/setup.ts",
1652
+ "--non-interactive"
1653
+ ], { cwd: root, timeoutMs: 1200000 });
1654
+ if (!result.ok)
1655
+ return failed(`Shared toolchain/dependency sync failed (${result.reason}). Inspect setup locally; partial installs are possible.`);
1656
+ const after = await fingerprint(root, run);
1657
+ if (JSON.stringify(expected) !== JSON.stringify(after))
1658
+ return failed("Checkout inputs changed during sync. Retry from a stable checkout.");
1659
+ await writeReceipt(root, after);
1660
+ return [
1661
+ {
1662
+ id: "sync",
1663
+ status: "pass",
1664
+ summary: "Shared setup succeeded; frozen-sync receipt recorded. Auth and live connectivity still need doctor checks."
1665
+ }
1666
+ ];
1667
+ }
1668
+ function failed(summary) {
1669
+ return [
1670
+ {
1671
+ id: "sync",
1672
+ status: "fail",
1673
+ summary,
1674
+ remedy: {
1675
+ action: "Resolve locally using scripts/dev/README.md, then review and retry fleet sync."
1676
+ }
1677
+ }
1678
+ ];
1679
+ }
1680
+ function completeFingerprint(value) {
1681
+ return value.head !== null && Object.values(value.files).every(Boolean);
1682
+ }
1683
+
1684
+ // packages/fleet/src/onboard.ts
1685
+ function onboarding(node, launcher = "bun run fleet") {
1686
+ return {
1687
+ version: 1,
1688
+ node: node.id,
1689
+ applied: false,
1690
+ steps: [
1691
+ "On the new node, review and run scripts/dev/bootstrap.sh (or bootstrap.ps1 for native Windows). Use scripts/dev/README.md. This installs the shared locked toolchain; it does not enable SSH.",
1692
+ "Enable the OS SSH service as the operator on a trusted private network. Do not expose SSH publicly or create cloud resources.",
1693
+ "Create a distinct per-machine SSH key and authorize its PUBLIC key on the destination. Verify host fingerprints out of band. Keep private keys and auth tokens on their original machines.",
1694
+ "In the controller SSH config, configure this alias with the real HostName, User, IdentityFile, optional Port/ProxyJump and IdentitiesOnly yes. Fleet will not write or overwrite SSH configuration.",
1695
+ "Test ssh ALIAS manually once after fingerprint verification. Ensure noninteractive shell startup writes no stdout. Set runtime to an absolute Node/Bun executable/shim if SSH PATH omits it.",
1696
+ "Authenticate separately on the node: gh auth login; gh auth setup-git; vault login using the approved method. Configure coding-provider sessions independently; never copy auth directories.",
1697
+ "For T3 Connect, review t3 status and t3 service status, then complete t3 up --connect and operator pairing/linking. T3 pairing/auth state is not an SSH credential.",
1698
+ "For VS Code, install Remote-SSH in the LOCAL editor profile, use the same SSH alias/client/config, enable the login terminal temporarily, and read Output → Remote - SSH. Restart the editor after PATH/agent changes.",
1699
+ "VS Code needs TCP forwarding and compatible remote server prerequisites. Test with ssh -N -L 127.0.0.1:18080:127.0.0.1:REMOTE_PORT ALIAS against a known loopback service, then curl localhost:18080 from a second terminal. Review server policy; never loosen it automatically.",
1700
+ "Run fleet doctor --node NODE --json from every intended controller. This is directed connectivity, not an all-pairs proof. Review skipped T3/VS Code end-to-end checks manually."
1701
+ ],
1702
+ sshTemplate: `Host ${node.ssh}
1703
+ HostName REPLACE_WITH_PRIVATE_HOST
1704
+ User REPLACE_WITH_USER
1705
+ IdentityFile ~/.ssh/REPLACE_WITH_NODE_KEY
1706
+ IdentitiesOnly yes
1707
+ ForwardAgent no
1708
+ `,
1709
+ vscodeSettings: {
1710
+ "remote.SSH.path": "REPLACE_WITH_SAME_OPENSSH_BINARY",
1711
+ "remote.SSH.configFile": "REPLACE_WITH_ABSOLUTE_SSH_CONFIG",
1712
+ "remote.SSH.showLoginTerminal": true
1713
+ },
1714
+ workspaceCommand: `cd -- ${shellQuote(node.workspace)}`,
1715
+ verifyCommand: `${launcher} doctor --node ${node.id} --json`,
1716
+ sync: {
1717
+ command: `${launcher} sync --node ${node.id}`,
1718
+ steps: syncPlan(false)
1719
+ }
1720
+ };
1721
+ }
1722
+
1723
+ // packages/fleet/src/runner.ts
1724
+ import { spawn } from "node:child_process";
1725
+ var MAX_OUTPUT_BYTES = 1024 * 1024;
1726
+ function terminate(child) {
1727
+ try {
1728
+ if (process.platform !== "win32" && child.pid)
1729
+ process.kill(-child.pid, "SIGKILL");
1730
+ else
1731
+ child.kill("SIGKILL");
1732
+ } catch {}
1733
+ }
1734
+ function runCommand(command, options = {}) {
1735
+ return new Promise((resolve) => {
1736
+ const child = spawn(command[0], command.slice(1), {
1737
+ cwd: options.cwd,
1738
+ env: {
1739
+ ...process.env,
1740
+ ...options.env,
1741
+ GIT_TERMINAL_PROMPT: "0",
1742
+ CI: "1"
1743
+ },
1744
+ stdio: "pipe",
1745
+ windowsHide: true,
1746
+ detached: process.platform !== "win32"
1747
+ });
1748
+ let stdout = "";
1749
+ let stderr = "";
1750
+ let bytes = 0;
1751
+ let finished = false;
1752
+ let reason = "exit";
1753
+ const finish = (ok) => {
1754
+ if (finished)
1755
+ return;
1756
+ finished = true;
1757
+ clearTimeout(timer);
1758
+ child.stdin.destroy();
1759
+ child.stdout.destroy();
1760
+ child.stderr.destroy();
1761
+ resolve({ ok, stdout, stderr, reason });
1762
+ };
1763
+ const abort = (cause) => {
1764
+ reason = cause;
1765
+ terminate(child);
1766
+ finish(false);
1767
+ };
1768
+ const timer = setTimeout(() => {
1769
+ abort("timeout");
1770
+ }, options.timeoutMs ?? 15000);
1771
+ const capture = (stream, chunk) => {
1772
+ if (finished)
1773
+ return;
1774
+ bytes += Buffer.byteLength(chunk);
1775
+ if (bytes > MAX_OUTPUT_BYTES) {
1776
+ abort("overflow");
1777
+ return;
1778
+ }
1779
+ if (stream === "stdout")
1780
+ stdout += chunk;
1781
+ else
1782
+ stderr += chunk;
1783
+ };
1784
+ child.stdout.setEncoding("utf8").on("data", (chunk) => capture("stdout", chunk));
1785
+ child.stderr.setEncoding("utf8").on("data", (chunk) => capture("stderr", chunk));
1786
+ child.stdin.on("error", () => {
1787
+ return;
1788
+ });
1789
+ child.on("error", () => {
1790
+ abort("missing");
1791
+ });
1792
+ child.on("close", (code) => {
1793
+ finish(code === 0 && reason === "exit");
1794
+ });
1795
+ child.stdin.end(options.input);
1796
+ });
1797
+ }
1798
+ // packages/fleet/schema.json
1799
+ var schema_default = {
1800
+ $schema: "https://json-schema.org/draft/2020-12/schema",
1801
+ title: "Developer fleet roster (not infrastructure inventory)",
1802
+ type: "object",
1803
+ additionalProperties: false,
1804
+ required: ["version", "nodes"],
1805
+ properties: {
1806
+ $schema: { type: "string" },
1807
+ version: { const: 1 },
1808
+ nodes: {
1809
+ type: "array",
1810
+ minItems: 1,
1811
+ maxItems: 64,
1812
+ items: {
1813
+ type: "object",
1814
+ additionalProperties: false,
1815
+ required: ["id", "ssh", "workspace"],
1816
+ properties: {
1817
+ id: { type: "string", pattern: "^[a-z][a-z0-9-]{0,62}$" },
1818
+ ssh: {
1819
+ type: "string",
1820
+ pattern: "^[a-zA-Z0-9][a-zA-Z0-9._-]{0,252}$"
1821
+ },
1822
+ workspace: {
1823
+ type: "string",
1824
+ maxLength: 4096,
1825
+ pattern: "^/[^\\u0000-\\u001f\\u007f]*$"
1826
+ },
1827
+ runtime: {
1828
+ type: "string",
1829
+ maxLength: 4096,
1830
+ pattern: "^/[^\\u0000-\\u001f\\u007f]*$"
1831
+ },
1832
+ required: {
1833
+ type: "array",
1834
+ uniqueItems: true,
1835
+ default: ["github", "vault", "t3", "toolchain"],
1836
+ items: {
1837
+ enum: ["github", "vault", "t3", "vscode", "toolchain"]
1838
+ }
1839
+ }
1840
+ }
1841
+ }
1842
+ }
1843
+ }
1844
+ };
1845
+ // packages/fleet/fleet.example.json
1846
+ var fleet_example_default = {
1847
+ $schema: "./schema.json",
1848
+ version: 1,
1849
+ nodes: [
1850
+ {
1851
+ id: "dev-linux",
1852
+ ssh: "dev-linux",
1853
+ workspace: "/home/developer/workspace",
1854
+ runtime: "/home/developer/.local/share/mise/shims/node",
1855
+ required: ["github", "vault", "t3", "toolchain"]
1856
+ }
1857
+ ]
1858
+ };
1859
+
1860
+ // packages/personal-cli/src/options.ts
1861
+ function capabilities(value) {
1862
+ const values = value === "none" ? [] : value?.split(",") ?? CAPABILITIES;
1863
+ if (values.some((entry) => !CAPABILITIES.includes(entry)))
1864
+ throw new FleetUsageError("--require expects github,vault,t3,vscode,toolchain or none.");
1865
+ return [...new Set(values)];
1866
+ }
1867
+ function timeout(options) {
1868
+ const value = Number(options.timeout ?? (options.apply ? 1500000 : 120000));
1869
+ if (!Number.isInteger(value) || value < 1000 || value > 1800000)
1870
+ throw new FleetUsageError("--timeout must be 1000–1800000 milliseconds.");
1871
+ return value;
1872
+ }
1873
+ function validateOptions(command, options) {
1874
+ validateMutation(command, options);
1875
+ validateScope(command, options);
1876
+ if (options.timeout !== undefined)
1877
+ timeout(options);
1878
+ }
1879
+ function validateMutation(command, options) {
1880
+ if (options.apply && !["add", "init", "sync"].includes(command))
1881
+ throw new FleetUsageError("--apply is only valid for fleet add, init or sync.");
1882
+ if (options.apply && !options.yes)
1883
+ throw new FleetUsageError("Mutation requires both --apply and --yes.");
1884
+ if (options.pull && command !== "sync")
1885
+ throw new FleetUsageError("--pull is only valid for fleet sync.");
1886
+ }
1887
+ function validateScope(command, options) {
1888
+ if (options.local && !["doctor", "sync"].includes(command))
1889
+ throw new FleetUsageError("--local is only valid for fleet doctor or sync.");
1890
+ validateExclusive(options);
1891
+ if (options.all && command !== "doctor")
1892
+ throw new FleetUsageError("--all is only valid for fleet doctor; sync already targets all configured nodes by default.");
1893
+ }
1894
+ function validateExclusive(options) {
1895
+ const selected = [
1896
+ options.local,
1897
+ options.all,
1898
+ Boolean(options.node?.length)
1899
+ ].filter(Boolean);
1900
+ if (selected.length > 1)
1901
+ throw new FleetUsageError("Choose --local, --all, or --node, not a combination.");
1902
+ }
1903
+
1904
+ // packages/personal-cli/src/roster.ts
1905
+ import { lstat, mkdir as mkdir2, open, readFile as readFile3, rename, unlink as unlink2 } from "node:fs/promises";
1906
+ import { dirname as dirname2 } from "node:path";
1907
+ import { randomUUID } from "node:crypto";
1908
+ async function readRoster(path) {
1909
+ try {
1910
+ const info = await lstat(path);
1911
+ if (!info.isFile() || info.size > 1024 * 1024)
1912
+ throw new FleetUsageError("Roster must be a regular file smaller than 1 MiB; symlinks are not supported.");
1913
+ return parseConfig(JSON.parse(await readFile3(path, "utf8")));
1914
+ } catch (error) {
1915
+ if (record(error) && error["code"] === "ENOENT")
1916
+ return null;
1917
+ if (error instanceof FleetUsageError)
1918
+ throw error;
1919
+ throw new FleetUsageError("Cannot read a valid roster. Use fleet schema or choose --config PATH.");
1920
+ }
1921
+ }
1922
+ function nodeFromOptions(options) {
1923
+ if (!options.id || !options.ssh || !options.path)
1924
+ throw new FleetUsageError("fleet add requires --id NAME --ssh ALIAS --path /absolute/checkout.");
1925
+ const node = {
1926
+ id: options.id,
1927
+ ssh: options.ssh,
1928
+ workspace: options.path,
1929
+ ...options.runtime ? { runtime: options.runtime } : {},
1930
+ required: capabilities(options.require ?? "github,vault,t3,toolchain")
1931
+ };
1932
+ return parseConfig({ version: 1, nodes: [node] }).nodes[0];
1933
+ }
1934
+ function appendNode(existing, node) {
1935
+ return parseConfig({ version: 1, nodes: [...existing?.nodes ?? [], node] });
1936
+ }
1937
+ async function addNode(path, node, apply) {
1938
+ if (!apply)
1939
+ return appendNode(await readRoster(path), node);
1940
+ await mkdir2(dirname2(path), { recursive: true, mode: 448 });
1941
+ const lock = await acquireLock(path);
1942
+ try {
1943
+ const roster = appendNode(await readRoster(path), node);
1944
+ await atomicWrite(path, roster);
1945
+ return roster;
1946
+ } finally {
1947
+ await lock.close();
1948
+ await unlink2(`${path}.lock`);
1949
+ }
1950
+ }
1951
+ async function acquireLock(path) {
1952
+ try {
1953
+ return await open(`${path}.lock`, "wx", 384);
1954
+ } catch {
1955
+ throw new FleetUsageError("Roster is locked or unwritable. Check permissions and concurrent writers; remove a stale .lock only after verifying no operation is running.");
1956
+ }
1957
+ }
1958
+ async function atomicWrite(path, roster) {
1959
+ const temporary = `${path}.${randomUUID()}.tmp`;
1960
+ const handle = await open(temporary, "wx", 384);
1961
+ try {
1962
+ await handle.writeFile(`${JSON.stringify(roster, null, 2)}
1963
+ `);
1964
+ await handle.sync();
1965
+ await handle.close();
1966
+ await rename(temporary, path);
1967
+ } finally {
1968
+ await handle.close();
1969
+ await unlink2(temporary).catch(() => {
1970
+ return;
1971
+ });
1972
+ }
1973
+ }
1974
+
1975
+ // packages/personal-cli/src/probe-source.ts
1976
+ import { readFile as readFile4 } from "node:fs/promises";
1977
+ async function probeSource() {
1978
+ if (typeof Bun !== "undefined")
1979
+ return bundleProbe();
1980
+ return readFile4(new URL("./probe.mjs", import.meta.url), "utf8");
1981
+ }
1982
+
1983
+ // packages/personal-cli/src/fleet.ts
1984
+ var SERVICES = { run: runCommand, source: probeSource };
1985
+ async function executeFleet(command, options, settings, services = SERVICES) {
1986
+ validateOptions(command, options);
1987
+ if (command === "schema")
1988
+ return result(schema_default);
1989
+ if (command === "init")
1990
+ return initialize(settings, options);
1991
+ if (command === "add")
1992
+ return register(settings, options);
1993
+ const roster = options.local ? null : await readRoster(settings.config);
1994
+ const nodes = resolveNodes(command, options, roster);
1995
+ if (["nodes", "validate", "onboard"].includes(command))
1996
+ return result({
1997
+ version: 1,
1998
+ command,
1999
+ ok: true,
2000
+ nodes: command === "onboard" ? nodes.map((node) => onboarding(node, "personal fleet")) : nodes
2001
+ });
2002
+ if (command === "sync")
2003
+ return synchronize(nodes, options, settings, services);
2004
+ return diagnose(nodes, options, { ...settings, missingRoster: !roster }, services);
2005
+ }
2006
+ function resolveNodes(command, options, roster) {
2007
+ if (!roster && !options.local && command !== "doctor")
2008
+ throw new FleetUsageError("No fleet roster. Use fleet add or the interactive Register action; use fleet doctor --local for this machine.");
2009
+ if (options.node?.length && !roster)
2010
+ throw new FleetUsageError("Cannot select nodes without a roster. Use fleet add first.");
2011
+ return roster ? selectNodes(roster, options.node ?? []) : [];
2012
+ }
2013
+ function result(data, text) {
2014
+ return { exitCode: 0, data, ...text ? { text } : {} };
2015
+ }
2016
+ async function register(settings, options) {
2017
+ const node = nodeFromOptions(options);
2018
+ const config = await addNode(settings.config, node, options.apply === true);
2019
+ return result({
2020
+ version: 1,
2021
+ command: "add",
2022
+ ok: true,
2023
+ applied: options.apply === true,
2024
+ path: settings.config,
2025
+ node,
2026
+ nodes: config.nodes,
2027
+ next: options.apply ? "Run fleet onboard, then fleet doctor --all --json." : "Review this preview, then repeat with --apply --yes."
2028
+ });
2029
+ }
2030
+ async function initialize(settings, options) {
2031
+ const config = {
2032
+ ...fleet_example_default,
2033
+ $schema: new URL("./schema.json", import.meta.url).href
2034
+ };
2035
+ if (options.apply) {
2036
+ await mkdir3(dirname3(settings.config), { recursive: true, mode: 448 });
2037
+ try {
2038
+ await writeFile2(settings.config, `${JSON.stringify(config, null, 2)}
2039
+ `, {
2040
+ flag: "wx",
2041
+ mode: 384
2042
+ });
2043
+ } catch {
2044
+ throw new FleetUsageError("Cannot create roster. Existing files are never overwritten; use --config PATH.");
2045
+ }
2046
+ }
2047
+ return result({
2048
+ version: 1,
2049
+ command: "init",
2050
+ ok: true,
2051
+ applied: options.apply === true,
2052
+ path: settings.config,
2053
+ config,
2054
+ next: "This is example data, not discovered machines. Edit placeholders before doctor, or use fleet add to register real nodes."
2055
+ });
2056
+ }
2057
+ async function diagnose(nodes, options, settings, services) {
2058
+ const source = nodes.length ? await services.source() : undefined;
2059
+ const reports = await doctor(nodes, {
2060
+ root: settings.workspace,
2061
+ local: options.local === true,
2062
+ required: capabilities(options.require),
2063
+ timeoutMs: timeout(options),
2064
+ ...source ? { source } : {}
2065
+ }, services.run);
2066
+ if (!options.local && settings.missingRoster)
2067
+ reports.push(missingRoster());
2068
+ const report = {
2069
+ ...makeReport("doctor", reports),
2070
+ coverage: {
2071
+ scope: options.local ? "local" : options.node?.length ? "selected" : "all-configured",
2072
+ controller: true,
2073
+ configuredNodesTested: nodes.map((node) => node.id),
2074
+ rosterAvailable: !settings.missingRoster,
2075
+ note: "Directed SSH checks from this controller only. T3 relay/peer and VS Code forwarding require operator tests."
2076
+ }
2077
+ };
2078
+ return {
2079
+ exitCode: reportExitCode(report, options.strict === true),
2080
+ data: report,
2081
+ text: renderReport(report)
2082
+ };
2083
+ }
2084
+ function missingRoster() {
2085
+ return {
2086
+ node: "fleet",
2087
+ checks: [
2088
+ {
2089
+ id: "fleet:roster",
2090
+ status: "fail",
2091
+ summary: "No developer fleet roster is configured. Only this controller could be checked; fleet-wide health is unknown.",
2092
+ remedy: {
2093
+ action: "Open personal and choose Register a developer computer, or run personal fleet add --help. Then run fleet doctor --all."
2094
+ }
2095
+ }
2096
+ ]
2097
+ };
2098
+ }
2099
+ async function synchronize(nodes, options, settings, services) {
2100
+ if (!options.apply)
2101
+ return result({
2102
+ version: 1,
2103
+ command: "sync",
2104
+ ok: true,
2105
+ applied: false,
2106
+ targets: options.local ? ["local"] : nodes.map((node) => node.id),
2107
+ steps: syncPlan(options.pull === true),
2108
+ authorize: "Review the plan; repeat with --apply --yes to authorize changes."
2109
+ });
2110
+ const reports = options.local ? [
2111
+ {
2112
+ node: "local",
2113
+ checks: await syncWorkspace(settings.workspace, options.pull === true, services.run)
2114
+ }
2115
+ ] : await syncRemotes(nodes, options, services);
2116
+ const report = makeReport("sync", reports);
2117
+ return {
2118
+ exitCode: reportExitCode(report, options.strict === true),
2119
+ data: report,
2120
+ text: renderReport(report)
2121
+ };
2122
+ }
2123
+ async function syncRemotes(nodes, options, services) {
2124
+ const source = await services.source();
2125
+ return mapBounded(nodes, (node) => inspectRemote(node, {
2126
+ source,
2127
+ action: "sync",
2128
+ timeoutMs: timeout(options),
2129
+ pull: options.pull === true
2130
+ }, services.run), 1);
2131
+ }
2132
+
2133
+ // packages/personal-cli/src/program.ts
2134
+ var GLOBAL_OPTIONS = [
2135
+ {
2136
+ flag: "--json",
2137
+ description: "One versioned JSON document on stdout; never prompt"
2138
+ },
2139
+ { flag: "--non-interactive", description: "Never open interactive prompts" },
2140
+ {
2141
+ flag: "--workspace <path>",
2142
+ description: "Existing controller checkout; does not clone or change it"
2143
+ },
2144
+ { flag: "--config <path>", description: "Local, secret-free fleet roster" }
2145
+ ];
2146
+ function common(command) {
2147
+ return command.option("--node <id>", "Select an existing node; repeat for multiple nodes", collect, []).option("--local", "Controller only; no roster or SSH required").option("--all", "All configured nodes and the controller (doctor default)").option("--require <capabilities>", "Controller capabilities, comma separated, or none").option("--timeout <ms>", "Per-node deadline, 1000–1800000 milliseconds").option("--strict", "Warnings and skipped checks also produce exit 1").option("--pull", "Sync only: explicitly request a clean, fast-forward pull").option("--apply", "Authorize the displayed roster/sync mutation; requires --yes").option("--yes", "Confirm an explicitly requested --apply operation");
2148
+ }
2149
+ function collect(value, previous) {
2150
+ return [...previous, value];
2151
+ }
2152
+ function createProgram(version, output) {
2153
+ const program = new Command().name("personal").description("Developer command center · interactive for people, structured for agents").version(version).exitOverride().configureOutput({ writeOut: output, writeErr: () => {
2154
+ return;
2155
+ } }).showSuggestionAfterError(false).option("--interactive", "Open the interactive terminal application (TTY required)");
2156
+ for (const option of GLOBAL_OPTIONS)
2157
+ program.option(option.flag, option.description);
2158
+ return program;
2159
+ }
2160
+ function fleetCommands(program) {
2161
+ const fleet = program.command("fleet").description("Diagnose, explore and onboard developer computers");
2162
+ const definitions = commandCatalog().filter((entry) => entry.group === "Fleet" && entry.id !== "fleet doctor local");
2163
+ return definitions.map((entry) => {
2164
+ const command = common(fleet.command(entry.args[1]).description(entry.description));
2165
+ if (entry.args[1] === "add")
2166
+ command.requiredOption("--id <name>", "Unique lowercase node ID").requiredOption("--ssh <alias>", "Existing OpenSSH alias; never writes SSH configuration").requiredOption("--path <path>", "Absolute POSIX checkout path on the node").option("--runtime <path>", "Optional absolute Node/Bun runtime; defaults to node on SSH PATH");
2167
+ command.addHelpText("after", `
2168
+ Example: ${entry.examples[0]}
2169
+ Writes require both --apply and --yes. Diagnostics never log in or repair automatically.`);
2170
+ return command;
2171
+ });
2172
+ }
2173
+ function catalog(options, version) {
2174
+ const commands = commandCatalog(options.search, options.group);
2175
+ return {
2176
+ exitCode: 0,
2177
+ data: {
2178
+ version: 1,
2179
+ cliVersion: version,
2180
+ command: "commands",
2181
+ ok: true,
2182
+ commands,
2183
+ globalOptions: GLOBAL_OPTIONS,
2184
+ agentGuide: "personal agent --json",
2185
+ schema: "personal fleet schema --json"
2186
+ },
2187
+ text: [
2188
+ "Personal command catalog",
2189
+ ...commands.map((entry) => `
2190
+ ${entry.id}
2191
+ ${entry.description}`),
2192
+ `
2193
+ Run personal for the interactive application; add --json for agents.`
2194
+ ].join(`
2195
+ `)
2196
+ };
2197
+ }
2198
+ async function execute(args, defaults, version) {
2199
+ let outcome;
2200
+ let output = "";
2201
+ const program = createProgram(version, (text) => {
2202
+ output += text;
2203
+ });
2204
+ const commands = fleetCommands(program);
2205
+ for (const command of commands)
2206
+ command.action(async () => {
2207
+ const options = command.optsWithGlobals();
2208
+ const settings = await resolveSettings({
2209
+ ...defaults,
2210
+ ...settingsOverrides(options)
2211
+ });
2212
+ outcome = await executeFleet(command.name(), options, settings);
2213
+ });
2214
+ program.command("commands").description("Search the complete command catalog").option("--search <text>", "Filter names, descriptions and keywords").option("--group <name>", "Filter category, for example Fleet or Reference").action((_options, command) => {
2215
+ outcome = catalog(command.optsWithGlobals(), version);
2216
+ });
2217
+ program.command("agent").description("Machine-readable usage and safety contract").action(() => {
2218
+ outcome = { exitCode: 0, data: AGENT_GUIDE };
2219
+ });
2220
+ program.action(() => {
2221
+ outcome = catalog(program.opts(), version);
2222
+ });
2223
+ try {
2224
+ await program.parseAsync(args, { from: "user" });
2225
+ return outcome ?? {
2226
+ exitCode: 0,
2227
+ data: { version: 1, help: output.trim() },
2228
+ text: output.trim()
2229
+ };
2230
+ } catch (error) {
2231
+ return commandError(error, output);
2232
+ }
2233
+ }
2234
+ function settingsOverrides(options) {
2235
+ return {
2236
+ ...options.workspace ? { workspace: options.workspace } : {},
2237
+ ...options.config ? { config: options.config } : {}
2238
+ };
2239
+ }
2240
+ function commandError(error, output) {
2241
+ if (error instanceof CommanderError && error.exitCode === 0)
2242
+ return {
2243
+ exitCode: 0,
2244
+ data: { version: 1, help: output.trim() },
2245
+ text: output.trim()
2246
+ };
2247
+ const usage = error instanceof CommanderError || error instanceof FleetUsageError;
2248
+ return {
2249
+ exitCode: usage ? 2 : 1,
2250
+ data: {
2251
+ version: 1,
2252
+ ok: false,
2253
+ code: usage ? "invalid_usage" : "operation_failed",
2254
+ error: error instanceof FleetUsageError ? error.message : usage ? "Invalid command or option. Run personal --help or personal commands --json." : "Operation failed. Inspect configuration and permissions locally; raw errors are withheld to protect credentials."
2255
+ }
2256
+ };
2257
+ }
2258
+
2259
+ // packages/personal-cli/src/app.ts
2260
+ import { parseArgs } from "node:util";
2261
+ var VERSION = "0.1.0";
2262
+ function wantsInteractive(args, terminal, ci) {
2263
+ if (args.includes("--json") || args.includes("--non-interactive"))
2264
+ return false;
2265
+ return terminal && (args.includes("--interactive") || contextOnly(args) && !ci);
2266
+ }
2267
+ function startup(args) {
2268
+ return parseArgs({
2269
+ args,
2270
+ allowPositionals: true,
2271
+ strict: false,
2272
+ options: {
2273
+ workspace: { type: "string" },
2274
+ config: { type: "string" },
2275
+ interactive: { type: "boolean" },
2276
+ json: { type: "boolean" },
2277
+ "non-interactive": { type: "boolean" }
2278
+ }
2279
+ });
2280
+ }
2281
+ function contextOnly(args) {
2282
+ const parsed = startup(args);
2283
+ return parsed.positionals.length === 0 && Object.keys(parsed.values).every((key) => ["workspace", "config", "interactive"].includes(key));
2284
+ }
2285
+ async function main(args = process.argv.slice(2)) {
2286
+ const { values } = startup(args);
2287
+ const settings = await resolveSettings({
2288
+ ...typeof values.workspace === "string" ? { workspace: values.workspace } : {},
2289
+ ...typeof values.config === "string" ? { config: values.config } : {}
2290
+ });
2291
+ const terminal = process.stdin.isTTY === true && process.stderr.isTTY === true;
2292
+ if (args.includes("--interactive") && !terminal) {
2293
+ process.stdout.write(`${JSON.stringify({ version: 1, ok: false, code: "tty_required", error: "Interactive mode needs a terminal. Use personal commands --json or fleet doctor --local --json." })}
2294
+ `);
2295
+ return 2;
2296
+ }
2297
+ if (wantsInteractive(args, terminal, Boolean(process.env["CI"]))) {
2298
+ await Promise.resolve().then(() => init_ui());
2299
+ await startInteractive({
2300
+ ...settings,
2301
+ version: VERSION,
2302
+ commands: COMMANDS,
2303
+ execute: (command) => execute(command, settings, VERSION)
2304
+ });
2305
+ return 0;
2306
+ }
2307
+ const outcome = await execute(args.filter((arg) => arg !== "--interactive"), settings, VERSION);
2308
+ const text = args.includes("--json") ? JSON.stringify(outcome.data, null, 2) : outcome.text ?? JSON.stringify(outcome.data, null, 2);
2309
+ process.stdout.write(`${text}
2310
+ `);
2311
+ return outcome.exitCode;
2312
+ }
2313
+
2314
+ // packages/personal-cli/src/cli.ts
2315
+ try {
2316
+ process.exitCode = await main();
2317
+ } catch (error) {
2318
+ const cancelled = error instanceof Error && ["ExitPromptError", "AbortPromptError"].includes(error.name);
2319
+ process.stdout.write(`${JSON.stringify({
2320
+ version: 1,
2321
+ ok: false,
2322
+ code: cancelled ? "cancelled" : "operation_failed",
2323
+ error: cancelled ? "Cancelled; no further actions will run." : "Command failed. Inspect the local setup; raw errors are withheld to protect credentials."
2324
+ })}
2325
+ `);
2326
+ process.exitCode = cancelled ? 130 : 1;
2327
+ }