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/README.md +10 -7
- package/dist/chunk-GUIHDRBB.js +659 -0
- package/dist/cli.js +54 -377
- package/dist/update-GAIUBEGX.js +799 -0
- package/package.json +1 -1
- package/templates/AGENTS.md +18 -33
- package/templates/agents/commands/refresh-repo-map.md +161 -0
- package/templates/agents/commands/setup-repo.md +67 -32
- package/templates/agents/skills/commit-convention/SKILL.md +83 -20
- package/templates/agents/skills/defer-work/SKILL.md +2 -2
- package/templates/agents/skills/enforcement-gates/SKILL.md +1 -0
- package/templates/agents/skills/implementing-task/SKILL.md +16 -7
- package/templates/agents/skills/installing-dependencies/SKILL.md +6 -0
- package/templates/agents/skills/recording-decisions/SKILL.md +51 -0
- package/templates/agents/skills/repo-navigation/SKILL.md +27 -8
- package/templates/agents/skills/resurface-deferred-work/SKILL.md +2 -2
- package/templates/agents/skills/secrets-handling/SKILL.md +1 -0
- package/templates/docs/ARCHITECTURE.md +37 -0
- package/templates/docs/CODEBASE_MAP.md +33 -0
- package/templates/docs/DOMAIN.md +33 -0
- package/templates/docs/adr/0000-template.md +12 -0
- package/templates/docs/adr/INDEX.md +6 -0
- package/templates/docs/deferred/INDEX.md +5 -3
- package/templates/agents/commands/current-state.md +0 -163
- package/templates/docs/CURRENT_STATE.md +0 -38
- package/templates/docs/conventions/current-state.md +0 -9
- package/templates/docs/conventions/doc-lifecycle.md +0 -16
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/
|
|
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
|
|
60
|
-
const
|
|
61
|
-
|
|
62
|
-
const
|
|
63
|
-
|
|
64
|
-
|
|
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 (
|
|
162
|
-
|
|
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
|
-
|
|
174
|
-
|
|
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
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
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
|
|
202
|
-
if (
|
|
203
|
-
|
|
204
|
-
|
|
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
|
-
|
|
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
|
-
|
|
303
|
-
|
|
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
|
|
390
|
-
const srcDir =
|
|
66
|
+
for (const kind of KINDS) {
|
|
67
|
+
const srcDir = join(repoDir, ".agents", kind);
|
|
391
68
|
let entries = [];
|
|
392
69
|
try {
|
|
393
|
-
entries = await
|
|
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 =
|
|
399
|
-
const dst =
|
|
400
|
-
const l = await
|
|
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
|
|
410
|
-
if (resolve(
|
|
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 =
|
|
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
|
-
|
|
432
|
-
|
|
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(
|
|
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
|
-
|
|
446
|
-
|
|
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(
|
|
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) => {
|