@rness/cli 0.4.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.
@@ -1,1740 +0,0 @@
1
- // src/core/fs.ts
2
- import {
3
- access,
4
- lstat,
5
- readFile,
6
- rename,
7
- rm,
8
- writeFile
9
- } from "fs/promises";
10
- import { basename, dirname, join } from "path";
11
- async function exists(p) {
12
- try {
13
- await access(p);
14
- return true;
15
- } catch {
16
- return false;
17
- }
18
- }
19
- async function isSymlink(p) {
20
- try {
21
- return (await lstat(p)).isSymbolicLink();
22
- } catch (e) {
23
- if (e.code === "ENOENT") return false;
24
- throw e;
25
- }
26
- }
27
- async function readOrNull(p) {
28
- try {
29
- return await readFile(p, "utf8");
30
- } catch (e) {
31
- if (e.code === "ENOENT") return null;
32
- throw e;
33
- }
34
- }
35
- var sequence = 0;
36
- async function writeFileAtomic(p, text) {
37
- sequence += 1;
38
- const tmp = join(dirname(p), `.${basename(p)}.${process.pid}.${sequence}.tmp`);
39
- await writeFile(tmp, text, "utf8");
40
- try {
41
- await rename(tmp, p);
42
- } catch (e) {
43
- await rm(tmp, { force: true }).catch(() => void 0);
44
- throw e;
45
- }
46
- }
47
-
48
- // src/core/remote.ts
49
- import { spawn } from "child_process";
50
-
51
- // src/core/git.ts
52
- import { execFile } from "child_process";
53
- import { dirname as dirname2, resolve } from "path";
54
- import { promisify } from "util";
55
- var execFileP = promisify(execFile);
56
- async function git(args, cwd) {
57
- try {
58
- const { stdout } = await execFileP("git", [...args], {
59
- ...cwd === void 0 ? {} : { cwd },
60
- env: {
61
- ...process.env,
62
- GIT_TERMINAL_PROMPT: "0",
63
- // Pin the command to `cwd` itself: without a ceiling, git walks up and
64
- // a plain directory under `org/` would report — or be pulled into —
65
- // whatever repository happens to sit above the workspace root. git
66
- // ignores a relative ceiling entry, hence the resolve().
67
- ...cwd === void 0 ? {} : { GIT_CEILING_DIRECTORIES: dirname2(resolve(cwd)) }
68
- },
69
- maxBuffer: 16 * 1024 * 1024
70
- });
71
- return stdout;
72
- } catch (e) {
73
- const err = e;
74
- const text = err.stderr !== void 0 && err.stderr !== "" ? err.stderr : err.message ?? "unknown error";
75
- const first = text.split("\n").find((l) => l.trim() !== "") ?? "unknown error";
76
- throw new Error(`git ${args[0] ?? ""} failed: ${first.trim()}`, {
77
- cause: e
78
- });
79
- }
80
- }
81
- function positional(value, what) {
82
- if (value.startsWith("-"))
83
- throw new Error(`refusing suspicious ${what}: ${value}`);
84
- return value;
85
- }
86
- async function clone(url, dir) {
87
- await git([
88
- "clone",
89
- "--quiet",
90
- "--",
91
- positional(url, "repository url"),
92
- positional(dir, "directory")
93
- ]);
94
- }
95
- async function isClean(dir, ignore = []) {
96
- const pathspec = ignore.map((p) => `:!${p}`);
97
- return (await git(
98
- [
99
- "status",
100
- "--porcelain",
101
- ...pathspec.length > 0 ? ["--", ...pathspec] : []
102
- ],
103
- dir
104
- )).trim() === "";
105
- }
106
- async function pullFastForward(dir) {
107
- await git(["pull", "--ff-only", "--quiet"], dir);
108
- }
109
- async function originUrl(dir) {
110
- try {
111
- return (await git(["remote", "get-url", "origin"], dir)).trim();
112
- } catch {
113
- return null;
114
- }
115
- }
116
- async function init(dir) {
117
- await git(["init", "-q", "-b", "main"], dir);
118
- }
119
- async function commitAll(dir, message) {
120
- await git(["add", "-A"], dir);
121
- await git(["commit", "-q", "-m", message], dir);
122
- }
123
-
124
- // src/core/remote.ts
125
- var DEFAULT_HOST = "https://github.com/";
126
- var SSH_HOST = "git@github.com:";
127
- function repoUrl(host, org, repo) {
128
- return `${host}${org}/${repo}.git`;
129
- }
130
- var AUTH = [
131
- /authentication failed/i,
132
- /permission denied/i,
133
- /could not read username/i,
134
- /terminal prompts disabled/i
135
- ];
136
- var NOT_FOUND = [
137
- /repository .*not found/i,
138
- /does not appear to be a git repository/i
139
- ];
140
- function firstLine(text) {
141
- return text.split("\n").find((l) => l.trim() !== "")?.trim() ?? "unknown error";
142
- }
143
- function classifyProbe(url, exitCode, stderr) {
144
- if (exitCode === 0 || exitCode === 2) return { kind: "found" };
145
- if (AUTH.some((r) => r.test(stderr)))
146
- return {
147
- kind: "error",
148
- message: `cannot access ${url}: ${firstLine(stderr)}`
149
- };
150
- if (NOT_FOUND.some((r) => r.test(stderr))) return { kind: "not-found" };
151
- return { kind: "error", message: `cannot reach ${url}: ${firstLine(stderr)}` };
152
- }
153
- var STDERR_FLUSH_MS = 200;
154
- async function probeRemote(url, timeoutMs = 6e4) {
155
- positional(url, "repository url");
156
- return new Promise((resolve4) => {
157
- const child = spawn("git", ["ls-remote", "--exit-code", url, "HEAD"], {
158
- stdio: ["ignore", "ignore", "pipe"],
159
- env: { ...process.env, GIT_TERMINAL_PROMPT: "0" }
160
- });
161
- const chunks = [];
162
- child.stderr?.setEncoding("utf8");
163
- child.stderr?.on("data", (chunk) => chunks.push(chunk));
164
- let settled = false;
165
- const settle = (probe) => {
166
- if (settled) return;
167
- settled = true;
168
- clearTimeout(timer);
169
- child.stderr?.destroy();
170
- resolve4(probe);
171
- };
172
- const timer = setTimeout(() => {
173
- child.kill("SIGKILL");
174
- settle({
175
- kind: "error",
176
- message: `cannot reach ${url}: timed out after ${Math.round(timeoutMs / 1e3)}s`
177
- });
178
- }, timeoutMs);
179
- child.on("error", (e) => settle(classifyProbe(url, 1, e.message)));
180
- child.on("exit", (code) => {
181
- const finish = () => settle(classifyProbe(url, code ?? 1, chunks.join("")));
182
- const stderr = child.stderr;
183
- if (stderr === null || stderr.readableEnded || stderr.destroyed)
184
- return finish();
185
- const flush = setTimeout(finish, STDERR_FLUSH_MS);
186
- const done = () => {
187
- clearTimeout(flush);
188
- finish();
189
- };
190
- stderr.once("end", done);
191
- stderr.once("error", done);
192
- });
193
- });
194
- }
195
-
196
- // src/core/manifest.ts
197
- import { readFile as readFile2 } from "fs/promises";
198
- import { basename as basename2, join as join2 } from "path";
199
- var NAME = /^[a-z0-9][a-z0-9-]*$/;
200
- var KEYS = ["contract", "org", "repos", "scopes"];
201
- function fail(message) {
202
- throw new Error(`rness.json: ${message}`);
203
- }
204
- function isRecord(value) {
205
- return value !== null && typeof value === "object" && !Array.isArray(value);
206
- }
207
- function checkPath(scope, path) {
208
- if (typeof path !== "string" || path === "")
209
- fail(`scope "${scope}" needs a string "path"`);
210
- if (path.startsWith("/")) fail(`scope "${scope}" path must be relative`);
211
- if (path.split("/").includes(".."))
212
- fail(`scope "${scope}" path must not contain ".."`);
213
- if (path.split("/").some((segment) => segment === ""))
214
- fail(`scope "${scope}" path must not contain empty segments`);
215
- return path;
216
- }
217
- function readOrg(value) {
218
- if (value === void 0 || value === null) return null;
219
- if (typeof value !== "string" || !NAME.test(value)) {
220
- fail(
221
- `"org" must be a name matching [a-z0-9-] (got ${JSON.stringify(value)})`
222
- );
223
- }
224
- return value;
225
- }
226
- function resolveOrg(manifest, root) {
227
- if (manifest.org !== null) return { org: manifest.org, warning: null };
228
- const name = basename2(root);
229
- return {
230
- org: name,
231
- warning: `rness.json: no "org"; using the directory name "${name}"`
232
- };
233
- }
234
- function readRepos(value) {
235
- const raw = value ?? {};
236
- if (!isRecord(raw)) fail('"repos" must be an object');
237
- const repos = {};
238
- for (const [name, entry] of Object.entries(raw)) {
239
- if (!NAME.test(name)) fail(`repo name "${name}" is not [a-z0-9-]`);
240
- if (!isRecord(entry) || typeof entry.url !== "string")
241
- fail(`repo "${name}" needs a string "url"`);
242
- repos[name] = { url: entry.url };
243
- }
244
- return repos;
245
- }
246
- function readScopes(value) {
247
- const raw = value ?? {};
248
- if (!isRecord(raw)) fail('"scopes" must be an object');
249
- const scopes = {};
250
- for (const [name, entry] of Object.entries(raw)) {
251
- if (!NAME.test(name)) fail(`scope name "${name}" is not [a-z0-9-]`);
252
- if (!isRecord(entry)) fail(`scope "${name}" must be an object`);
253
- const path = checkPath(name, entry.path);
254
- const ext = entry.extends ?? [];
255
- if (!Array.isArray(ext) || !ext.every((t) => typeof t === "string")) {
256
- fail(`scope "${name}" extends must be an array of scope names`);
257
- }
258
- scopes[name] = { path, extends: ext };
259
- }
260
- for (const [name, entry] of Object.entries(scopes)) {
261
- for (const target of entry.extends) {
262
- if (!Object.hasOwn(scopes, target))
263
- fail(`scope "${name}" extends unknown scope "${target}"`);
264
- }
265
- }
266
- return scopes;
267
- }
268
- async function loadManifest(rnessDir) {
269
- let raw;
270
- try {
271
- raw = await readFile2(join2(rnessDir, "rness.json"), "utf8");
272
- } catch {
273
- fail("not found");
274
- }
275
- let data;
276
- try {
277
- data = JSON.parse(raw);
278
- } catch (e) {
279
- fail(`invalid JSON (${e instanceof Error ? e.message : String(e)})`);
280
- }
281
- if (!isRecord(data)) fail("must be a JSON object");
282
- if (data.contract !== 1)
283
- fail(`unsupported contract: ${JSON.stringify(data.contract)} (expected 1)`);
284
- for (const key of Object.keys(data)) {
285
- if (!KEYS.includes(key)) fail(`unknown key ${JSON.stringify(key)}`);
286
- }
287
- return {
288
- contract: 1,
289
- org: readOrg(data.org),
290
- repos: readRepos(data.repos),
291
- scopes: readScopes(data.scopes)
292
- };
293
- }
294
- async function writeManifest(rnessDir, manifest) {
295
- const repos = Object.entries(manifest.repos).map(
296
- ([name, r]) => ` ${JSON.stringify(name)}: { "url": ${JSON.stringify(r.url)} }`
297
- );
298
- const scopes = Object.entries(manifest.scopes).map(([name, s]) => {
299
- const ext = s.extends.length === 0 ? "" : `, "extends": [${s.extends.map((e) => JSON.stringify(e)).join(", ")}]`;
300
- return ` ${JSON.stringify(name)}: { "path": ${JSON.stringify(s.path)}${ext} }`;
301
- });
302
- const lines = ["{", ' "contract": 1,'];
303
- if (manifest.org !== null)
304
- lines.push(` "org": ${JSON.stringify(manifest.org)},`);
305
- lines.push(
306
- ' "repos": {',
307
- repos.join(",\n"),
308
- " },",
309
- ' "scopes": {',
310
- scopes.join(",\n"),
311
- " }",
312
- "}"
313
- );
314
- const text = `${lines.filter((l) => l !== "").join("\n")}
315
- `;
316
- await writeFileAtomic(join2(rnessDir, "rness.json"), text);
317
- }
318
- var FULL_URL = /^(https?:\/\/|git@|ssh:\/\/|file:\/\/)/;
319
- function parseRepoSpec(spec, org, host) {
320
- let url;
321
- if (FULL_URL.test(spec)) {
322
- url = spec;
323
- } else {
324
- const parts = spec.split("/");
325
- if (parts.length === 1) url = repoUrl(host, org, spec);
326
- else if (parts.length === 2 && parts[0] !== void 0 && parts[1] !== void 0)
327
- url = repoUrl(host, parts[0], parts[1]);
328
- else
329
- throw new Error(
330
- `repository name "${spec}" is not [a-z0-9-] (use <repo>, <owner>/<repo>, or a URL)`
331
- );
332
- }
333
- const last = url.replace(/\/+$/, "").split(/[/:]/).at(-1) ?? "";
334
- const name = last.endsWith(".git") ? last.slice(0, -4) : last;
335
- if (!NAME.test(name))
336
- throw new Error(`repository name "${name}" is not [a-z0-9-]`);
337
- return { name, url };
338
- }
339
-
340
- // src/core/repos.ts
341
- import { glob, readFile as readFile3, rm as rm2 } from "fs/promises";
342
- import { join as join3, posix, sep } from "path";
343
- async function addRepository(input) {
344
- const { name, url } = parseRepoSpec(input.spec, input.org, input.host);
345
- const dir = join3(input.root, "org", name);
346
- const repoPath = `org/${name}`;
347
- const current = input.manifest.scopes[name];
348
- if (current !== void 0 && current.path !== repoPath)
349
- throw new Error(`scope "${name}" already points at ${current.path}`);
350
- const scopes = {
351
- ...input.manifest.scopes,
352
- [name]: { path: repoPath, extends: current?.extends ?? [] }
353
- };
354
- const subs = [];
355
- for (const sub of input.scopes) {
356
- const clean = sub.replace(/^\/+|\/+$/g, "");
357
- const segments = clean.split("/");
358
- if (segments.some((s) => s === "" || s === "." || s === ".."))
359
- throw new Error(
360
- `sub-scope "${clean}" must be a relative path inside the repository`
361
- );
362
- const base = segments.at(-1) ?? "";
363
- const path = `org/${name}/${clean}`;
364
- if (!NAME.test(base))
365
- throw new Error(`sub-scope "${clean}" needs a [a-z0-9-] last segment`);
366
- const existing = scopes[base];
367
- if (existing !== void 0 && existing.path !== path)
368
- throw new Error(`scope "${base}" already points at ${existing.path}`);
369
- scopes[base] = { path, extends: [name] };
370
- subs.push({ path, base });
371
- }
372
- let action;
373
- if (await exists(dir)) {
374
- const origin = await originUrl(dir);
375
- if (origin === null)
376
- throw new Error(`org/${name} exists and is not a git clone`);
377
- if (origin !== url)
378
- throw new Error(`org/${name} is a clone of ${origin}, not ${url}`);
379
- action = "adopted";
380
- } else {
381
- await clone(url, dir);
382
- action = "cloned";
383
- }
384
- const declared = [name];
385
- for (const { path, base } of subs) {
386
- if (!await exists(join3(input.root, ...path.split("/")))) {
387
- if (action === "cloned") await rm2(dir, { recursive: true, force: true });
388
- throw new Error(`${path} does not exist`);
389
- }
390
- declared.push(base);
391
- }
392
- const manifest = {
393
- ...input.manifest,
394
- repos: { ...input.manifest.repos, [name]: { url } },
395
- scopes
396
- };
397
- await writeManifest(input.rnessDir, manifest);
398
- return { manifest, name, url, action, declared };
399
- }
400
- async function workspaceDirs(repoDir) {
401
- let patterns = [];
402
- try {
403
- const pkg = JSON.parse(
404
- await readFile3(join3(repoDir, "package.json"), "utf8")
405
- );
406
- const w = pkg.workspaces;
407
- if (Array.isArray(w))
408
- patterns = w.filter((p) => typeof p === "string");
409
- else if (w !== null && typeof w === "object" && Array.isArray(w.packages)) {
410
- patterns = w.packages.filter(
411
- (p) => typeof p === "string"
412
- );
413
- }
414
- } catch {
415
- return [];
416
- }
417
- const dirs = /* @__PURE__ */ new Set();
418
- for (const pattern of patterns) {
419
- for await (const match of glob(pattern, { cwd: repoDir })) {
420
- const rel = match.split(sep).join(posix.sep);
421
- if (await exists(join3(repoDir, ...rel.split(posix.sep), "package.json")))
422
- dirs.add(rel);
423
- }
424
- }
425
- return [...dirs].sort();
426
- }
427
-
428
- // src/core/workspace.ts
429
- import { dirname as dirname3, join as join4, parse, resolve as resolve2 } from "path";
430
- async function findWorkspace(startDir) {
431
- const start = resolve2(startDir);
432
- const { root: fsRoot } = parse(start);
433
- const ancestors = [];
434
- for (let dir = start; ; dir = dirname3(dir)) {
435
- ancestors.push(dir);
436
- if (dir === fsRoot) break;
437
- }
438
- for (const dir of ancestors) {
439
- if (await exists(join4(dir, ".rness", "rness.json")))
440
- return { root: dir, rnessDir: join4(dir, ".rness") };
441
- }
442
- for (const dir of ancestors) {
443
- if (await exists(join4(dir, "rness.json")))
444
- return { root: dirname3(dir), rnessDir: dir };
445
- }
446
- throw new Error(`no rness workspace found above ${startDir}`);
447
- }
448
-
449
- // src/report.ts
450
- function reportError(e) {
451
- const debug = process.env["RNESS_DEBUG"] === "1";
452
- if (debug && e instanceof Error && e.stack) {
453
- process.stderr.write(`${e.stack}
454
- `);
455
- } else {
456
- const message = e instanceof Error ? e.message : String(e);
457
- process.stderr.write(`${message}
458
- `);
459
- }
460
- return 1;
461
- }
462
-
463
- // src/core/block.ts
464
- import { createHash } from "crypto";
465
- var BEGIN = "<!-- BEGIN rness -->";
466
- var END = "<!-- END rness -->";
467
- var BLOCK_SIZE_WARNING = 32 * 1024;
468
- var HEADER = /^<!-- rness (\S+) · scope: (\S+) · contract: 1 · hash: ([0-9a-f]{12}) · generated: run `rness sync`, never edit inside this block -->$/;
469
- function toLf(s) {
470
- return s.replace(/\r\n?/g, "\n");
471
- }
472
- function blockHash(body) {
473
- return createHash("sha256").update(toLf(body), "utf8").digest("hex").slice(0, 12);
474
- }
475
- function intro(scope, org, depth) {
476
- if (scope === null) {
477
- return [
478
- `This is the root of rness workspace \`${org}\`. Full context lives in \`.rness/\` \u2014`,
479
- "read `STATUS.md`, then task-relevant `adr/`, `specs/`, `plans/`; live: `rness context`.",
480
- "Every `org/<repo>/` carries its own block with the rules that apply there."
481
- ].join("\n");
482
- }
483
- const rel = `${"../".repeat(depth)}.rness/`;
484
- return [
485
- `This directory is scope \`${scope}\` of rness workspace \`${org}\`. Full context lives in`,
486
- `\`${rel}\` \u2014 read \`STATUS.md\`, then task-relevant \`adr/\`, \`specs/\`, \`plans/\`;`,
487
- `live: \`rness context --scope ${scope}\`. If \`.rness/\` is not reachable, this is a`,
488
- "standalone clone: the rules below are all you have."
489
- ].join("\n");
490
- }
491
- function header(version, scope, hash) {
492
- return `<!-- rness ${version} \xB7 scope: ${scope ?? "global"} \xB7 contract: 1 \xB7 hash: ${hash} \xB7 generated: run \`rness sync\`, never edit inside this block -->`;
493
- }
494
- function renderBlock({
495
- scope,
496
- org,
497
- depth,
498
- context,
499
- version
500
- }) {
501
- const standards = context.collections.find((c) => c.name === "standards")?.files ?? [];
502
- const rules = standards.length === 0 ? ["_No standards apply to this scope yet._"] : standards.map(
503
- (f) => `<!-- rness: standards/${f.rel} -->
504
- ${toLf(f.content).trim()}`
505
- );
506
- const body = `${intro(scope, org, depth)}
507
-
508
- ## Rules
509
- ${rules.join("\n\n")}`;
510
- const hash = blockHash(body);
511
- const text = [BEGIN, header(version, scope, hash), body, END].join("\n");
512
- return { text, hash, bytes: Buffer.byteLength(text, "utf8") };
513
- }
514
- function parseHeader(line) {
515
- const m = HEADER.exec(line.trimEnd());
516
- if (m === null) return null;
517
- return { version: m[1] ?? "", scope: m[2] ?? "", hash: m[3] ?? "" };
518
- }
519
- function bodyOf(blockLines) {
520
- return blockLines.slice(2, -1).join("\n");
521
- }
522
-
523
- // src/core/frontmatter.ts
524
- import { parse as parse2 } from "yaml";
525
- var FRONT_MATTER = /^---\r?\n(?:([\s\S]*?)\r?\n)?---(?:\r?\n|$)/;
526
- function fail2(message) {
527
- throw new Error(`front matter: ${message}`);
528
- }
529
- function stripBom(source) {
530
- return source.startsWith("\uFEFF") ? source.slice(1) : source;
531
- }
532
- function parseFrontMatter(source) {
533
- const match = stripBom(source).match(FRONT_MATTER);
534
- if (match === null) return null;
535
- const block = match[1] ?? "";
536
- if (block.trim() === "") return {};
537
- let doc;
538
- try {
539
- doc = parse2(block, { logLevel: "error" });
540
- } catch (e) {
541
- fail2(
542
- e instanceof Error ? (e.message.split("\n")[0] ?? "invalid YAML").replace(/:$/, "") : "invalid YAML"
543
- );
544
- }
545
- if (doc === null || doc === void 0) return {};
546
- if (typeof doc !== "object" || Array.isArray(doc))
547
- fail2("block must be a mapping of keys to values");
548
- return doc;
549
- }
550
- function stripFrontMatter(source) {
551
- const text = stripBom(source);
552
- const match = text.match(FRONT_MATTER);
553
- if (match === null) return text;
554
- return text.slice(match[0].length).replace(/^(?:[ \t]*\r?\n)+/, "");
555
- }
556
- function extractTitle(source) {
557
- return stripBom(source).match(/^# (.+)$/m)?.[1]?.trim() ?? null;
558
- }
559
-
560
- // src/core/collect.ts
561
- import { glob as glob2, readFile as readFile4 } from "fs/promises";
562
- import { join as join5, posix as posix2, sep as sep2 } from "path";
563
- function toPosix(rel) {
564
- return rel.split(sep2).join(posix2.sep);
565
- }
566
- async function collectMarkdown(dir) {
567
- const rels = [];
568
- for await (const rel of glob2("**/*.md", { cwd: dir })) rels.push(toPosix(rel));
569
- const items = await Promise.all(
570
- rels.map(async (rel) => {
571
- const path = join5(dir, ...rel.split(posix2.sep));
572
- const body = await readFile4(path, "utf8");
573
- let fields = null;
574
- let fieldsError = null;
575
- try {
576
- fields = parseFrontMatter(body);
577
- } catch (e) {
578
- fieldsError = e instanceof Error ? e.message : String(e);
579
- }
580
- return {
581
- path,
582
- rel,
583
- fields,
584
- fieldsError,
585
- title: extractTitle(body),
586
- body,
587
- content: stripFrontMatter(body)
588
- };
589
- })
590
- );
591
- return items.sort((a, b) => a.rel < b.rel ? -1 : a.rel > b.rel ? 1 : 0);
592
- }
593
-
594
- // src/core/scope.ts
595
- function isPrefix(prefix, target) {
596
- return prefix === target || target.startsWith(`${prefix}/`);
597
- }
598
- function resolveScope(manifest, relDir) {
599
- let best = null;
600
- let bestLen = -1;
601
- for (const [name, entry] of Object.entries(manifest.scopes)) {
602
- if (isPrefix(entry.path, relDir) && entry.path.length > bestLen) {
603
- best = name;
604
- bestLen = entry.path.length;
605
- }
606
- }
607
- return best;
608
- }
609
- function scopeChain(manifest, scope) {
610
- if (scope === null) return [];
611
- const chain = [];
612
- const seen = /* @__PURE__ */ new Set();
613
- const visit = (name, stack) => {
614
- if (stack.has(name)) throw new Error(`extends cycle at scope "${name}"`);
615
- if (seen.has(name)) return;
616
- seen.add(name);
617
- chain.push(name);
618
- stack.add(name);
619
- const entry = Object.hasOwn(manifest.scopes, name) ? manifest.scopes[name] : void 0;
620
- for (const target of entry?.extends ?? []) visit(target, stack);
621
- stack.delete(name);
622
- };
623
- visit(scope, /* @__PURE__ */ new Set());
624
- return chain;
625
- }
626
-
627
- // src/core/context.ts
628
- import { join as join6 } from "path";
629
- var COLLECTIONS = [
630
- "standards",
631
- "adr",
632
- "specs",
633
- "plans",
634
- "skills"
635
- ];
636
- function ownerScope(rel) {
637
- const slash = rel.indexOf("/");
638
- return slash === -1 ? null : rel.slice(0, slash);
639
- }
640
- async function assembleCollection(dir, name, chain) {
641
- const items = await collectMarkdown(dir);
642
- const groups = new Map(chain.map((s) => [s, []]));
643
- const rootFiles = [];
644
- for (const item of items) {
645
- const owner = ownerScope(item.rel);
646
- if (owner === null) rootFiles.push({ ...item, scope: null });
647
- else groups.get(owner)?.push({ ...item, scope: owner });
648
- }
649
- const files = [];
650
- const push = (f) => {
651
- files.push({
652
- rel: f.rel,
653
- scope: f.scope,
654
- title: f.title,
655
- body: f.body,
656
- content: f.content
657
- });
658
- };
659
- for (const s of chain) for (const f of groups.get(s) ?? []) push(f);
660
- for (const f of rootFiles) push(f);
661
- return { name, files };
662
- }
663
- async function assembleContext({
664
- rnessDir,
665
- manifest,
666
- scope
667
- }) {
668
- const chain = scopeChain(manifest, scope);
669
- const collections = await Promise.all(
670
- COLLECTIONS.map(
671
- (name) => assembleCollection(join6(rnessDir, name), name, chain)
672
- )
673
- );
674
- return { scope, collections };
675
- }
676
-
677
- // src/core/merge.ts
678
- import { join as join7 } from "path";
679
- function findBlock(lines) {
680
- const begins = [];
681
- const ends = [];
682
- lines.forEach((line, i) => {
683
- const l = line.trimEnd();
684
- if (l === BEGIN) begins.push(i);
685
- if (l === END) ends.push(i);
686
- });
687
- if (begins.length === 0 && ends.length === 0) return { kind: "none" };
688
- const begin = begins[0];
689
- const end = ends[0];
690
- if (begins.length !== 1 || ends.length !== 1 || begin === void 0 || end === void 0) {
691
- return {
692
- kind: "error",
693
- message: `expected exactly one ${BEGIN} \u2026 ${END} span, found ${begins.length} BEGIN and ${ends.length} END`
694
- };
695
- }
696
- if (end < begin)
697
- return { kind: "error", message: `${END} comes before ${BEGIN}` };
698
- return { kind: "one", begin, end };
699
- }
700
- function mergeBlock(existing, block) {
701
- if (existing === null) return { ok: true, text: `${block}
702
- `, changed: true };
703
- const eol = existing.includes("\r\n") ? "\r\n" : "\n";
704
- const lines = existing.split(/\r?\n/);
705
- const found = findBlock(lines);
706
- if (found.kind === "error") return { ok: false, error: found.message };
707
- const rendered = block.split("\n").join(eol);
708
- const starts = [0];
709
- for (const m of existing.matchAll(/\r?\n/g))
710
- starts.push((m.index ?? 0) + m[0].length);
711
- const lineStart = (i) => starts[i] ?? existing.length;
712
- let text;
713
- if (found.kind === "one") {
714
- const endLine = lines[found.end] ?? "";
715
- const afterEnd = lineStart(found.end) + endLine.length;
716
- text = `${existing.slice(0, lineStart(found.begin))}${rendered}${existing.slice(afterEnd)}`;
717
- } else if (lines.length >= 3 && (lines[0] ?? "").startsWith("# ") && lines[1] === "") {
718
- const at = lineStart(2);
719
- text = `${existing.slice(0, at)}${rendered}${eol}${eol}${existing.slice(at)}`;
720
- } else {
721
- text = `${rendered}${eol}${eol}${existing}`;
722
- }
723
- return { ok: true, text, changed: text !== existing };
724
- }
725
- async function ensureClaudeMd(dir) {
726
- const file = join7(dir, "CLAUDE.md");
727
- if (await isSymlink(file)) return "unchanged";
728
- const existing = await readOrNull(file);
729
- if (existing === null) {
730
- await writeFileAtomic(file, "@AGENTS.md\n");
731
- return "created";
732
- }
733
- if (existing.split(/\r?\n/).some((l) => l.trim() === "@AGENTS.md"))
734
- return "unchanged";
735
- const eol = existing.includes("\r\n") ? "\r\n" : "\n";
736
- await writeFileAtomic(file, `@AGENTS.md${eol}${existing}`);
737
- return "prepended";
738
- }
739
-
740
- // src/version.ts
741
- var VERSION = "0.4.0";
742
-
743
- // src/commands/sync.ts
744
- import { join as join8 } from "path";
745
- var MANAGED_FILES = ["AGENTS.md", "CLAUDE.md"];
746
- function targetsOf(manifest, root, only) {
747
- const targets = [];
748
- if (only === void 0)
749
- targets.push({ scope: null, dir: root, depth: 0, label: "AGENTS.md" });
750
- for (const [name, entry] of Object.entries(manifest.scopes)) {
751
- if (only !== void 0 && name !== only) continue;
752
- const segments = entry.path.split("/");
753
- targets.push({
754
- scope: name,
755
- dir: join8(root, ...segments),
756
- depth: segments.length,
757
- label: `${entry.path}/AGENTS.md`
758
- });
759
- }
760
- return targets;
761
- }
762
- async function confirmOrExit(org) {
763
- if (!process.stdin.isTTY || !process.stdout.isTTY) {
764
- process.stderr.write(
765
- "rness sync writes files; pass --yes to run without a prompt\n"
766
- );
767
- return 2;
768
- }
769
- const { confirm, isCancel } = await import("@clack/prompts");
770
- const answer = await confirm({
771
- message: `Sync workspace \`${org}\`: clone missing repositories and rewrite the rness blocks?`
772
- });
773
- if (isCancel(answer) || answer !== true) {
774
- process.stderr.write("cancelled\n");
775
- return 1;
776
- }
777
- return null;
778
- }
779
- async function syncCommand(opts) {
780
- const cwd = opts.cwd ?? process.cwd();
781
- const check = opts.check === true;
782
- try {
783
- const ws = await findWorkspace(cwd);
784
- const manifest = await loadManifest(ws.rnessDir);
785
- const { org, warning } = resolveOrg(manifest, ws.root);
786
- if (warning !== null) process.stderr.write(`warning: ${warning}
787
- `);
788
- if (opts.scope !== void 0 && !Object.hasOwn(manifest.scopes, opts.scope)) {
789
- process.stderr.write(`unknown scope: ${opts.scope}
790
- `);
791
- return 1;
792
- }
793
- if (!check && opts.yes !== true) {
794
- const refused = await confirmOrExit(org);
795
- if (refused !== null) return refused;
796
- }
797
- const lines = [];
798
- const problems = [];
799
- try {
800
- for (const [name, repo] of Object.entries(manifest.repos)) {
801
- const dir = join8(ws.root, "org", name);
802
- const label = `org/${name}`;
803
- if (!await exists(dir)) {
804
- if (check) {
805
- lines.push(`missing ${label} (would clone ${repo.url})`);
806
- continue;
807
- }
808
- try {
809
- await clone(repo.url, dir);
810
- lines.push(`cloned ${label}`);
811
- } catch (e) {
812
- problems.push(
813
- `${label}: ${e instanceof Error ? e.message : String(e)}`
814
- );
815
- }
816
- } else if (opts.pull === true && !check) {
817
- try {
818
- if (!await isClean(dir, MANAGED_FILES)) {
819
- lines.push(`skipped ${label} (working tree not clean)`);
820
- continue;
821
- }
822
- await pullFastForward(dir);
823
- lines.push(`pulled ${label}`);
824
- } catch (e) {
825
- problems.push(
826
- `${label}: ${e instanceof Error ? e.message : String(e)}`
827
- );
828
- }
829
- }
830
- }
831
- if (!await exists(join8(ws.root, "org"))) {
832
- lines.push("skipped blocks (no org/ directory here)");
833
- } else {
834
- let differences = 0;
835
- for (const t of targetsOf(manifest, ws.root, opts.scope)) {
836
- if (!await exists(t.dir)) {
837
- lines.push(`skipped ${t.label} (directory not present)`);
838
- continue;
839
- }
840
- const linked = await Promise.all(
841
- MANAGED_FILES.map((f) => isSymlink(join8(t.dir, f)))
842
- );
843
- if (linked.includes(true)) {
844
- lines.push(`skipped ${t.label} (symlink)`);
845
- continue;
846
- }
847
- const context = await assembleContext({
848
- rnessDir: ws.rnessDir,
849
- manifest,
850
- scope: t.scope
851
- });
852
- const block = renderBlock({
853
- scope: t.scope,
854
- org,
855
- depth: t.depth,
856
- context,
857
- version: VERSION
858
- });
859
- if (block.bytes > BLOCK_SIZE_WARNING) {
860
- process.stderr.write(
861
- `warning: ${t.label}: block is ${Math.round(block.bytes / 1024)} KB (over 32 KB)
862
- `
863
- );
864
- }
865
- const file = join8(t.dir, "AGENTS.md");
866
- const merged = mergeBlock(await readOrNull(file), block.text);
867
- if (!merged.ok) {
868
- problems.push(`${t.label}: ${merged.error}`);
869
- continue;
870
- }
871
- if (!merged.changed) {
872
- lines.push(`unchanged ${t.label}`);
873
- if (!check) await ensureClaudeMd(t.dir);
874
- continue;
875
- }
876
- if (check) {
877
- lines.push(`stale ${t.label}`);
878
- differences += 1;
879
- continue;
880
- }
881
- await writeFileAtomic(file, merged.text);
882
- await ensureClaudeMd(t.dir);
883
- lines.push(`updated ${t.label}`);
884
- }
885
- if (check && differences > 0)
886
- problems.push(`${differences} block(s) out of date \u2014 run rness sync`);
887
- }
888
- return problems.length > 0 ? 1 : 0;
889
- } finally {
890
- for (const l of lines) process.stdout.write(`${l}
891
- `);
892
- for (const p of problems) process.stderr.write(`${p}
893
- `);
894
- }
895
- } catch (e) {
896
- return reportError(e);
897
- }
898
- }
899
-
900
- // src/commands/add.ts
901
- import { join as join9 } from "path";
902
- function isTty() {
903
- return process.stdin.isTTY === true && process.stdout.isTTY === true;
904
- }
905
- function printDeclared(result, already) {
906
- for (const name of result.declared) {
907
- if (already.has(name)) continue;
908
- const entry = result.manifest.scopes[name];
909
- if (entry === void 0) continue;
910
- const ext = entry.extends.length === 0 ? "" : `, extends ${entry.extends.join(", ")}`;
911
- process.stdout.write(`declared scope ${name} (${entry.path}${ext})
912
- `);
913
- }
914
- }
915
- async function addCommand(spec, opts) {
916
- const cwd = opts.cwd ?? process.cwd();
917
- const host = opts.host ?? (opts.ssh === true ? SSH_HOST : DEFAULT_HOST);
918
- try {
919
- const ws = await findWorkspace(cwd);
920
- const manifest = await loadManifest(ws.rnessDir);
921
- const { org, warning } = resolveOrg(manifest, ws.root);
922
- if (warning !== null) process.stderr.write(`warning: ${warning}
923
- `);
924
- let parsed;
925
- try {
926
- parsed = parseRepoSpec(spec, org, host);
927
- } catch (e) {
928
- process.stderr.write(`${e instanceof Error ? e.message : String(e)}
929
- `);
930
- return 2;
931
- }
932
- const scopes = (opts.scopes ?? "").split(",").map((s) => s.trim()).filter((s) => s !== "");
933
- const dir = join9(ws.root, "org", parsed.name);
934
- const present = await exists(dir);
935
- if (opts.yes !== true) {
936
- if (!isTty()) {
937
- process.stderr.write(
938
- "rness add clones and writes files; pass --yes to run without a prompt\n"
939
- );
940
- return 2;
941
- }
942
- const { confirm, isCancel } = await import("@clack/prompts");
943
- const plan = present ? `adopt org/${parsed.name}` : `clone ${parsed.url} into org/${parsed.name}`;
944
- const ok = await confirm({
945
- message: `${plan}, declare scope "${parsed.name}" in rness.json, then sync?`
946
- });
947
- if (isCancel(ok) || ok !== true) {
948
- process.stderr.write("cancelled\n");
949
- return 1;
950
- }
951
- }
952
- let result = await addRepository({
953
- root: ws.root,
954
- rnessDir: ws.rnessDir,
955
- manifest,
956
- spec,
957
- org,
958
- host,
959
- scopes
960
- });
961
- process.stdout.write(
962
- `${result.action === "cloned" ? "cloned " : "adopted "} org/${result.name}
963
- `
964
- );
965
- const announced = /* @__PURE__ */ new Set();
966
- printDeclared(result, announced);
967
- for (const name of result.declared) announced.add(name);
968
- if (opts.yes !== true && scopes.length === 0) {
969
- const candidates = await workspaceDirs(dir);
970
- if (candidates.length > 0) {
971
- const { isCancel, multiselect } = await import("@clack/prompts");
972
- const picked = await multiselect({
973
- message: "Declare these workspace packages as scopes?",
974
- options: candidates.map((c) => ({ value: c, label: c })),
975
- required: false
976
- });
977
- if (isCancel(picked)) {
978
- process.stderr.write("cancelled\n");
979
- return 1;
980
- }
981
- if (Array.isArray(picked) && picked.length > 0) {
982
- result = await addRepository({
983
- root: ws.root,
984
- rnessDir: ws.rnessDir,
985
- manifest: result.manifest,
986
- spec,
987
- org,
988
- host,
989
- scopes: picked
990
- });
991
- printDeclared(result, announced);
992
- }
993
- }
994
- }
995
- return syncCommand({ yes: true, cwd: ws.root });
996
- } catch (e) {
997
- return reportError(e);
998
- }
999
- }
1000
-
1001
- // src/commands/context.ts
1002
- import { posix as posix3, relative, sep as sep3 } from "path";
1003
- function renderMarkdown(ctx) {
1004
- const parts = [];
1005
- for (const collection of ctx.collections) {
1006
- if (collection.files.length === 0) continue;
1007
- parts.push(`## ${collection.name}
1008
- `);
1009
- for (const file of collection.files) {
1010
- parts.push(
1011
- `<!-- rness: ${collection.name}/${file.rel} -->
1012
- ${file.content.trim()}
1013
- `
1014
- );
1015
- }
1016
- }
1017
- return `${parts.join("\n")}
1018
- `;
1019
- }
1020
- async function contextCommand(opts) {
1021
- const cwd = opts.cwd ?? process.cwd();
1022
- try {
1023
- const ws = await findWorkspace(cwd);
1024
- const manifest = await loadManifest(ws.rnessDir);
1025
- let scope;
1026
- if (opts.scope !== void 0) {
1027
- if (!Object.hasOwn(manifest.scopes, opts.scope)) {
1028
- process.stderr.write(`unknown scope: ${opts.scope}
1029
- `);
1030
- return 1;
1031
- }
1032
- scope = opts.scope;
1033
- } else {
1034
- const rel = relative(ws.root, cwd).split(sep3).join(posix3.sep);
1035
- scope = resolveScope(manifest, rel);
1036
- }
1037
- const ctx = await assembleContext({
1038
- rnessDir: ws.rnessDir,
1039
- manifest,
1040
- scope
1041
- });
1042
- process.stdout.write(
1043
- opts.json ? `${JSON.stringify(ctx, null, 2)}
1044
- ` : renderMarkdown(ctx)
1045
- );
1046
- return 0;
1047
- } catch (e) {
1048
- return reportError(e);
1049
- }
1050
- }
1051
-
1052
- // src/core/delegate.ts
1053
- import { readFile as readFile5 } from "fs/promises";
1054
- import { join as join10 } from "path";
1055
- var DEFAULT_BIN = "dist/bin/rness.js";
1056
- async function readPinnedCli(rnessDir) {
1057
- const dir = join10(rnessDir, "node_modules", "@rness", "cli");
1058
- let pkg;
1059
- try {
1060
- pkg = JSON.parse(await readFile5(join10(dir, "package.json"), "utf8"));
1061
- } catch {
1062
- return null;
1063
- }
1064
- const declared = pkg.bin;
1065
- let bin = DEFAULT_BIN;
1066
- if (typeof declared === "string") bin = declared;
1067
- else if (declared !== null && typeof declared === "object") {
1068
- const named = declared["rness"];
1069
- if (typeof named === "string") bin = named;
1070
- }
1071
- return {
1072
- dir,
1073
- bin,
1074
- version: typeof pkg.version === "string" ? pkg.version : null
1075
- };
1076
- }
1077
- async function findDelegate(cwd, ownVersion) {
1078
- let root;
1079
- let rnessDir;
1080
- try {
1081
- const ws = await findWorkspace(cwd);
1082
- root = ws.root;
1083
- rnessDir = ws.rnessDir;
1084
- } catch {
1085
- return null;
1086
- }
1087
- const pinned = await readPinnedCli(rnessDir);
1088
- if (pinned === null) return null;
1089
- const { version } = pinned;
1090
- if (version === null || version === ownVersion) return null;
1091
- return { entry: join10(pinned.dir, "dist", "index.js"), version, root };
1092
- }
1093
-
1094
- // src/core/pm.ts
1095
- import { execFile as execFile2 } from "child_process";
1096
- import { promisify as promisify2 } from "util";
1097
- var execFileP2 = promisify2(execFile2);
1098
- var PACKAGE_MANAGERS = [
1099
- "npm",
1100
- "pnpm",
1101
- "yarn",
1102
- "bun"
1103
- ];
1104
- function isPackageManager(value) {
1105
- return PACKAGE_MANAGERS.includes(value);
1106
- }
1107
- function detectPackageManager(userAgent = process.env["npm_config_user_agent"] ?? "") {
1108
- const name = userAgent.split("/")[0] ?? "";
1109
- return isPackageManager(name) ? name : "npm";
1110
- }
1111
- function firstLine2(text) {
1112
- return text.split("\n").find((l) => l.trim() !== "")?.trim() ?? "unknown error";
1113
- }
1114
- async function packageManagerVersion(pm) {
1115
- try {
1116
- const { stdout } = await execFileP2(pm, ["--version"]);
1117
- return stdout.trim();
1118
- } catch (e) {
1119
- throw new Error(`${pm} is not available on PATH`, { cause: e });
1120
- }
1121
- }
1122
- async function installDependencies(pm, dir) {
1123
- try {
1124
- await execFileP2(pm, ["install"], { cwd: dir, maxBuffer: 16 * 1024 * 1024 });
1125
- } catch (e) {
1126
- const err = e;
1127
- throw new Error(
1128
- `${pm} install failed in ${dir}: ${firstLine2(err.stderr ?? err.message)}`,
1129
- { cause: e }
1130
- );
1131
- }
1132
- }
1133
-
1134
- // src/core/scaffold.ts
1135
- import { statSync } from "fs";
1136
- import { dirname as dirname4, join as join11 } from "path";
1137
- import { fileURLToPath } from "url";
1138
- var SCAFFOLD_FILES = [
1139
- "AGENTS.md",
1140
- "CONVENTIONS.md",
1141
- "README.md",
1142
- "WORKSPACE.md",
1143
- "rness.json",
1144
- "package.json",
1145
- "_gitignore",
1146
- ".githooks/pre-commit",
1147
- ".github/workflows/validate.yml",
1148
- "pnpm-workspace.yaml",
1149
- "adr/0000-template.md",
1150
- "standards/.gitkeep",
1151
- "specs/.gitkeep",
1152
- "plans/.gitkeep",
1153
- "skills/.gitkeep",
1154
- "docs/.gitkeep"
1155
- ];
1156
- function scaffoldDir() {
1157
- let dir = dirname4(fileURLToPath(import.meta.url));
1158
- for (let i = 0; i < 4; i += 1) {
1159
- const candidate = join11(dir, "scaffold");
1160
- try {
1161
- if (statSync(join11(candidate, "rness.json")).isFile()) return candidate;
1162
- } catch {
1163
- }
1164
- dir = dirname4(dir);
1165
- }
1166
- throw new Error("scaffold directory not found next to the @rness/cli package");
1167
- }
1168
-
1169
- // src/core/scaffold-copy.ts
1170
- import { chmod, mkdir, readFile as readFile6, stat, writeFile as writeFile2 } from "fs/promises";
1171
- import { dirname as dirname5, join as join12 } from "path";
1172
- var TOKENISED = /* @__PURE__ */ new Set(["package.json", ".github/workflows/validate.yml"]);
1173
- async function copyScaffold(dest, tokens) {
1174
- const source = scaffoldDir();
1175
- for (const rel of SCAFFOLD_FILES) {
1176
- if (rel === "rness.json") continue;
1177
- const from = join12(source, ...rel.split("/"));
1178
- const targetRel = rel === "_gitignore" ? ".gitignore" : rel;
1179
- const to = join12(dest, ...targetRel.split("/"));
1180
- await mkdir(dirname5(to), { recursive: true });
1181
- let content = await readFile6(from, "utf8");
1182
- if (TOKENISED.has(rel)) {
1183
- content = content.replaceAll("__RNESS_VERSION__", tokens.version).replaceAll("__RNESS_PM__", tokens.packageManager);
1184
- }
1185
- await writeFile2(to, content, "utf8");
1186
- const mode = (await stat(from)).mode & 511;
1187
- if (mode & 73) await chmod(to, mode);
1188
- }
1189
- }
1190
-
1191
- // src/commands/create.ts
1192
- import { execFile as execFile3 } from "child_process";
1193
- import { mkdir as mkdir2, readdir, rm as rm3 } from "fs/promises";
1194
- import { dirname as dirname6, join as join13, relative as relative2, resolve as resolve3 } from "path";
1195
- import { promisify as promisify3 } from "util";
1196
- var execFileP3 = promisify3(execFile3);
1197
- function isTty2() {
1198
- return process.stdin.isTTY === true && process.stdout.isTTY === true;
1199
- }
1200
- async function isEmptyDir(dir) {
1201
- return (await readdir(dir)).length === 0;
1202
- }
1203
- async function ghAvailable() {
1204
- try {
1205
- await execFileP3("gh", ["--version"]);
1206
- return true;
1207
- } catch {
1208
- return false;
1209
- }
1210
- }
1211
- async function insideWorkspace(dir) {
1212
- try {
1213
- await findWorkspace(dir);
1214
- return true;
1215
- } catch {
1216
- return false;
1217
- }
1218
- }
1219
- function firstLine3(text) {
1220
- return text.split("\n").find((l) => l.trim() !== "")?.trim() ?? "unknown error";
1221
- }
1222
- function splitSpecs(list) {
1223
- return list.split(",").map((s) => s.trim()).filter((s) => s !== "");
1224
- }
1225
- async function addRepositories(input) {
1226
- let manifest = input.manifest;
1227
- const failures = [];
1228
- for (const spec of input.specs) {
1229
- try {
1230
- const result = await addRepository({
1231
- root: input.root,
1232
- rnessDir: input.rnessDir,
1233
- manifest,
1234
- spec,
1235
- org: input.org,
1236
- host: input.host,
1237
- scopes: []
1238
- });
1239
- manifest = result.manifest;
1240
- process.stdout.write(
1241
- `${result.action === "cloned" ? "cloned " : "adopted "} org/${result.name}
1242
- declared scope ${result.name} (org/${result.name})
1243
- `
1244
- );
1245
- } catch (e) {
1246
- const message = firstLine3(e instanceof Error ? e.message : String(e));
1247
- process.stderr.write(`${spec}: ${message}
1248
- `);
1249
- failures.push({ spec, message });
1250
- }
1251
- }
1252
- return failures;
1253
- }
1254
- async function syncPinned(root, shown) {
1255
- const rnessDir = join13(root, ".rness");
1256
- const installed = await readPinnedCli(rnessDir);
1257
- if (installed === null) return syncCommand({ yes: true, cwd: root });
1258
- const pinned = join13(installed.dir, ...installed.bin.split("/"));
1259
- if (!await exists(pinned)) {
1260
- process.stderr.write(
1261
- `@rness/cli in ${shown}/.rness is not installed correctly (missing ${installed.bin}); reinstall in .rness/
1262
- `
1263
- );
1264
- return 1;
1265
- }
1266
- try {
1267
- const { stdout, stderr } = await execFileP3(
1268
- process.execPath,
1269
- [pinned, "sync", "--yes"],
1270
- {
1271
- cwd: root,
1272
- maxBuffer: 16 * 1024 * 1024
1273
- }
1274
- );
1275
- process.stdout.write(stdout);
1276
- if (stderr !== "") process.stderr.write(stderr);
1277
- return 0;
1278
- } catch (e) {
1279
- const err = e;
1280
- if (err.stdout !== void 0 && err.stdout !== "")
1281
- process.stdout.write(err.stdout);
1282
- if (err.stderr !== void 0 && err.stderr !== "")
1283
- process.stderr.write(err.stderr);
1284
- return typeof err.code === "number" ? err.code : 1;
1285
- }
1286
- }
1287
- async function createCommand(org, opts) {
1288
- const cwd = opts.cwd ?? process.cwd();
1289
- if (opts.template !== void 0) {
1290
- process.stderr.write("--template is reserved for a later version\n");
1291
- return 2;
1292
- }
1293
- if (!NAME.test(org)) {
1294
- process.stderr.write(`organisation name "${org}" is not [a-z0-9-]
1295
- `);
1296
- return 2;
1297
- }
1298
- if (opts.pm !== void 0 && !isPackageManager(opts.pm)) {
1299
- process.stderr.write(`--pm must be one of ${PACKAGE_MANAGERS.join(", ")}
1300
- `);
1301
- return 2;
1302
- }
1303
- const pm = opts.pm !== void 0 && isPackageManager(opts.pm) ? opts.pm : detectPackageManager();
1304
- const host = opts.host ?? (opts.ssh === true ? SSH_HOST : DEFAULT_HOST);
1305
- try {
1306
- const root = resolve3(cwd, opts.dir ?? org);
1307
- const shown = relative2(cwd, root) || ".";
1308
- if (await insideWorkspace(cwd) || await insideWorkspace(dirname6(root))) {
1309
- process.stderr.write(
1310
- "already inside an rness workspace; create runs from outside one\n"
1311
- );
1312
- return 1;
1313
- }
1314
- if (await exists(root)) {
1315
- if (await exists(join13(root, ".rness", "rness.json"))) {
1316
- if (!await exists(join13(root, ".rness", "node_modules"))) {
1317
- process.stderr.write(
1318
- `${shown} holds an rness workspace whose dependencies are not installed; cd ${shown}/.rness && ${pm} install, then rness sync
1319
- `
1320
- );
1321
- return 1;
1322
- }
1323
- process.stderr.write(
1324
- `${shown} already holds an rness workspace (.rness/); run rness add inside it to add repositories
1325
- `
1326
- );
1327
- return 1;
1328
- }
1329
- if (!await isEmptyDir(root)) {
1330
- process.stderr.write(`${shown} is not empty
1331
- `);
1332
- return 1;
1333
- }
1334
- }
1335
- if (opts.yes !== true && !isTty2()) {
1336
- process.stderr.write(
1337
- "rness create writes files; pass --yes to run without a prompt\n"
1338
- );
1339
- return 2;
1340
- }
1341
- const contextUrl = repoUrl(host, org, ".rness");
1342
- const probe = await probeRemote(contextUrl);
1343
- if (probe.kind === "error") {
1344
- process.stderr.write(`${probe.message}
1345
- `);
1346
- return 1;
1347
- }
1348
- const joining = probe.kind === "found";
1349
- if (opts.yes !== true) {
1350
- const { confirm, isCancel } = await import("@clack/prompts");
1351
- const message = joining ? `Join organisation \`${org}\`: clone ${contextUrl} into ${shown}/.rness and sync its repositories?` : `No ${contextUrl} found (or no access to it). Start a new workspace \`${org}\` in ${shown}/?`;
1352
- const ok = await confirm({ message });
1353
- if (isCancel(ok) || ok !== true) {
1354
- process.stderr.write("cancelled\n");
1355
- return 1;
1356
- }
1357
- }
1358
- const rnessDir = join13(root, ".rness");
1359
- if (joining) {
1360
- await mkdir2(root, { recursive: true });
1361
- await clone(contextUrl, rnessDir);
1362
- const manifest2 = await loadManifest(rnessDir);
1363
- process.stdout.write(`cloned ${shown}/.rness (joined ${org})
1364
- `);
1365
- if (opts.skipInstall === true)
1366
- process.stdout.write("skipped install (--skip-install)\n");
1367
- else {
1368
- await installDependencies(pm, rnessDir);
1369
- process.stdout.write(`installed dependencies with ${pm}
1370
- `);
1371
- }
1372
- await mkdir2(join13(root, "org"), { recursive: true });
1373
- const failures2 = await addRepositories({
1374
- root,
1375
- rnessDir,
1376
- manifest: manifest2,
1377
- org,
1378
- host,
1379
- specs: splitSpecs(opts.repos ?? "")
1380
- });
1381
- const code2 = await syncPinned(root, shown);
1382
- if (code2 !== 0) return code2;
1383
- return failures2.length > 0 ? 1 : 0;
1384
- }
1385
- const pmVersion = await packageManagerVersion(pm);
1386
- await mkdir2(root, { recursive: true });
1387
- const manifest = { contract: 1, org, repos: {}, scopes: {} };
1388
- try {
1389
- await copyScaffold(rnessDir, {
1390
- version: VERSION,
1391
- packageManager: `${pm}@${pmVersion}`
1392
- });
1393
- await writeManifest(rnessDir, manifest);
1394
- process.stdout.write(`created ${shown}/.rness (new workspace)
1395
- `);
1396
- if (opts.skipInstall === true)
1397
- process.stdout.write("skipped install (--skip-install)\n");
1398
- else {
1399
- await installDependencies(pm, rnessDir);
1400
- process.stdout.write(`installed dependencies with ${pm}
1401
- `);
1402
- }
1403
- } catch (e) {
1404
- reportError(e);
1405
- await rm3(rnessDir, { recursive: true, force: true }).catch(
1406
- () => void 0
1407
- );
1408
- if (root !== resolve3(cwd) && await isEmptyDir(root).catch(() => false))
1409
- await rm3(root, { recursive: true, force: true }).catch(() => void 0);
1410
- process.stderr.write(
1411
- `removed ${shown}/.rness after the failure; fix it and retry
1412
- `
1413
- );
1414
- return 1;
1415
- }
1416
- await mkdir2(join13(root, "org"), { recursive: true });
1417
- let specs = splitSpecs(opts.repos ?? "");
1418
- if (opts.repos === void 0 && opts.yes !== true) {
1419
- const { isCancel, text } = await import("@clack/prompts");
1420
- const answer = await text({
1421
- message: "Repositories to add now (owner/repo, comma-separated; empty for none)",
1422
- defaultValue: ""
1423
- });
1424
- if (isCancel(answer)) {
1425
- process.stderr.write("cancelled\n");
1426
- return 1;
1427
- }
1428
- if (typeof answer === "string") specs = splitSpecs(answer);
1429
- }
1430
- const failures = await addRepositories({
1431
- root,
1432
- rnessDir,
1433
- manifest,
1434
- org,
1435
- host,
1436
- specs
1437
- });
1438
- await init(rnessDir);
1439
- try {
1440
- await commitAll(rnessDir, "chore: rness workspace context");
1441
- process.stdout.write(`committed ${shown}/.rness
1442
- `);
1443
- } catch (e) {
1444
- process.stderr.write(
1445
- `warning: ${e instanceof Error ? e.message : String(e)} \u2014 commit ${shown}/.rness yourself
1446
- `
1447
- );
1448
- }
1449
- const code = await syncPinned(root, shown);
1450
- if (code !== 0) return code;
1451
- const gh = await ghAvailable();
1452
- process.stdout.write(
1453
- [
1454
- "",
1455
- `Workspace \`${org}\` is ready in ${shown}/.`,
1456
- "",
1457
- "Next:",
1458
- ` cd ${shown}/.rness`,
1459
- // What was asked for and did not happen, as the command that retries
1460
- // it — the workspace is complete otherwise.
1461
- ...failures.map(
1462
- (f) => ` rness add ${f.spec} # failed: ${f.message}`
1463
- ),
1464
- gh ? ` gh repo create ${org}/.rness --private --source . --push` : ` git remote add origin ${contextUrl} && git push -u origin main`,
1465
- ` # then, for every teammate: npm create rness ${org}`,
1466
- ""
1467
- ].join("\n")
1468
- );
1469
- return failures.length > 0 ? 1 : 0;
1470
- } catch (e) {
1471
- return reportError(e);
1472
- }
1473
- }
1474
-
1475
- // src/core/blocks.ts
1476
- import { join as join14 } from "path";
1477
- async function checkBlocks({
1478
- root,
1479
- rnessDir,
1480
- manifest,
1481
- org
1482
- }) {
1483
- const problems = [];
1484
- const warnings = [];
1485
- if (!await exists(join14(root, "org"))) return { problems, warnings };
1486
- const targets = [{ scope: null, dir: root, depth: 0, label: "AGENTS.md" }];
1487
- for (const [name, entry] of Object.entries(manifest.scopes)) {
1488
- const segments = entry.path.split("/");
1489
- targets.push({
1490
- scope: name,
1491
- dir: join14(root, ...segments),
1492
- depth: segments.length,
1493
- label: `${entry.path}/AGENTS.md`
1494
- });
1495
- }
1496
- for (const t of targets) {
1497
- if (!await exists(t.dir)) {
1498
- warnings.push(`${t.label}: directory not present, block not checked`);
1499
- continue;
1500
- }
1501
- const text = await readOrNull(join14(t.dir, "AGENTS.md"));
1502
- const lines = text === null ? [] : text.split(/\r?\n/);
1503
- const found = findBlock(lines);
1504
- if (found.kind === "none") {
1505
- warnings.push(`${t.label}: no rness block yet (run rness sync)`);
1506
- continue;
1507
- }
1508
- if (found.kind === "error") {
1509
- problems.push(`${t.label}: ${found.message}`);
1510
- continue;
1511
- }
1512
- const header2 = parseHeader(lines[found.begin + 1] ?? "");
1513
- const context = await assembleContext({
1514
- rnessDir,
1515
- manifest,
1516
- scope: t.scope
1517
- });
1518
- const fresh = renderBlock({
1519
- scope: t.scope,
1520
- org,
1521
- depth: t.depth,
1522
- context,
1523
- version: VERSION
1524
- });
1525
- const actual = blockHash(bodyOf(lines.slice(found.begin, found.end + 1)));
1526
- if (header2 === null || header2.hash !== fresh.hash || header2.hash !== actual) {
1527
- problems.push(`${t.label}: stale rness block (run rness sync)`);
1528
- }
1529
- }
1530
- return { problems, warnings };
1531
- }
1532
-
1533
- // src/core/contract.ts
1534
- import { join as join15 } from "path";
1535
- var STATUSES = {
1536
- adr: ["Proposed", "Accepted", "Rejected", "Superseded"],
1537
- specs: [
1538
- "Draft",
1539
- "Proposed",
1540
- "Approved",
1541
- "Implemented",
1542
- "Superseded",
1543
- "Rejected"
1544
- ],
1545
- plans: ["Draft", "Ready", "In progress", "Blocked", "Completed", "Abandoned"]
1546
- };
1547
- var SKIP = /* @__PURE__ */ new Set(["adr/0000-template.md"]);
1548
- var PLACEMENT_KEYS = ["scopes", "scope"];
1549
- async function checkContract(rnessDir) {
1550
- const problems = [];
1551
- for (const name of COLLECTIONS) {
1552
- const allowed = STATUSES[name];
1553
- const items = await collectMarkdown(join15(rnessDir, name));
1554
- for (const item of items) {
1555
- const where = `${name}/${item.rel}`;
1556
- if (SKIP.has(where)) continue;
1557
- if (item.fieldsError !== null) {
1558
- problems.push(`${where}: invalid front matter (${item.fieldsError})`);
1559
- continue;
1560
- }
1561
- if (item.fields !== null) {
1562
- for (const key of PLACEMENT_KEYS) {
1563
- if (Object.hasOwn(item.fields, key)) {
1564
- problems.push(
1565
- `${where}: front matter "${key}" is not supported \u2014 the directory decides the scope; move the file`
1566
- );
1567
- }
1568
- }
1569
- }
1570
- if (allowed === void 0) continue;
1571
- if (item.fields === null) {
1572
- problems.push(`${where}: missing a front-matter block`);
1573
- continue;
1574
- }
1575
- if (item.title === null) problems.push(`${where}: missing a "# " title`);
1576
- const status = item.fields["status"];
1577
- if (status === void 0 || status === null || status === "") {
1578
- problems.push(`${where}: missing "status"`);
1579
- } else if (typeof status !== "string" || !allowed.includes(status)) {
1580
- problems.push(
1581
- `${where}: unknown status ${JSON.stringify(status)} (expected ${allowed.join(", ")})`
1582
- );
1583
- }
1584
- }
1585
- }
1586
- return problems;
1587
- }
1588
-
1589
- // src/commands/validate.ts
1590
- async function validateCommand(opts) {
1591
- const cwd = opts.cwd ?? process.cwd();
1592
- try {
1593
- const ws = await findWorkspace(cwd);
1594
- const manifest = await loadManifest(ws.rnessDir);
1595
- for (const s of Object.keys(manifest.scopes)) scopeChain(manifest, s);
1596
- const { org, warning } = resolveOrg(manifest, ws.root);
1597
- const warnings = warning === null ? [] : [warning];
1598
- const problems = await checkContract(ws.rnessDir);
1599
- const blocks = await checkBlocks({
1600
- root: ws.root,
1601
- rnessDir: ws.rnessDir,
1602
- manifest,
1603
- org
1604
- });
1605
- problems.push(...blocks.problems);
1606
- warnings.push(...blocks.warnings);
1607
- for (const p of problems) process.stderr.write(`${p}
1608
- `);
1609
- for (const w of warnings) process.stderr.write(`warning: ${w}
1610
- `);
1611
- if (problems.length > 0) return 1;
1612
- process.stdout.write("context ok\n");
1613
- return 0;
1614
- } catch (e) {
1615
- return reportError(e);
1616
- }
1617
- }
1618
-
1619
- // src/cli.ts
1620
- import { Command, CommanderError, Option } from "commander";
1621
- function buildProgram(state) {
1622
- const program = new Command().name("rness").description("Configuration-plane CLI for rness workspaces").version(VERSION, "-v, --version", "print the version").exitOverride().showHelpAfterError().configureOutput({
1623
- writeOut: (s) => {
1624
- process.stdout.write(s);
1625
- },
1626
- writeErr: (s) => {
1627
- process.stderr.write(s);
1628
- }
1629
- });
1630
- program.command("context").description("Resolve and print the context for a scope").option(
1631
- "--scope <name>",
1632
- "scope to resolve (default: the scope owning the current directory)"
1633
- ).option("--json", "print JSON instead of Markdown").addOption(new Option("--cwd <dir>").hideHelp()).action(async (opts) => {
1634
- state.code = await contextCommand(opts);
1635
- });
1636
- program.command("validate").description(
1637
- "Check the .rness/ tree against the contract and every generated block"
1638
- ).addOption(new Option("--cwd <dir>").hideHelp()).action(async (opts) => {
1639
- state.code = await validateCommand(opts);
1640
- });
1641
- program.command("sync").description(
1642
- "Clone missing repositories and write the rness block into every AGENTS.md"
1643
- ).option("--scope <name>", "only this scope (the root block is skipped)").option(
1644
- "--check",
1645
- "render and compare only; exit 1 when a block is out of date"
1646
- ).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) => {
1647
- state.code = await syncCommand(opts);
1648
- });
1649
- program.command("add").description(
1650
- "Clone (or adopt) a repository under org/ and declare it in rness.json"
1651
- ).argument("<repo>", "<repo>, <owner>/<repo>, or a clone URL").option(
1652
- "--scopes <list>",
1653
- "comma-separated sub-directories to declare as scopes extending the repository"
1654
- ).option("--ssh", "use git@github.com: instead of https://github.com/").option("-y, --yes", "do not ask for confirmation").addOption(new Option("--host <base>").hideHelp()).addOption(new Option("--cwd <dir>").hideHelp()).action(async (repo, opts) => {
1655
- state.code = await addCommand(repo, opts);
1656
- });
1657
- program.command("create").description(
1658
- "Start a workspace for a GitHub organisation, or join its existing .rness"
1659
- ).argument("<org>", "the GitHub organisation (github.com/<org>)").option("--dir <path>", "target directory (default: ./<org>)").option("--repos <list>", "comma-separated repositories to add right away").option(
1660
- "--pm <name>",
1661
- `package manager for .rness/ (${PACKAGE_MANAGERS.join(", ")}; default: the one running this command)`
1662
- ).option("--ssh", "use git@github.com: instead of https://github.com/").option("--skip-install", "do not install .rness/ dependencies").option("-y, --yes", "do not ask for confirmation").option("--template <name>", "reserved").addOption(new Option("--host <base>").hideHelp()).addOption(new Option("--cwd <dir>").hideHelp()).action(async (org, opts) => {
1663
- state.code = await createCommand(org, opts);
1664
- });
1665
- return program;
1666
- }
1667
- async function run(argv) {
1668
- const state = { code: 0 };
1669
- const program = buildProgram(state);
1670
- if (argv.length === 0) {
1671
- process.stdout.write(program.helpInformation());
1672
- return 0;
1673
- }
1674
- try {
1675
- await program.parseAsync(argv, { from: "user" });
1676
- } catch (e) {
1677
- if (e instanceof CommanderError) {
1678
- return e.exitCode === 0 ? 0 : 2;
1679
- }
1680
- throw e;
1681
- }
1682
- return state.code;
1683
- }
1684
-
1685
- export {
1686
- exists,
1687
- readOrNull,
1688
- writeFileAtomic,
1689
- DEFAULT_HOST,
1690
- SSH_HOST,
1691
- repoUrl,
1692
- classifyProbe,
1693
- probeRemote,
1694
- NAME,
1695
- loadManifest,
1696
- writeManifest,
1697
- parseRepoSpec,
1698
- addRepository,
1699
- workspaceDirs,
1700
- findWorkspace,
1701
- reportError,
1702
- BEGIN,
1703
- END,
1704
- BLOCK_SIZE_WARNING,
1705
- blockHash,
1706
- renderBlock,
1707
- parseHeader,
1708
- bodyOf,
1709
- parseFrontMatter,
1710
- extractTitle,
1711
- collectMarkdown,
1712
- resolveScope,
1713
- scopeChain,
1714
- COLLECTIONS,
1715
- assembleContext,
1716
- findBlock,
1717
- mergeBlock,
1718
- ensureClaudeMd,
1719
- VERSION,
1720
- syncCommand,
1721
- addCommand,
1722
- renderMarkdown,
1723
- contextCommand,
1724
- findDelegate,
1725
- PACKAGE_MANAGERS,
1726
- isPackageManager,
1727
- detectPackageManager,
1728
- packageManagerVersion,
1729
- installDependencies,
1730
- SCAFFOLD_FILES,
1731
- scaffoldDir,
1732
- copyScaffold,
1733
- createCommand,
1734
- checkBlocks,
1735
- STATUSES,
1736
- checkContract,
1737
- validateCommand,
1738
- run
1739
- };
1740
- //# sourceMappingURL=chunk-IW5IHSPC.js.map