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
|
@@ -0,0 +1,411 @@
|
|
|
1
|
+
import { loadEffectiveCatalog, } from "../core/catalog.js";
|
|
2
|
+
import { detectAgents, parseAgentSelection } from "../core/paths.js";
|
|
3
|
+
import { readFile } from "node:fs/promises";
|
|
4
|
+
import { resolve } from "node:path";
|
|
5
|
+
import { buildSkillPlan, applySkillInstall, installedAgents, } from "../core/install.js";
|
|
6
|
+
import { listSnapshotIds, readSnapshot, restoreSnapshot, summarizeSnapshot, } from "../core/snapshot.js";
|
|
7
|
+
import { fetchRepositorySnapshot } from "../core/source.js";
|
|
8
|
+
import { applyPackageUpdate, buildUpdatePlan, formatUpdatePlan, selectSafeAutomaticUpdates, } from "../core/update.js";
|
|
9
|
+
import { startApiServer } from "../core/api.js";
|
|
10
|
+
import { readInstallState } from "../core/state.js";
|
|
11
|
+
import { evaluateInstalledProfile, formatInstalledProfileStatus, } from "../core/profile-state.js";
|
|
12
|
+
import { resolveCatalogProfile } from "../core/profiles.js";
|
|
13
|
+
import { compileConversion, } from "../core/conversion.js";
|
|
14
|
+
import { writeFileAtomically } from "../core/atomic-file.js";
|
|
15
|
+
import { formatCanaryResult, runCanary } from "../core/canary.js";
|
|
16
|
+
import { applyPreparedCatalogInstall, formatCatalogApplyGuidance, formatPreparedCatalogInstall, prepareCatalogInstall, } from "../core/catalog-install.js";
|
|
17
|
+
import { withMutationLock } from "../core/transaction.js";
|
|
18
|
+
import { setupSelection, printSetupProgress } from "./support.js";
|
|
19
|
+
export function registerLifecycle(program) {
|
|
20
|
+
program
|
|
21
|
+
.command("plan")
|
|
22
|
+
.description("Plan installing packages from a local directory, catalog, or public GitHub repository")
|
|
23
|
+
.option("--source <directory>", "local package directory containing SKILL.md")
|
|
24
|
+
.option("--repository <owner/repo>", "public GitHub repository containing SKILL.md")
|
|
25
|
+
.option("--package <id>", "package identifier (repeat for custom mode)", (value, previous = []) => [...previous, value], [])
|
|
26
|
+
.option("--mode <mode>", "catalog selection mode: stable, power, maximum, or custom")
|
|
27
|
+
.option("--agents <ids>", "comma-separated agent ids; defaults to all detected agents")
|
|
28
|
+
.action(async (options) => {
|
|
29
|
+
const packageIds = options.package ?? [];
|
|
30
|
+
const hasSource = Boolean(options.source || options.repository);
|
|
31
|
+
if (hasSource && options.mode)
|
|
32
|
+
throw new Error("--mode cannot be combined with --source or --repository");
|
|
33
|
+
if (hasSource && packageIds.length !== 1)
|
|
34
|
+
throw new Error("A source or repository requires exactly one --package");
|
|
35
|
+
if (!hasSource && !options.mode)
|
|
36
|
+
throw new Error("Provide --mode or exactly one of --source/--repository");
|
|
37
|
+
if (!hasSource) {
|
|
38
|
+
const prepared = await prepareCatalogInstall(setupSelection(options.mode, packageIds), {
|
|
39
|
+
requestedAgents: parseAgentSelection(options.agents),
|
|
40
|
+
onProgress: printSetupProgress,
|
|
41
|
+
});
|
|
42
|
+
console.log(JSON.stringify({
|
|
43
|
+
mode: prepared.selection.mode,
|
|
44
|
+
agents: prepared.agents.map((agent) => agent.id),
|
|
45
|
+
packages: prepared.entries.map((entry) => ({
|
|
46
|
+
...entry.plan,
|
|
47
|
+
repository: entry.metadata?.repository,
|
|
48
|
+
resolvedCommit: entry.metadata?.resolvedCommit,
|
|
49
|
+
safety: entry.safety,
|
|
50
|
+
})),
|
|
51
|
+
skipped: prepared.skipped,
|
|
52
|
+
profile: {
|
|
53
|
+
deferred: prepared.resolution.deferred.map((pkg) => pkg.id),
|
|
54
|
+
conflicts: prepared.resolution.conflicts,
|
|
55
|
+
warnings: prepared.resolution.warnings,
|
|
56
|
+
},
|
|
57
|
+
}, null, 2));
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
const resolution = hasSource
|
|
61
|
+
? undefined
|
|
62
|
+
: resolveCatalogProfile(await loadEffectiveCatalog(), {
|
|
63
|
+
mode: options.mode,
|
|
64
|
+
packageIds,
|
|
65
|
+
});
|
|
66
|
+
const selected = hasSource
|
|
67
|
+
? [{ id: packageIds[0] }]
|
|
68
|
+
: resolution.packages;
|
|
69
|
+
const agents = installedAgents(await detectAgents(), parseAgentSelection(options.agents));
|
|
70
|
+
const plans = [];
|
|
71
|
+
const skipped = [];
|
|
72
|
+
for (const pkg of selected) {
|
|
73
|
+
const fetched = options.repository
|
|
74
|
+
? await fetchRepositorySnapshot(options.repository)
|
|
75
|
+
: !options.source
|
|
76
|
+
? await fetchRepositorySnapshot(pkg.repository)
|
|
77
|
+
: undefined;
|
|
78
|
+
try {
|
|
79
|
+
plans.push(await buildSkillPlan(fetched?.path ?? options.source, pkg.id, agents));
|
|
80
|
+
}
|
|
81
|
+
catch (error) {
|
|
82
|
+
if (!options.mode ||
|
|
83
|
+
!(error instanceof Error) ||
|
|
84
|
+
!error.message.startsWith("No SKILL.md found"))
|
|
85
|
+
throw error;
|
|
86
|
+
skipped.push({
|
|
87
|
+
packageId: pkg.id,
|
|
88
|
+
reason: "No SKILL.md found; this package is not skill-installable yet (inspect its MCP manifest instead).",
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
console.log(JSON.stringify(options.mode
|
|
93
|
+
? {
|
|
94
|
+
mode: options.mode,
|
|
95
|
+
packages: plans,
|
|
96
|
+
skipped,
|
|
97
|
+
profile: resolution && {
|
|
98
|
+
deferred: resolution.deferred.map((pkg) => pkg.id),
|
|
99
|
+
conflicts: resolution.conflicts,
|
|
100
|
+
warnings: resolution.warnings,
|
|
101
|
+
},
|
|
102
|
+
}
|
|
103
|
+
: plans[0], null, 2));
|
|
104
|
+
});
|
|
105
|
+
program
|
|
106
|
+
.command("install")
|
|
107
|
+
.description("Install packages from a local directory, catalog, or public GitHub repository")
|
|
108
|
+
.option("--source <directory>", "local package directory containing SKILL.md")
|
|
109
|
+
.option("--repository <owner/repo>", "public GitHub repository containing SKILL.md")
|
|
110
|
+
.option("--package <id>", "package identifier (repeat for custom mode)", (value, previous = []) => [...previous, value], [])
|
|
111
|
+
.option("--mode <mode>", "catalog selection mode: stable, power, maximum, or custom")
|
|
112
|
+
.option("--agents <ids>", "comma-separated agent ids; defaults to all detected agents")
|
|
113
|
+
.option("--yes", "apply without interactive confirmation")
|
|
114
|
+
.option("--approve-risk", "approve reviewed safety findings for catalog mode")
|
|
115
|
+
.option("--details", "show every quarantined and deferred unit")
|
|
116
|
+
.action(async (options) => {
|
|
117
|
+
const packageIds = options.package ?? [];
|
|
118
|
+
const hasSource = Boolean(options.source || options.repository);
|
|
119
|
+
if (hasSource && options.mode)
|
|
120
|
+
throw new Error("--mode cannot be combined with --source or --repository");
|
|
121
|
+
if (hasSource && packageIds.length !== 1)
|
|
122
|
+
throw new Error("A source or repository requires exactly one --package");
|
|
123
|
+
if (!hasSource && !options.mode)
|
|
124
|
+
throw new Error("Provide --mode or exactly one of --source/--repository");
|
|
125
|
+
if (!hasSource) {
|
|
126
|
+
const prepared = await prepareCatalogInstall(setupSelection(options.mode, packageIds), {
|
|
127
|
+
requestedAgents: parseAgentSelection(options.agents),
|
|
128
|
+
onProgress: printSetupProgress,
|
|
129
|
+
additive: options.mode === "custom",
|
|
130
|
+
});
|
|
131
|
+
console.log(formatPreparedCatalogInstall(prepared, {
|
|
132
|
+
details: options.details,
|
|
133
|
+
}));
|
|
134
|
+
if (!options.yes) {
|
|
135
|
+
console.log(formatCatalogApplyGuidance(prepared.entries.some((entry) => entry.safety.approvalRequired)));
|
|
136
|
+
return;
|
|
137
|
+
}
|
|
138
|
+
const snapshotId = await applyPreparedCatalogInstall(prepared, {
|
|
139
|
+
approveRisk: options.approveRisk,
|
|
140
|
+
});
|
|
141
|
+
console.log(`Installed ${prepared.entries.length} repositories as one transaction. Snapshot: ${snapshotId}`);
|
|
142
|
+
return;
|
|
143
|
+
}
|
|
144
|
+
const resolution = hasSource
|
|
145
|
+
? undefined
|
|
146
|
+
: resolveCatalogProfile(await loadEffectiveCatalog(), {
|
|
147
|
+
mode: options.mode,
|
|
148
|
+
packageIds,
|
|
149
|
+
});
|
|
150
|
+
const selected = hasSource
|
|
151
|
+
? [{ id: packageIds[0] }]
|
|
152
|
+
: resolution.packages;
|
|
153
|
+
const agents = installedAgents(await detectAgents(), parseAgentSelection(options.agents));
|
|
154
|
+
const plans = [];
|
|
155
|
+
const skipped = [];
|
|
156
|
+
for (const pkg of selected) {
|
|
157
|
+
const fetched = options.repository
|
|
158
|
+
? await fetchRepositorySnapshot(options.repository)
|
|
159
|
+
: !options.source
|
|
160
|
+
? await fetchRepositorySnapshot(pkg.repository)
|
|
161
|
+
: undefined;
|
|
162
|
+
try {
|
|
163
|
+
plans.push({
|
|
164
|
+
plan: await buildSkillPlan(fetched?.path ?? options.source, pkg.id, agents),
|
|
165
|
+
repository: fetched?.repository,
|
|
166
|
+
commit: fetched?.commit,
|
|
167
|
+
});
|
|
168
|
+
}
|
|
169
|
+
catch (error) {
|
|
170
|
+
if (!options.mode ||
|
|
171
|
+
!(error instanceof Error) ||
|
|
172
|
+
!error.message.startsWith("No SKILL.md found"))
|
|
173
|
+
throw error;
|
|
174
|
+
skipped.push({
|
|
175
|
+
packageId: pkg.id,
|
|
176
|
+
reason: "No SKILL.md found; this package is not skill-installable yet (inspect its MCP manifest instead).",
|
|
177
|
+
});
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
console.log(`Installing ${plans.map(({ plan }) => plan.packageId).join(", ")} for ${agents.map((agent) => agent.id).join(", ")}...`);
|
|
181
|
+
for (const warning of resolution?.warnings ?? [])
|
|
182
|
+
console.log(`Profile warning: ${warning}`);
|
|
183
|
+
for (const entry of skipped)
|
|
184
|
+
console.log(`Skipping ${entry.packageId}: ${entry.reason}`);
|
|
185
|
+
if (!options.yes)
|
|
186
|
+
console.log("Review the plan with `loadout plan`; use --yes to apply it.");
|
|
187
|
+
if (!options.yes)
|
|
188
|
+
return;
|
|
189
|
+
for (const entry of plans) {
|
|
190
|
+
const snapshotId = await applySkillInstall(entry.plan, entry.repository
|
|
191
|
+
? { repository: entry.repository, resolvedCommit: entry.commit }
|
|
192
|
+
: undefined);
|
|
193
|
+
console.log(`Installed ${entry.plan.packageId} successfully. Snapshot: ${snapshotId}`);
|
|
194
|
+
}
|
|
195
|
+
});
|
|
196
|
+
program
|
|
197
|
+
.command("rollback")
|
|
198
|
+
.description("Restore the most recent Loadout snapshot")
|
|
199
|
+
.option("--snapshot <id>", "specific snapshot id")
|
|
200
|
+
.option("--list", "list snapshot ids without restoring anything")
|
|
201
|
+
.action(async (options) => {
|
|
202
|
+
if (options.list) {
|
|
203
|
+
const snapshotIds = await listSnapshotIds();
|
|
204
|
+
if (!snapshotIds.length)
|
|
205
|
+
return console.log("No Loadout snapshots found.");
|
|
206
|
+
console.log("Loadout rollback history (oldest to newest):");
|
|
207
|
+
for (const [index, id] of snapshotIds.entries()) {
|
|
208
|
+
const summary = summarizeSnapshot(await readSnapshot(id));
|
|
209
|
+
const impact = summary.changedEntries === undefined
|
|
210
|
+
? "legacy impact unknown"
|
|
211
|
+
: `${summary.changedEntries} changed filesystem entr${summary.changedEntries === 1 ? "y" : "ies"}`;
|
|
212
|
+
console.log(`${index === snapshotIds.length - 1 ? "latest" : " "} ${summary.createdAt} ${summary.label} ${impact} ${summary.roots} root(s) ${id}`);
|
|
213
|
+
}
|
|
214
|
+
console.log("Restore an exact entry with: loadout rollback --snapshot <id>");
|
|
215
|
+
return;
|
|
216
|
+
}
|
|
217
|
+
const selected = await withMutationLock(async () => {
|
|
218
|
+
const snapshotIds = await listSnapshotIds();
|
|
219
|
+
const chosen = options.snapshot ?? snapshotIds.at(-1);
|
|
220
|
+
if (!chosen)
|
|
221
|
+
throw new Error("No Loadout snapshots found");
|
|
222
|
+
const snapshot = await readSnapshot(chosen);
|
|
223
|
+
await restoreSnapshot(snapshot, {
|
|
224
|
+
requireUnchangedPostMutationState: true,
|
|
225
|
+
});
|
|
226
|
+
return summarizeSnapshot(snapshot);
|
|
227
|
+
});
|
|
228
|
+
console.log(`Restored ${selected.label} snapshot ${selected.id} (${selected.changedEntries ?? "unknown"} recorded changed filesystem entries across ${selected.roots} root(s)).`);
|
|
229
|
+
if (selected.changedEntries === 0)
|
|
230
|
+
console.log("This snapshot recorded no effective filesystem change. Choose an older explicit snapshot if you meant to undo an earlier installation.");
|
|
231
|
+
});
|
|
232
|
+
program
|
|
233
|
+
.command("update")
|
|
234
|
+
.description("Check the saved profile and every managed package for updates")
|
|
235
|
+
.option("--json", "emit machine-readable JSON")
|
|
236
|
+
.option("--yes", "apply reviewed profile drift and safe screened package updates")
|
|
237
|
+
.option("--apply", "legacy alias for --yes with --package")
|
|
238
|
+
.option("--package <id>", "managed package id to update")
|
|
239
|
+
.option("--approve-risk", "explicitly approve an update containing blocked safety findings")
|
|
240
|
+
.action(async (options) => {
|
|
241
|
+
const applying = Boolean(options.apply || options.yes);
|
|
242
|
+
if (options.apply && !options.package)
|
|
243
|
+
throw new Error("--apply requires --package <id>");
|
|
244
|
+
if (applying && options.package) {
|
|
245
|
+
const result = await applyPackageUpdate(options.package, {
|
|
246
|
+
approveRisk: options.approveRisk,
|
|
247
|
+
});
|
|
248
|
+
console.log(options.json
|
|
249
|
+
? JSON.stringify({ packageId: options.package, ...result }, null, 2)
|
|
250
|
+
: `Updated ${options.package} to ${result.commit}. Snapshot: ${result.snapshotId}`);
|
|
251
|
+
return;
|
|
252
|
+
}
|
|
253
|
+
const catalog = await loadEffectiveCatalog();
|
|
254
|
+
let profile = await evaluateInstalledProfile(catalog);
|
|
255
|
+
let profileSnapshotId;
|
|
256
|
+
const appliedPackages = [];
|
|
257
|
+
const failedPackages = [];
|
|
258
|
+
if (applying && profile.installed && profile.needsRefresh) {
|
|
259
|
+
const saved = (await readInstallState()).profile;
|
|
260
|
+
const prepared = await prepareCatalogInstall({
|
|
261
|
+
mode: saved.mode,
|
|
262
|
+
...(saved.packageIds ? { packageIds: saved.packageIds } : {}),
|
|
263
|
+
}, {
|
|
264
|
+
requestedAgents: saved.agents,
|
|
265
|
+
catalog,
|
|
266
|
+
access: { modelApis: [] },
|
|
267
|
+
onProgress: printSetupProgress,
|
|
268
|
+
});
|
|
269
|
+
profileSnapshotId = await applyPreparedCatalogInstall(prepared, {
|
|
270
|
+
approveRisk: options.approveRisk,
|
|
271
|
+
});
|
|
272
|
+
if (!options.json)
|
|
273
|
+
console.log(`Applied reviewed ${saved.mode} profile changes. Snapshot: ${profileSnapshotId}`);
|
|
274
|
+
profile = await evaluateInstalledProfile(catalog);
|
|
275
|
+
}
|
|
276
|
+
if (!options.json)
|
|
277
|
+
console.error("Checking repository commits (4 at a time; changed sources may take up to 120s for safety review)…");
|
|
278
|
+
let plans = await buildUpdatePlan(undefined, {
|
|
279
|
+
packageId: options.package,
|
|
280
|
+
onProgress: options.json
|
|
281
|
+
? undefined
|
|
282
|
+
: ({ completed, total, packageId }) => console.error(`✓ [${completed}/${total}] ${packageId}`),
|
|
283
|
+
});
|
|
284
|
+
if (options.package && plans.length === 0)
|
|
285
|
+
throw new Error(`No Loadout-managed installation named '${options.package}' was found. Run \`loadout list\` to see tracked packages.`);
|
|
286
|
+
if (applying) {
|
|
287
|
+
const safe = selectSafeAutomaticUpdates(plans);
|
|
288
|
+
for (const plan of safe)
|
|
289
|
+
try {
|
|
290
|
+
await applyPackageUpdate(plan.packageId);
|
|
291
|
+
appliedPackages.push(plan.packageId);
|
|
292
|
+
}
|
|
293
|
+
catch (error) {
|
|
294
|
+
failedPackages.push({
|
|
295
|
+
packageId: plan.packageId,
|
|
296
|
+
error: error instanceof Error ? error.message : String(error),
|
|
297
|
+
});
|
|
298
|
+
}
|
|
299
|
+
plans = await buildUpdatePlan(undefined, {
|
|
300
|
+
onProgress: options.json
|
|
301
|
+
? undefined
|
|
302
|
+
: ({ completed, total, packageId }) => console.error(`✓ [${completed}/${total}] ${packageId}`),
|
|
303
|
+
});
|
|
304
|
+
if (!options.json) {
|
|
305
|
+
console.log(appliedPackages.length
|
|
306
|
+
? `Updated safely: ${appliedPackages.join(", ")}`
|
|
307
|
+
: "No safe active package update needed applying.");
|
|
308
|
+
for (const item of failedPackages)
|
|
309
|
+
console.log(`Could not update ${item.packageId}: ${item.error}`);
|
|
310
|
+
const held = plans.filter((plan) => plan.status === "update-available" &&
|
|
311
|
+
(plan.approvalRequired || plan.disabledAgents?.length));
|
|
312
|
+
if (held.length)
|
|
313
|
+
console.log(`Held for explicit review: ${held.map((item) => item.packageId).join(", ")}`);
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
console.log(options.json
|
|
317
|
+
? JSON.stringify({
|
|
318
|
+
profile,
|
|
319
|
+
packages: plans,
|
|
320
|
+
...(applying
|
|
321
|
+
? {
|
|
322
|
+
applied: {
|
|
323
|
+
profileSnapshotId,
|
|
324
|
+
packages: appliedPackages,
|
|
325
|
+
failures: failedPackages,
|
|
326
|
+
},
|
|
327
|
+
}
|
|
328
|
+
: {}),
|
|
329
|
+
}, null, 2)
|
|
330
|
+
: `${formatInstalledProfileStatus(profile)}\n\n${formatUpdatePlan(plans)}`);
|
|
331
|
+
});
|
|
332
|
+
program
|
|
333
|
+
.command("convert")
|
|
334
|
+
.description("Convert a subagent or hook into a static, loss-reported artifact")
|
|
335
|
+
.requiredOption("--kind <kind>", "subagent or hook")
|
|
336
|
+
.requiredOption("--target <target>", "codex-skill, claude-skill, or static-review")
|
|
337
|
+
.requiredOption("--name <name>", "source component name")
|
|
338
|
+
.requiredOption("--input <path>", "UTF-8 instruction or hook source file")
|
|
339
|
+
.requiredOption("--output <directory>", "artifact output directory")
|
|
340
|
+
.option("--yes", "write the artifact; otherwise preview only")
|
|
341
|
+
.option("--json", "emit machine-readable JSON")
|
|
342
|
+
.action(async (options) => {
|
|
343
|
+
const kinds = ["subagent", "hook"];
|
|
344
|
+
const targets = [
|
|
345
|
+
"codex-skill",
|
|
346
|
+
"claude-skill",
|
|
347
|
+
"static-review",
|
|
348
|
+
];
|
|
349
|
+
if (!kinds.includes(options.kind))
|
|
350
|
+
throw new Error("--kind must be subagent or hook");
|
|
351
|
+
if (!targets.includes(options.target))
|
|
352
|
+
throw new Error("--target must be codex-skill, claude-skill, or static-review");
|
|
353
|
+
const result = compileConversion({
|
|
354
|
+
kind: options.kind,
|
|
355
|
+
name: options.name,
|
|
356
|
+
body: await readFile(options.input, "utf8"),
|
|
357
|
+
}, options.target);
|
|
358
|
+
const destination = resolve(options.output, result.relativePath);
|
|
359
|
+
if (options.yes)
|
|
360
|
+
await writeFileAtomically(destination, result.content, 0o600);
|
|
361
|
+
const report = {
|
|
362
|
+
...result,
|
|
363
|
+
...(options.yes
|
|
364
|
+
? { destination }
|
|
365
|
+
: { destination, write: "preview-only" }),
|
|
366
|
+
};
|
|
367
|
+
console.log(options.json
|
|
368
|
+
? JSON.stringify(report, null, 2)
|
|
369
|
+
: `${options.yes ? "Wrote" : "Previewed"} ${destination}.\n` +
|
|
370
|
+
`Preserved: ${result.preserved.join(", ")}. Dropped: ${result.dropped.length} field(s).\n` +
|
|
371
|
+
(result.requiresApproval
|
|
372
|
+
? "Manual approval is required before using this artifact."
|
|
373
|
+
: "No additional approval is required."));
|
|
374
|
+
});
|
|
375
|
+
program
|
|
376
|
+
.command("canary")
|
|
377
|
+
.description("Run a static canary policy gate for a candidate without installing it")
|
|
378
|
+
.requiredOption("--source <directory>", "candidate package directory")
|
|
379
|
+
.requiredOption("--package <id>", "candidate package id")
|
|
380
|
+
.option("--repository <owner/repo>", "candidate repository")
|
|
381
|
+
.option("--commit <sha>", "candidate immutable commit")
|
|
382
|
+
.option("--approve", "approve promotion if a promotion callback is supplied")
|
|
383
|
+
.option("--allow-unready", "allow review findings in the static gate")
|
|
384
|
+
.option("--json", "emit machine-readable JSON")
|
|
385
|
+
.action(async (options) => {
|
|
386
|
+
const result = await runCanary({
|
|
387
|
+
packageId: options.package,
|
|
388
|
+
root: options.source,
|
|
389
|
+
...(options.repository ? { repository: options.repository } : {}),
|
|
390
|
+
...(options.commit ? { commit: options.commit } : {}),
|
|
391
|
+
}, { enabled: true, requireStaticReady: !options.allowUnready }, { approve: options.approve });
|
|
392
|
+
console.log(options.json
|
|
393
|
+
? JSON.stringify(result, null, 2)
|
|
394
|
+
: formatCanaryResult(result));
|
|
395
|
+
if (result.status === "blocked")
|
|
396
|
+
process.exitCode = 1;
|
|
397
|
+
});
|
|
398
|
+
program
|
|
399
|
+
.command("serve")
|
|
400
|
+
.description("Start a loopback-only read-only API for status, health, catalog, and updates")
|
|
401
|
+
.option("--port <port>", "TCP port (0 selects an available port)", "0")
|
|
402
|
+
.action(async (options) => {
|
|
403
|
+
const handle = await startApiServer({ port: Number(options.port) });
|
|
404
|
+
console.log(`Loadout API listening at http://${handle.host}:${handle.port}`);
|
|
405
|
+
await new Promise((resolve) => {
|
|
406
|
+
process.once("SIGINT", resolve);
|
|
407
|
+
process.once("SIGTERM", resolve);
|
|
408
|
+
});
|
|
409
|
+
await handle.close();
|
|
410
|
+
});
|
|
411
|
+
}
|