@michaelfromyeg/loom-core 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/dist/index.js ADDED
@@ -0,0 +1,858 @@
1
+ import { existsSync, statSync, mkdirSync, writeFileSync, readFileSync, mkdtempSync, cpSync, rmSync, readdirSync } from 'fs';
2
+ import { join, isAbsolute, resolve, dirname, relative } from 'path';
3
+ import { refOf, kindOf, leafNameOf, fqid, targetsOf, ALL_TARGETS, loadPlugin, loadManifest, Marketplace, validate, Lockfile, stringifyDocument } from '@michaelfromyeg/loom-schema';
4
+ import { parseFrontmatter } from '@michaelfromyeg/loom-adapter-kit';
5
+ import semver from 'semver';
6
+ import { homedir, tmpdir } from 'os';
7
+ import { createHash, generateKeyPairSync, sign, verify } from 'crypto';
8
+ import { execa } from 'execa';
9
+
10
+ // src/api.ts
11
+
12
+ // src/diagnostics.ts
13
+ var Diagnostics = class {
14
+ items = [];
15
+ error(where, message) {
16
+ this.items.push({ severity: "error", where, message });
17
+ }
18
+ warn(where, message) {
19
+ this.items.push({ severity: "warning", where, message });
20
+ }
21
+ info(where, message) {
22
+ this.items.push({ severity: "info", where, message });
23
+ }
24
+ get hasErrors() {
25
+ return this.items.some((d) => d.severity === "error");
26
+ }
27
+ get errors() {
28
+ return this.items.filter((d) => d.severity === "error");
29
+ }
30
+ };
31
+ var CompileError = class extends Error {
32
+ constructor(message, diagnostics) {
33
+ super(message);
34
+ this.diagnostics = diagnostics;
35
+ this.name = "CompileError";
36
+ }
37
+ diagnostics;
38
+ };
39
+
40
+ // src/namespace.ts
41
+ function resolveAliases(components) {
42
+ const byLeaf = /* @__PURE__ */ new Map();
43
+ for (const c of components) {
44
+ const ids = byLeaf.get(c.leaf) ?? [];
45
+ ids.push(c.id);
46
+ byLeaf.set(c.leaf, ids);
47
+ }
48
+ const aliases = {};
49
+ const collisions = [];
50
+ for (const [leaf, ids] of byLeaf) {
51
+ if (ids.length === 1) aliases[leaf] = ids[0];
52
+ else collisions.push({ leaf, ids: [...ids].sort() });
53
+ }
54
+ return { aliases, collisions };
55
+ }
56
+ function validatePlugin(fb, diags) {
57
+ fb.plugin.components.forEach((component, i) => {
58
+ const where = `components[${i}]`;
59
+ const ref = refOf(component);
60
+ const abs = join(fb.root, ref);
61
+ if (!existsSync(abs)) {
62
+ diags.error(where, `referenced path "${ref}" does not exist`);
63
+ return;
64
+ }
65
+ switch (kindOf(component)) {
66
+ case "skill":
67
+ validateSkill(fb, ref, abs, where, diags);
68
+ break;
69
+ case "mcp":
70
+ validateMcp(fb, ref, abs, where, diags);
71
+ break;
72
+ case "agent":
73
+ validateMarkdownComponent(fb, ref, abs, where, diags);
74
+ break;
75
+ }
76
+ });
77
+ }
78
+ function validateSkill(fb, ref, abs, where, diags) {
79
+ if (!statSync(abs).isDirectory()) {
80
+ diags.error(where, `skill "${ref}" must be a directory containing SKILL.md`);
81
+ return;
82
+ }
83
+ const skillMd = join(abs, "SKILL.md");
84
+ if (!existsSync(skillMd)) {
85
+ diags.error(where, `skill "${ref}" is missing SKILL.md`);
86
+ return;
87
+ }
88
+ const { data } = parseFrontmatter(fb.read(join(ref, "SKILL.md")).toString("utf8"));
89
+ if (!data.name) diags.error(`${where}.skill`, "SKILL.md frontmatter is missing `name`");
90
+ checkDescription(`${where}.skill`, data.description, diags);
91
+ }
92
+ function validateMcp(fb, ref, abs, where, diags) {
93
+ if (!statSync(abs).isDirectory()) {
94
+ diags.error(where, `mcp "${ref}" must be a directory containing server.json`);
95
+ return;
96
+ }
97
+ const serverJsonPath = join(abs, "server.json");
98
+ if (!existsSync(serverJsonPath)) {
99
+ diags.error(where, `mcp "${ref}" is missing server.json`);
100
+ return;
101
+ }
102
+ let server;
103
+ try {
104
+ server = JSON.parse(fb.read(join(ref, "server.json")).toString("utf8"));
105
+ } catch (err) {
106
+ diags.error(`${where}.mcp`, `server.json is not valid JSON: ${err.message}`);
107
+ return;
108
+ }
109
+ if (!server.name) diags.error(`${where}.mcp`, "server.json is missing `name`");
110
+ const hasRunnable = server.packages || server.remotes || server.command;
111
+ if (!hasRunnable) {
112
+ diags.warn(`${where}.mcp`, "server.json declares no `packages`, `remotes`, or `command`");
113
+ }
114
+ checkDescription(`${where}.mcp`, server.description, diags);
115
+ }
116
+ function validateMarkdownComponent(fb, ref, abs, where, diags) {
117
+ if (statSync(abs).isDirectory()) return;
118
+ const { data } = parseFrontmatter(fb.read(ref).toString("utf8"));
119
+ if (!data.name) diags.warn(`${where}.agent`, "agent frontmatter is missing `name`");
120
+ checkDescription(`${where}.agent`, data.description, diags);
121
+ }
122
+ function checkDescription(where, description, diags) {
123
+ if (!description) {
124
+ diags.error(where, "missing `description` (required for discovery + the tested badge)");
125
+ return;
126
+ }
127
+ if (String(description).trim().length < 16) {
128
+ diags.warn(where, "description is very short; harnesses route on it -- make it specific");
129
+ }
130
+ }
131
+ var LOOM_VERSION = "0.1.0";
132
+ function checkMinVersion(min) {
133
+ if (!min) return null;
134
+ const cleaned = semver.valid(semver.coerce(min) ?? min);
135
+ if (!cleaned) return `loom_min_version "${min}" is not a valid semver`;
136
+ if (semver.lt(LOOM_VERSION, cleaned)) {
137
+ return `plugin requires Loom >= ${min}, but this is ${LOOM_VERSION}`;
138
+ }
139
+ return null;
140
+ }
141
+
142
+ // src/compile.ts
143
+ function synthMarketplace(plugin) {
144
+ return {
145
+ name: plugin.name,
146
+ owner: plugin.owner,
147
+ ...plugin.description ? { description: plugin.description } : {},
148
+ entries: [
149
+ {
150
+ name: plugin.name,
151
+ source: `./plugins/${plugin.name}`,
152
+ ...plugin.description ? { description: plugin.description } : {},
153
+ version: plugin.version
154
+ }
155
+ ]
156
+ };
157
+ }
158
+ function staticPass(fb) {
159
+ const diagnostics = new Diagnostics();
160
+ const { plugin } = fb;
161
+ const id = `${plugin.owner.namespace}/${plugin.name}`;
162
+ const minErr = checkMinVersion(plugin.loom_min_version);
163
+ if (minErr) diagnostics.error("loom_min_version", minErr);
164
+ validatePlugin(fb, diagnostics);
165
+ const resolved = plugin.components.map((component) => ({
166
+ id: fqid(plugin.owner.namespace, plugin.name, leafNameOf(component)),
167
+ leaf: leafNameOf(component),
168
+ kind: kindOf(component),
169
+ component
170
+ }));
171
+ const { aliases, collisions } = resolveAliases(resolved);
172
+ for (const c of collisions) {
173
+ diagnostics.error(
174
+ `components.${c.leaf}`,
175
+ `ambiguous component name "${c.leaf}": ${c.ids.join(" vs ")}; qualify or choose which keeps the bare alias`
176
+ );
177
+ }
178
+ return { id, diagnostics, aliases, resolved };
179
+ }
180
+ function compile(fb, opts) {
181
+ const { plugin } = fb;
182
+ const pass = staticPass(fb);
183
+ const { id, diagnostics } = pass;
184
+ const onlySet = opts.only && opts.only.length > 0 ? new Set(opts.only) : null;
185
+ if (onlySet) {
186
+ for (const leaf of onlySet) {
187
+ if (!pass.resolved.some((rc) => rc.leaf === leaf)) {
188
+ diagnostics.error(
189
+ "only",
190
+ `component "${leaf}" not found; available: ${pass.resolved.map((r) => r.leaf).join(", ")}`
191
+ );
192
+ }
193
+ }
194
+ }
195
+ const selected = onlySet ? pass.resolved.filter((rc) => onlySet.has(rc.leaf)) : pass.resolved;
196
+ const effectivePlugin = onlySet ? { ...plugin, components: selected.map((rc) => rc.component) } : plugin;
197
+ const aliases = onlySet ? Object.fromEntries(
198
+ Object.entries(pass.aliases).filter(([, cid]) => selected.some((rc) => rc.id === cid))
199
+ ) : pass.aliases;
200
+ const reverseAlias = new Map(Object.entries(aliases).map(([leaf, cid]) => [cid, leaf]));
201
+ const requested = opts.targets ?? opts.registry.targets;
202
+ const targets = [];
203
+ for (const target of requested) {
204
+ const adapter = opts.registry.get(target);
205
+ if (!adapter) {
206
+ if (opts.targets) diagnostics.warn(target, `no adapter registered for target "${target}"`);
207
+ continue;
208
+ }
209
+ const ctx = {
210
+ plugin: effectivePlugin,
211
+ read: fb.read,
212
+ list: fb.list,
213
+ aliasFor: (componentId) => reverseAlias.get(componentId) ?? componentId
214
+ };
215
+ const pluginArtifacts = [];
216
+ for (const rc of selected) {
217
+ if (!targetsOf(rc.component, ALL_TARGETS).includes(target)) continue;
218
+ for (const a of adapter.transform(rc.component, ctx)) {
219
+ pluginArtifacts.push({ componentId: rc.id, artifact: a });
220
+ }
221
+ }
222
+ for (const a of adapter.emitManifest(effectivePlugin, ctx)) {
223
+ pluginArtifacts.push({ componentId: id, artifact: a });
224
+ }
225
+ targets.push({ target, adapter, artifacts: pluginArtifacts });
226
+ }
227
+ return { fb, id, components: selected, aliases, diagnostics, targets };
228
+ }
229
+ function configVarsOf(component) {
230
+ return "config" in component && component.config ? component.config : [];
231
+ }
232
+ function resolveConfig(plugin, cwd, env = process.env) {
233
+ const resolved = [];
234
+ const values = {};
235
+ for (const component of plugin.components) {
236
+ for (const v of configVarsOf(component)) {
237
+ const fromEnv = env[v.env];
238
+ if (fromEnv !== void 0) {
239
+ values[v.env] = fromEnv;
240
+ resolved.push({ env: v.env, source: "env", secret: v.secret });
241
+ } else if (v.default !== void 0) {
242
+ values[v.env] = v.default;
243
+ resolved.push({ env: v.env, source: "default", secret: v.secret });
244
+ } else {
245
+ resolved.push({ env: v.env, source: "missing", secret: v.secret });
246
+ }
247
+ }
248
+ }
249
+ if (Object.keys(values).length === 0) return { resolved, path: null };
250
+ const dir = join(cwd, ".loom");
251
+ mkdirSync(dir, { recursive: true });
252
+ writeFileSync(join(dir, ".gitignore"), "*\n");
253
+ const path = join(dir, "secrets.local.json");
254
+ writeFileSync(path, `${JSON.stringify(values, null, 2)}
255
+ `);
256
+ return { resolved, path };
257
+ }
258
+ function walkFiles(root, dir) {
259
+ if (!existsSync(dir) || !statSync(dir).isDirectory()) return [];
260
+ const out = [];
261
+ for (const entry of readdirSync(dir).sort()) {
262
+ const abs = join(dir, entry);
263
+ if (statSync(abs).isDirectory()) out.push(...walkFiles(root, abs));
264
+ else out.push(relative(root, abs));
265
+ }
266
+ return out;
267
+ }
268
+ var MANIFEST_NAMES = ["loom.yaml", "loom.yml", "loom.json5", "loom.json"];
269
+ function fileAccessors(root) {
270
+ return {
271
+ read: (relPath) => readFileSync(join(root, relPath)),
272
+ list: (relDir) => walkFiles(root, join(root, relDir))
273
+ };
274
+ }
275
+ function loadPluginDir(dir) {
276
+ const manifestPath = MANIFEST_NAMES.map((n) => join(dir, n)).find((p) => existsSync(p));
277
+ if (!manifestPath) {
278
+ return {
279
+ ok: false,
280
+ issues: [
281
+ { path: dir, message: `no plugin manifest found (one of: ${MANIFEST_NAMES.join(", ")})` }
282
+ ]
283
+ };
284
+ }
285
+ const text = readFileSync(manifestPath, "utf8");
286
+ const parsed = loadPlugin(text, { filename: manifestPath });
287
+ if (!parsed.ok) return parsed;
288
+ return {
289
+ ok: true,
290
+ value: { plugin: parsed.value, root: dir, manifestPath, ...fileAccessors(dir) }
291
+ };
292
+ }
293
+ var MARKETPLACE_NAMES = [
294
+ "marketplace.yaml",
295
+ "marketplace.yml",
296
+ "marketplace.json5",
297
+ "marketplace.json"
298
+ ];
299
+ function hasMarketplaceManifest(dir) {
300
+ return MARKETPLACE_NAMES.some((n) => existsSync(join(dir, n)));
301
+ }
302
+ function loadMarketplaceDir(dir) {
303
+ const manifestPath = MARKETPLACE_NAMES.map((n) => join(dir, n)).find((p) => existsSync(p));
304
+ if (!manifestPath) {
305
+ return {
306
+ ok: false,
307
+ issues: [
308
+ {
309
+ path: dir,
310
+ message: `no marketplace manifest found (one of: ${MARKETPLACE_NAMES.join(", ")})`
311
+ }
312
+ ]
313
+ };
314
+ }
315
+ const text = readFileSync(manifestPath, "utf8");
316
+ const parsed = loadManifest(Marketplace, text, { filename: manifestPath });
317
+ if (!parsed.ok) return parsed;
318
+ return { ok: true, value: { marketplace: parsed.value, root: dir, manifestPath } };
319
+ }
320
+ var CACHE_DIR = join(homedir(), ".loom", "cache");
321
+ function parseSource(src) {
322
+ if (src.startsWith("./") || src.startsWith("../") || isAbsolute(src)) {
323
+ return { kind: "local", path: src };
324
+ }
325
+ if (src.startsWith("github:")) {
326
+ const [repo, ref] = src.slice("github:".length).split("#");
327
+ return ref ? { kind: "github", repo, ref } : { kind: "github", repo };
328
+ }
329
+ if (src.startsWith("npm:")) {
330
+ const spec = src.slice("npm:".length);
331
+ const at = spec.lastIndexOf("@");
332
+ if (at > 0) return { kind: "npm", pkg: spec.slice(0, at), version: spec.slice(at + 1) };
333
+ return { kind: "npm", pkg: spec };
334
+ }
335
+ if (src.startsWith("git@") || src.startsWith("git+") || src.startsWith("file://") || /^https?:\/\//.test(src)) {
336
+ const [url, ref] = src.replace(/^git\+/, "").split("#");
337
+ return ref ? { kind: "git", url, ref } : { kind: "git", url };
338
+ }
339
+ if (/^[\w.-]+\/[\w.-]+$/.test(src)) return { kind: "github", repo: src };
340
+ return { kind: "local", path: src };
341
+ }
342
+ function cacheDirFor(key) {
343
+ return join(CACHE_DIR, createHash("sha256").update(key).digest("hex").slice(0, 16));
344
+ }
345
+ var SHA_RE = /^[0-9a-f]{40}$/i;
346
+ async function gitFetch(url, ref) {
347
+ mkdirSync(CACHE_DIR, { recursive: true });
348
+ const dir = cacheDirFor(`${url}@${ref ?? "default"}`);
349
+ const isSha = ref !== void 0 && SHA_RE.test(ref);
350
+ if (!existsSync(join(dir, ".git"))) {
351
+ if (isSha) {
352
+ await execa("git", ["clone", url, dir]);
353
+ await execa("git", ["checkout", ref], { cwd: dir });
354
+ } else {
355
+ const branchArgs = ref ? ["--branch", ref] : [];
356
+ await execa("git", ["clone", "--depth", "1", ...branchArgs, url, dir]);
357
+ }
358
+ } else {
359
+ await execa("git", ["fetch", "origin", ...ref ? [ref] : []], { cwd: dir });
360
+ await execa("git", ["checkout", ref ?? "FETCH_HEAD"], { cwd: dir, reject: false });
361
+ }
362
+ const sha = (await execa("git", ["rev-parse", "HEAD"], { cwd: dir })).stdout.trim();
363
+ return { dir, sha };
364
+ }
365
+ async function resolvePluginRefFull(source, fromRoot) {
366
+ const src = parseSource(source);
367
+ if (src.kind === "local") {
368
+ const dir = isAbsolute(src.path) ? src.path : resolve(fromRoot, src.path);
369
+ return { fb: loadOrThrow(dir, source), ref: "local", sha: "" };
370
+ }
371
+ if (src.kind === "github" || src.kind === "git") {
372
+ const url = src.kind === "github" ? `https://github.com/${src.repo}.git` : src.url;
373
+ const { dir, sha } = await gitFetch(url, src.ref);
374
+ return { fb: loadOrThrow(dir, source), ref: src.ref ?? "default", sha };
375
+ }
376
+ throw new Error(`npm source resolution ("${source}") is not implemented yet`);
377
+ }
378
+ async function resolvePluginRef(source, fromRoot) {
379
+ return (await resolvePluginRefFull(source, fromRoot)).fb;
380
+ }
381
+ function loadOrThrow(dir, source) {
382
+ const loaded = loadPluginDir(dir);
383
+ if (!loaded.ok) {
384
+ throw new Error(
385
+ `failed to load "${source}":
386
+ ${loaded.issues.map((i) => ` ${i.path}: ${i.message}`).join("\n")}`
387
+ );
388
+ }
389
+ return loaded.value;
390
+ }
391
+ async function resolveDependency(dep, fromRoot) {
392
+ return resolvePluginRefFull(dep.plugin, fromRoot);
393
+ }
394
+ async function gitInfo(dir) {
395
+ try {
396
+ const sha = (await execa("git", ["rev-parse", "HEAD"], { cwd: dir })).stdout.trim();
397
+ let ref = "HEAD";
398
+ try {
399
+ ref = (await execa("git", ["describe", "--tags", "--exact-match"], { cwd: dir })).stdout.trim();
400
+ } catch {
401
+ try {
402
+ ref = (await execa("git", ["rev-parse", "--abbrev-ref", "HEAD"], { cwd: dir })).stdout.trim();
403
+ } catch {
404
+ }
405
+ }
406
+ return { ref, sha };
407
+ } catch {
408
+ return { ref: "local", sha: "" };
409
+ }
410
+ }
411
+
412
+ // src/deps.ts
413
+ var KIND_KEY = {
414
+ skill: "skill",
415
+ mcp: "mcp",
416
+ agent: "agent",
417
+ hook: "hook",
418
+ command: "command",
419
+ passthrough: "passthrough"
420
+ };
421
+ function rewriteRef(c, newRef) {
422
+ return { ...c, [KIND_KEY[kindOf(c)]]: newRef };
423
+ }
424
+ var skipGit = (src) => !/\/\.git(\/|$)/.test(src);
425
+ async function resolveDependencies(fb, tmpRoot) {
426
+ const depends = fb.plugin.depends ?? [];
427
+ if (depends.length === 0) return { fb, dependencies: [] };
428
+ const merged = mkdtempSync(join(tmpRoot ?? tmpdir(), "loom-merged-"));
429
+ cpSync(fb.root, merged, { recursive: true, filter: skipGit });
430
+ const components = [...fb.plugin.components];
431
+ const dependencies = [];
432
+ const seen = /* @__PURE__ */ new Set([`${fb.plugin.owner.namespace}/${fb.plugin.name}`]);
433
+ for (const dep of depends) {
434
+ const resolved = await resolveDependency(dep, fb.root);
435
+ const dp = resolved.fb.plugin;
436
+ const depId = `${dp.owner.namespace}/${dp.name}`;
437
+ if (seen.has(depId)) throw new Error(`dependency cycle detected at ${depId}`);
438
+ seen.add(depId);
439
+ const want = dep.components;
440
+ if (want) {
441
+ const missing = want.filter((leaf) => !dp.components.some((c) => leafNameOf(c) === leaf));
442
+ if (missing.length > 0) {
443
+ throw new Error(`dependency ${depId} has no component(s): ${missing.join(", ")}`);
444
+ }
445
+ }
446
+ const selected = dp.components.filter((c) => !want || want.includes(leafNameOf(c)));
447
+ const destBase = join("_deps", dp.name);
448
+ cpSync(resolved.fb.root, join(merged, destBase), { recursive: true, filter: skipGit });
449
+ for (const c of selected) components.push(rewriteRef(c, join(destBase, refOf(c))));
450
+ dependencies.push({ id: depId, range: dep.version ?? "*", resolvedSha: resolved.sha });
451
+ }
452
+ const plugin = { ...fb.plugin, components };
453
+ const mergedFb = {
454
+ plugin,
455
+ root: merged,
456
+ manifestPath: join(merged, "loom.yaml"),
457
+ ...fileAccessors(merged)
458
+ };
459
+ return { fb: mergedFb, dependencies };
460
+ }
461
+ function buildLockfile(input) {
462
+ const adapters = {};
463
+ for (const t of input.result.targets) {
464
+ adapters[t.target] = { version: t.adapter.version, targetSchema: t.adapter.targetSchema };
465
+ }
466
+ return {
467
+ loomVersion: LOOM_VERSION,
468
+ generatedAt: input.generatedAt,
469
+ plugin: {
470
+ id: input.result.id,
471
+ version: input.result.fb.plugin.version,
472
+ ref: input.ref,
473
+ sha: input.sha
474
+ },
475
+ dependencies: input.dependencies ?? [],
476
+ artifacts: input.artifacts,
477
+ adapters,
478
+ aliases: input.result.aliases
479
+ };
480
+ }
481
+ function serializeLock(lock) {
482
+ return `${JSON.stringify(lock, null, 2)}
483
+ `;
484
+ }
485
+ function writeLock(dir, lock) {
486
+ const p = join(dir, "loom.lock");
487
+ writeFileSync(p, serializeLock(lock));
488
+ return p;
489
+ }
490
+ function readLock(dir) {
491
+ try {
492
+ const text = readFileSync(join(dir, "loom.lock"), "utf8");
493
+ const res = validate(Lockfile, JSON.parse(text));
494
+ return res.ok ? res.value : null;
495
+ } catch {
496
+ return null;
497
+ }
498
+ }
499
+
500
+ // src/managed.ts
501
+ function checkManagedPolicy(policy, ctx) {
502
+ if (!policy) return null;
503
+ if (policy.allowNamespaces && !policy.allowNamespaces.includes(ctx.namespace)) {
504
+ return `namespace "${ctx.namespace}" is not allowlisted (managed mode allows: ${policy.allowNamespaces.join(", ")})`;
505
+ }
506
+ if (policy.requireBadges && policy.requireBadges.length > 0) {
507
+ const have = new Set(ctx.badges ?? []);
508
+ const missing = policy.requireBadges.filter((b) => !have.has(b));
509
+ if (missing.length > 0) return `plugin is missing required badge(s): ${missing.join(", ")}`;
510
+ }
511
+ return null;
512
+ }
513
+ function sha256(contents) {
514
+ return createHash("sha256").update(contents).digest("hex");
515
+ }
516
+
517
+ // src/place.ts
518
+ function writeArtifact(baseDir, relPath, contents) {
519
+ const abs = join(baseDir, relPath);
520
+ mkdirSync(dirname(abs), { recursive: true });
521
+ const buf = typeof contents === "string" ? Buffer.from(contents, "utf8") : contents;
522
+ writeFileSync(abs, buf);
523
+ return { abs, hash: sha256(buf) };
524
+ }
525
+ function placePluginArtifacts(target, baseDir, pluginName) {
526
+ const pluginPrefix = join("plugins", pluginName);
527
+ return target.artifacts.map(({ artifact }) => {
528
+ const rel = join(pluginPrefix, artifact.relPath);
529
+ const { abs, hash } = writeArtifact(baseDir, rel, artifact.contents);
530
+ return { target: target.target, relPath: rel, abs, hash, kind: artifact.kind };
531
+ });
532
+ }
533
+ function placeCatalog(adapter, marketplace, baseDir) {
534
+ return adapter.emitCatalog(marketplace).map((artifact) => {
535
+ const { abs, hash } = writeArtifact(baseDir, artifact.relPath, artifact.contents);
536
+ return { target: adapter.target, relPath: artifact.relPath, abs, hash, kind: artifact.kind };
537
+ });
538
+ }
539
+ function buildToDir(result, outDir) {
540
+ const written = [];
541
+ const { plugin } = result.fb;
542
+ const marketplace = synthMarketplace(plugin);
543
+ for (const t of result.targets) {
544
+ const base = join(outDir, t.target);
545
+ written.push(...placePluginArtifacts(t, base, plugin.name));
546
+ written.push(...placeCatalog(t.adapter, marketplace, base));
547
+ }
548
+ return written;
549
+ }
550
+ function toBuffer(contents) {
551
+ return typeof contents === "string" ? Buffer.from(contents, "utf8") : contents;
552
+ }
553
+ function planScopeArtifacts(result, scope, cwd) {
554
+ const planned = [];
555
+ const name = result.fb.plugin.name;
556
+ for (const t of result.targets) {
557
+ const paths = t.adapter.detect(scope, cwd);
558
+ const pluginDir = join(paths.plugins, name);
559
+ for (const { componentId, artifact } of t.artifacts) {
560
+ const contents = toBuffer(artifact.contents);
561
+ planned.push({
562
+ contents,
563
+ record: {
564
+ component: componentId,
565
+ target: t.target,
566
+ scope,
567
+ path: join(pluginDir, artifact.relPath),
568
+ hash: sha256(contents),
569
+ placement: "copy",
570
+ enabled: artifact.executable !== true
571
+ }
572
+ });
573
+ }
574
+ }
575
+ return planned;
576
+ }
577
+ function installToScope(result, scope, cwd) {
578
+ return planScopeArtifacts(result, scope, cwd).map(({ record, contents }) => {
579
+ mkdirSync(dirname(record.path), { recursive: true });
580
+ writeFileSync(record.path, contents);
581
+ return record;
582
+ });
583
+ }
584
+
585
+ // src/api.ts
586
+ async function loadResolved(pluginDir) {
587
+ return resolveDependencies(loadOrThrow2(pluginDir));
588
+ }
589
+ function issuesToDiagnostics(issues) {
590
+ return issues.map((i) => ({ severity: "error", where: i.path, message: i.message }));
591
+ }
592
+ function loadOrThrow2(pluginDir) {
593
+ const loaded = loadPluginDir(pluginDir);
594
+ if (!loaded.ok) {
595
+ throw new CompileError(
596
+ `failed to load plugin in ${pluginDir}`,
597
+ issuesToDiagnostics(loaded.issues)
598
+ );
599
+ }
600
+ return loaded.value;
601
+ }
602
+ function lint(pluginDir) {
603
+ const fb = loadOrThrow2(pluginDir);
604
+ const pass = staticPass(fb);
605
+ return { id: pass.id, plugin: fb.plugin, aliases: pass.aliases, diagnostics: pass.diagnostics };
606
+ }
607
+ async function build(opts) {
608
+ const { fb } = await loadResolved(opts.pluginDir);
609
+ const result = compile(fb, { registry: opts.registry, targets: opts.targets });
610
+ if (result.diagnostics.hasErrors) {
611
+ throw new CompileError("compile failed", result.diagnostics.errors);
612
+ }
613
+ const written = buildToDir(result, opts.outDir);
614
+ return { result, written };
615
+ }
616
+ async function buildMarketplace(opts) {
617
+ const loaded = loadMarketplaceDir(opts.marketplaceDir);
618
+ if (!loaded.ok) {
619
+ throw new CompileError(
620
+ `failed to load marketplace in ${opts.marketplaceDir}`,
621
+ issuesToDiagnostics(loaded.issues)
622
+ );
623
+ }
624
+ const { marketplace, root } = loaded.value;
625
+ const compiled = [];
626
+ for (const entry of marketplace.plugins) {
627
+ let fb;
628
+ try {
629
+ fb = (await resolvePluginRefFull(entry.plugin, root)).fb;
630
+ } catch (err) {
631
+ throw new CompileError(`marketplace entry "${entry.plugin}" failed`, [
632
+ { severity: "error", where: "plugins", message: err.message }
633
+ ]);
634
+ }
635
+ if (entry.version) fb = { ...fb, plugin: { ...fb.plugin, version: entry.version } };
636
+ const merged = (await resolveDependencies(fb)).fb;
637
+ const result = compile(merged, { registry: opts.registry, targets: opts.targets });
638
+ if (result.diagnostics.hasErrors) {
639
+ throw new CompileError(`plugin "${result.id}" failed`, result.diagnostics.errors);
640
+ }
641
+ compiled.push({ entry, result });
642
+ }
643
+ const targets = opts.targets ?? opts.registry.targets;
644
+ const written = [];
645
+ for (const target of targets) {
646
+ const adapter = opts.registry.get(target);
647
+ if (!adapter) continue;
648
+ const base = join(opts.outDir, target);
649
+ const entries = [];
650
+ for (const { entry, result } of compiled) {
651
+ const output = result.targets.find((t) => t.target === target);
652
+ if (!output) continue;
653
+ const p = result.fb.plugin;
654
+ written.push(...placePluginArtifacts(output, base, p.name));
655
+ entries.push({
656
+ name: p.name,
657
+ source: `./plugins/${p.name}`,
658
+ ...p.description ? { description: p.description } : {},
659
+ version: entry.version ?? p.version,
660
+ ...entry.category ? { category: entry.category } : {},
661
+ ...entry.tags ? { tags: entry.tags } : {}
662
+ });
663
+ }
664
+ const resolved = {
665
+ name: marketplace.name,
666
+ owner: marketplace.owner,
667
+ ...marketplace.description ? { description: marketplace.description } : {},
668
+ entries
669
+ };
670
+ written.push(...placeCatalog(adapter, resolved, base));
671
+ }
672
+ return { marketplace, plugins: compiled.map((c) => c.result), written };
673
+ }
674
+ async function install(opts) {
675
+ const { fb, dependencies } = await loadResolved(opts.pluginDir);
676
+ const result = compile(fb, {
677
+ registry: opts.registry,
678
+ targets: opts.targets,
679
+ only: opts.only
680
+ });
681
+ if (result.diagnostics.hasErrors) {
682
+ throw new CompileError("compile failed", result.diagnostics.errors);
683
+ }
684
+ const blocked = checkManagedPolicy(opts.managed, {
685
+ namespace: result.fb.plugin.owner.namespace,
686
+ badges: opts.badges
687
+ });
688
+ if (blocked) {
689
+ throw new CompileError("install blocked by managed policy", [
690
+ { severity: "error", where: "managed", message: blocked }
691
+ ]);
692
+ }
693
+ const artifacts = installToScope(result, opts.scope, opts.cwd);
694
+ const secrets = resolveConfig(result.fb.plugin, opts.cwd);
695
+ const { ref, sha } = await gitInfo(opts.pluginDir);
696
+ const lockfile = buildLockfile({
697
+ result,
698
+ artifacts,
699
+ dependencies,
700
+ ref,
701
+ sha,
702
+ generatedAt: opts.now ?? (/* @__PURE__ */ new Date()).toISOString()
703
+ });
704
+ const lockPath = writeLock(opts.lockDir ?? opts.pluginDir, lockfile);
705
+ return { result, lockfile, lockPath, secrets };
706
+ }
707
+ function pruneEmptyDirs(paths) {
708
+ const dirs = new Set(paths.map((p) => dirname(p)));
709
+ for (const start of dirs) {
710
+ let d = start;
711
+ while (d && d !== dirname(d)) {
712
+ try {
713
+ if (readdirSync(d).length > 0) break;
714
+ rmSync(d, { recursive: true, force: true });
715
+ } catch {
716
+ break;
717
+ }
718
+ d = dirname(d);
719
+ }
720
+ }
721
+ }
722
+ function uninstall(opts) {
723
+ const dir = opts.lockDir ?? opts.pluginDir;
724
+ const lock = readLock(dir);
725
+ if (!lock) {
726
+ throw new CompileError("nothing to uninstall", [
727
+ { severity: "error", where: "loom.lock", message: `no loom.lock found in ${dir}` }
728
+ ]);
729
+ }
730
+ const removed = [];
731
+ for (const a of lock.artifacts) {
732
+ if (existsSync(a.path)) {
733
+ rmSync(a.path, { force: true });
734
+ removed.push(a.path);
735
+ }
736
+ }
737
+ pruneEmptyDirs(removed);
738
+ rmSync(join(dir, "loom.lock"), { force: true });
739
+ return { removed };
740
+ }
741
+ async function update(opts) {
742
+ const prev = readLock(opts.pluginDir);
743
+ const prevHash = new Map((prev?.artifacts ?? []).map((a) => [a.path, a.hash]));
744
+ const { fb, dependencies } = await loadResolved(opts.pluginDir);
745
+ const result = compile(fb, { registry: opts.registry, targets: opts.targets, only: opts.only });
746
+ if (result.diagnostics.hasErrors) {
747
+ throw new CompileError("compile failed", result.diagnostics.errors);
748
+ }
749
+ const planned = planScopeArtifacts(result, opts.scope, opts.cwd);
750
+ const changed = [];
751
+ for (const { record, contents } of planned) {
752
+ if (prevHash.get(record.path) === record.hash) continue;
753
+ mkdirSync(dirname(record.path), { recursive: true });
754
+ writeFileSync(record.path, contents);
755
+ changed.push(record.path);
756
+ }
757
+ const { ref, sha } = await gitInfo(opts.pluginDir);
758
+ const lockfile = buildLockfile({
759
+ result,
760
+ artifacts: planned.map((p) => p.record),
761
+ dependencies,
762
+ ref,
763
+ sha,
764
+ generatedAt: opts.now ?? (/* @__PURE__ */ new Date()).toISOString()
765
+ });
766
+ const lockPath = writeLock(opts.pluginDir, lockfile);
767
+ return { lockfile, lockPath, changed };
768
+ }
769
+ function importNativePlugin(opts) {
770
+ if (!opts.adapter.importNative) {
771
+ throw new Error(`adapter "${opts.adapter.target}" does not support import`);
772
+ }
773
+ const result = opts.adapter.importNative(opts.dir, { namespace: opts.namespace });
774
+ if (!result) {
775
+ throw new Error(`no ${opts.adapter.target} plugin or marketplace found in ${opts.dir}`);
776
+ }
777
+ mkdirSync(opts.outDir, { recursive: true });
778
+ if (result.kind === "marketplace") {
779
+ const manifestPath2 = join(opts.outDir, "marketplace.yaml");
780
+ writeFileSync(manifestPath2, stringifyDocument(result.marketplace));
781
+ return {
782
+ kind: "marketplace",
783
+ name: result.marketplace.name,
784
+ outDir: opts.outDir,
785
+ manifestPath: manifestPath2,
786
+ fileCount: 0
787
+ };
788
+ }
789
+ const manifestPath = join(opts.outDir, "loom.yaml");
790
+ writeFileSync(manifestPath, stringifyDocument(result.plugin));
791
+ for (const f of result.files) {
792
+ const abs = join(opts.outDir, f.relPath);
793
+ mkdirSync(dirname(abs), { recursive: true });
794
+ writeFileSync(
795
+ abs,
796
+ typeof f.contents === "string" ? Buffer.from(f.contents, "utf8") : f.contents
797
+ );
798
+ }
799
+ return {
800
+ kind: "plugin",
801
+ name: result.plugin.name,
802
+ outDir: opts.outDir,
803
+ manifestPath,
804
+ fileCount: result.files.length,
805
+ id: `${result.plugin.owner.namespace}/${result.plugin.name}`
806
+ };
807
+ }
808
+
809
+ // src/registry.ts
810
+ var AdapterRegistry = class {
811
+ adapters = /* @__PURE__ */ new Map();
812
+ register(adapter) {
813
+ this.adapters.set(adapter.target, adapter);
814
+ return this;
815
+ }
816
+ get(target) {
817
+ return this.adapters.get(target);
818
+ }
819
+ has(target) {
820
+ return this.adapters.has(target);
821
+ }
822
+ get targets() {
823
+ return [...this.adapters.keys()];
824
+ }
825
+ };
826
+ function artifactDigest(lock) {
827
+ const lines = lock.artifacts.map((a) => `${a.component} ${a.target} ${a.hash}`).sort();
828
+ return createHash("sha256").update(lines.join("\n")).digest();
829
+ }
830
+ function generateSigningKeys() {
831
+ return generateKeyPairSync("ed25519");
832
+ }
833
+ function signLock(lock, privateKey) {
834
+ return sign(null, artifactDigest(lock), privateKey).toString("base64");
835
+ }
836
+ function verifyLockSignature(lock, publicKey, signature) {
837
+ try {
838
+ return verify(null, artifactDigest(lock), publicKey, Buffer.from(signature, "base64"));
839
+ } catch {
840
+ return false;
841
+ }
842
+ }
843
+ function verifyArtifacts(lock, publicKey, signature) {
844
+ const tampered = [];
845
+ for (const a of lock.artifacts) {
846
+ if (!existsSync(a.path)) {
847
+ tampered.push(a.path);
848
+ continue;
849
+ }
850
+ const actual = createHash("sha256").update(readFileSync(a.path)).digest("hex");
851
+ if (actual !== a.hash) tampered.push(a.path);
852
+ }
853
+ return { signatureValid: verifyLockSignature(lock, publicKey, signature), tampered };
854
+ }
855
+
856
+ export { AdapterRegistry, CACHE_DIR, CompileError, Diagnostics, LOOM_VERSION, artifactDigest, build, buildLockfile, buildMarketplace, buildToDir, checkManagedPolicy, checkMinVersion, compile, fileAccessors, generateSigningKeys, gitInfo, hasMarketplaceManifest, importNativePlugin, install, installToScope, lint, loadMarketplaceDir, loadPluginDir, parseSource, placeCatalog, placePluginArtifacts, planScopeArtifacts, readLock, resolveAliases, resolveConfig, resolveDependencies, resolveDependency, resolvePluginRef, resolvePluginRefFull, serializeLock, sha256, signLock, staticPass, synthMarketplace, uninstall, update, validatePlugin, verifyArtifacts, verifyLockSignature, writeLock };
857
+ //# sourceMappingURL=index.js.map
858
+ //# sourceMappingURL=index.js.map