dw-sdlc 0.0.0-stage → 0.1.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.
Files changed (48) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +63 -2
  3. package/config/agentic.config.schema.json +212 -0
  4. package/config/references.schema.json +56 -0
  5. package/dist/agentic-config.d.ts +167 -0
  6. package/dist/agentic-config.js +307 -0
  7. package/dist/catalogue.d.ts +36 -0
  8. package/dist/catalogue.js +74 -0
  9. package/dist/checks/agents-map.d.ts +7 -0
  10. package/dist/checks/agents-map.js +166 -0
  11. package/dist/checks/check.d.ts +17 -0
  12. package/dist/checks/check.js +21 -0
  13. package/dist/checks/index.d.ts +8 -0
  14. package/dist/checks/index.js +19 -0
  15. package/dist/checks/primitives.d.ts +8 -0
  16. package/dist/checks/primitives.js +128 -0
  17. package/dist/cli.d.ts +2 -0
  18. package/dist/cli.js +43 -0
  19. package/dist/commands/config.d.ts +2 -0
  20. package/dist/commands/config.js +76 -0
  21. package/dist/commands/doctor.d.ts +57 -0
  22. package/dist/commands/doctor.js +341 -0
  23. package/dist/commands/gate.d.ts +2 -0
  24. package/dist/commands/gate.js +175 -0
  25. package/dist/commands/primitives.d.ts +2 -0
  26. package/dist/commands/primitives.js +106 -0
  27. package/dist/commands/references-setup.d.ts +2 -0
  28. package/dist/commands/references-setup.js +213 -0
  29. package/dist/commands/references-sync.d.ts +2 -0
  30. package/dist/commands/references-sync.js +347 -0
  31. package/dist/commands/setup.d.ts +31 -0
  32. package/dist/commands/setup.js +369 -0
  33. package/dist/json-schema.d.ts +8 -0
  34. package/dist/json-schema.js +227 -0
  35. package/dist/markdown.d.ts +19 -0
  36. package/dist/markdown.js +77 -0
  37. package/dist/pins.d.ts +16 -0
  38. package/dist/pins.js +83 -0
  39. package/dist/references-key.d.ts +31 -0
  40. package/dist/references-key.js +150 -0
  41. package/dist/report.d.ts +13 -0
  42. package/dist/report.js +29 -0
  43. package/dist/router-block.d.ts +13 -0
  44. package/dist/router-block.js +62 -0
  45. package/dist/setup-rules.d.ts +22 -0
  46. package/dist/setup-rules.js +98 -0
  47. package/package.json +30 -3
  48. package/templates/router.md +38 -0
