bearings 0.2.0 → 0.3.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/cli.js CHANGED
@@ -1,53 +1,22 @@
1
1
  #!/usr/bin/env node
2
+ import {
3
+ inspectManifest,
4
+ renderVerifyReport,
5
+ runInit,
6
+ sha256,
7
+ validateHarnesses
8
+ } from "./chunk-GUIHDRBB.js";
2
9
 
3
10
  // src/cli.ts
4
11
  import { Command } from "commander";
5
12
  import { readFileSync } from "fs";
6
- import { fileURLToPath as fileURLToPath2 } from "url";
7
- import { dirname as dirname4, join as join8 } from "path";
8
-
9
- // src/commands/init.ts
10
- import { lstat as lstat2, readdir as readdir2 } from "fs/promises";
11
- import { join as join6 } from "path";
12
-
13
- // src/scanner.ts
14
- import { access, symlink, rm } from "fs/promises";
15
- import { join as join2 } from "path";
16
-
17
- // src/paths.ts
18
13
  import { fileURLToPath } from "url";
19
- import { dirname, join } from "path";
20
- function templatesDir() {
21
- return join(dirname(fileURLToPath(import.meta.url)), "..", "templates");
22
- }
23
- var SKILLS = [
24
- "repo-navigation",
25
- "implementing-task",
26
- "installing-dependencies",
27
- "secrets-handling",
28
- "enforcement-gates",
29
- "commit-convention",
30
- "defer-work",
31
- "resurface-deferred-work"
32
- ];
33
- var SCAFFOLD = [
34
- { template: "AGENTS.md", target: "AGENTS.md", owner: "agent" },
35
- { template: "CLAUDE.md", target: "CLAUDE.md", owner: "bearings" },
36
- { template: "docs/CURRENT_STATE.md", target: "docs/CURRENT_STATE.md", owner: "agent" },
37
- { template: "docs/conventions/current-state.md", target: "docs/conventions/current-state.md", owner: "bearings" },
38
- { template: "docs/conventions/doc-lifecycle.md", target: "docs/conventions/doc-lifecycle.md", owner: "bearings" },
39
- { template: "docs/adr/0000-template.md", target: "docs/adr/0000-template.md", owner: "bearings" },
40
- { template: "docs/deferred/INDEX.md", target: "docs/deferred/INDEX.md", owner: "agent" },
41
- { template: "agents/commands/current-state.md", target: ".agents/commands/current-state.md", owner: "bearings" },
42
- { template: "agents/commands/setup-repo.md", target: ".agents/commands/setup-repo.md", owner: "bearings" },
43
- ...SKILLS.map((s) => ({
44
- template: `agents/skills/${s}/SKILL.md`,
45
- target: `.agents/skills/${s}/SKILL.md`,
46
- owner: s === "installing-dependencies" || s === "secrets-handling" ? "agent" : "bearings"
47
- }))
48
- ];
14
+ import { dirname as dirname2, join as join2 } from "path";
49
15
 
