loadout-ai 0.5.8 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (59) hide show
  1. package/CHANGELOG.md +17 -0
  2. package/MASTER_PLAN.md +9 -9
  3. package/README.md +84 -121
  4. package/catalog/discovered.json +59800 -10065
  5. package/catalog/packages.json +4 -3
  6. package/dist/src/cli.js +26 -2979
  7. package/dist/src/commands/agents.js +168 -0
  8. package/dist/src/commands/catalog.js +708 -0
  9. package/dist/src/commands/health.js +98 -0
  10. package/dist/src/commands/inventory.js +417 -0
  11. package/dist/src/commands/lifecycle.js +411 -0
  12. package/dist/src/commands/mcp.js +424 -0
  13. package/dist/src/commands/setup.js +67 -0
  14. package/dist/src/commands/sharing.js +358 -0
  15. package/dist/src/commands/support.js +246 -0
  16. package/dist/src/core/active-limit.js +2 -0
  17. package/dist/src/core/active-policy.js +2 -1
  18. package/dist/src/core/catalog-install.js +47 -10
  19. package/dist/src/core/catalog.js +15 -96
  20. package/dist/src/core/cli-guide.js +43 -37
  21. package/dist/src/core/completion.js +2 -20
  22. package/dist/src/core/doctor.js +73 -24
  23. package/dist/src/core/github.js +3 -2
  24. package/dist/src/core/handoff.js +161 -0
  25. package/dist/src/core/health-score-evidence.js +2 -2
  26. package/dist/src/core/health.js +83 -0
  27. package/dist/src/core/install.js +8 -3
  28. package/dist/src/core/loadout-card.js +14 -2
  29. package/dist/src/core/review-queue.js +7 -2
  30. package/dist/src/core/route.js +248 -0
  31. package/dist/src/core/skill-inventory.js +3 -3
  32. package/dist/src/core/skills.js +6 -7
  33. package/dist/src/core/source.js +10 -3
  34. package/dist/src/core/upgrade.js +10 -9
  35. package/docs/ACTIVE_SET.md +1 -1
  36. package/docs/ACTIVE_SET_POLICY.md +1 -1
  37. package/docs/CATALOG.md +2 -2
  38. package/docs/DEMO_SCRIPT.md +5 -5
  39. package/docs/DISCOVERED.md +250 -253
  40. package/docs/RELEASE_REVIEW.md +28 -143
  41. package/docs/SUBMISSION_COPY.md +4 -4
  42. package/docs/UPSTREAM_LICENSE_DECISIONS.md +19 -12
  43. package/docs/USER_TEST_GUIDE.md +1 -1
  44. package/docs/evidence/readme-claims.json +2 -14
  45. package/package.json +4 -3
  46. package/dist/src/core/benchmark-campaign.js +0 -496
  47. package/dist/src/core/benchmark-evidence.js +0 -458
  48. package/dist/src/core/benchmark-fixtures.js +0 -1157
  49. package/dist/src/core/benchmark-runner.js +0 -301
  50. package/dist/src/core/benchmark-trust.js +0 -795
  51. package/dist/src/core/catalog-release.js +0 -287
  52. package/dist/src/core/compatibility-intelligence.js +0 -465
  53. package/dist/src/core/ecosystem-import.js +0 -1072
  54. package/dist/src/core/head-to-head.js +0 -437
  55. package/dist/src/core/intelligence-feed-build.js +0 -59
  56. package/dist/src/core/intelligence-feed.js +0 -353
  57. package/dist/src/core/registry-api.js +0 -97
  58. package/dist/src/core/release-claims.js +0 -175
  59. package/dist/src/core/signing.js +0 -115
