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.
- package/CHANGELOG.md +17 -0
- package/MASTER_PLAN.md +9 -9
- package/README.md +84 -121
- package/catalog/discovered.json +59800 -10065
- package/catalog/packages.json +4 -3
- package/dist/src/cli.js +26 -2979
- package/dist/src/commands/agents.js +168 -0
- package/dist/src/commands/catalog.js +708 -0
- package/dist/src/commands/health.js +98 -0
- package/dist/src/commands/inventory.js +417 -0
- package/dist/src/commands/lifecycle.js +411 -0
- package/dist/src/commands/mcp.js +424 -0
- package/dist/src/commands/setup.js +67 -0
- package/dist/src/commands/sharing.js +358 -0
- package/dist/src/commands/support.js +246 -0
- package/dist/src/core/active-limit.js +2 -0
- package/dist/src/core/active-policy.js +2 -1
- package/dist/src/core/catalog-install.js +47 -10
- package/dist/src/core/catalog.js +15 -96
- package/dist/src/core/cli-guide.js +43 -37
- package/dist/src/core/completion.js +2 -20
- package/dist/src/core/doctor.js +73 -24
- package/dist/src/core/github.js +3 -2
- package/dist/src/core/handoff.js +161 -0
- package/dist/src/core/health-score-evidence.js +2 -2
- package/dist/src/core/health.js +83 -0
- package/dist/src/core/install.js +8 -3
- package/dist/src/core/loadout-card.js +14 -2
- package/dist/src/core/review-queue.js +7 -2
- package/dist/src/core/route.js +248 -0
- package/dist/src/core/skill-inventory.js +3 -3
- package/dist/src/core/skills.js +6 -7
- package/dist/src/core/source.js +10 -3
- package/dist/src/core/upgrade.js +10 -9
- package/docs/ACTIVE_SET.md +1 -1
- package/docs/ACTIVE_SET_POLICY.md +1 -1
- package/docs/CATALOG.md +2 -2
- package/docs/DEMO_SCRIPT.md +5 -5
- package/docs/DISCOVERED.md +250 -253
- package/docs/RELEASE_REVIEW.md +28 -143
- package/docs/SUBMISSION_COPY.md +4 -4
- package/docs/UPSTREAM_LICENSE_DECISIONS.md +19 -12
- package/docs/USER_TEST_GUIDE.md +1 -1
- package/docs/evidence/readme-claims.json +2 -14
- package/package.json +4 -3
- package/dist/src/core/benchmark-campaign.js +0 -496
- package/dist/src/core/benchmark-evidence.js +0 -458
- package/dist/src/core/benchmark-fixtures.js +0 -1157
- package/dist/src/core/benchmark-runner.js +0 -301
- package/dist/src/core/benchmark-trust.js +0 -795
- package/dist/src/core/catalog-release.js +0 -287
- package/dist/src/core/compatibility-intelligence.js +0 -465
- package/dist/src/core/ecosystem-import.js +0 -1072
- package/dist/src/core/head-to-head.js +0 -437
- package/dist/src/core/intelligence-feed-build.js +0 -59
- package/dist/src/core/intelligence-feed.js +0 -353
- package/dist/src/core/registry-api.js +0 -97
- package/dist/src/core/release-claims.js +0 -175
- package/dist/src/core/signing.js +0 -115
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { DEFAULT_ACTIVE_SKILL_LIMIT } from "./active-limit.js";
|
|
1
2
|
import { loadEffectiveCatalog } from "./catalog.js";
|
|
2
3
|
import { detectAgents } from "./paths.js";
|
|
3
4
|
import { isStableSkillSelected, isPowerSkillSelected, resolveCatalogProfile, } from "./profiles.js";
|
|
@@ -6,7 +7,7 @@ import { fetchRepositorySnapshot, } from "./source.js";
|
|
|
6
7
|
import { analyzeInstallPlanSafety, } from "./safety.js";
|
|
7
8
|
import { formatModelApiAccess } from "./access.js";
|
|
8
9
|
import { recordInstalledProfile } from "./profile-state.js";
|
|
9
|
-
|
|
10
|
+
import { readInstallState } from "./state.js";
|
|
10
11
|
async function parallelMap(values, concurrency, worker) {
|
|
11
12
|
const results = new Array(values.length);
|
|
12
13
|
let cursor = 0;
|
|
@@ -151,11 +152,40 @@ export async function prepareCatalogInstall(selection, options = {}) {
|
|
|
151
152
|
// Broad collections frequently publish the same conventional skill name.
|
|
152
153
|
// Resolution order is already deterministic and evidence-ranked, so keep
|
|
153
154
|
// the first source for a target and defer only the lower-ranked duplicate
|
|
154
|
-
// instead of overwriting it or blocking every other useful skill.
|
|
155
|
+
// instead of overwriting it or blocking every other useful skill. Maximum
|
|
156
|
+
// must reserve targets already owned by active managed units before applying
|
|
157
|
+
// that ordinary ranking, or a higher-ranked overlapping package can remove
|
|
158
|
+
// the source needed to preserve the active unit.
|
|
159
|
+
const reservedTargets = new Map();
|
|
160
|
+
if (selection.mode === "maximum") {
|
|
161
|
+
const selectedPackages = new Set(entries.map((entry) => entry.plan.packageId));
|
|
162
|
+
const state = await readInstallState();
|
|
163
|
+
for (const activation of state.activations ?? []) {
|
|
164
|
+
if (!selectedPackages.has(activation.packageId) ||
|
|
165
|
+
activation.installationState !== "installed" ||
|
|
166
|
+
activation.activationState !== "active")
|
|
167
|
+
continue;
|
|
168
|
+
for (const target of activation.targets) {
|
|
169
|
+
const owner = reservedTargets.get(target.activePath);
|
|
170
|
+
if (owner && owner !== activation.packageId)
|
|
171
|
+
throw new Error(`Maximum Library cannot reserve active target '${target.activePath}' because it is claimed by both '${owner}' and '${activation.packageId}'.`);
|
|
172
|
+
reservedTargets.set(target.activePath, activation.packageId);
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
}
|
|
155
176
|
const claimedTargets = new Map();
|
|
156
177
|
const collisions = [];
|
|
157
178
|
for (const entry of entries) {
|
|
158
179
|
entry.plan.files = entry.plan.files.filter((file) => {
|
|
180
|
+
const reservedPackageId = reservedTargets.get(file.target);
|
|
181
|
+
if (reservedPackageId && reservedPackageId !== entry.plan.packageId) {
|
|
182
|
+
collisions.push({
|
|
183
|
+
target: file.target,
|
|
184
|
+
keptPackageId: reservedPackageId,
|
|
185
|
+
deferredPackageId: entry.plan.packageId,
|
|
186
|
+
});
|
|
187
|
+
return false;
|
|
188
|
+
}
|
|
159
189
|
const keptPackageId = claimedTargets.get(file.target);
|
|
160
190
|
if (!keptPackageId) {
|
|
161
191
|
claimedTargets.set(file.target, entry.plan.packageId);
|
|
@@ -209,19 +239,26 @@ export function formatPreparedCatalogInstall(prepared, options = {}) {
|
|
|
209
239
|
const failures = prepared.skipped.filter((item) => item.kind === "preparation-failed");
|
|
210
240
|
const quarantined = prepared.skipped.filter((item) => item.kind === "quarantined");
|
|
211
241
|
const retired = prepared.reconciliation?.obsoleteUnits ?? [];
|
|
242
|
+
const modeLabel = prepared.selection.mode === "maximum"
|
|
243
|
+
? "Maximum Library"
|
|
244
|
+
: prepared.selection.mode === "power"
|
|
245
|
+
? "Power Boost"
|
|
246
|
+
: prepared.selection.mode === "stable"
|
|
247
|
+
? "Stable Boost"
|
|
248
|
+
: "Custom";
|
|
212
249
|
const lines = [
|
|
213
|
-
`Loadout: ${
|
|
250
|
+
`Loadout: ${modeLabel}`,
|
|
214
251
|
`Detected agents: ${prepared.agents.map((agent) => agent.displayName).join(", ")}`,
|
|
215
|
-
`
|
|
216
|
-
`Ready to install: ${prepared.entries.length} skill repositories (${targetDirectories} agent skill directories)`,
|
|
217
|
-
`Explicit setup later: ${explicit.length} repository/repositories`,
|
|
218
|
-
`Separately billed model API access: ${formatModelApiAccess(prepared.access)} (ChatGPT and Claude subscriptions do not count as API access)`,
|
|
219
|
-
"Automatic skill setup does not require an OpenAI, Anthropic, or OpenRouter API key; credentialed MCP/runtime integrations remain explicit and deferred.",
|
|
252
|
+
`Ready to install: ${prepared.entries.length} skill repositories (${targetDirectories} skill directories) from ${prepared.resolution.packages.length} reviewed source(s)`,
|
|
220
253
|
];
|
|
254
|
+
if (explicit.length)
|
|
255
|
+
lines.push(`Deferred to explicit setup: ${explicit.length} source(s)`);
|
|
256
|
+
if (prepared.access?.modelApis?.length)
|
|
257
|
+
lines.push(`Declared model API access: ${formatModelApiAccess(prepared.access)}`);
|
|
221
258
|
if (prepared.additive)
|
|
222
259
|
lines.push("Additive install: existing managed skills will stay active.");
|
|
223
|
-
if (directoriesPerAgent >
|
|
224
|
-
lines.push(`Capacity notice: about ${directoriesPerAgent} skill directories per agent exceeds Stable's ${
|
|
260
|
+
if (directoriesPerAgent > DEFAULT_ACTIVE_SKILL_LIMIT)
|
|
261
|
+
lines.push(`Capacity notice: about ${directoriesPerAgent} skill directories per agent exceeds Stable's ${DEFAULT_ACTIVE_SKILL_LIMIT}-skill bound.${prepared.selection.mode === "maximum" ? " Maximum stores them in the disabled library; optimize or activate a project-relevant working set." : prepared.selection.mode === "power" ? " Power is the explicit larger active mode; choose Stable or project optimization when lower context use matters." : " Use project-aware activation for a smaller working set."}`);
|
|
225
262
|
if (failures.length)
|
|
226
263
|
lines.push(`Preparation failures (installation will remain blocked): ${failures.map((item) => item.packageId).join(", ")}`);
|
|
227
264
|
if (quarantined.length)
|
package/dist/src/core/catalog.js
CHANGED
|
@@ -2,54 +2,13 @@ import { existsSync } from "node:fs";
|
|
|
2
2
|
import { mkdir, readFile, writeFile } from "node:fs/promises";
|
|
3
3
|
import { dirname, join } from "node:path";
|
|
4
4
|
import { fileURLToPath } from "node:url";
|
|
5
|
-
import { createHash } from "node:crypto";
|
|
6
5
|
import { catalogSchema, formatSchemaError } from "../shared/schemas.js";
|
|
7
6
|
import { fetchGitHubMetadata } from "./github.js";
|
|
8
7
|
import { loadoutHome } from "./paths.js";
|
|
9
8
|
import { compareCatalogPackages, explainCatalogScore } from "./ranking.js";
|
|
10
9
|
import { resolveCatalogProfile } from "./profiles.js";
|
|
11
10
|
import { recordCatalogObservations } from "./observations.js";
|
|
12
|
-
import { publicKeyFingerprint, verifyEnvelope, } from "./signing.js";
|
|
13
11
|
const cachedCatalogPath = () => join(loadoutHome(), "catalog.json");
|
|
14
|
-
export const trustedCatalogPath = () => join(loadoutHome(), "catalog-releases", "trusted.json");
|
|
15
|
-
export const catalogTrustPath = () => join(loadoutHome(), "catalog-releases", "trust.json");
|
|
16
|
-
export async function readCatalogTrustState() {
|
|
17
|
-
let value;
|
|
18
|
-
try {
|
|
19
|
-
value = JSON.parse(await readFile(catalogTrustPath(), "utf8"));
|
|
20
|
-
}
|
|
21
|
-
catch (error) {
|
|
22
|
-
if (error &&
|
|
23
|
-
typeof error === "object" &&
|
|
24
|
-
"code" in error &&
|
|
25
|
-
error.code === "ENOENT")
|
|
26
|
-
return undefined;
|
|
27
|
-
throw new Error(`Catalog trust anchor is unreadable at ${catalogTrustPath()}`);
|
|
28
|
-
}
|
|
29
|
-
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
30
|
-
throw new Error("Catalog trust anchor is invalid");
|
|
31
|
-
const state = value;
|
|
32
|
-
if (state.schemaVersion !== 1 ||
|
|
33
|
-
typeof state.fingerprint !== "string" ||
|
|
34
|
-
!/^sha256:[a-f0-9]{64}$/.test(state.fingerprint) ||
|
|
35
|
-
typeof state.publicKeyPem !== "string" ||
|
|
36
|
-
publicKeyFingerprint(state.publicKeyPem) !== state.fingerprint ||
|
|
37
|
-
!validDateString(state.highWaterCreatedAt) ||
|
|
38
|
-
!validDateString(state.pinnedAt) ||
|
|
39
|
-
(state.pendingCreatedAt !== undefined &&
|
|
40
|
-
!validDateString(state.pendingCreatedAt)) ||
|
|
41
|
-
(state.pendingEnvelopeSha256 !== undefined &&
|
|
42
|
-
!/^[a-f0-9]{64}$/.test(state.pendingEnvelopeSha256)) ||
|
|
43
|
-
(state.pendingCreatedAt !== undefined) !==
|
|
44
|
-
(state.pendingEnvelopeSha256 !== undefined) ||
|
|
45
|
-
(state.pendingFirstPin !== undefined &&
|
|
46
|
-
(state.pendingFirstPin !== true || state.pendingCreatedAt === undefined)))
|
|
47
|
-
throw new Error("Catalog trust anchor is invalid");
|
|
48
|
-
return state;
|
|
49
|
-
}
|
|
50
|
-
function validDateString(value) {
|
|
51
|
-
return typeof value === "string" && !Number.isNaN(Date.parse(value));
|
|
52
|
-
}
|
|
53
12
|
function bundledCatalogPath() {
|
|
54
13
|
const moduleDirectory = dirname(fileURLToPath(import.meta.url));
|
|
55
14
|
const candidates = [
|
|
@@ -186,62 +145,9 @@ export async function loadCatalog(path = bundledCatalogPath()) {
|
|
|
186
145
|
validateCatalog(value, { requireEvidence: true });
|
|
187
146
|
return value;
|
|
188
147
|
}
|
|
189
|
-
/**
|
|
190
|
-
export async function readTrustedCatalogState() {
|
|
191
|
-
let value;
|
|
192
|
-
try {
|
|
193
|
-
value = JSON.parse(await readFile(trustedCatalogPath(), "utf8"));
|
|
194
|
-
}
|
|
195
|
-
catch (error) {
|
|
196
|
-
if (error &&
|
|
197
|
-
typeof error === "object" &&
|
|
198
|
-
"code" in error &&
|
|
199
|
-
error.code === "ENOENT")
|
|
200
|
-
return undefined;
|
|
201
|
-
throw new Error(`Trusted catalog state is unreadable at ${trustedCatalogPath()}: ${error instanceof Error ? error.message : String(error)}`);
|
|
202
|
-
}
|
|
203
|
-
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
204
|
-
throw new Error("Trusted catalog state is invalid");
|
|
205
|
-
const state = value;
|
|
206
|
-
if (state.schemaVersion !== 1 ||
|
|
207
|
-
typeof state.appliedAt !== "string" ||
|
|
208
|
-
Number.isNaN(Date.parse(state.appliedAt)) ||
|
|
209
|
-
typeof state.source !== "string" ||
|
|
210
|
-
typeof state.publicKeyPem !== "string" ||
|
|
211
|
-
(state.snapshotId !== undefined &&
|
|
212
|
-
!/^\d+-[a-f0-9]{12}$/.test(state.snapshotId)) ||
|
|
213
|
-
!state.envelope ||
|
|
214
|
-
typeof state.envelope !== "object" ||
|
|
215
|
-
typeof state.envelope.createdAt !== "string" ||
|
|
216
|
-
Number.isNaN(Date.parse(state.envelope.createdAt)))
|
|
217
|
-
throw new Error("Trusted catalog state is invalid");
|
|
218
|
-
const verification = verifyEnvelope(state.envelope, state.publicKeyPem);
|
|
219
|
-
if (!verification.valid)
|
|
220
|
-
throw new Error("Trusted catalog state has an invalid Ed25519 signature");
|
|
221
|
-
const trust = await readCatalogTrustState();
|
|
222
|
-
if (!trust)
|
|
223
|
-
throw new Error("Trusted catalog state has no pinned trust anchor");
|
|
224
|
-
const anchored = verifyEnvelope(state.envelope, trust.publicKeyPem);
|
|
225
|
-
if (!anchored.valid || anchored.fingerprint !== trust.fingerprint)
|
|
226
|
-
throw new Error("Trusted catalog state does not match the pinned signing key");
|
|
227
|
-
if (Date.parse(state.envelope.createdAt) > Date.parse(trust.highWaterCreatedAt)) {
|
|
228
|
-
const coordinatedPending = state.envelope.createdAt === trust.pendingCreatedAt &&
|
|
229
|
-
createHash("sha256").update(state.envelope.signature).digest("hex") ===
|
|
230
|
-
trust.pendingEnvelopeSha256;
|
|
231
|
-
const transactionStillPending = Boolean(state.snapshotId &&
|
|
232
|
-
existsSync(join(loadoutHome(), "staging", state.snapshotId)));
|
|
233
|
-
if (!coordinatedPending || transactionStillPending)
|
|
234
|
-
throw new Error(transactionStillPending
|
|
235
|
-
? "Trusted catalog release is still pending transaction recovery"
|
|
236
|
-
: "Trusted catalog state is not coordinated with its trust anchor");
|
|
237
|
-
}
|
|
238
|
-
validateCatalog(state.envelope.payload, { requireEvidence: true });
|
|
239
|
-
return state;
|
|
240
|
-
}
|
|
241
|
-
/** Load the most recently refreshed catalog, falling back to the bundled catalog offline. */
|
|
148
|
+
/** Load the bundled catalog, then overlay the local GitHub-freshness cache offline. */
|
|
242
149
|
export async function loadEffectiveCatalog(path = bundledCatalogPath()) {
|
|
243
|
-
const
|
|
244
|
-
const base = trusted?.envelope.payload ?? (await loadCatalog(path));
|
|
150
|
+
const base = await loadCatalog(path);
|
|
245
151
|
try {
|
|
246
152
|
const raw = await readFile(cachedCatalogPath(), "utf8");
|
|
247
153
|
const value = JSON.parse(raw);
|
|
@@ -312,6 +218,19 @@ export async function refreshCatalog(packages, options = {}) {
|
|
|
312
218
|
observationFailures: observations.failures,
|
|
313
219
|
};
|
|
314
220
|
}
|
|
221
|
+
/** Merge a human-approved proposal into the bundled catalog. */
|
|
222
|
+
export async function promoteCatalogCandidate(proposal) {
|
|
223
|
+
const catalogPath = bundledCatalogPath();
|
|
224
|
+
const current = JSON.parse(await readFile(catalogPath, "utf8"));
|
|
225
|
+
if (current.some((pkg) => pkg.id === proposal.id))
|
|
226
|
+
throw new Error(`Catalog already contains id '${proposal.id}'`);
|
|
227
|
+
if (current.some((pkg) => pkg.repository.toLowerCase() === proposal.repository.toLowerCase()))
|
|
228
|
+
throw new Error(`Catalog already contains repository '${proposal.repository}'`);
|
|
229
|
+
const merged = [...current, proposal].sort((a, b) => a.id.localeCompare(b.id));
|
|
230
|
+
validateCatalog(merged, { requireEvidence: true });
|
|
231
|
+
await writeFile(catalogPath, `${JSON.stringify(merged, null, 2)}\n`);
|
|
232
|
+
return { catalogPath, totalRecords: merged.length };
|
|
233
|
+
}
|
|
315
234
|
export function rankCatalog(packages) {
|
|
316
235
|
return [...packages].sort(compareCatalogPackages);
|
|
317
236
|
}
|
|
@@ -23,7 +23,7 @@ START HERE
|
|
|
23
23
|
5. See reviewed MCP integrations and their credential needs
|
|
24
24
|
loadout mcp-recipe
|
|
25
25
|
loadout mcp-recipe --credential-free
|
|
26
|
-
loadout mcp-recipe playwright --agent
|
|
26
|
+
loadout mcp-recipe playwright --agent claude-code
|
|
27
27
|
|
|
28
28
|
If you decide to install something, Loadout shows a preview first and creates a
|
|
29
29
|
snapshot before changing managed files. Recover with: loadout rollback
|
|
@@ -41,17 +41,23 @@ loadout advanced
|
|
|
41
41
|
export const ADVANCED_GUIDE = [
|
|
42
42
|
"ADVANCED COMMANDS",
|
|
43
43
|
"",
|
|
44
|
-
"
|
|
44
|
+
"Hidden from the first screen so daily use stays focused.",
|
|
45
45
|
"",
|
|
46
|
-
"Discovery
|
|
47
|
-
"
|
|
48
|
-
"Integrations
|
|
49
|
-
"
|
|
46
|
+
"Discovery: candidate, discover, review-queue, evaluate, skill-audit",
|
|
47
|
+
"Sharing: init, lock, export, import, share, card, badge",
|
|
48
|
+
"Integrations: mcp-recipe, mcp-config, credentials, models, handoff, convert",
|
|
49
|
+
"Lifecycle: plan, adopt, uninstall, profiles, autopilot, tool, watch",
|
|
50
50
|
"",
|
|
51
|
-
"
|
|
51
|
+
"`loadout <command> --help` for options. Mutations preview first or require --yes.",
|
|
52
52
|
].join("\n");
|
|
53
|
-
/**
|
|
53
|
+
/**
|
|
54
|
+
* The first-screen help shows only the everyday core (~24 commands). Everything
|
|
55
|
+
* else stays fully available and runnable, just omitted from the default help so
|
|
56
|
+
* the surface reads as a focused product rather than a tool dump. `loadout
|
|
57
|
+
* advanced` lists the hidden surface; `loadout <command> --help` documents each.
|
|
58
|
+
*/
|
|
54
59
|
export const HIDDEN_FROM_FIRST_SCREEN = new Set([
|
|
60
|
+
// manifests, authoring, sharing
|
|
55
61
|
"init",
|
|
56
62
|
"lock",
|
|
57
63
|
"export",
|
|
@@ -60,54 +66,54 @@ export const HIDDEN_FROM_FIRST_SCREEN = new Set([
|
|
|
60
66
|
"create",
|
|
61
67
|
"pack",
|
|
62
68
|
"publish",
|
|
63
|
-
"
|
|
69
|
+
"add",
|
|
70
|
+
"unadd",
|
|
71
|
+
"share",
|
|
72
|
+
"card",
|
|
73
|
+
"badge",
|
|
74
|
+
"compare-loadouts",
|
|
64
75
|
"search",
|
|
76
|
+
// analytics / evidence / discovery
|
|
65
77
|
"report",
|
|
66
78
|
"outcomes",
|
|
67
79
|
"outcome",
|
|
68
|
-
"share",
|
|
69
|
-
"card",
|
|
70
|
-
"compare-loadouts",
|
|
71
|
-
"badge",
|
|
72
|
-
"claims",
|
|
73
|
-
"alert-ignore",
|
|
74
|
-
"alert-pin",
|
|
75
|
-
"alert-unpin",
|
|
76
|
-
"alert-pins",
|
|
77
80
|
"improve",
|
|
78
81
|
"improve-feedback",
|
|
79
|
-
"
|
|
80
|
-
"
|
|
81
|
-
"compatibility",
|
|
82
|
+
"compare",
|
|
83
|
+
"versions",
|
|
82
84
|
"skill-audit",
|
|
83
|
-
"interop",
|
|
84
|
-
"benchmark",
|
|
85
85
|
"capabilities",
|
|
86
86
|
"candidate",
|
|
87
|
-
"catalog-update",
|
|
88
87
|
"discover",
|
|
89
88
|
"review-queue",
|
|
90
89
|
"review",
|
|
90
|
+
"inspect",
|
|
91
|
+
"propose",
|
|
92
|
+
"evaluate",
|
|
93
|
+
// integrations / credentials / runtime
|
|
91
94
|
"credentials",
|
|
92
95
|
"models",
|
|
93
|
-
"
|
|
94
|
-
"catalog-sign",
|
|
95
|
-
"catalog-verify",
|
|
96
|
+
"handoff",
|
|
96
97
|
"completion",
|
|
97
98
|
"mcp-recipe",
|
|
98
|
-
"mcp",
|
|
99
|
-
"inspect",
|
|
100
|
-
"evaluate",
|
|
101
|
-
"head-to-head",
|
|
102
|
-
"watch",
|
|
103
|
-
"schedule",
|
|
104
|
-
"unschedule",
|
|
105
|
-
"sandbox-run",
|
|
106
99
|
"mcp-config",
|
|
107
100
|
"codex-mcp-config",
|
|
108
|
-
"
|
|
109
|
-
"install",
|
|
101
|
+
"sandbox-run",
|
|
110
102
|
"convert",
|
|
111
103
|
"canary",
|
|
112
104
|
"serve",
|
|
105
|
+
// lifecycle extras
|
|
106
|
+
"plan",
|
|
107
|
+
"adopt",
|
|
108
|
+
"uninstall",
|
|
109
|
+
"profiles",
|
|
110
|
+
"autopilot",
|
|
111
|
+
"tool",
|
|
112
|
+
"watch",
|
|
113
|
+
"schedule",
|
|
114
|
+
"unschedule",
|
|
115
|
+
"alert-ignore",
|
|
116
|
+
"alert-pin",
|
|
117
|
+
"alert-unpin",
|
|
118
|
+
"alert-pins",
|
|
113
119
|
]);
|
|
@@ -11,7 +11,6 @@ const commands = [
|
|
|
11
11
|
"create",
|
|
12
12
|
"pack",
|
|
13
13
|
"publish",
|
|
14
|
-
"registry-serve",
|
|
15
14
|
"search",
|
|
16
15
|
"add",
|
|
17
16
|
"unadd",
|
|
@@ -20,11 +19,7 @@ const commands = [
|
|
|
20
19
|
"reconcile",
|
|
21
20
|
"status",
|
|
22
21
|
"versions",
|
|
23
|
-
"intelligence",
|
|
24
|
-
"compatibility",
|
|
25
22
|
"skill-audit",
|
|
26
|
-
"interop",
|
|
27
|
-
"benchmark",
|
|
28
23
|
"doctor",
|
|
29
24
|
"health",
|
|
30
25
|
"compare",
|
|
@@ -59,12 +54,6 @@ const commands = [
|
|
|
59
54
|
"card",
|
|
60
55
|
"compare-loadouts",
|
|
61
56
|
"badge",
|
|
62
|
-
"claims",
|
|
63
|
-
"head-to-head",
|
|
64
|
-
"keygen",
|
|
65
|
-
"catalog-sign",
|
|
66
|
-
"catalog-verify",
|
|
67
|
-
"catalog-update",
|
|
68
57
|
"inspect",
|
|
69
58
|
"evaluate",
|
|
70
59
|
"watch",
|
|
@@ -91,7 +80,6 @@ const commands = [
|
|
|
91
80
|
const modelCommands = ["status", "set", "verify"];
|
|
92
81
|
const credentialCommands = ["status", "set", "check", "delete"];
|
|
93
82
|
const candidateCommands = ["list", "inspect", "propose"];
|
|
94
|
-
const benchmarkCommands = ["plan"];
|
|
95
83
|
export function renderShellCompletion(shell) {
|
|
96
84
|
const words = commands.join(" ");
|
|
97
85
|
switch (shell) {
|
|
@@ -108,20 +96,17 @@ _loadout() {
|
|
|
108
96
|
COMPREPLY=( $(compgen -W "${credentialCommands.join(" ")}" -- "$current") )
|
|
109
97
|
elif [[ COMP_CWORD -eq 2 && "\${COMP_WORDS[1]}" == "candidate" ]]; then
|
|
110
98
|
COMPREPLY=( $(compgen -W "${candidateCommands.join(" ")}" -- "$current") )
|
|
111
|
-
elif [[ COMP_CWORD -eq 2 && "\${COMP_WORDS[1]}" == "benchmark" ]]; then
|
|
112
|
-
COMPREPLY=( $(compgen -W "${benchmarkCommands.join(" ")}" -- "$current") )
|
|
113
99
|
fi
|
|
114
100
|
}
|
|
115
101
|
complete -F _loadout loadout
|
|
116
102
|
`;
|
|
117
103
|
case "zsh":
|
|
118
104
|
return `#compdef loadout
|
|
119
|
-
typeset -a commands model_commands credential_commands candidate_commands
|
|
105
|
+
typeset -a commands model_commands credential_commands candidate_commands
|
|
120
106
|
commands=(${commands.map((command) => `'${command}'`).join(" ")})
|
|
121
107
|
model_commands=(${modelCommands.map((command) => `'${command}'`).join(" ")})
|
|
122
108
|
credential_commands=(${credentialCommands.map((command) => `'${command}'`).join(" ")})
|
|
123
109
|
candidate_commands=(${candidateCommands.map((command) => `'${command}'`).join(" ")})
|
|
124
|
-
benchmark_commands=(${benchmarkCommands.map((command) => `'${command}'`).join(" ")})
|
|
125
110
|
if (( CURRENT == 2 )); then
|
|
126
111
|
_describe 'command' commands
|
|
127
112
|
elif (( CURRENT == 3 )) && [[ $words[2] == models ]]; then
|
|
@@ -130,8 +115,6 @@ elif (( CURRENT == 3 )) && [[ $words[2] == credentials ]]; then
|
|
|
130
115
|
_describe 'credentials command' credential_commands
|
|
131
116
|
elif (( CURRENT == 3 )) && [[ $words[2] == candidate ]]; then
|
|
132
117
|
_describe 'candidate command' candidate_commands
|
|
133
|
-
elif (( CURRENT == 3 )) && [[ $words[2] == benchmark ]]; then
|
|
134
|
-
_describe 'benchmark command' benchmark_commands
|
|
135
118
|
fi
|
|
136
119
|
`;
|
|
137
120
|
case "fish":
|
|
@@ -140,7 +123,6 @@ fi
|
|
|
140
123
|
...modelCommands.map((command) => `complete -c loadout -f -n '__fish_seen_subcommand_from models' -a ${command}`),
|
|
141
124
|
...credentialCommands.map((command) => `complete -c loadout -f -n '__fish_seen_subcommand_from credentials' -a ${command}`),
|
|
142
125
|
...candidateCommands.map((command) => `complete -c loadout -f -n '__fish_seen_subcommand_from candidate' -a ${command}`),
|
|
143
|
-
...benchmarkCommands.map((command) => `complete -c loadout -f -n '__fish_seen_subcommand_from benchmark' -a ${command}`),
|
|
144
126
|
]
|
|
145
127
|
.join("\n")
|
|
146
128
|
.concat("\n");
|
|
@@ -148,7 +130,7 @@ fi
|
|
|
148
130
|
return `Register-ArgumentCompleter -Native -CommandName loadout -ScriptBlock {
|
|
149
131
|
param($wordToComplete, $commandAst, $cursorPosition)
|
|
150
132
|
$elements = @($commandAst.CommandElements | ForEach-Object { $_.Extent.Text })
|
|
151
|
-
$candidates = if ($elements.Count -ge 2 -and $elements[1] -eq 'models') { @(${modelCommands.map((command) => `'${command}'`).join(", ")}) } elseif ($elements.Count -ge 2 -and $elements[1] -eq 'credentials') { @(${credentialCommands.map((command) => `'${command}'`).join(", ")}) } elseif ($elements.Count -ge 2 -and $elements[1] -eq 'candidate') { @(${candidateCommands.map((command) => `'${command}'`).join(", ")}) }
|
|
133
|
+
$candidates = if ($elements.Count -ge 2 -and $elements[1] -eq 'models') { @(${modelCommands.map((command) => `'${command}'`).join(", ")}) } elseif ($elements.Count -ge 2 -and $elements[1] -eq 'credentials') { @(${credentialCommands.map((command) => `'${command}'`).join(", ")}) } elseif ($elements.Count -ge 2 -and $elements[1] -eq 'candidate') { @(${candidateCommands.map((command) => `'${command}'`).join(", ")}) } else { @(${commands.map((command) => `'${command}'`).join(", ")}) }
|
|
152
134
|
$candidates | Where-Object { $_ -like "$wordToComplete*" } | ForEach-Object { [System.Management.Automation.CompletionResult]::new($_, $_, 'ParameterValue', $_) }
|
|
153
135
|
}
|
|
154
136
|
`;
|
package/dist/src/core/doctor.js
CHANGED
|
@@ -62,34 +62,83 @@ export async function runDoctor() {
|
|
|
62
62
|
issues,
|
|
63
63
|
};
|
|
64
64
|
}
|
|
65
|
-
export function formatDoctorReport(report) {
|
|
65
|
+
export function formatDoctorReport(report, options = {}) {
|
|
66
|
+
const installed = report.agents.filter((a) => a.agent.installed);
|
|
67
|
+
const notInstalled = report.agents.filter((a) => !a.agent.installed);
|
|
68
|
+
const allIssues = [
|
|
69
|
+
...report.issues,
|
|
70
|
+
...installed.flatMap((a) => a.issues),
|
|
71
|
+
];
|
|
72
|
+
const grade = allIssues.length === 0 && installed.length > 0
|
|
73
|
+
? "HEALTHY"
|
|
74
|
+
: allIssues.length <= 2
|
|
75
|
+
? "OK"
|
|
76
|
+
: "NEEDS ATTENTION";
|
|
66
77
|
const lines = [
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
`
|
|
70
|
-
`State
|
|
78
|
+
`loadout doctor — ${grade}`,
|
|
79
|
+
"",
|
|
80
|
+
`Platform: ${report.platform}`,
|
|
81
|
+
`State: ${report.loadoutHome} ${report.loadoutHomeExists ? "✓" : "⚠ not created"} ${report.loadoutHomeWritable ? "writable" : "⚠ not writable"}`,
|
|
82
|
+
`Agents: ${installed.length} detected, ${notInstalled.length} available`,
|
|
83
|
+
"",
|
|
71
84
|
];
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
85
|
+
// --- Detected agents: compact summary ---
|
|
86
|
+
if (installed.length) {
|
|
87
|
+
lines.push("DETECTED AGENTS");
|
|
88
|
+
for (const entry of installed) {
|
|
89
|
+
const skillCount = entry.inventory.components
|
|
90
|
+
.filter((c) => c.scanned && c.directoryExists)
|
|
91
|
+
.reduce((sum, c) => sum + c.entries.length, 0);
|
|
92
|
+
const supported = entry.inventory.components
|
|
93
|
+
.filter((c) => c.compatibility === "native" || c.compatibility === "adapted")
|
|
94
|
+
.map((c) => c.type);
|
|
95
|
+
lines.push(` ✓ ${entry.agent.displayName}`, ` ${entry.agent.skillsDirectory}`, ` ${skillCount} items | supports: ${supported.join(", ")}`);
|
|
96
|
+
if (entry.issues.length) {
|
|
97
|
+
for (const issue of entry.issues)
|
|
98
|
+
lines.push(` ⚠ ${issue}`);
|
|
99
|
+
}
|
|
100
|
+
if (options.verbose) {
|
|
101
|
+
for (const component of entry.inventory.components) {
|
|
102
|
+
if (component.compatibility === "unsupported")
|
|
103
|
+
continue;
|
|
104
|
+
const detail = component.scanned
|
|
105
|
+
? component.directoryExists
|
|
106
|
+
? `${component.entries.length} items`
|
|
107
|
+
: "not created"
|
|
108
|
+
: (component.note ?? "—");
|
|
109
|
+
lines.push(` ${component.type}: ${detail}`);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
81
112
|
}
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
113
|
+
lines.push("");
|
|
114
|
+
}
|
|
115
|
+
// --- Not installed: one-liner ---
|
|
116
|
+
if (notInstalled.length) {
|
|
117
|
+
lines.push(`NOT DETECTED: ${notInstalled.map((a) => a.agent.displayName).join(", ")}`, "");
|
|
118
|
+
}
|
|
119
|
+
// --- Issues ---
|
|
120
|
+
if (allIssues.length) {
|
|
121
|
+
lines.push("ISSUES");
|
|
122
|
+
for (const issue of allIssues)
|
|
123
|
+
lines.push(` ⚠ ${issue}`);
|
|
124
|
+
lines.push("");
|
|
86
125
|
}
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
126
|
+
// --- Next steps ---
|
|
127
|
+
lines.push("NEXT STEPS");
|
|
128
|
+
if (allIssues.length) {
|
|
129
|
+
lines.push(" Fix the issues above, then run `loadout doctor` again.");
|
|
130
|
+
}
|
|
131
|
+
else if (installed.length === 0) {
|
|
132
|
+
lines.push(" Install an AI coding agent (Claude Code, Codex, Cursor, etc.),", " then run `loadout doctor` again.");
|
|
133
|
+
}
|
|
134
|
+
else {
|
|
135
|
+
lines.push(" loadout status see what's managed", " loadout setup install a curated skill set", " loadout route find the right model for your task", " loadout health check for updates");
|
|
91
136
|
}
|
|
92
|
-
else
|
|
93
|
-
lines.push("", "No global blocking issues found.");
|
|
94
137
|
return lines.join("\n");
|
|
95
138
|
}
|
|
139
|
+
/**
|
|
140
|
+
* Legacy-compatible verbose format. Used when --verbose is passed.
|
|
141
|
+
*/
|
|
142
|
+
export function formatDoctorReportVerbose(report) {
|
|
143
|
+
return formatDoctorReport(report, { verbose: true });
|
|
144
|
+
}
|
package/dist/src/core/github.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { mkdir, readFile, rename, writeFile } from "node:fs/promises";
|
|
2
|
+
import { randomUUID } from "node:crypto";
|
|
2
3
|
import { join } from "node:path";
|
|
3
4
|
import { loadoutHome } from "./paths.js";
|
|
4
5
|
function normalizeRepository(input) {
|
|
@@ -79,7 +80,7 @@ export async function fetchGitHubMetadata(input, options = {}) {
|
|
|
79
80
|
await mkdir(join(loadoutHome(), "cache", "github-metadata"), {
|
|
80
81
|
recursive: true,
|
|
81
82
|
});
|
|
82
|
-
const temporary = `${path}.${process.pid}.tmp`;
|
|
83
|
+
const temporary = `${path}.${process.pid}.${randomUUID()}.tmp`;
|
|
83
84
|
await writeFile(temporary, JSON.stringify(metadata, null, 2), "utf8");
|
|
84
85
|
await rename(temporary, path);
|
|
85
86
|
return metadata;
|
|
@@ -141,7 +142,7 @@ export async function fetchGitHubReleaseMetadata(input, options = {}) {
|
|
|
141
142
|
await mkdir(join(loadoutHome(), "cache", "github-releases"), {
|
|
142
143
|
recursive: true,
|
|
143
144
|
});
|
|
144
|
-
const temporary = `${path}.${process.pid}.tmp`;
|
|
145
|
+
const temporary = `${path}.${process.pid}.${randomUUID()}.tmp`;
|
|
145
146
|
await writeFile(temporary, JSON.stringify(metadata, null, 2), "utf8");
|
|
146
147
|
await rename(temporary, path);
|
|
147
148
|
return metadata;
|