@rness/cli 0.3.0 → 0.5.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 +177 -7
- package/dist/bin/rness.js +32 -33
- package/dist/bin/rness.js.map +1 -1
- package/dist/chunk-WSGPDTPQ.js +3857 -0
- package/dist/chunk-WSGPDTPQ.js.map +1 -0
- package/dist/index.js +61 -37
- package/dist/index.js.map +1 -1
- package/package.json +16 -7
- package/scaffold/.github/workflows/validate.yml +2 -1
- package/scaffold/README.md +1 -1
- package/scaffold/WORKSPACE.md +7 -5
- package/scaffold/pnpm-workspace.yaml +4 -0
- package/dist/chunk-4KNZT4G4.js +0 -791
- package/dist/chunk-4KNZT4G4.js.map +0 -1
package/dist/chunk-4KNZT4G4.js
DELETED
|
@@ -1,791 +0,0 @@
|
|
|
1
|
-
// src/version.ts
|
|
2
|
-
var VERSION = "0.3.0";
|
|
3
|
-
|
|
4
|
-
// src/core/fs.ts
|
|
5
|
-
import { access, lstat, readFile, rename, rm, writeFile } from "fs/promises";
|
|
6
|
-
import { basename, dirname, join } from "path";
|
|
7
|
-
async function exists(p) {
|
|
8
|
-
try {
|
|
9
|
-
await access(p);
|
|
10
|
-
return true;
|
|
11
|
-
} catch {
|
|
12
|
-
return false;
|
|
13
|
-
}
|
|
14
|
-
}
|
|
15
|
-
async function isSymlink(p) {
|
|
16
|
-
try {
|
|
17
|
-
return (await lstat(p)).isSymbolicLink();
|
|
18
|
-
} catch (e) {
|
|
19
|
-
if (e.code === "ENOENT") return false;
|
|
20
|
-
throw e;
|
|
21
|
-
}
|
|
22
|
-
}
|
|
23
|
-
async function readOrNull(p) {
|
|
24
|
-
try {
|
|
25
|
-
return await readFile(p, "utf8");
|
|
26
|
-
} catch (e) {
|
|
27
|
-
if (e.code === "ENOENT") return null;
|
|
28
|
-
throw e;
|
|
29
|
-
}
|
|
30
|
-
}
|
|
31
|
-
var sequence = 0;
|
|
32
|
-
async function writeFileAtomic(p, text) {
|
|
33
|
-
sequence += 1;
|
|
34
|
-
const tmp = join(dirname(p), `.${basename(p)}.${process.pid}.${sequence}.tmp`);
|
|
35
|
-
await writeFile(tmp, text, "utf8");
|
|
36
|
-
try {
|
|
37
|
-
await rename(tmp, p);
|
|
38
|
-
} catch (e) {
|
|
39
|
-
await rm(tmp, { force: true }).catch(() => void 0);
|
|
40
|
-
throw e;
|
|
41
|
-
}
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
// src/core/workspace.ts
|
|
45
|
-
import { dirname as dirname2, join as join2, parse, resolve } from "path";
|
|
46
|
-
async function findWorkspace(startDir) {
|
|
47
|
-
const start = resolve(startDir);
|
|
48
|
-
const { root: fsRoot } = parse(start);
|
|
49
|
-
const ancestors = [];
|
|
50
|
-
for (let dir = start; ; dir = dirname2(dir)) {
|
|
51
|
-
ancestors.push(dir);
|
|
52
|
-
if (dir === fsRoot) break;
|
|
53
|
-
}
|
|
54
|
-
for (const dir of ancestors) {
|
|
55
|
-
if (await exists(join2(dir, ".rness", "rness.json"))) return { root: dir, rnessDir: join2(dir, ".rness") };
|
|
56
|
-
}
|
|
57
|
-
for (const dir of ancestors) {
|
|
58
|
-
if (await exists(join2(dir, "rness.json"))) return { root: dirname2(dir), rnessDir: dir };
|
|
59
|
-
}
|
|
60
|
-
throw new Error(`no rness workspace found above ${startDir}`);
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
// src/core/manifest.ts
|
|
64
|
-
import { readFile as readFile2 } from "fs/promises";
|
|
65
|
-
import { basename as basename2, join as join3 } from "path";
|
|
66
|
-
var NAME = /^[a-z0-9][a-z0-9-]*$/;
|
|
67
|
-
function fail(message) {
|
|
68
|
-
throw new Error(`rness.json: ${message}`);
|
|
69
|
-
}
|
|
70
|
-
function isRecord(value) {
|
|
71
|
-
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
72
|
-
}
|
|
73
|
-
function checkPath(scope, path) {
|
|
74
|
-
if (typeof path !== "string" || path === "") fail(`scope "${scope}" needs a string "path"`);
|
|
75
|
-
if (path.startsWith("/")) fail(`scope "${scope}" path must be relative`);
|
|
76
|
-
if (path.split("/").includes("..")) fail(`scope "${scope}" path must not contain ".."`);
|
|
77
|
-
if (path.split("/").some((segment) => segment === "")) fail(`scope "${scope}" path must not contain empty segments`);
|
|
78
|
-
return path;
|
|
79
|
-
}
|
|
80
|
-
function readOrg(value) {
|
|
81
|
-
if (value === void 0 || value === null) return null;
|
|
82
|
-
if (typeof value !== "string" || !NAME.test(value)) {
|
|
83
|
-
fail(`"org" must be a name matching [a-z0-9-] (got ${JSON.stringify(value)})`);
|
|
84
|
-
}
|
|
85
|
-
return value;
|
|
86
|
-
}
|
|
87
|
-
function resolveOrg(manifest, root) {
|
|
88
|
-
if (manifest.org !== null) return { org: manifest.org, warning: null };
|
|
89
|
-
const name = basename2(root);
|
|
90
|
-
return { org: name, warning: `rness.json: no "org"; using the directory name "${name}"` };
|
|
91
|
-
}
|
|
92
|
-
function readRepos(value) {
|
|
93
|
-
const raw = value ?? {};
|
|
94
|
-
if (!isRecord(raw)) fail('"repos" must be an object');
|
|
95
|
-
const repos = {};
|
|
96
|
-
for (const [name, entry] of Object.entries(raw)) {
|
|
97
|
-
if (!NAME.test(name)) fail(`repo name "${name}" is not [a-z0-9-]`);
|
|
98
|
-
if (!isRecord(entry) || typeof entry.url !== "string") fail(`repo "${name}" needs a string "url"`);
|
|
99
|
-
repos[name] = { url: entry.url };
|
|
100
|
-
}
|
|
101
|
-
return repos;
|
|
102
|
-
}
|
|
103
|
-
function readScopes(value) {
|
|
104
|
-
const raw = value ?? {};
|
|
105
|
-
if (!isRecord(raw)) fail('"scopes" must be an object');
|
|
106
|
-
const scopes = {};
|
|
107
|
-
for (const [name, entry] of Object.entries(raw)) {
|
|
108
|
-
if (!NAME.test(name)) fail(`scope name "${name}" is not [a-z0-9-]`);
|
|
109
|
-
if (!isRecord(entry)) fail(`scope "${name}" must be an object`);
|
|
110
|
-
const path = checkPath(name, entry.path);
|
|
111
|
-
const ext = entry.extends ?? [];
|
|
112
|
-
if (!Array.isArray(ext) || !ext.every((t) => typeof t === "string")) {
|
|
113
|
-
fail(`scope "${name}" extends must be an array of scope names`);
|
|
114
|
-
}
|
|
115
|
-
scopes[name] = { path, extends: ext };
|
|
116
|
-
}
|
|
117
|
-
for (const [name, entry] of Object.entries(scopes)) {
|
|
118
|
-
for (const target of entry.extends) {
|
|
119
|
-
if (!Object.hasOwn(scopes, target)) fail(`scope "${name}" extends unknown scope "${target}"`);
|
|
120
|
-
}
|
|
121
|
-
}
|
|
122
|
-
return scopes;
|
|
123
|
-
}
|
|
124
|
-
async function loadManifest(rnessDir) {
|
|
125
|
-
let raw;
|
|
126
|
-
try {
|
|
127
|
-
raw = await readFile2(join3(rnessDir, "rness.json"), "utf8");
|
|
128
|
-
} catch {
|
|
129
|
-
fail("not found");
|
|
130
|
-
}
|
|
131
|
-
let data;
|
|
132
|
-
try {
|
|
133
|
-
data = JSON.parse(raw);
|
|
134
|
-
} catch (e) {
|
|
135
|
-
fail(`invalid JSON (${e instanceof Error ? e.message : String(e)})`);
|
|
136
|
-
}
|
|
137
|
-
if (!isRecord(data)) fail("must be a JSON object");
|
|
138
|
-
if (data.contract !== 1) fail(`unsupported contract: ${JSON.stringify(data.contract)} (expected 1)`);
|
|
139
|
-
return { contract: 1, org: readOrg(data.org), repos: readRepos(data.repos), scopes: readScopes(data.scopes) };
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
// src/core/scope.ts
|
|
143
|
-
function isPrefix(prefix, target) {
|
|
144
|
-
return prefix === target || target.startsWith(`${prefix}/`);
|
|
145
|
-
}
|
|
146
|
-
function resolveScope(manifest, relDir) {
|
|
147
|
-
let best = null;
|
|
148
|
-
let bestLen = -1;
|
|
149
|
-
for (const [name, entry] of Object.entries(manifest.scopes)) {
|
|
150
|
-
if (isPrefix(entry.path, relDir) && entry.path.length > bestLen) {
|
|
151
|
-
best = name;
|
|
152
|
-
bestLen = entry.path.length;
|
|
153
|
-
}
|
|
154
|
-
}
|
|
155
|
-
return best;
|
|
156
|
-
}
|
|
157
|
-
function scopeChain(manifest, scope) {
|
|
158
|
-
if (scope === null) return [];
|
|
159
|
-
const chain = [];
|
|
160
|
-
const seen = /* @__PURE__ */ new Set();
|
|
161
|
-
const visit = (name, stack) => {
|
|
162
|
-
if (stack.has(name)) throw new Error(`extends cycle at scope "${name}"`);
|
|
163
|
-
if (seen.has(name)) return;
|
|
164
|
-
seen.add(name);
|
|
165
|
-
chain.push(name);
|
|
166
|
-
stack.add(name);
|
|
167
|
-
const entry = Object.hasOwn(manifest.scopes, name) ? manifest.scopes[name] : void 0;
|
|
168
|
-
for (const target of entry?.extends ?? []) visit(target, stack);
|
|
169
|
-
stack.delete(name);
|
|
170
|
-
};
|
|
171
|
-
visit(scope, /* @__PURE__ */ new Set());
|
|
172
|
-
return chain;
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
// src/core/frontmatter.ts
|
|
176
|
-
import { parse as parse2 } from "yaml";
|
|
177
|
-
var FRONT_MATTER = /^---\r?\n(?:([\s\S]*?)\r?\n)?---(?:\r?\n|$)/;
|
|
178
|
-
function fail2(message) {
|
|
179
|
-
throw new Error(`front matter: ${message}`);
|
|
180
|
-
}
|
|
181
|
-
function stripBom(source) {
|
|
182
|
-
return source.startsWith("\uFEFF") ? source.slice(1) : source;
|
|
183
|
-
}
|
|
184
|
-
function parseFrontMatter(source) {
|
|
185
|
-
const match = stripBom(source).match(FRONT_MATTER);
|
|
186
|
-
if (match === null) return null;
|
|
187
|
-
const block = match[1] ?? "";
|
|
188
|
-
if (block.trim() === "") return {};
|
|
189
|
-
let doc;
|
|
190
|
-
try {
|
|
191
|
-
doc = parse2(block, { logLevel: "error" });
|
|
192
|
-
} catch (e) {
|
|
193
|
-
fail2(e instanceof Error ? (e.message.split("\n")[0] ?? "invalid YAML").replace(/:$/, "") : "invalid YAML");
|
|
194
|
-
}
|
|
195
|
-
if (doc === null || doc === void 0) return {};
|
|
196
|
-
if (typeof doc !== "object" || Array.isArray(doc)) fail2("block must be a mapping of keys to values");
|
|
197
|
-
return doc;
|
|
198
|
-
}
|
|
199
|
-
function stripFrontMatter(source) {
|
|
200
|
-
const text = stripBom(source);
|
|
201
|
-
const match = text.match(FRONT_MATTER);
|
|
202
|
-
if (match === null) return text;
|
|
203
|
-
return text.slice(match[0].length).replace(/^(?:[ \t]*\r?\n)+/, "");
|
|
204
|
-
}
|
|
205
|
-
function extractTitle(source) {
|
|
206
|
-
return stripBom(source).match(/^# (.+)$/m)?.[1]?.trim() ?? null;
|
|
207
|
-
}
|
|
208
|
-
|
|
209
|
-
// src/core/collect.ts
|
|
210
|
-
import { glob, readFile as readFile3 } from "fs/promises";
|
|
211
|
-
import { join as join4, posix, sep } from "path";
|
|
212
|
-
function toPosix(rel) {
|
|
213
|
-
return rel.split(sep).join(posix.sep);
|
|
214
|
-
}
|
|
215
|
-
async function collectMarkdown(dir) {
|
|
216
|
-
const rels = [];
|
|
217
|
-
for await (const rel of glob("**/*.md", { cwd: dir })) rels.push(toPosix(rel));
|
|
218
|
-
const items = await Promise.all(
|
|
219
|
-
rels.map(async (rel) => {
|
|
220
|
-
const path = join4(dir, ...rel.split(posix.sep));
|
|
221
|
-
const body = await readFile3(path, "utf8");
|
|
222
|
-
let fields = null;
|
|
223
|
-
let fieldsError = null;
|
|
224
|
-
try {
|
|
225
|
-
fields = parseFrontMatter(body);
|
|
226
|
-
} catch (e) {
|
|
227
|
-
fieldsError = e instanceof Error ? e.message : String(e);
|
|
228
|
-
}
|
|
229
|
-
return { path, rel, fields, fieldsError, title: extractTitle(body), body, content: stripFrontMatter(body) };
|
|
230
|
-
})
|
|
231
|
-
);
|
|
232
|
-
return items.sort((a, b) => a.rel < b.rel ? -1 : a.rel > b.rel ? 1 : 0);
|
|
233
|
-
}
|
|
234
|
-
|
|
235
|
-
// src/core/context.ts
|
|
236
|
-
import { join as join5 } from "path";
|
|
237
|
-
var COLLECTIONS = ["standards", "adr", "specs", "plans", "skills"];
|
|
238
|
-
function ownerScope(rel) {
|
|
239
|
-
const slash = rel.indexOf("/");
|
|
240
|
-
return slash === -1 ? null : rel.slice(0, slash);
|
|
241
|
-
}
|
|
242
|
-
async function assembleCollection(dir, name, chain) {
|
|
243
|
-
const items = await collectMarkdown(dir);
|
|
244
|
-
const groups = new Map(chain.map((s) => [s, []]));
|
|
245
|
-
const rootFiles = [];
|
|
246
|
-
for (const item of items) {
|
|
247
|
-
const owner = ownerScope(item.rel);
|
|
248
|
-
if (owner === null) rootFiles.push({ ...item, scope: null });
|
|
249
|
-
else groups.get(owner)?.push({ ...item, scope: owner });
|
|
250
|
-
}
|
|
251
|
-
const files = [];
|
|
252
|
-
const push = (f) => {
|
|
253
|
-
files.push({ rel: f.rel, scope: f.scope, title: f.title, body: f.body, content: f.content });
|
|
254
|
-
};
|
|
255
|
-
for (const s of chain) for (const f of groups.get(s) ?? []) push(f);
|
|
256
|
-
for (const f of rootFiles) push(f);
|
|
257
|
-
return { name, files };
|
|
258
|
-
}
|
|
259
|
-
async function assembleContext({ rnessDir, manifest, scope }) {
|
|
260
|
-
const chain = scopeChain(manifest, scope);
|
|
261
|
-
const collections = await Promise.all(
|
|
262
|
-
COLLECTIONS.map((name) => assembleCollection(join5(rnessDir, name), name, chain))
|
|
263
|
-
);
|
|
264
|
-
return { scope, collections };
|
|
265
|
-
}
|
|
266
|
-
|
|
267
|
-
// src/report.ts
|
|
268
|
-
function reportError(e) {
|
|
269
|
-
const debug = process.env["RNESS_DEBUG"] === "1";
|
|
270
|
-
if (debug && e instanceof Error && e.stack) {
|
|
271
|
-
process.stderr.write(`${e.stack}
|
|
272
|
-
`);
|
|
273
|
-
} else {
|
|
274
|
-
const message = e instanceof Error ? e.message : String(e);
|
|
275
|
-
process.stderr.write(`${message}
|
|
276
|
-
`);
|
|
277
|
-
}
|
|
278
|
-
return 1;
|
|
279
|
-
}
|
|
280
|
-
|
|
281
|
-
// src/commands/context.ts
|
|
282
|
-
import { posix as posix2, relative, sep as sep2 } from "path";
|
|
283
|
-
function renderMarkdown(ctx) {
|
|
284
|
-
const parts = [];
|
|
285
|
-
for (const collection of ctx.collections) {
|
|
286
|
-
if (collection.files.length === 0) continue;
|
|
287
|
-
parts.push(`## ${collection.name}
|
|
288
|
-
`);
|
|
289
|
-
for (const file of collection.files) {
|
|
290
|
-
parts.push(`<!-- rness: ${collection.name}/${file.rel} -->
|
|
291
|
-
${file.content.trim()}
|
|
292
|
-
`);
|
|
293
|
-
}
|
|
294
|
-
}
|
|
295
|
-
return `${parts.join("\n")}
|
|
296
|
-
`;
|
|
297
|
-
}
|
|
298
|
-
async function contextCommand(opts) {
|
|
299
|
-
const cwd = opts.cwd ?? process.cwd();
|
|
300
|
-
try {
|
|
301
|
-
const ws = await findWorkspace(cwd);
|
|
302
|
-
const manifest = await loadManifest(ws.rnessDir);
|
|
303
|
-
let scope;
|
|
304
|
-
if (opts.scope !== void 0) {
|
|
305
|
-
if (!Object.hasOwn(manifest.scopes, opts.scope)) {
|
|
306
|
-
process.stderr.write(`unknown scope: ${opts.scope}
|
|
307
|
-
`);
|
|
308
|
-
return 1;
|
|
309
|
-
}
|
|
310
|
-
scope = opts.scope;
|
|
311
|
-
} else {
|
|
312
|
-
const rel = relative(ws.root, cwd).split(sep2).join(posix2.sep);
|
|
313
|
-
scope = resolveScope(manifest, rel);
|
|
314
|
-
}
|
|
315
|
-
const ctx = await assembleContext({ rnessDir: ws.rnessDir, manifest, scope });
|
|
316
|
-
process.stdout.write(opts.json ? `${JSON.stringify(ctx, null, 2)}
|
|
317
|
-
` : renderMarkdown(ctx));
|
|
318
|
-
return 0;
|
|
319
|
-
} catch (e) {
|
|
320
|
-
return reportError(e);
|
|
321
|
-
}
|
|
322
|
-
}
|
|
323
|
-
|
|
324
|
-
// src/core/contract.ts
|
|
325
|
-
import { join as join6 } from "path";
|
|
326
|
-
var STATUSES = {
|
|
327
|
-
adr: ["Proposed", "Accepted", "Rejected", "Superseded"],
|
|
328
|
-
specs: ["Draft", "Proposed", "Approved", "Implemented", "Superseded", "Rejected"],
|
|
329
|
-
plans: ["Draft", "Ready", "In progress", "Blocked", "Completed", "Abandoned"]
|
|
330
|
-
};
|
|
331
|
-
var SKIP = /* @__PURE__ */ new Set(["adr/0000-template.md"]);
|
|
332
|
-
var PLACEMENT_KEYS = ["scopes", "scope"];
|
|
333
|
-
async function checkContract(rnessDir) {
|
|
334
|
-
const problems = [];
|
|
335
|
-
for (const name of COLLECTIONS) {
|
|
336
|
-
const allowed = STATUSES[name];
|
|
337
|
-
const items = await collectMarkdown(join6(rnessDir, name));
|
|
338
|
-
for (const item of items) {
|
|
339
|
-
const where = `${name}/${item.rel}`;
|
|
340
|
-
if (SKIP.has(where)) continue;
|
|
341
|
-
if (item.fieldsError !== null) {
|
|
342
|
-
problems.push(`${where}: invalid front matter (${item.fieldsError})`);
|
|
343
|
-
continue;
|
|
344
|
-
}
|
|
345
|
-
if (item.fields !== null) {
|
|
346
|
-
for (const key of PLACEMENT_KEYS) {
|
|
347
|
-
if (Object.hasOwn(item.fields, key)) {
|
|
348
|
-
problems.push(`${where}: front matter "${key}" is not supported \u2014 the directory decides the scope; move the file`);
|
|
349
|
-
}
|
|
350
|
-
}
|
|
351
|
-
}
|
|
352
|
-
if (allowed === void 0) continue;
|
|
353
|
-
if (item.fields === null) {
|
|
354
|
-
problems.push(`${where}: missing a front-matter block`);
|
|
355
|
-
continue;
|
|
356
|
-
}
|
|
357
|
-
if (item.title === null) problems.push(`${where}: missing a "# " title`);
|
|
358
|
-
const status = item.fields["status"];
|
|
359
|
-
if (status === void 0 || status === null || status === "") {
|
|
360
|
-
problems.push(`${where}: missing "status"`);
|
|
361
|
-
} else if (typeof status !== "string" || !allowed.includes(status)) {
|
|
362
|
-
problems.push(`${where}: unknown status ${JSON.stringify(status)} (expected ${allowed.join(", ")})`);
|
|
363
|
-
}
|
|
364
|
-
}
|
|
365
|
-
}
|
|
366
|
-
return problems;
|
|
367
|
-
}
|
|
368
|
-
|
|
369
|
-
// src/core/block.ts
|
|
370
|
-
import { createHash } from "crypto";
|
|
371
|
-
var BEGIN = "<!-- BEGIN rness -->";
|
|
372
|
-
var END = "<!-- END rness -->";
|
|
373
|
-
var BLOCK_SIZE_WARNING = 32 * 1024;
|
|
374
|
-
var HEADER = /^<!-- rness (\S+) · scope: (\S+) · contract: 1 · hash: ([0-9a-f]{12}) · generated: run `rness sync`, never edit inside this block -->$/;
|
|
375
|
-
function toLf(s) {
|
|
376
|
-
return s.replace(/\r\n?/g, "\n");
|
|
377
|
-
}
|
|
378
|
-
function blockHash(body) {
|
|
379
|
-
return createHash("sha256").update(toLf(body), "utf8").digest("hex").slice(0, 12);
|
|
380
|
-
}
|
|
381
|
-
function intro(scope, org, depth) {
|
|
382
|
-
if (scope === null) {
|
|
383
|
-
return [
|
|
384
|
-
`This is the root of rness workspace \`${org}\`. Full context lives in \`.rness/\` \u2014`,
|
|
385
|
-
"read `STATUS.md`, then task-relevant `adr/`, `specs/`, `plans/`; live: `rness context`.",
|
|
386
|
-
"Every `org/<repo>/` carries its own block with the rules that apply there."
|
|
387
|
-
].join("\n");
|
|
388
|
-
}
|
|
389
|
-
const rel = `${"../".repeat(depth)}.rness/`;
|
|
390
|
-
return [
|
|
391
|
-
`This directory is scope \`${scope}\` of rness workspace \`${org}\`. Full context lives in`,
|
|
392
|
-
`\`${rel}\` \u2014 read \`STATUS.md\`, then task-relevant \`adr/\`, \`specs/\`, \`plans/\`;`,
|
|
393
|
-
`live: \`rness context --scope ${scope}\`. If \`.rness/\` is not reachable, this is a`,
|
|
394
|
-
"standalone clone: the rules below are all you have."
|
|
395
|
-
].join("\n");
|
|
396
|
-
}
|
|
397
|
-
function header(version, scope, hash) {
|
|
398
|
-
return `<!-- rness ${version} \xB7 scope: ${scope ?? "global"} \xB7 contract: 1 \xB7 hash: ${hash} \xB7 generated: run \`rness sync\`, never edit inside this block -->`;
|
|
399
|
-
}
|
|
400
|
-
function renderBlock({ scope, org, depth, context, version }) {
|
|
401
|
-
const standards = context.collections.find((c) => c.name === "standards")?.files ?? [];
|
|
402
|
-
const rules = standards.length === 0 ? ["_No standards apply to this scope yet._"] : standards.map((f) => `<!-- rness: standards/${f.rel} -->
|
|
403
|
-
${toLf(f.content).trim()}`);
|
|
404
|
-
const body = `${intro(scope, org, depth)}
|
|
405
|
-
|
|
406
|
-
## Rules
|
|
407
|
-
${rules.join("\n\n")}`;
|
|
408
|
-
const hash = blockHash(body);
|
|
409
|
-
const text = [BEGIN, header(version, scope, hash), body, END].join("\n");
|
|
410
|
-
return { text, hash, bytes: Buffer.byteLength(text, "utf8") };
|
|
411
|
-
}
|
|
412
|
-
function parseHeader(line) {
|
|
413
|
-
const m = HEADER.exec(line.trimEnd());
|
|
414
|
-
if (m === null) return null;
|
|
415
|
-
return { version: m[1] ?? "", scope: m[2] ?? "", hash: m[3] ?? "" };
|
|
416
|
-
}
|
|
417
|
-
function bodyOf(blockLines) {
|
|
418
|
-
return blockLines.slice(2, -1).join("\n");
|
|
419
|
-
}
|
|
420
|
-
|
|
421
|
-
// src/core/merge.ts
|
|
422
|
-
import { join as join7 } from "path";
|
|
423
|
-
function findBlock(lines) {
|
|
424
|
-
const begins = [];
|
|
425
|
-
const ends = [];
|
|
426
|
-
lines.forEach((line, i) => {
|
|
427
|
-
const l = line.trimEnd();
|
|
428
|
-
if (l === BEGIN) begins.push(i);
|
|
429
|
-
if (l === END) ends.push(i);
|
|
430
|
-
});
|
|
431
|
-
if (begins.length === 0 && ends.length === 0) return { kind: "none" };
|
|
432
|
-
const begin = begins[0];
|
|
433
|
-
const end = ends[0];
|
|
434
|
-
if (begins.length !== 1 || ends.length !== 1 || begin === void 0 || end === void 0) {
|
|
435
|
-
return { kind: "error", message: `expected exactly one ${BEGIN} \u2026 ${END} span, found ${begins.length} BEGIN and ${ends.length} END` };
|
|
436
|
-
}
|
|
437
|
-
if (end < begin) return { kind: "error", message: `${END} comes before ${BEGIN}` };
|
|
438
|
-
return { kind: "one", begin, end };
|
|
439
|
-
}
|
|
440
|
-
function mergeBlock(existing, block) {
|
|
441
|
-
if (existing === null) return { ok: true, text: `${block}
|
|
442
|
-
`, changed: true };
|
|
443
|
-
const eol = existing.includes("\r\n") ? "\r\n" : "\n";
|
|
444
|
-
const lines = existing.split(/\r?\n/);
|
|
445
|
-
const found = findBlock(lines);
|
|
446
|
-
if (found.kind === "error") return { ok: false, error: found.message };
|
|
447
|
-
const rendered = block.split("\n").join(eol);
|
|
448
|
-
const starts = [0];
|
|
449
|
-
for (const m of existing.matchAll(/\r?\n/g)) starts.push((m.index ?? 0) + m[0].length);
|
|
450
|
-
const lineStart = (i) => starts[i] ?? existing.length;
|
|
451
|
-
let text;
|
|
452
|
-
if (found.kind === "one") {
|
|
453
|
-
const endLine = lines[found.end] ?? "";
|
|
454
|
-
const afterEnd = lineStart(found.end) + endLine.length;
|
|
455
|
-
text = `${existing.slice(0, lineStart(found.begin))}${rendered}${existing.slice(afterEnd)}`;
|
|
456
|
-
} else if (lines.length >= 3 && (lines[0] ?? "").startsWith("# ") && lines[1] === "") {
|
|
457
|
-
const at = lineStart(2);
|
|
458
|
-
text = `${existing.slice(0, at)}${rendered}${eol}${eol}${existing.slice(at)}`;
|
|
459
|
-
} else {
|
|
460
|
-
text = `${rendered}${eol}${eol}${existing}`;
|
|
461
|
-
}
|
|
462
|
-
return { ok: true, text, changed: text !== existing };
|
|
463
|
-
}
|
|
464
|
-
async function ensureClaudeMd(dir) {
|
|
465
|
-
const file = join7(dir, "CLAUDE.md");
|
|
466
|
-
if (await isSymlink(file)) return "unchanged";
|
|
467
|
-
const existing = await readOrNull(file);
|
|
468
|
-
if (existing === null) {
|
|
469
|
-
await writeFileAtomic(file, "@AGENTS.md\n");
|
|
470
|
-
return "created";
|
|
471
|
-
}
|
|
472
|
-
if (existing.split(/\r?\n/).some((l) => l.trim() === "@AGENTS.md")) return "unchanged";
|
|
473
|
-
const eol = existing.includes("\r\n") ? "\r\n" : "\n";
|
|
474
|
-
await writeFileAtomic(file, `@AGENTS.md${eol}${existing}`);
|
|
475
|
-
return "prepended";
|
|
476
|
-
}
|
|
477
|
-
|
|
478
|
-
// src/core/blocks.ts
|
|
479
|
-
import { join as join8 } from "path";
|
|
480
|
-
async function checkBlocks({ root, rnessDir, manifest, org }) {
|
|
481
|
-
const problems = [];
|
|
482
|
-
const warnings = [];
|
|
483
|
-
if (!await exists(join8(root, "org"))) return { problems, warnings };
|
|
484
|
-
const targets = [
|
|
485
|
-
{ scope: null, dir: root, depth: 0, label: "AGENTS.md" }
|
|
486
|
-
];
|
|
487
|
-
for (const [name, entry] of Object.entries(manifest.scopes)) {
|
|
488
|
-
const segments = entry.path.split("/");
|
|
489
|
-
targets.push({ scope: name, dir: join8(root, ...segments), depth: segments.length, label: `${entry.path}/AGENTS.md` });
|
|
490
|
-
}
|
|
491
|
-
for (const t of targets) {
|
|
492
|
-
if (!await exists(t.dir)) {
|
|
493
|
-
warnings.push(`${t.label}: directory not present, block not checked`);
|
|
494
|
-
continue;
|
|
495
|
-
}
|
|
496
|
-
const text = await readOrNull(join8(t.dir, "AGENTS.md"));
|
|
497
|
-
const lines = text === null ? [] : text.split(/\r?\n/);
|
|
498
|
-
const found = findBlock(lines);
|
|
499
|
-
if (found.kind === "none") {
|
|
500
|
-
warnings.push(`${t.label}: no rness block yet (run rness sync)`);
|
|
501
|
-
continue;
|
|
502
|
-
}
|
|
503
|
-
if (found.kind === "error") {
|
|
504
|
-
problems.push(`${t.label}: ${found.message}`);
|
|
505
|
-
continue;
|
|
506
|
-
}
|
|
507
|
-
const header2 = parseHeader(lines[found.begin + 1] ?? "");
|
|
508
|
-
const context = await assembleContext({ rnessDir, manifest, scope: t.scope });
|
|
509
|
-
const fresh = renderBlock({ scope: t.scope, org, depth: t.depth, context, version: VERSION });
|
|
510
|
-
const actual = blockHash(bodyOf(lines.slice(found.begin, found.end + 1)));
|
|
511
|
-
if (header2 === null || header2.hash !== fresh.hash || header2.hash !== actual) {
|
|
512
|
-
problems.push(`${t.label}: stale rness block (run rness sync)`);
|
|
513
|
-
}
|
|
514
|
-
}
|
|
515
|
-
return { problems, warnings };
|
|
516
|
-
}
|
|
517
|
-
|
|
518
|
-
// src/commands/validate.ts
|
|
519
|
-
async function validateCommand(opts) {
|
|
520
|
-
const cwd = opts.cwd ?? process.cwd();
|
|
521
|
-
try {
|
|
522
|
-
const ws = await findWorkspace(cwd);
|
|
523
|
-
const manifest = await loadManifest(ws.rnessDir);
|
|
524
|
-
for (const s of Object.keys(manifest.scopes)) scopeChain(manifest, s);
|
|
525
|
-
const { org, warning } = resolveOrg(manifest, ws.root);
|
|
526
|
-
const warnings = warning === null ? [] : [warning];
|
|
527
|
-
const problems = await checkContract(ws.rnessDir);
|
|
528
|
-
const blocks = await checkBlocks({ root: ws.root, rnessDir: ws.rnessDir, manifest, org });
|
|
529
|
-
problems.push(...blocks.problems);
|
|
530
|
-
warnings.push(...blocks.warnings);
|
|
531
|
-
for (const p of problems) process.stderr.write(`${p}
|
|
532
|
-
`);
|
|
533
|
-
for (const w of warnings) process.stderr.write(`warning: ${w}
|
|
534
|
-
`);
|
|
535
|
-
if (problems.length > 0) return 1;
|
|
536
|
-
process.stdout.write("context ok\n");
|
|
537
|
-
return 0;
|
|
538
|
-
} catch (e) {
|
|
539
|
-
return reportError(e);
|
|
540
|
-
}
|
|
541
|
-
}
|
|
542
|
-
|
|
543
|
-
// src/commands/sync.ts
|
|
544
|
-
import { join as join9 } from "path";
|
|
545
|
-
|
|
546
|
-
// src/core/git.ts
|
|
547
|
-
import { execFile } from "child_process";
|
|
548
|
-
import { dirname as dirname3, resolve as resolve2 } from "path";
|
|
549
|
-
import { promisify } from "util";
|
|
550
|
-
var execFileP = promisify(execFile);
|
|
551
|
-
async function git(args, cwd) {
|
|
552
|
-
try {
|
|
553
|
-
const { stdout } = await execFileP("git", [...args], {
|
|
554
|
-
...cwd === void 0 ? {} : { cwd },
|
|
555
|
-
env: {
|
|
556
|
-
...process.env,
|
|
557
|
-
GIT_TERMINAL_PROMPT: "0",
|
|
558
|
-
// Pin the command to `cwd` itself: without a ceiling, git walks up and
|
|
559
|
-
// a plain directory under `org/` would report — or be pulled into —
|
|
560
|
-
// whatever repository happens to sit above the workspace root. git
|
|
561
|
-
// ignores a relative ceiling entry, hence the resolve().
|
|
562
|
-
...cwd === void 0 ? {} : { GIT_CEILING_DIRECTORIES: dirname3(resolve2(cwd)) }
|
|
563
|
-
},
|
|
564
|
-
maxBuffer: 16 * 1024 * 1024
|
|
565
|
-
});
|
|
566
|
-
return stdout;
|
|
567
|
-
} catch (e) {
|
|
568
|
-
const err = e;
|
|
569
|
-
const text = err.stderr !== void 0 && err.stderr !== "" ? err.stderr : err.message ?? "unknown error";
|
|
570
|
-
const first = text.split("\n").find((l) => l.trim() !== "") ?? "unknown error";
|
|
571
|
-
throw new Error(`git ${args[0] ?? ""} failed: ${first.trim()}`);
|
|
572
|
-
}
|
|
573
|
-
}
|
|
574
|
-
function positional(value, what) {
|
|
575
|
-
if (value.startsWith("-")) throw new Error(`refusing suspicious ${what}: ${value}`);
|
|
576
|
-
return value;
|
|
577
|
-
}
|
|
578
|
-
async function clone(url, dir) {
|
|
579
|
-
await git(["clone", "--quiet", "--", positional(url, "repository url"), positional(dir, "directory")]);
|
|
580
|
-
}
|
|
581
|
-
async function isClean(dir, ignore = []) {
|
|
582
|
-
const pathspec = ignore.map((p) => `:!${p}`);
|
|
583
|
-
return (await git(["status", "--porcelain", ...pathspec.length > 0 ? ["--", ...pathspec] : []], dir)).trim() === "";
|
|
584
|
-
}
|
|
585
|
-
async function pullFastForward(dir) {
|
|
586
|
-
await git(["pull", "--ff-only", "--quiet"], dir);
|
|
587
|
-
}
|
|
588
|
-
|
|
589
|
-
// src/commands/sync.ts
|
|
590
|
-
var MANAGED_FILES = ["AGENTS.md", "CLAUDE.md"];
|
|
591
|
-
function targetsOf(manifest, root, only) {
|
|
592
|
-
const targets = [];
|
|
593
|
-
if (only === void 0) targets.push({ scope: null, dir: root, depth: 0, label: "AGENTS.md" });
|
|
594
|
-
for (const [name, entry] of Object.entries(manifest.scopes)) {
|
|
595
|
-
if (only !== void 0 && name !== only) continue;
|
|
596
|
-
const segments = entry.path.split("/");
|
|
597
|
-
targets.push({ scope: name, dir: join9(root, ...segments), depth: segments.length, label: `${entry.path}/AGENTS.md` });
|
|
598
|
-
}
|
|
599
|
-
return targets;
|
|
600
|
-
}
|
|
601
|
-
async function confirmOrExit(org) {
|
|
602
|
-
if (!process.stdin.isTTY || !process.stdout.isTTY) {
|
|
603
|
-
process.stderr.write("rness sync writes files; pass --yes to run without a prompt\n");
|
|
604
|
-
return 2;
|
|
605
|
-
}
|
|
606
|
-
const { confirm, isCancel } = await import("@clack/prompts");
|
|
607
|
-
const answer = await confirm({ message: `Sync workspace \`${org}\`: clone missing repositories and rewrite the rness blocks?` });
|
|
608
|
-
if (isCancel(answer) || answer !== true) {
|
|
609
|
-
process.stderr.write("cancelled\n");
|
|
610
|
-
return 1;
|
|
611
|
-
}
|
|
612
|
-
return null;
|
|
613
|
-
}
|
|
614
|
-
async function syncCommand(opts) {
|
|
615
|
-
const cwd = opts.cwd ?? process.cwd();
|
|
616
|
-
const check = opts.check === true;
|
|
617
|
-
try {
|
|
618
|
-
const ws = await findWorkspace(cwd);
|
|
619
|
-
const manifest = await loadManifest(ws.rnessDir);
|
|
620
|
-
const { org, warning } = resolveOrg(manifest, ws.root);
|
|
621
|
-
if (warning !== null) process.stderr.write(`warning: ${warning}
|
|
622
|
-
`);
|
|
623
|
-
if (opts.scope !== void 0 && !Object.hasOwn(manifest.scopes, opts.scope)) {
|
|
624
|
-
process.stderr.write(`unknown scope: ${opts.scope}
|
|
625
|
-
`);
|
|
626
|
-
return 1;
|
|
627
|
-
}
|
|
628
|
-
if (!check && opts.yes !== true) {
|
|
629
|
-
const refused = await confirmOrExit(org);
|
|
630
|
-
if (refused !== null) return refused;
|
|
631
|
-
}
|
|
632
|
-
const lines = [];
|
|
633
|
-
const problems = [];
|
|
634
|
-
try {
|
|
635
|
-
for (const [name, repo] of Object.entries(manifest.repos)) {
|
|
636
|
-
const dir = join9(ws.root, "org", name);
|
|
637
|
-
const label = `org/${name}`;
|
|
638
|
-
if (!await exists(dir)) {
|
|
639
|
-
if (check) {
|
|
640
|
-
lines.push(`missing ${label} (would clone ${repo.url})`);
|
|
641
|
-
continue;
|
|
642
|
-
}
|
|
643
|
-
try {
|
|
644
|
-
await clone(repo.url, dir);
|
|
645
|
-
lines.push(`cloned ${label}`);
|
|
646
|
-
} catch (e) {
|
|
647
|
-
problems.push(`${label}: ${e instanceof Error ? e.message : String(e)}`);
|
|
648
|
-
}
|
|
649
|
-
} else if (opts.pull === true && !check) {
|
|
650
|
-
try {
|
|
651
|
-
if (!await isClean(dir, MANAGED_FILES)) {
|
|
652
|
-
lines.push(`skipped ${label} (working tree not clean)`);
|
|
653
|
-
continue;
|
|
654
|
-
}
|
|
655
|
-
await pullFastForward(dir);
|
|
656
|
-
lines.push(`pulled ${label}`);
|
|
657
|
-
} catch (e) {
|
|
658
|
-
problems.push(`${label}: ${e instanceof Error ? e.message : String(e)}`);
|
|
659
|
-
}
|
|
660
|
-
}
|
|
661
|
-
}
|
|
662
|
-
if (!await exists(join9(ws.root, "org"))) {
|
|
663
|
-
lines.push("skipped blocks (no org/ directory here)");
|
|
664
|
-
} else {
|
|
665
|
-
let differences = 0;
|
|
666
|
-
for (const t of targetsOf(manifest, ws.root, opts.scope)) {
|
|
667
|
-
if (!await exists(t.dir)) {
|
|
668
|
-
lines.push(`skipped ${t.label} (directory not present)`);
|
|
669
|
-
continue;
|
|
670
|
-
}
|
|
671
|
-
const linked = await Promise.all(MANAGED_FILES.map((f) => isSymlink(join9(t.dir, f))));
|
|
672
|
-
if (linked.includes(true)) {
|
|
673
|
-
lines.push(`skipped ${t.label} (symlink)`);
|
|
674
|
-
continue;
|
|
675
|
-
}
|
|
676
|
-
const context = await assembleContext({ rnessDir: ws.rnessDir, manifest, scope: t.scope });
|
|
677
|
-
const block = renderBlock({ scope: t.scope, org, depth: t.depth, context, version: VERSION });
|
|
678
|
-
if (block.bytes > BLOCK_SIZE_WARNING) {
|
|
679
|
-
process.stderr.write(`warning: ${t.label}: block is ${Math.round(block.bytes / 1024)} KB (over 32 KB)
|
|
680
|
-
`);
|
|
681
|
-
}
|
|
682
|
-
const file = join9(t.dir, "AGENTS.md");
|
|
683
|
-
const merged = mergeBlock(await readOrNull(file), block.text);
|
|
684
|
-
if (!merged.ok) {
|
|
685
|
-
problems.push(`${t.label}: ${merged.error}`);
|
|
686
|
-
continue;
|
|
687
|
-
}
|
|
688
|
-
if (!merged.changed) {
|
|
689
|
-
lines.push(`unchanged ${t.label}`);
|
|
690
|
-
if (!check) await ensureClaudeMd(t.dir);
|
|
691
|
-
continue;
|
|
692
|
-
}
|
|
693
|
-
if (check) {
|
|
694
|
-
lines.push(`stale ${t.label}`);
|
|
695
|
-
differences += 1;
|
|
696
|
-
continue;
|
|
697
|
-
}
|
|
698
|
-
await writeFileAtomic(file, merged.text);
|
|
699
|
-
await ensureClaudeMd(t.dir);
|
|
700
|
-
lines.push(`updated ${t.label}`);
|
|
701
|
-
}
|
|
702
|
-
if (check && differences > 0) problems.push(`${differences} block(s) out of date \u2014 run rness sync`);
|
|
703
|
-
}
|
|
704
|
-
return problems.length > 0 ? 1 : 0;
|
|
705
|
-
} finally {
|
|
706
|
-
for (const l of lines) process.stdout.write(`${l}
|
|
707
|
-
`);
|
|
708
|
-
for (const p of problems) process.stderr.write(`${p}
|
|
709
|
-
`);
|
|
710
|
-
}
|
|
711
|
-
} catch (e) {
|
|
712
|
-
return reportError(e);
|
|
713
|
-
}
|
|
714
|
-
}
|
|
715
|
-
|
|
716
|
-
// src/cli.ts
|
|
717
|
-
import { Command, CommanderError, Option } from "commander";
|
|
718
|
-
function buildProgram(state) {
|
|
719
|
-
const program = new Command().name("rness").description("Configuration-plane CLI for rness workspaces").version(VERSION, "-v, --version", "print the version").exitOverride().showHelpAfterError().configureOutput({
|
|
720
|
-
writeOut: (s) => {
|
|
721
|
-
process.stdout.write(s);
|
|
722
|
-
},
|
|
723
|
-
writeErr: (s) => {
|
|
724
|
-
process.stderr.write(s);
|
|
725
|
-
}
|
|
726
|
-
});
|
|
727
|
-
program.command("context").description("Resolve and print the context for a scope").option("--scope <name>", "scope to resolve (default: the scope owning the current directory)").option("--json", "print JSON instead of Markdown").addOption(new Option("--cwd <dir>").hideHelp()).action(async (opts) => {
|
|
728
|
-
state.code = await contextCommand(opts);
|
|
729
|
-
});
|
|
730
|
-
program.command("validate").description("Check the .rness/ tree against the contract and every generated block").addOption(new Option("--cwd <dir>").hideHelp()).action(async (opts) => {
|
|
731
|
-
state.code = await validateCommand(opts);
|
|
732
|
-
});
|
|
733
|
-
program.command("sync").description("Clone missing repositories and write the rness block into every AGENTS.md").option("--scope <name>", "only this scope (the root block is skipped)").option("--check", "render and compare only; exit 1 when a block is out of date").option("--pull", "git pull --ff-only in every clean clone").option("-y, --yes", "do not ask for confirmation").addOption(new Option("--cwd <dir>").hideHelp()).action(async (opts) => {
|
|
734
|
-
state.code = await syncCommand(opts);
|
|
735
|
-
});
|
|
736
|
-
return program;
|
|
737
|
-
}
|
|
738
|
-
async function run(argv) {
|
|
739
|
-
const state = { code: 0 };
|
|
740
|
-
const program = buildProgram(state);
|
|
741
|
-
if (argv.length === 0) {
|
|
742
|
-
process.stdout.write(program.helpInformation());
|
|
743
|
-
return 0;
|
|
744
|
-
}
|
|
745
|
-
try {
|
|
746
|
-
await program.parseAsync(argv, { from: "user" });
|
|
747
|
-
} catch (e) {
|
|
748
|
-
if (e instanceof CommanderError) {
|
|
749
|
-
return e.exitCode === 0 ? 0 : 2;
|
|
750
|
-
}
|
|
751
|
-
throw e;
|
|
752
|
-
}
|
|
753
|
-
return state.code;
|
|
754
|
-
}
|
|
755
|
-
|
|
756
|
-
export {
|
|
757
|
-
VERSION,
|
|
758
|
-
exists,
|
|
759
|
-
readOrNull,
|
|
760
|
-
writeFileAtomic,
|
|
761
|
-
findWorkspace,
|
|
762
|
-
NAME,
|
|
763
|
-
loadManifest,
|
|
764
|
-
resolveScope,
|
|
765
|
-
scopeChain,
|
|
766
|
-
parseFrontMatter,
|
|
767
|
-
extractTitle,
|
|
768
|
-
collectMarkdown,
|
|
769
|
-
COLLECTIONS,
|
|
770
|
-
assembleContext,
|
|
771
|
-
reportError,
|
|
772
|
-
renderMarkdown,
|
|
773
|
-
contextCommand,
|
|
774
|
-
STATUSES,
|
|
775
|
-
checkContract,
|
|
776
|
-
BEGIN,
|
|
777
|
-
END,
|
|
778
|
-
BLOCK_SIZE_WARNING,
|
|
779
|
-
blockHash,
|
|
780
|
-
renderBlock,
|
|
781
|
-
parseHeader,
|
|
782
|
-
bodyOf,
|
|
783
|
-
findBlock,
|
|
784
|
-
mergeBlock,
|
|
785
|
-
ensureClaudeMd,
|
|
786
|
-
checkBlocks,
|
|
787
|
-
validateCommand,
|
|
788
|
-
syncCommand,
|
|
789
|
-
run
|
|
790
|
-
};
|
|
791
|
-
//# sourceMappingURL=chunk-4KNZT4G4.js.map
|