@@ -0,0 +1,424 @@
1
+ import { userHome } from "../core/paths.js";
2
+ import { readFile } from "node:fs/promises";
3
+ import { join } from "node:path";
4
+ import { fetchRepositorySnapshot } from "../core/source.js";
5
+ import { discoverMcpManifests, summarizeMcpManifest, planMcpConfig, summarizeMcpConfigPlan, applyMcpConfigPlan, } from "../core/mcp.js";
6
+ import { REVIEWED_MCP_RECIPES, buildMcpRecipeServer, findMcpRecipe, formatMcpRecipePlan, planMcpRecipe, verifyMcpRecipe, verifyMcpRecipeConnection, } from "../core/mcp-recipes.js";
7
+ import { inspectPackage, formatPackageInspection } from "../core/package.js";
8
+ import { installStatePath, recordInstallTransaction, recordMcpInstall, } from "../core/state.js";
9
+ import { applyCodexMcpConfigPlan, codexMcpServerFingerprint, defaultCodexMcpConfigPath, planCodexMcpConfig, } from "../core/codex-mcp.js";
10
+ import { evaluatePackage, formatPackageEvaluation } from "../core/evaluate.js";
11
+ import { checkForUpdates, startUpdateWatcher } from "../core/update-watch.js";
12
+ import { runDisposableSandbox } from "../core/sandbox.js";
13
+ import { applyNativeScheduler, formatNativeScheduler, planNativeScheduler, } from "../core/scheduler.js";
14
+ import { collectOption, parseMcpCredentialMappings, durableSchedulerLauncher, } from "./support.js";
15
+ export function registerMcp(program) {
16
+ program
17
+ .command("mcp-recipe")
18
+ .description("Preview/configure a reviewed MCP recipe, or explicitly verify its real connection")
19
+ .argument("[id]", "recipe id; omit to list reviewed recipes")
20
+ .option("--agent <id>", "target host: codex or claude-code; selects its default config format and path")
21
+ .option("--config <path>", "target JSON MCP config path")
22
+ .option("--yes", "write the reviewed server entry after preview")
23
+ .option("--verify", "verify the configured entry without starting a server")
24
+ .option("--connect", "launch the exact pinned artifact and perform an MCP initialize handshake")
25
+ .option("--credential <mapping>", "credential mapping NAME=env:VARIABLE or NAME=keychain:SERVICE (repeatable)", collectOption, [])
26
+ .option("--credential-account <account>", "account for keychain mappings")
27
+ .option("--timeout <milliseconds>", "real connection timeout", "30000")
28
+ .option("--approve-risk", "approve launching the reviewed pinned MCP artifact for --connect")
29
+ .option("--json", "emit machine-readable JSON")
30
+ .option("--no-key", "list recipes needing no separately billed AI/model API key")
31
+ .option("--no-model-key", "clear alias for --no-key; service tokens may still be required")
32
+ .option("--credential-free", "list recipes needing no credential of any kind")
33
+ .action(async (id, options) => {
34
+ if (!id) {
35
+ if (options.connect || options.verify || options.yes)
36
+ throw new Error("Select an MCP recipe id for this operation");
37
+ const selected = REVIEWED_MCP_RECIPES.filter((recipe) => !((options.key === false || options.modelKey === false) &&
38
+ recipe.modelApiProviders.length > 0) && !(options.credentialFree && recipe.environment.length > 0));
39
+ const listed = selected.map((recipe) => ({
40
+ id: recipe.id,
41
+ displayName: recipe.displayName,
42
+ source: recipe.source,
43
+ permissions: recipe.permissions,
44
+ environment: recipe.environment,
45
+ modelApiProviders: recipe.modelApiProviders,
46
+ }));
47
+ const output = options.json
48
+ ? JSON.stringify(listed, null, 2)
49
+ : listed
50
+ .map((recipe) => `${recipe.id} — ${recipe.displayName} — ${recipe.modelApiProviders.length ? `AI API required: ${recipe.modelApiProviders.join(", ")}` : "No AI API key required"} · ${recipe.environment.length ? `${recipe.environment.includes("GITHUB_PERSONAL_ACCESS_TOKEN") ? "GitHub token required" : `service credential required: ${recipe.environment.join(", ")}`}` : "no other credential"}`)
51
+ .join("\n");
52
+ console.log(output);
53
+ if (!options.json)
54
+ console.log("\nInstall for one host (preview first):\n loadout mcp-recipe playwright --agent codex\n loadout mcp-recipe playwright --agent claude-code");
55
+ return;
56
+ }
57
+ if (options.connect) {
58
+ if (options.verify || options.yes)
59
+ throw new Error("--connect cannot be combined with --verify or --yes");
60
+ const recipe = findMcpRecipe(id);
61
+ const credentialReferences = parseMcpCredentialMappings(recipe, options.credential, options.credentialAccount);
62
+ const timeoutMs = Number(options.timeout);
63
+ const controller = new AbortController();
64
+ const abort = () => controller.abort();
65
+ process.once("SIGINT", abort);
66
+ process.once("SIGTERM", abort);
67
+ try {
68
+ const result = await verifyMcpRecipeConnection(id, {
69
+ approveRisk: Boolean(options.approveRisk),
70
+ credentialReferences,
71
+ timeoutMs,
72
+ signal: controller.signal,
73
+ });
74
+ console.log(options.json
75
+ ? JSON.stringify(result, null, 2)
76
+ : `Connected: ${result.recipeId} · MCP ${result.protocolVersion}${result.serverInfo ? ` · ${result.serverInfo.name}${result.serverInfo.version ? ` ${result.serverInfo.version}` : ""}` : ""}\n${result.checks.join("\n")}`);
77
+ }
78
+ finally {
79
+ process.off("SIGINT", abort);
80
+ process.off("SIGTERM", abort);
81
+ }
82
+ return;
83
+ }
84
+ if (options.agent &&
85
+ options.agent !== "codex" &&
86
+ options.agent !== "claude-code")
87
+ throw new Error("--agent must be codex or claude-code");
88
+ if (options.agent && options.config)
89
+ throw new Error("Choose --agent for a managed default path or --config for an explicit JSON path, not both");
90
+ const configPath = options.config ??
91
+ (options.agent === "codex"
92
+ ? defaultCodexMcpConfigPath()
93
+ : options.agent === "claude-code"
94
+ ? join(userHome(), ".claude.json")
95
+ : undefined);
96
+ if (!configPath)
97
+ throw new Error("Choose --agent codex, --agent claude-code, or an explicit --config path");
98
+ if (options.verify) {
99
+ if (options.yes)
100
+ throw new Error("--verify cannot be combined with --yes");
101
+ if (options.agent === "codex") {
102
+ const recipe = findMcpRecipe(id);
103
+ let content = "";
104
+ try {
105
+ content = await readFile(configPath, "utf8");
106
+ }
107
+ catch {
108
+ // Missing config is rendered as a normal not-configured result.
109
+ }
110
+ const configured = Boolean(codexMcpServerFingerprint(content, recipe.serverName));
111
+ console.log(options.json
112
+ ? JSON.stringify({ recipeId: id, configPath, configured }, null, 2)
113
+ : `${configured ? "Configured" : "Not configured"}: ${id}\nCodex config: ${configPath}\nConfiguration presence does not launch the MCP server; add --connect --approve-risk for a bounded handshake.`);
114
+ if (!configured)
115
+ process.exitCode = 1;
116
+ return;
117
+ }
118
+ const verification = await verifyMcpRecipe(id, configPath);
119
+ console.log(options.json
120
+ ? JSON.stringify(verification, null, 2)
121
+ : `${verification.configured ? "Configured" : "Not configured"}: ${verification.recipeId}\n${[...verification.checks, ...verification.warnings].join("\n")}`);
122
+ if (!verification.configured)
123
+ process.exitCode = 1;
124
+ return;
125
+ }
126
+ const recipe = findMcpRecipe(id);
127
+ const credentialReferences = parseMcpCredentialMappings(recipe, options.credential, options.credentialAccount);
128
+ if (options.agent === "codex") {
129
+ const server = buildMcpRecipeServer(id, configPath, {
130
+ credentialReferences,
131
+ requireResolvedCredentials: Boolean(options.yes),
132
+ });
133
+ const plan = await planCodexMcpConfig(configPath, server, server.name);
134
+ const recipe = findMcpRecipe(id);
135
+ const preview = [
136
+ `MCP recipe: ${recipe.displayName}`,
137
+ `Target: Codex`,
138
+ `Config: ${plan.path}`,
139
+ `Change: ${plan.summary}`,
140
+ `Artifact: ${recipe.artifact}`,
141
+ `Permissions: ${recipe.permissions.join(", ")}`,
142
+ recipe.environment.length
143
+ ? `Credential references required: ${recipe.environment.join(", ")}`
144
+ : "Credentials: none",
145
+ ].join("\n");
146
+ if (!options.yes) {
147
+ console.log(options.json
148
+ ? JSON.stringify({
149
+ recipe,
150
+ agent: "codex",
151
+ path: plan.path,
152
+ summary: plan.summary,
153
+ }, null, 2)
154
+ : `${preview}\nDry run only. Re-run with --yes to add this reviewed Codex MCP server.`);
155
+ return;
156
+ }
157
+ const fingerprint = codexMcpServerFingerprint(plan.proposed, recipe.serverName);
158
+ if (!fingerprint)
159
+ throw new Error("Could not fingerprint the applied Codex MCP entry");
160
+ const snapshot = await applyCodexMcpConfigPlan(plan, {
161
+ extraTargets: [installStatePath()],
162
+ afterApply: async (_result, transactionSnapshot) => recordMcpInstall({
163
+ packageId: `mcp-recipe:${id}:codex`,
164
+ configPath: plan.path,
165
+ serverName: recipe.serverName,
166
+ fingerprint,
167
+ snapshotId: transactionSnapshot.id,
168
+ installedAt: new Date().toISOString(),
169
+ configFormat: "codex-toml",
170
+ }),
171
+ });
172
+ console.log(options.json
173
+ ? JSON.stringify({
174
+ recipe,
175
+ agent: "codex",
176
+ snapshotId: snapshot.rollbackSnapshotId,
177
+ }, null, 2)
178
+ : `${preview}\nConfigured for Codex. Managed package: mcp-recipe:${id}:codex\nRollback: loadout rollback --snapshot ${snapshot.rollbackSnapshotId}\nNext: loadout mcp-recipe ${id} --agent codex --verify`);
179
+ return;
180
+ }
181
+ const plan = await planMcpRecipe(id, configPath, {
182
+ credentialReferences,
183
+ requireResolvedCredentials: Boolean(options.yes),
184
+ });
185
+ if (!options.yes) {
186
+ const target = options.agent === "claude-code" ? "Target: Claude Code\n" : "";
187
+ console.log(options.json
188
+ ? JSON.stringify(options.agent ? { agent: options.agent, ...plan } : plan, null, 2)
189
+ : `${target}${formatMcpRecipePlan(plan)}\nDry run only. Re-run with --yes to write this server entry.`);
190
+ return;
191
+ }
192
+ const managedId = options.agent
193
+ ? `mcp-recipe:${id}:${options.agent}`
194
+ : `mcp-recipe:${id}`;
195
+ const snapshot = await applyMcpConfigPlan(plan.config, {
196
+ extraTargets: [installStatePath()],
197
+ afterApply: async (_result, transactionSnapshot) => {
198
+ await recordInstallTransaction([], [{ packageId: managedId, plan: plan.config }], transactionSnapshot.id);
199
+ },
200
+ });
201
+ console.log(options.json
202
+ ? JSON.stringify({ plan, snapshot }, null, 2)
203
+ : `${options.agent === "claude-code" ? "Target: Claude Code\n" : ""}${formatMcpRecipePlan(plan)}\nConfigured${options.agent ? " for Claude Code" : ""}. Managed package: ${managedId}\nRollback: loadout rollback --snapshot ${snapshot.rollbackSnapshotId}\nAuthorize the service separately, then run: loadout mcp-recipe ${id} ${options.agent ? `--agent ${options.agent}` : `--config ${plan.config.path}`} --verify`);
204
+ });
205
+ program
206
+ .command("mcp")
207
+ .description("Inspect MCP manifests without executing servers or scripts")
208
+ .option("--source <directory>", "local repository directory")
209
+ .option("--repository <owner/repo>", "public GitHub repository")
210
+ .option("--json", "emit normalized JSON")
211
+ .action(async (options) => {
212
+ if ((options.source ? 1 : 0) + (options.repository ? 1 : 0) !== 1) {
213
+ throw new Error("Provide exactly one of --source or --repository");
214
+ }
215
+ const source = options.repository
216
+ ? (await fetchRepositorySnapshot(options.repository)).path
217
+ : options.source;
218
+ const manifests = await discoverMcpManifests(source);
219
+ if (options.json)
220
+ console.log(JSON.stringify(manifests, null, 2));
221
+ else if (manifests.length === 0)
222
+ console.log("No supported MCP manifests found.");
223
+ else
224
+ for (const manifest of manifests)
225
+ console.log(summarizeMcpManifest(manifest));
226
+ });
227
+ program
228
+ .command("inspect")
229
+ .description("Inspect skills and MCP components in a local directory or public GitHub repository")
230
+ .option("--source <directory>", "local package directory")
231
+ .option("--repository <owner/repo>", "public GitHub repository")
232
+ .option("--json", "emit normalized JSON")
233
+ .action(async (options) => {
234
+ if ((options.source ? 1 : 0) + (options.repository ? 1 : 0) !== 1)
235
+ throw new Error("Provide exactly one of --source or --repository");
236
+ const source = options.repository
237
+ ? (await fetchRepositorySnapshot(options.repository)).path
238
+ : options.source;
239
+ const result = await inspectPackage(source);
240
+ console.log(options.json
241
+ ? JSON.stringify(result, null, 2)
242
+ : formatPackageInspection(result));
243
+ });
244
+ program
245
+ .command("evaluate")
246
+ .description("Evaluate static skill and MCP evidence without executing package code")
247
+ .option("--source <directory>", "local package directory")
248
+ .option("--repository <owner/repo>", "public GitHub repository")
249
+ .option("--json", "emit evaluation JSON")
250
+ .action(async (options) => {
251
+ if ((options.source ? 1 : 0) + (options.repository ? 1 : 0) !== 1)
252
+ throw new Error("Provide exactly one of --source or --repository");
253
+ const source = options.repository
254
+ ? (await fetchRepositorySnapshot(options.repository)).path
255
+ : options.source;
256
+ const result = await evaluatePackage(source);
257
+ console.log(options.json
258
+ ? JSON.stringify(result, null, 2)
259
+ : formatPackageEvaluation(result));
260
+ });
261
+ program
262
+ .command("watch")
263
+ .description("Watch for read-only updates; never applies changes automatically")
264
+ .option("--interval <minutes>", "check interval", "1440")
265
+ .option("--once", "check once and exit")
266
+ .option("--json", "emit each notification as JSON")
267
+ .action(async (options) => {
268
+ const minutes = Number(options.interval);
269
+ if (!Number.isFinite(minutes) || minutes <= 0)
270
+ throw new Error("--interval must be a positive number of minutes");
271
+ const notify = (notification) => console.log(options.json
272
+ ? JSON.stringify(notification)
273
+ : `${notification.checkedAt}: ${notification.message}`);
274
+ if (options.once) {
275
+ notify(await checkForUpdates());
276
+ return;
277
+ }
278
+ const stop = startUpdateWatcher({
279
+ intervalMs: minutes * 60_000,
280
+ notify,
281
+ });
282
+ const shutdown = () => {
283
+ stop();
284
+ process.exit(0);
285
+ };
286
+ process.once("SIGINT", shutdown);
287
+ process.once("SIGTERM", shutdown);
288
+ await new Promise(() => undefined);
289
+ });
290
+ for (const action of [
291
+ "schedule",
292
+ "unschedule",
293
+ ]) {
294
+ program
295
+ .command(action)
296
+ .description(`${action === "schedule" ? "Install" : "Remove"} a native daily read-only update or candidate-discovery check`)
297
+ .option("--time <HH:MM>", "local daily check time", "09:00")
298
+ .option("--job <updates|discovery>", "daily read-only job", "updates")
299
+ .option("--yes", "apply the native scheduler change")
300
+ .option("--json", "emit machine-readable JSON")
301
+ .action(async (options) => {
302
+ if (options.job !== "updates" && options.job !== "discovery")
303
+ throw new Error("--job must be updates or discovery");
304
+ const plan = planNativeScheduler(action, {
305
+ time: options.time,
306
+ launcher: durableSchedulerLauncher(),
307
+ job: options.job,
308
+ });
309
+ if (!options.yes) {
310
+ console.log(options.json
311
+ ? JSON.stringify(plan, null, 2)
312
+ : `${formatNativeScheduler(plan)}\nDry run only. Re-run with --yes to change the native scheduler.`);
313
+ return;
314
+ }
315
+ const snapshotId = await applyNativeScheduler(plan);
316
+ console.log(options.json
317
+ ? JSON.stringify({ plan, snapshotId }, null, 2)
318
+ : `${formatNativeScheduler(plan)}\nApplied. Snapshot: ${snapshotId}`);
319
+ });
320
+ }
321
+ program
322
+ .command("sandbox-run")
323
+ .description("Run an explicitly approved command in a disposable networkless Docker sandbox")
324
+ .requiredOption("--source <directory>", "read-only source directory")
325
+ .requiredOption("--image <image>", "reviewed/pinned Docker image reference")
326
+ .requiredOption("--command <argument>", "command argument (repeatable; first is executable)", (value, previous = []) => [...previous, value], [])
327
+ .requiredOption("--approve-risk", "explicitly approve sandbox execution")
328
+ .option("--timeout <milliseconds>", "execution timeout", "120000")
329
+ .option("--json", "emit result JSON")
330
+ .action(async (options) => {
331
+ const result = await runDisposableSandbox({
332
+ sourceDirectory: options.source,
333
+ image: options.image,
334
+ command: options.command,
335
+ approveRisk: options.approveRisk,
336
+ timeoutMs: Number(options.timeout),
337
+ });
338
+ console.log(options.json
339
+ ? JSON.stringify(result, null, 2)
340
+ : `Sandbox exited ${result.exitCode}\n${result.stdout}${result.stderr ? `\n${result.stderr}` : ""}`);
341
+ if (result.exitCode !== 0)
342
+ process.exitCode = result.exitCode;
343
+ });
344
+ program
345
+ .command("mcp-config")
346
+ .description("Plan or apply a safe MCP server configuration change (dry-run by default)")
347
+ .requiredOption("--config <path>", "MCP JSON configuration path")
348
+ .requiredOption("--name <name>", "server name")
349
+ .option("--command <command>", "local server command")
350
+ .option("--url <url>", "remote MCP server URL")
351
+ .option("--arg <value>", "server argument (repeatable)", (value, previous = []) => [...previous, value], [])
352
+ .option("--env <NAME=VALUE>", "environment variable (repeatable; values are never printed)", (value, previous = []) => [...previous, value], [])
353
+ .option("--yes", "apply the change; without this flag only a plan is shown")
354
+ .action(async (options) => {
355
+ if ((options.command ? 1 : 0) + (options.url ? 1 : 0) !== 1)
356
+ throw new Error("Provide exactly one of --command or --url");
357
+ const env = {};
358
+ for (const item of options.env) {
359
+ const separator = item.indexOf("=");
360
+ if (separator <= 0)
361
+ throw new Error(`Invalid --env '${item}'; expected NAME=VALUE`);
362
+ const key = item.slice(0, separator);
363
+ if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(key))
364
+ throw new Error(`Invalid environment variable name '${key}'`);
365
+ env[key] = item.slice(separator + 1);
366
+ }
367
+ const server = {
368
+ name: options.name,
369
+ command: options.command,
370
+ url: options.url,
371
+ args: options.arg,
372
+ env,
373
+ sourcePath: options.config,
374
+ warnings: [],
375
+ };
376
+ const plan = await planMcpConfig(options.config, server);
377
+ console.log(summarizeMcpConfigPlan(plan));
378
+ if (!options.yes) {
379
+ console.log("Dry run only. Re-run with --yes to apply this change.");
380
+ return;
381
+ }
382
+ const snapshot = await applyMcpConfigPlan(plan);
383
+ console.log(`Applied successfully. Snapshot: ${snapshot.rollbackSnapshotId}`);
384
+ });
385
+ program
386
+ .command("codex-mcp-config")
387
+ .description("Plan or add a Codex TOML MCP server (dry-run by default)")
388
+ .option("--config <path>", "Codex config.toml path", defaultCodexMcpConfigPath())
389
+ .requiredOption("--name <name>", "server name")
390
+ .option("--command <command>", "local server command")
391
+ .option("--url <url>", "remote MCP server URL")
392
+ .option("--arg <value>", "server argument (repeatable)", (value, previous = []) => [...previous, value], [])
393
+ .option("--env <NAME=VALUE>", "environment variable (repeatable; values are never printed)", (value, previous = []) => [...previous, value], [])
394
+ .option("--yes", "apply the change; without this flag only a plan is shown")
395
+ .action(async (options) => {
396
+ if ((options.command ? 1 : 0) + (options.url ? 1 : 0) !== 1)
397
+ throw new Error("Provide exactly one of --command or --url");
398
+ const env = {};
399
+ for (const item of options.env) {
400
+ const separator = item.indexOf("=");
401
+ if (separator <= 0)
402
+ throw new Error(`Invalid --env '${item}'; expected NAME=VALUE`);
403
+ const key = item.slice(0, separator);
404
+ if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(key))
405
+ throw new Error(`Invalid environment variable name '${key}'`);
406
+ env[key] = item.slice(separator + 1);
407
+ }
408
+ const server = {
409
+ name: options.name,
410
+ command: options.command,
411
+ url: options.url,
412
+ args: options.arg,
413
+ env,
414
+ sourcePath: options.config,
415
+ warnings: [],
416
+ };
417
+ const plan = await planCodexMcpConfig(options.config, server);
418
+ console.log(`Codex config: ${plan.path}\n - ${plan.summary}`);
419
+ if (!options.yes)
420
+ return console.log("Dry run only. Re-run with --yes to add this Codex MCP server.");
421
+ const snapshot = await applyCodexMcpConfigPlan(plan);
422
+ console.log(`Applied successfully. Snapshot: ${snapshot.rollbackSnapshotId}`);
423
+ });
424
+ }
@@ -0,0 +1,67 @@
1
+ import { parseAgentSelection } from "../core/paths.js";
2
+ import { formatHealthReport } from "../core/health.js";
3
+ import { applyUpgrade, formatUpgradePlan, planUpgrade, summarizeUpgradePlan, } from "../core/upgrade.js";
4
+ import { parseModelApiAccess } from "../core/access.js";
5
+ import { ADVANCED_GUIDE } from "../core/cli-guide.js";
6
+ import { collectOption, setupSelection, printSetupProgress, runSetup, printBeginnerGuide, } from "./support.js";
7
+ export function registerSetup(program) {
8
+ program
9
+ .command("guide")
10
+ .description("Show the simple, read-only path for using Loadout")
11
+ .action(printBeginnerGuide);
12
+ program
13
+ .command("advanced")
14
+ .description("Explain where to find advanced and maintainer-only commands")
15
+ .action(() => console.log(ADVANCED_GUIDE));
16
+ program
17
+ .command("setup")
18
+ .description("Preview and install a screened skill loadout for detected agents")
19
+ .option("--mode <mode>", "stable, power, maximum, or custom")
20
+ .option("--agents <ids>", "comma-separated target agent ids")
21
+ .option("--package <id>", "package id for custom mode", collectOption, [])
22
+ .option("--api-access <providers>", "separately billed model API access: none, openai, anthropic, openrouter, or other (comma-separated; never a key)")
23
+ .option("-y, --yes", "install after preparing the screened plan")
24
+ .option("--approve-risk", "approve reviewed safety findings in non-interactive mode")
25
+ .option("--details", "show every quarantined and deferred unit")
26
+ .action((options) => runSetup(options));
27
+ program
28
+ .command("upgrade")
29
+ .description("Diagnose, recommend, preview, and transactionally apply one screened upgrade")
30
+ .option("--mode <mode>", "stable, power, maximum, or custom", "stable")
31
+ .option("--project <path>", "project directory", process.cwd())
32
+ .option("--agents <ids>", "comma-separated target agent ids")
33
+ .option("--package <id>", "package id for custom mode", collectOption, [])
34
+ .option("--api-access <providers>", "separately billed model API access: none, openai, anthropic, openrouter, or other (comma-separated; never a key)")
35
+ .option("--yes", "apply the exact displayed upgrade")
36
+ .option("--approve-risk", "approve the displayed reviewed safety findings")
37
+ .option("--json", "emit a machine-readable preview or result")
38
+ .action(async (options) => {
39
+ const plan = await planUpgrade(setupSelection(options.mode, options.package), {
40
+ projectPath: options.project,
41
+ requestedAgents: parseAgentSelection(options.agents),
42
+ onProgress: options.json ? undefined : printSetupProgress,
43
+ access: parseModelApiAccess(options.apiAccess),
44
+ });
45
+ if (!options.yes) {
46
+ console.log(options.json
47
+ ? JSON.stringify(summarizeUpgradePlan(plan), null, 2)
48
+ : `${formatUpgradePlan(plan)}\n\nPreview complete; nothing was changed. Re-run with --yes${plan.riskApprovalRequired ? " --approve-risk" : ""} to apply this exact upgrade.`);
49
+ return;
50
+ }
51
+ const result = await applyUpgrade(plan, {
52
+ approveRisk: options.approveRisk,
53
+ });
54
+ console.log(options.json
55
+ ? JSON.stringify({
56
+ plan: summarizeUpgradePlan(plan),
57
+ result,
58
+ }, null, 2)
59
+ : [
60
+ `Upgrade applied as one transaction. Snapshot: ${result.snapshotId}`,
61
+ formatHealthReport(result.healthAfter),
62
+ ...result.healthScoresAfter.map((score) => `Agent Health Score (${score.agent}): ${score.score}/100 (${score.rating}; evidence coverage ${score.evidenceCoverage}%)`),
63
+ "Scores summarize stored evidence only; they do not claim task improvement until benchmark or local outcome evidence exists.",
64
+ "Next: run `loadout rollback` to restore or `loadout outcome` after real use.",
65
+ ].join("\n"));
66
+ });
67
+ }