@crvouga/personal 0.1.0 → 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/README.md +136 -3
- package/cli.mjs +2174 -294
- package/commands.json +264 -0
- package/package.json +3 -3
- package/probe.mjs +561 -30
- package/releasing.md +6 -5
- package/schema.json +11 -1
package/probe.mjs
CHANGED
|
@@ -1,3 +1,8 @@
|
|
|
1
|
+
// packages/fleet/src/configure.ts
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { mkdir, writeFile, rename } from "node:fs/promises";
|
|
4
|
+
import { randomUUID } from "node:crypto";
|
|
5
|
+
|
|
1
6
|
// packages/fleet/src/errors.ts
|
|
2
7
|
class FleetUsageError extends Error {
|
|
3
8
|
}
|
|
@@ -8,7 +13,11 @@ var CAPABILITIES = [
|
|
|
8
13
|
"vault",
|
|
9
14
|
"t3",
|
|
10
15
|
"vscode",
|
|
11
|
-
"toolchain"
|
|
16
|
+
"toolchain",
|
|
17
|
+
"codex",
|
|
18
|
+
"claude",
|
|
19
|
+
"services",
|
|
20
|
+
"workstation"
|
|
12
21
|
];
|
|
13
22
|
function record(value) {
|
|
14
23
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
@@ -75,14 +84,119 @@ function parseConfig(value) {
|
|
|
75
84
|
return { version: 1, nodes };
|
|
76
85
|
}
|
|
77
86
|
|
|
87
|
+
// packages/fleet/src/configure.ts
|
|
88
|
+
function validConfiguration(value) {
|
|
89
|
+
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"]);
|
|
90
|
+
}
|
|
91
|
+
async function configureWorkspace(root, request, run) {
|
|
92
|
+
if (!validConfiguration(request))
|
|
93
|
+
return failure("Invalid reviewed revision or configuration selection.");
|
|
94
|
+
const directory = join(root, "node_modules", ".fleet-revisions", request.revision);
|
|
95
|
+
const head = await run(["git", "rev-parse", "--verify", `${request.revision}^{commit}`], { cwd: root });
|
|
96
|
+
if (!head.ok || head.stdout.trim() !== request.revision)
|
|
97
|
+
return failure("Reviewed commit is unavailable locally. Fetch it explicitly and retry.");
|
|
98
|
+
const prepared = await prepareRevision(root, directory, request.revision, run);
|
|
99
|
+
if (prepared.length)
|
|
100
|
+
return prepared;
|
|
101
|
+
const setup = await run([
|
|
102
|
+
"mise",
|
|
103
|
+
"exec",
|
|
104
|
+
"--locked",
|
|
105
|
+
"bun",
|
|
106
|
+
"--",
|
|
107
|
+
"bun",
|
|
108
|
+
"scripts/dev/setup.ts",
|
|
109
|
+
"--non-interactive"
|
|
110
|
+
], { cwd: directory, timeoutMs: 1200000 });
|
|
111
|
+
if (!setup.ok)
|
|
112
|
+
return failure("Existing setup owner failed. Partial tool/dependency installation is possible; no configuration receipt was advanced.");
|
|
113
|
+
const applied = await applyOwner(directory, request, run);
|
|
114
|
+
if (!applied.ok)
|
|
115
|
+
return failure("personal workstation configuration failed. Inspect preview on this node; no rollout receipt was advanced.");
|
|
116
|
+
const receipt = decodeReceipt(applied.stdout, request.revision);
|
|
117
|
+
if (!receipt)
|
|
118
|
+
return failure("personal workstation returned an invalid receipt; rollout remains unverified.");
|
|
119
|
+
await recordConfiguration(root, request, receipt);
|
|
120
|
+
return [
|
|
121
|
+
{
|
|
122
|
+
id: "configuration:apply",
|
|
123
|
+
status: "pass",
|
|
124
|
+
summary: `personal workstation applied revision ${request.revision}; configuration fingerprint ${receipt}. Authentication and T3 end-to-end remain separate gates.`
|
|
125
|
+
}
|
|
126
|
+
];
|
|
127
|
+
}
|
|
128
|
+
function decodeReceipt(output, revision) {
|
|
129
|
+
try {
|
|
130
|
+
const data = JSON.parse(output);
|
|
131
|
+
return record(data) && data["ok"] === true && data["revision"] === revision && typeof data["fingerprint"] === "string" && /^[a-f0-9]{64}$/.test(data["fingerprint"]) ? data["fingerprint"] : null;
|
|
132
|
+
} catch {
|
|
133
|
+
return null;
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
function failure(summary) {
|
|
137
|
+
return [{ id: "configuration:apply", status: "fail", summary }];
|
|
138
|
+
}
|
|
139
|
+
async function prepareRevision(root, directory, revision, run) {
|
|
140
|
+
await mkdir(join(root, "node_modules", ".fleet-revisions"), {
|
|
141
|
+
recursive: true
|
|
142
|
+
});
|
|
143
|
+
let deployed = await run(["git", "rev-parse", "HEAD"], { cwd: directory });
|
|
144
|
+
if (!deployed.ok) {
|
|
145
|
+
const created = await run(["git", "worktree", "add", "--detach", directory, revision], { cwd: root, timeoutMs: 30000 });
|
|
146
|
+
if (!created.ok)
|
|
147
|
+
return failure("Cannot create pinned deployment worktree. Existing data was preserved.");
|
|
148
|
+
deployed = await run(["git", "rev-parse", "HEAD"], { cwd: directory });
|
|
149
|
+
}
|
|
150
|
+
const clean = await run(["git", "status", "--porcelain"], { cwd: directory });
|
|
151
|
+
if (!deployed.ok || deployed.stdout.trim() !== revision || !clean.ok || clean.stdout.trim())
|
|
152
|
+
return failure("Pinned deployment checkout differs or is dirty. Inspect it; fleet never resets it.");
|
|
153
|
+
const top = await run(["git", "rev-parse", "--show-toplevel"], {
|
|
154
|
+
cwd: directory
|
|
155
|
+
});
|
|
156
|
+
if (!top.ok || top.stdout.trim() !== directory)
|
|
157
|
+
return failure("Deployment path is not its own Git worktree.");
|
|
158
|
+
return [];
|
|
159
|
+
}
|
|
160
|
+
async function recordConfiguration(root, request, receipt) {
|
|
161
|
+
const receipts = join(root, "node_modules", ".fleet-configuration");
|
|
162
|
+
await mkdir(receipts, { recursive: true, mode: 448 });
|
|
163
|
+
const path = join(receipts, `${request.node}-${request.tool}-${request.profile}.json`);
|
|
164
|
+
const temp = `${path}.${randomUUID()}.tmp`;
|
|
165
|
+
await writeFile(temp, JSON.stringify({ version: 1, ...request, fingerprint: receipt }) + `
|
|
166
|
+
`, { flag: "wx", mode: 384 });
|
|
167
|
+
await rename(temp, path);
|
|
168
|
+
}
|
|
169
|
+
async function applyOwner(directory, request, run) {
|
|
170
|
+
return run([
|
|
171
|
+
"mise",
|
|
172
|
+
"exec",
|
|
173
|
+
"--locked",
|
|
174
|
+
"bun",
|
|
175
|
+
"--",
|
|
176
|
+
"bun",
|
|
177
|
+
"packages/workstation/cli/internal.ts",
|
|
178
|
+
"--json",
|
|
179
|
+
"--yes",
|
|
180
|
+
"--non-interactive",
|
|
181
|
+
"agents",
|
|
182
|
+
"apply",
|
|
183
|
+
"--node",
|
|
184
|
+
request.node,
|
|
185
|
+
"--tool",
|
|
186
|
+
request.tool,
|
|
187
|
+
"--profile",
|
|
188
|
+
request.profile
|
|
189
|
+
], { cwd: directory, timeoutMs: 120000 });
|
|
190
|
+
}
|
|
191
|
+
|
|
78
192
|
// packages/fleet/src/fingerprint.ts
|
|
79
|
-
import { readFile as readFile2, mkdir, writeFile } from "node:fs/promises";
|
|
80
|
-
import { join as
|
|
193
|
+
import { readFile as readFile2, mkdir as mkdir2, writeFile as writeFile2 } from "node:fs/promises";
|
|
194
|
+
import { join as join3 } from "node:path";
|
|
81
195
|
|
|
82
196
|
// packages/fleet/src/input-hashes.ts
|
|
83
197
|
import { createHash } from "node:crypto";
|
|
84
198
|
import { readFile, readdir } from "node:fs/promises";
|
|
85
|
-
import { join } from "node:path";
|
|
199
|
+
import { join as join2 } from "node:path";
|
|
86
200
|
var INPUT_FILES = [
|
|
87
201
|
"package.json",
|
|
88
202
|
"bun.lock",
|
|
@@ -103,12 +217,12 @@ async function fileHash(path) {
|
|
|
103
217
|
}
|
|
104
218
|
async function manifestHash(root) {
|
|
105
219
|
try {
|
|
106
|
-
const entries = await readdir(
|
|
220
|
+
const entries = await readdir(join2(root, "packages"), {
|
|
107
221
|
withFileTypes: true
|
|
108
222
|
});
|
|
109
223
|
const hashes = [];
|
|
110
224
|
for (const entry of entries.filter((item) => item.isDirectory()).sort((left, right) => left.name.localeCompare(right.name, "en"))) {
|
|
111
|
-
hashes.push(`${entry.name}:${await fileHash(
|
|
225
|
+
hashes.push(`${entry.name}:${await fileHash(join2(root, "packages", entry.name, "package.json"))}`);
|
|
112
226
|
}
|
|
113
227
|
return digest(hashes.join(`
|
|
114
228
|
`));
|
|
@@ -118,19 +232,19 @@ async function manifestHash(root) {
|
|
|
118
232
|
}
|
|
119
233
|
async function sidecarHash(root) {
|
|
120
234
|
try {
|
|
121
|
-
const files = await readdir(
|
|
235
|
+
const files = await readdir(join2(root, ".mise/locks"), {
|
|
122
236
|
recursive: true,
|
|
123
237
|
withFileTypes: true
|
|
124
238
|
});
|
|
125
239
|
if (files.length > 4096 || files.some((entry) => entry.isSymbolicLink()))
|
|
126
240
|
return null;
|
|
127
241
|
const hashes = [];
|
|
128
|
-
const paths = files.filter((entry) => entry.isFile()).map((entry) =>
|
|
242
|
+
const paths = files.filter((entry) => entry.isFile()).map((entry) => join2(entry.parentPath, entry.name)).sort();
|
|
129
243
|
for (const path of paths) {
|
|
130
244
|
const hash = await fileHash(path);
|
|
131
245
|
if (hash === null)
|
|
132
246
|
return null;
|
|
133
|
-
const relative = path.slice(
|
|
247
|
+
const relative = path.slice(join2(root, ".mise/locks").length).replaceAll("\\", "/");
|
|
134
248
|
hashes.push(`${relative}:${hash}`);
|
|
135
249
|
}
|
|
136
250
|
return digest(hashes.join(`
|
|
@@ -148,7 +262,7 @@ var RECEIPT = "node_modules/.fleet-sync.json";
|
|
|
148
262
|
async function fingerprint(root, run) {
|
|
149
263
|
const files = {};
|
|
150
264
|
for (const name of INPUT_FILES)
|
|
151
|
-
files[name] = await fileHash(
|
|
265
|
+
files[name] = await fileHash(join3(root, name));
|
|
152
266
|
files["workspace-manifests"] = await manifestHash(root);
|
|
153
267
|
files["mise-sidecars"] = await sidecarHash(root);
|
|
154
268
|
const head = await run(["git", "rev-parse", "HEAD"], { cwd: root });
|
|
@@ -162,7 +276,7 @@ async function dependencyCheck(root, current) {
|
|
|
162
276
|
action: "Review fleet sync, then explicitly apply a frozen toolchain/dependency sync."
|
|
163
277
|
};
|
|
164
278
|
try {
|
|
165
|
-
const receipt = JSON.parse(await readFile2(
|
|
279
|
+
const receipt = JSON.parse(await readFile2(join3(root, RECEIPT), "utf8"));
|
|
166
280
|
const matches = record(receipt) && receipt["version"] === 1 && JSON.stringify(receipt["files"]) === JSON.stringify(current.files);
|
|
167
281
|
return {
|
|
168
282
|
id: "dependencies",
|
|
@@ -180,15 +294,204 @@ async function dependencyCheck(root, current) {
|
|
|
180
294
|
}
|
|
181
295
|
}
|
|
182
296
|
async function writeReceipt(root, current) {
|
|
183
|
-
await
|
|
184
|
-
await
|
|
297
|
+
await mkdir2(join3(root, "node_modules"), { recursive: true });
|
|
298
|
+
await writeFile2(join3(root, RECEIPT), JSON.stringify({ version: 1, files: current.files }), { mode: 384 });
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
// packages/fleet/src/service-auth.ts
|
|
302
|
+
function json(result) {
|
|
303
|
+
try {
|
|
304
|
+
const data = JSON.parse(result.stdout);
|
|
305
|
+
return record(data) ? data : null;
|
|
306
|
+
} catch {
|
|
307
|
+
return null;
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
async function codingAuth(id, run) {
|
|
311
|
+
return [
|
|
312
|
+
await serviceCheck(id === "codex" ? {
|
|
313
|
+
id,
|
|
314
|
+
command: ["codex", "login", "status"],
|
|
315
|
+
action: "Run codex login on this computer as its operator.",
|
|
316
|
+
valid: (result) => /^Logged in using (ChatGPT|an API key|API key)\b/im.test(`${result.stdout}
|
|
317
|
+
${result.stderr}`)
|
|
318
|
+
} : {
|
|
319
|
+
id,
|
|
320
|
+
command: ["claude", "auth", "status", "--json"],
|
|
321
|
+
action: "Run claude auth login on this computer as its operator.",
|
|
322
|
+
valid: (result) => json(result)?.["loggedIn"] === true
|
|
323
|
+
}, run)
|
|
324
|
+
];
|
|
325
|
+
}
|
|
326
|
+
async function serviceAuth(run) {
|
|
327
|
+
const services = [
|
|
328
|
+
{
|
|
329
|
+
id: "railway",
|
|
330
|
+
command: ["railway", "whoami", "--json"],
|
|
331
|
+
action: "Run railway login on this computer.",
|
|
332
|
+
valid: (result) => typeof json(result)?.["id"] === "string"
|
|
333
|
+
},
|
|
334
|
+
{
|
|
335
|
+
id: "vercel",
|
|
336
|
+
command: ["vercel", "whoami"],
|
|
337
|
+
action: "Run vercel login on this computer.",
|
|
338
|
+
valid: (result) => /^[a-zA-Z0-9][a-zA-Z0-9_-]{0,100}$/.test(result.stdout.trim())
|
|
339
|
+
},
|
|
340
|
+
{
|
|
341
|
+
id: "cloudflare",
|
|
342
|
+
command: ["wrangler", "whoami", "--json"],
|
|
343
|
+
action: "Run wrangler login on this computer.",
|
|
344
|
+
valid: (result) => json(result)?.["loggedIn"] === true
|
|
345
|
+
},
|
|
346
|
+
{
|
|
347
|
+
id: "aws",
|
|
348
|
+
command: [
|
|
349
|
+
"aws",
|
|
350
|
+
"sts",
|
|
351
|
+
"get-caller-identity",
|
|
352
|
+
"--output",
|
|
353
|
+
"json",
|
|
354
|
+
"--no-cli-pager"
|
|
355
|
+
],
|
|
356
|
+
action: "Select and authenticate the intended AWS profile on this computer.",
|
|
357
|
+
valid: (result) => typeof json(result)?.["Account"] === "string" && typeof json(result)?.["Arn"] === "string"
|
|
358
|
+
},
|
|
359
|
+
{
|
|
360
|
+
id: "npm",
|
|
361
|
+
command: ["npm", "whoami", "--registry=https://registry.npmjs.org/"],
|
|
362
|
+
action: "Run npm login only if this computer should publish packages.",
|
|
363
|
+
valid: (result) => /^[a-z0-9][a-z0-9._-]{0,213}$/.test(result.stdout.trim())
|
|
364
|
+
}
|
|
365
|
+
];
|
|
366
|
+
const checks = [];
|
|
367
|
+
for (const service of services)
|
|
368
|
+
checks.push(await serviceCheck(service, run));
|
|
369
|
+
for (const id of ["gemini", "copilot", "opencode"])
|
|
370
|
+
checks.push({
|
|
371
|
+
id: `auth:${id}`,
|
|
372
|
+
status: "skip",
|
|
373
|
+
summary: `${id} provider authorization has no verified noninteractive status adapter in this CLI. Installed binaries or saved keys are not proof of access.`,
|
|
374
|
+
remedy: {
|
|
375
|
+
action: `Verify ${id} provider access locally using its supported account diagnostics. No prompt or billable request was made.`
|
|
376
|
+
}
|
|
377
|
+
});
|
|
378
|
+
return checks;
|
|
379
|
+
}
|
|
380
|
+
async function serviceCheck(service, run) {
|
|
381
|
+
const result = await run(service.command, { timeoutMs: 15000 });
|
|
382
|
+
const ok = result.ok && service.valid(result);
|
|
383
|
+
return {
|
|
384
|
+
id: `auth:${service.id}`,
|
|
385
|
+
status: ok ? "pass" : "fail",
|
|
386
|
+
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.`,
|
|
387
|
+
...!ok ? { remedy: { action: service.action } } : {}
|
|
388
|
+
};
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
// packages/fleet/src/workstation-checks.ts
|
|
392
|
+
async function workstationChecks(root, run) {
|
|
393
|
+
const result = await run(["personal", "workstation", "doctor", "--json", "--non-interactive"], {
|
|
394
|
+
cwd: root,
|
|
395
|
+
timeoutMs: 20000
|
|
396
|
+
});
|
|
397
|
+
const fallback = {
|
|
398
|
+
id: "workstation:configuration",
|
|
399
|
+
status: "fail",
|
|
400
|
+
summary: "Managed workstation configuration could not be verified.",
|
|
401
|
+
remedy: {
|
|
402
|
+
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."
|
|
403
|
+
}
|
|
404
|
+
};
|
|
405
|
+
try {
|
|
406
|
+
const data = JSON.parse(result.stdout);
|
|
407
|
+
if (!record(data) || !Array.isArray(data["checks"]) || !data["checks"].length || data["checks"].length > 100)
|
|
408
|
+
return [fallback];
|
|
409
|
+
const checks = data["checks"].map(configurationCheck);
|
|
410
|
+
if (checks.some((check) => check === null))
|
|
411
|
+
return [fallback];
|
|
412
|
+
return [
|
|
413
|
+
...!result.ok || data["ok"] !== true ? [fallback] : [],
|
|
414
|
+
...checks
|
|
415
|
+
];
|
|
416
|
+
} catch {
|
|
417
|
+
return [fallback];
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
function configurationCheck(value) {
|
|
421
|
+
if (!record(value) || typeof value["id"] !== "string" || !/^[a-zA-Z0-9:._-]{1,75}$/.test(value["id"]) || !["pass", "warn", "fail"].includes(String(value["severity"])))
|
|
422
|
+
return null;
|
|
423
|
+
const ok = value["severity"] === "pass";
|
|
424
|
+
return {
|
|
425
|
+
id: `workstation:${value["id"].toLowerCase()}`,
|
|
426
|
+
status: value["severity"],
|
|
427
|
+
summary: ok ? "Managed configuration check succeeded (configuration presence; provider authorization is checked separately)." : "Managed configuration is missing, conflicting or unverified.",
|
|
428
|
+
...!ok ? {
|
|
429
|
+
remedy: {
|
|
430
|
+
action: "Inspect personal workstation doctor --json on this computer. Use explicit personal workstation sync only after reviewing the configuration changes."
|
|
431
|
+
}
|
|
432
|
+
} : {}
|
|
433
|
+
};
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
// packages/fleet/src/toolchain-checks.ts
|
|
437
|
+
import { join as join4 } from "node:path";
|
|
438
|
+
async function toolchainChecks(root, run) {
|
|
439
|
+
const result = await run(["bun", join4(root, "scripts/dev/doctor.ts"), "--json", "--docker"], { cwd: root, timeoutMs: 90000 });
|
|
440
|
+
const fallback = {
|
|
441
|
+
id: "auth:toolchain",
|
|
442
|
+
status: "fail",
|
|
443
|
+
summary: "Pinned tool/dependency and Docker checks could not be verified.",
|
|
444
|
+
remedy: {
|
|
445
|
+
action: "Run the checkout scripts/dev/doctor.ts --json --docker on this computer. Reconcile versions only through the shared mise setup."
|
|
446
|
+
}
|
|
447
|
+
};
|
|
448
|
+
try {
|
|
449
|
+
const data = JSON.parse(result.stdout);
|
|
450
|
+
if (!record(data) || typeof data["ok"] !== "boolean" || !Array.isArray(data["checks"]) || !data["checks"].length || data["checks"].length > 150)
|
|
451
|
+
return [fallback];
|
|
452
|
+
const checks = data["checks"].map(toolCheck);
|
|
453
|
+
if (checks.some((check) => check === null))
|
|
454
|
+
return [fallback];
|
|
455
|
+
return [
|
|
456
|
+
...!result.ok || !data["ok"] ? [fallback] : [],
|
|
457
|
+
...checks
|
|
458
|
+
];
|
|
459
|
+
} catch {
|
|
460
|
+
return [fallback];
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
function toolCheck(value) {
|
|
464
|
+
if (!record(value) || typeof value["id"] !== "string" || !/^[a-zA-Z0-9@:/._-]{1,75}$/.test(value["id"]) || !["pass", "fail", "warn", "skip"].includes(String(value["status"])))
|
|
465
|
+
return null;
|
|
466
|
+
const ok = value["status"] === "pass";
|
|
467
|
+
const version = typeof value["detail"] === "string" && /^v?\d{1,6}(?:\.\d{1,6}){1,3}$/.test(value["detail"]) ? ` ${value["detail"]}` : "";
|
|
468
|
+
return {
|
|
469
|
+
id: `tool:${value["id"].toLowerCase().replace(/[^a-z0-9:._-]/g, "_")}`,
|
|
470
|
+
status: value["status"],
|
|
471
|
+
summary: ok ? `Expected tool version or runtime check succeeded${version}.` : "Expected tool version, runtime or authentication check did not pass.",
|
|
472
|
+
...!ok ? {
|
|
473
|
+
remedy: {
|
|
474
|
+
action: "Review scripts/dev/doctor.ts --json --docker locally. Use shared mise/workstation setup to reconcile this computer."
|
|
475
|
+
}
|
|
476
|
+
} : {}
|
|
477
|
+
};
|
|
185
478
|
}
|
|
186
479
|
|
|
187
480
|
// packages/fleet/src/auth-checks.ts
|
|
188
|
-
import { join as join3 } from "node:path";
|
|
189
481
|
async function authCheck(capability, run, root) {
|
|
190
482
|
if (capability === "t3")
|
|
191
483
|
return t3Checks(run);
|
|
484
|
+
if (capability === "codex" || capability === "claude")
|
|
485
|
+
return codingAuth(capability, run);
|
|
486
|
+
if (capability === "services")
|
|
487
|
+
return serviceAuth(run);
|
|
488
|
+
if (capability === "workstation")
|
|
489
|
+
return workstationChecks(root, run);
|
|
490
|
+
if (capability === "toolchain")
|
|
491
|
+
return toolchainChecks(root, run);
|
|
492
|
+
return basicAuth(capability, run, root);
|
|
493
|
+
}
|
|
494
|
+
async function basicAuth(capability, run, root) {
|
|
192
495
|
const spec = {
|
|
193
496
|
github: {
|
|
194
497
|
command: ["gh", "auth", "status", "--active"],
|
|
@@ -201,15 +504,11 @@ async function authCheck(capability, run, root) {
|
|
|
201
504
|
vscode: {
|
|
202
505
|
command: ["code", "--list-extensions"],
|
|
203
506
|
action: "Install the VS Code CLI on PATH and ms-vscode-remote.remote-ssh in the local editor profile."
|
|
204
|
-
},
|
|
205
|
-
toolchain: {
|
|
206
|
-
command: ["bun", join3(root, "scripts/dev/doctor.ts"), "--json"],
|
|
207
|
-
action: "Run ws tools check --json; use the shared mise setup to reconcile tools."
|
|
208
507
|
}
|
|
209
508
|
}[capability];
|
|
210
509
|
const result = await run(spec.command, {
|
|
211
510
|
cwd: root,
|
|
212
|
-
timeoutMs:
|
|
511
|
+
timeoutMs: 15000
|
|
213
512
|
});
|
|
214
513
|
const extensions = result.stdout.toLowerCase().split(/\s+/);
|
|
215
514
|
const ok = result.ok && (capability !== "vscode" || extensions.includes("ms-vscode-remote.remote-ssh"));
|
|
@@ -288,13 +587,7 @@ async function probe(root, required, run) {
|
|
|
288
587
|
summary: dirty.ok && !dirty.stdout.trim() ? "Working tree is clean." : "Working tree is dirty or unreadable; sync --pull will refuse."
|
|
289
588
|
});
|
|
290
589
|
checks.push(await dependencyCheck(root, current));
|
|
291
|
-
for (const capability of
|
|
292
|
-
"github",
|
|
293
|
-
"vault",
|
|
294
|
-
"t3",
|
|
295
|
-
"vscode",
|
|
296
|
-
"toolchain"
|
|
297
|
-
]) {
|
|
590
|
+
for (const capability of CAPABILITIES) {
|
|
298
591
|
if (required.includes(capability))
|
|
299
592
|
checks.push(...await authCheck(capability, run, root));
|
|
300
593
|
else
|
|
@@ -390,7 +683,7 @@ function runCommand(command, options = {}) {
|
|
|
390
683
|
|
|
391
684
|
// packages/fleet/src/sync.ts
|
|
392
685
|
import { unlink } from "node:fs/promises";
|
|
393
|
-
import { join as
|
|
686
|
+
import { join as join5 } from "node:path";
|
|
394
687
|
async function syncWorkspace(root, pull, run) {
|
|
395
688
|
const before = await fingerprint(root, run);
|
|
396
689
|
if (!completeFingerprint(before)) {
|
|
@@ -404,7 +697,7 @@ async function syncWorkspace(root, pull, run) {
|
|
|
404
697
|
if (!dirty.ok || dirty.stdout.trim() || !branch.ok)
|
|
405
698
|
return failed("Refusing pull: dirty, detached, or unreadable checkout. Reconcile manually.");
|
|
406
699
|
}
|
|
407
|
-
await unlink(
|
|
700
|
+
await unlink(join5(root, "node_modules/.fleet-sync.json")).catch(() => {
|
|
408
701
|
return;
|
|
409
702
|
});
|
|
410
703
|
if (pull) {
|
|
@@ -456,13 +749,242 @@ function completeFingerprint(value) {
|
|
|
456
749
|
return value.head !== null && Object.values(value.files).every(Boolean);
|
|
457
750
|
}
|
|
458
751
|
|
|
752
|
+
// packages/fleet/src/repair-catalog.ts
|
|
753
|
+
var login = (id, title, commands, action) => ({
|
|
754
|
+
id: `login:${id}`,
|
|
755
|
+
title,
|
|
756
|
+
owner: `${id} native CLI`,
|
|
757
|
+
automatic: false,
|
|
758
|
+
action,
|
|
759
|
+
commands
|
|
760
|
+
});
|
|
761
|
+
var REPAIR_RECIPES = [
|
|
762
|
+
{
|
|
763
|
+
id: "workspace",
|
|
764
|
+
title: "Install locked tools and frozen dependencies",
|
|
765
|
+
owner: "mise / scripts/dev/setup.ts",
|
|
766
|
+
automatic: true,
|
|
767
|
+
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.",
|
|
768
|
+
commands: [
|
|
769
|
+
[
|
|
770
|
+
"mise",
|
|
771
|
+
"exec",
|
|
772
|
+
"--locked",
|
|
773
|
+
"bun",
|
|
774
|
+
"--",
|
|
775
|
+
"bun",
|
|
776
|
+
"scripts/dev/setup.ts",
|
|
777
|
+
"--non-interactive"
|
|
778
|
+
]
|
|
779
|
+
]
|
|
780
|
+
},
|
|
781
|
+
{
|
|
782
|
+
id: "workstation",
|
|
783
|
+
title: "Install launcher and converge managed workstation configuration",
|
|
784
|
+
owner: "workstation",
|
|
785
|
+
automatic: true,
|
|
786
|
+
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.",
|
|
787
|
+
commands: [["bun", "run", "personal:install", "--yes"]]
|
|
788
|
+
},
|
|
789
|
+
{
|
|
790
|
+
id: "providers",
|
|
791
|
+
title: "Refresh shared provider configuration from Vault",
|
|
792
|
+
owner: "workstation / secret-store",
|
|
793
|
+
automatic: true,
|
|
794
|
+
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.",
|
|
795
|
+
commands: [
|
|
796
|
+
[
|
|
797
|
+
"bun",
|
|
798
|
+
"packages/workstation/cli/internal.ts",
|
|
799
|
+
"opencode",
|
|
800
|
+
"sync",
|
|
801
|
+
"--strict",
|
|
802
|
+
"--json",
|
|
803
|
+
"--non-interactive",
|
|
804
|
+
"--yes"
|
|
805
|
+
]
|
|
806
|
+
]
|
|
807
|
+
},
|
|
808
|
+
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."),
|
|
809
|
+
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."),
|
|
810
|
+
login("github", "Sign in to GitHub and configure Git", [
|
|
811
|
+
[
|
|
812
|
+
"gh",
|
|
813
|
+
"auth",
|
|
814
|
+
"login",
|
|
815
|
+
"--hostname",
|
|
816
|
+
"github.com",
|
|
817
|
+
"--web",
|
|
818
|
+
"--git-protocol",
|
|
819
|
+
"https"
|
|
820
|
+
],
|
|
821
|
+
["gh", "auth", "setup-git"]
|
|
822
|
+
], "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."),
|
|
823
|
+
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."),
|
|
824
|
+
login("railway", "Sign in to Railway", [["railway", "login", "--browserless"]], "Complete the browserless native flow, then run railway whoami --json."),
|
|
825
|
+
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."),
|
|
826
|
+
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."),
|
|
827
|
+
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."),
|
|
828
|
+
login("npm", "Sign in to npm on this machine", [
|
|
829
|
+
[
|
|
830
|
+
"npm",
|
|
831
|
+
"login",
|
|
832
|
+
"--auth-type=web",
|
|
833
|
+
"--registry=https://registry.npmjs.org/"
|
|
834
|
+
]
|
|
835
|
+
], "Complete the specific CLI approval and run npm whoami. Prefer the existing trusted-publisher CI workflow for releases."),
|
|
836
|
+
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."),
|
|
837
|
+
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."),
|
|
838
|
+
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."),
|
|
839
|
+
{
|
|
840
|
+
id: "t3",
|
|
841
|
+
title: "Enable and authorize T3 Connect",
|
|
842
|
+
owner: "T3",
|
|
843
|
+
automatic: false,
|
|
844
|
+
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.",
|
|
845
|
+
commands: [["t3", "up", "--connect"]]
|
|
846
|
+
},
|
|
847
|
+
{
|
|
848
|
+
id: "docker",
|
|
849
|
+
title: "Start the intended Docker runtime",
|
|
850
|
+
owner: "OS / Docker",
|
|
851
|
+
automatic: false,
|
|
852
|
+
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.",
|
|
853
|
+
commands: [
|
|
854
|
+
["docker", "context", "ls"],
|
|
855
|
+
["docker", "info"]
|
|
856
|
+
]
|
|
857
|
+
},
|
|
858
|
+
{
|
|
859
|
+
id: "password-manager",
|
|
860
|
+
title: "Review shared human login onboarding",
|
|
861
|
+
owner: "password-manager / Bitwarden",
|
|
862
|
+
automatic: false,
|
|
863
|
+
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.",
|
|
864
|
+
commands: [
|
|
865
|
+
["bun", "run", "password-manager", "doctor"],
|
|
866
|
+
["bun", "run", "password-manager", "actions", "list"]
|
|
867
|
+
]
|
|
868
|
+
},
|
|
869
|
+
{
|
|
870
|
+
id: "operator",
|
|
871
|
+
title: "Resolve the check with its owning tool",
|
|
872
|
+
owner: "operator / existing owner",
|
|
873
|
+
automatic: false,
|
|
874
|
+
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.",
|
|
875
|
+
commands: []
|
|
876
|
+
}
|
|
877
|
+
];
|
|
878
|
+
var AUTOMATED_REPAIRS = [
|
|
879
|
+
"workspace",
|
|
880
|
+
"workstation",
|
|
881
|
+
"providers"
|
|
882
|
+
];
|
|
883
|
+
function recipe(id) {
|
|
884
|
+
return REPAIR_RECIPES.find((entry) => entry.id === id) ?? REPAIR_RECIPES.find((entry) => entry.id === "operator");
|
|
885
|
+
}
|
|
886
|
+
|
|
887
|
+
// packages/fleet/src/repair-runner.ts
|
|
888
|
+
function validRepairs(value) {
|
|
889
|
+
return Array.isArray(value) && value.length > 0 && value.length <= 3 && new Set(value).size === value.length && value.every((id) => AUTOMATED_REPAIRS.includes(id));
|
|
890
|
+
}
|
|
891
|
+
async function repairWorkspace(root, repairs, run, expected) {
|
|
892
|
+
if (!validRepairs(repairs))
|
|
893
|
+
throw new Error("Unsupported repairs");
|
|
894
|
+
const before = await fingerprint(root, run);
|
|
895
|
+
if (!before.head || !Object.values(before.files).every(Boolean))
|
|
896
|
+
return [
|
|
897
|
+
{
|
|
898
|
+
id: "repair:workspace",
|
|
899
|
+
status: "fail",
|
|
900
|
+
summary: "Repairs require a complete existing checkout. Register/bootstrap it first."
|
|
901
|
+
}
|
|
902
|
+
];
|
|
903
|
+
if (expected && JSON.stringify(before) !== JSON.stringify(expected))
|
|
904
|
+
return [
|
|
905
|
+
{
|
|
906
|
+
id: "repair:inputs",
|
|
907
|
+
status: "fail",
|
|
908
|
+
summary: "Checkout inputs changed since diagnostics. Generate a fresh repair plan before applying."
|
|
909
|
+
}
|
|
910
|
+
];
|
|
911
|
+
const checks = [];
|
|
912
|
+
for (const id of AUTOMATED_REPAIRS.filter((entry) => repairs.includes(entry))) {
|
|
913
|
+
const result = id === "workspace" ? await syncWorkspace(root, false, run) : await ownerCommand(root, id, run);
|
|
914
|
+
checks.push(...result);
|
|
915
|
+
if (result.some((check) => check.status !== "pass"))
|
|
916
|
+
break;
|
|
917
|
+
}
|
|
918
|
+
const after = await fingerprint(root, run);
|
|
919
|
+
if (JSON.stringify(before) !== JSON.stringify(after))
|
|
920
|
+
checks.push({
|
|
921
|
+
id: "repair:inputs",
|
|
922
|
+
status: "fail",
|
|
923
|
+
summary: "Checkout inputs changed during repair. Stop and review the checkout before retrying."
|
|
924
|
+
});
|
|
925
|
+
return checks;
|
|
926
|
+
}
|
|
927
|
+
async function ownerCommand(root, id, run) {
|
|
928
|
+
const result = await run(recipe(id).commands[0], {
|
|
929
|
+
cwd: root,
|
|
930
|
+
timeoutMs: 1200000
|
|
931
|
+
});
|
|
932
|
+
return [
|
|
933
|
+
{
|
|
934
|
+
id: `repair:${id}`,
|
|
935
|
+
status: result.ok ? "pass" : "fail",
|
|
936
|
+
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.`
|
|
937
|
+
}
|
|
938
|
+
];
|
|
939
|
+
}
|
|
940
|
+
|
|
941
|
+
// packages/fleet/src/protocol.ts
|
|
942
|
+
var STATUSES = ["pass", "warn", "fail", "skip"];
|
|
943
|
+
function validCheck(value) {
|
|
944
|
+
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)
|
|
945
|
+
return false;
|
|
946
|
+
const remedy = value["remedy"];
|
|
947
|
+
return remedy === undefined || validRemedy(remedy);
|
|
948
|
+
}
|
|
949
|
+
function validRemedy(value) {
|
|
950
|
+
return record(value) && typeof value["action"] === "string" && value["action"].length < 2000 && (value["command"] === undefined || typeof value["command"] === "string");
|
|
951
|
+
}
|
|
952
|
+
function validSnapshot(value) {
|
|
953
|
+
if (!record(value) || value["version"] !== 1 || typeof value["platform"] !== "string" || !Array.isArray(value["checks"]) || value["checks"].length > 200 || !value["checks"].every(validCheck))
|
|
954
|
+
return false;
|
|
955
|
+
return validFingerprint(value["fingerprint"]);
|
|
956
|
+
}
|
|
957
|
+
function validFingerprint(value) {
|
|
958
|
+
if (!record(value) || !record(value["files"]))
|
|
959
|
+
return false;
|
|
960
|
+
if (!Object.keys(value["files"]).every((key) => FINGERPRINT_FILES.includes(key)))
|
|
961
|
+
return false;
|
|
962
|
+
const head = value["head"];
|
|
963
|
+
return (head === null || typeof head === "string" && /^[a-f0-9]{40,64}$/.test(head)) && Object.values(value["files"]).every(validHash);
|
|
964
|
+
}
|
|
965
|
+
function validHash(value) {
|
|
966
|
+
return value === null || typeof value === "string" && /^[a-f0-9]{64}$/.test(value);
|
|
967
|
+
}
|
|
968
|
+
|
|
459
969
|
// packages/fleet/src/remote-entry.ts
|
|
460
970
|
async function main() {
|
|
461
971
|
const raw = JSON.parse(Buffer.from(process.argv.at(-1) ?? "", "base64").toString());
|
|
462
|
-
if (!
|
|
972
|
+
if (!validRequest(raw))
|
|
463
973
|
throw new Error("Invalid request.");
|
|
464
974
|
const node = parseConfig({ version: 1, nodes: [raw["node"]] }).nodes[0];
|
|
465
|
-
if (raw["action"] === "
|
|
975
|
+
if (raw["action"] === "configure") {
|
|
976
|
+
await configureRequest(raw, node);
|
|
977
|
+
} else if (raw["action"] === "repair") {
|
|
978
|
+
if (raw["apply"] !== true || !validRepairs(raw["repairs"]) || !validSnapshot({
|
|
979
|
+
version: 1,
|
|
980
|
+
platform: "request",
|
|
981
|
+
checks: [],
|
|
982
|
+
fingerprint: raw["expected"]
|
|
983
|
+
}))
|
|
984
|
+
throw new Error("Repair requires explicit authorization and supported actions.");
|
|
985
|
+
const checks = await repairWorkspace(node.workspace, raw["repairs"], runCommand, raw["expected"]);
|
|
986
|
+
process.stdout.write(JSON.stringify({ version: 1, checks }));
|
|
987
|
+
} else if (raw["action"] === "sync") {
|
|
466
988
|
if (raw["apply"] !== true)
|
|
467
989
|
throw new Error("Sync requires explicit authorization.");
|
|
468
990
|
const checks = await syncWorkspace(node.workspace, raw["pull"] === true, runCommand);
|
|
@@ -477,3 +999,12 @@ await main().catch(() => {
|
|
|
477
999
|
}));
|
|
478
1000
|
process.exitCode = 1;
|
|
479
1001
|
});
|
|
1002
|
+
async function configureRequest(raw, node) {
|
|
1003
|
+
if (raw["apply"] !== true || !validConfiguration(raw["configuration"]) || raw["configuration"].node !== node.id)
|
|
1004
|
+
throw new Error("Invalid configuration request.");
|
|
1005
|
+
const checks = await configureWorkspace(node.workspace, raw["configuration"], runCommand);
|
|
1006
|
+
process.stdout.write(JSON.stringify({ version: 1, checks }));
|
|
1007
|
+
}
|
|
1008
|
+
function validRequest(raw) {
|
|
1009
|
+
return record(raw) && ["probe", "sync", "repair", "configure"].includes(String(raw["action"]));
|
|
1010
|
+
}
|