bearings 0.2.0 → 0.3.1

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.
@@ -0,0 +1,654 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/paths.ts
4
+ import { fileURLToPath } from "url";
5
+ import { dirname, join } from "path";
6
+ function templatesDir() {
7
+ return join(dirname(fileURLToPath(import.meta.url)), "..", "templates");
8
+ }
9
+ var SKILLS = [
10
+ "commit-convention",
11
+ "defer-work",
12
+ "resurface-deferred-work",
13
+ "recording-decisions"
14
+ ];
15
+ var SCAFFOLD = [
16
+ { template: "AGENTS.md", target: "AGENTS.md", owner: "agent" },
17
+ { template: "CLAUDE.md", target: "CLAUDE.md", owner: "bearings" },
18
+ { template: "docs/DOMAIN.md", target: "docs/DOMAIN.md", owner: "agent" },
19
+ { template: "docs/ARCHITECTURE.md", target: "docs/ARCHITECTURE.md", owner: "agent" },
20
+ { template: "docs/CODEBASE_MAP.md", target: "docs/CODEBASE_MAP.md", owner: "agent" },
21
+ { template: "docs/adr/INDEX.md", target: "docs/adr/INDEX.md", owner: "agent" },
22
+ { template: "docs/adr/0000-template.md", target: "docs/adr/0000-template.md", owner: "bearings" },
23
+ { template: "docs/deferred/INDEX.md", target: "docs/deferred/INDEX.md", owner: "agent" },
24
+ { template: "agents/commands/refresh-repo-map.md", target: ".agents/commands/refresh-repo-map.md", owner: "bearings" },
25
+ { template: "agents/commands/setup-repo.md", target: ".agents/commands/setup-repo.md", owner: "bearings" },
26
+ ...SKILLS.map((skill) => ({
27
+ template: `agents/skills/${skill}/SKILL.md`,
28
+ target: `.agents/skills/${skill}/SKILL.md`,
29
+ owner: "bearings"
30
+ }))
31
+ ];
32
+
33
+ // src/manifest.ts
34
+ import { createHash } from "crypto";
35
+ import { access, mkdir, readFile, writeFile } from "fs/promises";
36
+ import { join as join2 } from "path";
37
+ function sha256(content) {
38
+ return "sha256:" + createHash("sha256").update(content).digest("hex");
39
+ }
40
+ function manifestPath(repoDir) {
41
+ return join2(repoDir, ".agents", "bearings.json");
42
+ }
43
+ async function saveManifest(repoDir, m) {
44
+ await mkdir(join2(repoDir, ".agents"), { recursive: true });
45
+ await writeFile(manifestPath(repoDir), JSON.stringify(m, null, 2) + "\n");
46
+ }
47
+ var HASH = /^sha256:[0-9a-f]{64}$/;
48
+ var HARNESSES = ["claude", "opencode"];
49
+ var EXPOSURES = ["symlink", "copy"];
50
+ var OWNERS = ["bearings", "agent"];
51
+ var RECONCILIATION_REASONS = ["init-collision", "update-merge"];
52
+ var SETUP_PENDING_KINDS = ["update", "reconstruction"];
53
+ function fail(message) {
54
+ throw new Error(message);
55
+ }
56
+ function isPlainObject(value) {
57
+ return typeof value === "object" && value !== null && !Array.isArray(value);
58
+ }
59
+ function isSafeManifestPath(value) {
60
+ if (typeof value !== "string" || !value || value.includes("\\")) return false;
61
+ if (value.startsWith("/") || value.split("/").some((part) => !part || part === "." || part === "..")) return false;
62
+ return true;
63
+ }
64
+ function isNonEmptyString(value) {
65
+ return typeof value === "string" && value.length > 0;
66
+ }
67
+ function isHash(value) {
68
+ return typeof value === "string" && HASH.test(value);
69
+ }
70
+ function isHarnessArray(value) {
71
+ return Array.isArray(value) && value.every((h) => HARNESSES.includes(h));
72
+ }
73
+ function isExposure(value) {
74
+ return typeof value === "string" && EXPOSURES.includes(value);
75
+ }
76
+ function isOwner(value) {
77
+ return typeof value === "string" && OWNERS.includes(value);
78
+ }
79
+ function validateReconciliation(value, context) {
80
+ if (!isPlainObject(value)) fail(`${context} must be an object`);
81
+ const { backup, reason, sourceHash, incomingTemplateVersion, incomingHash } = value;
82
+ if (!isSafeManifestPath(backup)) fail(`${context}.backup is unsafe: ${String(backup)}`);
83
+ if (typeof reason !== "string" || !RECONCILIATION_REASONS.includes(reason)) {
84
+ fail(`${context}.reason must be "init-collision" or "update-merge"`);
85
+ }
86
+ if (!isHash(sourceHash)) fail(`${context}.sourceHash must match sha256 format`);
87
+ if (!isNonEmptyString(incomingTemplateVersion)) fail(`${context}.incomingTemplateVersion must be a string`);
88
+ if (!isHash(incomingHash)) fail(`${context}.incomingHash must match sha256 format`);
89
+ return {
90
+ backup,
91
+ reason,
92
+ sourceHash,
93
+ incomingTemplateVersion,
94
+ incomingHash
95
+ };
96
+ }
97
+ function validateFileV1(value, index) {
98
+ const context = `files[${index}]`;
99
+ if (!isPlainObject(value)) fail(`${context} must be an object`);
100
+ const { path, template, templateVersion, hash, owner, backup } = value;
101
+ if (!isSafeManifestPath(path)) fail(`${context}.path is unsafe: ${String(path)}`);
102
+ if (!isNonEmptyString(template)) fail(`${context}.template must be a string`);
103
+ if (!isNonEmptyString(templateVersion)) fail(`${context}.templateVersion must be a string`);
104
+ if (!isHash(hash)) fail(`${context}.hash must match sha256 format`);
105
+ if (!isOwner(owner)) fail(`${context}.owner must be "bearings" or "agent"`);
106
+ if (backup !== void 0 && !isSafeManifestPath(backup)) fail(`${context}.backup is unsafe: ${String(backup)}`);
107
+ return {
108
+ path,
109
+ template,
110
+ templateVersion,
111
+ hash,
112
+ owner,
113
+ ...backup !== void 0 ? { backup } : {}
114
+ };
115
+ }
116
+ function validateFileV2(value, index) {
117
+ const context = `files[${index}]`;
118
+ if (!isPlainObject(value)) fail(`${context} must be an object`);
119
+ const { path, template, templateVersion, hash, owner, retired, skippedTemplate, reconciliations } = value;
120
+ if (!isSafeManifestPath(path)) fail(`${context}.path is unsafe: ${String(path)}`);
121
+ if (!isNonEmptyString(template)) fail(`${context}.template must be a string`);
122
+ if (!isNonEmptyString(templateVersion)) fail(`${context}.templateVersion must be a string`);
123
+ if (!isHash(hash)) fail(`${context}.hash must match sha256 format`);
124
+ if (!isOwner(owner)) fail(`${context}.owner must be "bearings" or "agent"`);
125
+ if (retired !== void 0 && retired !== true) fail(`${context}.retired must be true when present`);
126
+ let normalizedSkippedTemplate;
127
+ if (skippedTemplate !== void 0) {
128
+ if (!isPlainObject(skippedTemplate)) fail(`${context}.skippedTemplate must be an object`);
129
+ const { templateVersion: skippedVersion, hash: skippedHash } = skippedTemplate;
130
+ if (!isNonEmptyString(skippedVersion)) fail(`${context}.skippedTemplate.templateVersion must be a string`);
131
+ if (!isHash(skippedHash)) fail(`${context}.skippedTemplate.hash must match sha256 format`);
132
+ normalizedSkippedTemplate = { templateVersion: skippedVersion, hash: skippedHash };
133
+ }
134
+ let normalizedReconciliations;
135
+ if (reconciliations !== void 0) {
136
+ if (!Array.isArray(reconciliations)) fail(`${context}.reconciliations must be an array`);
137
+ normalizedReconciliations = reconciliations.map((entry, reconciliationIndex) => validateReconciliation(entry, `${context}.reconciliations[${reconciliationIndex}]`));
138
+ }
139
+ if (retired === true && !normalizedReconciliations?.length) {
140
+ fail(`${context}.retired requires at least one reconciliation`);
141
+ }
142
+ return {
143
+ path,
144
+ template,
145
+ templateVersion,
146
+ hash,
147
+ owner,
148
+ ...retired === true ? { retired: true } : {},
149
+ ...normalizedSkippedTemplate ? { skippedTemplate: normalizedSkippedTemplate } : {},
150
+ ...normalizedReconciliations ? { reconciliations: normalizedReconciliations } : {}
151
+ };
152
+ }
153
+ function assertUniquePaths(paths) {
154
+ const seen = /* @__PURE__ */ new Set();
155
+ for (const path of paths) {
156
+ if (seen.has(path)) fail(`duplicate managed path: ${path}`);
157
+ seen.add(path);
158
+ }
159
+ }
160
+ function validateSetupPending(value) {
161
+ if (!isPlainObject(value)) fail("setupPending must be an object");
162
+ const { kind, fromVersion, toVersion } = value;
163
+ if (typeof kind !== "string" || !SETUP_PENDING_KINDS.includes(kind)) {
164
+ fail('setupPending.kind must be "update" or "reconstruction"');
165
+ }
166
+ if (fromVersion !== void 0 && !isNonEmptyString(fromVersion)) fail("setupPending.fromVersion must be a string");
167
+ if (!isNonEmptyString(toVersion)) fail("setupPending.toVersion must be a string");
168
+ return {
169
+ kind,
170
+ ...fromVersion !== void 0 ? { fromVersion } : {},
171
+ toVersion
172
+ };
173
+ }
174
+ function parseManifest(raw) {
175
+ if (!isPlainObject(raw)) fail("manifest must be an object");
176
+ const { version, bearingsVersion, harnesses, exposure, files } = raw;
177
+ if (!isNonEmptyString(bearingsVersion)) fail("bearingsVersion must be a string");
178
+ if (!isHarnessArray(harnesses)) fail('harnesses must contain only "claude" or "opencode"');
179
+ if (!isExposure(exposure)) fail('exposure must be "symlink" or "copy"');
180
+ if (!Array.isArray(files)) fail("files must be an array");
181
+ if (version === 1) {
182
+ const validatedFiles = files.map((file, index) => validateFileV1(file, index));
183
+ assertUniquePaths(validatedFiles.map((file) => file.path));
184
+ return { version: 1, bearingsVersion, harnesses, exposure, files: validatedFiles };
185
+ }
186
+ if (version === 2) {
187
+ const validatedFiles = files.map((file, index) => validateFileV2(file, index));
188
+ assertUniquePaths(validatedFiles.map((file) => file.path));
189
+ const { setupPending } = raw;
190
+ return {
191
+ version: 2,
192
+ bearingsVersion,
193
+ harnesses,
194
+ exposure,
195
+ ...setupPending !== void 0 ? { setupPending: validateSetupPending(setupPending) } : {},
196
+ files: validatedFiles
197
+ };
198
+ }
199
+ fail(`unsupported manifest version: ${String(version)}`);
200
+ }
201
+ async function inspectManifest(repoDir) {
202
+ let raw;
203
+ try {
204
+ raw = await readFile(manifestPath(repoDir), "utf8");
205
+ } catch (error) {
206
+ if (error.code === "ENOENT") return { kind: "absent" };
207
+ return { kind: "invalid", message: error.message };
208
+ }
209
+ try {
210
+ return { kind: "valid", manifest: parseManifest(JSON.parse(raw)) };
211
+ } catch (error) {
212
+ return { kind: "invalid", message: error.message };
213
+ }
214
+ }
215
+ async function migrateV1(repoDir, manifest) {
216
+ const files = await Promise.all(manifest.files.map(async (file) => {
217
+ const backupExists = file.backup ? await access(join2(repoDir, file.backup)).then(() => true, () => false) : false;
218
+ const reconciliation = backupExists && file.backup ? {
219
+ backup: file.backup,
220
+ reason: "init-collision",
221
+ sourceHash: sha256(await readFile(join2(repoDir, file.backup), "utf8")),
222
+ incomingTemplateVersion: file.templateVersion,
223
+ incomingHash: file.hash
224
+ } : void 0;
225
+ return {
226
+ path: file.path,
227
+ template: file.template,
228
+ templateVersion: file.templateVersion,
229
+ hash: file.hash,
230
+ owner: file.owner,
231
+ ...reconciliation ? { reconciliations: [reconciliation] } : {}
232
+ };
233
+ }));
234
+ return { version: 2, bearingsVersion: manifest.bearingsVersion, harnesses: manifest.harnesses, exposure: manifest.exposure, files };
235
+ }
236
+
237
+ // src/report.ts
238
+ function renderInitReport(r, exposed, m) {
239
+ const lines = ["bearings init complete.", ""];
240
+ lines.push(`Written (${r.written.length}):`, ...r.written.map((p) => ` + ${p}`));
241
+ if (r.backedUp.length) {
242
+ lines.push("", `Backed up (${r.backedUp.length}) \u2014 review during /setup-repo:`);
243
+ lines.push(...r.backedUp.map((b) => ` \u26A0 ${b.path} -> ${b.backup}`));
244
+ }
245
+ if (r.skippedUnchanged.length) {
246
+ lines.push(
247
+ "",
248
+ `Skipped, unchanged (${r.skippedUnchanged.length}):`,
249
+ ...r.skippedUnchanged.map((p) => ` = ${p}`)
250
+ );
251
+ }
252
+ lines.push(
253
+ "",
254
+ `Exposed to harnesses [${m.harnesses.join(", ")}] via ${m.exposure}:`,
255
+ ...exposed.map((p) => ` ~ ${p}`)
256
+ );
257
+ lines.push(
258
+ "",
259
+ "Next step \u2014 finish setup with your AI agent:",
260
+ " Open your agent (Claude Code, OpenCode, ...) in this repo and run:",
261
+ " /setup-repo",
262
+ " It will review any backups, interview you, and tailor the setup.",
263
+ " Finish by running: bearings verify"
264
+ );
265
+ return lines.join("\n");
266
+ }
267
+ function categorizeActions(actions) {
268
+ const result = {
269
+ added: [],
270
+ restored: [],
271
+ replaced: [],
272
+ merged: [],
273
+ skipped: [],
274
+ removed: [],
275
+ keptUntracked: []
276
+ };
277
+ for (const action of actions) {
278
+ if (action.kind === "write") {
279
+ if (action.reason === "add") result.added.push(action.path);
280
+ else if (action.reason === "restore") result.restored.push(action.path);
281
+ else result.replaced.push(action.path);
282
+ } else if (action.kind === "merge") {
283
+ result.merged.push(`${action.path} -> ${action.backup}`);
284
+ } else if (action.kind === "skip") {
285
+ result.skipped.push(action.path);
286
+ } else if (action.kind === "delete") {
287
+ result.removed.push(action.path);
288
+ } else if (action.kind === "keep-untracked") {
289
+ result.keptUntracked.push(action.path);
290
+ }
291
+ }
292
+ return result;
293
+ }
294
+ function pushSection(lines, title, items) {
295
+ if (!items.length) return;
296
+ lines.push("", `${title} (${items.length}):`, ...items.map((i) => ` ${i}`));
297
+ }
298
+ function renderUpdatePlan(input) {
299
+ const categories = categorizeActions(input.actions);
300
+ const lines = [input.reconstruction ? "bearings init: reconstruction plan" : "bearings init: update plan"];
301
+ pushSection(lines, "Added", categories.added);
302
+ pushSection(lines, "Restored", categories.restored);
303
+ pushSection(lines, "Replaced", categories.replaced);
304
+ pushSection(lines, "Merged", categories.merged);
305
+ pushSection(lines, "Skipped", categories.skipped);
306
+ pushSection(lines, "Removed", categories.removed);
307
+ pushSection(lines, "Kept and untracked", categories.keptUntracked);
308
+ if (input.adapterActions.length) {
309
+ lines.push("", `Adapter changes (${input.adapterActions.length}):`);
310
+ for (const a of input.adapterActions) lines.push(` ${a.kind} ${a.path}`);
311
+ }
312
+ lines.push(
313
+ "",
314
+ `Manifest changes: schema ${input.fromSchema ?? "\u2014"} -> ${input.toSchema}, version ${input.fromVersion ?? "\u2014"} -> ${input.toVersion}`
315
+ );
316
+ lines.push("", "Proceed?");
317
+ return lines.join("\n");
318
+ }
319
+ function renderUpdateReport(input) {
320
+ const categories = categorizeActions(input.actions);
321
+ const lines = [
322
+ input.reconstruction ? "bearings init: reconstruction complete." : "bearings init: update complete."
323
+ ];
324
+ pushSection(lines, "Added", categories.added);
325
+ pushSection(lines, "Restored", categories.restored);
326
+ pushSection(lines, "Replaced", categories.replaced);
327
+ pushSection(lines, "Merged", categories.merged);
328
+ pushSection(lines, "Skipped", categories.skipped);
329
+ pushSection(lines, "Removed", categories.removed);
330
+ pushSection(lines, "Kept and untracked", categories.keptUntracked);
331
+ if (input.adapterActions.length) {
332
+ lines.push("", `Adapter changes (${input.adapterActions.length}):`);
333
+ for (const a of input.adapterActions) lines.push(` ${a.kind} ${a.path}`);
334
+ }
335
+ lines.push(
336
+ "",
337
+ `Manifest changes: schema ${input.fromSchema ?? "\u2014"} -> ${input.toSchema}, version ${input.fromVersion ?? "\u2014"} -> ${input.toVersion}`
338
+ );
339
+ if (input.setupRequired) {
340
+ lines.push(
341
+ "",
342
+ "Next step \u2014 finish setup with your AI agent:",
343
+ " Open your agent (Claude Code, OpenCode, ...) in this repo and run:",
344
+ " /setup-repo",
345
+ " It will review any backups, interview you, and tailor the setup.",
346
+ " Finish by running: bearings verify"
347
+ );
348
+ } else {
349
+ lines.push("", "Finish by running: bearings verify");
350
+ }
351
+ return lines.join("\n");
352
+ }
353
+ function renderVerifyReport(v) {
354
+ const lines = [];
355
+ for (const f of v.failures) lines.push(`FAIL ${f.code} ${f.path} \u2014 ${f.message}`);
356
+ for (const w of v.warnings) lines.push(`warn ${w.code} ${w.path} \u2014 ${w.message}`);
357
+ lines.push(v.failures.length ? `
358
+ ${v.failures.length} failure(s).` : "\nbearings verify: OK");
359
+ return lines.join("\n");
360
+ }
361
+
362
+ // src/commands/init.ts
363
+ import { lstat as lstat2, readdir as readdir2 } from "fs/promises";
364
+ import { join as join6 } from "path";
365
+
366
+ // src/scanner.ts
367
+ import { access as access2, symlink, rm } from "fs/promises";
368
+ import { join as join3 } from "path";
369
+ async function exists(p) {
370
+ try {
371
+ await access2(p);
372
+ return true;
373
+ } catch {
374
+ return false;
375
+ }
376
+ }
377
+ async function scan(repoDir) {
378
+ const collisions = [];
379
+ for (const e of SCAFFOLD) if (await exists(join3(repoDir, e.target))) collisions.push(e.target);
380
+ const harnessDirsPresent = [];
381
+ for (const h of ["claude", "opencode"])
382
+ if (await exists(join3(repoDir, `.${h}`))) harnessDirsPresent.push(h);
383
+ let symlinksSupported = true;
384
+ const probe = join3(repoDir, ".bearings-symlink-probe");
385
+ try {
386
+ await symlink(".", probe);
387
+ await rm(probe);
388
+ } catch {
389
+ symlinksSupported = false;
390
+ }
391
+ return { collisions, harnessDirsPresent, symlinksSupported };
392
+ }
393
+
394
+ // src/generator.ts
395
+ import { mkdir as mkdir2, readFile as readFile2, writeFile as writeFile2, rename, access as access3 } from "fs/promises";
396
+ import { dirname as dirname2, join as join4 } from "path";
397
+ async function exists2(p) {
398
+ try {
399
+ await access3(p);
400
+ return true;
401
+ } catch {
402
+ return false;
403
+ }
404
+ }
405
+ async function freeBackupPath(repoDir, target) {
406
+ let candidate = `${target}.bkp`;
407
+ for (let i = 1; await exists2(join4(repoDir, candidate)); i++) candidate = `${target}.bkp.${i}`;
408
+ return candidate;
409
+ }
410
+ async function generate(repoDir, bearingsVersion) {
411
+ const priorState = await inspectManifest(repoDir);
412
+ if (priorState.kind === "invalid") throw new Error(`Invalid manifest: ${priorState.message}`);
413
+ const prior = priorState.kind === "valid" ? priorState.manifest.version === 1 ? await migrateV1(repoDir, priorState.manifest) : priorState.manifest : null;
414
+ const result = { written: [], backedUp: [], skippedUnchanged: [], files: [] };
415
+ for (const entry of SCAFFOLD) {
416
+ const abs = join4(repoDir, entry.target);
417
+ const templateContent = await readFile2(join4(templatesDir(), entry.template), "utf8");
418
+ const priorEntry = prior?.files.find((f) => f.path === entry.target);
419
+ let backup;
420
+ let currentContent;
421
+ if (await exists2(abs)) {
422
+ currentContent = await readFile2(abs, "utf8");
423
+ if (priorEntry && sha256(currentContent) === priorEntry.hash) {
424
+ result.skippedUnchanged.push(entry.target);
425
+ result.files.push(priorEntry);
426
+ continue;
427
+ }
428
+ backup = await freeBackupPath(repoDir, entry.target);
429
+ await rename(abs, join4(repoDir, backup));
430
+ result.backedUp.push({ path: entry.target, backup });
431
+ }
432
+ await mkdir2(dirname2(abs), { recursive: true });
433
+ await writeFile2(abs, templateContent);
434
+ result.written.push(entry.target);
435
+ const incomingHash = sha256(templateContent);
436
+ const reconciliation = backup && currentContent !== void 0 ? {
437
+ backup,
438
+ reason: "init-collision",
439
+ sourceHash: sha256(currentContent),
440
+ incomingTemplateVersion: bearingsVersion,
441
+ incomingHash
442
+ } : void 0;
443
+ result.files.push({
444
+ path: entry.target,
445
+ template: entry.template,
446
+ templateVersion: bearingsVersion,
447
+ hash: incomingHash,
448
+ owner: entry.owner,
449
+ ...reconciliation ? { reconciliations: [reconciliation] } : {}
450
+ });
451
+ }
452
+ return result;
453
+ }
454
+
455
+ // src/adapters.ts
456
+ import { mkdir as mkdir3, readdir, symlink as symlink2, lstat, readlink, rm as rm2, cp, readFile as readFile3 } from "fs/promises";
457
+ import { join as join5 } from "path";
458
+ var ADAPTER_KINDS = ["skills", "commands"];
459
+ async function canonicalAdapterEntries(repoDir) {
460
+ const entries = /* @__PURE__ */ new Map();
461
+ for (const kind of ADAPTER_KINDS) {
462
+ const directory = join5(repoDir, ".agents", kind);
463
+ for (const name of await readdir(directory).catch(() => [])) {
464
+ entries.set(`${kind}/${name}`, join5(directory, name));
465
+ }
466
+ }
467
+ return entries;
468
+ }
469
+ async function entriesMatch(src, dst) {
470
+ const srcStat = await lstat(src).catch(() => null);
471
+ const dstStat = await lstat(dst).catch(() => null);
472
+ if (!srcStat || !dstStat) return false;
473
+ if (srcStat.isSymbolicLink() || dstStat.isSymbolicLink()) {
474
+ return srcStat.isSymbolicLink() && dstStat.isSymbolicLink() && await readlink(src) === await readlink(dst);
475
+ }
476
+ if (srcStat.isDirectory() || dstStat.isDirectory()) {
477
+ if (!srcStat.isDirectory() || !dstStat.isDirectory()) return false;
478
+ const [srcEntries, dstEntries] = await Promise.all([readdir(src), readdir(dst)]);
479
+ if (srcEntries.length !== dstEntries.length) return false;
480
+ srcEntries.sort();
481
+ dstEntries.sort();
482
+ for (let i = 0; i < srcEntries.length; i++) {
483
+ if (srcEntries[i] !== dstEntries[i]) return false;
484
+ if (!await entriesMatch(join5(src, srcEntries[i]), join5(dst, dstEntries[i]))) return false;
485
+ }
486
+ return true;
487
+ }
488
+ if (srcStat.isFile() || dstStat.isFile()) {
489
+ return srcStat.isFile() && dstStat.isFile() && (await readFile3(src)).equals(await readFile3(dst));
490
+ }
491
+ return false;
492
+ }
493
+ async function expose(repoDir, harness, mode) {
494
+ const created = [];
495
+ for (const kind of ADAPTER_KINDS) {
496
+ const srcDir = join5(repoDir, ".agents", kind);
497
+ let entries;
498
+ try {
499
+ entries = await readdir(srcDir);
500
+ } catch {
501
+ continue;
502
+ }
503
+ const dstDir = join5(repoDir, `.${harness}`, kind);
504
+ await mkdir3(dstDir, { recursive: true });
505
+ for (const name of entries) {
506
+ const dst = join5(dstDir, name);
507
+ const adapterPath = join5(`.${harness}`, kind, name);
508
+ const relTarget = join5("..", "..", ".agents", kind, name);
509
+ const stat = await lstat(dst).catch(() => null);
510
+ if (mode === "symlink") {
511
+ if (stat?.isSymbolicLink() && await readlink(dst) === relTarget) continue;
512
+ if (stat && !stat.isSymbolicLink()) throw new Error(`Refusing to replace existing adapter entry: ${adapterPath}`);
513
+ if (stat) await rm2(dst, { recursive: true });
514
+ await symlink2(relTarget, dst);
515
+ } else {
516
+ const src = join5(srcDir, name);
517
+ if (stat && await entriesMatch(src, dst)) continue;
518
+ if (stat && !stat.isSymbolicLink()) throw new Error(`Refusing to replace existing adapter entry: ${adapterPath}`);
519
+ if (stat) await rm2(dst, { recursive: true });
520
+ await cp(src, dst, { recursive: true });
521
+ }
522
+ created.push(adapterPath);
523
+ }
524
+ }
525
+ return created;
526
+ }
527
+
528
+ // src/commands/init.ts
529
+ var VALID_HARNESSES = ["claude", "opencode"];
530
+ var KINDS = ["skills", "commands"];
531
+ function validateHarnesses(harnesses) {
532
+ if (!harnesses) return void 0;
533
+ for (const harness of harnesses) {
534
+ if (!VALID_HARNESSES.includes(harness)) {
535
+ throw new Error(`Invalid harness: ${String(harness)}`);
536
+ }
537
+ }
538
+ return harnesses;
539
+ }
540
+ function cancelInit(p) {
541
+ p.cancel("Init cancelled.");
542
+ throw new Error("Init cancelled.");
543
+ }
544
+ async function plannedAdapterSources(repoDir) {
545
+ const sources = { skills: /* @__PURE__ */ new Map(), commands: /* @__PURE__ */ new Map() };
546
+ for (const entry of SCAFFOLD) {
547
+ for (const kind of KINDS) {
548
+ const prefix = `.agents/${kind}/`;
549
+ if (entry.target.startsWith(prefix)) {
550
+ const name = entry.target.slice(prefix.length).split("/")[0];
551
+ sources[kind].set(name, join6(templatesDir(), "agents", kind, name));
552
+ }
553
+ }
554
+ }
555
+ for (const kind of KINDS) {
556
+ const srcDir = join6(repoDir, ".agents", kind);
557
+ for (const name of await readdir2(srcDir).catch(() => [])) {
558
+ if (!sources[kind].has(name)) sources[kind].set(name, join6(srcDir, name));
559
+ }
560
+ }
561
+ return sources;
562
+ }
563
+ async function preflightAdapterCollisions(repoDir, harnesses, exposure) {
564
+ const sources = await plannedAdapterSources(repoDir);
565
+ for (const h of harnesses) {
566
+ for (const kind of KINDS) {
567
+ for (const [name, source] of sources[kind]) {
568
+ const adapterPath = join6(`.${h}`, kind, name);
569
+ const target = join6(repoDir, adapterPath);
570
+ const stat = await lstat2(target).catch(() => null);
571
+ if (!stat || stat.isSymbolicLink()) continue;
572
+ if (exposure === "copy" && await entriesMatch(source, target)) continue;
573
+ throw new Error(`Refusing to replace existing adapter entry: ${adapterPath}`);
574
+ }
575
+ }
576
+ }
577
+ }
578
+ async function runFreshInit(repoDir, flags, version) {
579
+ const s = await scan(repoDir);
580
+ let harnesses = validateHarnesses(flags.harnesses);
581
+ let exposure = flags.exposure;
582
+ if (!harnesses || !exposure) {
583
+ if (flags.yes || !process.stdin.isTTY) {
584
+ harnesses ??= s.harnessDirsPresent.length ? s.harnessDirsPresent : ["claude", "opencode"];
585
+ exposure ??= s.symlinksSupported ? "symlink" : "copy";
586
+ } else {
587
+ const p = await import("@clack/prompts");
588
+ if (!harnesses) {
589
+ const selectedHarnesses = await p.multiselect({
590
+ message: "Expose skills/commands to which harnesses?",
591
+ options: [
592
+ { value: "claude", label: "Claude Code (.claude/)" },
593
+ { value: "opencode", label: "OpenCode (.opencode/)" }
594
+ ],
595
+ initialValues: s.harnessDirsPresent.length ? s.harnessDirsPresent : ["claude", "opencode"]
596
+ });
597
+ if (p.isCancel(selectedHarnesses)) {
598
+ cancelInit(p);
599
+ }
600
+ harnesses = validateHarnesses(selectedHarnesses);
601
+ }
602
+ if (!exposure && s.symlinksSupported) {
603
+ const selectedExposure = await p.select({
604
+ message: "Exposure mode?",
605
+ options: [
606
+ { value: "symlink", label: "Symlinks (recommended)" },
607
+ { value: "copy", label: "Copies (verify checks drift)" }
608
+ ]
609
+ });
610
+ if (p.isCancel(selectedExposure)) {
611
+ cancelInit(p);
612
+ }
613
+ exposure = selectedExposure;
614
+ } else {
615
+ exposure ??= "copy";
616
+ }
617
+ }
618
+ }
619
+ await preflightAdapterCollisions(repoDir, harnesses, exposure);
620
+ const gen = await generate(repoDir, version);
621
+ const exposed = [];
622
+ for (const h of harnesses) exposed.push(...await expose(repoDir, h, exposure));
623
+ const manifest = {
624
+ version: 2,
625
+ bearingsVersion: version,
626
+ harnesses,
627
+ exposure,
628
+ files: gen.files
629
+ };
630
+ await saveManifest(repoDir, manifest);
631
+ return { manifest, report: renderInitReport(gen, exposed, manifest) };
632
+ }
633
+ async function runInit(repoDir, flags, version) {
634
+ const state = await inspectManifest(repoDir);
635
+ if (state.kind === "absent") return runFreshInit(repoDir, flags, version);
636
+ const { runUpdate } = await import("./update-5F7TZEUH.js");
637
+ return runUpdate(repoDir, flags, version, state);
638
+ }
639
+
640
+ export {
641
+ templatesDir,
642
+ SCAFFOLD,
643
+ sha256,
644
+ inspectManifest,
645
+ migrateV1,
646
+ ADAPTER_KINDS,
647
+ canonicalAdapterEntries,
648
+ entriesMatch,
649
+ renderUpdatePlan,
650
+ renderUpdateReport,
651
+ renderVerifyReport,
652
+ validateHarnesses,
653
+ runInit
654
+ };