loadout-ai 0.4.0 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +65 -0
- package/MASTER_PLAN.md +173 -9
- package/README.md +57 -19
- package/catalog/discovered.json +8422 -6957
- package/catalog/packages.json +26 -0
- package/dist/src/cli.js +140 -68
- package/dist/src/core/active-policy.js +216 -42
- package/dist/src/core/active-set.js +14 -5
- package/dist/src/core/adopt.js +1 -1
- package/dist/src/core/catalog-coverage.js +2 -1
- package/dist/src/core/catalog-install.js +23 -8
- package/dist/src/core/cli-guide.js +5 -2
- package/dist/src/core/codex-mcp.js +49 -5
- package/dist/src/core/completion.js +0 -3
- package/dist/src/core/health.js +19 -11
- package/dist/src/core/install.js +6 -39
- package/dist/src/core/mcp-recipes.js +13 -4
- package/dist/src/core/mcp.js +6 -5
- package/dist/src/core/model-config.js +1 -1
- package/dist/src/core/portable.js +1 -1
- package/dist/src/core/recommend.js +129 -27
- package/dist/src/core/remove.js +28 -2
- package/dist/src/core/runtime-tool-recipe.js +2 -2
- package/dist/src/core/runtime-tools.js +15 -4
- package/dist/src/core/snapshot.js +20 -0
- package/dist/src/core/state.js +9 -0
- package/dist/src/core/sync.js +1 -1
- package/dist/src/core/target-occupancy.js +50 -0
- package/dist/src/core/transaction.js +2 -1
- package/dist/src/shared/schemas.js +1 -0
- package/docs/CATALOG.md +3 -2
- package/docs/DISCOVERED.md +253 -255
- package/docs/FEATURE_TEST_MATRIX.md +19 -32
- package/docs/GITHUB_AUTHORIZATION.md +2 -2
- package/docs/TESTING.md +3 -16
- package/docs/USER_TEST_GUIDE.md +39 -13
- package/docs/assets/loadout-workflow.png +0 -0
- package/docs/evidence/readme-claims.json +0 -8
- package/docs/superpowers/plans/2026-07-20-loadout-readme-explainer.md +116 -0
- package/docs/superpowers/plans/2026-07-20-project-activation-safety.md +469 -0
- package/docs/superpowers/specs/2026-07-20-loadout-readme-explainer-design.md +55 -0
- package/docs/superpowers/specs/2026-07-20-project-activation-safety-design.md +228 -0
- package/package.json +4 -6
- package/dashboard/app.js +0 -607
- package/dashboard/index.html +0 -249
- package/dashboard/styles.css +0 -384
- package/dist/src/core/demo.js +0 -136
- package/dist/src/dashboard.js +0 -421
package/catalog/packages.json
CHANGED
|
@@ -1146,5 +1146,31 @@
|
|
|
1146
1146
|
"evidencePaths": ["server.js", "package.json"],
|
|
1147
1147
|
"verifiedAt": "2026-07-16T08:45:18Z"
|
|
1148
1148
|
}
|
|
1149
|
+
},
|
|
1150
|
+
{
|
|
1151
|
+
"id": "obsidian-skills",
|
|
1152
|
+
"displayName": "Obsidian Skills",
|
|
1153
|
+
"repository": "kepano/obsidian-skills",
|
|
1154
|
+
"description": "Agent skills for working with Obsidian Markdown, Bases, JSON Canvas, Defuddle, and the Obsidian CLI.",
|
|
1155
|
+
"category": "knowledge-management",
|
|
1156
|
+
"tier": "community",
|
|
1157
|
+
"license": "MIT",
|
|
1158
|
+
"components": ["skill", "plugin"],
|
|
1159
|
+
"operatingSystems": ["windows", "macos", "linux"],
|
|
1160
|
+
"stars": 42827,
|
|
1161
|
+
"pushedAt": "2026-06-08T16:12:01Z",
|
|
1162
|
+
"source": {
|
|
1163
|
+
"type": "github",
|
|
1164
|
+
"url": "https://github.com/kepano/obsidian-skills",
|
|
1165
|
+
"defaultBranch": "main",
|
|
1166
|
+
"commit": "a1dc48e68138490d522c04cbf5822214c6eb1202",
|
|
1167
|
+
"evidencePaths": [
|
|
1168
|
+
"skills/obsidian-markdown/SKILL.md",
|
|
1169
|
+
"skills/obsidian-bases/SKILL.md",
|
|
1170
|
+
"skills/json-canvas/SKILL.md",
|
|
1171
|
+
".claude-plugin/plugin.json"
|
|
1172
|
+
],
|
|
1173
|
+
"verifiedAt": "2026-07-21T08:45:00Z"
|
|
1174
|
+
}
|
|
1149
1175
|
}
|
|
1150
1176
|
]
|
package/dist/src/cli.js
CHANGED
|
@@ -3,21 +3,21 @@ import { Command, CommanderError } from "commander";
|
|
|
3
3
|
import { existsSync } from "node:fs";
|
|
4
4
|
import { createInterface } from "node:readline/promises";
|
|
5
5
|
import { explainCatalogScore, loadEffectiveCatalog, loadCatalog, rankCatalog, refreshCatalog, } from "./core/catalog.js";
|
|
6
|
-
import { detectAgents, parseAgentSelection } from "./core/paths.js";
|
|
6
|
+
import { detectAgents, parseAgentSelection, userHome } from "./core/paths.js";
|
|
7
7
|
import { readFile } from "node:fs/promises";
|
|
8
8
|
import { dirname, join, resolve } from "node:path";
|
|
9
9
|
import { buildSkillPlan, applySkillInstall, installedAgents, } from "./core/install.js";
|
|
10
|
-
import { listSnapshotIds, readSnapshot, restoreSnapshot, } from "./core/snapshot.js";
|
|
10
|
+
import { listSnapshotIds, readSnapshot, restoreSnapshot, summarizeSnapshot, } from "./core/snapshot.js";
|
|
11
11
|
import { fetchRepositorySnapshot } from "./core/source.js";
|
|
12
12
|
import { discoverMcpManifests, summarizeMcpManifest, planMcpConfig, summarizeMcpConfigPlan, applyMcpConfigPlan, } from "./core/mcp.js";
|
|
13
|
-
import { REVIEWED_MCP_RECIPES, findMcpRecipe, formatMcpRecipePlan, planMcpRecipe, verifyMcpRecipe, verifyMcpRecipeConnection, } from "./core/mcp-recipes.js";
|
|
13
|
+
import { REVIEWED_MCP_RECIPES, buildMcpRecipeServer, findMcpRecipe, formatMcpRecipePlan, planMcpRecipe, verifyMcpRecipe, verifyMcpRecipeConnection, } from "./core/mcp-recipes.js";
|
|
14
14
|
import { runDoctor, formatDoctorReport } from "./core/doctor.js";
|
|
15
15
|
import { applyPackageUpdate, buildUpdatePlan, formatUpdatePlan, selectSafeAutomaticUpdates, } from "./core/update.js";
|
|
16
16
|
import { startApiServer } from "./core/api.js";
|
|
17
17
|
import { inspectPackage, formatPackageInspection } from "./core/package.js";
|
|
18
18
|
import { addManifestPackage, applyProfileToManifest, initManifest, readManifest, removeManifestPackage, writeLockfile, } from "./core/manifest.js";
|
|
19
19
|
import { buildHealthReport, formatHealthReport } from "./core/health.js";
|
|
20
|
-
import { readInstallState, recordInstallTransaction } from "./core/state.js";
|
|
20
|
+
import { installStatePath, readInstallState, recordInstallTransaction, recordMcpInstall, } from "./core/state.js";
|
|
21
21
|
import { applyRemove, planRemove } from "./core/remove.js";
|
|
22
22
|
import { applyUninstall, buildUninstallPlan, formatUninstallPlan, uninstallGlobalCli, } from "./core/uninstall.js";
|
|
23
23
|
import { evaluateInstalledProfile, formatInstalledProfileStatus, } from "./core/profile-state.js";
|
|
@@ -31,8 +31,7 @@ import { ADAPTER_CAPABILITIES, buildAdapterCapabilityGaps, formatAdapterCapabili
|
|
|
31
31
|
import { formatAgentInventory, inspectAgents, } from "./core/agent-inspection.js";
|
|
32
32
|
import { generateSigningKeys, signJsonFile, verifyJsonFile, } from "./core/signing.js";
|
|
33
33
|
import { applyPortableImport, exportPortableLoadout, planPortableImport, } from "./core/portable.js";
|
|
34
|
-
import { applyCodexMcpConfigPlan, defaultCodexMcpConfigPath, planCodexMcpConfig, } from "./core/codex-mcp.js";
|
|
35
|
-
import { formatDemoResult, runIsolatedDemo } from "./core/demo.js";
|
|
34
|
+
import { applyCodexMcpConfigPlan, codexMcpServerFingerprint, defaultCodexMcpConfigPath, planCodexMcpConfig, } from "./core/codex-mcp.js";
|
|
36
35
|
import { catalogTrustStage, formatCatalogTrustStage, resolveCatalogProfile, } from "./core/profiles.js";
|
|
37
36
|
import { discoverHackerNewsRepositories } from "./core/community.js";
|
|
38
37
|
import { discoverPrivateRepositories } from "./core/private-discovery.js";
|
|
@@ -44,7 +43,6 @@ import { runDisposableSandbox } from "./core/sandbox.js";
|
|
|
44
43
|
import { compileConversion, } from "./core/conversion.js";
|
|
45
44
|
import { writeFileAtomically } from "./core/atomic-file.js";
|
|
46
45
|
import { formatCanaryResult, runCanary } from "./core/canary.js";
|
|
47
|
-
import { startDashboardServer } from "./dashboard.js";
|
|
48
46
|
import { applyPreparedCatalogInstall, formatCatalogApplyGuidance, formatPreparedCatalogInstall, prepareCatalogInstall, } from "./core/catalog-install.js";
|
|
49
47
|
import { formatInstalledSkillInventory, scanInstalledSkills, } from "./core/skill-inventory.js";
|
|
50
48
|
import { enrichInventoryWithProvenance, formatProvenanceSummary, resolveCatalogSkillIndex, } from "./core/provenance.js";
|
|
@@ -222,7 +220,7 @@ async function runSetup(options) {
|
|
|
222
220
|
onProgress: printSetupProgress,
|
|
223
221
|
access,
|
|
224
222
|
});
|
|
225
|
-
console.log(`\n${formatPreparedCatalogInstall(prepared)}\n`);
|
|
223
|
+
console.log(`\n${formatPreparedCatalogInstall(prepared, { details: options.details })}\n`);
|
|
226
224
|
const risky = riskyPackageSummary(prepared);
|
|
227
225
|
let approved = Boolean(options.yes);
|
|
228
226
|
let riskApproved = Boolean(options.approveRisk);
|
|
@@ -265,7 +263,7 @@ async function runSetup(options) {
|
|
|
265
263
|
reader?.close();
|
|
266
264
|
}
|
|
267
265
|
}
|
|
268
|
-
const LOADOUT_VERSION = "0.
|
|
266
|
+
const LOADOUT_VERSION = "0.5.0";
|
|
269
267
|
function durableSchedulerLauncher() {
|
|
270
268
|
return [
|
|
271
269
|
join(dirname(process.execPath), process.platform === "win32" ? "npx.cmd" : "npx"),
|
|
@@ -308,6 +306,7 @@ program
|
|
|
308
306
|
.option("--api-access <providers>", "separately billed model API access: none, openai, anthropic, openrouter, or other (comma-separated; never a key)")
|
|
309
307
|
.option("-y, --yes", "install after preparing the screened plan")
|
|
310
308
|
.option("--approve-risk", "approve reviewed safety findings in non-interactive mode")
|
|
309
|
+
.option("--details", "show every quarantined and deferred unit")
|
|
311
310
|
.action((options) => runSetup(options));
|
|
312
311
|
program
|
|
313
312
|
.command("upgrade")
|
|
@@ -1077,6 +1076,12 @@ program
|
|
|
1077
1076
|
: Object.entries(TESTED_PROFILES)
|
|
1078
1077
|
.map(([id, profile]) => `${id} — ${profile.description}`)
|
|
1079
1078
|
.join("\n"));
|
|
1079
|
+
if (name === "custom" && !options.applyTo) {
|
|
1080
|
+
const profile = TESTED_PROFILES.custom;
|
|
1081
|
+
return console.log(options.json
|
|
1082
|
+
? JSON.stringify({ name, ...profile, packages: [] }, null, 2)
|
|
1083
|
+
: `custom: ${profile.description}\n Use: loadout setup --mode custom --package <id>`);
|
|
1084
|
+
}
|
|
1080
1085
|
const packages = profileManifestPackages(name, await loadEffectiveCatalog());
|
|
1081
1086
|
if (options.applyTo) {
|
|
1082
1087
|
const manifest = await applyProfileToManifest(options.applyTo, name, packages);
|
|
@@ -1465,26 +1470,6 @@ benchmark
|
|
|
1465
1470
|
if (!summary.withinBudget)
|
|
1466
1471
|
process.exitCode = 2;
|
|
1467
1472
|
});
|
|
1468
|
-
program
|
|
1469
|
-
.command("demo")
|
|
1470
|
-
.alias("test-drive")
|
|
1471
|
-
.description("Test-drive a reviewed install + rollback in a disposable profile; never touches local agent config")
|
|
1472
|
-
.option("--repository <owner/repo>", "public GitHub skill repository", "obra/superpowers")
|
|
1473
|
-
.option("--package <id>", "package identifier", "obra-superpowers")
|
|
1474
|
-
.option("--agents <ids>", "comma-separated virtual demo targets", "codex")
|
|
1475
|
-
.option("--keep", "retain the isolated profile after install for inspection")
|
|
1476
|
-
.option("--json", "emit the demo result as JSON")
|
|
1477
|
-
.action(async (options) => {
|
|
1478
|
-
const result = await runIsolatedDemo({
|
|
1479
|
-
repository: options.repository,
|
|
1480
|
-
packageId: options.package,
|
|
1481
|
-
agents: parseAgentSelection(options.agents),
|
|
1482
|
-
keep: options.keep,
|
|
1483
|
-
});
|
|
1484
|
-
console.log(options.json
|
|
1485
|
-
? JSON.stringify(result, null, 2)
|
|
1486
|
-
: formatDemoResult(result));
|
|
1487
|
-
});
|
|
1488
1473
|
program
|
|
1489
1474
|
.command("capabilities")
|
|
1490
1475
|
.description("Show honest native, adapted, and unsupported adapter capabilities")
|
|
@@ -2150,6 +2135,7 @@ program
|
|
|
2150
2135
|
.command("mcp-recipe")
|
|
2151
2136
|
.description("Preview/configure a reviewed MCP recipe, or explicitly verify its real connection")
|
|
2152
2137
|
.argument("[id]", "recipe id; omit to list reviewed recipes")
|
|
2138
|
+
.option("--agent <id>", "target host: codex or claude-code; selects its default config format and path")
|
|
2153
2139
|
.option("--config <path>", "target JSON MCP config path")
|
|
2154
2140
|
.option("--yes", "write the reviewed server entry after preview")
|
|
2155
2141
|
.option("--verify", "verify the configured entry without starting a server")
|
|
@@ -2176,11 +2162,14 @@ program
|
|
|
2176
2162
|
environment: recipe.environment,
|
|
2177
2163
|
modelApiProviders: recipe.modelApiProviders,
|
|
2178
2164
|
}));
|
|
2179
|
-
|
|
2165
|
+
const output = options.json
|
|
2180
2166
|
? JSON.stringify(listed, null, 2)
|
|
2181
2167
|
: listed
|
|
2182
2168
|
.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"}`)
|
|
2183
|
-
.join("\n")
|
|
2169
|
+
.join("\n");
|
|
2170
|
+
console.log(output);
|
|
2171
|
+
if (!options.json)
|
|
2172
|
+
console.log("\nInstall for one host (preview first):\n loadout mcp-recipe playwright --agent codex\n loadout mcp-recipe playwright --agent claude-code");
|
|
2184
2173
|
return;
|
|
2185
2174
|
}
|
|
2186
2175
|
if (options.connect) {
|
|
@@ -2210,12 +2199,41 @@ program
|
|
|
2210
2199
|
}
|
|
2211
2200
|
return;
|
|
2212
2201
|
}
|
|
2213
|
-
if (
|
|
2214
|
-
|
|
2202
|
+
if (options.agent &&
|
|
2203
|
+
options.agent !== "codex" &&
|
|
2204
|
+
options.agent !== "claude-code")
|
|
2205
|
+
throw new Error("--agent must be codex or claude-code");
|
|
2206
|
+
if (options.agent && options.config)
|
|
2207
|
+
throw new Error("Choose --agent for a managed default path or --config for an explicit JSON path, not both");
|
|
2208
|
+
const configPath = options.config ??
|
|
2209
|
+
(options.agent === "codex"
|
|
2210
|
+
? defaultCodexMcpConfigPath()
|
|
2211
|
+
: options.agent === "claude-code"
|
|
2212
|
+
? join(userHome(), ".claude.json")
|
|
2213
|
+
: undefined);
|
|
2214
|
+
if (!configPath)
|
|
2215
|
+
throw new Error("Choose --agent codex, --agent claude-code, or an explicit --config path");
|
|
2215
2216
|
if (options.verify) {
|
|
2216
2217
|
if (options.yes)
|
|
2217
2218
|
throw new Error("--verify cannot be combined with --yes");
|
|
2218
|
-
|
|
2219
|
+
if (options.agent === "codex") {
|
|
2220
|
+
const recipe = findMcpRecipe(id);
|
|
2221
|
+
let content = "";
|
|
2222
|
+
try {
|
|
2223
|
+
content = await readFile(configPath, "utf8");
|
|
2224
|
+
}
|
|
2225
|
+
catch {
|
|
2226
|
+
// Missing config is rendered as a normal not-configured result.
|
|
2227
|
+
}
|
|
2228
|
+
const configured = Boolean(codexMcpServerFingerprint(content, recipe.serverName));
|
|
2229
|
+
console.log(options.json
|
|
2230
|
+
? JSON.stringify({ recipeId: id, configPath, configured }, null, 2)
|
|
2231
|
+
: `${configured ? "Configured" : "Not configured"}: ${id}\nCodex config: ${configPath}\nConfiguration presence does not launch the MCP server; add --connect --approve-risk for a bounded handshake.`);
|
|
2232
|
+
if (!configured)
|
|
2233
|
+
process.exitCode = 1;
|
|
2234
|
+
return;
|
|
2235
|
+
}
|
|
2236
|
+
const verification = await verifyMcpRecipe(id, configPath);
|
|
2219
2237
|
console.log(options.json
|
|
2220
2238
|
? JSON.stringify(verification, null, 2)
|
|
2221
2239
|
: `${verification.configured ? "Configured" : "Not configured"}: ${verification.recipeId}\n${[...verification.checks, ...verification.warnings].join("\n")}`);
|
|
@@ -2225,21 +2243,82 @@ program
|
|
|
2225
2243
|
}
|
|
2226
2244
|
const recipe = findMcpRecipe(id);
|
|
2227
2245
|
const credentialReferences = parseMcpCredentialMappings(recipe, options.credential, options.credentialAccount);
|
|
2228
|
-
|
|
2246
|
+
if (options.agent === "codex") {
|
|
2247
|
+
const server = buildMcpRecipeServer(id, configPath, {
|
|
2248
|
+
credentialReferences,
|
|
2249
|
+
requireResolvedCredentials: Boolean(options.yes),
|
|
2250
|
+
});
|
|
2251
|
+
const plan = await planCodexMcpConfig(configPath, server, server.name);
|
|
2252
|
+
const recipe = findMcpRecipe(id);
|
|
2253
|
+
const preview = [
|
|
2254
|
+
`MCP recipe: ${recipe.displayName}`,
|
|
2255
|
+
`Target: Codex`,
|
|
2256
|
+
`Config: ${plan.path}`,
|
|
2257
|
+
`Change: ${plan.summary}`,
|
|
2258
|
+
`Artifact: ${recipe.artifact}`,
|
|
2259
|
+
`Permissions: ${recipe.permissions.join(", ")}`,
|
|
2260
|
+
recipe.environment.length
|
|
2261
|
+
? `Credential references required: ${recipe.environment.join(", ")}`
|
|
2262
|
+
: "Credentials: none",
|
|
2263
|
+
].join("\n");
|
|
2264
|
+
if (!options.yes) {
|
|
2265
|
+
console.log(options.json
|
|
2266
|
+
? JSON.stringify({
|
|
2267
|
+
recipe,
|
|
2268
|
+
agent: "codex",
|
|
2269
|
+
path: plan.path,
|
|
2270
|
+
summary: plan.summary,
|
|
2271
|
+
}, null, 2)
|
|
2272
|
+
: `${preview}\nDry run only. Re-run with --yes to add this reviewed Codex MCP server.`);
|
|
2273
|
+
return;
|
|
2274
|
+
}
|
|
2275
|
+
const fingerprint = codexMcpServerFingerprint(plan.proposed, recipe.serverName);
|
|
2276
|
+
if (!fingerprint)
|
|
2277
|
+
throw new Error("Could not fingerprint the applied Codex MCP entry");
|
|
2278
|
+
const snapshot = await applyCodexMcpConfigPlan(plan, {
|
|
2279
|
+
extraTargets: [installStatePath()],
|
|
2280
|
+
afterApply: async (_result, transactionSnapshot) => recordMcpInstall({
|
|
2281
|
+
packageId: `mcp-recipe:${id}:codex`,
|
|
2282
|
+
configPath: plan.path,
|
|
2283
|
+
serverName: recipe.serverName,
|
|
2284
|
+
fingerprint,
|
|
2285
|
+
snapshotId: transactionSnapshot.id,
|
|
2286
|
+
installedAt: new Date().toISOString(),
|
|
2287
|
+
configFormat: "codex-toml",
|
|
2288
|
+
}),
|
|
2289
|
+
});
|
|
2290
|
+
console.log(options.json
|
|
2291
|
+
? JSON.stringify({
|
|
2292
|
+
recipe,
|
|
2293
|
+
agent: "codex",
|
|
2294
|
+
snapshotId: snapshot.rollbackSnapshotId,
|
|
2295
|
+
}, null, 2)
|
|
2296
|
+
: `${preview}\nConfigured for Codex. Managed package: mcp-recipe:${id}:codex\nRollback: loadout rollback --snapshot ${snapshot.rollbackSnapshotId}\nNext: loadout mcp-recipe ${id} --agent codex --verify`);
|
|
2297
|
+
return;
|
|
2298
|
+
}
|
|
2299
|
+
const plan = await planMcpRecipe(id, configPath, {
|
|
2229
2300
|
credentialReferences,
|
|
2230
2301
|
requireResolvedCredentials: Boolean(options.yes),
|
|
2231
2302
|
});
|
|
2232
2303
|
if (!options.yes) {
|
|
2304
|
+
const target = options.agent === "claude-code" ? "Target: Claude Code\n" : "";
|
|
2233
2305
|
console.log(options.json
|
|
2234
|
-
? JSON.stringify(plan, null, 2)
|
|
2235
|
-
: `${formatMcpRecipePlan(plan)}\nDry run only. Re-run with --yes to write this server entry.`);
|
|
2306
|
+
? JSON.stringify(options.agent ? { agent: options.agent, ...plan } : plan, null, 2)
|
|
2307
|
+
: `${target}${formatMcpRecipePlan(plan)}\nDry run only. Re-run with --yes to write this server entry.`);
|
|
2236
2308
|
return;
|
|
2237
2309
|
}
|
|
2238
|
-
const
|
|
2239
|
-
|
|
2310
|
+
const managedId = options.agent
|
|
2311
|
+
? `mcp-recipe:${id}:${options.agent}`
|
|
2312
|
+
: `mcp-recipe:${id}`;
|
|
2313
|
+
const snapshot = await applyMcpConfigPlan(plan.config, {
|
|
2314
|
+
extraTargets: [installStatePath()],
|
|
2315
|
+
afterApply: async (_result, transactionSnapshot) => {
|
|
2316
|
+
await recordInstallTransaction([], [{ packageId: managedId, plan: plan.config }], transactionSnapshot.id);
|
|
2317
|
+
},
|
|
2318
|
+
});
|
|
2240
2319
|
console.log(options.json
|
|
2241
2320
|
? JSON.stringify({ plan, snapshot }, null, 2)
|
|
2242
|
-
: `${formatMcpRecipePlan(plan)}\nConfigured.
|
|
2321
|
+
: `${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`);
|
|
2243
2322
|
});
|
|
2244
2323
|
program
|
|
2245
2324
|
.command("mcp")
|
|
@@ -2439,7 +2518,7 @@ program
|
|
|
2439
2518
|
return;
|
|
2440
2519
|
}
|
|
2441
2520
|
const snapshot = await applyMcpConfigPlan(plan);
|
|
2442
|
-
console.log(`Applied successfully. Snapshot: ${snapshot.
|
|
2521
|
+
console.log(`Applied successfully. Snapshot: ${snapshot.rollbackSnapshotId}`);
|
|
2443
2522
|
});
|
|
2444
2523
|
program
|
|
2445
2524
|
.command("codex-mcp-config")
|
|
@@ -2478,7 +2557,7 @@ program
|
|
|
2478
2557
|
if (!options.yes)
|
|
2479
2558
|
return console.log("Dry run only. Re-run with --yes to add this Codex MCP server.");
|
|
2480
2559
|
const snapshot = await applyCodexMcpConfigPlan(plan);
|
|
2481
|
-
console.log(`Applied successfully. Snapshot: ${snapshot.
|
|
2560
|
+
console.log(`Applied successfully. Snapshot: ${snapshot.rollbackSnapshotId}`);
|
|
2482
2561
|
});
|
|
2483
2562
|
program
|
|
2484
2563
|
.command("plan")
|
|
@@ -2575,6 +2654,7 @@ program
|
|
|
2575
2654
|
.option("--agents <ids>", "comma-separated agent ids; defaults to all detected agents")
|
|
2576
2655
|
.option("--yes", "apply without interactive confirmation")
|
|
2577
2656
|
.option("--approve-risk", "approve reviewed safety findings for catalog mode")
|
|
2657
|
+
.option("--details", "show every quarantined and deferred unit")
|
|
2578
2658
|
.action(async (options) => {
|
|
2579
2659
|
const packageIds = options.package ?? [];
|
|
2580
2660
|
const hasSource = Boolean(options.source || options.repository);
|
|
@@ -2589,7 +2669,9 @@ program
|
|
|
2589
2669
|
requestedAgents: parseAgentSelection(options.agents),
|
|
2590
2670
|
onProgress: printSetupProgress,
|
|
2591
2671
|
});
|
|
2592
|
-
console.log(formatPreparedCatalogInstall(prepared
|
|
2672
|
+
console.log(formatPreparedCatalogInstall(prepared, {
|
|
2673
|
+
details: options.details,
|
|
2674
|
+
}));
|
|
2593
2675
|
if (!options.yes) {
|
|
2594
2676
|
console.log(formatCatalogApplyGuidance(prepared.entries.some((entry) => entry.safety.approvalRequired)));
|
|
2595
2677
|
return;
|
|
@@ -2662,8 +2744,15 @@ program
|
|
|
2662
2744
|
const snapshotIds = await listSnapshotIds();
|
|
2663
2745
|
if (!snapshotIds.length)
|
|
2664
2746
|
return console.log("No Loadout snapshots found.");
|
|
2665
|
-
|
|
2666
|
-
|
|
2747
|
+
console.log("Loadout rollback history (oldest to newest):");
|
|
2748
|
+
for (const [index, id] of snapshotIds.entries()) {
|
|
2749
|
+
const summary = summarizeSnapshot(await readSnapshot(id));
|
|
2750
|
+
const impact = summary.changedEntries === undefined
|
|
2751
|
+
? "legacy impact unknown"
|
|
2752
|
+
: `${summary.changedEntries} changed filesystem entr${summary.changedEntries === 1 ? "y" : "ies"}`;
|
|
2753
|
+
console.log(`${index === snapshotIds.length - 1 ? "latest" : " "} ${summary.createdAt} ${summary.label} ${impact} ${summary.roots} root(s) ${id}`);
|
|
2754
|
+
}
|
|
2755
|
+
console.log("Restore an exact entry with: loadout rollback --snapshot <id>");
|
|
2667
2756
|
return;
|
|
2668
2757
|
}
|
|
2669
2758
|
const selected = await withMutationLock(async () => {
|
|
@@ -2671,12 +2760,15 @@ program
|
|
|
2671
2760
|
const chosen = options.snapshot ?? snapshotIds.at(-1);
|
|
2672
2761
|
if (!chosen)
|
|
2673
2762
|
throw new Error("No Loadout snapshots found");
|
|
2674
|
-
|
|
2763
|
+
const snapshot = await readSnapshot(chosen);
|
|
2764
|
+
await restoreSnapshot(snapshot, {
|
|
2675
2765
|
requireUnchangedPostMutationState: true,
|
|
2676
2766
|
});
|
|
2677
|
-
return
|
|
2767
|
+
return summarizeSnapshot(snapshot);
|
|
2678
2768
|
});
|
|
2679
|
-
console.log(`Restored snapshot ${selected}
|
|
2769
|
+
console.log(`Restored ${selected.label} snapshot ${selected.id} (${selected.changedEntries ?? "unknown"} recorded changed filesystem entries across ${selected.roots} root(s)).`);
|
|
2770
|
+
if (selected.changedEntries === 0)
|
|
2771
|
+
console.log("This snapshot recorded no effective filesystem change. Choose an older explicit snapshot if you meant to undo an earlier installation.");
|
|
2680
2772
|
});
|
|
2681
2773
|
program
|
|
2682
2774
|
.command("update")
|
|
@@ -2844,26 +2936,6 @@ program
|
|
|
2844
2936
|
if (result.status === "blocked")
|
|
2845
2937
|
process.exitCode = 1;
|
|
2846
2938
|
});
|
|
2847
|
-
program
|
|
2848
|
-
.command("dashboard")
|
|
2849
|
-
.description("Start the local Loadout dashboard on a loopback-only port")
|
|
2850
|
-
.option("--port <port>", "TCP port (0 selects an available port)", "0")
|
|
2851
|
-
.action(async (options) => {
|
|
2852
|
-
const port = Number(options.port);
|
|
2853
|
-
if (!Number.isInteger(port) || port < 0 || port > 65_535)
|
|
2854
|
-
throw new Error("--port must be an integer between 0 and 65535");
|
|
2855
|
-
const handle = await startDashboardServer({}, port);
|
|
2856
|
-
console.log(`Loadout dashboard: http://${handle.host}:${handle.port}`);
|
|
2857
|
-
await new Promise((resolve) => {
|
|
2858
|
-
const stop = () => {
|
|
2859
|
-
process.off("SIGINT", stop);
|
|
2860
|
-
process.off("SIGTERM", stop);
|
|
2861
|
-
void handle.close().then(resolve);
|
|
2862
|
-
};
|
|
2863
|
-
process.on("SIGINT", stop);
|
|
2864
|
-
process.on("SIGTERM", stop);
|
|
2865
|
-
});
|
|
2866
|
-
});
|
|
2867
2939
|
program
|
|
2868
2940
|
.command("serve")
|
|
2869
2941
|
.description("Start a loopback-only read-only API for status, health, catalog, and updates")
|