@@ -0,0 +1,175 @@
1
+ // `dw-sdlc gate`: the Gate of a repository, from the `gate` key of its resolved agentic config.
2
+ import { spawnSync } from "node:child_process";
3
+ import { parseArgs } from "node:util";
4
+ import { repositoryRoot, resolveConfig } from "../agentic-config.js";
5
+ import { CannotRun } from "../checks/check.js";
6
+ import { enabledChecks } from "../checks/index.js";
7
+ import { annotation, failureJson, failureLine } from "../report.js";
8
+ export const summary = "run the Gate: the Steps of gate.steps in order, then every Check under gate.checks";
9
+ const USAGE = `Usage: dw-sdlc gate [--json] [--root <dir>]
10
+
11
+ Runs the Gate of the repository holding --root (default: the current directory): each Step of
12
+ gate.steps in order from the repository root, its output passed through, stopping at the first that
13
+ fails; then every Check under gate.checks, the run scripts and then the Checks the package ships,
14
+ none stopping the next. Prints one line per failure: the file where there is one, the rule, the
15
+ remedy; under GitHub Actions each line is also a workflow annotation. --json prints one document
16
+ instead and sends the commands' own output to stderr.
17
+
18
+ Exit 0 when everything passes, 1 when a Step or a Check fails, 2 when the Gate cannot run: an
19
+ invalid config layer, a Step or Check whose command the shell cannot find or execute, or a Check
20
+ missing an input it reads.`;
21
+ const EXIT_CODES = { pass: 0, fail: 1, error: 2 };
22
+ /** What the shell answers for a command it cannot find, and for one it cannot execute. */
23
+ const NOT_RUNNABLE = { 127: "was not found", 126: "could not be executed" };
24
+ const SKIPPED = { status: "skipped", exitCode: null };
25
+ const REMEDIES = {
26
+ invalidLayer: "fix the file; the Gate runs once every layer is valid",
27
+ failed: "fix what it printed above",
28
+ stepNotRunnable: "install the command or fix gate.steps in the agentic config",
29
+ checkNotRunnable: "install the command or fix gate.checks.run in the agentic config",
30
+ checkStopped: "fix what stopped it, or report it to dw-sdlc"
31
+ };
32
+ function plural(count, noun) {
33
+ return `${count} ${noun}${count === 1 ? "" : "s"}`;
34
+ }
35
+ function parseCli(argv) {
36
+ try {
37
+ const { values } = parseArgs({
38
+ args: argv,
39
+ options: { help: { type: "boolean", short: "h" }, json: { type: "boolean" }, root: { type: "string" } }
40
+ });
41
+ return { help: values.help === true, json: values.json === true, root: values.root };
42
+ }
43
+ catch (error) {
44
+ return { error: error instanceof Error ? error.message : String(error) };
45
+ }
46
+ }
47
+ function configFailure({ file, path, message }) {
48
+ const where = path === "" ? message : `${path}: ${message}`;
49
+ return { file, rule: "config", message: where, remedy: REMEDIES.invalidLayer };
50
+ }
51
+ /**
52
+ * Runs `command` through `sh -c` from `root`, stdin closed and stderr passed through; stdout too, or
53
+ * onto stderr when `--json` keeps stdout for the document. `detail` says what went wrong, when it did.
54
+ */
55
+ function runCommand(command, root, json) {
56
+ const result = spawnSync(command, { shell: true, cwd: root, stdio: ["ignore", json ? 2 : "inherit", "inherit"] });
57
+ if (result.error !== undefined) {
58
+ return { status: "error", exitCode: null, detail: `could not start: ${result.error.message}` };
59
+ }
60
+ if (result.status === 0) {
61
+ return { status: "pass", exitCode: 0 };
62
+ }
63
+ if (result.status === null) {
64
+ return { status: "fail", exitCode: null, detail: `was stopped by ${result.signal ?? "a signal"}` };
65
+ }
66
+ const notRunnable = NOT_RUNNABLE[result.status];
67
+ if (notRunnable !== undefined) {
68
+ return { status: "error", exitCode: result.status, detail: `${notRunnable} (exit ${result.status})` };
69
+ }
70
+ return { status: "fail", exitCode: result.status, detail: `exited ${result.status}` };
71
+ }
72
+ /** Runs one Check the package ships, its failures named by the Check; one it cannot judge is an error. */
73
+ function runShipped(check, root) {
74
+ try {
75
+ const failures = check.run(root).map((finding) => ({ rule: check.name, ...finding }));
76
+ const status = failures.length === 0 ? "pass" : "fail";
77
+ return { status, exitCode: EXIT_CODES[status], failures };
78
+ }
79
+ catch (error) {
80
+ const finding = error instanceof CannotRun
81
+ ? error.finding
82
+ : {
83
+ message: `stopped: ${error instanceof Error ? error.message : String(error)}`,
84
+ remedy: REMEDIES.checkStopped
85
+ };
86
+ return { status: "error", exitCode: EXIT_CODES.error, failures: [{ rule: check.name, ...finding }] };
87
+ }
88
+ }
89
+ function worst(outcomes) {
90
+ const statuses = new Set(outcomes.map(({ status }) => status));
91
+ return statuses.has("error") ? "error" : statuses.has("fail") ? "fail" : "pass";
92
+ }
93
+ function runGate(root, json) {
94
+ const log = json ? process.stderr : process.stdout;
95
+ const { config, problems } = resolveConfig(root);
96
+ const gateCommand = config.gate.command ?? null;
97
+ if (problems.length > 0) {
98
+ return { status: "error", command: gateCommand, steps: [], checks: [], failures: problems.map(configFailure) };
99
+ }
100
+ const failures = [];
101
+ /** Runs one Step or Check and records its failure under `rule`, if it has one. */
102
+ const runOne = (command, rule, notRunnable) => {
103
+ const { detail, ...outcome } = runCommand(command, root, json);
104
+ if (detail !== undefined) {
105
+ const remedy = outcome.status === "error" ? notRunnable : REMEDIES.failed;
106
+ failures.push({ rule, message: `${JSON.stringify(command)} ${detail}`, remedy });
107
+ }
108
+ return outcome;
109
+ };
110
+ const steps = [];
111
+ let stopped = false;
112
+ for (const step of config.gate.steps) {
113
+ if (stopped) {
114
+ steps.push({ command: step, ...SKIPPED });
115
+ continue;
116
+ }
117
+ log.write(`gate: step ${step}\n`);
118
+ const outcome = runOne(step, "step", REMEDIES.stepNotRunnable);
119
+ steps.push({ command: step, ...outcome });
120
+ stopped = outcome.status !== "pass";
121
+ }
122
+ const checks = [];
123
+ for (const check of config.gate.checks.run) {
124
+ if (stopped) {
125
+ checks.push({ ...check, ...SKIPPED });
126
+ continue;
127
+ }
128
+ log.write(`gate: check ${check.name}\n`);
129
+ checks.push({ ...check, ...runOne(check.command, check.name, REMEDIES.checkNotRunnable) });
130
+ }
131
+ for (const check of enabledChecks(config.gate.checks)) {
132
+ if (stopped) {
133
+ checks.push({ name: check.name, command: null, ...SKIPPED });
134
+ continue;
135
+ }
136
+ log.write(`gate: check ${check.name}\n`);
137
+ const { failures: found, ...outcome } = runShipped(check, root);
138
+ failures.push(...found);
139
+ checks.push({ name: check.name, command: null, ...outcome });
140
+ }
141
+ return { status: worst([...steps, ...checks]), command: gateCommand, steps, checks, failures };
142
+ }
143
+ function verdict(report) {
144
+ if (report.status === "pass") {
145
+ return `gate: green — ${plural(report.steps.length, "step")}, ${plural(report.checks.length, "check")}`;
146
+ }
147
+ const failures = plural(report.failures.length, "failure");
148
+ return report.status === "fail" ? `gate: failed — ${failures}` : `gate: cannot run — ${failures}`;
149
+ }
150
+ export function run(argv) {
151
+ const cli = parseCli(argv);
152
+ if ("error" in cli) {
153
+ process.stderr.write(`dw-sdlc gate: ${cli.error}\n\n${USAGE}\n`);
154
+ return 2;
155
+ }
156
+ if (cli.help) {
157
+ process.stdout.write(`${USAGE}\n`);
158
+ return 0;
159
+ }
160
+ const report = runGate(repositoryRoot(cli.root ?? process.cwd()), cli.json);
161
+ const actions = process.env.GITHUB_ACTIONS === "true";
162
+ if (cli.json) {
163
+ const document = { ...report, failures: report.failures.map(failureJson) };
164
+ process.stdout.write(`${JSON.stringify(document, null, 2)}\n`);
165
+ // The runner reads workflow commands from stderr as from stdout, so stdout stays one document.
166
+ for (const failure of actions ? report.failures : []) {
167
+ process.stderr.write(`${annotation(failure)}\n`);
168
+ }
169
+ }
170
+ else {
171
+ const lines = report.failures.flatMap((failure) => actions ? [failureLine(failure), annotation(failure)] : [failureLine(failure)]);
172
+ process.stdout.write(`${[...lines, verdict(report)].join("\n")}\n`);
173
+ }
174
+ return EXIT_CODES[report.status];
175
+ }
@@ -0,0 +1,2 @@
1
+ export declare const summary = "name the Primitives that own each path, with their summaries and documents";
2
+ export declare function run(argv: string[]): number;
@@ -0,0 +1,106 @@
1
+ // `dw-sdlc primitives --paths <files>`: for each path, the Primitives of the catalogue that name it,
2
+ // with their summaries and the documents to read, or that none does.
3
+ import { existsSync, realpathSync } from "node:fs";
4
+ import { basename, dirname, join, relative, resolve } from "node:path";
5
+ import { parseArgs } from "node:util";
6
+ import { repositoryRoot, resolveConfig } from "../agentic-config.js";
7
+ import { primitivesNaming, readCatalogue } from "../catalogue.js";
8
+ import { catalogueFile } from "../checks/primitives.js";
9
+ export const summary = "name the Primitives that own each path, with their summaries and documents";
10
+ const USAGE = `Usage: dw-sdlc primitives --paths <file>... [--json] [--root <dir>]
11
+
12
+ Prints, for each path, the Primitives of the catalogue whose code names it, the file itself or the
13
+ nearest directory above it: the id, the name, the summary and the documents to read; or that no
14
+ Primitive names it. A path is read from the current directory. The catalogue is the one the
15
+ primitives Check reads, in the repository holding --root (default: the current directory). --json
16
+ prints one document instead.
17
+
18
+ Exit 0 with an answer for every path, 2 when there is no catalogue to read or it is not one.`;
19
+ const REMEDIES = {
20
+ missing: "create it, or point gate.checks.primitives.file at the catalogue",
21
+ invalid: "fix the catalogue; dw-sdlc gate names each problem under the primitives Check"
22
+ };
23
+ function parseCli(argv) {
24
+ try {
25
+ const { values, positionals } = parseArgs({
26
+ args: argv,
27
+ allowPositionals: true,
28
+ options: {
29
+ help: { type: "boolean", short: "h" },
30
+ json: { type: "boolean" },
31
+ paths: { type: "boolean" },
32
+ root: { type: "string" }
33
+ }
34
+ });
35
+ const cli = {
36
+ help: values.help === true,
37
+ json: values.json === true,
38
+ paths: values.paths === true,
39
+ files: positionals,
40
+ root: values.root
41
+ };
42
+ return cli.help || cli.paths ? cli : { error: "name the paths with --paths" };
43
+ }
44
+ catch (error) {
45
+ return { error: error instanceof Error ? error.message : String(error) };
46
+ }
47
+ }
48
+ /**
49
+ * `path` with every symbolic link in it resolved, as git reports the root: the nearest part on disk
50
+ * resolved and the rest kept, so a deleted file still maps into the repository.
51
+ */
52
+ function realPath(path) {
53
+ if (existsSync(path)) {
54
+ return realpathSync(path);
55
+ }
56
+ const parent = dirname(path);
57
+ return parent === path ? path : join(realPath(parent), basename(path));
58
+ }
59
+ function brief({ id, name, summary: what, docs }) {
60
+ return { id, name, summary: what, docs };
61
+ }
62
+ function plain(path, primitives) {
63
+ if (primitives.length === 0) {
64
+ return [`${path}: no Primitive names this path`];
65
+ }
66
+ return primitives.flatMap(({ id, name, summary: what, docs }) => [
67
+ `${path}: ${id} (${name}) — ${what}`,
68
+ ` read: ${docs.join(", ")}`
69
+ ]);
70
+ }
71
+ export function run(argv) {
72
+ const cli = parseCli(argv);
73
+ if ("error" in cli) {
74
+ process.stderr.write(`dw-sdlc primitives: ${cli.error}\n\n${USAGE}\n`);
75
+ return 2;
76
+ }
77
+ if (cli.help) {
78
+ process.stdout.write(`${USAGE}\n`);
79
+ return 0;
80
+ }
81
+ const root = repositoryRoot(cli.root ?? process.cwd());
82
+ const file = catalogueFile(resolveConfig(root).config.gate.checks);
83
+ const read = readCatalogue(root, file);
84
+ if (read.state !== "valid") {
85
+ const problems = read.state === "missing"
86
+ ? [`the catalogue does not exist — ${REMEDIES.missing}`]
87
+ : read.problems.map((problem) => `${problem} — ${REMEDIES.invalid}`);
88
+ process.stderr.write(problems.map((problem) => `dw-sdlc primitives: ${file}: ${problem}\n`).join(""));
89
+ return 2;
90
+ }
91
+ const answers = cli.files.map((path) => {
92
+ const fromRoot = relative(root, realPath(resolve(path)));
93
+ return { path, primitives: primitivesNaming(read.catalogue, fromRoot) };
94
+ });
95
+ if (cli.json) {
96
+ const paths = answers.map(({ path, primitives }) => ({ path, primitives: primitives.map(brief) }));
97
+ process.stdout.write(`${JSON.stringify({ paths }, null, 2)}\n`);
98
+ }
99
+ else {
100
+ process.stdout.write(answers
101
+ .flatMap(({ path, primitives }) => plain(path, primitives))
102
+ .map((line) => `${line}\n`)
103
+ .join(""));
104
+ }
105
+ return 0;
106
+ }
@@ -0,0 +1,2 @@
1
+ export declare const summary = "set a repository up to declare References: the Router, its table and the ignore rules";
2
+ export declare function run(argv: string[]): number;
@@ -0,0 +1,213 @@
1
+ // `dw-sdlc references setup` sets a repository up to declare References. Five files, each written only
2
+ // when it would change, so a repeat with nothing changed writes nothing:
3
+ //
4
+ // references/AGENTS.md the Router, from templates/router.md when there is none; on every run the
5
+ // block between its two markers, a table of each Reference's dir and when
6
+ // from the key, while the text around the block stays the author's
7
+ // references/CLAUDE.md a symlink to AGENTS.md, so Claude Code loads the Router; written once
8
+ // .claude/settings.json claudeMdExcludes for the clones' own CLAUDE.md and AGENTS.md
9
+ // .gitignore references/* and the two exceptions that keep the Router tracked
10
+ // .ai/agentic.config.json an empty references key when the config has none
11
+ //
12
+ // Existing content stays: a list gains the entries it lacks at its end, and nothing is removed or
13
+ // reordered. A settings file or a config that is not a JSON object is refused, not rewritten, and so
14
+ // is an ignore file that ignores references/ itself and a Router with one marker and not the other,
15
+ // or whose key fails the parser. --check writes nothing and exits 1 when any file would change.
16
+ import { spawnSync } from "node:child_process";
17
+ import { existsSync, lstatSync, mkdirSync, readFileSync, symlinkSync, writeFileSync } from "node:fs";
18
+ import { dirname, join } from "node:path";
19
+ import { parseArgs } from "node:util";
20
+ import { CONFIG, isObject, readReferences } from "../references-key.js";
21
+ import { renderBlock, withBlock } from "../router-block.js";
22
+ const ROUTER = "references/AGENTS.md";
23
+ const ROUTER_LINK = "references/CLAUDE.md";
24
+ const SETTINGS = ".claude/settings.json";
25
+ const IGNORE = ".gitignore";
26
+ const TEMPLATE = join(import.meta.dirname, "..", "..", "templates", "router.md");
27
+ /** A clone's own agent files at any depth; Claude Code matches the globs against absolute paths. */
28
+ const EXCLUDES = [
29
+ "**/references/*/CLAUDE.md",
30
+ "**/references/*/**/CLAUDE.md",
31
+ "**/references/*/AGENTS.md",
32
+ "**/references/*/**/AGENTS.md"
33
+ ];
34
+ /** Every clone ignored, then the Router and its symlink tracked: an exception counts after its pattern. */
35
+ const IGNORED = "references/*";
36
+ const TRACKED = ["!references/AGENTS.md", "!references/CLAUDE.md"];
37
+ /** Lines that ignore references/ itself: git never looks inside it, so no exception tracks the Router. */
38
+ const WHOLE = new Set(["references", "references/", "/references", "/references/"]);
39
+ const IGNORE_COMMENT = "# References are cloned on each machine by the references plugin; only the Router is tracked.";
40
+ export const summary = "set a repository up to declare References: the Router, its table and the ignore rules";
41
+ const USAGE = `Usage: dw-sdlc references setup [--root <dir>] [--dry-run | --check]
42
+ or: pnpm references:setup with the same flags, in a checkout of dw-sdlc
43
+
44
+ Sets the repository at --root, or the one the current directory sits in, up to declare References:
45
+ the Router in ${ROUTER} with its CLAUDE.md symlink and, between its two markers, the table of each
46
+ Reference's dir and when from the references key, claudeMdExcludes in ${SETTINGS} for the clones'
47
+ own agent files, the ${IGNORE} lines that keep every clone out of git but the Router, and an empty
48
+ references key in ${CONFIG}. Every file is written only when it would change, and the text around
49
+ the Router's table never; --dry-run reports without writing, and --check does the same and exits 1
50
+ when a file would change. Prints one line per file. Exit 1 when a file is refused, 2 on a bad
51
+ argument.`;
52
+ function messageOf(error) {
53
+ return error instanceof Error ? error.message : String(error);
54
+ }
55
+ function fail(message) {
56
+ console.error(`references:setup: ${message}`);
57
+ process.exit(2);
58
+ }
59
+ function render(value) {
60
+ return `${JSON.stringify(value, null, 2)}\n`;
61
+ }
62
+ /** The JSON object `text` holds, or undefined when it holds anything else. */
63
+ function parseObject(text) {
64
+ let parsed;
65
+ try {
66
+ parsed = JSON.parse(text);
67
+ }
68
+ catch {
69
+ return undefined;
70
+ }
71
+ return isObject(parsed) ? parsed : undefined;
72
+ }
73
+ class Writer {
74
+ steps = [];
75
+ root;
76
+ dryRun;
77
+ constructor(root, dryRun) {
78
+ this.root = root;
79
+ this.dryRun = dryRun;
80
+ }
81
+ record(file, changed, write) {
82
+ if (changed && !this.dryRun) {
83
+ const path = join(this.root, file);
84
+ mkdirSync(dirname(path), { recursive: true });
85
+ write(path);
86
+ }
87
+ this.steps.push({ file, outcome: changed ? "wrote" : "unchanged" });
88
+ }
89
+ /** The text `change` gives `file`, from what `file` holds now; written when the two differ. */
90
+ put(file, change) {
91
+ const path = join(this.root, file);
92
+ const current = existsSync(path) ? readFileSync(path, "utf8") : undefined;
93
+ const next = change(current);
94
+ if ("refusal" in next) {
95
+ this.steps.push({ file, outcome: "refused", detail: next.refusal });
96
+ return;
97
+ }
98
+ this.record(file, next.text !== current, (target) => {
99
+ writeFileSync(target, next.text);
100
+ });
101
+ }
102
+ /** `file`, made by `make` when nothing sits at its path, a dangling symlink included; never replaced. */
103
+ create(file, make) {
104
+ this.record(file, lstatSync(join(this.root, file), { throwIfNoEntry: false }) === undefined, make);
105
+ }
106
+ }
107
+ /** The settings with claudeMdExcludes holding every glob of EXCLUDES, those it lacks appended. */
108
+ function withExcludes(current) {
109
+ if (current === undefined) {
110
+ return { text: render({ claudeMdExcludes: EXCLUDES }) };
111
+ }
112
+ const settings = parseObject(current);
113
+ if (settings === undefined) {
114
+ return { refusal: "is not a JSON object; fix it by hand first" };
115
+ }
116
+ const value = settings.claudeMdExcludes ?? [];
117
+ if (!Array.isArray(value)) {
118
+ return { refusal: "claudeMdExcludes is not a list; fix it by hand first" };
119
+ }
120
+ const existing = value;
121
+ const missing = EXCLUDES.filter((glob) => !existing.includes(glob));
122
+ return { text: missing.length === 0 ? current : render({ ...settings, claudeMdExcludes: [...existing, ...missing] }) };
123
+ }
124
+ /**
125
+ * The ignore file with the lines it lacks appended. Without `references/*` all three go in, so each
126
+ * exception lands after the pattern it excepts; with it, only the exceptions it lacks.
127
+ */
128
+ function withIgnoreLines(current) {
129
+ const text = current ?? "";
130
+ const lines = text.split(/\r?\n/).map((line) => line.trimEnd());
131
+ if (lines.some((line) => WHOLE.has(line))) {
132
+ return { refusal: `ignores references/ itself, so no exception tracks the Router; ignore ${IGNORED} instead` };
133
+ }
134
+ const ignored = lines.includes(IGNORED);
135
+ const missing = ignored ? TRACKED.filter((line) => !lines.includes(line)) : [IGNORE_COMMENT, IGNORED, ...TRACKED];
136
+ if (missing.length === 0) {
137
+ return { text };
138
+ }
139
+ const head = text === "" || text.endsWith("\n") ? text : `${text}\n`;
140
+ const gap = head !== "" && !ignored ? "\n" : "";
141
+ return { text: `${head}${gap}${missing.join("\n")}\n` };
142
+ }
143
+ /** The config with an empty `references` key when it has none, inserted so its own layout stays. */
144
+ function withEmptyKey(current) {
145
+ if (current === undefined) {
146
+ return { text: render({ references: [] }) };
147
+ }
148
+ const config = parseObject(current);
149
+ if (config === undefined) {
150
+ return { refusal: "is not a JSON object; fix it by hand first" };
151
+ }
152
+ if (Object.hasOwn(config, "references")) {
153
+ return { text: current };
154
+ }
155
+ const close = current.lastIndexOf("}");
156
+ const body = current.slice(0, close).trimEnd();
157
+ const comma = body.endsWith("{") ? "" : ",";
158
+ return { text: `${body}${comma}\n "references": []\n${current.slice(close)}` };
159
+ }
160
+ /** The repository `dir` sits in, or `dir` itself outside one. */
161
+ function toplevel(dir) {
162
+ const result = spawnSync("git", ["-C", dir, "rev-parse", "--show-toplevel"], { encoding: "utf8" });
163
+ return result.status === 0 ? result.stdout.trim() : dir;
164
+ }
165
+ function setup(root, dryRun) {
166
+ const writer = new Writer(root, dryRun);
167
+ const template = readFileSync(TEMPLATE, "utf8");
168
+ const reading = readReferences(root);
169
+ writer.put(ROUTER, (current) => {
170
+ if ("problem" in reading) {
171
+ return { refusal: `${reading.problem}; the table comes from the key, so fix it first` };
172
+ }
173
+ const next = withBlock(current ?? template, renderBlock(reading.references ?? []));
174
+ return "problem" in next ? { refusal: next.problem } : next;
175
+ });
176
+ writer.create(ROUTER_LINK, (path) => {
177
+ symlinkSync("AGENTS.md", path);
178
+ });
179
+ writer.put(SETTINGS, withExcludes);
180
+ writer.put(IGNORE, withIgnoreLines);
181
+ writer.put(CONFIG, withEmptyKey);
182
+ return writer.steps;
183
+ }
184
+ export function run(argv) {
185
+ let values;
186
+ try {
187
+ values = parseArgs({
188
+ args: argv,
189
+ options: {
190
+ help: { type: "boolean", short: "h" },
191
+ root: { type: "string" },
192
+ "dry-run": { type: "boolean" },
193
+ check: { type: "boolean" }
194
+ }
195
+ }).values;
196
+ }
197
+ catch (error) {
198
+ fail(`${messageOf(error)}\n\n${USAGE}`);
199
+ }
200
+ if (values.help === true) {
201
+ console.log(USAGE);
202
+ return 0;
203
+ }
204
+ const check = values.check === true;
205
+ const dryRun = values["dry-run"] === true || check;
206
+ const steps = setup(toplevel(values.root ?? process.cwd()), dryRun);
207
+ for (const step of steps) {
208
+ const verb = step.outcome === "wrote" && dryRun ? "would write" : step.outcome;
209
+ console.log(`${verb.padEnd(11)} ${step.file}${step.detail === undefined ? "" : ` — ${step.detail}`}`);
210
+ }
211
+ const refused = steps.some((step) => step.outcome === "refused");
212
+ return refused || (check && steps.some((step) => step.outcome === "wrote")) ? 1 : 0;
213
+ }
@@ -0,0 +1,2 @@
1
+ export declare const summary = "bring every declared Reference to its ref under references/";
2
+ export declare function run(argv: string[]): Promise<number>;