loadout-ai 0.1.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/LICENSE +21 -0
- package/MASTER_PLAN.md +1711 -0
- package/README.md +453 -0
- package/SECURITY.md +18 -0
- package/SIMPLE_PLAN.md +44 -0
- package/catalog/discovered.json +10469 -0
- package/catalog/packages.json +1150 -0
- package/dashboard/app.js +607 -0
- package/dashboard/index.html +249 -0
- package/dashboard/styles.css +384 -0
- package/dist/src/cli.js +2735 -0
- package/dist/src/core/active-policy.js +233 -0
- package/dist/src/core/active-set.js +382 -0
- package/dist/src/core/adapters.js +254 -0
- package/dist/src/core/adopt.js +92 -0
- package/dist/src/core/agent-health-score.js +475 -0
- package/dist/src/core/agent-inspection.js +152 -0
- package/dist/src/core/agent-versions.js +131 -0
- package/dist/src/core/api.js +90 -0
- package/dist/src/core/atomic-file.js +22 -0
- package/dist/src/core/audit.js +174 -0
- package/dist/src/core/benchmark-campaign.js +496 -0
- package/dist/src/core/benchmark-evidence.js +458 -0
- package/dist/src/core/benchmark-fixtures.js +1157 -0
- package/dist/src/core/benchmark-runner.js +301 -0
- package/dist/src/core/benchmark-trust.js +795 -0
- package/dist/src/core/canary.js +77 -0
- package/dist/src/core/candidate-intelligence.js +661 -0
- package/dist/src/core/catalog-coverage.js +85 -0
- package/dist/src/core/catalog-install.js +186 -0
- package/dist/src/core/catalog-release.js +286 -0
- package/dist/src/core/catalog.js +327 -0
- package/dist/src/core/codex-mcp.js +122 -0
- package/dist/src/core/community.js +86 -0
- package/dist/src/core/compatibility-intelligence.js +465 -0
- package/dist/src/core/completion.js +160 -0
- package/dist/src/core/components.js +138 -0
- package/dist/src/core/conversion.js +56 -0
- package/dist/src/core/credentials.js +377 -0
- package/dist/src/core/demo.js +136 -0
- package/dist/src/core/diff.js +83 -0
- package/dist/src/core/discovery-connector.js +165 -0
- package/dist/src/core/doctor.js +95 -0
- package/dist/src/core/ecosystem-import.js +1072 -0
- package/dist/src/core/evaluate.js +72 -0
- package/dist/src/core/file-lock.js +140 -0
- package/dist/src/core/freshness-alerts.js +230 -0
- package/dist/src/core/github-discovery.js +97 -0
- package/dist/src/core/github.js +148 -0
- package/dist/src/core/head-to-head.js +437 -0
- package/dist/src/core/health-score-evidence.js +169 -0
- package/dist/src/core/health.js +129 -0
- package/dist/src/core/improve.js +223 -0
- package/dist/src/core/install.js +233 -0
- package/dist/src/core/intelligence-feed-build.js +59 -0
- package/dist/src/core/intelligence-feed.js +353 -0
- package/dist/src/core/loadout-badge.js +55 -0
- package/dist/src/core/loadout-card.js +87 -0
- package/dist/src/core/manifest.js +307 -0
- package/dist/src/core/mcp-recipes.js +419 -0
- package/dist/src/core/mcp-registry-discovery.js +455 -0
- package/dist/src/core/mcp.js +287 -0
- package/dist/src/core/model-config.js +102 -0
- package/dist/src/core/observations.js +87 -0
- package/dist/src/core/outcomes.js +164 -0
- package/dist/src/core/package.js +225 -0
- package/dist/src/core/paths.js +214 -0
- package/dist/src/core/portable.js +130 -0
- package/dist/src/core/private-discovery.js +45 -0
- package/dist/src/core/profiles.js +255 -0
- package/dist/src/core/provenance.js +278 -0
- package/dist/src/core/ranking.js +104 -0
- package/dist/src/core/recommend.js +181 -0
- package/dist/src/core/registry-api.js +97 -0
- package/dist/src/core/registry.js +316 -0
- package/dist/src/core/release-claims.js +175 -0
- package/dist/src/core/remove.js +122 -0
- package/dist/src/core/review-queue.js +200 -0
- package/dist/src/core/runtime-tool-recipe.js +315 -0
- package/dist/src/core/runtime-tools.js +453 -0
- package/dist/src/core/safety.js +234 -0
- package/dist/src/core/sandbox.js +78 -0
- package/dist/src/core/scheduler.js +264 -0
- package/dist/src/core/share-report.js +85 -0
- package/dist/src/core/signing.js +115 -0
- package/dist/src/core/skill-compare.js +239 -0
- package/dist/src/core/skill-inventory.js +193 -0
- package/dist/src/core/skill-security.js +340 -0
- package/dist/src/core/skills-sh-discovery.js +336 -0
- package/dist/src/core/skills.js +208 -0
- package/dist/src/core/snapshot.js +210 -0
- package/dist/src/core/source.js +305 -0
- package/dist/src/core/state.js +300 -0
- package/dist/src/core/sync.js +348 -0
- package/dist/src/core/terminal.js +53 -0
- package/dist/src/core/transaction.js +211 -0
- package/dist/src/core/update-watch.js +39 -0
- package/dist/src/core/update.js +216 -0
- package/dist/src/core/upgrade.js +207 -0
- package/dist/src/dashboard.js +418 -0
- package/dist/src/shared/schemas.js +336 -0
- package/dist/src/shared/types.js +1 -0
- package/docs/ACTIVE_SET.md +53 -0
- package/docs/ACTIVE_SET_POLICY.md +45 -0
- package/docs/CANDIDATE_INTELLIGENCE.md +139 -0
- package/docs/CATALOG.md +78 -0
- package/docs/CATALOG_POLICY.md +41 -0
- package/docs/COMMUNITY_DISCOVERY.md +14 -0
- package/docs/COMPATIBILITY_POLICY.md +22 -0
- package/docs/CONVERSION_AND_SANDBOX.md +27 -0
- package/docs/CREDENTIAL_AND_UPDATE_POLICY.md +31 -0
- package/docs/DISCOVERED.md +280 -0
- package/docs/EVALUATION_PROTOCOL.md +40 -0
- package/docs/EVALUATION_PROTOCOL_V1.md +300 -0
- package/docs/FEATURE_TEST_MATRIX.md +802 -0
- package/docs/GITHUB_AUTHORIZATION.md +38 -0
- package/docs/HEAD_TO_HEAD_EVALUATION.md +79 -0
- package/docs/PROVENANCE_AND_COMPARISON.md +72 -0
- package/docs/PROVIDER_CONFIGURATION.md +45 -0
- package/docs/RELEASE_REVIEW.md +117 -0
- package/docs/SAFE_UPDATE_DEMO.md +25 -0
- package/docs/SCHEMA_DECISIONS.md +25 -0
- package/docs/TEAM_POLICY.md +18 -0
- package/docs/TESTING.md +190 -0
- package/package.json +82 -0
|
@@ -0,0 +1,307 @@
|
|
|
1
|
+
import { readFile, writeFile } from "node:fs/promises";
|
|
2
|
+
import { resolve } from "node:path";
|
|
3
|
+
import { formatSchemaError, loadoutManifestSchema } from "../shared/schemas.js";
|
|
4
|
+
import { readInstallState } from "./state.js";
|
|
5
|
+
import { writeFileAtomically } from "./atomic-file.js";
|
|
6
|
+
const AGENTS = new Set([
|
|
7
|
+
"claude-code",
|
|
8
|
+
"codex",
|
|
9
|
+
"cursor",
|
|
10
|
+
"gemini-cli",
|
|
11
|
+
"opencode",
|
|
12
|
+
"hermes",
|
|
13
|
+
"windsurf",
|
|
14
|
+
"cline",
|
|
15
|
+
"github-copilot",
|
|
16
|
+
"roo-code",
|
|
17
|
+
"kiro-cli",
|
|
18
|
+
"junie",
|
|
19
|
+
]);
|
|
20
|
+
function source(value, label) {
|
|
21
|
+
if (!value || typeof value !== "object")
|
|
22
|
+
throw new Error(`${label}.source must be an object`);
|
|
23
|
+
const item = value;
|
|
24
|
+
if (item.type === "catalog" && typeof item.id === "string" && item.id)
|
|
25
|
+
return { type: "catalog", id: item.id };
|
|
26
|
+
if (item.type === "github" &&
|
|
27
|
+
typeof item.repository === "string" &&
|
|
28
|
+
item.repository) {
|
|
29
|
+
return {
|
|
30
|
+
type: "github",
|
|
31
|
+
repository: item.repository,
|
|
32
|
+
...(typeof item.ref === "string" ? { ref: item.ref } : {}),
|
|
33
|
+
...(typeof item.path === "string" ? { path: item.path } : {}),
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
if (item.type === "git" && typeof item.url === "string" && item.url) {
|
|
37
|
+
return {
|
|
38
|
+
type: "git",
|
|
39
|
+
url: item.url,
|
|
40
|
+
...(typeof item.ref === "string" ? { ref: item.ref } : {}),
|
|
41
|
+
...(typeof item.path === "string" ? { path: item.path } : {}),
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
if (item.type === "registry" &&
|
|
45
|
+
typeof item.name === "string" &&
|
|
46
|
+
item.name &&
|
|
47
|
+
typeof item.version === "string" &&
|
|
48
|
+
item.version)
|
|
49
|
+
return { type: "registry", name: item.name, version: item.version };
|
|
50
|
+
if (item.type === "remote-registry" &&
|
|
51
|
+
typeof item.registry === "string" &&
|
|
52
|
+
item.registry &&
|
|
53
|
+
typeof item.name === "string" &&
|
|
54
|
+
item.name &&
|
|
55
|
+
typeof item.version === "string" &&
|
|
56
|
+
item.version)
|
|
57
|
+
return {
|
|
58
|
+
type: "remote-registry",
|
|
59
|
+
registry: item.registry,
|
|
60
|
+
name: item.name,
|
|
61
|
+
version: item.version,
|
|
62
|
+
};
|
|
63
|
+
if (item.type === "local" && typeof item.path === "string" && item.path)
|
|
64
|
+
return { type: "local", path: item.path };
|
|
65
|
+
throw new Error(`${label}.source is not a supported catalog, github, git, registry, remote-registry, or local source`);
|
|
66
|
+
}
|
|
67
|
+
function agents(value, label) {
|
|
68
|
+
if (!Array.isArray(value))
|
|
69
|
+
throw new Error(`${label} must be an array`);
|
|
70
|
+
const result = value.map((agent) => {
|
|
71
|
+
if (typeof agent !== "string" || !AGENTS.has(agent))
|
|
72
|
+
throw new Error(`${label} contains unsupported agent '${String(agent)}'`);
|
|
73
|
+
return agent;
|
|
74
|
+
});
|
|
75
|
+
if (new Set(result).size !== result.length)
|
|
76
|
+
throw new Error(`${label} contains duplicates`);
|
|
77
|
+
return result;
|
|
78
|
+
}
|
|
79
|
+
export function parseManifest(value) {
|
|
80
|
+
if (!value || typeof value !== "object")
|
|
81
|
+
throw new Error("Manifest must be an object");
|
|
82
|
+
const item = value;
|
|
83
|
+
if (item.schemaVersion !== 1)
|
|
84
|
+
throw new Error("Manifest schemaVersion must be 1");
|
|
85
|
+
if (typeof item.name !== "string" || !item.name.trim())
|
|
86
|
+
throw new Error("Manifest name is required");
|
|
87
|
+
if (item.scope !== "project" && item.scope !== "global")
|
|
88
|
+
throw new Error("Manifest scope must be project or global");
|
|
89
|
+
if (!Array.isArray(item.packages))
|
|
90
|
+
throw new Error("Manifest packages must be an array");
|
|
91
|
+
const parsedPackages = item.packages.map((entry, index) => {
|
|
92
|
+
if (!entry || typeof entry !== "object")
|
|
93
|
+
throw new Error(`packages[${index}] must be an object`);
|
|
94
|
+
const pkg = entry;
|
|
95
|
+
if (typeof pkg.id !== "string" || !pkg.id.trim())
|
|
96
|
+
throw new Error(`packages[${index}].id is required`);
|
|
97
|
+
const dependsOn = pkg.dependsOn === undefined
|
|
98
|
+
? undefined
|
|
99
|
+
: (() => {
|
|
100
|
+
if (!Array.isArray(pkg.dependsOn) ||
|
|
101
|
+
pkg.dependsOn.some((id) => typeof id !== "string" || !id))
|
|
102
|
+
throw new Error(`packages[${index}].dependsOn must contain package ids`);
|
|
103
|
+
if (new Set(pkg.dependsOn).size !== pkg.dependsOn.length)
|
|
104
|
+
throw new Error(`packages[${index}].dependsOn contains duplicates`);
|
|
105
|
+
return pkg.dependsOn;
|
|
106
|
+
})();
|
|
107
|
+
const mcp = pkg.mcp === undefined
|
|
108
|
+
? undefined
|
|
109
|
+
: (() => {
|
|
110
|
+
if (!pkg.mcp ||
|
|
111
|
+
typeof pkg.mcp !== "object" ||
|
|
112
|
+
Array.isArray(pkg.mcp))
|
|
113
|
+
throw new Error(`packages[${index}].mcp must be an object`);
|
|
114
|
+
const value = pkg.mcp;
|
|
115
|
+
if (typeof value.config !== "string" || !value.config)
|
|
116
|
+
throw new Error(`packages[${index}].mcp.config is required`);
|
|
117
|
+
if (value.servers !== undefined &&
|
|
118
|
+
(!Array.isArray(value.servers) ||
|
|
119
|
+
value.servers.some((name) => typeof name !== "string" || !name)))
|
|
120
|
+
throw new Error(`packages[${index}].mcp.servers must contain server names`);
|
|
121
|
+
return {
|
|
122
|
+
config: value.config,
|
|
123
|
+
...(value.servers
|
|
124
|
+
? { servers: value.servers }
|
|
125
|
+
: {}),
|
|
126
|
+
};
|
|
127
|
+
})();
|
|
128
|
+
const rootFiles = pkg.rootFiles === undefined
|
|
129
|
+
? undefined
|
|
130
|
+
: (() => {
|
|
131
|
+
if (!Array.isArray(pkg.rootFiles) ||
|
|
132
|
+
pkg.rootFiles.some((entry) => !entry ||
|
|
133
|
+
typeof entry !== "object" ||
|
|
134
|
+
typeof entry.source !==
|
|
135
|
+
"string" ||
|
|
136
|
+
typeof entry.target !==
|
|
137
|
+
"string"))
|
|
138
|
+
throw new Error(`packages[${index}].rootFiles must contain source and target paths`);
|
|
139
|
+
return pkg.rootFiles;
|
|
140
|
+
})();
|
|
141
|
+
return {
|
|
142
|
+
id: pkg.id,
|
|
143
|
+
source: source(pkg.source, `packages[${index}]`),
|
|
144
|
+
...(pkg.agents === undefined
|
|
145
|
+
? {}
|
|
146
|
+
: { agents: agents(pkg.agents, `packages[${index}].agents`) }),
|
|
147
|
+
...(dependsOn ? { dependsOn } : {}),
|
|
148
|
+
...(typeof pkg.includeDevDependencies === "boolean"
|
|
149
|
+
? { includeDevDependencies: pkg.includeDevDependencies }
|
|
150
|
+
: {}),
|
|
151
|
+
...(mcp ? { mcp } : {}),
|
|
152
|
+
...(rootFiles ? { rootFiles } : {}),
|
|
153
|
+
...(typeof pkg.enabled === "boolean" ? { enabled: pkg.enabled } : {}),
|
|
154
|
+
};
|
|
155
|
+
});
|
|
156
|
+
const ids = parsedPackages.map((pkg) => pkg.id);
|
|
157
|
+
if (new Set(ids).size !== ids.length)
|
|
158
|
+
throw new Error("Manifest package ids must be unique");
|
|
159
|
+
const known = new Set(ids);
|
|
160
|
+
for (const pkg of parsedPackages)
|
|
161
|
+
for (const dependency of pkg.dependsOn ?? [])
|
|
162
|
+
if (!known.has(dependency))
|
|
163
|
+
throw new Error(`Package '${pkg.id}' depends on missing package '${dependency}'`);
|
|
164
|
+
orderManifestPackages(parsedPackages);
|
|
165
|
+
const manifest = {
|
|
166
|
+
schemaVersion: 1,
|
|
167
|
+
name: item.name.trim(),
|
|
168
|
+
scope: item.scope,
|
|
169
|
+
agents: agents(item.agents, "agents"),
|
|
170
|
+
...(typeof item.profile === "string" ? { profile: item.profile } : {}),
|
|
171
|
+
packages: parsedPackages,
|
|
172
|
+
...(item.policy && typeof item.policy === "object"
|
|
173
|
+
? { policy: item.policy }
|
|
174
|
+
: {}),
|
|
175
|
+
};
|
|
176
|
+
const schema = loadoutManifestSchema.safeParse(manifest);
|
|
177
|
+
if (!schema.success)
|
|
178
|
+
throw new Error(`Manifest schema is invalid: ${formatSchemaError(schema.error)}`);
|
|
179
|
+
return schema.data;
|
|
180
|
+
}
|
|
181
|
+
export function orderManifestPackages(packages) {
|
|
182
|
+
const byId = new Map(packages.map((pkg) => [pkg.id, pkg]));
|
|
183
|
+
const visiting = new Set();
|
|
184
|
+
const visited = new Set();
|
|
185
|
+
const result = [];
|
|
186
|
+
function visit(id, chain) {
|
|
187
|
+
if (visited.has(id))
|
|
188
|
+
return;
|
|
189
|
+
if (visiting.has(id))
|
|
190
|
+
throw new Error(`Manifest dependency cycle: ${[...chain, id].join(" -> ")}`);
|
|
191
|
+
const pkg = byId.get(id);
|
|
192
|
+
if (!pkg)
|
|
193
|
+
throw new Error(`Missing manifest dependency '${id}'`);
|
|
194
|
+
visiting.add(id);
|
|
195
|
+
for (const dependency of pkg.dependsOn ?? [])
|
|
196
|
+
visit(dependency, [...chain, id]);
|
|
197
|
+
visiting.delete(id);
|
|
198
|
+
visited.add(id);
|
|
199
|
+
result.push(pkg);
|
|
200
|
+
}
|
|
201
|
+
for (const pkg of packages)
|
|
202
|
+
visit(pkg.id, []);
|
|
203
|
+
return result;
|
|
204
|
+
}
|
|
205
|
+
export async function readManifest(path = "loadout.json") {
|
|
206
|
+
try {
|
|
207
|
+
return parseManifest(JSON.parse(await readFile(resolve(path), "utf8")));
|
|
208
|
+
}
|
|
209
|
+
catch (error) {
|
|
210
|
+
if (error &&
|
|
211
|
+
typeof error === "object" &&
|
|
212
|
+
"code" in error &&
|
|
213
|
+
error.code === "ENOENT")
|
|
214
|
+
throw new Error(`Loadout manifest not found: ${resolve(path)}`);
|
|
215
|
+
throw new Error(`Invalid Loadout manifest at ${resolve(path)}: ${error instanceof Error ? error.message : String(error)}`);
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
export async function initManifest(path = "loadout.json", options = {}) {
|
|
219
|
+
const manifest = {
|
|
220
|
+
schemaVersion: 1,
|
|
221
|
+
name: options.name ?? "my-loadout",
|
|
222
|
+
scope: options.scope ?? "project",
|
|
223
|
+
agents: options.agents ?? ["codex", "claude-code"],
|
|
224
|
+
profile: "stable",
|
|
225
|
+
packages: [],
|
|
226
|
+
};
|
|
227
|
+
await writeFile(resolve(path), `${JSON.stringify(manifest, null, 2)}\n`, {
|
|
228
|
+
flag: "wx",
|
|
229
|
+
});
|
|
230
|
+
return manifest;
|
|
231
|
+
}
|
|
232
|
+
async function writeManifest(manifest, path) {
|
|
233
|
+
await writeFileAtomically(resolve(path), `${JSON.stringify(manifest, null, 2)}\n`, 0o644);
|
|
234
|
+
}
|
|
235
|
+
export async function addManifestPackage(path, pkg) {
|
|
236
|
+
const manifest = await readManifest(path);
|
|
237
|
+
if (manifest.packages.some((item) => item.id === pkg.id))
|
|
238
|
+
throw new Error(`Manifest already contains package '${pkg.id}'`);
|
|
239
|
+
const updated = parseManifest({
|
|
240
|
+
...manifest,
|
|
241
|
+
packages: [...manifest.packages, pkg],
|
|
242
|
+
});
|
|
243
|
+
await writeManifest(updated, path);
|
|
244
|
+
return updated;
|
|
245
|
+
}
|
|
246
|
+
export async function removeManifestPackage(path, packageId) {
|
|
247
|
+
const manifest = await readManifest(path);
|
|
248
|
+
const packages = manifest.packages.filter((item) => item.id !== packageId);
|
|
249
|
+
if (packages.length === manifest.packages.length)
|
|
250
|
+
throw new Error(`Manifest does not contain package '${packageId}'`);
|
|
251
|
+
const updated = parseManifest({ ...manifest, packages });
|
|
252
|
+
await writeManifest(updated, path);
|
|
253
|
+
return updated;
|
|
254
|
+
}
|
|
255
|
+
export async function applyProfileToManifest(path, profile, packages) {
|
|
256
|
+
const manifest = await readManifest(path);
|
|
257
|
+
const existing = new Set(manifest.packages.map((pkg) => pkg.id));
|
|
258
|
+
const additions = packages
|
|
259
|
+
.filter((pkg) => !existing.has(pkg.id))
|
|
260
|
+
.map((pkg) => ({
|
|
261
|
+
id: pkg.id,
|
|
262
|
+
source: { type: "github", repository: pkg.repository },
|
|
263
|
+
}));
|
|
264
|
+
const updated = parseManifest({
|
|
265
|
+
...manifest,
|
|
266
|
+
profile,
|
|
267
|
+
packages: [...manifest.packages, ...additions],
|
|
268
|
+
});
|
|
269
|
+
await writeManifest(updated, path);
|
|
270
|
+
return updated;
|
|
271
|
+
}
|
|
272
|
+
export async function writeLockfile(manifest, path = "loadout.lock") {
|
|
273
|
+
const state = await readInstallState();
|
|
274
|
+
const sourceById = new Map(manifest.packages.map((pkg) => [pkg.id, pkg.source]));
|
|
275
|
+
const lockfile = {
|
|
276
|
+
schemaVersion: 1,
|
|
277
|
+
manifestName: manifest.name,
|
|
278
|
+
generatedAt: new Date().toISOString(),
|
|
279
|
+
packages: state.installs
|
|
280
|
+
.filter((entry) => sourceById.has(entry.packageId))
|
|
281
|
+
.map((entry) => ({
|
|
282
|
+
id: entry.packageId,
|
|
283
|
+
source: sourceById.get(entry.packageId),
|
|
284
|
+
repository: entry.repository,
|
|
285
|
+
resolvedCommit: entry.resolvedCommit,
|
|
286
|
+
targetAgents: entry.targetAgents,
|
|
287
|
+
files: entry.files,
|
|
288
|
+
installedAt: entry.installedAt,
|
|
289
|
+
...(manifest.packages.find((pkg) => pkg.id === entry.packageId)
|
|
290
|
+
?.dependsOn?.length
|
|
291
|
+
? {
|
|
292
|
+
dependencies: manifest.packages.find((pkg) => pkg.id === entry.packageId).dependsOn,
|
|
293
|
+
}
|
|
294
|
+
: {}),
|
|
295
|
+
})),
|
|
296
|
+
mcpServers: (state.mcpInstalls ?? [])
|
|
297
|
+
.filter((entry) => sourceById.has(entry.packageId))
|
|
298
|
+
.map(({ packageId, configPath, serverName, fingerprint }) => ({
|
|
299
|
+
packageId,
|
|
300
|
+
configPath,
|
|
301
|
+
serverName,
|
|
302
|
+
fingerprint,
|
|
303
|
+
})),
|
|
304
|
+
};
|
|
305
|
+
await writeFileAtomically(resolve(path), `${JSON.stringify(lockfile, null, 2)}\n`, 0o600);
|
|
306
|
+
return lockfile;
|
|
307
|
+
}
|