loadout-ai 0.2.3 → 0.4.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 +71 -0
- package/MASTER_PLAN.md +141 -33
- package/README.md +160 -255
- package/dashboard/app.js +4 -4
- package/dashboard/index.html +4 -4
- package/dist/src/cli.js +189 -32
- package/dist/src/core/active-set.js +37 -2
- package/dist/src/core/adapters.js +10 -0
- package/dist/src/core/adopt.js +165 -32
- package/dist/src/core/agent-health-score.js +2 -2
- package/dist/src/core/catalog-coverage.js +2 -1
- package/dist/src/core/catalog-install.js +8 -1
- package/dist/src/core/catalog-release.js +2 -1
- package/dist/src/core/cli-guide.js +101 -0
- package/dist/src/core/completion.js +3 -0
- package/dist/src/core/conformance.js +74 -0
- package/dist/src/core/install.js +36 -3
- package/dist/src/core/mcp-recipes.js +21 -0
- package/dist/src/core/profile-state.js +101 -0
- package/dist/src/core/profiles.js +9 -4
- package/dist/src/core/ranking.js +1 -1
- package/dist/src/core/readme-claims.js +10 -0
- package/dist/src/core/readme-facts.js +40 -0
- package/dist/src/core/recommend.js +9 -3
- package/dist/src/core/remove.js +6 -7
- package/dist/src/core/runtime-tools.js +10 -2
- package/dist/src/core/scheduler.js +4 -3
- package/dist/src/core/snapshot.js +58 -13
- package/dist/src/core/state.js +13 -3
- package/dist/src/core/transaction.js +2 -1
- package/dist/src/core/uninstall.js +133 -0
- package/dist/src/core/update.js +87 -58
- package/dist/src/dashboard.js +5 -2
- package/dist/src/shared/schemas.js +69 -0
- package/docs/FEATURE_TEST_MATRIX.md +16 -0
- package/docs/README_RESEARCH.md +36 -0
- package/docs/RELEASE_REVIEW.md +31 -5
- package/docs/REPOSITORY_STABILIZATION.md +190 -0
- package/docs/TESTING.md +75 -2
- package/docs/USER_TEST_GUIDE.md +202 -0
- package/docs/assets/loadout-hero.svg +259 -0
- package/docs/assets/loadout-mark.svg +54 -0
- package/docs/evidence/live-checks-2026-07-19.json +22 -0
- package/docs/evidence/live-checks.schema.json +28 -0
- package/docs/evidence/readme-claims.json +286 -0
- package/docs/superpowers/plans/2026-07-19-relatable-readme-hero.md +283 -0
- package/docs/superpowers/specs/2026-07-19-relatable-readme-hero-design.md +80 -0
- package/package.json +8 -4
- package/SIMPLE_PLAN.md +0 -44
package/dist/src/cli.js
CHANGED
|
@@ -12,14 +12,16 @@ import { fetchRepositorySnapshot } from "./core/source.js";
|
|
|
12
12
|
import { discoverMcpManifests, summarizeMcpManifest, planMcpConfig, summarizeMcpConfigPlan, applyMcpConfigPlan, } from "./core/mcp.js";
|
|
13
13
|
import { REVIEWED_MCP_RECIPES, findMcpRecipe, formatMcpRecipePlan, planMcpRecipe, verifyMcpRecipe, verifyMcpRecipeConnection, } from "./core/mcp-recipes.js";
|
|
14
14
|
import { runDoctor, formatDoctorReport } from "./core/doctor.js";
|
|
15
|
-
import { applyPackageUpdate, buildUpdatePlan, formatUpdatePlan, } from "./core/update.js";
|
|
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 } from "./core/state.js";
|
|
20
|
+
import { readInstallState, recordInstallTransaction } from "./core/state.js";
|
|
21
21
|
import { applyRemove, planRemove } from "./core/remove.js";
|
|
22
|
-
import {
|
|
22
|
+
import { applyUninstall, buildUninstallPlan, formatUninstallPlan, uninstallGlobalCli, } from "./core/uninstall.js";
|
|
23
|
+
import { evaluateInstalledProfile, formatInstalledProfileStatus, } from "./core/profile-state.js";
|
|
24
|
+
import { formatRecommendations, personalizeRecommendations, profileManifestPackages, recommendPackages, RECOMMENDATION_BOUNDARY, scanProject, TESTED_PROFILES, } from "./core/recommend.js";
|
|
23
25
|
import { buildImprovementCycle, formatImprovementCycle, recordImprovementOutcome, writeImprovementCycle, } from "./core/improve.js";
|
|
24
26
|
import { applySyncPlan, buildSyncPlan } from "./core/sync.js";
|
|
25
27
|
import { createPackage, packPackage, publishLocalPackage, publishRemotePackage, searchLocalRegistry, } from "./core/registry.js";
|
|
@@ -31,7 +33,7 @@ import { generateSigningKeys, signJsonFile, verifyJsonFile, } from "./core/signi
|
|
|
31
33
|
import { applyPortableImport, exportPortableLoadout, planPortableImport, } from "./core/portable.js";
|
|
32
34
|
import { applyCodexMcpConfigPlan, defaultCodexMcpConfigPath, planCodexMcpConfig, } from "./core/codex-mcp.js";
|
|
33
35
|
import { formatDemoResult, runIsolatedDemo } from "./core/demo.js";
|
|
34
|
-
import { catalogTrustStage, resolveCatalogProfile } from "./core/profiles.js";
|
|
36
|
+
import { catalogTrustStage, formatCatalogTrustStage, resolveCatalogProfile, } from "./core/profiles.js";
|
|
35
37
|
import { discoverHackerNewsRepositories } from "./core/community.js";
|
|
36
38
|
import { discoverPrivateRepositories } from "./core/private-discovery.js";
|
|
37
39
|
import { defaultGitHubDiscoveryQueries, discoverGitHubRepositories, } from "./core/github-discovery.js";
|
|
@@ -43,11 +45,11 @@ import { compileConversion, } from "./core/conversion.js";
|
|
|
43
45
|
import { writeFileAtomically } from "./core/atomic-file.js";
|
|
44
46
|
import { formatCanaryResult, runCanary } from "./core/canary.js";
|
|
45
47
|
import { startDashboardServer } from "./dashboard.js";
|
|
46
|
-
import { applyPreparedCatalogInstall, formatPreparedCatalogInstall, prepareCatalogInstall, } from "./core/catalog-install.js";
|
|
48
|
+
import { applyPreparedCatalogInstall, formatCatalogApplyGuidance, formatPreparedCatalogInstall, prepareCatalogInstall, } from "./core/catalog-install.js";
|
|
47
49
|
import { formatInstalledSkillInventory, scanInstalledSkills, } from "./core/skill-inventory.js";
|
|
48
50
|
import { enrichInventoryWithProvenance, formatProvenanceSummary, resolveCatalogSkillIndex, } from "./core/provenance.js";
|
|
49
51
|
import { compareSkill, formatSkillComparison } from "./core/skill-compare.js";
|
|
50
|
-
import { applyActivationChange, buildLibraryStateReport, formatActivationPlan, formatLibraryStateReport, planActivationChange, } from "./core/active-set.js";
|
|
52
|
+
import { applyActivationChange, buildLibraryStateReport, formatActivationPlan, formatLibrarySummary, formatLibraryStateReport, planActivationChange, } from "./core/active-set.js";
|
|
51
53
|
import { applyProjectActivation, formatProjectActivation, planProjectActivation, } from "./core/active-policy.js";
|
|
52
54
|
import { applySkillAdoption, formatAdoptionPlan, planSkillAdoption, } from "./core/adopt.js";
|
|
53
55
|
import { formatReviewQueue, mergeReviewQueue, readReviewQueue, setReviewDecision, } from "./core/review-queue.js";
|
|
@@ -79,6 +81,7 @@ import { applyIntelligenceFeed, previewIntelligenceFeed, readCachedIntelligenceF
|
|
|
79
81
|
import { scanSkillSecurity } from "./core/skill-security.js";
|
|
80
82
|
import { planApmImportFiles, planOpenPackageImportFiles, } from "./core/ecosystem-import.js";
|
|
81
83
|
import { buildCompatibilityIntelligence, parseCompatibilityNoticeSet, } from "./core/compatibility-intelligence.js";
|
|
84
|
+
import { ADVANCED_GUIDE, BEGINNER_GUIDE, HIDDEN_FROM_FIRST_SCREEN, } from "./core/cli-guide.js";
|
|
82
85
|
const collectOption = (value, previous = []) => [
|
|
83
86
|
...previous,
|
|
84
87
|
value,
|
|
@@ -180,14 +183,14 @@ async function runSetup(options) {
|
|
|
180
183
|
try {
|
|
181
184
|
if (!mode) {
|
|
182
185
|
if (!interactive) {
|
|
183
|
-
console.log("Loadout is CLI-first. Run `loadout setup --mode stable` for
|
|
186
|
+
console.log("Loadout is CLI-first. Run `loadout setup --mode stable` for Loadout's bounded policy selection, `--mode power` for broader opt-in skills, or `--mode maximum` to download the screened library without activating it.");
|
|
184
187
|
return;
|
|
185
188
|
}
|
|
186
189
|
reader = createInterface({
|
|
187
190
|
input: process.stdin,
|
|
188
191
|
output: process.stdout,
|
|
189
192
|
});
|
|
190
|
-
const answer = (await reader.question("Choose a loadout: [1] Stable Daily Driver (
|
|
193
|
+
const answer = (await reader.question("Choose a loadout: [1] Stable Daily Driver (Loadout policy selection), [2] Power Boost, [3] Maximum Library, [4] Custom: ")).trim();
|
|
191
194
|
mode =
|
|
192
195
|
answer === "2"
|
|
193
196
|
? "power"
|
|
@@ -225,7 +228,7 @@ async function runSetup(options) {
|
|
|
225
228
|
let riskApproved = Boolean(options.approveRisk);
|
|
226
229
|
if (!approved) {
|
|
227
230
|
if (!interactive) {
|
|
228
|
-
console.log(
|
|
231
|
+
console.log(formatCatalogApplyGuidance(Boolean(risky)));
|
|
229
232
|
return;
|
|
230
233
|
}
|
|
231
234
|
reader ??= createInterface({
|
|
@@ -262,7 +265,7 @@ async function runSetup(options) {
|
|
|
262
265
|
reader?.close();
|
|
263
266
|
}
|
|
264
267
|
}
|
|
265
|
-
const LOADOUT_VERSION = "0.
|
|
268
|
+
const LOADOUT_VERSION = "0.4.0";
|
|
266
269
|
function durableSchedulerLauncher() {
|
|
267
270
|
return [
|
|
268
271
|
join(dirname(process.execPath), process.platform === "win32" ? "npx.cmd" : "npx"),
|
|
@@ -271,6 +274,9 @@ function durableSchedulerLauncher() {
|
|
|
271
274
|
];
|
|
272
275
|
}
|
|
273
276
|
const program = new Command();
|
|
277
|
+
function printBeginnerGuide() {
|
|
278
|
+
console.log(BEGINNER_GUIDE);
|
|
279
|
+
}
|
|
274
280
|
program
|
|
275
281
|
.name("loadout")
|
|
276
282
|
.description("The trusted upgrade layer for AI coding agents")
|
|
@@ -285,6 +291,14 @@ program
|
|
|
285
291
|
// writes its own parse error first, producing two stderr documents and
|
|
286
292
|
// breaking --json-errors consumers.
|
|
287
293
|
.configureOutput({ writeErr: () => undefined });
|
|
294
|
+
program
|
|
295
|
+
.command("guide")
|
|
296
|
+
.description("Show the simple, read-only path for using Loadout")
|
|
297
|
+
.action(printBeginnerGuide);
|
|
298
|
+
program
|
|
299
|
+
.command("advanced")
|
|
300
|
+
.description("Explain where to find advanced and maintainer-only commands")
|
|
301
|
+
.action(() => console.log(ADVANCED_GUIDE));
|
|
288
302
|
program
|
|
289
303
|
.command("setup")
|
|
290
304
|
.description("Preview and install a screened skill loadout for detected agents")
|
|
@@ -620,11 +634,14 @@ program
|
|
|
620
634
|
.command("library")
|
|
621
635
|
.description("Show separate cache, review, installation, and per-agent activation state")
|
|
622
636
|
.option("--json", "emit machine-readable JSON")
|
|
637
|
+
.option("--all", "show every managed skill and its source package")
|
|
623
638
|
.action(async (options) => {
|
|
624
639
|
const report = await buildLibraryStateReport();
|
|
625
640
|
console.log(options.json
|
|
626
641
|
? JSON.stringify(report, null, 2)
|
|
627
|
-
:
|
|
642
|
+
: options.all
|
|
643
|
+
? formatLibraryStateReport(report)
|
|
644
|
+
: formatLibrarySummary(report));
|
|
628
645
|
});
|
|
629
646
|
program
|
|
630
647
|
.command("report")
|
|
@@ -712,8 +729,8 @@ for (const workflow of ["activate", "optimize"]) {
|
|
|
712
729
|
program
|
|
713
730
|
.command(workflow)
|
|
714
731
|
.description(workflow === "activate"
|
|
715
|
-
? "Select and activate
|
|
716
|
-
: "Scan a project and propose
|
|
732
|
+
? "Select and activate inspected library skills for a project"
|
|
733
|
+
: "Scan a project and propose rule-selected inspected active-set additions")
|
|
717
734
|
.option("--project <path>", "project directory to scan", ".")
|
|
718
735
|
.option("--agents <ids>", "comma-separated agent ids")
|
|
719
736
|
.option("--limit <count>", "maximum active skills per agent", "40")
|
|
@@ -891,7 +908,17 @@ program
|
|
|
891
908
|
.option("--explain", "add deterministic score dimensions, evidence, uncertainty, and remediation")
|
|
892
909
|
.option("--agents <ids>", "limit explained scores to selected agent ids")
|
|
893
910
|
.action(async (options) => {
|
|
894
|
-
|
|
911
|
+
if (options.updates && !options.json)
|
|
912
|
+
console.error("Checking managed repositories (4 at a time, 30s per network operation)…");
|
|
913
|
+
const report = await buildHealthReport({
|
|
914
|
+
updates: options.updates
|
|
915
|
+
? () => buildUpdatePlan(undefined, {
|
|
916
|
+
onProgress: options.json
|
|
917
|
+
? undefined
|
|
918
|
+
: ({ completed, total, packageId }) => console.error(`✓ [${completed}/${total}] ${packageId}`),
|
|
919
|
+
})
|
|
920
|
+
: undefined,
|
|
921
|
+
});
|
|
895
922
|
if (options.agents && !options.explain)
|
|
896
923
|
throw new Error("--agents requires --explain");
|
|
897
924
|
const selectedAgents = parseAgentSelection(options.agents);
|
|
@@ -985,6 +1012,29 @@ program
|
|
|
985
1012
|
const snapshot = await applyRemove(plan, { force: options.force });
|
|
986
1013
|
console.log(`Removed ${packageId}. Snapshot: ${snapshot}`);
|
|
987
1014
|
});
|
|
1015
|
+
program
|
|
1016
|
+
.command("uninstall")
|
|
1017
|
+
.description("Preview or completely remove Loadout-managed data")
|
|
1018
|
+
.option("--yes", "remove managed data; otherwise show a preview")
|
|
1019
|
+
.option("--force", "also remove managed files changed outside Loadout")
|
|
1020
|
+
.option("--remove-cli", "also uninstall the global loadout-ai npm command")
|
|
1021
|
+
.action(async (options) => {
|
|
1022
|
+
const plan = await buildUninstallPlan();
|
|
1023
|
+
console.log(formatUninstallPlan(plan));
|
|
1024
|
+
if (!options.yes)
|
|
1025
|
+
return;
|
|
1026
|
+
const result = await applyUninstall(plan, undefined, {
|
|
1027
|
+
force: options.force,
|
|
1028
|
+
});
|
|
1029
|
+
console.log(`Removed ${result.removedPackages} managed package(s), ${result.removedRuntimeTools} runtime tool(s), daily jobs, cache, library, and Loadout state.`);
|
|
1030
|
+
if (options.removeCli) {
|
|
1031
|
+
await uninstallGlobalCli();
|
|
1032
|
+
console.log("Removed the global loadout-ai CLI. Open a new shell before checking the command again.");
|
|
1033
|
+
}
|
|
1034
|
+
else {
|
|
1035
|
+
console.log("The CLI is still installed. Remove it too with: npm uninstall -g loadout-ai");
|
|
1036
|
+
}
|
|
1037
|
+
});
|
|
988
1038
|
program
|
|
989
1039
|
.command("recommend")
|
|
990
1040
|
.description("Recommend catalog packages from local project signals")
|
|
@@ -1004,6 +1054,7 @@ program
|
|
|
1004
1054
|
? JSON.stringify({
|
|
1005
1055
|
signals,
|
|
1006
1056
|
recommendations,
|
|
1057
|
+
recommendationBoundary: RECOMMENDATION_BOUNDARY,
|
|
1007
1058
|
personalization: options.agent
|
|
1008
1059
|
? {
|
|
1009
1060
|
agent: options.agent,
|
|
@@ -1015,7 +1066,7 @@ program
|
|
|
1015
1066
|
});
|
|
1016
1067
|
program
|
|
1017
1068
|
.command("profiles")
|
|
1018
|
-
.description("List
|
|
1069
|
+
.description("List Loadout policy profiles or inspect one profile")
|
|
1019
1070
|
.argument("[name]", "profile name")
|
|
1020
1071
|
.option("--json", "emit machine-readable JSON")
|
|
1021
1072
|
.option("--apply-to <path>", "add the selected profile packages to a manifest")
|
|
@@ -1132,7 +1183,7 @@ program
|
|
|
1132
1183
|
.description("Take ownership of one explicitly selected installed skill without changing its bytes")
|
|
1133
1184
|
.argument("<skill>", "installed skill name, directory name, or exact path")
|
|
1134
1185
|
.requiredOption("--agent <id>", "agent that owns the installed skill")
|
|
1135
|
-
.option("--refresh-provenance", "rebuild the
|
|
1186
|
+
.option("--refresh-provenance", "rebuild the inspected catalog skill index")
|
|
1136
1187
|
.option("--yes", "record ownership; otherwise show a dry-run plan")
|
|
1137
1188
|
.option("--json", "emit machine-readable JSON")
|
|
1138
1189
|
.action(async (skill, options) => {
|
|
@@ -1167,7 +1218,7 @@ program
|
|
|
1167
1218
|
.description("Compare an installed or reviewed skill with evidence-related catalog alternatives without changing anything")
|
|
1168
1219
|
.argument("<skill>", "installed skill name, directory name, or catalog package id")
|
|
1169
1220
|
.option("--agent <id>", "select one installed agent when names are ambiguous")
|
|
1170
|
-
.option("--refresh", "rebuild the
|
|
1221
|
+
.option("--refresh", "rebuild the inspected catalog skill index")
|
|
1171
1222
|
.option("--offline", "never fetch; use the existing local index only")
|
|
1172
1223
|
.option("--limit <count>", "maximum alternatives to show", "10")
|
|
1173
1224
|
.option("--json", "emit the complete machine-readable comparison")
|
|
@@ -1508,6 +1559,7 @@ program
|
|
|
1508
1559
|
category: pkg.category,
|
|
1509
1560
|
tier: pkg.tier,
|
|
1510
1561
|
trustStage: catalogTrustStage(pkg),
|
|
1562
|
+
evidenceLabel: formatCatalogTrustStage(catalogTrustStage(pkg)),
|
|
1511
1563
|
},
|
|
1512
1564
|
ranking: explainCatalogScore(pkg),
|
|
1513
1565
|
}, null, 2));
|
|
@@ -1520,12 +1572,16 @@ program
|
|
|
1520
1572
|
: formatCatalogCoverage(coverage));
|
|
1521
1573
|
return;
|
|
1522
1574
|
}
|
|
1575
|
+
if (options.json) {
|
|
1576
|
+
console.log(JSON.stringify(rankCatalog(result.catalog), null, 2));
|
|
1577
|
+
return;
|
|
1578
|
+
}
|
|
1523
1579
|
for (const pkg of rankCatalog(result.catalog)) {
|
|
1524
1580
|
const topics = pkg.topics?.length ? ` — ${pkg.topics.join(", ")}` : "";
|
|
1525
1581
|
const updated = pkg.lastUpdatedAt
|
|
1526
1582
|
? ` — updated ${pkg.lastUpdatedAt.slice(0, 10)}`
|
|
1527
1583
|
: "";
|
|
1528
|
-
console.log(`${pkg.displayName} [${pkg.tier}; ${catalogTrustStage(pkg)}] ★${pkg.stars ?? "?"} — ${pkg.repository}${topics}${updated}`);
|
|
1584
|
+
console.log(`${pkg.displayName} [${pkg.tier}; ${formatCatalogTrustStage(catalogTrustStage(pkg))}] ★${pkg.stars ?? "?"} — ${pkg.repository}${topics}${updated}`);
|
|
1529
1585
|
}
|
|
1530
1586
|
for (const failure of result.failures)
|
|
1531
1587
|
console.error(`Warning: could not refresh ${failure.repository}: ${failure.error}`);
|
|
@@ -1593,7 +1649,7 @@ candidate
|
|
|
1593
1649
|
.description("Convert a reviewed dossier into a catalog-record proposal; never edits the catalog")
|
|
1594
1650
|
.argument("<dossier>", "persisted candidate dossier JSON")
|
|
1595
1651
|
.requiredOption("--id <id>", "lowercase kebab-case catalog id")
|
|
1596
|
-
.requiredOption("--category <category>", "
|
|
1652
|
+
.requiredOption("--category <category>", "inspected catalog category")
|
|
1597
1653
|
.requiredOption("--platforms <ids>", "explicitly reviewed comma-separated platforms: windows,macos,linux")
|
|
1598
1654
|
.option("--display-name <name>", "reviewed display name")
|
|
1599
1655
|
.option("--description <text>", "reviewed description")
|
|
@@ -2103,21 +2159,27 @@ program
|
|
|
2103
2159
|
.option("--timeout <milliseconds>", "real connection timeout", "8000")
|
|
2104
2160
|
.option("--approve-risk", "approve launching the reviewed pinned MCP artifact for --connect")
|
|
2105
2161
|
.option("--json", "emit machine-readable JSON")
|
|
2162
|
+
.option("--no-key", "list recipes needing no separately billed AI/model API key")
|
|
2163
|
+
.option("--no-model-key", "clear alias for --no-key; service tokens may still be required")
|
|
2164
|
+
.option("--credential-free", "list recipes needing no credential of any kind")
|
|
2106
2165
|
.action(async (id, options) => {
|
|
2107
2166
|
if (!id) {
|
|
2108
2167
|
if (options.connect || options.verify || options.yes)
|
|
2109
2168
|
throw new Error("Select an MCP recipe id for this operation");
|
|
2110
|
-
const
|
|
2169
|
+
const selected = REVIEWED_MCP_RECIPES.filter((recipe) => !((options.key === false || options.modelKey === false) &&
|
|
2170
|
+
recipe.modelApiProviders.length > 0) && !(options.credentialFree && recipe.environment.length > 0));
|
|
2171
|
+
const listed = selected.map((recipe) => ({
|
|
2111
2172
|
id: recipe.id,
|
|
2112
2173
|
displayName: recipe.displayName,
|
|
2113
2174
|
source: recipe.source,
|
|
2114
2175
|
permissions: recipe.permissions,
|
|
2115
2176
|
environment: recipe.environment,
|
|
2177
|
+
modelApiProviders: recipe.modelApiProviders,
|
|
2116
2178
|
}));
|
|
2117
2179
|
console.log(options.json
|
|
2118
2180
|
? JSON.stringify(listed, null, 2)
|
|
2119
2181
|
: listed
|
|
2120
|
-
.map((recipe) => `${recipe.id} — ${recipe.displayName} —
|
|
2182
|
+
.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"}`)
|
|
2121
2183
|
.join("\n"));
|
|
2122
2184
|
return;
|
|
2123
2185
|
}
|
|
@@ -2174,6 +2236,7 @@ program
|
|
|
2174
2236
|
return;
|
|
2175
2237
|
}
|
|
2176
2238
|
const snapshot = await applyMcpConfigPlan(plan.config);
|
|
2239
|
+
await recordInstallTransaction([], [{ packageId: `mcp-recipe:${id}`, plan: plan.config }], snapshot.id);
|
|
2177
2240
|
console.log(options.json
|
|
2178
2241
|
? JSON.stringify({ plan, snapshot }, null, 2)
|
|
2179
2242
|
: `${formatMcpRecipePlan(plan)}\nConfigured. Snapshot: ${snapshot.id}\nAuthorize the service separately, then run: loadout mcp-recipe ${id} --config ${plan.config.path} --verify`);
|
|
@@ -2528,7 +2591,7 @@ program
|
|
|
2528
2591
|
});
|
|
2529
2592
|
console.log(formatPreparedCatalogInstall(prepared));
|
|
2530
2593
|
if (!options.yes) {
|
|
2531
|
-
console.log(
|
|
2594
|
+
console.log(formatCatalogApplyGuidance(prepared.entries.some((entry) => entry.safety.approvalRequired)));
|
|
2532
2595
|
return;
|
|
2533
2596
|
}
|
|
2534
2597
|
const snapshotId = await applyPreparedCatalogInstall(prepared, {
|
|
@@ -2608,30 +2671,112 @@ program
|
|
|
2608
2671
|
const chosen = options.snapshot ?? snapshotIds.at(-1);
|
|
2609
2672
|
if (!chosen)
|
|
2610
2673
|
throw new Error("No Loadout snapshots found");
|
|
2611
|
-
await restoreSnapshot(await readSnapshot(chosen)
|
|
2674
|
+
await restoreSnapshot(await readSnapshot(chosen), {
|
|
2675
|
+
requireUnchangedPostMutationState: true,
|
|
2676
|
+
});
|
|
2612
2677
|
return chosen;
|
|
2613
2678
|
});
|
|
2614
2679
|
console.log(`Restored snapshot ${selected}`);
|
|
2615
2680
|
});
|
|
2616
2681
|
program
|
|
2617
2682
|
.command("update")
|
|
2618
|
-
.description("
|
|
2683
|
+
.description("Check the saved profile and every managed package for updates")
|
|
2619
2684
|
.option("--json", "emit machine-readable JSON")
|
|
2620
|
-
.option("--
|
|
2685
|
+
.option("--yes", "apply reviewed profile drift and safe screened package updates")
|
|
2686
|
+
.option("--apply", "legacy alias for --yes with --package")
|
|
2621
2687
|
.option("--package <id>", "managed package id to update")
|
|
2622
2688
|
.option("--approve-risk", "explicitly approve an update containing blocked safety findings")
|
|
2623
2689
|
.action(async (options) => {
|
|
2624
|
-
|
|
2625
|
-
|
|
2626
|
-
|
|
2690
|
+
const applying = Boolean(options.apply || options.yes);
|
|
2691
|
+
if (options.apply && !options.package)
|
|
2692
|
+
throw new Error("--apply requires --package <id>");
|
|
2693
|
+
if (applying && options.package) {
|
|
2627
2694
|
const result = await applyPackageUpdate(options.package, {
|
|
2628
2695
|
approveRisk: options.approveRisk,
|
|
2629
2696
|
});
|
|
2630
|
-
console.log(
|
|
2697
|
+
console.log(options.json
|
|
2698
|
+
? JSON.stringify({ packageId: options.package, ...result }, null, 2)
|
|
2699
|
+
: `Updated ${options.package} to ${result.commit}. Snapshot: ${result.snapshotId}`);
|
|
2631
2700
|
return;
|
|
2632
2701
|
}
|
|
2633
|
-
const
|
|
2634
|
-
|
|
2702
|
+
const catalog = await loadEffectiveCatalog();
|
|
2703
|
+
let profile = await evaluateInstalledProfile(catalog);
|
|
2704
|
+
let profileSnapshotId;
|
|
2705
|
+
const appliedPackages = [];
|
|
2706
|
+
const failedPackages = [];
|
|
2707
|
+
if (applying && profile.installed && profile.needsRefresh) {
|
|
2708
|
+
const saved = (await readInstallState()).profile;
|
|
2709
|
+
const prepared = await prepareCatalogInstall({
|
|
2710
|
+
mode: saved.mode,
|
|
2711
|
+
...(saved.packageIds ? { packageIds: saved.packageIds } : {}),
|
|
2712
|
+
}, {
|
|
2713
|
+
requestedAgents: saved.agents,
|
|
2714
|
+
catalog,
|
|
2715
|
+
access: { modelApis: [] },
|
|
2716
|
+
onProgress: printSetupProgress,
|
|
2717
|
+
});
|
|
2718
|
+
profileSnapshotId = await applyPreparedCatalogInstall(prepared, {
|
|
2719
|
+
approveRisk: options.approveRisk,
|
|
2720
|
+
});
|
|
2721
|
+
if (!options.json)
|
|
2722
|
+
console.log(`Applied reviewed ${saved.mode} profile changes. Snapshot: ${profileSnapshotId}`);
|
|
2723
|
+
profile = await evaluateInstalledProfile(catalog);
|
|
2724
|
+
}
|
|
2725
|
+
if (!options.json)
|
|
2726
|
+
console.error("Checking managed repositories (4 at a time, 30s per network operation)…");
|
|
2727
|
+
let plans = await buildUpdatePlan(undefined, {
|
|
2728
|
+
packageId: options.package,
|
|
2729
|
+
onProgress: options.json
|
|
2730
|
+
? undefined
|
|
2731
|
+
: ({ completed, total, packageId }) => console.error(`✓ [${completed}/${total}] ${packageId}`),
|
|
2732
|
+
});
|
|
2733
|
+
if (options.package && plans.length === 0)
|
|
2734
|
+
throw new Error(`No Loadout-managed installation named '${options.package}' was found. Run \`loadout list\` to see tracked packages.`);
|
|
2735
|
+
if (applying) {
|
|
2736
|
+
const safe = selectSafeAutomaticUpdates(plans);
|
|
2737
|
+
for (const plan of safe)
|
|
2738
|
+
try {
|
|
2739
|
+
await applyPackageUpdate(plan.packageId);
|
|
2740
|
+
appliedPackages.push(plan.packageId);
|
|
2741
|
+
}
|
|
2742
|
+
catch (error) {
|
|
2743
|
+
failedPackages.push({
|
|
2744
|
+
packageId: plan.packageId,
|
|
2745
|
+
error: error instanceof Error ? error.message : String(error),
|
|
2746
|
+
});
|
|
2747
|
+
}
|
|
2748
|
+
plans = await buildUpdatePlan(undefined, {
|
|
2749
|
+
onProgress: options.json
|
|
2750
|
+
? undefined
|
|
2751
|
+
: ({ completed, total, packageId }) => console.error(`✓ [${completed}/${total}] ${packageId}`),
|
|
2752
|
+
});
|
|
2753
|
+
if (!options.json) {
|
|
2754
|
+
console.log(appliedPackages.length
|
|
2755
|
+
? `Updated safely: ${appliedPackages.join(", ")}`
|
|
2756
|
+
: "No safe active package update needed applying.");
|
|
2757
|
+
for (const item of failedPackages)
|
|
2758
|
+
console.log(`Could not update ${item.packageId}: ${item.error}`);
|
|
2759
|
+
const held = plans.filter((plan) => plan.status === "update-available" &&
|
|
2760
|
+
(plan.approvalRequired || plan.disabledAgents?.length));
|
|
2761
|
+
if (held.length)
|
|
2762
|
+
console.log(`Held for explicit review: ${held.map((item) => item.packageId).join(", ")}`);
|
|
2763
|
+
}
|
|
2764
|
+
}
|
|
2765
|
+
console.log(options.json
|
|
2766
|
+
? JSON.stringify({
|
|
2767
|
+
profile,
|
|
2768
|
+
packages: plans,
|
|
2769
|
+
...(applying
|
|
2770
|
+
? {
|
|
2771
|
+
applied: {
|
|
2772
|
+
profileSnapshotId,
|
|
2773
|
+
packages: appliedPackages,
|
|
2774
|
+
failures: failedPackages,
|
|
2775
|
+
},
|
|
2776
|
+
}
|
|
2777
|
+
: {}),
|
|
2778
|
+
}, null, 2)
|
|
2779
|
+
: `${formatInstalledProfileStatus(profile)}\n\n${formatUpdatePlan(plans)}`);
|
|
2635
2780
|
});
|
|
2636
2781
|
program
|
|
2637
2782
|
.command("convert")
|
|
@@ -2732,7 +2877,19 @@ program
|
|
|
2732
2877
|
});
|
|
2733
2878
|
await handle.close();
|
|
2734
2879
|
});
|
|
2735
|
-
|
|
2880
|
+
for (const command of program.commands)
|
|
2881
|
+
// Commander supports `hidden` when a command is created. These commands are
|
|
2882
|
+
// registered by separate feature blocks, so use the same runtime flag here
|
|
2883
|
+
// rather than `Option#hideHelp`, which does not exist on Command objects.
|
|
2884
|
+
if (HIDDEN_FROM_FIRST_SCREEN.has(command.name()))
|
|
2885
|
+
command._hidden = true;
|
|
2886
|
+
program.addHelpText("after", "\nStart here: `loadout guide` shows the safe everyday path. `loadout advanced` lists the retained maintainer and integration commands.\n");
|
|
2887
|
+
program.argument("[unknown-command]");
|
|
2888
|
+
program.action((unknownCommand) => {
|
|
2889
|
+
if (unknownCommand)
|
|
2890
|
+
throw new Error(`Unknown command '${unknownCommand}'. Run 'loadout --help' to list available commands.`);
|
|
2891
|
+
printBeginnerGuide();
|
|
2892
|
+
});
|
|
2736
2893
|
try {
|
|
2737
2894
|
await recoverPendingTransactions();
|
|
2738
2895
|
await program.parseAsync();
|
|
@@ -156,9 +156,13 @@ async function activeTreeFiles(targets) {
|
|
|
156
156
|
export async function buildLibraryStateReport() {
|
|
157
157
|
const state = await readInstallState();
|
|
158
158
|
const records = allActivationRecords(state.installs, state.activations ?? []);
|
|
159
|
+
const repositories = Object.fromEntries(state.installs
|
|
160
|
+
.filter((install) => Boolean(install.repository))
|
|
161
|
+
.map((install) => [install.packageId, install.repository]));
|
|
159
162
|
return {
|
|
160
163
|
generatedAt: new Date().toISOString(),
|
|
161
164
|
records,
|
|
165
|
+
repositories,
|
|
162
166
|
counts: {
|
|
163
167
|
packages: new Set(records.map((record) => record.packageId)).size,
|
|
164
168
|
downloaded: records.filter((record) => record.cacheState === "downloaded")
|
|
@@ -357,17 +361,48 @@ export async function applyActivationChange(plan) {
|
|
|
357
361
|
});
|
|
358
362
|
return applied.snapshotId;
|
|
359
363
|
}
|
|
364
|
+
function formatLibraryHeadline(report) {
|
|
365
|
+
return `Managed library: ${report.counts.packages} package(s), ${report.counts.active} active, ${report.counts.disabled} disabled, ${report.counts.downloaded} cached`;
|
|
366
|
+
}
|
|
367
|
+
function countByAgent(report, activationState) {
|
|
368
|
+
const counts = new Map();
|
|
369
|
+
for (const record of report.records)
|
|
370
|
+
if (record.activationState === activationState)
|
|
371
|
+
counts.set(record.agent, (counts.get(record.agent) ?? 0) + 1);
|
|
372
|
+
return [...counts]
|
|
373
|
+
.sort(([left], [right]) => left.localeCompare(right))
|
|
374
|
+
.map(([agent, count]) => `${agent} ${count}`)
|
|
375
|
+
.join(", ");
|
|
376
|
+
}
|
|
377
|
+
/** Concise default for people with a broad Maximum library. */
|
|
378
|
+
export function formatLibrarySummary(report) {
|
|
379
|
+
if (!report.records.length)
|
|
380
|
+
return "No Loadout-managed skill activations exist.";
|
|
381
|
+
return [
|
|
382
|
+
formatLibraryHeadline(report),
|
|
383
|
+
`Active now: ${countByAgent(report, "active") || "none"}`,
|
|
384
|
+
`Disabled library: ${countByAgent(report, "disabled") || "none"}`,
|
|
385
|
+
"Disabled library skills are reviewed copies, not extra agent context.",
|
|
386
|
+
...(report.migrationPending
|
|
387
|
+
? [
|
|
388
|
+
`Migration pending: ${report.migrationPending} active installation(s) will be cached on first disable.`,
|
|
389
|
+
]
|
|
390
|
+
: []),
|
|
391
|
+
"Use `loadout library --all` for every skill, source package, and activation state.",
|
|
392
|
+
].join("\n");
|
|
393
|
+
}
|
|
394
|
+
/** Full per-skill provenance report for audits and explicit investigation. */
|
|
360
395
|
export function formatLibraryStateReport(report) {
|
|
361
396
|
if (!report.records.length)
|
|
362
397
|
return "No Loadout-managed skill activations exist.";
|
|
363
398
|
return [
|
|
364
|
-
|
|
399
|
+
formatLibraryHeadline(report),
|
|
365
400
|
...(report.migrationPending
|
|
366
401
|
? [
|
|
367
402
|
`Migration pending: ${report.migrationPending} active installation(s) will be cached on first disable.`,
|
|
368
403
|
]
|
|
369
404
|
: []),
|
|
370
|
-
...report.records.map((record) => `${record.packageId}${record.unitId ? `/${record.unitId}` : ""} — ${record.agent} — cache:${record.cacheState} review:${record.reviewState} install:${record.installationState} activation:${record.activationState} — ${record.targets.length} target(s)`),
|
|
405
|
+
...report.records.map((record) => `${record.packageId}${record.unitId ? `/${record.unitId}` : ""}${report.repositories[record.packageId] ? ` (${report.repositories[record.packageId]})` : ""} — ${record.agent} — cache:${record.cacheState} review:${record.reviewState} install:${record.installationState} activation:${record.activationState} — ${record.targets.length} target(s)`),
|
|
371
406
|
].join("\n");
|
|
372
407
|
}
|
|
373
408
|
export function formatActivationPlan(plan) {
|
|
@@ -140,6 +140,16 @@ export function adapterCapabilities(agent) {
|
|
|
140
140
|
throw new Error(`No adapter capability declaration for '${agent}'`);
|
|
141
141
|
return found;
|
|
142
142
|
}
|
|
143
|
+
/**
|
|
144
|
+
* Return the display names of agents with an explicitly declared native skill
|
|
145
|
+
* directory. This is a filesystem-support boundary, not a claim about native
|
|
146
|
+
* application runtime behavior.
|
|
147
|
+
*/
|
|
148
|
+
export function supportedAdapterNames(adapters = ADAPTER_CAPABILITIES) {
|
|
149
|
+
return adapters
|
|
150
|
+
.filter((adapter) => adapter.components.skill === "native")
|
|
151
|
+
.map((adapter) => adapter.displayName);
|
|
152
|
+
}
|
|
143
153
|
/**
|
|
144
154
|
* Produce a read-only native-skill plan for one agent. This is deliberately
|
|
145
155
|
* narrower than package normalization: adapters may only target a directory
|