@crvouga/personal 0.0.0-stage → 0.1.1

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,4207 @@
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))
82
+ return [];
83
+ const nodes = Array.isArray(data["nodes"]) ? data["nodes"] : [];
84
+ return [
85
+ ...nodes.flatMap(nodeChecks),
86
+ ...nodeChecks({ node: "tailscale", checks: data["checks"] })
87
+ ];
88
+ }
89
+ function nodeChecks(value) {
90
+ if (!record2(value) || !Array.isArray(value["checks"]))
91
+ return [];
92
+ const node = text(value["node"], "controller");
93
+ return value["checks"].flatMap((check) => {
94
+ if (!record2(check) || !isStatus(check["status"]))
95
+ return [];
96
+ const remedy = record2(check["remedy"]) ? check["remedy"] : {};
97
+ return [
98
+ {
99
+ node,
100
+ id: text(check["id"], "check"),
101
+ status: check["status"],
102
+ summary: text(check["summary"]),
103
+ remedy: text(remedy["action"]),
104
+ command: text(remedy["command"])
105
+ }
106
+ ];
107
+ });
108
+ }
109
+ function isStatus(value) {
110
+ return ["pass", "warn", "fail", "skip"].includes(String(value));
111
+ }
112
+ function isCancellation(error) {
113
+ if (!record2(error))
114
+ return false;
115
+ return ["ExitPromptError", "AbortPromptError", "AbortError"].includes(text(error["name"]));
116
+ }
117
+ function absolutePath2(value) {
118
+ return value.startsWith("/") && value.length <= 4096 && !/[\x00-\x1f\x7f]/.test(value) || "Enter an absolute POSIX path (no ~ or control characters).";
119
+ }
120
+ var init_ui_data = __esm(() => {
121
+ init_search();
122
+ });
123
+
124
+ // packages/personal-cli/src/ui-repairs.ts
125
+ function showRepairs(session, data) {
126
+ const catalog = data["command"] === "repairs";
127
+ const entries = data[catalog ? "recipes" : "steps"];
128
+ if (!Array.isArray(entries))
129
+ return;
130
+ const steps = entries.filter(record2);
131
+ session.display.table(["Computer / owner", "Repair", "Execution"], steps.slice(0, 15).map((step) => [
132
+ text(step[catalog ? "owner" : "node"]),
133
+ text(step["title"]),
134
+ step["automatic"] === true && (catalog || step["eligible"] === true) ? "Owner command after explicit apply" : "Operator action; inspect instructions"
135
+ ]));
136
+ if (steps.length > 15)
137
+ session.display.write(`Showing 15 of ${steps.length} repair entries. Inspect structured details for the complete plan and native login commands.`);
138
+ if (!catalog)
139
+ session.display.write(data["applied"] === true ? `Owner commands completed: ${text(data["completed"])}. Fresh health verified: ${data["healthOk"] === true ? "yes" : "no; review remaining checks"}.` : "Preview only. Review the repair effects and native login instructions in structured details before applying.");
140
+ }
141
+ var init_ui_repairs = __esm(() => {
142
+ init_ui_data();
143
+ });
144
+
145
+ // packages/personal-cli/src/ui-execute.ts
146
+ async function execute2(session, args, title) {
147
+ session.display.write(`
148
+ $ ${reproduction(session, args)}`);
149
+ const progress = session.display.progress(title);
150
+ try {
151
+ return await session.options.execute(callArgs(session, args));
152
+ } catch (error) {
153
+ if (isCancellation(error))
154
+ throw error;
155
+ return { exitCode: 1, data: { ok: false, code: "operation_failed" } };
156
+ } finally {
157
+ progress.stop();
158
+ }
159
+ }
160
+ function successful(outcome) {
161
+ return outcome.exitCode === 0 && !(record2(outcome.data) && outcome.data["ok"] === false);
162
+ }
163
+ function showOutcome(session, outcome) {
164
+ const checks = reportChecks(outcome.data);
165
+ const incomplete = checks.filter((check) => ["warn", "skip"].includes(check.status)).length;
166
+ const state = outcomeState(outcome, incomplete);
167
+ const counts = ["pass", "warn", "fail", "skip"].map((status) => `${checks.filter((check) => check.status === status).length} ${status}`);
168
+ session.display.panel(state, checks.length ? counts.join(" · ") : outcomeMessage(outcome));
169
+ showAttention(session, checks);
170
+ showData(session, outcome);
171
+ if (!successful(outcome))
172
+ session.display.write(record2(outcome.data) && outcome.data["applied"] === true ? "Actions were attempted. Review execution results, partial changes and fresh checks before retrying." : "No automatic repair was attempted. Review the command, context and setup.");
173
+ }
174
+ function outcomeMessage(outcome) {
175
+ if (!successful(outcome))
176
+ return failureMessage(outcome.data);
177
+ if (record2(outcome.data) && outcome.data["applied"] === false) {
178
+ return "Preview only — no changes applied. Review before explicitly authorizing a write.";
179
+ }
180
+ return "Command completed. Warnings and skipped checks are not passes.";
181
+ }
182
+ function failureMessage(data) {
183
+ if (!record2(data))
184
+ return "Operation failed. Raw errors and subprocess output are intentionally hidden.";
185
+ if (data["code"] === "invalid_usage" && typeof data["error"] === "string")
186
+ return clean(data["error"]);
187
+ const messages = {
188
+ invalid_usage: "Configuration or arguments need attention. Use Fleet setup, validate, or session context.",
189
+ config_missing: "No roster found. Add a node in Fleet setup, or run controller-only diagnostics.",
190
+ operation_failed: "Operation failed safely. Check connectivity and configuration before retrying."
191
+ };
192
+ return messages[String(data["code"])] ?? "Operation did not complete. Review setup and the noninteractive command.";
193
+ }
194
+ function showNodes(session, data) {
195
+ const nodes = Array.isArray(data["nodes"]) ? data["nodes"].filter(record2) : [];
196
+ if (!nodes.length) {
197
+ session.display.write("No computers registered yet. Choose Fleet: Register a developer computer to add one, or Fleet: Discover Tailscale computers.");
198
+ return;
199
+ }
200
+ session.display.table(["Computer", "SSH", "Workspace"], nodes.map((node) => [
201
+ text(node["id"]),
202
+ text(node["ssh"]),
203
+ text(node["workspace"])
204
+ ]));
205
+ }
206
+ function showComputers(session, data) {
207
+ const computers = Array.isArray(data["computers"]) ? data["computers"].filter(record2) : [];
208
+ session.display.table(["Computer", "OS", "Online"], computers.map((peer) => [
209
+ text(peer["dns"]),
210
+ text(peer["os"]),
211
+ peer["online"] === true ? "yes" : "no"
212
+ ]));
213
+ session.display.write("Visible Tailscale computers only; mobile devices excluded. Register each remote computer with its existing SSH alias and checkout path before full health checks.");
214
+ }
215
+ function showAttention(session, checks) {
216
+ const attention = checks.filter((check) => check.status !== "pass");
217
+ if (!attention.length)
218
+ return;
219
+ session.display.table(["Computer", "Check", "Status / next step"], attention.slice(0, 15).map((check) => [
220
+ check.node,
221
+ check.id,
222
+ `${check.status}: ${check.remedy ?? check.summary}`
223
+ ]));
224
+ if (attention.length > 15)
225
+ session.display.write(`Showing 15 of ${attention.length} checks needing attention. Explore checks and remedies for the complete report.`);
226
+ }
227
+ function showData(session, outcome) {
228
+ if (!record2(outcome.data))
229
+ return;
230
+ if (["repair", "repairs"].includes(String(outcome.data["command"])))
231
+ showRepairs(session, outcome.data);
232
+ if (successful(outcome) && outcome.data["command"] === "nodes")
233
+ showNodes(session, outcome.data);
234
+ if (outcome.data["command"] === "discover")
235
+ showComputers(session, outcome.data);
236
+ }
237
+ function outcomeState(outcome, incomplete) {
238
+ if (record2(outcome.data) && outcome.data["applied"] === false)
239
+ return "PREVIEW";
240
+ if (!successful(outcome))
241
+ return "FAILED";
242
+ return incomplete ? "WARNING" : "SUCCESS";
243
+ }
244
+ var init_ui_execute = __esm(() => {
245
+ init_ui_repairs();
246
+ init_ui_data();
247
+ });
248
+
249
+ // packages/personal-cli/src/ui-reports.ts
250
+ async function inspectChecks(session, data) {
251
+ const checks = reportChecks(data);
252
+ let node = "*";
253
+ let status = "*";
254
+ while (true) {
255
+ const visible = checks.filter((check) => (node === "*" || check.node === node) && (status === "*" || check.status === status));
256
+ const choice = await session.prompts.search(`Report · ${visible.length}/${checks.length} checks · node ${node} · status ${status}`, (term) => checkChoices(visible, term));
257
+ if (choice === "back")
258
+ return;
259
+ if (choice === "node")
260
+ node = await nodeFilter(session, checks);
261
+ else if (choice === "status")
262
+ status = await statusFilter(session);
263
+ else {
264
+ const check = visible[Number(choice)];
265
+ if (check)
266
+ showCheck(session, check);
267
+ }
268
+ }
269
+ }
270
+ function checkChoices(checks, term) {
271
+ const choices = checks.flatMap((check, index) => {
272
+ const name = `${check.status.toUpperCase()} · ${check.node} · ${check.id}`;
273
+ return `${name} ${check.summary}`.toLowerCase().includes(term.toLowerCase()) ? [{ name, value: String(index), description: check.summary }] : [];
274
+ });
275
+ return [
276
+ ...choices.length ? choices : [
277
+ {
278
+ name: "No matching checks — change search or filters",
279
+ value: "none"
280
+ }
281
+ ],
282
+ { name: "Filter by node", value: "node" },
283
+ { name: "Filter by status", value: "status" },
284
+ { name: "Back to result", value: "back" }
285
+ ];
286
+ }
287
+ function nodeFilter(session, checks) {
288
+ return session.prompts.select("Node filter", [
289
+ { name: "All nodes", value: "*" },
290
+ ...[...new Set(checks.map((check) => check.node))].map((node) => ({
291
+ name: node,
292
+ value: node
293
+ }))
294
+ ]);
295
+ }
296
+ function statusFilter(session) {
297
+ return session.prompts.select("Status filter", [
298
+ { name: "All statuses (including warnings and skips)", value: "*" },
299
+ ...["fail", "warn", "skip", "pass"].map((status) => ({
300
+ name: status.toUpperCase(),
301
+ value: status
302
+ }))
303
+ ]);
304
+ }
305
+ function showCheck(session, check) {
306
+ session.display.panel(`${check.status.toUpperCase()} · ${check.node} · ${check.id}`, check.summary);
307
+ session.display.write(`Remedy: ${check.remedy || "No automated remedy. Review this check manually."}`);
308
+ if (check.command)
309
+ session.display.write(`Suggested operator command (not executed):
310
+ ${check.command}`);
311
+ session.display.write("Remedies are guidance only. Credentials, SSH and host-key configuration stay operator-owned.");
312
+ }
313
+ async function inspectDetails(session, data) {
314
+ const sections = detailSections(data);
315
+ if (!sections.length) {
316
+ session.display.write("No additional safe display fields. Use the reproducible command for the structured document.");
317
+ return;
318
+ }
319
+ let next = true;
320
+ while (next) {
321
+ const selection = await session.prompts.select("Details", [
322
+ ...sections.map((section, index) => ({
323
+ name: section.title,
324
+ value: String(index)
325
+ })),
326
+ { name: "Back to result", value: "back" }
327
+ ]);
328
+ const section = sections[Number(selection)];
329
+ if (selection === "back")
330
+ next = false;
331
+ else if (section)
332
+ session.display.panel(section.title, section.body);
333
+ }
334
+ }
335
+ function detailSections(data) {
336
+ if (!record2(data))
337
+ return [];
338
+ const sections = [
339
+ "help",
340
+ "guide",
341
+ "next",
342
+ "authorize",
343
+ "path",
344
+ "sshTemplate",
345
+ "workspaceCommand",
346
+ "verifyCommand",
347
+ "invocation",
348
+ "discovery"
349
+ ].flatMap((key) => typeof data[key] === "string" ? [{ title: key, body: text(data[key]) }] : []);
350
+ for (const key of ["steps", "targets", "safety", "examples"]) {
351
+ if (Array.isArray(data[key]))
352
+ sections.push(...data[key].flatMap((value, index) => typeof value === "string" ? [{ title: `${key} · ${index + 1}`, body: text(value) }] : []));
353
+ }
354
+ if (Array.isArray(data["nodes"]))
355
+ sections.push(...nodeSections(data["nodes"]));
356
+ if (Array.isArray(data["commands"]))
357
+ sections.push(...commandSections(data["commands"]));
358
+ for (const key of ["configuration", "output", "coverage"]) {
359
+ if (record2(data[key]))
360
+ sections.push({ title: key, body: safeFields(data[key]) });
361
+ }
362
+ if (typeof data["$schema"] === "string")
363
+ sections.push({
364
+ title: "JSON schema",
365
+ body: JSON.stringify(data, null, 2)
366
+ });
367
+ return sections;
368
+ }
369
+ function commandSections(commands) {
370
+ return commands.flatMap((command) => {
371
+ if (!record2(command))
372
+ return [];
373
+ return [
374
+ {
375
+ title: text(command["id"], "Command"),
376
+ body: [
377
+ text(command["title"]),
378
+ text(command["description"]),
379
+ ...Array.isArray(command["examples"]) ? command["examples"].map((value) => text(value)) : []
380
+ ].filter(Boolean).join(`
381
+ `)
382
+ }
383
+ ];
384
+ });
385
+ }
386
+ function safeFields(value) {
387
+ return Object.entries(value).flatMap(([key, field]) => typeof field === "string" ? [`${key}: ${text(field)}`] : []).join(`
388
+ `);
389
+ }
390
+ function nodeSections(nodes) {
391
+ return nodes.flatMap((node) => {
392
+ if (!record2(node))
393
+ return [];
394
+ const id = text(node["id"], text(node["node"], "Node"));
395
+ const body = ["id", "ssh", "workspace", "runtime"].flatMap((key) => typeof node[key] === "string" ? [`${key}: ${text(node[key])}`] : []);
396
+ if (Array.isArray(node["required"]))
397
+ body.push(`required: ${node["required"].map((value) => text(value)).join(", ") || "none"}`);
398
+ return [
399
+ ...body.length ? [{ title: id, body: body.join(`
400
+ `) }] : [],
401
+ ...detailSections({
402
+ steps: node["steps"],
403
+ sshTemplate: node["sshTemplate"],
404
+ verifyCommand: node["verifyCommand"]
405
+ }).map((section) => ({
406
+ title: `${id} · ${section.title}`,
407
+ body: section.body
408
+ }))
409
+ ];
410
+ });
411
+ }
412
+ var init_ui_reports = __esm(() => {
413
+ init_ui_data();
414
+ });
415
+
416
+ // packages/personal-cli/src/ui-targets.ts
417
+ async function chooseTargets(session, action) {
418
+ const selection = await session.prompts.select("Choose target scope", [
419
+ ...action === "onboard" ? [] : [
420
+ {
421
+ name: "Controller only / local",
422
+ value: "local",
423
+ description: "No roster or SSH required; uses this session workspace."
424
+ },
425
+ {
426
+ name: allTargetsName(action),
427
+ value: "all",
428
+ description: allTargetsDescription(action)
429
+ }
430
+ ],
431
+ { name: "Select roster nodes…", value: "selected" },
432
+ { name: "Back to palette", value: "back" }
433
+ ]);
434
+ if (selection === "back")
435
+ return;
436
+ if (selection === "local")
437
+ return ["--local"];
438
+ if (selection === "all" && action === "repair")
439
+ return [];
440
+ const nodes = await availableNodes(session);
441
+ if (!nodes.length)
442
+ return missingRoster2(session, action);
443
+ if (selection === "all")
444
+ return [];
445
+ const selected = await session.prompts.checkbox("Select nodes · space to toggle, enter to continue", nodes);
446
+ if (!selected.length) {
447
+ session.display.write("No nodes selected. Nothing ran; returning to the palette.");
448
+ return;
449
+ }
450
+ return selected.flatMap((id) => ["--node", id]);
451
+ }
452
+ async function availableNodes(session) {
453
+ const outcome = await execute2(session, ["fleet", "nodes"], "Reading local roster (no SSH)");
454
+ if (!successful(outcome)) {
455
+ showOutcome(session, outcome);
456
+ return [];
457
+ }
458
+ if (!record2(outcome.data) || !Array.isArray(outcome.data["nodes"]))
459
+ return [];
460
+ return outcome.data["nodes"].flatMap((node) => {
461
+ if (!record2(node) || typeof node["id"] !== "string")
462
+ return [];
463
+ return [
464
+ {
465
+ name: text(node["id"]),
466
+ value: node["id"],
467
+ description: `${text(node["ssh"])} · ${text(node["workspace"])}`
468
+ }
469
+ ];
470
+ });
471
+ }
472
+ async function missingRoster2(session, action) {
473
+ session.display.panel("Fleet setup needed", [
474
+ "No usable roster nodes. Return to Fleet setup / add node to register an existing computer.",
475
+ "Fleet init creates an EXAMPLE only; saving it requires --apply --yes. Edit its aliases and paths before use.",
476
+ "Controller-only doctor works without a roster or SSH. No login, host-key change or repair is automatic."
477
+ ].join(`
478
+ `));
479
+ if (action !== "doctor")
480
+ return;
481
+ const choice = await session.prompts.select("Continue without a roster?", [
482
+ { name: "Run controller-only diagnostics", value: "local" },
483
+ { name: "Back to palette / Fleet setup", value: "back" }
484
+ ]);
485
+ return choice === "local" ? ["--local"] : undefined;
486
+ }
487
+ function allTargetsName(action) {
488
+ return action === "repair" ? "All visible Tailscale computers" : "All roster nodes";
489
+ }
490
+ function allTargetsDescription(action) {
491
+ if (action === "doctor")
492
+ return "Controller + every configured node; read-only.";
493
+ return action === "repair" ? "Inspect this controller and every visible computer; preview repairs before applying." : "Every configured remote checkout.";
494
+ }
495
+ var init_ui_targets = __esm(() => {
496
+ init_ui_execute();
497
+ init_ui_data();
498
+ });
499
+
500
+ // packages/personal-cli/src/ui-actions.ts
501
+ async function runCommand2(session, command) {
502
+ session.display.panel(clean(command.title), clean(command.description));
503
+ const args = await actionArgs(session, command);
504
+ if (!args)
505
+ return;
506
+ const preview = args[0] === "fleet" && ["sync", "init", "repair"].includes(args[1] ?? "");
507
+ if (args[1] === "init")
508
+ session.display.write("EXAMPLE ONLY: init previews a starter roster, not a configured fleet. Saving requires --apply --yes.");
509
+ const mutation = !preview && (command.mutation || args.includes("--apply"));
510
+ if (mutation && !await confirmMutation(session, args))
511
+ return;
512
+ await runWithResults(session, args, command.title, { preview, mutation });
513
+ }
514
+ async function actionArgs(session, command) {
515
+ const args = [...command.args];
516
+ if (args[0] !== "fleet")
517
+ return args;
518
+ if (args.includes("--local"))
519
+ return args;
520
+ const action = args[1] ?? "";
521
+ if (["sync", "init", "repair"].includes(action)) {
522
+ const preview = args.filter((arg) => !["--apply", "--yes"].includes(arg));
523
+ if (action === "init")
524
+ return preview;
525
+ const targets = await chooseTargets(session, action);
526
+ return targets ? [...preview, ...targets] : undefined;
527
+ }
528
+ if (!["doctor", "onboard"].includes(action))
529
+ return args;
530
+ const targets = await chooseTargets(session, action);
531
+ return targets ? [...args, ...targets] : undefined;
532
+ }
533
+ async function runWithResults(session, initialArgs, title, settings = {}) {
534
+ let args = initialArgs;
535
+ let outcome = await execute2(session, args, title);
536
+ let preview = settings.preview === true;
537
+ let mutation = settings.mutation === true;
538
+ while (true) {
539
+ showOutcome(session, outcome);
540
+ const selection = await session.prompts.select("Next step", resultChoices(outcome, preview));
541
+ if (selection === "back")
542
+ return;
543
+ await inspectSelection(session, outcome, selection);
544
+ if (selection === "apply") {
545
+ const appliedArgs = applyArgs(args, outcome);
546
+ if (!await confirmMutation(session, appliedArgs))
547
+ continue;
548
+ args = appliedArgs;
549
+ preview = false;
550
+ mutation = true;
551
+ outcome = await execute2(session, args, title);
552
+ }
553
+ if (selection === "retry") {
554
+ if (mutation && !await confirmMutation(session, args))
555
+ continue;
556
+ outcome = await execute2(session, args, title);
557
+ }
558
+ }
559
+ }
560
+ async function inspectSelection(session, outcome, selection) {
561
+ if (selection === "checks")
562
+ await inspectChecks(session, outcome.data);
563
+ if (selection === "details")
564
+ await inspectDetails(session, outcome.data);
565
+ }
566
+ function resultChoices(outcome, preview) {
567
+ return [
568
+ { name: "Back to commands", value: "back" },
569
+ ...reportChecks(outcome.data).length ? [{ name: "Explore checks and remedies…", value: "checks" }] : [],
570
+ { name: "Inspect structured details / plan…", value: "details" },
571
+ ...preview && successful(outcome) ? [{ name: "Review and explicitly apply…", value: "apply" }] : [],
572
+ ...!successful(outcome) ? [{ name: "Retry command…", value: "retry" }] : []
573
+ ];
574
+ }
575
+ async function confirmMutation(session, args) {
576
+ const action = args[1];
577
+ const warning = action === "repair" ? "Repair runs the eligible owner actions in the displayed plan, including trusted setup/install scripts. Workstation actions may read this machine’s Vault session and write managed 0600 provider configuration. No login session is transferred between computers. Failures can leave partial changes; fresh diagnostics follow." : 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.";
578
+ session.display.panel("Explicit approval required", [
579
+ action === "init" ? "Create an EXAMPLE roster only. Replace sample aliases and paths before diagnostics." : warning,
580
+ `Workspace: ${clean(session.workspace ?? session.options.workspace)}`,
581
+ `Config: ${clean(session.config ?? session.options.config)}`,
582
+ `$ ${reproduction(session, args)}`
583
+ ].join(`
584
+ `));
585
+ return session.prompts.confirm("Apply this exact command? (default: no)");
586
+ }
587
+ function applyArgs(args, outcome) {
588
+ const guard = args[1] === "repair" && record2(outcome.data) && typeof outcome.data["planHash"] === "string" ? ["--plan-hash", outcome.data["planHash"]] : [];
589
+ return [...args, ...guard, "--apply", "--yes"];
590
+ }
591
+ var init_ui_actions = __esm(() => {
592
+ init_ui_execute();
593
+ init_ui_data();
594
+ init_ui_reports();
595
+ init_ui_targets();
596
+ });
597
+
598
+ // packages/personal-cli/src/ui-wizard.ts
599
+ async function addNode2(session) {
600
+ showIntro(session);
601
+ await registerNode(session);
602
+ }
603
+ function showIntro(session) {
604
+ session.display.panel("Fleet setup / add node", [
605
+ "Register an EXISTING computer in your local secret-free roster.",
606
+ "Use a previously verified SSH alias. This wizard never connects, provisions, logs in or changes host keys.",
607
+ "Cancel with Ctrl+C. No write occurs until you approve the preview."
608
+ ].join(`
609
+ `));
610
+ }
611
+ async function registerNode(session) {
612
+ const id = await session.prompts.input("Node ID (lowercase, e.g. dev-linux)", "", validId);
613
+ const alias = await session.prompts.input("Existing SSH alias / hostname", id, validAlias);
614
+ const workspace = await session.prompts.input("Absolute POSIX workspace on the node", "", absolutePath2);
615
+ const runtime = await session.prompts.input("Optional absolute Node/Bun executable (blank = node on SSH PATH)", "", (value) => value === "" || absolutePath2(value));
616
+ const capabilities = await session.prompts.checkbox("Required capabilities · space to toggle", CAPABILITIES2.map((capability) => ({
617
+ name: capability,
618
+ value: capability,
619
+ checked: capability !== "vscode"
620
+ })));
621
+ const args = [
622
+ "fleet",
623
+ "add",
624
+ "--id",
625
+ id,
626
+ "--ssh",
627
+ alias,
628
+ "--path",
629
+ workspace,
630
+ ...runtime ? ["--runtime", runtime] : [],
631
+ "--require",
632
+ capabilities.join(",") || "none",
633
+ "--apply",
634
+ "--yes"
635
+ ];
636
+ session.display.table(["Field", "Preview"], [
637
+ ["ID", clean(id)],
638
+ ["SSH", clean(alias)],
639
+ ["Workspace", clean(workspace)],
640
+ ["Runtime", clean(runtime) || "node (remote non-login PATH)"],
641
+ ["Required", capabilities.join(", ") || "none"]
642
+ ]);
643
+ session.display.write(`$ ${reproduction(session, args)}`);
644
+ if (!await confirmMutation(session, args)) {
645
+ session.display.write("Cancelled — no roster was written.");
646
+ return;
647
+ }
648
+ await runWithResults(session, args, "Saving roster node (no SSH)", {
649
+ mutation: true
650
+ });
651
+ session.display.write("Next: fleet validate, then fleet onboard for operator-owned SSH / authentication guidance.");
652
+ }
653
+ function validId(value) {
654
+ return /^[a-z][a-z0-9-]{0,62}$/.test(value) || "Use 1–63 lowercase letters, digits or hyphens; start with a letter.";
655
+ }
656
+ function validAlias(value) {
657
+ 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.";
658
+ }
659
+ var CAPABILITIES2;
660
+ var init_ui_wizard = __esm(() => {
661
+ init_ui_actions();
662
+ init_ui_data();
663
+ CAPABILITIES2 = ["github", "vault", "t3", "vscode", "toolchain"];
664
+ });
665
+
666
+ // packages/personal-cli/src/ui-controller.ts
667
+ async function runInteractive(options, prompts, display) {
668
+ const session = { options, prompts, display };
669
+ showHeader(session);
670
+ let running = true;
671
+ while (running) {
672
+ try {
673
+ const choice = await prompts.search("Command palette · type to search · arrows + enter", (term) => paletteChoices(session, term));
674
+ running = await dispatch(session, choice);
675
+ } catch (error) {
676
+ if (isCancellation(error)) {
677
+ display.write(`
678
+ Goodbye. No further actions will run.`);
679
+ throw error;
680
+ }
681
+ display.write("The interactive session could not continue safely. Use personal commands --json for noninteractive help.");
682
+ throw new Error("Interactive session failed safely.");
683
+ }
684
+ }
685
+ display.write("Goodbye. Session context was not saved.");
686
+ }
687
+ function showHeader(session) {
688
+ session.display.panel(`personal · ${clean(session.options.version)} · operator console`, [
689
+ "Your developer fleet, without the guesswork.",
690
+ `Workspace: ${clean(session.workspace ?? session.options.workspace)}`,
691
+ `Config: ${clean(session.config ?? session.options.config)}`,
692
+ "Read-only first · explicit writes · no automatic repairs",
693
+ "Search IDs, keywords or descriptions. Ctrl+C exits safely. Nothing is cleared."
694
+ ].join(`
695
+ `));
696
+ }
697
+ function paletteChoices(session, term) {
698
+ const commands = commandChoices(session.options.commands, term);
699
+ const navigation = [
700
+ {
701
+ name: "Session: Workspace / config…",
702
+ value: "context",
703
+ description: "Session-only overrides; no hidden configuration writes."
704
+ },
705
+ { name: "Exit", value: "exit", description: "Close the command palette." }
706
+ ].filter((choice) => `${choice.name} ${choice.description}`.toLowerCase().includes(term.toLowerCase().trim()));
707
+ const choices = [...navigation, ...commands].sort((a, b) => a.name.localeCompare(b.name, "en"));
708
+ return choices.length ? choices : [{ name: "No matching commands — change search", value: "none" }];
709
+ }
710
+ async function dispatch(session, choice) {
711
+ if (choice === "exit")
712
+ return false;
713
+ if (choice === "context")
714
+ await changeContext(session);
715
+ if (choice.startsWith("command:"))
716
+ await openCommand(session, choice.slice(8));
717
+ return true;
718
+ }
719
+ async function openCommand(session, id) {
720
+ const command = session.options.commands.find((entry) => entry.id === id);
721
+ if (!command)
722
+ return;
723
+ if (command.args[0] === "fleet" && command.args[1] === "add")
724
+ await addNode2(session);
725
+ else
726
+ await runCommand2(session, command);
727
+ }
728
+ async function changeContext(session) {
729
+ session.display.write("Session only. Blank inputs reset to startup defaults; no file is written.");
730
+ const workspace = await session.prompts.input("Workspace override", session.workspace ?? "", validContext);
731
+ const config = await session.prompts.input("Config override", session.config ?? "", validContext);
732
+ if (workspace)
733
+ session.workspace = workspace;
734
+ else
735
+ delete session.workspace;
736
+ if (config)
737
+ session.config = config;
738
+ else
739
+ delete session.config;
740
+ showHeader(session);
741
+ }
742
+ function validContext(value) {
743
+ return !/[\x00-\x1f\x7f]/.test(value) || "Control characters are not allowed in paths.";
744
+ }
745
+ var init_ui_controller = __esm(() => {
746
+ init_ui_actions();
747
+ init_ui_data();
748
+ init_ui_wizard();
749
+ });
750
+
751
+ // packages/personal-cli/src/ui-terminal.ts
752
+ import { checkbox, confirm, input, search, select } from "@inquirer/prompts";
753
+ import { Chalk } from "chalk";
754
+ import ora from "ora";
755
+ import boxen from "boxen";
756
+ import Table from "cli-table3";
757
+ function plainTerminal() {
758
+ return process.env["NO_COLOR"] !== undefined || process.env["TERM"] === "dumb";
759
+ }
760
+ function pageSize() {
761
+ return Math.max(3, Math.min(10, (process.stderr.rows || 24) - 10));
762
+ }
763
+ function terminalPrompts() {
764
+ const context = { output: process.stderr, clearPromptOnDone: false };
765
+ return {
766
+ select: (message, choices) => select({ message, choices, pageSize: pageSize(), loop: false }, context),
767
+ search: (message, source) => search({
768
+ message,
769
+ source: (term) => source(term ?? ""),
770
+ pageSize: pageSize(),
771
+ theme: { style: { highlight: (value) => value } }
772
+ }, context),
773
+ checkbox: (message, choices) => checkbox({ message, choices, pageSize: pageSize(), loop: false }, context),
774
+ input: (message, initial = "", validate = () => true) => input({ message, default: initial, validate }, context),
775
+ confirm: (message) => confirm({ message, default: false }, context)
776
+ };
777
+ }
778
+ function terminalDisplay() {
779
+ const color = new Chalk({
780
+ level: !plainTerminal() && process.stderr.isTTY ? 1 : 0
781
+ });
782
+ const write = (message) => {
783
+ process.stderr.write(`${message}
784
+ `);
785
+ };
786
+ return {
787
+ write,
788
+ panel: (title, body) => {
789
+ const content = `${color.bold(clean(title))}
790
+ ${body}`;
791
+ if ((process.stderr.columns || 80) < 48 || plainTerminal()) {
792
+ write(`
793
+ ${content}
794
+ `);
795
+ return;
796
+ }
797
+ write(boxen(content, {
798
+ padding: 1,
799
+ borderStyle: "round",
800
+ ...color.level ? { borderColor: "cyan" } : {},
801
+ width: Math.min(process.stderr.columns || 80, 96)
802
+ }));
803
+ },
804
+ table: (headers, rows) => drawTable(write, headers, rows),
805
+ progress: (message) => {
806
+ const spinner = ora({
807
+ text: message,
808
+ stream: process.stderr,
809
+ isEnabled: Boolean(process.stderr.isTTY) && !plainTerminal(),
810
+ isSilent: !process.stderr.isTTY || plainTerminal(),
811
+ discardStdin: false
812
+ });
813
+ if (spinner.isEnabled)
814
+ spinner.start();
815
+ else
816
+ write(`… ${message}`);
817
+ return {
818
+ stop: () => {
819
+ spinner.stop();
820
+ }
821
+ };
822
+ }
823
+ };
824
+ }
825
+ function drawTable(write, headers, rows) {
826
+ if ((process.stderr.columns || 80) < 64 || plainTerminal()) {
827
+ rows.forEach((row) => write(row.map((cell) => clean(cell)).join(" · ")));
828
+ return;
829
+ }
830
+ const width = Math.floor(((process.stderr.columns || 80) - headers.length * 3 - 1) / headers.length);
831
+ const table = new Table({
832
+ head: headers,
833
+ colWidths: headers.map(() => Math.max(8, width)),
834
+ wordWrap: true,
835
+ style: { head: [], border: [] }
836
+ });
837
+ table.push(...rows.map((row) => row.map((cell) => clean(cell))));
838
+ write(table.toString());
839
+ }
840
+ var init_ui_terminal = __esm(() => {
841
+ init_ui_data();
842
+ });
843
+
844
+ // packages/personal-cli/src/ui.ts
845
+ async function startInteractive(options) {
846
+ await runInteractive(options, terminalPrompts(), terminalDisplay());
847
+ }
848
+ var init_ui = __esm(() => {
849
+ init_ui_controller();
850
+ init_ui_terminal();
851
+ });
852
+
853
+ // packages/personal-cli/src/workstation.ts
854
+ import { join } from "node:path";
855
+
856
+ // packages/fleet/src/runner.ts
857
+ import { spawn } from "node:child_process";
858
+ var MAX_OUTPUT_BYTES = 1024 * 1024;
859
+ function terminate(child) {
860
+ try {
861
+ if (process.platform !== "win32" && child.pid)
862
+ process.kill(-child.pid, "SIGKILL");
863
+ else
864
+ child.kill("SIGKILL");
865
+ } catch {}
866
+ }
867
+ function runCommand(command, options = {}) {
868
+ return new Promise((resolve) => {
869
+ const child = spawn(command[0], command.slice(1), {
870
+ cwd: options.cwd,
871
+ env: {
872
+ ...process.env,
873
+ ...options.env,
874
+ GIT_TERMINAL_PROMPT: "0",
875
+ CI: "1"
876
+ },
877
+ stdio: "pipe",
878
+ windowsHide: true,
879
+ detached: process.platform !== "win32"
880
+ });
881
+ let stdout = "";
882
+ let stderr = "";
883
+ let bytes = 0;
884
+ let finished = false;
885
+ let reason = "exit";
886
+ const finish = (ok) => {
887
+ if (finished)
888
+ return;
889
+ finished = true;
890
+ clearTimeout(timer);
891
+ child.stdin.destroy();
892
+ child.stdout.destroy();
893
+ child.stderr.destroy();
894
+ resolve({ ok, stdout, stderr, reason });
895
+ };
896
+ const abort = (cause) => {
897
+ reason = cause;
898
+ terminate(child);
899
+ finish(false);
900
+ };
901
+ const timer = setTimeout(() => {
902
+ abort("timeout");
903
+ }, options.timeoutMs ?? 15000);
904
+ const capture = (stream, chunk) => {
905
+ if (finished)
906
+ return;
907
+ bytes += Buffer.byteLength(chunk);
908
+ if (bytes > MAX_OUTPUT_BYTES) {
909
+ abort("overflow");
910
+ return;
911
+ }
912
+ if (stream === "stdout")
913
+ stdout += chunk;
914
+ else
915
+ stderr += chunk;
916
+ };
917
+ child.stdout.setEncoding("utf8").on("data", (chunk) => capture("stdout", chunk));
918
+ child.stderr.setEncoding("utf8").on("data", (chunk) => capture("stderr", chunk));
919
+ child.stdin.on("error", () => {
920
+ return;
921
+ });
922
+ child.on("error", () => {
923
+ abort("missing");
924
+ });
925
+ child.on("close", (code) => {
926
+ finish(code === 0 && reason === "exit");
927
+ });
928
+ child.stdin.end(options.input);
929
+ });
930
+ }
931
+
932
+ // packages/personal-cli/src/workstation.ts
933
+ async function workstationCommand(args, settings) {
934
+ const help = args.length === 0 || args.includes("--help");
935
+ const result = await runCommand([
936
+ "bun",
937
+ join(settings.workspace, "packages/workstation/cli/internal.ts"),
938
+ ...help ? [...args, "--help"] : [...args, "--json", "--non-interactive"]
939
+ ], { cwd: settings.workspace, timeoutMs: 1200000 });
940
+ if (help && result.ok)
941
+ return {
942
+ exitCode: 0,
943
+ data: { version: 1, help: result.stdout },
944
+ text: result.stdout
945
+ };
946
+ if (result.ok) {
947
+ try {
948
+ return { exitCode: 0, data: JSON.parse(result.stdout) };
949
+ } catch {}
950
+ }
951
+ return {
952
+ exitCode: 1,
953
+ data: {
954
+ version: 1,
955
+ ok: false,
956
+ error: "Workstation operation failed. Check the personal checkout, Bun, conflicts and per-machine authentication."
957
+ }
958
+ };
959
+ }
960
+ async function forwardWorkstation(args, settings) {
961
+ const { spawn } = await import("node:child_process");
962
+ const childArgs = [];
963
+ let commandSeen = false;
964
+ for (let index = 0;index < args.length; index++) {
965
+ if (["--workspace", "--config"].includes(args[index])) {
966
+ index++;
967
+ continue;
968
+ }
969
+ if (args[index] === "workstation" && !commandSeen) {
970
+ commandSeen = true;
971
+ continue;
972
+ }
973
+ childArgs.push(args[index]);
974
+ }
975
+ return new Promise((resolve) => {
976
+ const child = spawn("bun", [
977
+ join(settings.workspace, "packages/workstation/cli/internal.ts"),
978
+ ...childArgs.length ? childArgs : ["--help"]
979
+ ], {
980
+ cwd: settings.workspace,
981
+ stdio: "inherit",
982
+ env: process.env
983
+ });
984
+ child.on("error", () => {
985
+ process.stderr.write(`Workstation requires the personal checkout and shared Bun toolchain.
986
+ `);
987
+ resolve(1);
988
+ });
989
+ child.on("exit", (code) => resolve(code ?? 1));
990
+ });
991
+ }
992
+
993
+ // packages/personal-cli/src/settings.ts
994
+ import { access } from "node:fs/promises";
995
+ import { dirname, join as join2, resolve } from "node:path";
996
+ import { homedir } from "node:os";
997
+ async function exists(path) {
998
+ try {
999
+ await access(path);
1000
+ return true;
1001
+ } catch {
1002
+ return false;
1003
+ }
1004
+ }
1005
+ async function findUp(start, name) {
1006
+ let current = resolve(start);
1007
+ while (true) {
1008
+ if (await exists(join2(current, name)))
1009
+ return join2(current, name);
1010
+ const parent = dirname(current);
1011
+ if (parent === current)
1012
+ return null;
1013
+ current = parent;
1014
+ }
1015
+ }
1016
+ async function resolveSettings(overrides = {}, cwd = process.cwd(), env = process.env) {
1017
+ const workspace = resolve(overrides.workspace ?? env["PERSONAL_WORKSPACE"] ?? dirname(await findUp(cwd, "mise.toml") ?? join2(cwd, "mise.toml")));
1018
+ const config = resolve(overrides.config ?? env["PERSONAL_FLEET_CONFIG"] ?? await findUp(cwd, ".fleet.local.json") ?? join2(homedir(), ".config/personal/fleet.json"));
1019
+ return { workspace, config };
1020
+ }
1021
+
1022
+ // packages/personal-cli/src/node-deployment.ts
1023
+ import { join as join3 } from "node:path";
1024
+
1025
+ // packages/personal-cli/src/t3/files.ts
1026
+ import {
1027
+ lstat,
1028
+ mkdir,
1029
+ readFile,
1030
+ rename,
1031
+ rm,
1032
+ writeFile
1033
+ } from "node:fs/promises";
1034
+ import { dirname as dirname2, parse } from "node:path";
1035
+ import { randomUUID, createHash } from "node:crypto";
1036
+
1037
+ // packages/fleet/src/errors.ts
1038
+ class FleetUsageError extends Error {
1039
+ }
1040
+
1041
+ // packages/personal-cli/src/t3/files.ts
1042
+ var hash = (value) => createHash("sha256").update(value).digest("hex");
1043
+ async function safePath(path) {
1044
+ for (let current = path;current !== parse(current).root; current = dirname2(current)) {
1045
+ try {
1046
+ if ((await lstat(current)).isSymbolicLink())
1047
+ throw new FleetUsageError("Managed T3 paths cannot be symlinks.");
1048
+ } catch (error) {
1049
+ if (error.code !== "ENOENT")
1050
+ throw error;
1051
+ }
1052
+ }
1053
+ }
1054
+ async function read(path) {
1055
+ await safePath(path);
1056
+ try {
1057
+ const info = await lstat(path);
1058
+ if (!info.isFile() || info.size > 4 * 1024 * 1024)
1059
+ throw new FleetUsageError("T3 configuration must be a regular file smaller than 4 MiB.");
1060
+ return await readFile(path, "utf8");
1061
+ } catch (error) {
1062
+ if (error.code === "ENOENT")
1063
+ return null;
1064
+ throw error;
1065
+ }
1066
+ }
1067
+ async function write(path, contents) {
1068
+ await safePath(path);
1069
+ await mkdir(dirname2(path), { recursive: true, mode: 448 });
1070
+ const temporary = `${path}.${randomUUID()}.tmp`;
1071
+ try {
1072
+ await writeFile(temporary, contents, { mode: 384, flag: "wx" });
1073
+ await rename(temporary, path);
1074
+ } finally {
1075
+ await rm(temporary, { force: true });
1076
+ }
1077
+ }
1078
+ function object(raw) {
1079
+ if (raw === null)
1080
+ return {};
1081
+ try {
1082
+ const value = JSON.parse(raw);
1083
+ if (value && typeof value === "object" && !Array.isArray(value))
1084
+ return value;
1085
+ } catch {}
1086
+ throw new FleetUsageError("Invalid T3 JSON object. Repair the file before reconciliation.");
1087
+ }
1088
+ function canonical(value) {
1089
+ if (Array.isArray(value))
1090
+ return `[${value.map(canonical).join(",")}]`;
1091
+ if (value && typeof value === "object") {
1092
+ return `{${Object.entries(value).sort(([a], [b]) => a.localeCompare(b)).map(([key, item]) => `${JSON.stringify(key)}:${canonical(item)}`).join(",")}}`;
1093
+ }
1094
+ return JSON.stringify(value) ?? "null";
1095
+ }
1096
+
1097
+ // packages/personal-cli/src/node-deployment.ts
1098
+ async function deployment(root) {
1099
+ const raw = await read(join3(root, "node_modules/.personal-node.json"));
1100
+ if (raw === null)
1101
+ return null;
1102
+ const data = object(raw);
1103
+ if (data["version"] !== 1 || typeof data["revision"] !== "string" || !/^[a-f0-9]{40}$/.test(data["revision"]) || typeof data["node"] !== "string" || !/^[a-z0-9][a-z0-9-]{0,62}$/.test(data["node"]))
1104
+ throw new FleetUsageError("Invalid local node receipt. Restore it before reconciling.");
1105
+ return {
1106
+ workspace: join3(root, "node_modules/.fleet-revisions", data["revision"]),
1107
+ node: data["node"],
1108
+ revision: data["revision"]
1109
+ };
1110
+ }
1111
+ async function recordDeployment(root, node, revision) {
1112
+ const path = join3(root, "node_modules/.personal-node.json");
1113
+ const content = JSON.stringify({ version: 1, node, revision }) + `
1114
+ `;
1115
+ if (await read(path) !== content)
1116
+ await write(path, content);
1117
+ }
1118
+
1119
+ // packages/personal-cli/src/t3/desired.ts
1120
+ import { join as join4, resolve as resolve2 } from "node:path";
1121
+ import { homedir as homedir2, hostname } from "node:os";
1122
+ function nodeId(value) {
1123
+ const id = value ?? hostname().toLowerCase().replace(/[^a-z0-9-]/g, "-").slice(0, 63);
1124
+ if (!/^[a-z0-9][a-z0-9-]{0,62}$/.test(id))
1125
+ throw new FleetUsageError("Invalid node ID. Use --id NAME.");
1126
+ return id;
1127
+ }
1128
+ function paths(options, workspace) {
1129
+ const base = resolve2(options.t3Home ?? process.env["T3CODE_HOME"] ?? join4(homedir2(), ".t3"));
1130
+ return {
1131
+ settings: join4(base, "userdata/settings.json"),
1132
+ state: join4(base, "userdata/personal-managed.json"),
1133
+ definition: resolve2(options.definition ?? join4(workspace, "packages/workstation/agents/t3.json"))
1134
+ };
1135
+ }
1136
+ async function desired(path, workspace, id) {
1137
+ const raw = await read(path);
1138
+ if (raw === null)
1139
+ throw new FleetUsageError("T3 definition is missing in this checkout.");
1140
+ const doc = object(raw);
1141
+ if (doc["version"] !== 1 || Object.keys(doc).some((key) => !["version", "instances"].includes(key)))
1142
+ throw new FleetUsageError("Unsupported T3 definition. Expected version 1 and instances.");
1143
+ const instances = object(JSON.stringify(doc["instances"] ?? null));
1144
+ for (const [key, value] of Object.entries(instances))
1145
+ validateInstance(key, value);
1146
+ const agents = process.platform === "win32" ? join4(homedir2(), "AppData/Local/personal/agents") : join4(homedir2(), ".config/personal/agents");
1147
+ const variables = {
1148
+ workspace: resolve2(workspace),
1149
+ agents,
1150
+ node: id
1151
+ };
1152
+ return expand(instances, variables);
1153
+ }
1154
+ function expand(value, variables) {
1155
+ if (typeof value === "string")
1156
+ return value.replace(/\$\{([^}]+)\}/g, (_, key) => {
1157
+ if (!(key in variables))
1158
+ throw new FleetUsageError("Unknown variable in T3 definition.");
1159
+ return variables[key];
1160
+ });
1161
+ if (Array.isArray(value))
1162
+ return value.map((item) => expand(item, variables));
1163
+ if (value && typeof value === "object")
1164
+ return Object.fromEntries(Object.entries(value).map(([key, item]) => [key, expand(item, variables)]));
1165
+ return value;
1166
+ }
1167
+ function validateInstance(id, value) {
1168
+ const instance = object(JSON.stringify(value));
1169
+ const config = object(JSON.stringify(instance["config"]));
1170
+ const env = instance["environment"];
1171
+ if (!/^[a-zA-Z][a-zA-Z0-9_-]{0,63}$/.test(id) || instance["driver"] !== "codex" || typeof instance["displayName"] !== "string" || typeof instance["enabled"] !== "boolean" || Object.keys(instance).some((key) => !["driver", "displayName", "enabled", "config", "environment"].includes(key)) || !validConfig(config) || !validEnvironment(env))
1172
+ throw new FleetUsageError("Invalid T3 instance definition; only the supported Codex adapter fields are accepted.");
1173
+ }
1174
+ function validConfig(config) {
1175
+ return Object.keys(config).every((key) => ["binaryPath", "homePath"].includes(key)) && typeof config["binaryPath"] === "string" && typeof config["homePath"] === "string";
1176
+ }
1177
+ function validEnvironment(env) {
1178
+ if (!Array.isArray(env) || env.length !== 1)
1179
+ return false;
1180
+ const variable = object(JSON.stringify(env[0]));
1181
+ return variable["name"] === "WS_AGENT_NODE" && variable["value"] === "${node}" && variable["sensitive"] === false && Object.keys(variable).length === 3;
1182
+ }
1183
+
1184
+ // packages/personal-cli/src/t3/reconcile.ts
1185
+ import { mkdir as mkdir2, rm as rm2 } from "node:fs/promises";
1186
+ import { dirname as dirname3 } from "node:path";
1187
+
1188
+ // packages/personal-cli/src/t3/plan.ts
1189
+ async function planT3(paths, desired) {
1190
+ const raw = await read(paths.settings);
1191
+ const stateRaw = await read(paths.state);
1192
+ const settings = object(raw);
1193
+ const current = object(JSON.stringify(settings["providerInstances"] ?? {}));
1194
+ const owned = readOwnership(stateRaw, paths.settings);
1195
+ const next = { ...current };
1196
+ const changes = [];
1197
+ for (const [id, instance] of Object.entries(desired)) {
1198
+ const exists = Object.hasOwn(current, id);
1199
+ const action = actionFor(exists, owned.includes(id), current[id], instance);
1200
+ changes.push({ id, action });
1201
+ if (action !== "conflict")
1202
+ next[id] = instance;
1203
+ }
1204
+ for (const id of owned.filter((id) => !Object.hasOwn(desired, id))) {
1205
+ changes.push({
1206
+ id,
1207
+ action: Object.hasOwn(current, id) ? "delete" : "unchanged"
1208
+ });
1209
+ delete next[id];
1210
+ }
1211
+ const settingsChanged = canonical(next) !== canonical(current);
1212
+ const state = ownership(paths.settings, Object.keys(desired));
1213
+ return {
1214
+ raw,
1215
+ stateRaw,
1216
+ settings: settingsChanged ? { ...settings, providerInstances: next } : settings,
1217
+ state,
1218
+ settingsChanged,
1219
+ stateChanged: canonical(object(stateRaw)) !== canonical(state),
1220
+ changes,
1221
+ planHash: hash(canonical({ target: paths.settings, raw, stateRaw, desired }))
1222
+ };
1223
+ }
1224
+ function ownership(target, ids) {
1225
+ return { version: 1, target, instances: [...ids].sort() };
1226
+ }
1227
+ function readOwnership(raw, target) {
1228
+ if (raw === null)
1229
+ return [];
1230
+ const state = object(raw);
1231
+ const ids = state["instances"];
1232
+ if (state["version"] !== 1 || state["target"] !== target || !Array.isArray(ids) || ids.some((id) => typeof id !== "string" || !/^[a-zA-Z][a-zA-Z0-9_-]{0,63}$/.test(id)) || new Set(ids).size !== ids.length)
1233
+ throw new FleetUsageError("Invalid T3 ownership state; restore its backup before reconciliation.");
1234
+ return ids;
1235
+ }
1236
+ function actionFor(exists, owned, current, desired) {
1237
+ if (exists && !owned)
1238
+ return "conflict";
1239
+ if (!exists)
1240
+ return "create";
1241
+ return canonical(current) === canonical(desired) ? "unchanged" : "update";
1242
+ }
1243
+
1244
+ // packages/personal-cli/src/t3/reconcile.ts
1245
+ async function reconcileT3(paths, desired, options) {
1246
+ if (!options.apply)
1247
+ return report(await planT3(paths, desired), false);
1248
+ return locked(paths, async () => {
1249
+ const plan = await planT3(paths, desired);
1250
+ if (options.planHash && options.planHash !== plan.planHash)
1251
+ throw new FleetUsageError("T3 plan is stale. Run personal t3-plan again.");
1252
+ if (plan.changes.some((change) => change.action === "conflict"))
1253
+ throw new FleetUsageError("T3 instance already exists without ownership. Review it and use personal t3-import --instance ID.");
1254
+ if (!plan.settingsChanged && !plan.stateChanged)
1255
+ return report(plan, true);
1256
+ await persist(paths, desired, plan);
1257
+ const verified = await planT3(paths, desired);
1258
+ if (verified.settingsChanged || verified.stateChanged)
1259
+ throw new FleetUsageError("T3 changed after apply; reconciliation is incomplete. Replan and retry.");
1260
+ return report(plan, true);
1261
+ });
1262
+ }
1263
+ async function importT3(paths, id) {
1264
+ return locked(paths, async () => {
1265
+ const raw = await read(paths.settings);
1266
+ const instances = object(JSON.stringify(object(raw)["providerInstances"] ?? {}));
1267
+ if (!Object.hasOwn(instances, id))
1268
+ throw new FleetUsageError("T3 import requires an existing instance ID.");
1269
+ const state = await read(paths.state);
1270
+ const owned = readOwnership(state, paths.settings);
1271
+ if (!owned.includes(id)) {
1272
+ if (state !== null)
1273
+ await write(`${paths.state}.backup`, state);
1274
+ if (await read(paths.settings) !== raw)
1275
+ throw new FleetUsageError("T3 changed during import; retry.");
1276
+ await write(paths.state, canonical(ownership(paths.settings, [...owned, id])) + `
1277
+ `);
1278
+ }
1279
+ return { version: 1, ok: true, imported: id, changed: !owned.includes(id) };
1280
+ });
1281
+ }
1282
+ async function locked(paths, action) {
1283
+ await safePath(paths.state);
1284
+ await mkdir2(dirname3(paths.state), { recursive: true, mode: 448 });
1285
+ const lock = `${paths.state}.lock`;
1286
+ try {
1287
+ await mkdir2(lock, { mode: 448 });
1288
+ } catch {
1289
+ throw new FleetUsageError("Another personal T3 operation holds the lock. Inspect interrupted operations before removing a stale lock.");
1290
+ }
1291
+ try {
1292
+ return await action();
1293
+ } finally {
1294
+ await rm2(lock, { recursive: true });
1295
+ }
1296
+ }
1297
+ function report(plan, applied) {
1298
+ return {
1299
+ version: 1,
1300
+ ok: !plan.changes.some((change) => change.action === "conflict"),
1301
+ applied,
1302
+ changed: plan.settingsChanged || plan.stateChanged,
1303
+ planHash: plan.planHash,
1304
+ changes: plan.changes,
1305
+ runtime: "T3 watches settings.json; new sessions use the reconciled instances. Active sessions are not restarted."
1306
+ };
1307
+ }
1308
+ async function persist(paths, desired, plan) {
1309
+ if (plan.raw !== null && plan.settingsChanged)
1310
+ await write(`${paths.state}.settings-backup`, plan.raw);
1311
+ if (plan.stateRaw !== null)
1312
+ await write(`${paths.state}.backup`, plan.stateRaw);
1313
+ if (await read(paths.settings) !== plan.raw || await read(paths.state) !== plan.stateRaw)
1314
+ throw new FleetUsageError("T3 configuration changed during apply. Replan and retry.");
1315
+ const previous = readOwnership(plan.stateRaw, paths.settings);
1316
+ await write(paths.state, canonical(ownership(paths.settings, [
1317
+ ...new Set([...previous, ...Object.keys(desired)])
1318
+ ])) + `
1319
+ `);
1320
+ if (plan.settingsChanged) {
1321
+ if (await read(paths.settings) !== plan.raw)
1322
+ throw new FleetUsageError("T3 changed during apply. Ownership intent was saved; replan and retry.");
1323
+ await write(paths.settings, JSON.stringify(plan.settings, null, 2) + `
1324
+ `);
1325
+ }
1326
+ await write(paths.state, canonical(plan.state) + `
1327
+ `);
1328
+ }
1329
+
1330
+ // packages/personal-cli/src/t3/command.ts
1331
+ async function t3Command(command, options, settings) {
1332
+ const deployed = options.definition ? null : await deployment(settings.workspace);
1333
+ const workspace = deployed?.workspace ?? settings.workspace;
1334
+ const target = paths(options, workspace);
1335
+ if (command === "t3-import")
1336
+ return importCommand(target, options);
1337
+ const instances = command === "t3-cleanup" ? {} : await desired(target.definition, workspace, nodeId(options.id ?? deployed?.node));
1338
+ const apply = shouldApply(command, options);
1339
+ if (apply)
1340
+ requireSupportedPlatform(instances);
1341
+ const data = await reconcileT3(target, instances, {
1342
+ apply,
1343
+ ...options.planHash ? { planHash: options.planHash } : {}
1344
+ });
1345
+ return {
1346
+ exitCode: outcomeCode(command, data),
1347
+ data: { command, ...data }
1348
+ };
1349
+ }
1350
+ async function importCommand(target, options) {
1351
+ if (options.dryRun)
1352
+ throw new FleetUsageError("t3-import is an explicit ownership operation; inspect t3-plan first.");
1353
+ if (!options.instance || !/^[a-zA-Z][a-zA-Z0-9_-]{0,63}$/.test(options.instance))
1354
+ throw new FleetUsageError("t3-import requires --instance ID. Import authorizes management of that entire provider instance.");
1355
+ return { exitCode: 0, data: await importT3(target, options.instance) };
1356
+ }
1357
+ function shouldApply(command, options) {
1358
+ return ["t3-apply", "t3-cleanup"].includes(command) && !options.dryRun;
1359
+ }
1360
+ function outcomeCode(command, data) {
1361
+ return !data.ok || command === "t3-status" && data.changed ? 1 : 0;
1362
+ }
1363
+ function requireSupportedPlatform(instances) {
1364
+ if (process.platform === "win32" && Object.keys(instances).length)
1365
+ throw new FleetUsageError("Managed Codex proxy execution requires macOS/Linux/WSL; native Windows settings were preserved.");
1366
+ }
1367
+
1368
+ // packages/personal-cli/src/t3/register.ts
1369
+ var T3_COMMANDS = [
1370
+ ["t3-plan", "Preview creates, updates and owned removals"],
1371
+ ["t3-apply", "Reconcile declared T3 provider instances"],
1372
+ ["t3-status", "Check managed T3 configuration for drift"],
1373
+ [
1374
+ "t3-cleanup",
1375
+ "Remove only provider instances recorded in personal ownership state"
1376
+ ],
1377
+ ["t3-import", "Adopt an existing provider instance into personal ownership"]
1378
+ ];
1379
+ function registerT3(program, defaults, complete) {
1380
+ for (const [name, description] of T3_COMMANDS) {
1381
+ program.command(name).description(description).option("--id <name>", "Node ID; defaults to hostname").option("--t3-home <path>", "T3 base directory; defaults to T3CODE_HOME or ~/.t3").option("--definition <path>", "Version 1 declarative T3 provider definition").option("--plan-hash <sha256>", "Reject any changes since the reviewed plan").option("--dry-run", "Preview without writing configuration or state").option("--instance <id>", "Existing instance to adopt with t3-import").action(async (_options, command) => {
1382
+ const options = command.optsWithGlobals();
1383
+ const settings = await resolveSettings({ ...defaults, ...options });
1384
+ complete(await t3Command(name, options, settings));
1385
+ });
1386
+ }
1387
+ }
1388
+
1389
+ // packages/personal-cli/src/registry.ts
1390
+ init_search();
1391
+ function fleet(command, title, description, keywords) {
1392
+ return {
1393
+ id: `fleet ${command}`,
1394
+ title,
1395
+ description,
1396
+ group: "Fleet",
1397
+ keywords,
1398
+ args: ["fleet", command],
1399
+ examples: [`personal fleet ${command} --json`],
1400
+ mutation: ["add", "sync", "init", "repair"].includes(command)
1401
+ };
1402
+ }
1403
+ var COMMANDS = [
1404
+ ...T3_COMMANDS.map(([id, description]) => ({
1405
+ id,
1406
+ title: description,
1407
+ description,
1408
+ group: "T3",
1409
+ keywords: ["t3", "configuration", "terraform", "drift"],
1410
+ args: [id],
1411
+ examples: [`personal ${id} --json`],
1412
+ mutation: ["t3-apply", "t3-cleanup", "t3-import"].includes(id)
1413
+ })),
1414
+ {
1415
+ id: "workstation",
1416
+ title: "Workstation configuration",
1417
+ description: "Tools, editor configuration, agent profiles, notifications and launcher lifecycle.",
1418
+ group: "Workstation",
1419
+ keywords: ["tools", "agents", "opencode", "install"],
1420
+ args: ["workstation"],
1421
+ examples: ["personal workstation --help"],
1422
+ mutation: false
1423
+ },
1424
+ ...[
1425
+ [
1426
+ "fleet-node-sync",
1427
+ "Sync this node",
1428
+ "Upsert and reconcile locked tools and the Codex proxy profile on this machine. Applies by default; --dry-run previews."
1429
+ ],
1430
+ [
1431
+ "fleet-status",
1432
+ "Fleet status",
1433
+ "Check this machine and all registered computers without changing them."
1434
+ ],
1435
+ [
1436
+ "fleet-discover",
1437
+ "Discover computers",
1438
+ "List computers visible through Tailscale."
1439
+ ]
1440
+ ].map(([id, title, description]) => ({
1441
+ id,
1442
+ title,
1443
+ description,
1444
+ group: "Fleet",
1445
+ keywords: ["node", "sync", "proxy", "status"],
1446
+ args: [id],
1447
+ examples: [`personal ${id} --json`],
1448
+ mutation: id === "fleet-node-sync"
1449
+ })),
1450
+ fleet("repairs", "Explore repairs and login commands", "List existing tool owners, automated repairs, native per-machine login commands and shared Vault access guidance. No roster or credentials required.", ["fix", "repair", "login", "credentials", "sharing", "vault", "resources"]),
1451
+ fleet("repair", "Repair tools and configuration", "Inspect visible Tailscale computers and preview owner-managed repairs. --apply --yes executes eligible tools/config repairs and reruns diagnostics. Logins and host trust remain explicit operator steps.", [
1452
+ "fix",
1453
+ "repair",
1454
+ "sync",
1455
+ "setup",
1456
+ "versions",
1457
+ "config",
1458
+ "login",
1459
+ "credentials"
1460
+ ]),
1461
+ fleet("health", "Check all Tailscale computers", "Discover every visible non-mobile Tailscale computer, test reachability, pinned tools, configuration and service logins. Offline, unregistered and unverified computers fail coverage.", [
1462
+ "tailscale",
1463
+ "tailnet",
1464
+ "all",
1465
+ "health",
1466
+ "versions",
1467
+ "codex",
1468
+ "claude",
1469
+ "login",
1470
+ "auth"
1471
+ ]),
1472
+ fleet("discover", "Discover Tailscale computers", "List all computers visible to this Tailscale account, including offline devices. Excludes iOS, Android and tvOS; never writes a roster.", ["tailscale", "tailnet", "computers", "discover", "machines"]),
1473
+ fleet("doctor", "Diagnose the entire fleet", "Read-only SSH, authentication, T3, VS Code and dependency checks for this controller and every roster node.", [
1474
+ "health",
1475
+ "ssh",
1476
+ "auth",
1477
+ "github",
1478
+ "vscode",
1479
+ "t3",
1480
+ "dependencies",
1481
+ "diagnose"
1482
+ ]),
1483
+ {
1484
+ ...fleet("doctor", "Diagnose this computer", "Check this process context without a roster or remote connections.", ["local", "controller", "health"]),
1485
+ id: "fleet doctor local",
1486
+ args: ["fleet", "doctor", "--local"],
1487
+ examples: ["personal fleet doctor --local --json"]
1488
+ },
1489
+ fleet("nodes", "Explore fleet nodes", "List registered developer computers and their required capabilities without contacting them.", ["roster", "list", "machines", "computers"]),
1490
+ 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"]),
1491
+ fleet("validate", "Validate the roster", "Check local configuration before making any network requests.", ["config", "schema", "verify"]),
1492
+ fleet("onboard", "Explore onboarding instructions", "Get per-node SSH templates, VS Code guidance, authentication steps and safe verification commands.", ["setup", "guide", "instructions", "vscode", "connect"]),
1493
+ fleet("sync", "Plan toolchain and dependency sync", "Preview convergence through the existing mise/workstation owners. Applying requires --apply --yes.", ["drift", "versions", "mise", "install", "dependencies"]),
1494
+ fleet("init", "Preview an example roster", "Inspect a starter configuration; only --apply --yes creates it. Prefer Register for real nodes.", ["setup", "example", "config"]),
1495
+ fleet("schema", "Explore the fleet JSON schema", "Read the versioned roster schema for editors and agents.", ["agent", "config", "json"]),
1496
+ {
1497
+ id: "commands",
1498
+ title: "Explore the command catalog",
1499
+ description: "Search command names, descriptions and keywords; emit the complete machine-readable interface.",
1500
+ group: "Reference",
1501
+ keywords: ["search", "help", "llm", "agent", "api"],
1502
+ args: ["commands"],
1503
+ examples: ["personal commands --json --search ssh"],
1504
+ mutation: false
1505
+ },
1506
+ {
1507
+ id: "agent",
1508
+ title: "Agent integration guide",
1509
+ description: "Noninteractive usage, JSON contracts, exit codes, safety boundaries and reproducible examples.",
1510
+ group: "Reference",
1511
+ keywords: ["llm", "automation", "help", "safety"],
1512
+ args: ["agent"],
1513
+ examples: ["personal agent --json"],
1514
+ mutation: false
1515
+ }
1516
+ ];
1517
+ function commandCatalog(search = "", group) {
1518
+ return searchCommands(COMMANDS, search).filter((command) => !group || command.group.toLowerCase() === group.toLowerCase());
1519
+ }
1520
+
1521
+ // packages/personal-cli/src/program.ts
1522
+ import { Command, CommanderError } from "commander";
1523
+
1524
+ // packages/personal-cli/src/fleet.ts
1525
+ import { writeFile as writeFile3, mkdir as mkdir5 } from "node:fs/promises";
1526
+ import { dirname as dirname5 } from "node:path";
1527
+
1528
+ // packages/fleet/src/config.ts
1529
+ var CAPABILITIES = [
1530
+ "github",
1531
+ "vault",
1532
+ "t3",
1533
+ "vscode",
1534
+ "toolchain",
1535
+ "codex",
1536
+ "claude",
1537
+ "services",
1538
+ "workstation"
1539
+ ];
1540
+ function record(value) {
1541
+ return typeof value === "object" && value !== null && !Array.isArray(value);
1542
+ }
1543
+ function fields(value, allowed) {
1544
+ if (Object.keys(value).some((key) => !allowed.includes(key))) {
1545
+ throw new FleetUsageError("Unknown configuration field. Run fleet schema.");
1546
+ }
1547
+ }
1548
+ function absolutePath(value) {
1549
+ return typeof value === "string" && value.startsWith("/") && value.length <= 4096 && !/[\x00-\x1f\x7f]/.test(value);
1550
+ }
1551
+ function parseNode(value) {
1552
+ if (!record(value))
1553
+ throw new FleetUsageError("Each node must be an object.");
1554
+ fields(value, ["id", "ssh", "workspace", "runtime", "required"]);
1555
+ const names = nodeNames(value);
1556
+ if (!absolutePath(value["workspace"]))
1557
+ throw new FleetUsageError("workspace must be an absolute POSIX path.");
1558
+ if (value["runtime"] !== undefined && !absolutePath(value["runtime"])) {
1559
+ throw new FleetUsageError("runtime must be an absolute POSIX path to Node or Bun.");
1560
+ }
1561
+ return {
1562
+ ...names,
1563
+ workspace: value["workspace"],
1564
+ ...typeof value["runtime"] === "string" ? { runtime: value["runtime"] } : {},
1565
+ required: requiredCapabilities(value["required"])
1566
+ };
1567
+ }
1568
+ function nodeNames(value) {
1569
+ if (typeof value["id"] !== "string" || !/^[a-z][a-z0-9-]{0,62}$/.test(value["id"])) {
1570
+ throw new FleetUsageError("Node IDs must be lowercase names (1–63 characters).");
1571
+ }
1572
+ if (typeof value["ssh"] !== "string" || !/^[a-zA-Z0-9][a-zA-Z0-9._-]{0,252}$/.test(value["ssh"])) {
1573
+ throw new FleetUsageError("ssh must be a plain SSH alias or hostname, not options or user@host.");
1574
+ }
1575
+ return { id: value["id"], ssh: value["ssh"] };
1576
+ }
1577
+ function requiredCapabilities(value) {
1578
+ const required = value === undefined ? ["github", "vault", "t3", "toolchain"] : value;
1579
+ if (!Array.isArray(required) || required.some((item) => !CAPABILITIES.includes(item))) {
1580
+ throw new FleetUsageError("required must contain only capability names from fleet schema.");
1581
+ }
1582
+ if (new Set(required).size !== required.length)
1583
+ throw new FleetUsageError("required capabilities must be unique.");
1584
+ return required;
1585
+ }
1586
+ function parseConfig(value) {
1587
+ if (!record(value))
1588
+ throw new FleetUsageError("Fleet configuration must be an object.");
1589
+ fields(value, ["$schema", "version", "nodes"]);
1590
+ if (value["$schema"] !== undefined && typeof value["$schema"] !== "string")
1591
+ throw new FleetUsageError("$schema must be a string.");
1592
+ if (value["version"] !== 1 || !Array.isArray(value["nodes"])) {
1593
+ throw new FleetUsageError("Expected version 1 and a nodes array.");
1594
+ }
1595
+ if (value["nodes"].length < 1 || value["nodes"].length > 64) {
1596
+ throw new FleetUsageError("Configure between 1 and 64 developer nodes.");
1597
+ }
1598
+ const nodes = value["nodes"].map(parseNode);
1599
+ if (new Set(nodes.map((node) => node.id)).size !== nodes.length) {
1600
+ throw new FleetUsageError("Node IDs must be unique.");
1601
+ }
1602
+ return { version: 1, nodes };
1603
+ }
1604
+ function selectNodes(config, ids) {
1605
+ if (ids.some((id) => !config.nodes.some((node) => node.id === id))) {
1606
+ throw new FleetUsageError("Unknown node selection. Run fleet nodes --json.");
1607
+ }
1608
+ return ids.length === 0 ? config.nodes : config.nodes.filter((node) => ids.includes(node.id));
1609
+ }
1610
+
1611
+ // packages/fleet/src/fingerprint.ts
1612
+ import { readFile as readFile3, mkdir as mkdir3, writeFile as writeFile2 } from "node:fs/promises";
1613
+ import { join as join6 } from "node:path";
1614
+
1615
+ // packages/fleet/src/input-hashes.ts
1616
+ import { createHash as createHash2 } from "node:crypto";
1617
+ import { readFile as readFile2, readdir } from "node:fs/promises";
1618
+ import { join as join5 } from "node:path";
1619
+ var INPUT_FILES = [
1620
+ "package.json",
1621
+ "bun.lock",
1622
+ "mise.toml",
1623
+ "mise.lock"
1624
+ ];
1625
+ var FINGERPRINT_FILES = [
1626
+ ...INPUT_FILES,
1627
+ "workspace-manifests",
1628
+ "mise-sidecars"
1629
+ ];
1630
+ async function fileHash(path) {
1631
+ try {
1632
+ return digest(await readFile2(path));
1633
+ } catch {
1634
+ return null;
1635
+ }
1636
+ }
1637
+ async function manifestHash(root) {
1638
+ try {
1639
+ const entries = await readdir(join5(root, "packages"), {
1640
+ withFileTypes: true
1641
+ });
1642
+ const hashes = [];
1643
+ for (const entry of entries.filter((item) => item.isDirectory()).sort((left, right) => left.name.localeCompare(right.name, "en"))) {
1644
+ hashes.push(`${entry.name}:${await fileHash(join5(root, "packages", entry.name, "package.json"))}`);
1645
+ }
1646
+ return digest(hashes.join(`
1647
+ `));
1648
+ } catch {
1649
+ return null;
1650
+ }
1651
+ }
1652
+ async function sidecarHash(root) {
1653
+ try {
1654
+ const files = await readdir(join5(root, ".mise/locks"), {
1655
+ recursive: true,
1656
+ withFileTypes: true
1657
+ });
1658
+ if (files.length > 4096 || files.some((entry) => entry.isSymbolicLink()))
1659
+ return null;
1660
+ const hashes = [];
1661
+ const paths = files.filter((entry) => entry.isFile()).map((entry) => join5(entry.parentPath, entry.name)).sort();
1662
+ for (const path of paths) {
1663
+ const hash = await fileHash(path);
1664
+ if (hash === null)
1665
+ return null;
1666
+ const relative = path.slice(join5(root, ".mise/locks").length).replaceAll("\\", "/");
1667
+ hashes.push(`${relative}:${hash}`);
1668
+ }
1669
+ return digest(hashes.join(`
1670
+ `));
1671
+ } catch {
1672
+ return null;
1673
+ }
1674
+ }
1675
+ function digest(value) {
1676
+ return createHash2("sha256").update(value).digest("hex");
1677
+ }
1678
+
1679
+ // packages/fleet/src/fingerprint.ts
1680
+ var RECEIPT = "node_modules/.fleet-sync.json";
1681
+ async function fingerprint(root, run) {
1682
+ const files = {};
1683
+ for (const name of INPUT_FILES)
1684
+ files[name] = await fileHash(join6(root, name));
1685
+ files["workspace-manifests"] = await manifestHash(root);
1686
+ files["mise-sidecars"] = await sidecarHash(root);
1687
+ const head = await run(["git", "rev-parse", "HEAD"], { cwd: root });
1688
+ return {
1689
+ head: head.ok && /^[a-f0-9]{40,64}\s*$/.test(head.stdout) ? head.stdout.trim() : null,
1690
+ files
1691
+ };
1692
+ }
1693
+ async function dependencyCheck(root, current) {
1694
+ const remedy = {
1695
+ action: "Review fleet sync, then explicitly apply a frozen toolchain/dependency sync."
1696
+ };
1697
+ try {
1698
+ const receipt = JSON.parse(await readFile3(join6(root, RECEIPT), "utf8"));
1699
+ const matches = record(receipt) && receipt["version"] === 1 && JSON.stringify(receipt["files"]) === JSON.stringify(current.files);
1700
+ return {
1701
+ id: "dependencies",
1702
+ status: matches ? "pass" : "fail",
1703
+ summary: matches ? "Last successful frozen sync matches input hashes; not a full integrity audit." : "Dependency sync receipt is stale or invalid.",
1704
+ ...matches ? {} : { remedy }
1705
+ };
1706
+ } catch {
1707
+ return {
1708
+ id: "dependencies",
1709
+ status: "warn",
1710
+ summary: "No frozen-sync receipt; installed dependencies are unverified.",
1711
+ remedy
1712
+ };
1713
+ }
1714
+ }
1715
+ async function writeReceipt(root, current) {
1716
+ await mkdir3(join6(root, "node_modules"), { recursive: true });
1717
+ await writeFile2(join6(root, RECEIPT), JSON.stringify({ version: 1, files: current.files }), { mode: 384 });
1718
+ }
1719
+ function compareFingerprints(expected, actual) {
1720
+ return [
1721
+ compare("revision", expected.head, actual.head),
1722
+ ...FINGERPRINT_FILES.map((name) => compare(`sync:${name}`, expected.files[name] ?? null, actual.files[name] ?? null))
1723
+ ];
1724
+ }
1725
+ function compare(id, expected, actual) {
1726
+ const known = expected !== null && actual !== null;
1727
+ const matches = known && expected === actual;
1728
+ return {
1729
+ id,
1730
+ status: matches ? "pass" : "fail",
1731
+ summary: matches ? "Matches the controller checkout." : known ? "Differs from the controller checkout." : "Cannot verify; reference or node input is missing.",
1732
+ ...matches ? {} : {
1733
+ remedy: {
1734
+ action: "Reconcile branches/lockfiles; review fleet sync --pull. Never reset a working tree."
1735
+ }
1736
+ }
1737
+ };
1738
+ }
1739
+
1740
+ // packages/fleet/src/service-auth.ts
1741
+ function json(result) {
1742
+ try {
1743
+ const data = JSON.parse(result.stdout);
1744
+ return record(data) ? data : null;
1745
+ } catch {
1746
+ return null;
1747
+ }
1748
+ }
1749
+ async function codingAuth(id, run) {
1750
+ return [
1751
+ await serviceCheck(id === "codex" ? {
1752
+ id,
1753
+ command: ["codex", "login", "status"],
1754
+ action: "Run codex login on this computer as its operator.",
1755
+ valid: (result) => /^Logged in using (ChatGPT|an API key|API key)\b/im.test(`${result.stdout}
1756
+ ${result.stderr}`)
1757
+ } : {
1758
+ id,
1759
+ command: ["claude", "auth", "status", "--json"],
1760
+ action: "Run claude auth login on this computer as its operator.",
1761
+ valid: (result) => json(result)?.["loggedIn"] === true
1762
+ }, run)
1763
+ ];
1764
+ }
1765
+ async function serviceAuth(run) {
1766
+ const services = [
1767
+ {
1768
+ id: "railway",
1769
+ command: ["railway", "whoami", "--json"],
1770
+ action: "Run railway login on this computer.",
1771
+ valid: (result) => typeof json(result)?.["id"] === "string"
1772
+ },
1773
+ {
1774
+ id: "vercel",
1775
+ command: ["vercel", "whoami"],
1776
+ action: "Run vercel login on this computer.",
1777
+ valid: (result) => /^[a-zA-Z0-9][a-zA-Z0-9_-]{0,100}$/.test(result.stdout.trim())
1778
+ },
1779
+ {
1780
+ id: "cloudflare",
1781
+ command: ["wrangler", "whoami", "--json"],
1782
+ action: "Run wrangler login on this computer.",
1783
+ valid: (result) => json(result)?.["loggedIn"] === true
1784
+ },
1785
+ {
1786
+ id: "aws",
1787
+ command: [
1788
+ "aws",
1789
+ "sts",
1790
+ "get-caller-identity",
1791
+ "--output",
1792
+ "json",
1793
+ "--no-cli-pager"
1794
+ ],
1795
+ action: "Select and authenticate the intended AWS profile on this computer.",
1796
+ valid: (result) => typeof json(result)?.["Account"] === "string" && typeof json(result)?.["Arn"] === "string"
1797
+ },
1798
+ {
1799
+ id: "npm",
1800
+ command: ["npm", "whoami", "--registry=https://registry.npmjs.org/"],
1801
+ action: "Run npm login only if this computer should publish packages.",
1802
+ valid: (result) => /^[a-z0-9][a-z0-9._-]{0,213}$/.test(result.stdout.trim())
1803
+ }
1804
+ ];
1805
+ const checks = [];
1806
+ for (const service of services)
1807
+ checks.push(await serviceCheck(service, run));
1808
+ for (const id of ["gemini", "copilot", "opencode"])
1809
+ checks.push({
1810
+ id: `auth:${id}`,
1811
+ status: "skip",
1812
+ summary: `${id} provider authorization has no verified noninteractive status adapter in this CLI. Installed binaries or saved keys are not proof of access.`,
1813
+ remedy: {
1814
+ action: `Verify ${id} provider access locally using its supported account diagnostics. No prompt or billable request was made.`
1815
+ }
1816
+ });
1817
+ return checks;
1818
+ }
1819
+ async function serviceCheck(service, run) {
1820
+ const result = await run(service.command, { timeoutMs: 15000 });
1821
+ const ok = result.ok && service.valid(result);
1822
+ return {
1823
+ id: `auth:${service.id}`,
1824
+ status: ok ? "pass" : "fail",
1825
+ summary: ok ? `${service.id} reports an authenticated session in this process context; no credential or account output retained.` : `${service.id} login is missing or could not be verified (${result.reason}); no credential output retained.`,
1826
+ ...!ok ? { remedy: { action: service.action } } : {}
1827
+ };
1828
+ }
1829
+
1830
+ // packages/fleet/src/workstation-checks.ts
1831
+ async function workstationChecks(root, run) {
1832
+ const result = await run(["personal", "workstation", "doctor", "--json", "--non-interactive"], {
1833
+ cwd: root,
1834
+ timeoutMs: 20000
1835
+ });
1836
+ const fallback = {
1837
+ id: "workstation:configuration",
1838
+ status: "fail",
1839
+ summary: "Managed workstation configuration could not be verified.",
1840
+ remedy: {
1841
+ action: "Run personal workstation doctor --json on this computer. Review managed links, PATH, coding-tool/provider configuration and notification settings; apply personal workstation sync only explicitly."
1842
+ }
1843
+ };
1844
+ try {
1845
+ const data = JSON.parse(result.stdout);
1846
+ if (!record(data) || !Array.isArray(data["checks"]) || !data["checks"].length || data["checks"].length > 100)
1847
+ return [fallback];
1848
+ const checks = data["checks"].map(configurationCheck);
1849
+ if (checks.some((check) => check === null))
1850
+ return [fallback];
1851
+ return [
1852
+ ...!result.ok || data["ok"] !== true ? [fallback] : [],
1853
+ ...checks
1854
+ ];
1855
+ } catch {
1856
+ return [fallback];
1857
+ }
1858
+ }
1859
+ function configurationCheck(value) {
1860
+ if (!record(value) || typeof value["id"] !== "string" || !/^[a-zA-Z0-9:._-]{1,75}$/.test(value["id"]) || !["pass", "warn", "fail"].includes(String(value["severity"])))
1861
+ return null;
1862
+ const ok = value["severity"] === "pass";
1863
+ return {
1864
+ id: `workstation:${value["id"].toLowerCase()}`,
1865
+ status: value["severity"],
1866
+ summary: ok ? "Managed configuration check succeeded (configuration presence; provider authorization is checked separately)." : "Managed configuration is missing, conflicting or unverified.",
1867
+ ...!ok ? {
1868
+ remedy: {
1869
+ action: "Inspect personal workstation doctor --json on this computer. Use explicit personal workstation sync only after reviewing the configuration changes."
1870
+ }
1871
+ } : {}
1872
+ };
1873
+ }
1874
+
1875
+ // packages/fleet/src/toolchain-checks.ts
1876
+ import { join as join7 } from "node:path";
1877
+ async function toolchainChecks(root, run) {
1878
+ const result = await run(["bun", join7(root, "scripts/dev/doctor.ts"), "--json", "--docker"], { cwd: root, timeoutMs: 90000 });
1879
+ const fallback = {
1880
+ id: "auth:toolchain",
1881
+ status: "fail",
1882
+ summary: "Pinned tool/dependency and Docker checks could not be verified.",
1883
+ remedy: {
1884
+ action: "Run the checkout scripts/dev/doctor.ts --json --docker on this computer. Reconcile versions only through the shared mise setup."
1885
+ }
1886
+ };
1887
+ try {
1888
+ const data = JSON.parse(result.stdout);
1889
+ if (!record(data) || typeof data["ok"] !== "boolean" || !Array.isArray(data["checks"]) || !data["checks"].length || data["checks"].length > 150)
1890
+ return [fallback];
1891
+ const checks = data["checks"].map(toolCheck);
1892
+ if (checks.some((check) => check === null))
1893
+ return [fallback];
1894
+ return [
1895
+ ...!result.ok || !data["ok"] ? [fallback] : [],
1896
+ ...checks
1897
+ ];
1898
+ } catch {
1899
+ return [fallback];
1900
+ }
1901
+ }
1902
+ function toolCheck(value) {
1903
+ if (!record(value) || typeof value["id"] !== "string" || !/^[a-zA-Z0-9@:/._-]{1,75}$/.test(value["id"]) || !["pass", "fail", "warn", "skip"].includes(String(value["status"])))
1904
+ return null;
1905
+ const ok = value["status"] === "pass";
1906
+ const version = typeof value["detail"] === "string" && /^v?\d{1,6}(?:\.\d{1,6}){1,3}$/.test(value["detail"]) ? ` ${value["detail"]}` : "";
1907
+ return {
1908
+ id: `tool:${value["id"].toLowerCase().replace(/[^a-z0-9:._-]/g, "_")}`,
1909
+ status: value["status"],
1910
+ summary: ok ? `Expected tool version or runtime check succeeded${version}.` : "Expected tool version, runtime or authentication check did not pass.",
1911
+ ...!ok ? {
1912
+ remedy: {
1913
+ action: "Review scripts/dev/doctor.ts --json --docker locally. Use shared mise/workstation setup to reconcile this computer."
1914
+ }
1915
+ } : {}
1916
+ };
1917
+ }
1918
+
1919
+ // packages/fleet/src/auth-checks.ts
1920
+ async function authCheck(capability, run, root) {
1921
+ if (capability === "t3")
1922
+ return t3Checks(run);
1923
+ if (capability === "codex" || capability === "claude")
1924
+ return codingAuth(capability, run);
1925
+ if (capability === "services")
1926
+ return serviceAuth(run);
1927
+ if (capability === "workstation")
1928
+ return workstationChecks(root, run);
1929
+ if (capability === "toolchain")
1930
+ return toolchainChecks(root, run);
1931
+ return basicAuth(capability, run, root);
1932
+ }
1933
+ async function basicAuth(capability, run, root) {
1934
+ const spec = {
1935
+ github: {
1936
+ command: ["gh", "auth", "status", "--active"],
1937
+ action: "Authenticate on this node with gh auth login, then gh auth setup-git."
1938
+ },
1939
+ vault: {
1940
+ command: ["vault", "token", "lookup", "-format=json"],
1941
+ action: "Authenticate to the configured Vault on this node. Do not copy a token from another computer."
1942
+ },
1943
+ vscode: {
1944
+ command: ["code", "--list-extensions"],
1945
+ action: "Install the VS Code CLI on PATH and ms-vscode-remote.remote-ssh in the local editor profile."
1946
+ }
1947
+ }[capability];
1948
+ const result = await run(spec.command, {
1949
+ cwd: root,
1950
+ timeoutMs: 15000
1951
+ });
1952
+ const extensions = result.stdout.toLowerCase().split(/\s+/);
1953
+ const ok = result.ok && (capability !== "vscode" || extensions.includes("ms-vscode-remote.remote-ssh"));
1954
+ return [
1955
+ {
1956
+ id: `auth:${capability}`,
1957
+ status: ok ? "pass" : "fail",
1958
+ summary: ok ? `${capability} check succeeded in this process context.` : `${capability} check failed (${result.reason}); no credential output retained.`,
1959
+ ...ok ? {} : { remedy: { action: spec.action } }
1960
+ }
1961
+ ];
1962
+ }
1963
+ async function t3Checks(run) {
1964
+ const config = await run(["t3", "status", "--json"]);
1965
+ const checks = [t3Settings(config)];
1966
+ const live = await run(["t3", "environment", "list", "--json"]);
1967
+ checks.push({
1968
+ id: "t3:api",
1969
+ status: live.ok ? "pass" : "fail",
1970
+ 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.",
1971
+ ...!live.ok ? {
1972
+ remedy: {
1973
+ action: "Check t3 service status and t3 status. Inspect linked peers in T3 Settings → Connections; relink only with operator approval."
1974
+ }
1975
+ } : {}
1976
+ });
1977
+ checks.push({
1978
+ id: "t3:end-to-end",
1979
+ status: "skip",
1980
+ summary: "Relay traversal and remote peer authorization require an operator connection test; local settings are not proof."
1981
+ });
1982
+ return checks;
1983
+ }
1984
+ function t3Settings(config) {
1985
+ const fallback = {
1986
+ id: "t3:settings",
1987
+ status: "fail",
1988
+ summary: "T3 settings unavailable or unsupported CLI response.",
1989
+ remedy: {
1990
+ action: "Run t3 status --json on this node. Complete t3 up --connect as its operator if needed."
1991
+ }
1992
+ };
1993
+ try {
1994
+ const data = JSON.parse(config.stdout);
1995
+ if (config.ok && record(data) && record(data["connect"])) {
1996
+ const enabled = data["connect"]["enabled"] === true && data["connect"]["revoked"] === false;
1997
+ return {
1998
+ id: "t3:settings",
1999
+ status: enabled ? "pass" : "fail",
2000
+ summary: enabled ? "T3 Connect enabled and not marked revoked (settings only)." : "T3 Connect disabled, revoked, or unknown.",
2001
+ ...!enabled ? {
2002
+ remedy: {
2003
+ action: "Review t3 status, then complete t3 up --connect as the operator. Never share pairing tokens."
2004
+ }
2005
+ } : {}
2006
+ };
2007
+ }
2008
+ } catch {}
2009
+ return fallback;
2010
+ }
2011
+
2012
+ // packages/fleet/src/probe.ts
2013
+ async function probe(root, required, run) {
2014
+ const current = await fingerprint(root, run);
2015
+ const checks = [
2016
+ {
2017
+ id: "workspace",
2018
+ status: Object.values(current.files).every(Boolean) && current.head ? "pass" : "fail",
2019
+ summary: "Checkout revision and shared dependency/toolchain input availability."
2020
+ }
2021
+ ];
2022
+ const dirty = await run(["git", "status", "--porcelain"], { cwd: root });
2023
+ checks.push({
2024
+ id: "git:clean",
2025
+ status: dirty.ok ? dirty.stdout.trim() ? "warn" : "pass" : "fail",
2026
+ summary: dirty.ok && !dirty.stdout.trim() ? "Working tree is clean." : "Working tree is dirty or unreadable; sync --pull will refuse."
2027
+ });
2028
+ checks.push(await dependencyCheck(root, current));
2029
+ for (const capability of CAPABILITIES) {
2030
+ if (required.includes(capability))
2031
+ checks.push(...await authCheck(capability, run, root));
2032
+ else
2033
+ checks.push({
2034
+ id: `auth:${capability}`,
2035
+ status: "skip",
2036
+ summary: "Not required by this node configuration."
2037
+ });
2038
+ }
2039
+ return {
2040
+ version: 1,
2041
+ platform: process.platform,
2042
+ fingerprint: current,
2043
+ checks
2044
+ };
2045
+ }
2046
+
2047
+ // packages/fleet/src/report.ts
2048
+ function makeReport(command, nodes) {
2049
+ const summary = {
2050
+ pass: 0,
2051
+ warn: 0,
2052
+ fail: 0,
2053
+ skip: 0
2054
+ };
2055
+ for (const node of nodes)
2056
+ for (const check of node.checks)
2057
+ summary[check.status] += 1;
2058
+ return {
2059
+ version: 1,
2060
+ command,
2061
+ ok: summary.fail === 0,
2062
+ generatedAt: new Date().toISOString(),
2063
+ nodes,
2064
+ summary
2065
+ };
2066
+ }
2067
+ function reportExitCode(report, strict) {
2068
+ return !report.ok || strict && (report.summary.warn > 0 || report.summary.skip > 0) ? 1 : 0;
2069
+ }
2070
+ function renderReport(report) {
2071
+ const lines = [
2072
+ `Fleet ${report.command}: ${report.ok ? "no failing checks" : "attention needed"}`
2073
+ ];
2074
+ for (const node of report.nodes) {
2075
+ lines.push(`
2076
+ ${node.node}`);
2077
+ for (const check of node.checks) {
2078
+ lines.push(` ${check.status.toUpperCase()} ${check.id}: ${check.summary}`);
2079
+ if (check.remedy)
2080
+ lines.push(` Next: ${check.remedy.action}`);
2081
+ }
2082
+ }
2083
+ lines.push(`
2084
+ ${report.summary.pass} pass, ${report.summary.warn} warn, ${report.summary.fail} fail, ${report.summary.skip} skip`);
2085
+ return lines.join(`
2086
+ `);
2087
+ }
2088
+ async function mapBounded(items, worker, concurrency = 4) {
2089
+ const results = new Array(items.length);
2090
+ let cursor = 0;
2091
+ const consume = async () => {
2092
+ while (cursor < items.length) {
2093
+ const index = cursor++;
2094
+ results[index] = await worker(items[index]);
2095
+ }
2096
+ };
2097
+ await Promise.all(Array.from({ length: Math.min(concurrency, items.length) }, consume));
2098
+ return results;
2099
+ }
2100
+
2101
+ // packages/fleet/src/ssh.ts
2102
+ function shellQuote(value) {
2103
+ return `'${value.replaceAll("'", "'\\''")}'`;
2104
+ }
2105
+ function sshArgs(node) {
2106
+ return [
2107
+ "ssh",
2108
+ "-T",
2109
+ "-a",
2110
+ "-o",
2111
+ "BatchMode=yes",
2112
+ "-o",
2113
+ "StrictHostKeyChecking=yes",
2114
+ "-o",
2115
+ "UpdateHostKeys=no",
2116
+ "-o",
2117
+ "ConnectTimeout=8",
2118
+ "-o",
2119
+ "ConnectionAttempts=1",
2120
+ "-o",
2121
+ "ServerAliveInterval=5",
2122
+ "-o",
2123
+ "ServerAliveCountMax=2",
2124
+ "-o",
2125
+ "ClearAllForwardings=yes",
2126
+ "-o",
2127
+ "PermitLocalCommand=no",
2128
+ "-o",
2129
+ "ControlMaster=no",
2130
+ "-o",
2131
+ "ControlPath=none",
2132
+ node.ssh
2133
+ ];
2134
+ }
2135
+ function remoteCommand(node, request) {
2136
+ const encoded = Buffer.from(JSON.stringify(request)).toString("base64");
2137
+ const command = `cd -- ${shellQuote(node.workspace)} && exec ${shellQuote(node.runtime ?? "node")} --input-type=module - ${shellQuote(encoded)}`;
2138
+ return [...sshArgs(node), command];
2139
+ }
2140
+ var FAILURES = [
2141
+ {
2142
+ pattern: /REMOTE HOST IDENTIFICATION HAS CHANGED|Host key verification failed|No .* host key is known/i,
2143
+ id: "ssh:host-key",
2144
+ summary: "SSH host key is unknown or changed.",
2145
+ action: "Verify the host fingerprint out of band, then update known_hosts manually. Never disable host-key checking."
2146
+ },
2147
+ {
2148
+ pattern: /Could not resolve hostname|Name or service not known|nodename nor servname/i,
2149
+ id: "ssh:dns",
2150
+ summary: "SSH hostname or jump-host resolution failed.",
2151
+ action: "Check the alias in ~/.ssh/config, DNS/VPN/Tailscale connectivity, and ProxyJump host resolution."
2152
+ },
2153
+ {
2154
+ pattern: /Permission denied|sign_and_send_pubkey|agent refused|Too many authentication failures/i,
2155
+ id: "ssh:auth",
2156
+ summary: "SSH key/agent authentication failed.",
2157
+ 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."
2158
+ },
2159
+ {
2160
+ pattern: /Connection refused|No route to host|Network is unreachable/i,
2161
+ id: "ssh:network",
2162
+ summary: "SSH endpoint or jump host is unreachable/refused.",
2163
+ 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."
2164
+ },
2165
+ {
2166
+ pattern: /(?:bun|node).*not found|No such file or directory|cannot cd|can.t cd/i,
2167
+ id: "ssh:runtime",
2168
+ summary: "Remote workspace or Node/Bun runtime is missing from the non-login SSH environment.",
2169
+ action: "Follow fleet onboard; configure absolute workspace/runtime paths. GUI terminals and SSH shells may have different PATHs."
2170
+ }
2171
+ ];
2172
+ function sshFailure(result) {
2173
+ if (result.reason === "timeout")
2174
+ return {
2175
+ id: "ssh:timeout",
2176
+ status: "fail",
2177
+ summary: "SSH or remote probe exceeded its deadline.",
2178
+ remedy: {
2179
+ action: "Check sleeping nodes, VPN, ProxyJump, network reachability and noninteractive shell startup. Increase --timeout only after diagnosis."
2180
+ }
2181
+ };
2182
+ if (result.reason === "missing")
2183
+ return {
2184
+ id: "ssh:missing",
2185
+ status: "fail",
2186
+ summary: "Cannot launch the local SSH client.",
2187
+ remedy: {
2188
+ action: "Install/enable the OS OpenSSH client and expose it to the terminal/editor PATH."
2189
+ }
2190
+ };
2191
+ const match = FAILURES.find((failure) => failure.pattern.test(result.stderr));
2192
+ return {
2193
+ id: match?.id ?? "ssh:probe",
2194
+ status: "fail",
2195
+ summary: match?.summary ?? `Remote probe failed (${result.reason}); raw output withheld.`,
2196
+ remedy: {
2197
+ 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."
2198
+ }
2199
+ };
2200
+ }
2201
+
2202
+ // packages/fleet/src/protocol.ts
2203
+ var STATUSES = ["pass", "warn", "fail", "skip"];
2204
+ function validCheck(value) {
2205
+ 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)
2206
+ return false;
2207
+ const remedy = value["remedy"];
2208
+ return remedy === undefined || validRemedy(remedy);
2209
+ }
2210
+ function validRemedy(value) {
2211
+ return record(value) && typeof value["action"] === "string" && value["action"].length < 2000 && (value["command"] === undefined || typeof value["command"] === "string");
2212
+ }
2213
+ function validSnapshot(value) {
2214
+ if (!record(value) || value["version"] !== 1 || typeof value["platform"] !== "string" || !Array.isArray(value["checks"]) || value["checks"].length > 200 || !value["checks"].every(validCheck))
2215
+ return false;
2216
+ return validFingerprint(value["fingerprint"]);
2217
+ }
2218
+ function validFingerprint(value) {
2219
+ if (!record(value) || !record(value["files"]))
2220
+ return false;
2221
+ if (!Object.keys(value["files"]).every((key) => FINGERPRINT_FILES.includes(key)))
2222
+ return false;
2223
+ const head = value["head"];
2224
+ return (head === null || typeof head === "string" && /^[a-f0-9]{40,64}$/.test(head)) && Object.values(value["files"]).every(validHash);
2225
+ }
2226
+ function validHash(value) {
2227
+ return value === null || typeof value === "string" && /^[a-f0-9]{64}$/.test(value);
2228
+ }
2229
+ function decodeSnapshot(output) {
2230
+ try {
2231
+ const data = JSON.parse(output);
2232
+ return validSnapshot(data) ? cleanSnapshot(data) : null;
2233
+ } catch {
2234
+ return null;
2235
+ }
2236
+ }
2237
+ function cleanCheck(check) {
2238
+ return {
2239
+ id: check.id,
2240
+ status: check.status,
2241
+ summary: check.summary,
2242
+ ...check.remedy ? {
2243
+ remedy: {
2244
+ action: check.remedy.action,
2245
+ ...check.remedy.command ? { command: check.remedy.command } : {}
2246
+ }
2247
+ } : {}
2248
+ };
2249
+ }
2250
+ function cleanSnapshot(snapshot) {
2251
+ return {
2252
+ version: 1,
2253
+ platform: snapshot.platform,
2254
+ fingerprint: {
2255
+ head: snapshot.fingerprint.head,
2256
+ files: snapshot.fingerprint.files
2257
+ },
2258
+ checks: snapshot.checks.map(cleanCheck)
2259
+ };
2260
+ }
2261
+
2262
+ // packages/fleet/src/transport.ts
2263
+ import { fileURLToPath } from "node:url";
2264
+ async function bundleProbe() {
2265
+ const result = await Bun.build({
2266
+ entrypoints: [fileURLToPath(new URL("./remote-entry.ts", import.meta.url))],
2267
+ target: "node",
2268
+ minify: true
2269
+ });
2270
+ if (!result.success || !result.outputs[0])
2271
+ throw new Error("Cannot bundle the fleet probe.");
2272
+ return result.outputs[0].text();
2273
+ }
2274
+ async function inspectRemote(node, options, run) {
2275
+ const command = remoteCommand(node, {
2276
+ action: options.action,
2277
+ configuration: options.configuration,
2278
+ node,
2279
+ apply: options.action !== "probe",
2280
+ repairs: options.repairs,
2281
+ expected: options.expected,
2282
+ pull: options.pull === true
2283
+ });
2284
+ const result = await run(command, {
2285
+ input: options.source,
2286
+ timeoutMs: options.timeoutMs
2287
+ });
2288
+ if (!result.ok)
2289
+ return { node: node.id, checks: [sshFailure(result)] };
2290
+ if (options.action !== "probe")
2291
+ return decodeSync(node.id, result.stdout);
2292
+ const snapshot = decodeSnapshot(result.stdout);
2293
+ if (!snapshot)
2294
+ return protocolFailure(node.id);
2295
+ return {
2296
+ node: node.id,
2297
+ snapshot,
2298
+ checks: [
2299
+ {
2300
+ id: "ssh:session",
2301
+ status: "pass",
2302
+ summary: "Fresh noninteractive SSH session and Node-compatible probe succeeded with verified host keys and no agent forwarding."
2303
+ },
2304
+ ...snapshot.checks,
2305
+ {
2306
+ id: "vscode:forwarding",
2307
+ status: "skip",
2308
+ summary: "VS Code Remote-SSH tunnel/server installation is not tested by a shell session. Use the onboarding forwarding test."
2309
+ }
2310
+ ]
2311
+ };
2312
+ }
2313
+ function decodeSync(node, output) {
2314
+ try {
2315
+ const data = JSON.parse(output);
2316
+ if (record(data) && data["version"] === 1 && Array.isArray(data["checks"]) && data["checks"].length > 0 && data["checks"].length <= 200 && data["checks"].every(validCheck)) {
2317
+ return { node, checks: data["checks"].map(cleanCheck) };
2318
+ }
2319
+ } catch {
2320
+ return protocolFailure(node);
2321
+ }
2322
+ return protocolFailure(node);
2323
+ }
2324
+ function protocolFailure(node) {
2325
+ return {
2326
+ node,
2327
+ checks: [
2328
+ {
2329
+ id: "ssh:protocol",
2330
+ status: "fail",
2331
+ summary: "Remote response is not a supported fleet JSON document.",
2332
+ remedy: {
2333
+ action: "Remove stdout banners from noninteractive shell startup; check the Node/Bun runtime. No raw remote output is retained."
2334
+ }
2335
+ }
2336
+ ]
2337
+ };
2338
+ }
2339
+
2340
+ // packages/fleet/src/controller.ts
2341
+ async function doctor(nodes, options, run) {
2342
+ const local = await probe(options.root, options.required, run);
2343
+ const agent = await run(["ssh-add", "-l"]);
2344
+ const reports = [
2345
+ {
2346
+ node: "controller",
2347
+ snapshot: local,
2348
+ checks: [
2349
+ ...local.checks,
2350
+ {
2351
+ id: "ssh:agent",
2352
+ status: agent.ok ? "pass" : "warn",
2353
+ summary: agent.ok ? "SSH agent exposes at least one identity." : "No accessible SSH agent identities; explicit IdentityFile keys may still work.",
2354
+ ...!agent.ok ? {
2355
+ remedy: {
2356
+ action: "Unlock/load the intended key in the controller session. Check SSH_AUTH_SOCK and GUI/editor agent visibility; do not copy private keys."
2357
+ }
2358
+ } : {}
2359
+ }
2360
+ ]
2361
+ }
2362
+ ];
2363
+ if (options.local || nodes.length === 0)
2364
+ return reports;
2365
+ const source = options.source ?? await bundleProbe();
2366
+ const reference = await fingerprint(options.root, run);
2367
+ const remote = await mapBounded(nodes, async (node) => {
2368
+ const report = await inspectRemote(node, { action: "probe", source, timeoutMs: options.timeoutMs }, run);
2369
+ if (report.snapshot)
2370
+ report.checks.push(...compareFingerprints(reference, report.snapshot.fingerprint));
2371
+ return report;
2372
+ });
2373
+ return [...reports, ...remote];
2374
+ }
2375
+
2376
+ // packages/fleet/src/sync.ts
2377
+ import { unlink } from "node:fs/promises";
2378
+ import { join as join8 } from "node:path";
2379
+ function syncPlan(pull) {
2380
+ return [
2381
+ "Validate the checkout and shared input files; invalidate any prior sync receipt.",
2382
+ ...pull ? [
2383
+ "Refuse dirty or detached checkouts; git pull --ff-only (no reset, stash, force or rebase)."
2384
+ ] : [],
2385
+ "Run mise exec --locked bun -- bun scripts/dev/setup.ts --non-interactive (shared tools, user mise/PATH, frozen dependencies).",
2386
+ "Record input hashes only after every step succeeds.",
2387
+ "Run fleet doctor afterward; workstation/provider configuration and logins remain explicit ws/operator actions."
2388
+ ];
2389
+ }
2390
+ async function syncWorkspace(root, pull, run) {
2391
+ const before = await fingerprint(root, run);
2392
+ if (!completeFingerprint(before)) {
2393
+ return failed("Not a complete workspace checkout. Follow fleet onboard first.");
2394
+ }
2395
+ if (pull) {
2396
+ const dirty = await run(["git", "status", "--porcelain"], { cwd: root });
2397
+ const branch = await run(["git", "symbolic-ref", "--quiet", "HEAD"], {
2398
+ cwd: root
2399
+ });
2400
+ if (!dirty.ok || dirty.stdout.trim() || !branch.ok)
2401
+ return failed("Refusing pull: dirty, detached, or unreadable checkout. Reconcile manually.");
2402
+ }
2403
+ await unlink(join8(root, "node_modules/.fleet-sync.json")).catch(() => {
2404
+ return;
2405
+ });
2406
+ if (pull) {
2407
+ const result = await run(["git", "pull", "--ff-only"], {
2408
+ cwd: root,
2409
+ timeoutMs: 120000
2410
+ });
2411
+ if (!result.ok)
2412
+ return failed("Fast-forward pull failed. Inspect upstream/network/auth manually; nothing was reset.");
2413
+ }
2414
+ const expected = await fingerprint(root, run);
2415
+ const result = await run([
2416
+ "mise",
2417
+ "exec",
2418
+ "--locked",
2419
+ "bun",
2420
+ "--",
2421
+ "bun",
2422
+ "scripts/dev/setup.ts",
2423
+ "--non-interactive"
2424
+ ], { cwd: root, timeoutMs: 1200000 });
2425
+ if (!result.ok)
2426
+ return failed(`Shared toolchain/dependency sync failed (${result.reason}). Inspect setup locally; partial installs are possible.`);
2427
+ const after = await fingerprint(root, run);
2428
+ if (JSON.stringify(expected) !== JSON.stringify(after))
2429
+ return failed("Checkout inputs changed during sync. Retry from a stable checkout.");
2430
+ await writeReceipt(root, after);
2431
+ return [
2432
+ {
2433
+ id: "sync",
2434
+ status: "pass",
2435
+ summary: "Shared setup succeeded; frozen-sync receipt recorded. Auth and live connectivity still need doctor checks."
2436
+ }
2437
+ ];
2438
+ }
2439
+ function failed(summary) {
2440
+ return [
2441
+ {
2442
+ id: "sync",
2443
+ status: "fail",
2444
+ summary,
2445
+ remedy: {
2446
+ action: "Resolve locally using scripts/dev/README.md, then review and retry fleet sync."
2447
+ }
2448
+ }
2449
+ ];
2450
+ }
2451
+ function completeFingerprint(value) {
2452
+ return value.head !== null && Object.values(value.files).every(Boolean);
2453
+ }
2454
+
2455
+ // packages/fleet/src/onboard.ts
2456
+ function onboarding(node, launcher = "bun run fleet") {
2457
+ return {
2458
+ version: 1,
2459
+ node: node.id,
2460
+ applied: false,
2461
+ steps: [
2462
+ "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.",
2463
+ "Enable the OS SSH service as the operator on a trusted private network. Do not expose SSH publicly or create cloud resources.",
2464
+ "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.",
2465
+ "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.",
2466
+ "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.",
2467
+ "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.",
2468
+ "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.",
2469
+ "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.",
2470
+ "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.",
2471
+ "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."
2472
+ ],
2473
+ sshTemplate: `Host ${node.ssh}
2474
+ HostName REPLACE_WITH_PRIVATE_HOST
2475
+ User REPLACE_WITH_USER
2476
+ IdentityFile ~/.ssh/REPLACE_WITH_NODE_KEY
2477
+ IdentitiesOnly yes
2478
+ ForwardAgent no
2479
+ `,
2480
+ vscodeSettings: {
2481
+ "remote.SSH.path": "REPLACE_WITH_SAME_OPENSSH_BINARY",
2482
+ "remote.SSH.configFile": "REPLACE_WITH_ABSOLUTE_SSH_CONFIG",
2483
+ "remote.SSH.showLoginTerminal": true
2484
+ },
2485
+ workspaceCommand: `cd -- ${shellQuote(node.workspace)}`,
2486
+ verifyCommand: `${launcher} doctor --node ${node.id} --json`,
2487
+ sync: {
2488
+ command: `${launcher} sync --node ${node.id}`,
2489
+ steps: syncPlan(false)
2490
+ }
2491
+ };
2492
+ }
2493
+ // packages/fleet/schema.json
2494
+ var schema_default = {
2495
+ $schema: "https://json-schema.org/draft/2020-12/schema",
2496
+ title: "Developer fleet roster (not infrastructure inventory)",
2497
+ type: "object",
2498
+ additionalProperties: false,
2499
+ required: ["version", "nodes"],
2500
+ properties: {
2501
+ $schema: { type: "string" },
2502
+ version: { const: 1 },
2503
+ nodes: {
2504
+ type: "array",
2505
+ minItems: 1,
2506
+ maxItems: 64,
2507
+ items: {
2508
+ type: "object",
2509
+ additionalProperties: false,
2510
+ required: ["id", "ssh", "workspace"],
2511
+ properties: {
2512
+ id: { type: "string", pattern: "^[a-z][a-z0-9-]{0,62}$" },
2513
+ ssh: {
2514
+ type: "string",
2515
+ pattern: "^[a-zA-Z0-9][a-zA-Z0-9._-]{0,252}$"
2516
+ },
2517
+ workspace: {
2518
+ type: "string",
2519
+ maxLength: 4096,
2520
+ pattern: "^/[^\\u0000-\\u001f\\u007f]*$"
2521
+ },
2522
+ runtime: {
2523
+ type: "string",
2524
+ maxLength: 4096,
2525
+ pattern: "^/[^\\u0000-\\u001f\\u007f]*$"
2526
+ },
2527
+ required: {
2528
+ type: "array",
2529
+ uniqueItems: true,
2530
+ default: ["github", "vault", "t3", "toolchain"],
2531
+ items: {
2532
+ enum: [
2533
+ "github",
2534
+ "vault",
2535
+ "t3",
2536
+ "vscode",
2537
+ "toolchain",
2538
+ "codex",
2539
+ "claude",
2540
+ "services",
2541
+ "workstation"
2542
+ ]
2543
+ }
2544
+ }
2545
+ }
2546
+ }
2547
+ }
2548
+ }
2549
+ };
2550
+ // packages/fleet/fleet.example.json
2551
+ var fleet_example_default = {
2552
+ $schema: "./schema.json",
2553
+ version: 1,
2554
+ nodes: [
2555
+ {
2556
+ id: "dev-linux",
2557
+ ssh: "dev-linux",
2558
+ workspace: "/home/developer/workspace",
2559
+ runtime: "/home/developer/.local/share/mise/shims/node",
2560
+ required: ["github", "vault", "t3", "toolchain"]
2561
+ }
2562
+ ]
2563
+ };
2564
+
2565
+ // packages/personal-cli/src/options.ts
2566
+ function capabilities(value) {
2567
+ const values = value === "none" ? [] : value?.split(",") ?? CAPABILITIES;
2568
+ if (values.some((entry) => !CAPABILITIES.includes(entry)))
2569
+ throw new FleetUsageError(`--require expects ${CAPABILITIES.join(",")} or none.`);
2570
+ return [...new Set(values)];
2571
+ }
2572
+ function timeout(options) {
2573
+ const value = Number(options.timeout ?? (options.apply ? 1500000 : 120000));
2574
+ if (!Number.isInteger(value) || value < 1000 || value > 1800000)
2575
+ throw new FleetUsageError("--timeout must be 1000–1800000 milliseconds.");
2576
+ return value;
2577
+ }
2578
+ function validateOptions(command, options) {
2579
+ validateMutation(command, options);
2580
+ validateRepair(command, options);
2581
+ validateScope(command, options);
2582
+ if (["discover", "health", "repair"].includes(command) && options.require !== undefined)
2583
+ throw new FleetUsageError("Tailscale health always checks the complete capability set; --require is not supported.");
2584
+ if (options.timeout !== undefined)
2585
+ timeout(options);
2586
+ }
2587
+ function validateMutation(command, options) {
2588
+ if (options.apply && !["add", "init", "sync", "repair"].includes(command))
2589
+ throw new FleetUsageError("--apply is only valid for fleet add, init, sync or repair.");
2590
+ if (options.apply && !options.yes)
2591
+ throw new FleetUsageError("Mutation requires both --apply and --yes.");
2592
+ if (options.pull && command !== "sync")
2593
+ throw new FleetUsageError("--pull is only valid for fleet sync.");
2594
+ }
2595
+ function validateScope(command, options) {
2596
+ if (["discover", "health"].includes(command) && (options.local || options.all || options.node?.length))
2597
+ throw new FleetUsageError("Tailscale discovery and health always cover all visible computers; scope filters are not supported.");
2598
+ if (options.local && !["doctor", "sync", "repair"].includes(command))
2599
+ throw new FleetUsageError("--local is only valid for fleet doctor, sync or repair.");
2600
+ validateExclusive(options);
2601
+ if (options.all && !["doctor", "repair"].includes(command))
2602
+ throw new FleetUsageError("--all is only valid for fleet doctor or repair; sync already targets all configured nodes by default.");
2603
+ }
2604
+ function validateExclusive(options) {
2605
+ const selected = [
2606
+ options.local,
2607
+ options.all,
2608
+ Boolean(options.node?.length)
2609
+ ].filter(Boolean);
2610
+ if (selected.length > 1)
2611
+ throw new FleetUsageError("Choose --local, --all, or --node, not a combination.");
2612
+ }
2613
+ function validateRepair(command, options) {
2614
+ if (options.planHash !== undefined && !/^[a-f0-9]{64}$/.test(options.planHash))
2615
+ throw new FleetUsageError("--plan-hash expects the SHA-256 value from a repair preview.");
2616
+ if ((options.repair !== undefined || options.script || options.planHash !== undefined) && command !== "repair")
2617
+ throw new FleetUsageError("--repair and --script are only valid for fleet repair.");
2618
+ if (options.script && options.apply)
2619
+ throw new FleetUsageError("--script only generates a preview; run a reviewed repair separately with --apply --yes.");
2620
+ validateCatalogScope(command, options);
2621
+ }
2622
+ function validateCatalogScope(command, options) {
2623
+ if (command === "repairs" && (options.local || options.node?.length || options.all || options.require !== undefined))
2624
+ throw new FleetUsageError("fleet repairs is a static catalog and does not accept target or capability filters.");
2625
+ }
2626
+
2627
+ // packages/personal-cli/src/roster.ts
2628
+ import { lstat as lstat2, mkdir as mkdir4, open, readFile as readFile4, rename as rename2, unlink as unlink2 } from "node:fs/promises";
2629
+ import { dirname as dirname4 } from "node:path";
2630
+ import { randomUUID as randomUUID2 } from "node:crypto";
2631
+ async function readRoster(path) {
2632
+ try {
2633
+ const info = await lstat2(path);
2634
+ if (!info.isFile() || info.size > 1024 * 1024)
2635
+ throw new FleetUsageError("Roster must be a regular file smaller than 1 MiB; symlinks are not supported.");
2636
+ return parseConfig(JSON.parse(await readFile4(path, "utf8")));
2637
+ } catch (error) {
2638
+ if (record(error) && error["code"] === "ENOENT")
2639
+ return null;
2640
+ if (error instanceof FleetUsageError)
2641
+ throw error;
2642
+ throw new FleetUsageError("Cannot read a valid roster. Use fleet schema or choose --config PATH.");
2643
+ }
2644
+ }
2645
+ function nodeFromOptions(options) {
2646
+ if (!options.id || !options.ssh || !options.path)
2647
+ throw new FleetUsageError("fleet add requires --id NAME --ssh ALIAS --path /absolute/checkout.");
2648
+ const node = {
2649
+ id: options.id,
2650
+ ssh: options.ssh,
2651
+ workspace: options.path,
2652
+ ...options.runtime ? { runtime: options.runtime } : {},
2653
+ required: capabilities(options.require ?? "github,vault,t3,toolchain")
2654
+ };
2655
+ return parseConfig({ version: 1, nodes: [node] }).nodes[0];
2656
+ }
2657
+ function appendNode(existing, node) {
2658
+ return parseConfig({ version: 1, nodes: [...existing?.nodes ?? [], node] });
2659
+ }
2660
+ async function addNode(path, node, apply) {
2661
+ if (!apply)
2662
+ return appendNode(await readRoster(path), node);
2663
+ await mkdir4(dirname4(path), { recursive: true, mode: 448 });
2664
+ const lock = await acquireLock(path);
2665
+ try {
2666
+ const roster = appendNode(await readRoster(path), node);
2667
+ await atomicWrite(path, roster);
2668
+ return roster;
2669
+ } finally {
2670
+ await lock.close();
2671
+ await unlink2(`${path}.lock`);
2672
+ }
2673
+ }
2674
+ async function acquireLock(path) {
2675
+ try {
2676
+ return await open(`${path}.lock`, "wx", 384);
2677
+ } catch {
2678
+ throw new FleetUsageError("Roster is locked or unwritable. Check permissions and concurrent writers; remove a stale .lock only after verifying no operation is running.");
2679
+ }
2680
+ }
2681
+ async function atomicWrite(path, roster) {
2682
+ const temporary = `${path}.${randomUUID2()}.tmp`;
2683
+ const handle = await open(temporary, "wx", 384);
2684
+ try {
2685
+ await handle.writeFile(`${JSON.stringify(roster, null, 2)}
2686
+ `);
2687
+ await handle.sync();
2688
+ await handle.close();
2689
+ await rename2(temporary, path);
2690
+ } finally {
2691
+ await handle.close();
2692
+ await unlink2(temporary).catch(() => {
2693
+ return;
2694
+ });
2695
+ }
2696
+ }
2697
+
2698
+ // packages/fleet/src/tailscale.ts
2699
+ import { isIP } from "node:net";
2700
+ async function discoverTailnet(run) {
2701
+ const result = await run(["tailscale", "status", "--json"], {
2702
+ timeoutMs: 15000
2703
+ });
2704
+ if (!result.ok)
2705
+ return unavailable("Tailscale status is unavailable; check the CLI, daemon and account login.");
2706
+ try {
2707
+ return parseTailnet(JSON.parse(result.stdout));
2708
+ } catch {
2709
+ return unavailable("Tailscale returned an unsupported status document. Update the CLI and check its daemon.");
2710
+ }
2711
+ }
2712
+ function parseTailnet(data) {
2713
+ if (!record(data) || data["BackendState"] !== "Running")
2714
+ return unavailable("Tailscale is not running and signed in. Review tailscale status locally.");
2715
+ validateStatusFields(data);
2716
+ const peers = Object.values(data["Peer"] ?? {});
2717
+ if (peers.length > 1024)
2718
+ throw new Error("Too many peers");
2719
+ const all = [
2720
+ parseComputer(data["Self"], true),
2721
+ ...peers.map((peer) => parseComputer(peer, false))
2722
+ ];
2723
+ if (new Set(all.map((peer) => peer.id)).size !== all.length)
2724
+ throw new Error("Duplicate peer");
2725
+ const mobile = (peer) => ["ios", "android", "tvos"].includes(peer.os.toLowerCase());
2726
+ const computers = all.filter((peer) => !mobile(peer)).sort((a, b) => a.name.localeCompare(b.name, "en"));
2727
+ const healthy = healthyDiscovery(data, computers.length);
2728
+ return {
2729
+ computers,
2730
+ excludedMobile: all.filter(mobile).length,
2731
+ checks: [
2732
+ {
2733
+ id: "tailscale:discovery",
2734
+ status: healthy ? "pass" : "fail",
2735
+ summary: healthy ? "Signed-in Tailscale daemon returned its visible computer inventory." : "Tailscale reports a daemon/coordination health problem or no computers; discovery may be incomplete.",
2736
+ ...!healthy ? {
2737
+ remedy: {
2738
+ action: "Review tailscale status and network connectivity. Resolve coordination/daemon health before claiming complete coverage."
2739
+ }
2740
+ } : {}
2741
+ }
2742
+ ]
2743
+ };
2744
+ }
2745
+ function healthyDiscovery(data, count) {
2746
+ const health = data["Health"] ?? [];
2747
+ return health.length === 0 && count > 0;
2748
+ }
2749
+ function validateStatusFields(data) {
2750
+ const health = data["Health"] ?? [];
2751
+ const peers = data["Peer"] ?? {};
2752
+ if (!record(data["Self"]) || !record(peers) || !Array.isArray(health))
2753
+ throw new Error("Unsupported Tailscale status");
2754
+ }
2755
+ function parseComputer(value, self) {
2756
+ if (!record(value))
2757
+ throw new Error("Invalid peer");
2758
+ const {
2759
+ ID: id,
2760
+ HostName: name,
2761
+ DNSName: dns,
2762
+ OS: os,
2763
+ TailscaleIPs: ips,
2764
+ Online: online
2765
+ } = value;
2766
+ if (![id, name, os].every((field) => typeof field === "string" && field.length > 0 && field.length <= 256 && !/[\x00-\x1f\x7f]/.test(field)) || typeof dns !== "string" || !/^[a-zA-Z0-9][a-zA-Z0-9.-]{0,252}\.?$/.test(dns) || !Array.isArray(ips) || ips.length < 1 || ips.length > 16 || !ips.every((ip) => typeof ip === "string" && isIP(ip) !== 0) || typeof online !== "boolean")
2767
+ throw new Error("Invalid peer fields");
2768
+ return {
2769
+ id,
2770
+ name,
2771
+ dns: dns.replace(/\.$/, "").toLowerCase(),
2772
+ os,
2773
+ ips,
2774
+ online,
2775
+ self
2776
+ };
2777
+ }
2778
+ function unavailable(summary) {
2779
+ return {
2780
+ computers: [],
2781
+ excludedMobile: 0,
2782
+ checks: [{ id: "tailscale:discovery", status: "fail", summary }]
2783
+ };
2784
+ }
2785
+
2786
+ // packages/fleet/src/tailnet-matching.ts
2787
+ function matchesComputer(host, peer) {
2788
+ const name = host.toLowerCase().replace(/\.$/, "");
2789
+ return [peer.dns, peer.dns.split(".")[0], ...peer.ips].includes(name);
2790
+ }
2791
+ async function matchComputers(nodes, peers, run) {
2792
+ const matches = new Map(peers.map((peer) => [peer.id, []]));
2793
+ for (const node of nodes) {
2794
+ const resolved = await run([
2795
+ "ssh",
2796
+ "-G",
2797
+ "-o",
2798
+ "CanonicalizeHostname=no",
2799
+ "-o",
2800
+ "PermitLocalCommand=no",
2801
+ node.ssh
2802
+ ], { timeoutMs: 5000 });
2803
+ const hostname = resolved.ok ? /^hostname ([^\r\n]+)$/im.exec(resolved.stdout)?.[1]?.trim() : undefined;
2804
+ const targets = peers.filter((peer) => matchesComputer(hostname ?? node.ssh, peer));
2805
+ for (const peer of targets)
2806
+ matches.get(peer.id).push(node);
2807
+ }
2808
+ return matches;
2809
+ }
2810
+
2811
+ // packages/fleet/src/tailnet-health.ts
2812
+ var HEALTH_CAPABILITIES = [
2813
+ "github",
2814
+ "vault",
2815
+ "t3",
2816
+ "toolchain",
2817
+ "codex",
2818
+ "claude",
2819
+ "services",
2820
+ "workstation"
2821
+ ];
2822
+ async function tailnetHealth(tailnet, nodes, options, run) {
2823
+ const controller = await probe(options.root, HEALTH_CAPABILITIES, run);
2824
+ const matches = await matchComputers(nodes, tailnet.computers, run);
2825
+ let source;
2826
+ const reports = await mapBounded(tailnet.computers, async (peer) => {
2827
+ const checks = await connectivity(peer, run);
2828
+ if (peer.self)
2829
+ return {
2830
+ node: peer.dns,
2831
+ snapshot: controller,
2832
+ checks: [...checks, ...controller.checks]
2833
+ };
2834
+ const candidates = matches.get(peer.id);
2835
+ if (!uniqueMapping(candidates, matches))
2836
+ return missingMapping(peer, checks, candidates.length);
2837
+ if (!peer.online)
2838
+ return { node: peer.dns, checks };
2839
+ const node = candidates[0];
2840
+ const report = await inspectComputer({
2841
+ ...node,
2842
+ required: [...new Set([...node.required, ...HEALTH_CAPABILITIES])]
2843
+ }, {
2844
+ action: "probe",
2845
+ source: source ??= Promise.resolve().then(options.source),
2846
+ timeoutMs: options.timeoutMs
2847
+ }, run);
2848
+ return combine(peer, checks, report, controller);
2849
+ });
2850
+ const absent = nodes.filter((node) => ![...matches.values()].some((matched) => matched.includes(node)));
2851
+ return [
2852
+ { node: "tailscale", checks: tailnet.checks },
2853
+ ...!tailnet.computers.some((peer) => peer.self) ? [
2854
+ {
2855
+ node: "controller",
2856
+ snapshot: controller,
2857
+ checks: controller.checks
2858
+ }
2859
+ ] : [],
2860
+ ...reports,
2861
+ ...absent.map((node) => ({
2862
+ node: node.id,
2863
+ checks: [
2864
+ {
2865
+ id: "tailscale:membership",
2866
+ status: "fail",
2867
+ summary: "Registered node is absent from visible Tailscale inventory. It was not silently omitted.",
2868
+ remedy: {
2869
+ action: "Check the node account, tailnet visibility, and the registered SSH alias."
2870
+ }
2871
+ }
2872
+ ]
2873
+ }))
2874
+ ];
2875
+ }
2876
+ function uniqueMapping(candidates, matches) {
2877
+ return candidates.length === 1 && [...matches.values()].filter((nodes) => nodes.includes(candidates[0])).length === 1;
2878
+ }
2879
+ async function inspectComputer(node, options, run) {
2880
+ try {
2881
+ return await inspectRemote(node, { ...options, source: await options.source }, run);
2882
+ } catch {
2883
+ return {
2884
+ node: node.id,
2885
+ checks: [
2886
+ {
2887
+ id: "fleet:probe",
2888
+ status: "fail",
2889
+ summary: "The current remote probe could not be prepared or completed; this computer remains unverified.",
2890
+ remedy: {
2891
+ action: "Reinstall the validated Personal CLI package and retry. No raw exception or remote output is retained."
2892
+ }
2893
+ }
2894
+ ]
2895
+ };
2896
+ }
2897
+ }
2898
+ async function connectivity(peer, run) {
2899
+ const online = {
2900
+ id: "tailscale:online",
2901
+ status: peer.online ? "pass" : "fail",
2902
+ summary: peer.online ? "Computer is online in Tailscale status." : "Computer is offline or Tailscale cannot confirm its online state."
2903
+ };
2904
+ if (peer.self || !peer.online)
2905
+ return [online];
2906
+ const ping = await run(["tailscale", "ping", "--c=1", "--timeout=5s", peer.ips[0]], { timeoutMs: 8000 });
2907
+ return [
2908
+ online,
2909
+ {
2910
+ id: "tailscale:reachability",
2911
+ status: ping.ok ? "pass" : "fail",
2912
+ summary: ping.ok ? "Tailscale peer answered a bounded reachability probe." : "Tailscale peer did not answer; online inventory is not proof of connectivity."
2913
+ }
2914
+ ];
2915
+ }
2916
+ function missingMapping(peer, checks, count) {
2917
+ return {
2918
+ node: peer.dns,
2919
+ checks: [
2920
+ ...checks,
2921
+ {
2922
+ id: "fleet:registration",
2923
+ status: "fail",
2924
+ summary: count ? "Roster mapping is ambiguous between entries or computers; no SSH probe was run." : "Computer is not registered with a verified SSH alias and checkout path; dependencies and authentication are unverified.",
2925
+ remedy: {
2926
+ action: "Use Fleet: Register a developer computer with its existing SSH alias and actual checkout path, then rerun Fleet: Check all Tailscale computers."
2927
+ }
2928
+ }
2929
+ ]
2930
+ };
2931
+ }
2932
+ function combine(peer, checks, report, reference) {
2933
+ return {
2934
+ ...report,
2935
+ node: peer.dns,
2936
+ checks: [
2937
+ ...checks,
2938
+ ...report.checks,
2939
+ ...report.snapshot ? compareFingerprints(reference.fingerprint, report.snapshot.fingerprint) : []
2940
+ ]
2941
+ };
2942
+ }
2943
+
2944
+ // packages/personal-cli/src/tailnet.ts
2945
+ async function executeTailnet(command, options, settings, services) {
2946
+ const tailnet = await discoverTailnet(services.run);
2947
+ const coverage = {
2948
+ scope: "tailscale-visible-computers",
2949
+ computers: tailnet.computers.length,
2950
+ excludedMobile: tailnet.excludedMobile,
2951
+ limitation: "This controller can inspect only devices visible to its Tailscale account/ACLs. Other tailnets, hidden devices and all-pairs connectivity are not verified."
2952
+ };
2953
+ if (command === "discover") {
2954
+ const ok = tailnet.checks.every((check) => check.status === "pass");
2955
+ return {
2956
+ exitCode: ok ? 0 : 1,
2957
+ data: {
2958
+ version: 1,
2959
+ command,
2960
+ ok,
2961
+ coverage,
2962
+ computers: tailnet.computers,
2963
+ checks: tailnet.checks
2964
+ }
2965
+ };
2966
+ }
2967
+ const roster = await readRoster(settings.config);
2968
+ const nodes = await tailnetHealth(tailnet, roster?.nodes ?? [], {
2969
+ root: settings.workspace,
2970
+ timeoutMs: timeout({ ...options, timeout: options.timeout ?? "300000" }),
2971
+ source: services.source
2972
+ }, services.run);
2973
+ const report = makeReport("health", nodes);
2974
+ const exitCode = reportExitCode(report, true);
2975
+ const verified = exitCode === 0;
2976
+ return {
2977
+ exitCode,
2978
+ data: {
2979
+ ...report,
2980
+ ok: verified,
2981
+ coverage: {
2982
+ ...coverage,
2983
+ verified,
2984
+ inspected: nodes.filter((node) => node.snapshot).length
2985
+ }
2986
+ },
2987
+ text: `${renderReport({ ...report, ok: verified })}
2988
+ Coverage: ${coverage.computers} visible computers; ${coverage.excludedMobile} mobile devices excluded.
2989
+ ${coverage.limitation}`
2990
+ };
2991
+ }
2992
+
2993
+ // packages/personal-cli/src/probe-source.ts
2994
+ import { readFile as readFile5 } from "node:fs/promises";
2995
+ async function probeSource() {
2996
+ if (typeof Bun !== "undefined")
2997
+ return bundleProbe();
2998
+ return readFile5(new URL("./probe.mjs", import.meta.url), "utf8");
2999
+ }
3000
+
3001
+ // packages/fleet/src/repair-catalog.ts
3002
+ var login = (id, title, commands, action) => ({
3003
+ id: `login:${id}`,
3004
+ title,
3005
+ owner: `${id} native CLI`,
3006
+ automatic: false,
3007
+ action,
3008
+ commands
3009
+ });
3010
+ var REPAIR_RECIPES = [
3011
+ {
3012
+ id: "workspace",
3013
+ title: "Install locked tools and frozen dependencies",
3014
+ owner: "mise / scripts/dev/setup.ts",
3015
+ automatic: true,
3016
+ action: "Runs the existing shared setup and trusted checkout lifecycle scripts. Updates user mise/PATH configuration and writes a sync receipt. Does not pull or reset Git.",
3017
+ commands: [
3018
+ [
3019
+ "mise",
3020
+ "exec",
3021
+ "--locked",
3022
+ "bun",
3023
+ "--",
3024
+ "bun",
3025
+ "scripts/dev/setup.ts",
3026
+ "--non-interactive"
3027
+ ]
3028
+ ]
3029
+ },
3030
+ {
3031
+ id: "workstation",
3032
+ title: "Install launcher and converge managed workstation configuration",
3033
+ owner: "workstation",
3034
+ automatic: true,
3035
+ action: "Runs personal:install --yes: frozen install, launcher/PATH, managed links, notifications and provider configuration. Reads available provider keys from this machine’s Vault session and writes owner-managed 0600 configuration. Conflicting unmanaged files fail.",
3036
+ commands: [["bun", "run", "personal:install", "--yes"]]
3037
+ },
3038
+ {
3039
+ id: "providers",
3040
+ title: "Refresh shared provider configuration from Vault",
3041
+ owner: "workstation / secret-store",
3042
+ automatic: true,
3043
+ action: "Uses this machine’s own Vault authorization to fetch the existing permitted provider keys. The workstation owner writes its 0600 tool configuration; no credentials are copied from another computer. Missing provider keys may remain skipped.",
3044
+ commands: [
3045
+ [
3046
+ "bun",
3047
+ "packages/workstation/cli/internal.ts",
3048
+ "opencode",
3049
+ "sync",
3050
+ "--strict",
3051
+ "--json",
3052
+ "--non-interactive",
3053
+ "--yes"
3054
+ ]
3055
+ ]
3056
+ },
3057
+ login("codex", "Sign in to Codex on this machine", [["codex", "login", "--device-auth"]], "Approve the device flow in your browser. Run codex login status afterward; retain this machine’s own credential store."),
3058
+ login("claude", "Sign in to Claude Code on this machine", [["claude", "auth", "login"]], "Complete the native browser login, then run claude auth status --json. API-key and subscription contexts can differ."),
3059
+ login("github", "Sign in to GitHub and configure Git", [
3060
+ [
3061
+ "gh",
3062
+ "auth",
3063
+ "login",
3064
+ "--hostname",
3065
+ "github.com",
3066
+ "--web",
3067
+ "--git-protocol",
3068
+ "https"
3069
+ ],
3070
+ ["gh", "auth", "setup-git"]
3071
+ ], "Approve GitHub in the browser on the intended account, then run gh auth status --active. Environment tokens can override stored login; inspect their source locally."),
3072
+ login("vault", "Authorize this machine for the shared Vault", [], "Run vault login -method=userpass username=YOUR_USERNAME in your own terminal. The native CLI prompts privately for the password. Use the existing permitted project/config; never copy VAULT_TOKEN between computers."),
3073
+ login("railway", "Sign in to Railway", [["railway", "login", "--browserless"]], "Complete the browserless native flow, then run railway whoami --json."),
3074
+ login("vercel", "Sign in to Vercel", [["vercel", "login"]], "Complete native login, then run vercel whoami. Select the intended team/project separately; do not create resources as a login repair."),
3075
+ login("cloudflare", "Sign in to Cloudflare", [["wrangler", "login", "--browser=false"]], "Complete native OAuth login, then run wrangler whoami --json. For headless SSH, prefer an existing scoped API token injected by the approved Vault config. --browser=false suppresses browser launch; it does not make a remote loopback callback reachable. Any OAuth callback tunnel needs explicit operator setup. Resource access still depends on account permissions."),
3076
+ login("aws", "Refresh the selected AWS SSO session", [["aws", "sso", "login"]], "Configure the intended profile with aws configure sso first if needed. Login and the health check use this shell’s current profile; no access keys are cloned."),
3077
+ login("npm", "Sign in to npm on this machine", [
3078
+ [
3079
+ "npm",
3080
+ "login",
3081
+ "--auth-type=web",
3082
+ "--registry=https://registry.npmjs.org/"
3083
+ ]
3084
+ ], "Complete the specific CLI approval and run npm whoami. Prefer the existing trusted-publisher CI workflow for releases."),
3085
+ login("opencode", "Authorize an OpenCode provider", [["opencode", "auth", "login"]], "Prefer the existing Vault-backed providers repair for managed API keys. Native auth login handles an operator-owned OAuth provider. Credential listing alone does not prove live provider access."),
3086
+ login("copilot", "Sign in to Copilot", [["copilot", "login"]], "Complete native login; provider authorization still needs supported operator diagnostics. No model prompt is used for health checking."),
3087
+ login("gemini", "Sign in to Gemini", [], "Open Gemini interactively and choose its supported sign-in flow. Provider authorization remains unverified without a supported read-only status adapter."),
3088
+ {
3089
+ id: "t3",
3090
+ title: "Enable and authorize T3 Connect",
3091
+ owner: "T3",
3092
+ automatic: false,
3093
+ action: "Run t3 up --connect in this machine’s terminal and approve the intended connection. Treat pairing material as private. Then check t3 status --json and perform an operator peer test.",
3094
+ commands: [["t3", "up", "--connect"]]
3095
+ },
3096
+ {
3097
+ id: "docker",
3098
+ title: "Start the intended Docker runtime",
3099
+ owner: "OS / Docker",
3100
+ automatic: false,
3101
+ action: "Start Docker Desktop on desktop systems, or the already-installed Docker service using the OS service manager on Linux. Confirm the intended context with docker context ls. This repair never installs a daemon, invokes sudo, changes contexts or exposes a TCP socket.",
3102
+ commands: [
3103
+ ["docker", "context", "ls"],
3104
+ ["docker", "info"]
3105
+ ]
3106
+ },
3107
+ {
3108
+ id: "password-manager",
3109
+ title: "Review shared human login onboarding",
3110
+ owner: "password-manager / Bitwarden",
3111
+ automatic: false,
3112
+ action: "Use the existing password-manager doctor and actions list on this computer. Its setup guides native consent and Bitwarden enrollment. Keep its private state and OS credential store local. Password vault access does not authorize native CLI OAuth sessions; imports and rotations remain separate reviewed workflows.",
3113
+ commands: [
3114
+ ["bun", "run", "password-manager", "doctor"],
3115
+ ["bun", "run", "password-manager", "actions", "list"]
3116
+ ]
3117
+ },
3118
+ {
3119
+ id: "operator",
3120
+ title: "Resolve the check with its owning tool",
3121
+ owner: "operator / existing owner",
3122
+ automatic: false,
3123
+ action: "Follow the check’s remedy on the affected machine. Verify host fingerprints, register real SSH aliases/checkouts, reconcile Git changes, or perform the named end-to-end test. Infrastructure changes remain OpenTofu-owned.",
3124
+ commands: []
3125
+ }
3126
+ ];
3127
+ var AUTOMATED_REPAIRS = [
3128
+ "workspace",
3129
+ "workstation",
3130
+ "providers"
3131
+ ];
3132
+ function recipe(id) {
3133
+ return REPAIR_RECIPES.find((entry) => entry.id === id) ?? REPAIR_RECIPES.find((entry) => entry.id === "operator");
3134
+ }
3135
+
3136
+ // packages/fleet/src/repair-plan.ts
3137
+ function repairId(check) {
3138
+ if (check.id.startsWith("tool:docker-"))
3139
+ return "docker";
3140
+ if (check.id.startsWith("tool:github-auth"))
3141
+ return "login:github";
3142
+ if (["dependencies", "auth:toolchain"].includes(check.id))
3143
+ return "workspace";
3144
+ if (check.id.startsWith("tool:"))
3145
+ return "workspace";
3146
+ if (check.id.startsWith("workstation:"))
3147
+ return "workstation";
3148
+ if (check.id.startsWith("auth:") && !["auth:vscode", "auth:workstation"].includes(check.id))
3149
+ return `login:${check.id.slice(5)}`;
3150
+ if (check.id === "t3:settings" || check.id === "t3:api")
3151
+ return "t3";
3152
+ return "operator";
3153
+ }
3154
+ function repairPlan(reports, targets) {
3155
+ const steps = [];
3156
+ for (const report of reports) {
3157
+ const groups = new Map;
3158
+ for (const check of report.checks.filter((entry) => entry.status !== "pass")) {
3159
+ const repair = recipe(repairId(check)).id;
3160
+ groups.set(repair, [...groups.get(repair) ?? [], check]);
3161
+ }
3162
+ for (const [id, checks] of groups)
3163
+ steps.push({
3164
+ ...recipe(id),
3165
+ node: report.node,
3166
+ eligible: targets.some((target) => target.name === report.node) && report.snapshot !== undefined,
3167
+ checks: checks.map((check) => check.id),
3168
+ next: [
3169
+ ...new Set(checks.flatMap((check) => check.remedy ? [check.remedy.action] : [check.summary]))
3170
+ ]
3171
+ });
3172
+ }
3173
+ return steps;
3174
+ }
3175
+ function automaticRepairs(steps, node) {
3176
+ return AUTOMATED_REPAIRS.flatMap((id) => steps.filter((step) => step.node === node && step.id === id && step.automatic && step.eligible));
3177
+ }
3178
+
3179
+ // packages/personal-cli/src/repair-inspect.ts
3180
+ async function inspectRepair(options, settings, services) {
3181
+ if (options.local)
3182
+ return inspectLocal(settings, services);
3183
+ const roster = await readRoster(settings.config);
3184
+ if (options.node?.length)
3185
+ return inspectSelected(options, roster, services);
3186
+ return inspectTailnet(options, settings, roster, services);
3187
+ }
3188
+ async function inspectLocal(settings, services) {
3189
+ const snapshot = await probe(settings.workspace, HEALTH_CAPABILITIES, services.run);
3190
+ return {
3191
+ reports: [{ node: "local", snapshot, checks: snapshot.checks }],
3192
+ targets: [
3193
+ {
3194
+ name: "local",
3195
+ root: settings.workspace,
3196
+ fingerprint: snapshot.fingerprint
3197
+ }
3198
+ ],
3199
+ coverage: {
3200
+ scope: "local",
3201
+ limitation: "Only this process context was tested."
3202
+ }
3203
+ };
3204
+ }
3205
+ async function inspectSelected(options, roster, services) {
3206
+ if (!roster)
3207
+ throw new FleetUsageError("Register real SSH aliases and checkout paths before selecting remote repairs.");
3208
+ const nodes = selectNodes(roster, options.node ?? []);
3209
+ const source = await services.source();
3210
+ const reports = await mapBounded(nodes, (node) => inspectRemote({
3211
+ ...node,
3212
+ required: [...new Set([...node.required, ...HEALTH_CAPABILITIES])]
3213
+ }, { action: "probe", source, timeoutMs: timeout(options) }, services.run));
3214
+ return {
3215
+ reports,
3216
+ targets: nodes.map((node) => attachFingerprint({ name: node.id, root: node.workspace, node }, reports)),
3217
+ coverage: {
3218
+ scope: "selected",
3219
+ limitation: "Only selected roster computers were tested; other devices are unverified."
3220
+ }
3221
+ };
3222
+ }
3223
+ async function inspectTailnet(options, settings, roster, services) {
3224
+ const tailnet = await discoverTailnet(services.run);
3225
+ const reports = await tailnetHealth(tailnet, roster?.nodes ?? [], {
3226
+ root: settings.workspace,
3227
+ timeoutMs: timeout(options),
3228
+ source: services.source
3229
+ }, services.run);
3230
+ const matches = await matchComputers(roster?.nodes ?? [], tailnet.computers, services.run);
3231
+ const targets = [];
3232
+ for (const peer of tailnet.computers) {
3233
+ if (peer.self)
3234
+ targets.push({ name: peer.dns, root: settings.workspace });
3235
+ else if (peer.online && uniqueMapping(matches.get(peer.id), matches)) {
3236
+ const node = matches.get(peer.id)[0];
3237
+ targets.push({ name: peer.dns, root: node.workspace, node });
3238
+ }
3239
+ }
3240
+ if (!tailnet.computers.some((peer) => peer.self))
3241
+ targets.push({ name: "controller", root: settings.workspace });
3242
+ return {
3243
+ reports,
3244
+ targets: targets.map((target) => attachFingerprint(target, reports)),
3245
+ coverage: {
3246
+ scope: "tailscale-visible-computers",
3247
+ limitation: "Only devices visible to this Tailscale account/ACLs. Offline, unregistered or ambiguous computers cannot be repaired. No credentials, host keys or resources are cloned."
3248
+ }
3249
+ };
3250
+ }
3251
+ function attachFingerprint(target, reports) {
3252
+ const snapshot = reports.find((report) => report.node === target.name)?.snapshot;
3253
+ return {
3254
+ ...target,
3255
+ ...snapshot ? { fingerprint: snapshot.fingerprint } : {}
3256
+ };
3257
+ }
3258
+
3259
+ // packages/fleet/src/repair-runner.ts
3260
+ function validRepairs(value) {
3261
+ return Array.isArray(value) && value.length > 0 && value.length <= 3 && new Set(value).size === value.length && value.every((id) => AUTOMATED_REPAIRS.includes(id));
3262
+ }
3263
+ async function repairWorkspace(root, repairs, run, expected) {
3264
+ if (!validRepairs(repairs))
3265
+ throw new Error("Unsupported repairs");
3266
+ const before = await fingerprint(root, run);
3267
+ if (!before.head || !Object.values(before.files).every(Boolean))
3268
+ return [
3269
+ {
3270
+ id: "repair:workspace",
3271
+ status: "fail",
3272
+ summary: "Repairs require a complete existing checkout. Register/bootstrap it first."
3273
+ }
3274
+ ];
3275
+ if (expected && JSON.stringify(before) !== JSON.stringify(expected))
3276
+ return [
3277
+ {
3278
+ id: "repair:inputs",
3279
+ status: "fail",
3280
+ summary: "Checkout inputs changed since diagnostics. Generate a fresh repair plan before applying."
3281
+ }
3282
+ ];
3283
+ const checks = [];
3284
+ for (const id of AUTOMATED_REPAIRS.filter((entry) => repairs.includes(entry))) {
3285
+ const result = id === "workspace" ? await syncWorkspace(root, false, run) : await ownerCommand(root, id, run);
3286
+ checks.push(...result);
3287
+ if (result.some((check) => check.status !== "pass"))
3288
+ break;
3289
+ }
3290
+ const after = await fingerprint(root, run);
3291
+ if (JSON.stringify(before) !== JSON.stringify(after))
3292
+ checks.push({
3293
+ id: "repair:inputs",
3294
+ status: "fail",
3295
+ summary: "Checkout inputs changed during repair. Stop and review the checkout before retrying."
3296
+ });
3297
+ return checks;
3298
+ }
3299
+ async function ownerCommand(root, id, run) {
3300
+ const result = await run(recipe(id).commands[0], {
3301
+ cwd: root,
3302
+ timeoutMs: 1200000
3303
+ });
3304
+ return [
3305
+ {
3306
+ id: `repair:${id}`,
3307
+ status: result.ok ? "pass" : "fail",
3308
+ summary: result.ok ? "The existing owner command completed. Fresh diagnostics determine whether configuration and authorization now pass." : `The owner command failed (${result.reason}); partial changes are possible. Raw output and credentials are withheld. Later repairs on this computer were stopped.`
3309
+ }
3310
+ ];
3311
+ }
3312
+
3313
+ // packages/personal-cli/src/repair-apply.ts
3314
+ async function applyRepairs(targets, steps, options, services) {
3315
+ let source;
3316
+ return mapBounded(targets, async (target) => {
3317
+ const actions = automaticRepairs(steps, target.name);
3318
+ const checks = [];
3319
+ for (const step of actions) {
3320
+ try {
3321
+ if (!target.fingerprint)
3322
+ throw new Error("No verified checkout fingerprint");
3323
+ const repairs = [step.id];
3324
+ const result = target.node ? (await inspectRemote(target.node, {
3325
+ action: "repair",
3326
+ repairs,
3327
+ expected: target.fingerprint,
3328
+ source: await (source ??= Promise.resolve().then(services.source)),
3329
+ timeoutMs: timeout(options)
3330
+ }, services.run)).checks : await repairWorkspace(target.root, repairs, services.run, target.fingerprint);
3331
+ checks.push(...result);
3332
+ if (result.some((check) => check.status !== "pass"))
3333
+ break;
3334
+ } catch {
3335
+ checks.push({
3336
+ id: "repair:operation",
3337
+ status: "fail",
3338
+ summary: "Repair could not be prepared or completed. Later actions on this computer were stopped; inspect partial changes locally. Raw errors are withheld."
3339
+ });
3340
+ break;
3341
+ }
3342
+ }
3343
+ return { node: target.name, checks };
3344
+ }, 1);
3345
+ }
3346
+
3347
+ // packages/personal-cli/src/repair-script.ts
3348
+ function targetCommand(target, commands) {
3349
+ const command = `cd -- ${shellQuote(target.root)} && ${commands.map((args) => args.map(shellQuote).join(" ")).join(" && ")}`;
3350
+ if (!target.node)
3351
+ return command;
3352
+ return [
3353
+ ...sshArgs(target.node).map((arg) => arg === "-T" ? "-t" : arg).map(shellQuote),
3354
+ shellQuote(command)
3355
+ ].join(" ");
3356
+ }
3357
+ function repairScript(steps, targets) {
3358
+ const lines = [
3359
+ "#!/bin/sh",
3360
+ "set -eu",
3361
+ "# Generated repair plan. Review before running in your own terminal.",
3362
+ "# Automated steps use existing owners. Native login commands below are commented and require operator approval.",
3363
+ "# POSIX shell required; Windows operators can use the personal commands directly."
3364
+ ];
3365
+ for (const target of targets) {
3366
+ const actions = automaticRepairs(steps, target.name);
3367
+ if (!actions.length)
3368
+ continue;
3369
+ const commands = actions.flatMap((step) => step.id === "workspace" ? [
3370
+ [
3371
+ "bun",
3372
+ "packages/fleet/src/cli.ts",
3373
+ "sync",
3374
+ "--local",
3375
+ "--apply",
3376
+ "--yes",
3377
+ "--json"
3378
+ ]
3379
+ ] : recipe(step.id).commands);
3380
+ lines.push("", targetCommand(target, commands));
3381
+ }
3382
+ for (const step of steps.filter((entry) => !entry.automatic || !entry.eligible)) {
3383
+ lines.push("", `# ${comment(step.node)} / ${comment(step.title)}`, `# ${comment(step.action)}`);
3384
+ const target = targets.find((entry) => entry.name === step.node);
3385
+ if (target && step.eligible && step.commands.length)
3386
+ lines.push(`# ${targetCommand(target, step.commands)}`);
3387
+ else
3388
+ for (const next of step.next)
3389
+ lines.push(`# ${comment(next)}`);
3390
+ }
3391
+ lines.push("", "# Re-run personal fleet health --json for fresh whole-tailnet coverage.");
3392
+ return `${lines.join(`
3393
+ `)}
3394
+ `;
3395
+ }
3396
+ function comment(value) {
3397
+ return value.replace(/[\r\n\x00-\x1f\x7f]/g, " ");
3398
+ }
3399
+
3400
+ // packages/personal-cli/src/repair-guard.ts
3401
+ import { createHash as createHash3 } from "node:crypto";
3402
+ function repairPlanHash(steps, targets) {
3403
+ return createHash3("sha256").update(JSON.stringify({ steps, targets })).digest("hex");
3404
+ }
3405
+
3406
+ // packages/personal-cli/src/repair.ts
3407
+ function requestedRepairs(value) {
3408
+ if (value === undefined)
3409
+ return;
3410
+ const ids = value.split(",");
3411
+ if (!ids.length || ids.some((id) => !AUTOMATED_REPAIRS.includes(id)))
3412
+ throw new FleetUsageError("--repair expects workspace,workstation,providers. Native logins require operator approval; inspect fleet repairs.");
3413
+ return [...new Set(ids)];
3414
+ }
3415
+ function repairCatalog() {
3416
+ return {
3417
+ exitCode: 0,
3418
+ data: {
3419
+ version: 1,
3420
+ command: "repairs",
3421
+ ok: true,
3422
+ recipes: REPAIR_RECIPES,
3423
+ credentials: [
3424
+ "Use the existing password-manager / Bitwarden owner for human passwords and shared login items. Run bun run password-manager doctor and actions list on each machine; never synchronize its private state.",
3425
+ "Authenticate each computer independently with the intended identity. Never clone OAuth caches, browser cookies, keychains, SSH private keys or Vault sessions.",
3426
+ "Access shared permitted API keys through this machine’s existing Vault/secret-store session. Prefer vault run --config dev -- COMMAND for process-scoped injection.",
3427
+ "The workstation owner can render its supported provider configuration from Vault with 0600 permissions. Missing provider keys and live provider access still require checks.",
3428
+ "Service roles, shared resources, Vault policies and infrastructure remain OpenTofu-owned. A login does not grant every account/team/resource permission."
3429
+ ]
3430
+ }
3431
+ };
3432
+ }
3433
+ async function executeRepairCommand(command, options, settings, services) {
3434
+ return command === "repairs" ? repairCatalog() : executeRepair(options, settings, services);
3435
+ }
3436
+ async function executeRepair(options, settings, services) {
3437
+ const selected = requestedRepairs(options.repair);
3438
+ const before = await inspectRepair(options, settings, services);
3439
+ const steps = selectRepairs(repairPlan(before.reports, before.targets), before, selected);
3440
+ const planHash = repairPlanHash(steps, before.targets);
3441
+ if (options.planHash && options.planHash !== planHash)
3442
+ throw new FleetUsageError("Repair plan changed since review. Generate and review a fresh preview; nothing was applied.");
3443
+ const initial = makeReport("repair", before.reports);
3444
+ const healthOk = reportExitCode(initial, true) === 0;
3445
+ const script = options.script ? repairScript(steps, before.targets) : undefined;
3446
+ if (!options.apply)
3447
+ return {
3448
+ exitCode: 0,
3449
+ data: {
3450
+ ...initial,
3451
+ ok: true,
3452
+ applied: false,
3453
+ planHash,
3454
+ healthOk,
3455
+ coverage: before.coverage,
3456
+ steps,
3457
+ ...script ? { script } : {},
3458
+ authorize: "Preview only. Review effects, then repeat with --apply --yes. Only eligible automatic steps execute. Native logins and manual steps remain operator work."
3459
+ },
3460
+ text: script ?? renderSteps(steps)
3461
+ };
3462
+ return appliedOutcome(steps, before, { options, settings, services });
3463
+ }
3464
+ async function appliedOutcome(steps, before, context) {
3465
+ const { options, settings, services } = context;
3466
+ const executions = await applyRepairs(before.targets, steps, options, services);
3467
+ const after = await recheckRepair(before, options, settings, services);
3468
+ const checked = makeReport("repair", after.reports);
3469
+ const executionOk = executions.every((node) => node.checks.every((check) => check.status === "pass"));
3470
+ const verified = reportExitCode(checked, true) === 0;
3471
+ const ok = executionOk && verified;
3472
+ return {
3473
+ exitCode: ok ? 0 : 1,
3474
+ data: {
3475
+ ...checked,
3476
+ ok,
3477
+ applied: true,
3478
+ executionOk,
3479
+ healthOk: verified,
3480
+ coverage: after.coverage,
3481
+ steps,
3482
+ executions,
3483
+ remaining: repairPlan(after.reports, after.targets),
3484
+ completed: executions.flatMap((node) => node.checks).filter((check) => check.status === "pass").length
3485
+ },
3486
+ text: `Repair commands ${executionOk ? "completed" : "need attention"}; fresh health ${verified ? "verified" : "still has failing or unverified checks"}.
3487
+ ${renderSteps(repairPlan(after.reports, after.targets))}`
3488
+ };
3489
+ }
3490
+ function selectRepairs(steps, inspection, selected) {
3491
+ if (!selected)
3492
+ return steps;
3493
+ const filtered = steps.filter((step) => !step.automatic || selected.includes(step.id));
3494
+ for (const target of inspection.targets) {
3495
+ const report = inspection.reports.find((entry) => entry.node === target.name);
3496
+ if (!report?.snapshot)
3497
+ continue;
3498
+ for (const id of selected)
3499
+ if (!filtered.some((step) => step.node === target.name && step.id === id))
3500
+ filtered.push({
3501
+ ...recipe(id),
3502
+ node: target.name,
3503
+ eligible: true,
3504
+ checks: ["explicit:request"],
3505
+ next: [
3506
+ "Explicitly requested owner action; fresh diagnostics still follow."
3507
+ ]
3508
+ });
3509
+ }
3510
+ return filtered;
3511
+ }
3512
+ function renderSteps(steps) {
3513
+ return steps.map((step) => `${step.node} / ${step.id}: ${step.automatic && step.eligible ? "automatic after --apply --yes" : "operator action"}
3514
+ ${step.action}`).join(`
3515
+ `);
3516
+ }
3517
+ async function recheckRepair(before, options, settings, services) {
3518
+ try {
3519
+ return await inspectRepair(options, settings, services);
3520
+ } catch {
3521
+ return {
3522
+ reports: before.reports.map((report) => ({
3523
+ node: report.node,
3524
+ checks: [
3525
+ {
3526
+ id: "repair:recheck",
3527
+ status: "fail",
3528
+ summary: "Fresh diagnostics could not complete after repair. Earlier diagnostics are not being treated as fresh; inspect this computer and rerun health."
3529
+ }
3530
+ ]
3531
+ })),
3532
+ targets: [],
3533
+ coverage: {
3534
+ ...before.coverage,
3535
+ limitation: "Post-repair diagnostics failed. Execution results are retained; machine health remains unverified."
3536
+ }
3537
+ };
3538
+ }
3539
+ }
3540
+
3541
+ // packages/personal-cli/src/fleet.ts
3542
+ var SERVICES = { run: runCommand, source: probeSource };
3543
+ async function executeFleet(command, options, settings, services = SERVICES) {
3544
+ validateOptions(command, options);
3545
+ if (["repair", "repairs"].includes(command))
3546
+ return executeRepairCommand(command, options, settings, services);
3547
+ if (["discover", "health"].includes(command))
3548
+ return executeTailnet(command, options, settings, services);
3549
+ if (command === "schema")
3550
+ return result(schema_default);
3551
+ if (command === "init")
3552
+ return initialize(settings, options);
3553
+ if (command === "add")
3554
+ return register(settings, options);
3555
+ const roster = options.local ? null : await readRoster(settings.config);
3556
+ const nodes = resolveNodes(command, options, roster);
3557
+ if (["nodes", "validate", "onboard"].includes(command))
3558
+ return rosterResult(command, nodes, roster !== null);
3559
+ if (command === "sync")
3560
+ return synchronize(nodes, options, settings, services);
3561
+ return diagnose(nodes, options, { ...settings, missingRoster: !roster }, services);
3562
+ }
3563
+ function rosterResult(command, nodes, configured) {
3564
+ return result({
3565
+ version: 1,
3566
+ command,
3567
+ ok: true,
3568
+ ...command === "nodes" ? { configured } : {},
3569
+ nodes: command === "onboard" ? nodes.map((node) => onboarding(node, "personal fleet")) : nodes
3570
+ });
3571
+ }
3572
+ function resolveNodes(command, options, roster) {
3573
+ if (!roster && !options.local && !["doctor", "nodes"].includes(command))
3574
+ throw new FleetUsageError("No fleet roster. Use fleet add or the interactive Register action; use fleet doctor --local for this machine.");
3575
+ if (options.node?.length && !roster)
3576
+ throw new FleetUsageError("Cannot select nodes without a roster. Use fleet add first.");
3577
+ return roster ? selectNodes(roster, options.node ?? []) : [];
3578
+ }
3579
+ function result(data, text) {
3580
+ return { exitCode: 0, data, ...text ? { text } : {} };
3581
+ }
3582
+ async function register(settings, options) {
3583
+ const node = nodeFromOptions(options);
3584
+ const config = await addNode(settings.config, node, options.apply === true);
3585
+ return result({
3586
+ version: 1,
3587
+ command: "add",
3588
+ ok: true,
3589
+ applied: options.apply === true,
3590
+ path: settings.config,
3591
+ node,
3592
+ nodes: config.nodes,
3593
+ next: options.apply ? "Run fleet onboard, then fleet doctor --all --json." : "Review this preview, then repeat with --apply --yes."
3594
+ });
3595
+ }
3596
+ async function initialize(settings, options) {
3597
+ const config = {
3598
+ ...fleet_example_default,
3599
+ $schema: new URL("./schema.json", import.meta.url).href
3600
+ };
3601
+ if (options.apply) {
3602
+ await mkdir5(dirname5(settings.config), { recursive: true, mode: 448 });
3603
+ try {
3604
+ await writeFile3(settings.config, `${JSON.stringify(config, null, 2)}
3605
+ `, {
3606
+ flag: "wx",
3607
+ mode: 384
3608
+ });
3609
+ } catch {
3610
+ throw new FleetUsageError("Cannot create roster. Existing files are never overwritten; use --config PATH.");
3611
+ }
3612
+ }
3613
+ return result({
3614
+ version: 1,
3615
+ command: "init",
3616
+ ok: true,
3617
+ applied: options.apply === true,
3618
+ path: settings.config,
3619
+ config,
3620
+ next: "This is example data, not discovered machines. Edit placeholders before doctor, or use fleet add to register real nodes."
3621
+ });
3622
+ }
3623
+ async function diagnose(nodes, options, settings, services) {
3624
+ const source = nodes.length ? await services.source() : undefined;
3625
+ const reports = await doctor(nodes, {
3626
+ root: settings.workspace,
3627
+ local: options.local === true,
3628
+ required: capabilities(options.require),
3629
+ timeoutMs: timeout(options),
3630
+ ...source ? { source } : {}
3631
+ }, services.run);
3632
+ if (!options.local && settings.missingRoster)
3633
+ reports.push(missingRoster());
3634
+ const report = {
3635
+ ...makeReport("doctor", reports),
3636
+ coverage: {
3637
+ scope: options.local ? "local" : options.node?.length ? "selected" : "all-configured",
3638
+ controller: true,
3639
+ configuredNodesTested: nodes.map((node) => node.id),
3640
+ rosterAvailable: !settings.missingRoster,
3641
+ note: "Directed SSH checks from this controller only. T3 relay/peer and VS Code forwarding require operator tests."
3642
+ }
3643
+ };
3644
+ return {
3645
+ exitCode: reportExitCode(report, options.strict === true),
3646
+ data: report,
3647
+ text: renderReport(report)
3648
+ };
3649
+ }
3650
+ function missingRoster() {
3651
+ return {
3652
+ node: "fleet",
3653
+ checks: [
3654
+ {
3655
+ id: "fleet:roster",
3656
+ status: "fail",
3657
+ summary: "No developer fleet roster is configured. Only this controller could be checked; fleet-wide health is unknown.",
3658
+ remedy: {
3659
+ action: "Open personal and choose Register a developer computer, or run personal fleet add --help. Then run fleet doctor --all."
3660
+ }
3661
+ }
3662
+ ]
3663
+ };
3664
+ }
3665
+ async function synchronize(nodes, options, settings, services) {
3666
+ if (!options.apply)
3667
+ return result({
3668
+ version: 1,
3669
+ command: "sync",
3670
+ ok: true,
3671
+ applied: false,
3672
+ targets: options.local ? ["local"] : nodes.map((node) => node.id),
3673
+ steps: syncPlan(options.pull === true),
3674
+ authorize: "Review the plan; repeat with --apply --yes to authorize changes."
3675
+ });
3676
+ const reports = options.local ? [
3677
+ {
3678
+ node: "local",
3679
+ checks: await syncWorkspace(settings.workspace, options.pull === true, services.run)
3680
+ }
3681
+ ] : await syncRemotes(nodes, options, services);
3682
+ const report = makeReport("sync", reports);
3683
+ return {
3684
+ exitCode: reportExitCode(report, options.strict === true),
3685
+ data: report,
3686
+ text: renderReport(report)
3687
+ };
3688
+ }
3689
+ async function syncRemotes(nodes, options, services) {
3690
+ const source = await services.source();
3691
+ return mapBounded(nodes, (node) => inspectRemote(node, {
3692
+ source,
3693
+ action: "sync",
3694
+ timeoutMs: timeout(options),
3695
+ pull: options.pull === true
3696
+ }, services.run), 1);
3697
+ }
3698
+
3699
+ // packages/personal-cli/src/node-sync.ts
3700
+ import { join as join10 } from "node:path";
3701
+
3702
+ // packages/fleet/src/configure.ts
3703
+ import { join as join9 } from "node:path";
3704
+ import { mkdir as mkdir6, writeFile as writeFile4, rename as rename3 } from "node:fs/promises";
3705
+ import { randomUUID as randomUUID3 } from "node:crypto";
3706
+ function validConfiguration(value) {
3707
+ return record(value) && typeof value["revision"] === "string" && /^[a-f0-9]{40}$/.test(value["revision"]) && ["codex", "claude", "opencode", "gemini"].includes(String(value["tool"])) && ["direct", "proxy"].includes(String(value["profile"])) && typeof value["node"] === "string" && /^[a-z0-9][a-z0-9-]{0,62}$/.test(value["node"]);
3708
+ }
3709
+ async function configureWorkspace(root, request, run) {
3710
+ if (!validConfiguration(request))
3711
+ return failure("Invalid reviewed revision or configuration selection.");
3712
+ const directory = join9(root, "node_modules", ".fleet-revisions", request.revision);
3713
+ const head = await run(["git", "rev-parse", "--verify", `${request.revision}^{commit}`], { cwd: root });
3714
+ if (!head.ok || head.stdout.trim() !== request.revision)
3715
+ return failure("Reviewed commit is unavailable locally. Fetch it explicitly and retry.");
3716
+ const prepared = await prepareRevision(root, directory, request.revision, run);
3717
+ if (prepared.length)
3718
+ return prepared;
3719
+ const setup = await run([
3720
+ "mise",
3721
+ "exec",
3722
+ "--locked",
3723
+ "bun",
3724
+ "--",
3725
+ "bun",
3726
+ "scripts/dev/setup.ts",
3727
+ "--non-interactive"
3728
+ ], { cwd: directory, timeoutMs: 1200000 });
3729
+ if (!setup.ok)
3730
+ return failure("Existing setup owner failed. Partial tool/dependency installation is possible; no configuration receipt was advanced.");
3731
+ const applied = await applyOwner(directory, request, run);
3732
+ if (!applied.ok)
3733
+ return failure("personal workstation configuration failed. Inspect preview on this node; no rollout receipt was advanced.");
3734
+ const receipt = decodeReceipt(applied.stdout, request.revision);
3735
+ if (!receipt)
3736
+ return failure("personal workstation returned an invalid receipt; rollout remains unverified.");
3737
+ await recordConfiguration(root, request, receipt);
3738
+ return [
3739
+ {
3740
+ id: "configuration:apply",
3741
+ status: "pass",
3742
+ summary: `personal workstation applied revision ${request.revision}; configuration fingerprint ${receipt}. Authentication and T3 end-to-end remain separate gates.`
3743
+ }
3744
+ ];
3745
+ }
3746
+ function decodeReceipt(output, revision) {
3747
+ try {
3748
+ const data = JSON.parse(output);
3749
+ return record(data) && data["ok"] === true && data["revision"] === revision && typeof data["fingerprint"] === "string" && /^[a-f0-9]{64}$/.test(data["fingerprint"]) ? data["fingerprint"] : null;
3750
+ } catch {
3751
+ return null;
3752
+ }
3753
+ }
3754
+ function failure(summary) {
3755
+ return [{ id: "configuration:apply", status: "fail", summary }];
3756
+ }
3757
+ async function prepareRevision(root, directory, revision, run) {
3758
+ await mkdir6(join9(root, "node_modules", ".fleet-revisions"), {
3759
+ recursive: true
3760
+ });
3761
+ let deployed = await run(["git", "rev-parse", "HEAD"], { cwd: directory });
3762
+ if (!deployed.ok) {
3763
+ const created = await run(["git", "worktree", "add", "--detach", directory, revision], { cwd: root, timeoutMs: 30000 });
3764
+ if (!created.ok)
3765
+ return failure("Cannot create pinned deployment worktree. Existing data was preserved.");
3766
+ deployed = await run(["git", "rev-parse", "HEAD"], { cwd: directory });
3767
+ }
3768
+ const clean = await run(["git", "status", "--porcelain"], { cwd: directory });
3769
+ if (!deployed.ok || deployed.stdout.trim() !== revision || !clean.ok || clean.stdout.trim())
3770
+ return failure("Pinned deployment checkout differs or is dirty. Inspect it; fleet never resets it.");
3771
+ const top = await run(["git", "rev-parse", "--show-toplevel"], {
3772
+ cwd: directory
3773
+ });
3774
+ if (!top.ok || top.stdout.trim() !== directory)
3775
+ return failure("Deployment path is not its own Git worktree.");
3776
+ return [];
3777
+ }
3778
+ async function recordConfiguration(root, request, receipt) {
3779
+ const receipts = join9(root, "node_modules", ".fleet-configuration");
3780
+ await mkdir6(receipts, { recursive: true, mode: 448 });
3781
+ const path = join9(receipts, `${request.node}-${request.tool}-${request.profile}.json`);
3782
+ const temp = `${path}.${randomUUID3()}.tmp`;
3783
+ await writeFile4(temp, JSON.stringify({ version: 1, ...request, fingerprint: receipt }) + `
3784
+ `, { flag: "wx", mode: 384 });
3785
+ await rename3(temp, path);
3786
+ }
3787
+ async function applyOwner(directory, request, run) {
3788
+ return run([
3789
+ "mise",
3790
+ "exec",
3791
+ "--locked",
3792
+ "bun",
3793
+ "--",
3794
+ "bun",
3795
+ "packages/workstation/cli/internal.ts",
3796
+ "--json",
3797
+ "--yes",
3798
+ "--non-interactive",
3799
+ "agents",
3800
+ "apply",
3801
+ "--node",
3802
+ request.node,
3803
+ "--tool",
3804
+ request.tool,
3805
+ "--profile",
3806
+ request.profile
3807
+ ], { cwd: directory, timeoutMs: 120000 });
3808
+ }
3809
+
3810
+ // packages/personal-cli/src/node-t3.ts
3811
+ async function syncT3(options, settings) {
3812
+ try {
3813
+ return await t3Command("t3-apply", options, settings);
3814
+ } catch (error) {
3815
+ return {
3816
+ exitCode: 1,
3817
+ data: {
3818
+ version: 1,
3819
+ command: "fleet-node-sync",
3820
+ ok: false,
3821
+ applied: true,
3822
+ stage: "t3",
3823
+ error: error instanceof FleetUsageError ? error.message : "T3 reconciliation failed; inspect local configuration and ownership state.",
3824
+ progress: "Tools, launcher and profile steps completed. Node receipt was not advanced. Fix the T3 conflict and repeat fleet-node-sync."
3825
+ }
3826
+ };
3827
+ }
3828
+ }
3829
+
3830
+ // packages/personal-cli/src/node-sync.ts
3831
+ async function syncNode(options, settings, run = runCommand) {
3832
+ if (process.platform === "win32" && !options.dryRun)
3833
+ throw new FleetUsageError("Codex proxy node sync requires macOS/Linux/WSL. Use personal workstation commands for native Windows configuration.");
3834
+ const node = nodeId(options.id);
3835
+ const head = await run(["git", "rev-parse", "--verify", "HEAD"], {
3836
+ cwd: settings.workspace
3837
+ });
3838
+ const revision = options.revision ?? head.stdout.trim();
3839
+ if (!head.ok || !/^[a-f0-9]{40}$/.test(revision))
3840
+ throw new FleetUsageError("An existing personal checkout is required; set --workspace PATH.");
3841
+ const request = { node, revision, tool: "codex", profile: "proxy" };
3842
+ if (options.dryRun)
3843
+ return {
3844
+ exitCode: 0,
3845
+ data: {
3846
+ version: 1,
3847
+ command: "fleet-node-sync",
3848
+ ok: true,
3849
+ applied: false,
3850
+ ...request,
3851
+ steps: [
3852
+ "Reconcile locked tools",
3853
+ "Install personal and retire owned ws launcher",
3854
+ "Upsert Codex proxy profile",
3855
+ "Reconcile declared T3 instances and remove stale owned entries"
3856
+ ]
3857
+ }
3858
+ };
3859
+ const checks = await configureWorkspace(settings.workspace, request, run);
3860
+ if (checks.some((check) => check.status !== "pass"))
3861
+ return {
3862
+ exitCode: 1,
3863
+ data: { version: 1, command: "fleet-node-sync", ok: false, checks }
3864
+ };
3865
+ return finishSync(options, settings, { request, checks }, run);
3866
+ }
3867
+ async function finishSync(options, settings, input, run) {
3868
+ const { request, checks } = input;
3869
+ const { node, revision } = request;
3870
+ const workspace = join10(settings.workspace, "node_modules/.fleet-revisions", revision);
3871
+ const installed = await run([
3872
+ "bun",
3873
+ "packages/workstation/cli/internal.ts",
3874
+ "--json",
3875
+ "--yes",
3876
+ "--non-interactive",
3877
+ "install"
3878
+ ], { cwd: workspace, timeoutMs: 1200000 });
3879
+ if (!installed.ok)
3880
+ return {
3881
+ exitCode: 1,
3882
+ data: {
3883
+ version: 1,
3884
+ ok: false,
3885
+ checks,
3886
+ error: "Launcher/workstation convergence failed; profile changes may already have applied."
3887
+ }
3888
+ };
3889
+ const t3 = await syncT3({ ...options, id: node }, { ...settings, workspace });
3890
+ if (t3.exitCode !== 0)
3891
+ return t3;
3892
+ await recordDeployment(settings.workspace, node, revision);
3893
+ return {
3894
+ exitCode: 0,
3895
+ data: {
3896
+ version: 1,
3897
+ command: "fleet-node-sync",
3898
+ ok: true,
3899
+ applied: true,
3900
+ ...request,
3901
+ checks,
3902
+ t3: t3.data,
3903
+ readiness: {
3904
+ authentication: "unverified",
3905
+ inference: "unverified",
3906
+ pairing: "unverified"
3907
+ },
3908
+ next: "Use personal t3-status for configuration drift and personal fleet-status for machine health."
3909
+ }
3910
+ };
3911
+ }
3912
+
3913
+ // packages/personal-cli/src/flat-commands.ts
3914
+ function registerFlatCommands(program, defaults, complete) {
3915
+ program.command("fleet-node-sync").description("Reconcile this node’s locked tools and Codex proxy profile; applies by default").option("--id <name>", "Stable node name; defaults to this machine hostname").option("--revision <sha>", "Reviewed commit; defaults to checkout HEAD").option("--dry-run", "Preview without changing this node").option("--t3-home <path>", "T3 base directory").action(async (_options, command) => {
3916
+ const options = command.optsWithGlobals();
3917
+ const settings = await resolveSettings({ ...defaults, ...options });
3918
+ complete(await syncNode(options, settings));
3919
+ });
3920
+ for (const [name, target, description] of [
3921
+ [
3922
+ "fleet-status",
3923
+ "doctor",
3924
+ "Check this computer and all registered fleet nodes"
3925
+ ],
3926
+ ["fleet-discover", "discover", "Find computers visible through Tailscale"]
3927
+ ]) {
3928
+ program.command(name).description(description).action(async (_options, command) => {
3929
+ const options = command.optsWithGlobals();
3930
+ const settings = await resolveSettings({ ...defaults, ...options });
3931
+ complete(await executeFleet(target, {}, settings));
3932
+ });
3933
+ }
3934
+ }
3935
+
3936
+ // packages/personal-cli/src/agent-guide.ts
3937
+ var AGENT_GUIDE = {
3938
+ version: 1,
3939
+ name: "@crvouga/personal",
3940
+ invocation: "personal <command> --json --non-interactive",
3941
+ discovery: "personal commands --json",
3942
+ configuration: {
3943
+ workspace: "--workspace PATH, PERSONAL_WORKSPACE, closest ancestor mise.toml, then current directory",
3944
+ roster: "--config PATH, PERSONAL_FLEET_CONFIG, closest ancestor .fleet.local.json, then ~/.config/personal/fleet.json"
3945
+ },
3946
+ output: {
3947
+ stdout: "One versioned JSON document with --json. Prompts and progress never contaminate stdout.",
3948
+ exitCodes: {
3949
+ "0": "Success or preview",
3950
+ "1": "Failed check or operation; strict also rejects warnings/skips",
3951
+ "2": "Invalid usage or configuration",
3952
+ "130": "Cancelled"
3953
+ },
3954
+ errors: "{version:1,ok:false,code,error}; raw subprocess output and exception details are withheld",
3955
+ reports: "{version:1,command,ok,generatedAt,nodes:[{node,checks:[{id,status,summary,remedy?}]}],summary,coverage}",
3956
+ statuses: ["pass", "warn", "fail", "skip"],
3957
+ 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."
3958
+ },
3959
+ safety: [
3960
+ "No arguments opens a menu only on an interactive terminal. --json and --non-interactive never prompt.",
3961
+ "Diagnostics, onboarding instructions, schema, catalogs, scripts and sync/repair previews are read-only.",
3962
+ "fleet-node-sync authorizes local reconciliation by invocation; --dry-run previews. Legacy roster writes, sync and repair require --apply --yes.",
3963
+ "Do not copy credentials, forward SSH agents, bypass host keys, provision infrastructure or execute unreviewed remedies.",
3964
+ "A healthy controller does not prove another machine can connect to it. Run doctor from each intended controller.",
3965
+ "Repair executes only allowlisted owner actions, then reruns diagnostics. Native logins, host trust and resource permissions remain explicit operator steps. Pass the reviewed planHash as --plan-hash to reject a changed plan.",
3966
+ "Shared provider keys are fetched through each machine’s existing Vault authorization by workstation tooling. Never copy machine-local login caches.",
3967
+ "Use exact npm versions for automation. The application never self-updates."
3968
+ ],
3969
+ reconciliation: {
3970
+ plan: "personal t3-plan --json",
3971
+ apply: "personal t3-apply --plan-hash HASH",
3972
+ cleanup: "personal t3-cleanup --dry-run",
3973
+ import: "personal t3-import --instance ID",
3974
+ ownership: "Only declared or explicitly imported T3 instances. Pairing, credentials, threads and unrelated settings are preserved.",
3975
+ concurrency: "Exclusive personal lock with optimistic external T3 file updates; avoid concurrent Settings edits."
3976
+ },
3977
+ examples: [
3978
+ "personal fleet-node-sync",
3979
+ "personal fleet-status",
3980
+ "personal t3-status",
3981
+ "personal fleet doctor --local --workspace /path/to/personal --json",
3982
+ "personal fleet doctor --all --json --strict --timeout 120000",
3983
+ "personal fleet add --id dev-linux --ssh dev-linux --path /home/dev/workspace --require github,t3,toolchain --json",
3984
+ "personal fleet add --id dev-linux --ssh dev-linux --path /home/dev/workspace --apply --yes --json",
3985
+ "personal fleet doctor --node dev-linux --json",
3986
+ "personal fleet sync --node dev-linux --json",
3987
+ "personal fleet repairs --json",
3988
+ "personal fleet repair --json",
3989
+ "personal fleet repair --local --repair workspace --json",
3990
+ "personal fleet repair --local --repair workspace --plan-hash <reviewed-sha256> --apply --yes --json"
3991
+ ]
3992
+ };
3993
+
3994
+ // packages/personal-cli/src/program.ts
3995
+ var GLOBAL_OPTIONS = [
3996
+ {
3997
+ flag: "--json",
3998
+ description: "One versioned JSON document on stdout; never prompt"
3999
+ },
4000
+ { flag: "--non-interactive", description: "Never open interactive prompts" },
4001
+ {
4002
+ flag: "--workspace <path>",
4003
+ description: "Existing controller checkout; does not clone or change it"
4004
+ },
4005
+ { flag: "--config <path>", description: "Local, secret-free fleet roster" }
4006
+ ];
4007
+ function common(command) {
4008
+ 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 targets: doctor uses the roster; repair uses the visible tailnet").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("--repair <ids>", "Repair only: workspace,workstation,providers; comma separated").option("--plan-hash <sha256>", "Repair only: require the reviewed plan to match current diagnostics").option("--script", "Repair only: generate a reviewable POSIX script without executing it").option("--pull", "Sync only: explicitly request a clean, fast-forward pull").option("--apply", "Authorize the displayed roster/sync/repair mutation; requires --yes").option("--yes", "Confirm an explicitly requested --apply operation");
4009
+ }
4010
+ function collect(value, previous) {
4011
+ return [...previous, value];
4012
+ }
4013
+ function createProgram(version, output) {
4014
+ const program = new Command().name("personal").description("Developer command center · interactive for people, structured for agents").version(version).exitOverride().configureOutput({ writeOut: output, writeErr: () => {
4015
+ return;
4016
+ } }).showSuggestionAfterError(false).option("--interactive", "Open the interactive terminal application (TTY required)");
4017
+ for (const option of GLOBAL_OPTIONS)
4018
+ program.option(option.flag, option.description);
4019
+ return program;
4020
+ }
4021
+ function fleetCommands(program) {
4022
+ const fleet = program.command("fleet").description("Diagnose, explore and onboard developer computers");
4023
+ const definitions = commandCatalog().filter((entry) => entry.group === "Fleet" && entry.args[0] === "fleet" && entry.id !== "fleet doctor local");
4024
+ return definitions.map((entry) => {
4025
+ const command = common(fleet.command(entry.args[1]).description(entry.description));
4026
+ if (entry.args[1] === "add")
4027
+ 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");
4028
+ command.addHelpText("after", `
4029
+ Example: ${entry.examples[0]}
4030
+ Writes require both --apply and --yes. Diagnostics never log in or repair automatically.`);
4031
+ return command;
4032
+ });
4033
+ }
4034
+ function catalog(options, version) {
4035
+ const commands = commandCatalog(options.search, options.group);
4036
+ return {
4037
+ exitCode: 0,
4038
+ data: {
4039
+ version: 1,
4040
+ cliVersion: version,
4041
+ command: "commands",
4042
+ ok: true,
4043
+ commands,
4044
+ globalOptions: GLOBAL_OPTIONS,
4045
+ agentGuide: "personal agent --json",
4046
+ schema: "personal fleet schema --json"
4047
+ },
4048
+ text: [
4049
+ "Personal command catalog",
4050
+ ...commands.map((entry) => `
4051
+ ${entry.id}
4052
+ ${entry.description}`),
4053
+ `
4054
+ Run personal for the interactive application; add --json for agents.`
4055
+ ].join(`
4056
+ `)
4057
+ };
4058
+ }
4059
+ async function execute(args, defaults, version) {
4060
+ let outcome;
4061
+ let output = "";
4062
+ const program = createProgram(version, (text) => {
4063
+ output += text;
4064
+ });
4065
+ registerT3(program, defaults, (result) => {
4066
+ outcome = result;
4067
+ });
4068
+ registerFlatCommands(program, defaults, (result) => {
4069
+ outcome = result;
4070
+ });
4071
+ program.command("workstation [args...]").description("Local configuration, tools, agent profiles and launcher lifecycle").allowUnknownOption().action(async (args, command) => {
4072
+ const options = command.optsWithGlobals();
4073
+ const settings = await resolveSettings({
4074
+ ...defaults,
4075
+ ...settingsOverrides(options)
4076
+ });
4077
+ outcome = await workstationCommand(args, settings);
4078
+ });
4079
+ const commands = fleetCommands(program);
4080
+ for (const command of commands)
4081
+ command.action(async () => {
4082
+ const options = command.optsWithGlobals();
4083
+ const settings = await resolveSettings({
4084
+ ...defaults,
4085
+ ...settingsOverrides(options)
4086
+ });
4087
+ outcome = await executeFleet(command.name(), options, settings);
4088
+ });
4089
+ 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) => {
4090
+ outcome = catalog(command.optsWithGlobals(), version);
4091
+ });
4092
+ program.command("agent").description("Machine-readable usage and safety contract").action(() => {
4093
+ outcome = { exitCode: 0, data: AGENT_GUIDE };
4094
+ });
4095
+ program.action(() => {
4096
+ outcome = catalog(program.opts(), version);
4097
+ });
4098
+ try {
4099
+ await program.parseAsync(args, { from: "user" });
4100
+ return outcome ?? {
4101
+ exitCode: 0,
4102
+ data: { version: 1, help: output.trim() },
4103
+ text: output.trim()
4104
+ };
4105
+ } catch (error) {
4106
+ return commandError(error, output);
4107
+ }
4108
+ }
4109
+ function settingsOverrides(options) {
4110
+ return {
4111
+ ...options.workspace ? { workspace: options.workspace } : {},
4112
+ ...options.config ? { config: options.config } : {}
4113
+ };
4114
+ }
4115
+ function commandError(error, output) {
4116
+ if (error instanceof CommanderError && error.exitCode === 0)
4117
+ return {
4118
+ exitCode: 0,
4119
+ data: { version: 1, help: output.trim() },
4120
+ text: output.trim()
4121
+ };
4122
+ const usage = error instanceof CommanderError || error instanceof FleetUsageError;
4123
+ return {
4124
+ exitCode: usage ? 2 : 1,
4125
+ data: {
4126
+ version: 1,
4127
+ ok: false,
4128
+ code: usage ? "invalid_usage" : "operation_failed",
4129
+ 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."
4130
+ }
4131
+ };
4132
+ }
4133
+
4134
+ // packages/personal-cli/src/app.ts
4135
+ import { parseArgs } from "node:util";
4136
+ var VERSION = "0.1.1";
4137
+ function wantsInteractive(args, terminal, ci) {
4138
+ if (args.includes("--json") || args.includes("--non-interactive"))
4139
+ return false;
4140
+ return terminal && (args.includes("--interactive") || contextOnly(args) && !ci);
4141
+ }
4142
+ function startup(args) {
4143
+ return parseArgs({
4144
+ args,
4145
+ allowPositionals: true,
4146
+ strict: false,
4147
+ options: {
4148
+ workspace: { type: "string" },
4149
+ config: { type: "string" },
4150
+ interactive: { type: "boolean" },
4151
+ json: { type: "boolean" },
4152
+ "non-interactive": { type: "boolean" }
4153
+ }
4154
+ });
4155
+ }
4156
+ function contextOnly(args) {
4157
+ const parsed = startup(args);
4158
+ return parsed.positionals.length === 0 && Object.keys(parsed.values).every((key) => ["workspace", "config", "interactive"].includes(key));
4159
+ }
4160
+ async function main(args = process.argv.slice(2)) {
4161
+ const { values } = startup(args);
4162
+ const settings = await resolveSettings({
4163
+ ...typeof values.workspace === "string" ? { workspace: values.workspace } : {},
4164
+ ...typeof values.config === "string" ? { config: values.config } : {}
4165
+ });
4166
+ if (startup(args).positionals[0] === "workstation")
4167
+ return forwardWorkstation(args, settings);
4168
+ const terminal = interactiveTerminal();
4169
+ if (args.includes("--interactive") && !terminal) {
4170
+ 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." })}
4171
+ `);
4172
+ return 2;
4173
+ }
4174
+ if (wantsInteractive(args, terminal, Boolean(process.env["CI"]))) {
4175
+ await Promise.resolve().then(() => init_ui());
4176
+ await startInteractive({
4177
+ ...settings,
4178
+ version: VERSION,
4179
+ commands: COMMANDS,
4180
+ execute: (command) => execute(command, settings, VERSION)
4181
+ });
4182
+ return 0;
4183
+ }
4184
+ const outcome = await execute(args.filter((arg) => arg !== "--interactive"), settings, VERSION);
4185
+ const text = args.includes("--json") ? JSON.stringify(outcome.data, null, 2) : outcome.text ?? JSON.stringify(outcome.data, null, 2);
4186
+ process.stdout.write(`${text}
4187
+ `);
4188
+ return outcome.exitCode;
4189
+ }
4190
+ function interactiveTerminal() {
4191
+ return process.stdin.isTTY === true && process.stderr.isTTY === true;
4192
+ }
4193
+
4194
+ // packages/personal-cli/src/cli.ts
4195
+ try {
4196
+ process.exitCode = await main();
4197
+ } catch (error) {
4198
+ const cancelled = error instanceof Error && ["ExitPromptError", "AbortPromptError"].includes(error.name);
4199
+ process.stdout.write(`${JSON.stringify({
4200
+ version: 1,
4201
+ ok: false,
4202
+ code: cancelled ? "cancelled" : "operation_failed",
4203
+ error: cancelled ? "Cancelled; no further actions will run." : "Command failed. Inspect the local setup; raw errors are withheld to protect credentials."
4204
+ })}
4205
+ `);
4206
+ process.exitCode = cancelled ? 130 : 1;
4207
+ }