@nikovirtala/adr 0.1.0 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +47 -7
- package/dist/cli.js +634 -47
- package/dist/cli.js.map +1 -1
- package/dist/index.js +3 -3
- package/dist/{kind-DqFhApu7.js → kind-BcHjCHvi.js} +9 -18
- package/dist/kind-BcHjCHvi.js.map +1 -0
- package/dist/kind.js +1 -1
- package/dist/types/cli/accept.d.ts +2 -2
- package/dist/types/cli/deprecate.d.ts +3 -0
- package/dist/types/cli/for.d.ts +3 -0
- package/dist/types/cli/init.d.ts +3 -0
- package/dist/types/cli/link.d.ts +3 -0
- package/dist/types/cli/new.d.ts +1 -1
- package/dist/types/cli/note.d.ts +3 -0
- package/dist/types/cli/open.d.ts +0 -2
- package/dist/types/cli/reject.d.ts +3 -0
- package/dist/types/cli/scope.d.ts +7 -0
- package/dist/types/cli/supersede.d.ts +1 -1
- package/dist/types/cli/text.d.ts +33 -3
- package/dist/types/cli/withdraw.d.ts +3 -0
- package/dist/types/config.d.ts +9 -1
- package/dist/types/index.d.ts +3 -1
- package/dist/types/init.d.ts +27 -0
- package/dist/types/scope.d.ts +51 -0
- package/dist/types/views.d.ts +2 -0
- package/dist/types/write.d.ts +179 -17
- package/dist/write-9IaqHRrY.js +1986 -0
- package/dist/write-9IaqHRrY.js.map +1 -0
- package/package.json +2 -2
- package/dist/kind-DqFhApu7.js.map +0 -1
- package/dist/write-C3IW5WrA.js +0 -1011
- package/dist/write-C3IW5WrA.js.map +0 -1
package/dist/cli.js
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import {
|
|
3
|
-
import { n as
|
|
2
|
+
import { n as DECISION_STATUSES, t as adrKind } from "./kind-BcHjCHvi.js";
|
|
3
|
+
import { C as initDecisionLog, S as unavailableReason, a as noteDecision, c as scopeDecision, d as withdrawDecision, f as adrChecks, g as showDecision, h as listDecisions, i as linkDecision, m as decisionsFor, n as deprecateDecision, o as proposeDecision, r as followScopeRenames, s as rejectDecision, t as acceptDecision, u as supersedeDecision, v as openDecisionLog, x as resolveDecision, y as readDecisions } from "./write-9IaqHRrY.js";
|
|
4
4
|
import { createRequire } from "node:module";
|
|
5
|
-
import { UsageError, renderStateHeader,
|
|
6
|
-
import { processIO, runCli, verifyCommand } from "@nikovirtala/ledger/cli";
|
|
5
|
+
import { UsageError, renderStateHeader, resolveAttribution, stagedRenames, templateHistoryRef } from "@nikovirtala/ledger";
|
|
6
|
+
import { COMMIT_OPTIONS, commitChoice, confirm, processIO, runCli, verifyCommand } from "@nikovirtala/ledger/cli";
|
|
7
7
|
import { spawn } from "node:child_process";
|
|
8
8
|
// src/cli/open.ts
|
|
9
9
|
/**
|
|
@@ -27,10 +27,6 @@ async function displayAfterWrite(log) {
|
|
|
27
27
|
const read = await readDecisions(log);
|
|
28
28
|
return { display: (id) => read.projection.views.get(id)?.display ?? id };
|
|
29
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
30
|
// src/cli/text.ts
|
|
35
31
|
/** A table: each column padded to its widest value, trailing spaces removed. */
|
|
36
32
|
function columns(rows) {
|
|
@@ -90,18 +86,15 @@ const REASONS = {
|
|
|
90
86
|
"is empty": "empty",
|
|
91
87
|
"still contains its guidance text": "guidance text left"
|
|
92
88
|
};
|
|
93
|
-
/**
|
|
94
|
-
|
|
95
|
-
* section findings, and one line per other finding.
|
|
96
|
-
*/
|
|
97
|
-
function draftAdvice(findings) {
|
|
89
|
+
/** The section findings of a draft lint as headings and reasons, and the other findings. */
|
|
90
|
+
function sectionGaps(findings) {
|
|
98
91
|
const headings = [];
|
|
99
92
|
const reasons = [];
|
|
100
93
|
const other = [];
|
|
101
94
|
for (const f of findings) {
|
|
102
95
|
const m = SECTION_FINDING.exec(f.message);
|
|
103
96
|
if (m === null) {
|
|
104
|
-
other.push(
|
|
97
|
+
other.push(f);
|
|
105
98
|
continue;
|
|
106
99
|
}
|
|
107
100
|
const [, heading = "", what = ""] = m;
|
|
@@ -109,47 +102,138 @@ function draftAdvice(findings) {
|
|
|
109
102
|
if (!headings.includes(heading)) headings.push(heading);
|
|
110
103
|
if (!reasons.includes(reason)) reasons.push(reason);
|
|
111
104
|
}
|
|
105
|
+
return {
|
|
106
|
+
headings,
|
|
107
|
+
reasons,
|
|
108
|
+
other
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
/**
|
|
112
|
+
* The draft lint of `new` as advice: `Fill in N sections before accepting: A, B (why)` for the
|
|
113
|
+
* section findings, and one line per other finding.
|
|
114
|
+
*/
|
|
115
|
+
function draftAdvice(findings) {
|
|
116
|
+
const { headings, reasons, other } = sectionGaps(findings);
|
|
117
|
+
const n = headings.length;
|
|
118
|
+
return [...n === 0 ? [] : [`Fill in ${n === 1 ? "1 section" : `${n} sections`} before accepting: ${headings.join(", ")} (${reasons.join(", ")})`], ...other.map((f) => `Draft: ${f.message} [${f.rule}]`)];
|
|
119
|
+
}
|
|
120
|
+
/**
|
|
121
|
+
* The stderr warning of `new` when the draft it wrote has unfilled sections (design §7.2), or
|
|
122
|
+
* `null` when there are none.
|
|
123
|
+
*/
|
|
124
|
+
function unfilledWarning(doc) {
|
|
125
|
+
if (doc.command !== "new" || !doc.ok || doc.written.length === 0) return null;
|
|
126
|
+
const { headings, reasons } = sectionGaps(doc.findings);
|
|
112
127
|
const n = headings.length;
|
|
113
|
-
|
|
128
|
+
const path = doc.findings.find((f) => f.file !== null)?.file ?? doc.decision?.body_path ?? null;
|
|
129
|
+
if (n === 0 || path === null) return null;
|
|
130
|
+
return `${path}: fill in ${n === 1 ? "1 required section" : `${n} required sections`} before committing or accepting: ${headings.join(", ")} (${reasons.join(", ")}); adr verify reports this until then`;
|
|
114
131
|
}
|
|
115
132
|
function findingLines(findings) {
|
|
116
133
|
return findings.map((f) => ` ${f.file === null ? "" : `${f.file}: `}${f.message} [${f.rule}]`);
|
|
117
134
|
}
|
|
118
135
|
const short = (sha) => sha.slice(0, 7);
|
|
119
136
|
/**
|
|
120
|
-
*
|
|
121
|
-
*
|
|
137
|
+
* `adr scope --follow-renames`: one `<named>: <old globs> → <new globs>` line per changed
|
|
138
|
+
* decision (`affected`), whether or not a decision was given.
|
|
139
|
+
*/
|
|
140
|
+
function followRenamesText(doc, display, o) {
|
|
141
|
+
if (!o.renamed) return "No staged renames.";
|
|
142
|
+
if (doc.affected.length === 0) return "No scope globs follow the staged renames.";
|
|
143
|
+
return [...doc.affected.map((v) => {
|
|
144
|
+
const before = o.before.get(v.id) ?? [];
|
|
145
|
+
const old = before.filter((g) => !v.applies_to.includes(g));
|
|
146
|
+
const now = v.applies_to.filter((g) => !before.includes(g));
|
|
147
|
+
const change = old.length === 0 ? `+ ${now.join(", ")}` : `${old.join(", ")} → ${now.join(", ")}`;
|
|
148
|
+
return `${named(v.id, display)}: ${change}`;
|
|
149
|
+
}), "Staged; commit the scoped events with the renames."].join("\n");
|
|
150
|
+
}
|
|
151
|
+
/** The past participle of each write command, for `Not <verb>: …`. */
|
|
152
|
+
const VERBS = {
|
|
153
|
+
new: "proposed",
|
|
154
|
+
accept: "accepted",
|
|
155
|
+
supersede: "superseded",
|
|
156
|
+
reject: "rejected",
|
|
157
|
+
withdraw: "withdrawn",
|
|
158
|
+
deprecate: "deprecated",
|
|
159
|
+
note: "noted",
|
|
160
|
+
link: "linked",
|
|
161
|
+
scope: "scoped"
|
|
162
|
+
};
|
|
163
|
+
/**
|
|
164
|
+
* The text of a write command (design §5.6, §6.1). `subject` names what a refused command was
|
|
165
|
+
* about; `number` is the decision's number in the write result, if it has one now; `reason` is
|
|
166
|
+
* the reason a lifecycle exit recorded.
|
|
122
167
|
*/
|
|
123
168
|
function writeText(doc, display, o) {
|
|
124
|
-
const verb =
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
169
|
+
const verb = VERBS[doc.command];
|
|
170
|
+
if (!doc.ok) {
|
|
171
|
+
const unfilled = doc.command === "new" && sectionGaps(doc.findings).headings.length > 0;
|
|
172
|
+
return [
|
|
173
|
+
`Not ${verb}: ${o.subject}`,
|
|
174
|
+
...findingLines(doc.findings),
|
|
175
|
+
...unfilled ? ["Fill them in with --section <id>=<markdown>, or run without --commit and edit the draft."] : []
|
|
176
|
+
].join("\n");
|
|
177
|
+
}
|
|
178
|
+
if (o.followRenames !== void 0) return followRenamesText(doc, display, o.followRenames);
|
|
130
179
|
const v = doc.decision;
|
|
131
180
|
if (v === null) return "";
|
|
132
181
|
const committed = doc.commit === null ? null : `Committed ${short(doc.commit)}`;
|
|
182
|
+
const recorded = committed === null ? "Staged; commit to record it." : `${committed}.`;
|
|
133
183
|
switch (doc.command) {
|
|
134
184
|
case "new": return [
|
|
135
185
|
`Proposed ${v.id}: ${v.title}`,
|
|
136
186
|
...doc.written.map((p) => ` ${p}`),
|
|
137
187
|
...draftAdvice(doc.findings),
|
|
138
188
|
...committed === null ? [] : [`${committed}.`],
|
|
139
|
-
`Next: adr accept ${v.id}`
|
|
189
|
+
o.actor?.kind === "agent" && o.acceptRequiresHuman === true ? `Next: ask a human to review and accept it (adr accept ${v.id})` : `Next: adr accept ${v.id}`
|
|
140
190
|
].join("\n");
|
|
141
191
|
case "accept": return [
|
|
142
192
|
`Accepted ${v.id}: ${v.title}`,
|
|
143
193
|
...doc.affected.map((a) => `Superseded ${named(a.id, display)}: ${a.title}`),
|
|
194
|
+
...o.copied === void 0 || o.copied === null ? [] : [`Copied scope from ${o.copied.displays.join(", ")}: ${o.copied.globs.join(", ")}`],
|
|
144
195
|
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
196
|
].join("\n");
|
|
146
197
|
case "supersede": return [
|
|
147
198
|
`Superseded ${named(v.id, display)}: ${v.title}`,
|
|
148
199
|
` by ${named(v.superseded_by ?? "", display)}`,
|
|
149
|
-
|
|
200
|
+
recorded
|
|
201
|
+
].join("\n");
|
|
202
|
+
case "reject":
|
|
203
|
+
case "withdraw":
|
|
204
|
+
case "deprecate": return [
|
|
205
|
+
`${verb.charAt(0).toUpperCase()}${verb.slice(1)} ${named(v.id, display)}: ${v.title}`,
|
|
206
|
+
...o.reason === void 0 || o.reason === null ? [] : [` reason: ${o.reason}`],
|
|
207
|
+
recorded
|
|
150
208
|
].join("\n");
|
|
209
|
+
case "note": return [`Noted on ${named(v.id, display)}: ${v.title}`, recorded].join("\n");
|
|
210
|
+
case "link": {
|
|
211
|
+
const link = doc.written.length === 0 ? void 0 : v.links.at(-1);
|
|
212
|
+
if (link === void 0) return `Links of ${named(v.id, display)} unchanged`;
|
|
213
|
+
return [`Linked ${named(v.id, display)} ${link.rel} ${named(link.to, display)}`, recorded].join("\n");
|
|
214
|
+
}
|
|
215
|
+
case "scope": {
|
|
216
|
+
const before = new Set(o.scopeBefore ?? []);
|
|
217
|
+
const after = new Set(v.applies_to);
|
|
218
|
+
const globs = v.applies_to.length === 0 ? "no globs" : v.applies_to.join(", ");
|
|
219
|
+
if (doc.written.length === 0) return `Scope of ${named(v.id, display)} unchanged: ${globs}`;
|
|
220
|
+
return [
|
|
221
|
+
`Scope of ${named(v.id, display)}: ${v.title}`,
|
|
222
|
+
...v.applies_to.filter((g) => !before.has(g)).map((g) => ` + ${g}`),
|
|
223
|
+
...[...before].filter((g) => !after.has(g)).map((g) => ` - ${g}`),
|
|
224
|
+
` = ${globs}`,
|
|
225
|
+
recorded
|
|
226
|
+
].join("\n");
|
|
227
|
+
}
|
|
151
228
|
}
|
|
152
229
|
}
|
|
230
|
+
/**
|
|
231
|
+
* "Applies to" without globs: nothing is recorded, which isn't "governs nothing" (design §7.3),
|
|
232
|
+
* with the command that adds globs while the decision can still take them.
|
|
233
|
+
*/
|
|
234
|
+
function noGlobs(v, display) {
|
|
235
|
+
return adrKind.machine.allowed(v.status, "scoped") ? `no globs recorded; add them with adr scope ${display(v.id)} --add <glob>` : "no globs recorded";
|
|
236
|
+
}
|
|
153
237
|
/** `adr show`: the state header, the decision's fields and its body. */
|
|
154
238
|
function showText(doc, display) {
|
|
155
239
|
const v = doc.decision;
|
|
@@ -160,7 +244,7 @@ function showText(doc, display) {
|
|
|
160
244
|
["Effective", v.effective === null ? "none (supersession cycle)" : named(v.effective, display)],
|
|
161
245
|
["Chain", v.chain.map(display).join(" → ")],
|
|
162
246
|
["Supersedes", supersedes],
|
|
163
|
-
["Applies to", v.applies_to.length === 0 ?
|
|
247
|
+
["Applies to", v.applies_to.length === 0 ? noGlobs(v, display) : v.applies_to.join(", ")],
|
|
164
248
|
["Body", bodyLine(v)]
|
|
165
249
|
];
|
|
166
250
|
if (doc.events === null) fields.push(["History", v.history.join(" · ")]);
|
|
@@ -170,13 +254,81 @@ function showText(doc, display) {
|
|
|
170
254
|
const body = doc.body === null ? "" : `\n${doc.body.endsWith("\n") ? doc.body : `${doc.body}\n`}`;
|
|
171
255
|
return `${renderStateHeader(doc.header)}\n${lines.join("\n")}\n${body}`;
|
|
172
256
|
}
|
|
257
|
+
/**
|
|
258
|
+
* `adr for`: per path, one row per decision (named, status, title, how it matched), its warning
|
|
259
|
+
* below it when it isn't in force, then the anchors that name no decision of this log.
|
|
260
|
+
*/
|
|
261
|
+
function forText(doc, display, keyword) {
|
|
262
|
+
const lines = [];
|
|
263
|
+
for (const p of doc.paths) {
|
|
264
|
+
lines.push(p.tracked ? p.path : `${p.path} (not tracked)`);
|
|
265
|
+
if (p.decisions.length === 0 && p.unresolved.length === 0) lines.push(" no decisions");
|
|
266
|
+
const rows = columns(p.decisions.map((d) => [
|
|
267
|
+
named(d.id, display),
|
|
268
|
+
d.status,
|
|
269
|
+
d.title,
|
|
270
|
+
d.via.map((v) => v.type === "glob" ? `glob ${v.glob}` : `anchor line ${v.line}`).join(", ")
|
|
271
|
+
])).split("\n");
|
|
272
|
+
p.decisions.forEach((d, i) => {
|
|
273
|
+
lines.push(` ${rows[i] ?? ""}`);
|
|
274
|
+
if (d.warning !== null) lines.push(` ${d.warning}`);
|
|
275
|
+
});
|
|
276
|
+
for (const u of p.unresolved) {
|
|
277
|
+
const why = u.reason === "other-log" ? "in another log" : "not in this log";
|
|
278
|
+
lines.push(` unresolved: ${keyword}:${u.ref} (line ${u.line}, ${why})`);
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
return lines.join("\n");
|
|
282
|
+
}
|
|
283
|
+
/** `adr init`: what was written and kept, then the next step; or that nothing was needed. */
|
|
284
|
+
function initText(doc) {
|
|
285
|
+
if (doc.written.length === 0) return `Already initialized: ${doc.root} (kept ${doc.kept.join(", ")})`;
|
|
286
|
+
return [
|
|
287
|
+
`Initialized ${doc.root}`,
|
|
288
|
+
...doc.written.map((p) => ` ${p}`),
|
|
289
|
+
...doc.kept.map((p) => `Kept ${p}`),
|
|
290
|
+
...doc.commit === null ? [] : [`Committed ${short(doc.commit)}.`],
|
|
291
|
+
"Next: adr new \"<title>\""
|
|
292
|
+
].join("\n");
|
|
293
|
+
}
|
|
173
294
|
// src/cli/accept.ts
|
|
174
|
-
|
|
295
|
+
const MAX_LISTED = 5;
|
|
296
|
+
/** `Copy the scope of ADR-0002 (src/a/**, src/b/**) to k3f9q8a1? [y/N] ` (design §6.10). */
|
|
297
|
+
function question(id, offer) {
|
|
298
|
+
const shown = offer.globs.slice(0, MAX_LISTED).join(", ");
|
|
299
|
+
const more = offer.globs.length - MAX_LISTED;
|
|
300
|
+
const globs = more > 0 ? `${shown} and ${more} more` : shown;
|
|
301
|
+
const ds = offer.displays;
|
|
302
|
+
return `Copy the scope of ${ds.length === 1 ? ds[0] : `${ds.slice(0, -1).join(", ")} and ${ds.at(-1)}`} (${ds.length === 1 ? globs : `${offer.globs.length} globs: ${globs}`}) to ${id}? [y/N] `;
|
|
303
|
+
}
|
|
304
|
+
/**
|
|
305
|
+
* The flags decide; without them a person at a terminal is asked, and everyone else (scripts,
|
|
306
|
+
* hooks, CI, `--json`) gets the note with the `adr scope` command (design §6.10, D-e). `seen`
|
|
307
|
+
* receives the offer that was copied, for the text.
|
|
308
|
+
*/
|
|
309
|
+
function copyChoice(io, json, value, seen) {
|
|
310
|
+
const copy = async (offer) => {
|
|
311
|
+
seen(offer);
|
|
312
|
+
return true;
|
|
313
|
+
};
|
|
314
|
+
if (value === true) return copy;
|
|
315
|
+
if (value === false) return "skip";
|
|
316
|
+
if (json || !io.interactive) return "suggest";
|
|
317
|
+
return async (offer) => {
|
|
318
|
+
const yes = await confirm(io, question(offer.id, offer), { json });
|
|
319
|
+
if (yes) seen(offer);
|
|
320
|
+
return yes;
|
|
321
|
+
};
|
|
322
|
+
}
|
|
323
|
+
/** `adr accept <decision> [--copy-scope | --no-copy-scope] …` (design §5.6, §6.10). */
|
|
175
324
|
const acceptCommand = {
|
|
176
325
|
name: "accept",
|
|
177
326
|
summary: "accept a proposed decision, superseding the decisions it declares",
|
|
178
|
-
usage: "accept <decision> [--commit | --no-commit]",
|
|
179
|
-
options: {
|
|
327
|
+
usage: "accept <decision> [--copy-scope | --no-copy-scope] [--commit | --no-commit] [-m <message>]",
|
|
328
|
+
options: {
|
|
329
|
+
"copy-scope": { type: "boolean" },
|
|
330
|
+
...COMMIT_OPTIONS
|
|
331
|
+
},
|
|
180
332
|
positionals: {
|
|
181
333
|
min: 1,
|
|
182
334
|
max: 1,
|
|
@@ -186,11 +338,171 @@ const acceptCommand = {
|
|
|
186
338
|
async run({ io, flags, values, positionals }) {
|
|
187
339
|
const input = positionals[0] ?? "";
|
|
188
340
|
const log = await openForCli(io, flags);
|
|
189
|
-
const actor = await
|
|
341
|
+
const { actor, notes: attribution } = await resolveAttribution(log.git, { env: io.env });
|
|
342
|
+
const seen = { offer: null };
|
|
343
|
+
const copyScope = copyChoice(io, flags.json, values["copy-scope"], (offer) => {
|
|
344
|
+
seen.offer = offer;
|
|
345
|
+
});
|
|
190
346
|
const doc = await acceptDecision(log, {
|
|
191
347
|
id: input,
|
|
192
348
|
actor
|
|
193
|
-
}, {
|
|
349
|
+
}, {
|
|
350
|
+
...commitChoice(values, { autoCommit: log.adr.autoCommit }),
|
|
351
|
+
notes: attribution,
|
|
352
|
+
copyScope
|
|
353
|
+
});
|
|
354
|
+
const { display } = await displayAfterWrite(log);
|
|
355
|
+
const v = doc.decision;
|
|
356
|
+
return {
|
|
357
|
+
exit: doc.ok ? 0 : 1,
|
|
358
|
+
json: doc,
|
|
359
|
+
text: writeText(doc, display, {
|
|
360
|
+
subject: v === null ? input.trim() : `${display(v.id)}: ${v.title}`,
|
|
361
|
+
number: v === null || doc.commit === null || v.number === null ? null : v.display,
|
|
362
|
+
copied: doc.ok ? seen.offer : null
|
|
363
|
+
}),
|
|
364
|
+
notes: doc.notes
|
|
365
|
+
};
|
|
366
|
+
}
|
|
367
|
+
};
|
|
368
|
+
// src/cli/deprecate.ts
|
|
369
|
+
const USAGE$4 = "deprecate <decision> --reason <text> [--commit | --no-commit] [-m <message>]";
|
|
370
|
+
/** `adr deprecate` (design §6.5): an accepted decision no longer in force. */
|
|
371
|
+
const deprecateCommand = {
|
|
372
|
+
name: "deprecate",
|
|
373
|
+
summary: "deprecate an accepted decision",
|
|
374
|
+
usage: USAGE$4,
|
|
375
|
+
options: {
|
|
376
|
+
reason: { type: "string" },
|
|
377
|
+
...COMMIT_OPTIONS
|
|
378
|
+
},
|
|
379
|
+
positionals: {
|
|
380
|
+
min: 1,
|
|
381
|
+
max: 1,
|
|
382
|
+
names: ["decision"]
|
|
383
|
+
},
|
|
384
|
+
ref: "reject",
|
|
385
|
+
async run({ io, flags, values, positionals }) {
|
|
386
|
+
const input = positionals[0] ?? "";
|
|
387
|
+
if (typeof values.reason !== "string" || values.reason.trim() === "") throw new UsageError(`missing --reason <text>; usage: adr ${USAGE$4}`);
|
|
388
|
+
const reason = typeof values.reason === "string" ? values.reason.trim() : null;
|
|
389
|
+
const log = await openForCli(io, flags);
|
|
390
|
+
const { actor, notes } = await resolveAttribution(log.git, { env: io.env });
|
|
391
|
+
const doc = await deprecateDecision(log, {
|
|
392
|
+
id: input,
|
|
393
|
+
reason,
|
|
394
|
+
actor
|
|
395
|
+
}, {
|
|
396
|
+
...commitChoice(values, { autoCommit: log.adr.autoCommit }),
|
|
397
|
+
notes
|
|
398
|
+
});
|
|
399
|
+
const { display } = await displayAfterWrite(log);
|
|
400
|
+
const v = doc.decision;
|
|
401
|
+
return {
|
|
402
|
+
exit: doc.ok ? 0 : 1,
|
|
403
|
+
json: doc,
|
|
404
|
+
text: writeText(doc, display, {
|
|
405
|
+
subject: v === null ? input.trim() : `${display(v.id)}: ${v.title}`,
|
|
406
|
+
number: null,
|
|
407
|
+
reason
|
|
408
|
+
}),
|
|
409
|
+
notes: doc.notes
|
|
410
|
+
};
|
|
411
|
+
}
|
|
412
|
+
};
|
|
413
|
+
// src/cli/for.ts
|
|
414
|
+
/** `adr for <path>… [--ref <ref>]` (design §6.9): the decisions that govern each file. */
|
|
415
|
+
const forCommand = {
|
|
416
|
+
name: "for",
|
|
417
|
+
summary: "list the decisions that govern files, by scope glob and code anchor",
|
|
418
|
+
usage: "for <path>…",
|
|
419
|
+
options: {},
|
|
420
|
+
positionals: {
|
|
421
|
+
min: 1,
|
|
422
|
+
max: 100,
|
|
423
|
+
names: ["path"]
|
|
424
|
+
},
|
|
425
|
+
ref: "read",
|
|
426
|
+
async run({ io, flags, positionals }) {
|
|
427
|
+
const log = await openForCli(io, flags);
|
|
428
|
+
const read = await readDecisions(log);
|
|
429
|
+
const doc = await decisionsFor(read, log, {
|
|
430
|
+
paths: positionals,
|
|
431
|
+
cwd: io.cwd
|
|
432
|
+
});
|
|
433
|
+
const display = (id) => read.projection.views.get(id)?.display ?? id;
|
|
434
|
+
return {
|
|
435
|
+
exit: 0,
|
|
436
|
+
json: doc,
|
|
437
|
+
text: forText(doc, display, read.adr.anchorKeyword),
|
|
438
|
+
notes: doc.notes
|
|
439
|
+
};
|
|
440
|
+
}
|
|
441
|
+
};
|
|
442
|
+
// src/cli/init.ts
|
|
443
|
+
/** `adr init [--template <name>]… [--commit | --no-commit] [-m <message>]` (design §6.2). */
|
|
444
|
+
const initCommand = {
|
|
445
|
+
name: "init",
|
|
446
|
+
summary: "set up a decision log: a config file and starter templates",
|
|
447
|
+
usage: "init [--template <name>]… [--commit | --no-commit] [-m <message>]",
|
|
448
|
+
options: {
|
|
449
|
+
template: {
|
|
450
|
+
type: "string",
|
|
451
|
+
multiple: true
|
|
452
|
+
},
|
|
453
|
+
...COMMIT_OPTIONS
|
|
454
|
+
},
|
|
455
|
+
positionals: {
|
|
456
|
+
min: 0,
|
|
457
|
+
max: 0,
|
|
458
|
+
names: []
|
|
459
|
+
},
|
|
460
|
+
ref: "reject",
|
|
461
|
+
async run({ io, flags, values }) {
|
|
462
|
+
const templates = Array.isArray(values.template) ? values.template : void 0;
|
|
463
|
+
const log = await openForCli(io, flags);
|
|
464
|
+
const doc = await initDecisionLog(log, {
|
|
465
|
+
...templates === void 0 ? {} : { templates },
|
|
466
|
+
...commitChoice(values, { autoCommit: log.adr.autoCommit })
|
|
467
|
+
});
|
|
468
|
+
return {
|
|
469
|
+
exit: 0,
|
|
470
|
+
json: doc,
|
|
471
|
+
text: initText(doc),
|
|
472
|
+
notes: doc.notes
|
|
473
|
+
};
|
|
474
|
+
}
|
|
475
|
+
};
|
|
476
|
+
// src/cli/link.ts
|
|
477
|
+
/** `adr link <decision> <target> [--rel <rel>]` (design §6.7): a typed link, in any state. */
|
|
478
|
+
const linkCommand = {
|
|
479
|
+
name: "link",
|
|
480
|
+
summary: "link a decision to another one, here or in a configured source",
|
|
481
|
+
usage: "link <decision> <target> [--rel relates-to|refines|depends-on] [--commit | --no-commit] [-m <message>]",
|
|
482
|
+
options: {
|
|
483
|
+
rel: { type: "string" },
|
|
484
|
+
...COMMIT_OPTIONS
|
|
485
|
+
},
|
|
486
|
+
positionals: {
|
|
487
|
+
min: 2,
|
|
488
|
+
max: 2,
|
|
489
|
+
names: ["decision", "target"]
|
|
490
|
+
},
|
|
491
|
+
ref: "reject",
|
|
492
|
+
async run({ io, flags, values, positionals }) {
|
|
493
|
+
const [input = "", to = ""] = positionals;
|
|
494
|
+
const rel = typeof values.rel === "string" ? values.rel : "relates-to";
|
|
495
|
+
const log = await openForCli(io, flags);
|
|
496
|
+
const { actor, notes } = await resolveAttribution(log.git, { env: io.env });
|
|
497
|
+
const doc = await linkDecision(log, {
|
|
498
|
+
id: input,
|
|
499
|
+
to,
|
|
500
|
+
rel,
|
|
501
|
+
actor
|
|
502
|
+
}, {
|
|
503
|
+
...commitChoice(values, { autoCommit: log.adr.autoCommit }),
|
|
504
|
+
notes
|
|
505
|
+
});
|
|
194
506
|
const { display } = await displayAfterWrite(log);
|
|
195
507
|
const v = doc.decision;
|
|
196
508
|
return {
|
|
@@ -198,7 +510,7 @@ const acceptCommand = {
|
|
|
198
510
|
json: doc,
|
|
199
511
|
text: writeText(doc, display, {
|
|
200
512
|
subject: v === null ? input.trim() : `${display(v.id)}: ${v.title}`,
|
|
201
|
-
number:
|
|
513
|
+
number: null
|
|
202
514
|
}),
|
|
203
515
|
notes: doc.notes
|
|
204
516
|
};
|
|
@@ -283,7 +595,7 @@ function editorFor(env, o) {
|
|
|
283
595
|
});
|
|
284
596
|
}
|
|
285
597
|
// src/cli/new.ts
|
|
286
|
-
const strings = (v) => Array.isArray(v) ? v : [];
|
|
598
|
+
const strings$1 = (v) => Array.isArray(v) ? v : [];
|
|
287
599
|
/** `--section <id>=<markdown>` values, split at the first `=`; each id at most once. */
|
|
288
600
|
function sections(values) {
|
|
289
601
|
const out = {};
|
|
@@ -303,7 +615,7 @@ function sections(values) {
|
|
|
303
615
|
const newCommand = {
|
|
304
616
|
name: "new",
|
|
305
617
|
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]",
|
|
618
|
+
usage: "new <title> [--template <name>] [--supersedes <decision>]… [--section <id>=<markdown>]… [--edit | --no-edit] [--commit | --no-commit] [-m <message>]",
|
|
307
619
|
options: {
|
|
308
620
|
template: { type: "string" },
|
|
309
621
|
supersedes: {
|
|
@@ -315,7 +627,7 @@ const newCommand = {
|
|
|
315
627
|
multiple: true
|
|
316
628
|
},
|
|
317
629
|
edit: { type: "boolean" },
|
|
318
|
-
|
|
630
|
+
...COMMIT_OPTIONS
|
|
319
631
|
},
|
|
320
632
|
positionals: {
|
|
321
633
|
min: 1,
|
|
@@ -325,10 +637,10 @@ const newCommand = {
|
|
|
325
637
|
ref: "reject",
|
|
326
638
|
async run({ io, flags, values, positionals }) {
|
|
327
639
|
const title = positionals[0] ?? "";
|
|
328
|
-
const given = sections(strings(values.section));
|
|
640
|
+
const given = sections(strings$1(values.section));
|
|
329
641
|
const log = await openForCli(io, flags);
|
|
330
|
-
const actor = await
|
|
331
|
-
const requested =
|
|
642
|
+
const { actor, notes: attribution } = await resolveAttribution(log.git, { env: io.env });
|
|
643
|
+
const { commit: requested, message } = commitChoice(values, { autoCommit: log.adr.autoCommit });
|
|
332
644
|
const notes = [];
|
|
333
645
|
const editor = editorFor(io.env, { stdoutIsTTY: io.stdoutIsTTY });
|
|
334
646
|
const wantsEdit = values.edit === true || values.edit === void 0 && io.interactive;
|
|
@@ -341,24 +653,245 @@ const newCommand = {
|
|
|
341
653
|
const doc = await proposeDecision(log, {
|
|
342
654
|
title,
|
|
343
655
|
sections: given,
|
|
344
|
-
supersedes: strings(values.supersedes),
|
|
656
|
+
supersedes: strings$1(values.supersedes),
|
|
345
657
|
actor,
|
|
346
658
|
...typeof values.template === "string" ? { template: values.template } : {}
|
|
347
659
|
}, {
|
|
348
660
|
commit: requested,
|
|
661
|
+
message,
|
|
662
|
+
notes: attribution,
|
|
349
663
|
...edit === void 0 ? {} : { edit },
|
|
350
664
|
onNote: (note) => notes.push(requested ? `not committed: ${note}` : note)
|
|
351
665
|
});
|
|
352
666
|
const { display } = await displayAfterWrite(log);
|
|
353
667
|
const v = doc.decision;
|
|
668
|
+
const unfilled = unfilledWarning(doc);
|
|
354
669
|
return {
|
|
355
670
|
exit: doc.ok ? 0 : 1,
|
|
356
671
|
json: doc,
|
|
357
672
|
text: writeText(doc, display, {
|
|
358
673
|
subject: title.trim(),
|
|
359
|
-
number: v === null || v.number === null ? null : v.display
|
|
674
|
+
number: v === null || v.number === null ? null : v.display,
|
|
675
|
+
actor,
|
|
676
|
+
acceptRequiresHuman: log.adr.acceptRequiresHuman
|
|
677
|
+
}),
|
|
678
|
+
notes: [
|
|
679
|
+
...doc.notes,
|
|
680
|
+
...notes,
|
|
681
|
+
...unfilled === null ? [] : [unfilled]
|
|
682
|
+
]
|
|
683
|
+
};
|
|
684
|
+
}
|
|
685
|
+
};
|
|
686
|
+
// src/cli/note.ts
|
|
687
|
+
/** `adr note <decision> <text>` (design §6.6): a note on a decision in any state. */
|
|
688
|
+
const noteCommand = {
|
|
689
|
+
name: "note",
|
|
690
|
+
summary: "add a note to a decision, in any state",
|
|
691
|
+
usage: "note <decision> <text> [--commit | --no-commit] [-m <message>]",
|
|
692
|
+
options: { ...COMMIT_OPTIONS },
|
|
693
|
+
positionals: {
|
|
694
|
+
min: 2,
|
|
695
|
+
max: 2,
|
|
696
|
+
names: ["decision", "text"]
|
|
697
|
+
},
|
|
698
|
+
ref: "reject",
|
|
699
|
+
async run({ io, flags, values, positionals }) {
|
|
700
|
+
const [input = "", text = ""] = positionals;
|
|
701
|
+
const log = await openForCli(io, flags);
|
|
702
|
+
const { actor, notes } = await resolveAttribution(log.git, { env: io.env });
|
|
703
|
+
const doc = await noteDecision(log, {
|
|
704
|
+
id: input,
|
|
705
|
+
text,
|
|
706
|
+
actor
|
|
707
|
+
}, {
|
|
708
|
+
...commitChoice(values, { autoCommit: log.adr.autoCommit }),
|
|
709
|
+
notes
|
|
710
|
+
});
|
|
711
|
+
const { display } = await displayAfterWrite(log);
|
|
712
|
+
const v = doc.decision;
|
|
713
|
+
return {
|
|
714
|
+
exit: doc.ok ? 0 : 1,
|
|
715
|
+
json: doc,
|
|
716
|
+
text: writeText(doc, display, {
|
|
717
|
+
subject: v === null ? input.trim() : `${display(v.id)}: ${v.title}`,
|
|
718
|
+
number: null
|
|
719
|
+
}),
|
|
720
|
+
notes: doc.notes
|
|
721
|
+
};
|
|
722
|
+
}
|
|
723
|
+
};
|
|
724
|
+
// src/cli/reject.ts
|
|
725
|
+
const USAGE$3 = "reject <decision> --reason <text> [--commit | --no-commit] [-m <message>]";
|
|
726
|
+
/** `adr reject` (design §6.3): a proposal that won't be adopted. */
|
|
727
|
+
const rejectCommand = {
|
|
728
|
+
name: "reject",
|
|
729
|
+
summary: "reject a proposed decision",
|
|
730
|
+
usage: USAGE$3,
|
|
731
|
+
options: {
|
|
732
|
+
reason: { type: "string" },
|
|
733
|
+
...COMMIT_OPTIONS
|
|
734
|
+
},
|
|
735
|
+
positionals: {
|
|
736
|
+
min: 1,
|
|
737
|
+
max: 1,
|
|
738
|
+
names: ["decision"]
|
|
739
|
+
},
|
|
740
|
+
ref: "reject",
|
|
741
|
+
async run({ io, flags, values, positionals }) {
|
|
742
|
+
const input = positionals[0] ?? "";
|
|
743
|
+
if (typeof values.reason !== "string" || values.reason.trim() === "") throw new UsageError(`missing --reason <text>; usage: adr ${USAGE$3}`);
|
|
744
|
+
const reason = typeof values.reason === "string" ? values.reason.trim() : null;
|
|
745
|
+
const log = await openForCli(io, flags);
|
|
746
|
+
const { actor, notes } = await resolveAttribution(log.git, { env: io.env });
|
|
747
|
+
const doc = await rejectDecision(log, {
|
|
748
|
+
id: input,
|
|
749
|
+
reason,
|
|
750
|
+
actor
|
|
751
|
+
}, {
|
|
752
|
+
...commitChoice(values, { autoCommit: log.adr.autoCommit }),
|
|
753
|
+
notes
|
|
754
|
+
});
|
|
755
|
+
const { display } = await displayAfterWrite(log);
|
|
756
|
+
const v = doc.decision;
|
|
757
|
+
return {
|
|
758
|
+
exit: doc.ok ? 0 : 1,
|
|
759
|
+
json: doc,
|
|
760
|
+
text: writeText(doc, display, {
|
|
761
|
+
subject: v === null ? input.trim() : `${display(v.id)}: ${v.title}`,
|
|
762
|
+
number: null,
|
|
763
|
+
reason
|
|
360
764
|
}),
|
|
361
|
-
notes:
|
|
765
|
+
notes: doc.notes
|
|
766
|
+
};
|
|
767
|
+
}
|
|
768
|
+
};
|
|
769
|
+
// src/cli/scope.ts
|
|
770
|
+
const USAGE$2 = "scope <decision> ([--set <glob>]… | --clear | [--add <glob>]… [--remove <glob>]…) [--commit | --no-commit] [-m <message>] | scope [<decision>] --follow-renames";
|
|
771
|
+
/** The flag names `applySetOp` uses in its messages (design §3.2). */
|
|
772
|
+
const NAMES = {
|
|
773
|
+
set: "--set",
|
|
774
|
+
add: "--add",
|
|
775
|
+
remove: "--remove",
|
|
776
|
+
clear: "--clear"
|
|
777
|
+
};
|
|
778
|
+
const strings = (v) => Array.isArray(v) ? v : [];
|
|
779
|
+
/** `--set`/`--clear`/`--add`/`--remove` as a set operation; `--clear` is `{ set: [] }`. */
|
|
780
|
+
function setOp(values) {
|
|
781
|
+
const set = strings(values.set);
|
|
782
|
+
const add = strings(values.add);
|
|
783
|
+
const remove = strings(values.remove);
|
|
784
|
+
if (values.clear === true) {
|
|
785
|
+
if (set.length > 0 || add.length > 0 || remove.length > 0) throw new UsageError("--clear can't be combined with --set, --add or --remove");
|
|
786
|
+
return { set: [] };
|
|
787
|
+
}
|
|
788
|
+
return {
|
|
789
|
+
...set.length > 0 ? { set } : {},
|
|
790
|
+
add,
|
|
791
|
+
remove
|
|
792
|
+
};
|
|
793
|
+
}
|
|
794
|
+
/**
|
|
795
|
+
* `--follow-renames` alone: no set operation, no commit and no message, all refused before
|
|
796
|
+
* `commitChoice` so the answer doesn't depend on `auto_commit` (design §6.8).
|
|
797
|
+
*/
|
|
798
|
+
function checkFollowRenames(values) {
|
|
799
|
+
if ([
|
|
800
|
+
"set",
|
|
801
|
+
"add",
|
|
802
|
+
"remove"
|
|
803
|
+
].some((k) => strings(values[k]).length > 0) || values.clear === true) throw new UsageError("--follow-renames can't be combined with --set, --clear, --add or --remove");
|
|
804
|
+
if (values.commit === true) throw new UsageError("--follow-renames stages its events for the commit that contains the renames; commit them together");
|
|
805
|
+
if (typeof values.message === "string") throw new UsageError("--follow-renames never commits, so it takes no --message");
|
|
806
|
+
}
|
|
807
|
+
/**
|
|
808
|
+
* `adr scope <decision> --set|--clear|--add|--remove …` (design §6.8): the globs of the code a
|
|
809
|
+
* decision governs, written as one whole-set `scoped` event. `adr scope [<decision>]
|
|
810
|
+
* --follow-renames` rewrites them after staged renames and stages the events, never committing.
|
|
811
|
+
*/
|
|
812
|
+
const scopeCommand = {
|
|
813
|
+
name: "scope",
|
|
814
|
+
summary: "set, add or remove the globs of the code a decision governs",
|
|
815
|
+
usage: USAGE$2,
|
|
816
|
+
options: {
|
|
817
|
+
set: {
|
|
818
|
+
type: "string",
|
|
819
|
+
multiple: true
|
|
820
|
+
},
|
|
821
|
+
add: {
|
|
822
|
+
type: "string",
|
|
823
|
+
multiple: true
|
|
824
|
+
},
|
|
825
|
+
remove: {
|
|
826
|
+
type: "string",
|
|
827
|
+
multiple: true
|
|
828
|
+
},
|
|
829
|
+
clear: { type: "boolean" },
|
|
830
|
+
"follow-renames": { type: "boolean" },
|
|
831
|
+
...COMMIT_OPTIONS
|
|
832
|
+
},
|
|
833
|
+
positionals: {
|
|
834
|
+
min: 0,
|
|
835
|
+
max: 1,
|
|
836
|
+
names: ["decision"]
|
|
837
|
+
},
|
|
838
|
+
ref: "reject",
|
|
839
|
+
async run({ io, flags, values, positionals }) {
|
|
840
|
+
const input = positionals[0];
|
|
841
|
+
if (values["follow-renames"] === true) {
|
|
842
|
+
checkFollowRenames(values);
|
|
843
|
+
const log = await openForCli(io, flags);
|
|
844
|
+
const { actor, notes } = await resolveAttribution(log.git, { env: io.env });
|
|
845
|
+
const read = await readDecisions(log);
|
|
846
|
+
const before = new Map([...read.projection.views].map(([id, v]) => [id, v.applies_to]));
|
|
847
|
+
const renamed = (await stagedRenames(log.git)).length > 0;
|
|
848
|
+
const doc = await followScopeRenames(log, {
|
|
849
|
+
...input === void 0 ? {} : { id: input },
|
|
850
|
+
actor
|
|
851
|
+
}, { notes });
|
|
852
|
+
const { display } = await displayAfterWrite(log);
|
|
853
|
+
const v = doc.decision;
|
|
854
|
+
return {
|
|
855
|
+
exit: doc.ok ? 0 : 1,
|
|
856
|
+
json: doc,
|
|
857
|
+
text: writeText(doc, display, {
|
|
858
|
+
subject: v === null ? (input ?? "").trim() : `${display(v.id)}: ${v.title}`,
|
|
859
|
+
number: null,
|
|
860
|
+
followRenames: {
|
|
861
|
+
before,
|
|
862
|
+
renamed
|
|
863
|
+
}
|
|
864
|
+
}),
|
|
865
|
+
notes: doc.notes
|
|
866
|
+
};
|
|
867
|
+
}
|
|
868
|
+
const op = setOp(values);
|
|
869
|
+
if (input === void 0) throw new UsageError(`missing argument <decision>; usage: adr ${USAGE$2}`);
|
|
870
|
+
const log = await openForCli(io, flags);
|
|
871
|
+
const { actor, notes } = await resolveAttribution(log.git, { env: io.env });
|
|
872
|
+
const choice = commitChoice(values, { autoCommit: log.adr.autoCommit });
|
|
873
|
+
const read = await readDecisions(log);
|
|
874
|
+
const before = read.projection.views.get(resolveDecision(read, input))?.applies_to ?? [];
|
|
875
|
+
const doc = await scopeDecision(log, {
|
|
876
|
+
id: input,
|
|
877
|
+
op,
|
|
878
|
+
actor,
|
|
879
|
+
names: NAMES
|
|
880
|
+
}, {
|
|
881
|
+
...choice,
|
|
882
|
+
notes
|
|
883
|
+
});
|
|
884
|
+
const { display } = await displayAfterWrite(log);
|
|
885
|
+
const v = doc.decision;
|
|
886
|
+
return {
|
|
887
|
+
exit: doc.ok ? 0 : 1,
|
|
888
|
+
json: doc,
|
|
889
|
+
text: writeText(doc, display, {
|
|
890
|
+
subject: v === null ? input.trim() : `${display(v.id)}: ${v.title}`,
|
|
891
|
+
number: null,
|
|
892
|
+
scopeBefore: before
|
|
893
|
+
}),
|
|
894
|
+
notes: doc.notes
|
|
362
895
|
};
|
|
363
896
|
}
|
|
364
897
|
};
|
|
@@ -389,16 +922,16 @@ const showCommand = {
|
|
|
389
922
|
}
|
|
390
923
|
};
|
|
391
924
|
// src/cli/supersede.ts
|
|
392
|
-
const USAGE = "supersede <decision> --by <decision> [--reason <text>] [--commit | --no-commit]";
|
|
925
|
+
const USAGE$1 = "supersede <decision> --by <decision> [--reason <text>] [--commit | --no-commit] [-m <message>]";
|
|
393
926
|
/** `adr supersede <decision> --by <decision> …` (design §5.6). */
|
|
394
927
|
const supersedeCommand = {
|
|
395
928
|
name: "supersede",
|
|
396
929
|
summary: "mark an accepted decision as superseded by another accepted one",
|
|
397
|
-
usage: USAGE,
|
|
930
|
+
usage: USAGE$1,
|
|
398
931
|
options: {
|
|
399
932
|
by: { type: "string" },
|
|
400
933
|
reason: { type: "string" },
|
|
401
|
-
|
|
934
|
+
...COMMIT_OPTIONS
|
|
402
935
|
},
|
|
403
936
|
positionals: {
|
|
404
937
|
min: 1,
|
|
@@ -408,15 +941,18 @@ const supersedeCommand = {
|
|
|
408
941
|
ref: "reject",
|
|
409
942
|
async run({ io, flags, values, positionals }) {
|
|
410
943
|
const input = positionals[0] ?? "";
|
|
411
|
-
if (typeof values.by !== "string") throw new UsageError(`missing --by <decision>; usage: adr ${USAGE}`);
|
|
944
|
+
if (typeof values.by !== "string") throw new UsageError(`missing --by <decision>; usage: adr ${USAGE$1}`);
|
|
412
945
|
const log = await openForCli(io, flags);
|
|
413
|
-
const actor = await
|
|
946
|
+
const { actor, notes: attribution } = await resolveAttribution(log.git, { env: io.env });
|
|
414
947
|
const doc = await supersedeDecision(log, {
|
|
415
948
|
id: input,
|
|
416
949
|
by: values.by,
|
|
417
950
|
actor,
|
|
418
951
|
...typeof values.reason === "string" ? { reason: values.reason } : {}
|
|
419
|
-
}, {
|
|
952
|
+
}, {
|
|
953
|
+
...commitChoice(values, { autoCommit: log.adr.autoCommit }),
|
|
954
|
+
notes: attribution
|
|
955
|
+
});
|
|
420
956
|
const { display } = await displayAfterWrite(log);
|
|
421
957
|
const v = doc.decision;
|
|
422
958
|
return {
|
|
@@ -430,6 +966,49 @@ const supersedeCommand = {
|
|
|
430
966
|
};
|
|
431
967
|
}
|
|
432
968
|
};
|
|
969
|
+
/** `adr withdraw` (design §6.4): a proposal its author takes back. */
|
|
970
|
+
const withdrawCommand = {
|
|
971
|
+
name: "withdraw",
|
|
972
|
+
summary: "withdraw a proposed decision",
|
|
973
|
+
usage: "withdraw <decision> [--reason <text>] [--commit | --no-commit] [-m <message>]",
|
|
974
|
+
options: {
|
|
975
|
+
reason: { type: "string" },
|
|
976
|
+
...COMMIT_OPTIONS
|
|
977
|
+
},
|
|
978
|
+
positionals: {
|
|
979
|
+
min: 1,
|
|
980
|
+
max: 1,
|
|
981
|
+
names: ["decision"]
|
|
982
|
+
},
|
|
983
|
+
ref: "reject",
|
|
984
|
+
async run({ io, flags, values, positionals }) {
|
|
985
|
+
const input = positionals[0] ?? "";
|
|
986
|
+
if (typeof values.reason === "string" && values.reason.trim() === "") throw new UsageError("--reason must not be empty");
|
|
987
|
+
const reason = typeof values.reason === "string" ? values.reason.trim() : null;
|
|
988
|
+
const log = await openForCli(io, flags);
|
|
989
|
+
const { actor, notes } = await resolveAttribution(log.git, { env: io.env });
|
|
990
|
+
const doc = await withdrawDecision(log, {
|
|
991
|
+
id: input,
|
|
992
|
+
reason,
|
|
993
|
+
actor
|
|
994
|
+
}, {
|
|
995
|
+
...commitChoice(values, { autoCommit: log.adr.autoCommit }),
|
|
996
|
+
notes
|
|
997
|
+
});
|
|
998
|
+
const { display } = await displayAfterWrite(log);
|
|
999
|
+
const v = doc.decision;
|
|
1000
|
+
return {
|
|
1001
|
+
exit: doc.ok ? 0 : 1,
|
|
1002
|
+
json: doc,
|
|
1003
|
+
text: writeText(doc, display, {
|
|
1004
|
+
subject: v === null ? input.trim() : `${display(v.id)}: ${v.title}`,
|
|
1005
|
+
number: null,
|
|
1006
|
+
reason
|
|
1007
|
+
}),
|
|
1008
|
+
notes: doc.notes
|
|
1009
|
+
};
|
|
1010
|
+
}
|
|
1011
|
+
};
|
|
433
1012
|
// src/cli/commands.ts
|
|
434
1013
|
/**
|
|
435
1014
|
* ledger's shared `verify`: the log as the mode reads it (the index for `--staged`, else the
|
|
@@ -453,9 +1032,17 @@ function adrCli(version) {
|
|
|
453
1032
|
version,
|
|
454
1033
|
summary: "Architecture decision records kept as event logs in git.",
|
|
455
1034
|
commands: [
|
|
1035
|
+
initCommand,
|
|
456
1036
|
newCommand,
|
|
457
1037
|
acceptCommand,
|
|
1038
|
+
rejectCommand,
|
|
1039
|
+
withdrawCommand,
|
|
1040
|
+
deprecateCommand,
|
|
458
1041
|
supersedeCommand,
|
|
1042
|
+
noteCommand,
|
|
1043
|
+
linkCommand,
|
|
1044
|
+
scopeCommand,
|
|
1045
|
+
forCommand,
|
|
459
1046
|
listCommand,
|
|
460
1047
|
showCommand,
|
|
461
1048
|
verify
|