50
- // src/scanner.ts
16
+ // src/verifier.ts
17
+ import { access, lstat, readFile, readdir, readlink, stat } from "fs/promises";
18
+ import { dirname, join, resolve } from "path";
19
+ var KINDS = ["skills", "commands"];
51
20
  async function exists(p) {
52
21
  try {
53
22
  await access(p);
@@ -56,348 +25,56 @@ async function exists(p) {
56
25
  return false;
57
26
  }
58
27
  }
59
- async function scan(repoDir) {
60
- const collisions = [];
61
- for (const e of SCAFFOLD) if (await exists(join2(repoDir, e.target))) collisions.push(e.target);
62
- const harnessDirsPresent = [];
63
- for (const h of ["claude", "opencode"])
64
- if (await exists(join2(repoDir, `.${h}`))) harnessDirsPresent.push(h);
65
- let symlinksSupported = true;
66
- const probe = join2(repoDir, ".bearings-symlink-probe");
67
- try {
68
- await symlink(".", probe);
69
- await rm(probe);
70
- } catch {
71
- symlinksSupported = false;
72
- }
73
- return { collisions, harnessDirsPresent, symlinksSupported };
74
- }
75
-
76
- // src/generator.ts
77
- import { mkdir as mkdir2, readFile as readFile2, writeFile as writeFile2, rename, access as access2 } from "fs/promises";
78
- import { dirname as dirname2, join as join4 } from "path";
79
-
80
- // src/manifest.ts
81
- import { createHash } from "crypto";
82
- import { mkdir, readFile, writeFile } from "fs/promises";
83
- import { join as join3 } from "path";
84
- function sha256(content) {
85
- return "sha256:" + createHash("sha256").update(content).digest("hex");
86
- }
87
- function manifestPath(repoDir) {
88
- return join3(repoDir, ".agents", "bearings.json");
89
- }
90
- async function saveManifest(repoDir, m) {
91
- await mkdir(join3(repoDir, ".agents"), { recursive: true });
92
- await writeFile(manifestPath(repoDir), JSON.stringify(m, null, 2) + "\n");
93
- }
94
- async function loadManifest(repoDir) {
95
- try {
96
- return JSON.parse(await readFile(manifestPath(repoDir), "utf8"));
97
- } catch {
98
- return null;
99
- }
100
- }
101
-
102
- // src/generator.ts
103
- async function exists2(p) {
104
- try {
105
- await access2(p);
106
- return true;
107
- } catch {
108
- return false;
109
- }
110
- }
111
- async function freeBackupPath(repoDir, target) {
112
- let candidate = `${target}.bkp`;
113
- for (let i = 1; await exists2(join4(repoDir, candidate)); i++) candidate = `${target}.bkp.${i}`;
114
- return candidate;
115
- }
116
- async function generate(repoDir, bearingsVersion) {
117
- const prior = await loadManifest(repoDir);
118
- const result = { written: [], backedUp: [], skippedUnchanged: [], files: [] };
119
- for (const entry of SCAFFOLD) {
120
- const abs = join4(repoDir, entry.target);
121
- const templateContent = await readFile2(join4(templatesDir(), entry.template), "utf8");
122
- const priorEntry = prior?.files.find((f) => f.path === entry.target);
123
- let backup;
124
- if (await exists2(abs)) {
125
- const current = await readFile2(abs, "utf8");
126
- if (priorEntry && sha256(current) === priorEntry.hash) {
127
- result.skippedUnchanged.push(entry.target);
128
- result.files.push(priorEntry);
129
- continue;
130
- }
131
- backup = await freeBackupPath(repoDir, entry.target);
132
- await rename(abs, join4(repoDir, backup));
133
- result.backedUp.push({ path: entry.target, backup });
134
- }
135
- await mkdir2(dirname2(abs), { recursive: true });
136
- await writeFile2(abs, templateContent);
137
- result.written.push(entry.target);
138
- result.files.push({
139
- path: entry.target,
140
- template: entry.template,
141
- templateVersion: bearingsVersion,
142
- hash: sha256(templateContent),
143
- owner: entry.owner,
144
- ...backup ? { backup } : {}
145
- });
146
- }
147
- return result;
148
- }
149
-
150
- // src/adapters.ts
151
- import { mkdir as mkdir3, readdir, symlink as symlink2, lstat, readlink, rm as rm2, cp, readFile as readFile3 } from "fs/promises";
152
- import { join as join5 } from "path";
153
- var KINDS = ["skills", "commands"];
154
- async function entriesMatch(src, dst) {
155
- const srcStat = await lstat(src).catch(() => null);
156
- const dstStat = await lstat(dst).catch(() => null);
157
- if (!srcStat || !dstStat) return false;
158
- if (srcStat.isSymbolicLink() || dstStat.isSymbolicLink()) {
159
- return srcStat.isSymbolicLink() && dstStat.isSymbolicLink() && await readlink(src) === await readlink(dst);
28
+ async function verify(repoDir) {
29
+ const failures = [];
30
+ const warnings = [];
31
+ const state = await inspectManifest(repoDir);
32
+ if (state.kind === "absent") {
33
+ return { failures: [{ code: "no-manifest", path: ".agents/bearings.json", message: "run bearings init first" }], warnings };
160
34
  }
161
- if (srcStat.isDirectory() || dstStat.isDirectory()) {
162
- if (!srcStat.isDirectory() || !dstStat.isDirectory()) return false;
163
- const [srcEntries, dstEntries] = await Promise.all([readdir(src), readdir(dst)]);
164
- if (srcEntries.length !== dstEntries.length) return false;
165
- srcEntries.sort();
166
- dstEntries.sort();
167
- for (let i = 0; i < srcEntries.length; i++) {
168
- if (srcEntries[i] !== dstEntries[i]) return false;
169
- if (!await entriesMatch(join5(src, srcEntries[i]), join5(dst, dstEntries[i]))) return false;
170
- }
171
- return true;
35
+ if (state.kind === "invalid") {
36
+ return { failures: [{ code: "invalid-manifest", path: ".agents/bearings.json", message: state.message }], warnings };
172
37
  }
173
- if (srcStat.isFile() || dstStat.isFile()) {
174
- return srcStat.isFile() && dstStat.isFile() && (await readFile3(src)).equals(await readFile3(dst));
38
+ const m = state.manifest;
39
+ if ("setupPending" in m && m.setupPending) {
40
+ warnings.push({ code: "setup-pending", path: ".agents/bearings.json", message: "run /setup-repo to finish the pending update" });
175
41
  }
176
- return false;
177
- }
178
- async function expose(repoDir, harness, mode) {
179
- const created = [];
180
- for (const kind of KINDS) {
181
- const srcDir = join5(repoDir, ".agents", kind);
182
- let entries;
183
- try {
184
- entries = await readdir(srcDir);
185
- } catch {
186
- continue;
187
- }
188
- const dstDir = join5(repoDir, `.${harness}`, kind);
189
- await mkdir3(dstDir, { recursive: true });
190
- for (const name of entries) {
191
- const dst = join5(dstDir, name);
192
- const adapterPath = join5(`.${harness}`, kind, name);
193
- const relTarget = join5("..", "..", ".agents", kind, name);
194
- const stat2 = await lstat(dst).catch(() => null);
195
- if (mode === "symlink") {
196
- if (stat2?.isSymbolicLink() && await readlink(dst) === relTarget) continue;
197
- if (stat2 && !stat2.isSymbolicLink()) throw new Error(`Refusing to replace existing adapter entry: ${adapterPath}`);
198
- if (stat2) await rm2(dst, { recursive: true });
199
- await symlink2(relTarget, dst);
42
+ for (const f of m.files) {
43
+ const retired = "retired" in f && f.retired === true;
44
+ if (!retired) {
45
+ const abs = join(repoDir, f.path);
46
+ if (!await exists(abs)) {
47
+ failures.push({ code: "missing-file", path: f.path, message: "managed file deleted" });
200
48
  } else {
201
- const src = join5(srcDir, name);
202
- if (stat2 && await entriesMatch(src, dst)) continue;
203
- if (stat2 && !stat2.isSymbolicLink()) throw new Error(`Refusing to replace existing adapter entry: ${adapterPath}`);
204
- if (stat2) await rm2(dst, { recursive: true });
205
- await cp(src, dst, { recursive: true });
206
- }
207
- created.push(adapterPath);
208
- }
209
- }
210
- return created;
211
- }
212
-
213
- // src/report.ts
214
- function renderInitReport(r, exposed, m) {
215
- const lines = ["bearings init complete.", ""];
216
- lines.push(`Written (${r.written.length}):`, ...r.written.map((p) => ` + ${p}`));
217
- if (r.backedUp.length) {
218
- lines.push("", `Backed up (${r.backedUp.length}) \u2014 review during /setup-repo:`);
219
- lines.push(...r.backedUp.map((b) => ` \u26A0 ${b.path} -> ${b.backup}`));
220
- }
221
- if (r.skippedUnchanged.length) {
222
- lines.push(
223
- "",
224
- `Skipped, unchanged (${r.skippedUnchanged.length}):`,
225
- ...r.skippedUnchanged.map((p) => ` = ${p}`)
226
- );
227
- }
228
- lines.push(
229
- "",
230
- `Exposed to harnesses [${m.harnesses.join(", ")}] via ${m.exposure}:`,
231
- ...exposed.map((p) => ` ~ ${p}`)
232
- );
233
- lines.push(
234
- "",
235
- "Next step \u2014 finish setup with your AI agent:",
236
- " Open your agent (Claude Code, OpenCode, ...) in this repo and run:",
237
- " /setup-repo",
238
- " It will review any backups, interview you, and tailor the setup.",
239
- " Finish by running: bearings verify"
240
- );
241
- return lines.join("\n");
242
- }
243
- function renderVerifyReport(v) {
244
- const lines = [];
245
- for (const f of v.failures) lines.push(`FAIL ${f.code} ${f.path} \u2014 ${f.message}`);
246
- for (const w of v.warnings) lines.push(`warn ${w.code} ${w.path} \u2014 ${w.message}`);
247
- lines.push(v.failures.length ? `
248
- ${v.failures.length} failure(s).` : "\nbearings verify: OK");
249
- return lines.join("\n");
250
- }
251
-
252
- // src/commands/init.ts
253
- var VALID_HARNESSES = ["claude", "opencode"];
254
- var KINDS2 = ["skills", "commands"];
255
- function validateHarnesses(harnesses) {
256
- if (!harnesses) return void 0;
257
- for (const harness of harnesses) {
258
- if (!VALID_HARNESSES.includes(harness)) {
259
- throw new Error(`Invalid harness: ${String(harness)}`);
260
- }
261
- }
262
- return harnesses;
263
- }
264
- function cancelInit(p) {
265
- p.cancel("Init cancelled.");
266
- throw new Error("Init cancelled.");
267
- }
268
- async function plannedAdapterSources(repoDir) {
269
- const sources = { skills: /* @__PURE__ */ new Map(), commands: /* @__PURE__ */ new Map() };
270
- for (const entry of SCAFFOLD) {
271
- for (const kind of KINDS2) {
272
- const prefix = `.agents/${kind}/`;
273
- if (entry.target.startsWith(prefix)) {
274
- const name = entry.target.slice(prefix.length).split("/")[0];
275
- sources[kind].set(name, join6(templatesDir(), "agents", kind, name));
49
+ const content = await readFile(abs, "utf8");
50
+ if (f.owner === "agent" && content.includes("<agent:")) {
51
+ warnings.push({ code: "unfilled-placeholder", path: f.path, message: "run /setup-repo to fill" });
52
+ }
276
53
  }
277
54
  }
278
- }
279
- for (const kind of KINDS2) {
280
- const srcDir = join6(repoDir, ".agents", kind);
281
- for (const name of await readdir2(srcDir).catch(() => [])) {
282
- if (!sources[kind].has(name)) sources[kind].set(name, join6(srcDir, name));
283
- }
284
- }
285
- return sources;
286
- }
287
- async function preflightAdapterCollisions(repoDir, harnesses, exposure) {
288
- const sources = await plannedAdapterSources(repoDir);
289
- for (const h of harnesses) {
290
- for (const kind of KINDS2) {
291
- for (const [name, source] of sources[kind]) {
292
- const adapterPath = join6(`.${h}`, kind, name);
293
- const target = join6(repoDir, adapterPath);
294
- const stat2 = await lstat2(target).catch(() => null);
295
- if (!stat2 || stat2.isSymbolicLink()) continue;
296
- if (exposure === "copy" && await entriesMatch(source, target)) continue;
297
- throw new Error(`Refusing to replace existing adapter entry: ${adapterPath}`);
298
- }
55
+ if ("backup" in f && f.backup && await exists(join(repoDir, f.backup))) {
56
+ warnings.push({ code: "unreviewed-backup", path: f.backup, message: "review during /setup-repo, then delete" });
299
57
  }
300
- }
301
- }
302
- async function runInit(repoDir, flags, version) {
303
- const s = await scan(repoDir);
304
- let harnesses = validateHarnesses(flags.harnesses);
305
- let exposure = flags.exposure;
306
- if (!harnesses || !exposure) {
307
- if (flags.yes || !process.stdin.isTTY) {
308
- harnesses ??= s.harnessDirsPresent.length ? s.harnessDirsPresent : ["claude", "opencode"];
309
- exposure ??= s.symlinksSupported ? "symlink" : "copy";
310
- } else {
311
- const p = await import("@clack/prompts");
312
- if (!harnesses) {
313
- const selectedHarnesses = await p.multiselect({
314
- message: "Expose skills/commands to which harnesses?",
315
- options: [
316
- { value: "claude", label: "Claude Code (.claude/)" },
317
- { value: "opencode", label: "OpenCode (.opencode/)" }
318
- ],
319
- initialValues: s.harnessDirsPresent.length ? s.harnessDirsPresent : ["claude", "opencode"]
320
- });
321
- if (p.isCancel(selectedHarnesses)) {
322
- cancelInit(p);
58
+ if ("reconciliations" in f && f.reconciliations) {
59
+ for (const r of f.reconciliations) {
60
+ if (await exists(join(repoDir, r.backup))) {
61
+ warnings.push({ code: "pending-reconciliation", path: r.backup, message: "review during /setup-repo, then delete" });
323
62
  }
324
- harnesses = validateHarnesses(selectedHarnesses);
325
63
  }
326
- if (!exposure && s.symlinksSupported) {
327
- const selectedExposure = await p.select({
328
- message: "Exposure mode?",
329
- options: [
330
- { value: "symlink", label: "Symlinks (recommended)" },
331
- { value: "copy", label: "Copies (verify checks drift)" }
332
- ]
333
- });
334
- if (p.isCancel(selectedExposure)) {
335
- cancelInit(p);
336
- }
337
- exposure = selectedExposure;
338
- } else {
339
- exposure ??= "copy";
340
- }
341
- }
342
- }
343
- await preflightAdapterCollisions(repoDir, harnesses, exposure);
344
- const gen = await generate(repoDir, version);
345
- const exposed = [];
346
- for (const h of harnesses) exposed.push(...await expose(repoDir, h, exposure));
347
- const manifest = {
348
- version: 1,
349
- bearingsVersion: version,
350
- harnesses,
351
- exposure,
352
- files: gen.files
353
- };
354
- await saveManifest(repoDir, manifest);
355
- return { manifest, report: renderInitReport(gen, exposed, manifest) };
356
- }
357
-
358
- // src/verifier.ts
359
- import { access as access3, lstat as lstat3, readFile as readFile4, readdir as readdir3, readlink as readlink2, stat } from "fs/promises";
360
- import { dirname as dirname3, join as join7, resolve } from "path";
361
- var KINDS3 = ["skills", "commands"];
362
- async function exists3(p) {
363
- try {
364
- await access3(p);
365
- return true;
366
- } catch {
367
- return false;
368
- }
369
- }
370
- async function verify(repoDir) {
371
- const failures = [];
372
- const warnings = [];
373
- const m = await loadManifest(repoDir);
374
- if (!m) return { failures: [{ code: "no-manifest", path: ".agents/bearings.json", message: "run bearings init first" }], warnings };
375
- for (const f of m.files) {
376
- const abs = join7(repoDir, f.path);
377
- if (!await exists3(abs)) {
378
- failures.push({ code: "missing-file", path: f.path, message: "managed file deleted" });
379
- continue;
380
- }
381
- const content = await readFile4(abs, "utf8");
382
- if (content.includes("<agent:")) {
383
- warnings.push({ code: "unfilled-placeholder", path: f.path, message: "run /setup-repo to fill" });
384
- }
385
- if (f.backup && await exists3(join7(repoDir, f.backup))) {
386
- warnings.push({ code: "unreviewed-backup", path: f.backup, message: "review during /setup-repo, then delete" });
387
64
  }
388
65
  }
389
- for (const kind of KINDS3) {
390
- const srcDir = join7(repoDir, ".agents", kind);
66
+ for (const kind of KINDS) {
67
+ const srcDir = join(repoDir, ".agents", kind);
391
68
  let entries = [];
392
69
  try {
393
- entries = await readdir3(srcDir);
70
+ entries = await readdir(srcDir);
394
71
  } catch {
395
72
  }
396
73
  for (const h of m.harnesses) {
397
74
  for (const name of entries) {
398
- const exposurePath = join7(`.${h}`, kind, name);
399
- const dst = join7(repoDir, `.${h}`, kind, name);
400
- const l = await lstat3(dst).catch(() => null);
75
+ const exposurePath = join(`.${h}`, kind, name);
76
+ const dst = join(repoDir, `.${h}`, kind, name);
77
+ const l = await lstat(dst).catch(() => null);
401
78
  if (!l) {
402
79
  failures.push({ code: "missing-exposure", path: exposurePath, message: `not exposed to ${h}` });
403
80
  continue;
@@ -406,15 +83,15 @@ async function verify(repoDir) {
406
83
  if (!await stat(dst).catch(() => null)) {
407
84
  failures.push({ code: "broken-symlink", path: exposurePath, message: "symlink target missing" });
408
85
  } else {
409
- const target = await readlink2(dst);
410
- if (resolve(dirname3(dst), target) !== resolve(srcDir, name)) {
86
+ const target = await readlink(dst);
87
+ if (resolve(dirname(dst), target) !== resolve(srcDir, name)) {
411
88
  failures.push({ code: "missing-exposure", path: exposurePath, message: `not exposed to ${h}` });
412
89
  }
413
90
  }
414
91
  } else if (m.exposure === "symlink") {
415
92
  failures.push({ code: "missing-exposure", path: exposurePath, message: `not exposed to ${h}` });
416
93
  } else {
417
- const src = join7(srcDir, name);
94
+ const src = join(srcDir, name);
418
95
  if (await copyDrifted(src, dst)) {
419
96
  failures.push({ code: "copy-drift", path: exposurePath, message: "diverged from .agents source" });
420
97
  }
@@ -428,8 +105,8 @@ async function copyDrifted(src, dst) {
428
105
  const s = await stat(src);
429
106
  if (s.isDirectory()) {
430
107
  const [srcChildren, dstChildren] = await Promise.all([
431
- readdir3(src),
432
- readdir3(dst).catch(() => null)
108
+ readdir(src),
109
+ readdir(dst).catch(() => null)
433
110
  ]);
434
111
  if (dstChildren === null) return true;
435
112
  const srcNames = new Set(srcChildren);
@@ -437,13 +114,13 @@ async function copyDrifted(src, dst) {
437
114
  if (!srcNames.has(child)) return true;
438
115
  }
439
116
  for (const child of srcChildren) {
440
- if (await copyDrifted(join7(src, child), join7(dst, child))) return true;
117
+ if (await copyDrifted(join(src, child), join(dst, child))) return true;
441
118
  }
442
119
  return false;
443
120
  }
444
121
  const [a, b] = await Promise.all([
445
- readFile4(src, "utf8"),
446
- readFile4(dst, "utf8").catch(() => null)
122
+ readFile(src, "utf8"),
123
+ readFile(dst, "utf8").catch(() => null)
447
124
  ]);
448
125
  return b === null || sha256(a) !== sha256(b);
449
126
  }
@@ -457,7 +134,7 @@ async function runVerify(repoDir) {
457
134
 
458
135
  // src/cli.ts
459
136
  var pkg = JSON.parse(
460
- readFileSync(join8(dirname4(fileURLToPath2(import.meta.url)), "../package.json"), "utf8")
137
+ readFileSync(join2(dirname2(fileURLToPath(import.meta.url)), "../package.json"), "utf8")
461
138
  );
462
139
  var program = new Command("bearings").version(pkg.version);
463
140
  program.command("init").description("Scaffold the agent-friendly setup in the current repository").option("--harness <name...>", "claude and/or opencode").option("--copy", "copy instead of symlink").option("-y, --yes", "accept defaults, no prompts").action(async (o) => {