@nikovirtala/adr 0.0.0 → 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.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Niko Virtala
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,116 @@
1
+ # @nikovirtala/adr
2
+
3
+ Architecture decision records kept as event logs in git, built on [`@nikovirtala/ledger`](https://www.npmjs.com/package/@nikovirtala/ledger). A decision is a Markdown body plus append-only event files. Its status (proposed, accepted, rejected, withdrawn, deprecated, superseded) is computed from the events, the body is frozen by its hash once accepted, and supersession chains always lead to the decision in force. See the [design doc](https://github.com/nikovirtala/ark/blob/main/docs/01-adr-v1.md).
4
+
5
+ This is phase 1 ("Core"), released at 0.x: the decision kind, the library and the `adr` CLI with `new`, `accept`, `supersede`, `list`, `show` and `verify`.
6
+
7
+ ## Install
8
+
9
+ ```sh
10
+ npm install --save-dev @nikovirtala/adr # Node 24 or newer, git 2.32.0 or newer
11
+ npx @nikovirtala/adr --help # or run it without installing
12
+ ```
13
+
14
+ ## Layout
15
+
16
+ ```
17
+ docs/adr/
18
+ adr.toml # optional config (or adr.toml at the repository root)
19
+ decisions/k3f9q8a1-use-aurora-for-orders.md
20
+ events/k3f9q8a1/20261016T081030Z-p4d2-proposed.yaml
21
+ events/k3f9q8a1/20261016T091512Z-7aq5-accepted.yaml
22
+ templates/nygard.md # copied from the built-in on first use
23
+ ```
24
+
25
+ Each decision has a canonical id (`k3f9q8a1`). Accepted decisions also get a number (`ADR-0001`), assigned in the order their acceptances land on the first-parent history of main and never stored or renumbered. Commands take an id or a number in any form: `k3f9q8a1`, `7`, `0007`, `ADR-7`, `adr-0007`.
26
+
27
+ ## Commands
28
+
29
+ ```sh
30
+ # Propose: render a draft from a template, write the body and the proposed event, and stage them.
31
+ adr new "Use Aurora for orders" --section context="We need SQL." --section scope=services/orders
32
+ adr new "Use Aurora Serverless" --supersedes ADR-0002 --template madr --edit
33
+
34
+ # Accept: lint the draft, hash the body, and write accepted (plus superseded on each declared target).
35
+ adr accept k3f9q8a1 --commit
36
+
37
+ # Supersede an accepted decision by another accepted one.
38
+ adr supersede ADR-0001 --by ADR-0002 --reason "Aurora fits the query load" --commit
39
+
40
+ # Read: list with computed status, show one decision with its state header.
41
+ adr list --status accepted
42
+ adr show ADR-0001 --history
43
+ adr list --ref v1.2.0 # time travel: the log as of any ref
44
+
45
+ # Verify the log: the whole tree, a range, or the staged changes (for a pre-commit hook).
46
+ adr verify
47
+ adr verify --base origin/main --format github
48
+ adr verify --staged
49
+ ```
50
+
51
+ - `new` takes `--template <name>` (default `nygard`; built-ins `nygard`, `madr`, `y-statement`, or your own `<root>/templates/<name>.md`), `--section <id>=<markdown>` (repeatable), `--supersedes <decision>` (repeatable), and `--edit`/`--no-edit`. It opens `$VISUAL` or `$EDITOR` with `--edit`, or by default in an interactive terminal; an editor failure never loses your edits and never commits content you didn't confirm.
52
+ - Writes stage their files and don't commit, unless you pass `--commit` or set `auto_commit`. A commit holds every file of the decisions it touches (body, events, template), so `adr verify --ref <commit>` is clean, and never your unrelated staged work. It refuses with exit 2 when a file it needs has unstaged changes or isn't tracked. Its `ADR: <id>` and `ADR-Event: <type>` trailers name one event each, in a fixed order, whatever your `trailer.*` config.
53
+ - `show` leads with a state header. A decision that isn't in force says so and names what to read instead, for example `SUPERSEDED by ADR-0002 (accepted 2026-10-18). Do not apply this decision; read ADR-0002 instead.`
54
+
55
+ Every command takes `--json`, `--root <dir>` (the log directory) and `--help`; reads also take `--ref <ref>`, and writes refuse it.
56
+
57
+ ## Exit codes and JSON
58
+
59
+ - **0**: success.
60
+ - **1**: failing findings from `verify`, or a write the log's rules forbid (an illegal transition, a draft that fails its template lint, an agent's acceptance, an unaccepted successor). Nothing is written, and the findings are in the output.
61
+ - **2**: bad usage (unknown command, flag or decision, a missing argument, `--ref` on a write), and config or environment errors.
62
+
63
+ With `--json`, stdout carries exactly one JSON document: `adr-list/v1`, `adr-show/v1`, `adr-write/v1` (for `new`, `accept` and `supersede`), ledger's `ledger-findings/v1` for `verify`, or `ark-error/v1` (`{ "format": "ark-error/v1", "error": { "code", "message" } }`) when the command exits 2. Every key is always present, with `null` where a value is unknown. Warnings and notes go to stderr.
64
+
65
+ ## Config (`adr.toml`)
66
+
67
+ Every key is optional.
68
+
69
+ ```toml
70
+ [log]
71
+ root = "docs/adr" # read from <repo>/adr.toml; the default
72
+ default_template = "nygard"
73
+ main_branch = "origin/main" # the ref numbers come from; default origin/HEAD, origin/main, main, …
74
+ index = "none" # validated now, used in phase 2
75
+
76
+ [policy]
77
+ accept_requires_human = true # refuse acceptances by an agent
78
+ auto_commit = false # commit writes without --commit
79
+ mcp_can_decide = false # validated now, used in phase 3
80
+
81
+ [verify]
82
+ fail_on = "error" # or "warning"
83
+ ```
84
+
85
+ The config is `<repo>/adr.toml`, whose `[log] root` chooses the log directory, or `<root>/adr.toml`. `--root` uses a repository-level file that names the same root, and overrides one that names another root with a warning.
86
+
87
+ ## Agents
88
+
89
+ An agent using the CLI sets `ARK_AGENT=<agent-id>`. Its events are recorded as the agent, acting on behalf of the git user, and with `accept_requires_human` on (the default) `adr accept` refuses them with exit 1 (`decision/agent-accepted`). The git identity comes from `user.name` and `user.email` and is never guessed.
90
+
91
+ ## Library
92
+
93
+ The CLI is a thin layer over the library, and phase 3's MCP server uses the same functions and returns the same JSON documents.
94
+
95
+ ```ts
96
+ import { listDecisions, openDecisionLog, readDecisions, showDecision, verifyDecisions } from "@nikovirtala/adr";
97
+
98
+ const log = await openDecisionLog({ cwd: process.cwd() });
99
+ const read = await readDecisions(log);
100
+ const { decisions } = listDecisions(read, { status: ["accepted"] });
101
+ const findings = await verifyDecisions(log, { type: "full" });
102
+ ```
103
+
104
+ `proposeDecision`, `acceptDecision` and `supersedeDecision` write, `resolveDecision` turns an id or number into an id, and `stateHeader` gives the header `show` prints.
105
+
106
+ `@nikovirtala/adr/kind` exports only the decision kind (`adrKind`, made with ledger's `defineKind`) and its types, without any CLI code, for tools that fold decision logs themselves, such as issues linking to decisions:
107
+
108
+ ```ts
109
+ import { adrKind } from "@nikovirtala/adr/kind";
110
+ ```
111
+
112
+ ## Not yet included
113
+
114
+ - **Reference checks are local in phase 1.** `verify` checks that `supersedes`, `superseded.by` and `linked.to` name decisions in the same log. Qualified references to other logs (`<source>:<id>`) are skipped; no phase 1 command writes them.
115
+ - **Phase 2:** `init`, `reject`, `withdraw`, `deprecate`, `note`, `link`, `scope` and `for`; git hooks; a generated index (and the number fallback for shallow clones); scope globs and code anchors; cross-log reference checks and listing across sources.
116
+ - **Phase 3:** an MCP server, a SKILL.md for agents, and search.
package/dist/cli.js ADDED
@@ -0,0 +1,470 @@
1
+ #!/usr/bin/env node
2
+ import { a as showDecision, f as resolveDecision, i as listDecisions, l as openDecisionLog, n as proposeDecision, p as unavailableReason, r as supersedeDecision, s as adrChecks, t as acceptDecision, u as readDecisions } from "./write-C3IW5WrA.js";
3
+ import { n as DECISION_STATUSES, t as adrKind } from "./kind-DqFhApu7.js";
4
+ import { createRequire } from "node:module";
5
+ import { UsageError, renderStateHeader, resolveActor, templateHistoryRef } from "@nikovirtala/ledger";
6
+ import { processIO, runCli, verifyCommand } from "@nikovirtala/ledger/cli";
7
+ import { spawn } from "node:child_process";
8
+ // src/cli/open.ts
9
+ /**
10
+ * The decision log a command works on: `--root` and `--ref` from the globals, git run with the
11
+ * CLI's environment.
12
+ */
13
+ function openForCli(io, flags, o = {}) {
14
+ return openDecisionLog({
15
+ cwd: io.cwd,
16
+ root: flags.root,
17
+ ref: flags.ref,
18
+ ...o.reader === void 0 ? {} : { reader: o.reader },
19
+ gitOptions: { env: io.env }
20
+ });
21
+ }
22
+ /**
23
+ * How other decisions display in a write's text, read again from the work tree, like the write
24
+ * result's own views: a commit on the main ref has numbered the decisions it accepted.
25
+ */
26
+ async function displayAfterWrite(log) {
27
+ const read = await readDecisions(log);
28
+ return { display: (id) => read.projection.views.get(id)?.display ?? id };
29
+ }
30
+ /** `--commit`/`--no-commit` when given, else `[policy] auto_commit`. */
31
+ function commitRequested(log, values) {
32
+ return typeof values.commit === "boolean" ? values.commit : log.adr.autoCommit;
33
+ }
34
+ // src/cli/text.ts
35
+ /** A table: each column padded to its widest value, trailing spaces removed. */
36
+ function columns(rows) {
37
+ const widths = [];
38
+ for (const row of rows) row.forEach((cell, i) => {
39
+ widths[i] = Math.max(widths[i] ?? 0, cell.length);
40
+ });
41
+ return rows.map((row) => row.map((cell, i) => i === row.length - 1 ? cell : cell.padEnd(widths[i] ?? 0)).join(" ").trimEnd()).join("\n");
42
+ }
43
+ /** `adr list`: one row per decision, `→ <effective>` when it differs from the decision. */
44
+ function listText(doc, display) {
45
+ if (doc.decisions.length === 0) return "No decisions.";
46
+ return columns(doc.decisions.map((v) => {
47
+ const arrow = v.effective === null ? "→ (cycle)" : v.effective === v.id ? "" : `→ ${display(v.effective)}`;
48
+ return [
49
+ v.number === null ? "-" : v.display,
50
+ v.id,
51
+ v.status,
52
+ v.title,
53
+ arrow
54
+ ];
55
+ }));
56
+ }
57
+ /** `ADR-0003 (m2x7p4d2)`, or the id alone when it has no number. */
58
+ function named(id, display) {
59
+ const shown = display(id);
60
+ return shown === id ? id : `${shown} (${id})`;
61
+ }
62
+ const day = (at) => (at ?? "").slice(0, 10);
63
+ const list = (ids, display) => ids.length === 0 ? "none" : ids.map((id) => named(id, display)).join(", ");
64
+ function bodyLine(v) {
65
+ if (v.body_path === null) return "missing";
66
+ if (v.body_intact === null) return v.body_path;
67
+ return `${v.body_path} (${v.body_intact ? "intact" : "changed after acceptance"})`;
68
+ }
69
+ function actorText(e) {
70
+ const a = e.actor;
71
+ return a.kind === "agent" ? `${a.name} (agent ${a.agent} for ${a.on_behalf_of})` : a.name;
72
+ }
73
+ /** The second line of an event in `show --history`: its reason, else a summary of its data. */
74
+ function eventDetail(e) {
75
+ if (typeof e.reason === "string") return `reason: ${e.reason}`;
76
+ const entries = Object.entries(e.data);
77
+ if (entries.length === 0) return null;
78
+ return entries.map(([k, v]) => `${k}: ${typeof v === "string" ? v : JSON.stringify(v)}`).join(", ");
79
+ }
80
+ function historyLines(events) {
81
+ const width = Math.max(...events.map((e) => e.type.length));
82
+ return events.flatMap((e) => {
83
+ const detail = eventDetail(e);
84
+ return [` ${e.at} ${e.type.padEnd(width)} ${actorText(e)}`, ...detail === null ? [] : [` ${detail}`]];
85
+ });
86
+ }
87
+ const SECTION_FINDING = /^(?:required )?section (.+?) (is missing|is empty|still contains its guidance text)$/;
88
+ const REASONS = {
89
+ "is missing": "missing",
90
+ "is empty": "empty",
91
+ "still contains its guidance text": "guidance text left"
92
+ };
93
+ /**
94
+ * The draft lint of `new` as advice: `Fill in N sections before accepting: A, B (why)` for the
95
+ * section findings, and one line per other finding.
96
+ */
97
+ function draftAdvice(findings) {
98
+ const headings = [];
99
+ const reasons = [];
100
+ const other = [];
101
+ for (const f of findings) {
102
+ const m = SECTION_FINDING.exec(f.message);
103
+ if (m === null) {
104
+ other.push(`Draft: ${f.message} [${f.rule}]`);
105
+ continue;
106
+ }
107
+ const [, heading = "", what = ""] = m;
108
+ const reason = REASONS[what] ?? what;
109
+ if (!headings.includes(heading)) headings.push(heading);
110
+ if (!reasons.includes(reason)) reasons.push(reason);
111
+ }
112
+ const n = headings.length;
113
+ return [...n === 0 ? [] : [`Fill in ${n === 1 ? "1 section" : `${n} sections`} before accepting: ${headings.join(", ")} (${reasons.join(", ")})`], ...other];
114
+ }
115
+ function findingLines(findings) {
116
+ return findings.map((f) => ` ${f.file === null ? "" : `${f.file}: `}${f.message} [${f.rule}]`);
117
+ }
118
+ const short = (sha) => sha.slice(0, 7);
119
+ /**
120
+ * The text of `adr new|accept|supersede` (design §5.6). `subject` names what a refused command
121
+ * was about; `number` is the decision's number in the write result, if it has one now.
122
+ */
123
+ function writeText(doc, display, o) {
124
+ const verb = {
125
+ new: "proposed",
126
+ accept: "accepted",
127
+ supersede: "superseded"
128
+ }[doc.command];
129
+ if (!doc.ok) return [`Not ${verb}: ${o.subject}`, ...findingLines(doc.findings)].join("\n");
130
+ const v = doc.decision;
131
+ if (v === null) return "";
132
+ const committed = doc.commit === null ? null : `Committed ${short(doc.commit)}`;
133
+ switch (doc.command) {
134
+ case "new": return [
135
+ `Proposed ${v.id}: ${v.title}`,
136
+ ...doc.written.map((p) => ` ${p}`),
137
+ ...draftAdvice(doc.findings),
138
+ ...committed === null ? [] : [`${committed}.`],
139
+ `Next: adr accept ${v.id}`
140
+ ].join("\n");
141
+ case "accept": return [
142
+ `Accepted ${v.id}: ${v.title}`,
143
+ ...doc.affected.map((a) => `Superseded ${named(a.id, display)}: ${a.title}`),
144
+ committed === null ? "Staged; commit to record it." : o.number === null ? `${committed}. The number is assigned when this commit lands on main.` : `${committed} as ${o.number}.`
145
+ ].join("\n");
146
+ case "supersede": return [
147
+ `Superseded ${named(v.id, display)}: ${v.title}`,
148
+ ` by ${named(v.superseded_by ?? "", display)}`,
149
+ committed === null ? "Staged; commit to record it." : `${committed}.`
150
+ ].join("\n");
151
+ }
152
+ }
153
+ /** `adr show`: the state header, the decision's fields and its body. */
154
+ function showText(doc, display) {
155
+ const v = doc.decision;
156
+ const supersedes = v.supersedes.length === 0 && v.declared_supersedes.length > 0 ? `none (declared: ${list(v.declared_supersedes, display)})` : list(v.supersedes, display);
157
+ const fields = [
158
+ ["Title", v.title],
159
+ ["Status", `${v.status} (${day(v.status_at)})`],
160
+ ["Effective", v.effective === null ? "none (supersession cycle)" : named(v.effective, display)],
161
+ ["Chain", v.chain.map(display).join(" → ")],
162
+ ["Supersedes", supersedes],
163
+ ["Applies to", v.applies_to.length === 0 ? "none" : v.applies_to.join(", ")],
164
+ ["Body", bodyLine(v)]
165
+ ];
166
+ if (doc.events === null) fields.push(["History", v.history.join(" · ")]);
167
+ const width = Math.max(...fields.map(([k]) => k.length)) + 2;
168
+ const lines = fields.map(([k, value]) => `${`${k}:`.padEnd(width)}${value}`);
169
+ if (doc.events !== null) lines.push("History:", ...historyLines(doc.events));
170
+ const body = doc.body === null ? "" : `\n${doc.body.endsWith("\n") ? doc.body : `${doc.body}\n`}`;
171
+ return `${renderStateHeader(doc.header)}\n${lines.join("\n")}\n${body}`;
172
+ }
173
+ // src/cli/accept.ts
174
+ /** `adr accept <decision> [--commit | --no-commit]` (design §5.6). */
175
+ const acceptCommand = {
176
+ name: "accept",
177
+ summary: "accept a proposed decision, superseding the decisions it declares",
178
+ usage: "accept <decision> [--commit | --no-commit]",
179
+ options: { commit: { type: "boolean" } },
180
+ positionals: {
181
+ min: 1,
182
+ max: 1,
183
+ names: ["decision"]
184
+ },
185
+ ref: "reject",
186
+ async run({ io, flags, values, positionals }) {
187
+ const input = positionals[0] ?? "";
188
+ const log = await openForCli(io, flags);
189
+ const actor = await resolveActor(log.git, { env: io.env });
190
+ const doc = await acceptDecision(log, {
191
+ id: input,
192
+ actor
193
+ }, { commit: commitRequested(log, values) });
194
+ const { display } = await displayAfterWrite(log);
195
+ const v = doc.decision;
196
+ return {
197
+ exit: doc.ok ? 0 : 1,
198
+ json: doc,
199
+ text: writeText(doc, display, {
200
+ subject: v === null ? input.trim() : `${display(v.id)}: ${v.title}`,
201
+ number: v === null || doc.commit === null || v.number === null ? null : v.display
202
+ }),
203
+ notes: doc.notes
204
+ };
205
+ }
206
+ };
207
+ // src/cli/list.ts
208
+ const plural = (n, one, many) => n === 1 ? `1 ${one}` : `${n} ${many}`;
209
+ function statuses(values) {
210
+ return (Array.isArray(values.status) ? values.status : []).map((s) => {
211
+ if (!DECISION_STATUSES.includes(s)) throw new UsageError(`unknown status ${JSON.stringify(s)}; use one of ${DECISION_STATUSES.join(", ")}`);
212
+ return s;
213
+ });
214
+ }
215
+ /** `adr list [--status <status>]…` (design §5.6). */
216
+ const listCommand = {
217
+ name: "list",
218
+ summary: "list decisions with their computed status",
219
+ usage: "list [--status <status>]…",
220
+ options: { status: {
221
+ type: "string",
222
+ multiple: true
223
+ } },
224
+ positionals: {
225
+ min: 0,
226
+ max: 0,
227
+ names: []
228
+ },
229
+ ref: "read",
230
+ async run({ io, flags, values }) {
231
+ const status = statuses(values);
232
+ const read = await readDecisions(await openForCli(io, flags));
233
+ const doc = listDecisions(read, { status });
234
+ const notes = [...doc.notes];
235
+ if (!read.numbering.available && doc.decisions.length > 0) notes.push(`numbers are unavailable (${unavailableReason(read.numbering)}); decisions display by id`);
236
+ if (doc.findings.length > 0) notes.push(`the decision log has ${plural(doc.findings.length, "problem", "problems")}; run adr verify`);
237
+ if (doc.unreadable.length > 0) notes.push(`${plural(doc.unreadable.length, "decision uses", "decisions use")} a newer format; upgrade adr`);
238
+ if (doc.unfoldable.length > 0) notes.push(`${plural(doc.unfoldable.length, "decision", "decisions")} can't be read; run adr verify`);
239
+ const display = (id) => read.projection.views.get(id)?.display ?? id;
240
+ return {
241
+ exit: 0,
242
+ json: doc,
243
+ text: listText(doc, display),
244
+ notes
245
+ };
246
+ }
247
+ };
248
+ // src/cli/editor.ts
249
+ /**
250
+ * The user's editor as an `edit` callback (design §5.7), or `null` when neither `VISUAL` nor
251
+ * `EDITOR` is set. The editor string is run by `sh -c` the way git runs `core.editor`, with the
252
+ * path as the positional parameter `$1`, never interpolated. When stdout isn't a TTY (piped
253
+ * `--json`, say), the editor's stdout goes to stderr so the CLI's stdout stays one document.
254
+ * The callback never throws: a spawn error or a non-zero exit becomes the note.
255
+ */
256
+ function editorFor(env, o) {
257
+ const editor = [env.VISUAL, env.EDITOR].map((v) => v?.trim() ?? "").find((v) => v !== "");
258
+ if (editor === void 0) return null;
259
+ return (absPath) => new Promise((resolve) => {
260
+ const fail = (e) => resolve(`could not start the editor: ${e instanceof Error ? e.message : String(e)}`);
261
+ try {
262
+ const child = spawn("sh", [
263
+ "-c",
264
+ `${editor} "$1"`,
265
+ "sh",
266
+ absPath
267
+ ], {
268
+ env,
269
+ stdio: o.stdoutIsTTY ? "inherit" : [
270
+ "inherit",
271
+ 2,
272
+ "inherit"
273
+ ]
274
+ });
275
+ child.on("error", fail);
276
+ child.on("close", (code, signal) => {
277
+ if (code === 0) resolve(null);
278
+ else resolve(`the editor ${code === null ? `was killed by ${signal ?? "a signal"}` : `exited with status ${code}`}; the draft is staged as rendered`);
279
+ });
280
+ } catch (e) {
281
+ fail(e);
282
+ }
283
+ });
284
+ }
285
+ // src/cli/new.ts
286
+ const strings = (v) => Array.isArray(v) ? v : [];
287
+ /** `--section <id>=<markdown>` values, split at the first `=`; each id at most once. */
288
+ function sections(values) {
289
+ const out = {};
290
+ for (const value of values) {
291
+ const at = value.indexOf("=");
292
+ const id = at < 0 ? "" : value.slice(0, at).trim();
293
+ if (id === "") throw new UsageError(`--section needs <id>=<markdown>, not ${JSON.stringify(value)}`);
294
+ if (Object.hasOwn(out, id)) throw new UsageError(`--section ${id} is given twice`);
295
+ out[id] = value.slice(at + 1);
296
+ }
297
+ return out;
298
+ }
299
+ /**
300
+ * `adr new <title> …` (design §5.6). The editor runs with `--edit`, or without `--no-edit` when
301
+ * the terminal is interactive and an editor is set.
302
+ */
303
+ const newCommand = {
304
+ name: "new",
305
+ summary: "propose a decision from a template and stage it",
306
+ usage: "new <title> [--template <name>] [--supersedes <decision>]… [--section <id>=<markdown>]… [--edit | --no-edit] [--commit | --no-commit]",
307
+ options: {
308
+ template: { type: "string" },
309
+ supersedes: {
310
+ type: "string",
311
+ multiple: true
312
+ },
313
+ section: {
314
+ type: "string",
315
+ multiple: true
316
+ },
317
+ edit: { type: "boolean" },
318
+ commit: { type: "boolean" }
319
+ },
320
+ positionals: {
321
+ min: 1,
322
+ max: 1,
323
+ names: ["title"]
324
+ },
325
+ ref: "reject",
326
+ async run({ io, flags, values, positionals }) {
327
+ const title = positionals[0] ?? "";
328
+ const given = sections(strings(values.section));
329
+ const log = await openForCli(io, flags);
330
+ const actor = await resolveActor(log.git, { env: io.env });
331
+ const requested = commitRequested(log, values);
332
+ const notes = [];
333
+ const editor = editorFor(io.env, { stdoutIsTTY: io.stdoutIsTTY });
334
+ const wantsEdit = values.edit === true || values.edit === void 0 && io.interactive;
335
+ if (values.edit === true && editor === null) notes.push("no editor: set VISUAL or EDITOR");
336
+ const edit = wantsEdit && editor !== null ? async (abs) => {
337
+ const note = await editor(abs);
338
+ if (note !== null) notes.push(note);
339
+ return note;
340
+ } : void 0;
341
+ const doc = await proposeDecision(log, {
342
+ title,
343
+ sections: given,
344
+ supersedes: strings(values.supersedes),
345
+ actor,
346
+ ...typeof values.template === "string" ? { template: values.template } : {}
347
+ }, {
348
+ commit: requested,
349
+ ...edit === void 0 ? {} : { edit },
350
+ onNote: (note) => notes.push(requested ? `not committed: ${note}` : note)
351
+ });
352
+ const { display } = await displayAfterWrite(log);
353
+ const v = doc.decision;
354
+ return {
355
+ exit: doc.ok ? 0 : 1,
356
+ json: doc,
357
+ text: writeText(doc, display, {
358
+ subject: title.trim(),
359
+ number: v === null || v.number === null ? null : v.display
360
+ }),
361
+ notes: [...doc.notes, ...notes]
362
+ };
363
+ }
364
+ };
365
+ // src/cli/show.ts
366
+ /** `adr show <decision> [--history]` (design §5.6). */
367
+ const showCommand = {
368
+ name: "show",
369
+ summary: "show a decision with its state header, chain, history and body",
370
+ usage: "show <decision> [--history]",
371
+ options: { history: { type: "boolean" } },
372
+ positionals: {
373
+ min: 1,
374
+ max: 1,
375
+ names: ["decision"]
376
+ },
377
+ ref: "read",
378
+ async run({ io, flags, values, positionals }) {
379
+ const read = await readDecisions(await openForCli(io, flags));
380
+ const id = resolveDecision(read, positionals[0] ?? "");
381
+ const doc = await showDecision(read, id, { history: values.history === true });
382
+ const display = (s) => read.projection.views.get(s)?.display ?? s;
383
+ return {
384
+ exit: 0,
385
+ json: doc,
386
+ text: showText(doc, display),
387
+ notes: doc.notes
388
+ };
389
+ }
390
+ };
391
+ // src/cli/supersede.ts
392
+ const USAGE = "supersede <decision> --by <decision> [--reason <text>] [--commit | --no-commit]";
393
+ /** `adr supersede <decision> --by <decision> …` (design §5.6). */
394
+ const supersedeCommand = {
395
+ name: "supersede",
396
+ summary: "mark an accepted decision as superseded by another accepted one",
397
+ usage: USAGE,
398
+ options: {
399
+ by: { type: "string" },
400
+ reason: { type: "string" },
401
+ commit: { type: "boolean" }
402
+ },
403
+ positionals: {
404
+ min: 1,
405
+ max: 1,
406
+ names: ["decision"]
407
+ },
408
+ ref: "reject",
409
+ async run({ io, flags, values, positionals }) {
410
+ const input = positionals[0] ?? "";
411
+ if (typeof values.by !== "string") throw new UsageError(`missing --by <decision>; usage: adr ${USAGE}`);
412
+ const log = await openForCli(io, flags);
413
+ const actor = await resolveActor(log.git, { env: io.env });
414
+ const doc = await supersedeDecision(log, {
415
+ id: input,
416
+ by: values.by,
417
+ actor,
418
+ ...typeof values.reason === "string" ? { reason: values.reason } : {}
419
+ }, { commit: commitRequested(log, values) });
420
+ const { display } = await displayAfterWrite(log);
421
+ const v = doc.decision;
422
+ return {
423
+ exit: doc.ok ? 0 : 1,
424
+ json: doc,
425
+ text: writeText(doc, display, {
426
+ subject: v === null ? input.trim() : `${display(v.id)}: ${v.title}`,
427
+ number: null
428
+ }),
429
+ notes: doc.notes
430
+ };
431
+ }
432
+ };
433
+ // src/cli/commands.ts
434
+ /**
435
+ * ledger's shared `verify`: the log as the mode reads it (the index for `--staged`, else the
436
+ * `--ref` tree or the work tree), every phase-1 check, and `[verify] fail_on`.
437
+ */
438
+ const verify = verifyCommand({ async open({ io, flags, mode }) {
439
+ const log = await openForCli(io, flags, { reader: mode.type === "staged" ? "index" : "worktree" });
440
+ return {
441
+ git: log.git,
442
+ kind: adrKind,
443
+ layout: log.layout,
444
+ checks: adrChecks(log, await templateHistoryRef(log.git, mode)),
445
+ failOn: log.adr.failOn,
446
+ notes: log.notes
447
+ };
448
+ } });
449
+ /** The `adr` tool: its commands, for ledger's `runCli`. */
450
+ function adrCli(version) {
451
+ return {
452
+ name: "adr",
453
+ version,
454
+ summary: "Architecture decision records kept as event logs in git.",
455
+ commands: [
456
+ newCommand,
457
+ acceptCommand,
458
+ supersedeCommand,
459
+ listCommand,
460
+ showCommand,
461
+ verify
462
+ ]
463
+ };
464
+ }
465
+ // src/cli.ts
466
+ const { version } = createRequire(import.meta.url)("../package.json");
467
+ process.exitCode = await runCli(adrCli(version), processIO());
468
+ export {};
469
+
470
+ //# sourceMappingURL=cli.js.map