@nikovirtala/adr 0.2.0 → 0.4.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 +77 -13
- package/dist/cli.js +348 -46
- package/dist/cli.js.map +1 -1
- package/dist/index.js +5 -3
- package/dist/init-YgW7g9Fx.js +588 -0
- package/dist/init-YgW7g9Fx.js.map +1 -0
- package/dist/{kind-BcHjCHvi.js → kind-DjvA3dyA.js} +2 -6
- package/dist/{kind-BcHjCHvi.js.map → kind-DjvA3dyA.js.map} +1 -1
- package/dist/kind.js +1 -1
- package/dist/list-Dt4KGxvj.js +58 -0
- package/dist/list-Dt4KGxvj.js.map +1 -0
- package/dist/{write-9IaqHRrY.js → search-CwIPlNGd.js} +1339 -560
- package/dist/search-CwIPlNGd.js.map +1 -0
- package/dist/server-CXCO5zfp.js +938 -0
- package/dist/server-CXCO5zfp.js.map +1 -0
- package/dist/types/cli/hooks.d.ts +6 -0
- package/dist/types/cli/index-command.d.ts +3 -0
- package/dist/types/cli/init.d.ts +5 -2
- package/dist/types/cli/list.d.ts +1 -1
- package/dist/types/cli/mcp.d.ts +6 -0
- package/dist/types/cli/open.d.ts +2 -0
- package/dist/types/cli/search.d.ts +3 -0
- package/dist/types/cli/text.d.ts +12 -0
- package/dist/types/commit-actions.d.ts +74 -0
- package/dist/types/config.d.ts +7 -1
- package/dist/types/hooks.d.ts +59 -0
- package/dist/types/index.d.ts +7 -2
- package/dist/types/init.d.ts +23 -2
- package/dist/types/log-index.d.ts +117 -0
- package/dist/types/log.d.ts +44 -1
- package/dist/types/mcp/policy.d.ts +33 -0
- package/dist/types/mcp/prompt.d.ts +7 -0
- package/dist/types/mcp/resources.d.ts +16 -0
- package/dist/types/mcp/server.d.ts +16 -0
- package/dist/types/mcp/text.d.ts +42 -0
- package/dist/types/mcp/tools.d.ts +27 -0
- package/dist/types/scope.d.ts +3 -1
- package/dist/types/search.d.ts +61 -0
- package/dist/types/skill.d.ts +5 -0
- package/dist/types/templates/list.d.ts +31 -0
- package/dist/types/verify.d.ts +6 -4
- package/dist/types/views.d.ts +9 -3
- package/dist/types/write.d.ts +65 -9
- package/package.json +5 -2
- package/dist/write-9IaqHRrY.js.map +0 -1
package/dist/cli.js
CHANGED
|
@@ -1,8 +1,11 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { n as
|
|
3
|
-
import {
|
|
2
|
+
import { A as changedSince, I as resolveDecision, M as numberingNote, N as openDecisionLog, O as runDecisionIndex, P as readDecisions, _ as withdrawDecision, c as followScopeRenames, d as proposeDecision, f as rejectDecision, h as supersedeDecision, i as showDecision, l as linkDecision, n as decisionsFor, o as acceptDecision, p as scopeDecision, r as listDecisions, s as deprecateDecision, t as searchDecisions, u as noteDecision } from "./search-CwIPlNGd.js";
|
|
3
|
+
import { n as DECISION_STATUSES, t as adrKind } from "./kind-DjvA3dyA.js";
|
|
4
|
+
import { a as installDecisionHooks, c as uninstallDecisionHooks, l as adrChecks, o as printDecisionHooks, r as addHookTrailers, s as runPreCommitHook, t as initDecisionLog } from "./init-YgW7g9Fx.js";
|
|
4
5
|
import { createRequire } from "node:module";
|
|
5
|
-
import { UsageError, renderStateHeader, resolveAttribution, stagedRenames, templateHistoryRef } from "@nikovirtala/ledger";
|
|
6
|
+
import { UsageError, exitCode, findingsReport, formatText, renderStateHeader, resolveAttribution, stagedRenames, templateHistoryRef } from "@nikovirtala/ledger";
|
|
7
|
+
import { realpath } from "node:fs/promises";
|
|
8
|
+
import { isAbsolute, relative, resolve, sep } from "node:path";
|
|
6
9
|
import { COMMIT_OPTIONS, commitChoice, confirm, processIO, runCli, verifyCommand } from "@nikovirtala/ledger/cli";
|
|
7
10
|
import { spawn } from "node:child_process";
|
|
8
11
|
// src/cli/open.ts
|
|
@@ -27,6 +30,8 @@ async function displayAfterWrite(log) {
|
|
|
27
30
|
const read = await readDecisions(log);
|
|
28
31
|
return { display: (id) => read.projection.views.get(id)?.display ?? id };
|
|
29
32
|
}
|
|
33
|
+
/** `notes` with `note` appended when there is one. */
|
|
34
|
+
const notesWith = (notes, note) => note === null ? notes : [...notes, note];
|
|
30
35
|
// src/cli/text.ts
|
|
31
36
|
/** A table: each column padded to its widest value, trailing spaces removed. */
|
|
32
37
|
function columns(rows) {
|
|
@@ -39,6 +44,21 @@ function columns(rows) {
|
|
|
39
44
|
/** `adr list`: one row per decision, `→ <effective>` when it differs from the decision. */
|
|
40
45
|
function listText(doc, display) {
|
|
41
46
|
if (doc.decisions.length === 0) return "No decisions.";
|
|
47
|
+
return listRows(doc, display).join("\n");
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* `adr list --since <rev>` (design 2b D7): a heading, then each row followed by the event types
|
|
51
|
+
* added since `rev`; nothing at all when no decision changed, so the post-merge hook stays quiet.
|
|
52
|
+
*/
|
|
53
|
+
function sinceText(doc, since, display) {
|
|
54
|
+
if (doc.decisions.length === 0) return "";
|
|
55
|
+
const rows = listRows(doc, display);
|
|
56
|
+
return [`Changed since ${since.rev} (${since.commit.slice(0, 7)}):`, ...doc.decisions.flatMap((v, i) => {
|
|
57
|
+
const types = [...new Set(since.events.filter((e) => e.subject === v.id).map((e) => e.type))];
|
|
58
|
+
return [rows[i] ?? "", ` [new: ${types.join(", ")}]`];
|
|
59
|
+
})].join("\n");
|
|
60
|
+
}
|
|
61
|
+
function listRows(doc, display) {
|
|
42
62
|
return columns(doc.decisions.map((v) => {
|
|
43
63
|
const arrow = v.effective === null ? "→ (cycle)" : v.effective === v.id ? "" : `→ ${display(v.effective)}`;
|
|
44
64
|
return [
|
|
@@ -48,7 +68,7 @@ function listText(doc, display) {
|
|
|
48
68
|
v.title,
|
|
49
69
|
arrow
|
|
50
70
|
];
|
|
51
|
-
}));
|
|
71
|
+
})).split("\n");
|
|
52
72
|
}
|
|
53
73
|
/** `ADR-0003 (m2x7p4d2)`, or the id alone when it has no number. */
|
|
54
74
|
function named(id, display) {
|
|
@@ -132,7 +152,7 @@ function unfilledWarning(doc) {
|
|
|
132
152
|
function findingLines(findings) {
|
|
133
153
|
return findings.map((f) => ` ${f.file === null ? "" : `${f.file}: `}${f.message} [${f.rule}]`);
|
|
134
154
|
}
|
|
135
|
-
const short = (sha) => sha.slice(0, 7);
|
|
155
|
+
const short$1 = (sha) => sha.slice(0, 7);
|
|
136
156
|
/**
|
|
137
157
|
* `adr scope --follow-renames`: one `<named>: <old globs> → <new globs>` line per changed
|
|
138
158
|
* decision (`affected`), whether or not a decision was given.
|
|
@@ -158,7 +178,8 @@ const VERBS = {
|
|
|
158
178
|
deprecate: "deprecated",
|
|
159
179
|
note: "noted",
|
|
160
180
|
link: "linked",
|
|
161
|
-
scope: "scoped"
|
|
181
|
+
scope: "scoped",
|
|
182
|
+
update: "updated"
|
|
162
183
|
};
|
|
163
184
|
/**
|
|
164
185
|
* The text of a write command (design §5.6, §6.1). `subject` names what a refused command was
|
|
@@ -178,7 +199,7 @@ function writeText(doc, display, o) {
|
|
|
178
199
|
if (o.followRenames !== void 0) return followRenamesText(doc, display, o.followRenames);
|
|
179
200
|
const v = doc.decision;
|
|
180
201
|
if (v === null) return "";
|
|
181
|
-
const committed = doc.commit === null ? null : `Committed ${short(doc.commit)}`;
|
|
202
|
+
const committed = doc.commit === null ? null : `Committed ${short$1(doc.commit)}`;
|
|
182
203
|
const recorded = committed === null ? "Staged; commit to record it." : `${committed}.`;
|
|
183
204
|
switch (doc.command) {
|
|
184
205
|
case "new": return [
|
|
@@ -225,6 +246,9 @@ function writeText(doc, display, o) {
|
|
|
225
246
|
recorded
|
|
226
247
|
].join("\n");
|
|
227
248
|
}
|
|
249
|
+
case "update":
|
|
250
|
+
if (doc.written.length === 0) return `Draft of ${named(v.id, display)} unchanged`;
|
|
251
|
+
return [`Updated the draft of ${named(v.id, display)}: ${v.title}`, recorded].join("\n");
|
|
228
252
|
}
|
|
229
253
|
}
|
|
230
254
|
/**
|
|
@@ -280,15 +304,39 @@ function forText(doc, display, keyword) {
|
|
|
280
304
|
}
|
|
281
305
|
return lines.join("\n");
|
|
282
306
|
}
|
|
307
|
+
/**
|
|
308
|
+
* `adr search`: one padded row per result with its excerpt (or which field matched) under it, and
|
|
309
|
+
* an inactive result's warning; then the count.
|
|
310
|
+
*/
|
|
311
|
+
function searchText(doc) {
|
|
312
|
+
const quoted = JSON.stringify(doc.query);
|
|
313
|
+
if (doc.results.length === 0) return `No ${doc.include_inactive ? "" : "active "}decisions match ${quoted}.`;
|
|
314
|
+
const rows = columns(doc.results.map((r) => [
|
|
315
|
+
r.decision.number === null ? r.decision.id : `${r.decision.display} (${r.decision.id})`,
|
|
316
|
+
r.decision.status,
|
|
317
|
+
r.decision.title
|
|
318
|
+
])).split("\n");
|
|
319
|
+
const lines = [];
|
|
320
|
+
doc.results.forEach((r, i) => {
|
|
321
|
+
lines.push(rows[i] ?? "");
|
|
322
|
+
if (r.header.warning !== null) lines.push(` ${r.header.warning}`);
|
|
323
|
+
const where = r.matched.filter((f) => f === "id" || f === "title").join(" and ");
|
|
324
|
+
lines.push(r.excerpt === null ? ` (${where} match)` : ` ${r.excerpt}`);
|
|
325
|
+
});
|
|
326
|
+
const n = doc.total;
|
|
327
|
+
lines.push(`${n === 1 ? "1 decision matches" : `${n} decisions match`} ${quoted}.`);
|
|
328
|
+
return lines.join("\n");
|
|
329
|
+
}
|
|
283
330
|
/** `adr init`: what was written and kept, then the next step; or that nothing was needed. */
|
|
284
331
|
function initText(doc) {
|
|
285
332
|
if (doc.written.length === 0) return `Already initialized: ${doc.root} (kept ${doc.kept.join(", ")})`;
|
|
333
|
+
const skill = doc.skill !== null && doc.written.includes(doc.skill);
|
|
286
334
|
return [
|
|
287
|
-
`Initialized ${doc.root}`,
|
|
335
|
+
doc.written.every((p) => p === doc.skill || p === "AGENTS.md") ? "Set up the adr agent skill" : `Initialized ${doc.root}`,
|
|
288
336
|
...doc.written.map((p) => ` ${p}`),
|
|
289
337
|
...doc.kept.map((p) => `Kept ${p}`),
|
|
290
|
-
...doc.commit === null ? [] : [`Committed ${short(doc.commit)}.`],
|
|
291
|
-
"Next: adr new \"<title>\""
|
|
338
|
+
...doc.commit === null ? [] : [`Committed ${short$1(doc.commit)}.`],
|
|
339
|
+
skill ? "Next: adr mcp, from your MCP client's config" : "Next: adr new \"<title>\""
|
|
292
340
|
].join("\n");
|
|
293
341
|
}
|
|
294
342
|
// src/cli/accept.ts
|
|
@@ -366,12 +414,12 @@ const acceptCommand = {
|
|
|
366
414
|
}
|
|
367
415
|
};
|
|
368
416
|
// src/cli/deprecate.ts
|
|
369
|
-
const USAGE$
|
|
417
|
+
const USAGE$5 = "deprecate <decision> --reason <text> [--commit | --no-commit] [-m <message>]";
|
|
370
418
|
/** `adr deprecate` (design §6.5): an accepted decision no longer in force. */
|
|
371
419
|
const deprecateCommand = {
|
|
372
420
|
name: "deprecate",
|
|
373
421
|
summary: "deprecate an accepted decision",
|
|
374
|
-
usage: USAGE$
|
|
422
|
+
usage: USAGE$5,
|
|
375
423
|
options: {
|
|
376
424
|
reason: { type: "string" },
|
|
377
425
|
...COMMIT_OPTIONS
|
|
@@ -384,7 +432,7 @@ const deprecateCommand = {
|
|
|
384
432
|
ref: "reject",
|
|
385
433
|
async run({ io, flags, values, positionals }) {
|
|
386
434
|
const input = positionals[0] ?? "";
|
|
387
|
-
if (typeof values.reason !== "string" || values.reason.trim() === "") throw new UsageError(`missing --reason <text>; usage: adr ${USAGE$
|
|
435
|
+
if (typeof values.reason !== "string" || values.reason.trim() === "") throw new UsageError(`missing --reason <text>; usage: adr ${USAGE$5}`);
|
|
388
436
|
const reason = typeof values.reason === "string" ? values.reason.trim() : null;
|
|
389
437
|
const log = await openForCli(io, flags);
|
|
390
438
|
const { actor, notes } = await resolveAttribution(log.git, { env: io.env });
|
|
@@ -435,21 +483,121 @@ const forCommand = {
|
|
|
435
483
|
exit: 0,
|
|
436
484
|
json: doc,
|
|
437
485
|
text: forText(doc, display, read.adr.anchorKeyword),
|
|
438
|
-
notes: doc.notes
|
|
486
|
+
notes: notesWith(doc.notes, numberingNote(read, { unavailable: false }))
|
|
439
487
|
};
|
|
440
488
|
}
|
|
441
489
|
};
|
|
442
|
-
// src/cli/
|
|
443
|
-
|
|
444
|
-
const
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
490
|
+
// src/cli/hooks.ts
|
|
491
|
+
const USAGE$4 = "hooks install | uninstall | print --manager lefthook|husky|pre-commit | run pre-commit | trailers <message-file>";
|
|
492
|
+
const MANAGERS = [
|
|
493
|
+
"lefthook",
|
|
494
|
+
"husky",
|
|
495
|
+
"pre-commit"
|
|
496
|
+
];
|
|
497
|
+
const outcome = (doc, text, notes) => ({
|
|
498
|
+
exit: 0,
|
|
499
|
+
json: doc,
|
|
500
|
+
text,
|
|
501
|
+
notes
|
|
502
|
+
});
|
|
503
|
+
/**
|
|
504
|
+
* `adr hooks install | uninstall | print --manager … | run pre-commit | trailers <message-file>`
|
|
505
|
+
* (design 2b D4, D8, §4.3): adr's git hooks through ledger's shims, shared with other ark tools.
|
|
506
|
+
*/
|
|
507
|
+
const hooksCommand = {
|
|
508
|
+
name: "hooks",
|
|
509
|
+
summary: "install, remove or print adr's git hooks, and run their steps",
|
|
510
|
+
usage: USAGE$4,
|
|
511
|
+
options: { manager: { type: "string" } },
|
|
512
|
+
positionals: {
|
|
513
|
+
min: 1,
|
|
514
|
+
max: 2,
|
|
515
|
+
names: ["action", "argument"]
|
|
516
|
+
},
|
|
517
|
+
ref: "reject",
|
|
518
|
+
async run({ io, flags, values, positionals }) {
|
|
519
|
+
const [action = "", argument] = positionals;
|
|
520
|
+
const manager = values.manager;
|
|
521
|
+
if (manager !== void 0 && action !== "print") throw new UsageError(`--manager goes with hooks print only; usage: adr ${USAGE$4}`);
|
|
522
|
+
const noArgument = () => {
|
|
523
|
+
if (argument !== void 0) throw new UsageError(`unexpected argument ${JSON.stringify(argument)}; usage: adr ${USAGE$4}`);
|
|
524
|
+
};
|
|
525
|
+
switch (action) {
|
|
526
|
+
case "install": {
|
|
527
|
+
noArgument();
|
|
528
|
+
const log = await openForCli(io, flags);
|
|
529
|
+
const doc = await installDecisionHooks(log);
|
|
530
|
+
const lines = [...[...doc.written, ...doc.unchanged].map((p) => `${doc.written.includes(p) ? "wrote" : "unchanged"} ${p}`).sort((a, b) => order(a) - order(b)), `adr hooks: ${doc.registrations.map((r) => r.hook).join(", ")}`];
|
|
531
|
+
return outcome(doc, lines.join("\n"), log.notes);
|
|
532
|
+
}
|
|
533
|
+
case "uninstall": {
|
|
534
|
+
noArgument();
|
|
535
|
+
const log = await openForCli(io, flags);
|
|
536
|
+
const doc = await uninstallDecisionHooks(log);
|
|
537
|
+
const text = doc.written.length === 0 ? "adr hooks were not installed" : doc.written.map((p) => `removed adr from ${p}`).join("\n");
|
|
538
|
+
return outcome(doc, text, log.notes);
|
|
539
|
+
}
|
|
540
|
+
case "print": {
|
|
541
|
+
noArgument();
|
|
542
|
+
if (manager === void 0) throw new UsageError("hooks print needs --manager lefthook, husky or pre-commit");
|
|
543
|
+
if (!MANAGERS.includes(manager)) throw new UsageError(`unknown hook manager ${JSON.stringify(manager)}; use lefthook, husky or pre-commit`);
|
|
544
|
+
const log = await openForCli(io, flags);
|
|
545
|
+
const doc = printDecisionHooks(log, manager);
|
|
546
|
+
return outcome(doc, doc.snippet ?? "", log.notes);
|
|
547
|
+
}
|
|
548
|
+
case "run": {
|
|
549
|
+
if (argument !== "pre-commit") throw new UsageError("adr hooks run supports pre-commit");
|
|
550
|
+
const worktree = await openForCli(io, flags);
|
|
551
|
+
const index = await openForCli(io, flags, { reader: "index" });
|
|
552
|
+
const r = await runPreCommitHook({
|
|
553
|
+
worktree,
|
|
554
|
+
index,
|
|
555
|
+
env: io.env
|
|
556
|
+
});
|
|
557
|
+
return {
|
|
558
|
+
exit: r.exit,
|
|
559
|
+
json: findingsReport(r.findings, { failOn: r.failOn }),
|
|
560
|
+
text: r.findings.length === 0 ? "" : formatText(r.findings),
|
|
561
|
+
notes: r.notes
|
|
562
|
+
};
|
|
563
|
+
}
|
|
564
|
+
case "trailers": {
|
|
565
|
+
if (argument === void 0) throw new UsageError(`missing argument <message-file>; usage: adr ${USAGE$4}`);
|
|
566
|
+
const log = await openForCli(io, flags);
|
|
567
|
+
const doc = await addHookTrailers(log, resolve(io.cwd, argument));
|
|
568
|
+
return outcome(doc, "", log.notes);
|
|
569
|
+
}
|
|
570
|
+
default: throw new UsageError(`unknown hooks action ${JSON.stringify(action)}; use install, uninstall, print, run or trailers`);
|
|
571
|
+
}
|
|
572
|
+
}
|
|
573
|
+
};
|
|
574
|
+
/** A shim line's rank in hook order, for the install text. */
|
|
575
|
+
function order(line) {
|
|
576
|
+
return [
|
|
577
|
+
"pre-commit",
|
|
578
|
+
"commit-msg",
|
|
579
|
+
"pre-push",
|
|
580
|
+
"post-merge"
|
|
581
|
+
].findIndex((h) => line.endsWith(`/${h}`));
|
|
582
|
+
}
|
|
583
|
+
// src/cli/index-command.ts
|
|
584
|
+
const short = (sha) => sha.slice(0, 7);
|
|
585
|
+
/** The text of `adr index` (design 2b §4.5). */
|
|
586
|
+
function indexText({ doc, refreshed }) {
|
|
587
|
+
const { path } = doc;
|
|
588
|
+
if (doc.check) return doc.findings.length === 0 ? `${path} is up to date` : formatText(doc.findings);
|
|
589
|
+
if (doc.commit !== null) return [`Committed ${path} as ${short(doc.commit)}`, ...refreshed ? [`Wrote ${path} with uncommitted log changes`] : []].join("\n");
|
|
590
|
+
if (refreshed) return `${path} is up to date in HEAD; wrote the work tree's copy, which includes uncommitted log changes`;
|
|
591
|
+
if (doc.fresh) return `${path} is up to date`;
|
|
592
|
+
return doc.written.length > 0 ? `Wrote ${path}` : `Staged ${path}`;
|
|
593
|
+
}
|
|
594
|
+
/** `adr index [--check] [--commit | --no-commit] [-m <message>]` (design 2b §4.5). */
|
|
595
|
+
const indexCommand = {
|
|
596
|
+
name: "index",
|
|
597
|
+
summary: "regenerate the decision index (index.md), or check that it is current",
|
|
598
|
+
usage: "index [--check] [--commit | --no-commit] [-m <message>]",
|
|
448
599
|
options: {
|
|
449
|
-
|
|
450
|
-
type: "string",
|
|
451
|
-
multiple: true
|
|
452
|
-
},
|
|
600
|
+
check: { type: "boolean" },
|
|
453
601
|
...COMMIT_OPTIONS
|
|
454
602
|
},
|
|
455
603
|
positionals: {
|
|
@@ -457,22 +605,87 @@ const initCommand = {
|
|
|
457
605
|
max: 0,
|
|
458
606
|
names: []
|
|
459
607
|
},
|
|
460
|
-
ref: "
|
|
608
|
+
ref: "read",
|
|
461
609
|
async run({ io, flags, values }) {
|
|
462
|
-
const
|
|
610
|
+
const check = values.check === true;
|
|
611
|
+
if (check && (values.commit === true || typeof values.message === "string")) throw new UsageError("--check writes nothing; drop --commit and --message");
|
|
612
|
+
if (!check && flags.ref !== null) throw new UsageError("adr index writes the work tree; use --ref only with --check");
|
|
463
613
|
const log = await openForCli(io, flags);
|
|
464
|
-
const
|
|
465
|
-
|
|
466
|
-
...commitChoice(values, { autoCommit: log.adr.autoCommit })
|
|
614
|
+
const run = await runDecisionIndex(log, {
|
|
615
|
+
check,
|
|
616
|
+
...check ? {} : commitChoice(values, { autoCommit: log.adr.autoCommit })
|
|
467
617
|
});
|
|
468
618
|
return {
|
|
469
|
-
exit: 0,
|
|
470
|
-
json: doc,
|
|
471
|
-
text:
|
|
472
|
-
notes: doc.notes
|
|
619
|
+
exit: check ? exitCode(run.doc.findings, { failOn: log.adr.failOn }) : 0,
|
|
620
|
+
json: run.doc,
|
|
621
|
+
text: indexText(run),
|
|
622
|
+
notes: run.doc.notes
|
|
473
623
|
};
|
|
474
624
|
}
|
|
475
625
|
};
|
|
626
|
+
// src/cli/init.ts
|
|
627
|
+
const DEFAULT_SKILL_DIR = ".claude/skills/adr";
|
|
628
|
+
const MAX_DIR = 1024;
|
|
629
|
+
/** `--skill-dir` as a repo-relative directory inside the work tree and outside `.git`. */
|
|
630
|
+
async function skillDir(root, cwd, given) {
|
|
631
|
+
const dir = given.trim();
|
|
632
|
+
const bad = () => new UsageError("--skill-dir must be a directory inside the repository");
|
|
633
|
+
if (dir === "" || dir.length > MAX_DIR || dir.includes("\0")) throw bad();
|
|
634
|
+
const base = await realpath(cwd).catch(() => cwd);
|
|
635
|
+
const rel = relative(root, resolve(base, dir));
|
|
636
|
+
if (rel === ".." || rel.startsWith(`..${sep}`) || isAbsolute(rel)) throw bad();
|
|
637
|
+
const segments = rel.split(sep).filter((s) => s !== "");
|
|
638
|
+
if (segments[0] === ".git") throw bad();
|
|
639
|
+
return segments.join("/");
|
|
640
|
+
}
|
|
641
|
+
/**
|
|
642
|
+
* `adr init [--template <name>]… [--skill [--skill-dir <path>]] [--commit | --no-commit]
|
|
643
|
+
* [-m <message>]` (design §6.2, phase 3 §9).
|
|
644
|
+
*/
|
|
645
|
+
function initCommand(version) {
|
|
646
|
+
return {
|
|
647
|
+
name: "init",
|
|
648
|
+
summary: "set up a decision log: a config file and starter templates",
|
|
649
|
+
usage: "init [--template <name>]… [--skill [--skill-dir <path>]] [--commit | --no-commit] [-m <message>]",
|
|
650
|
+
options: {
|
|
651
|
+
template: {
|
|
652
|
+
type: "string",
|
|
653
|
+
multiple: true
|
|
654
|
+
},
|
|
655
|
+
skill: { type: "boolean" },
|
|
656
|
+
"skill-dir": { type: "string" },
|
|
657
|
+
...COMMIT_OPTIONS
|
|
658
|
+
},
|
|
659
|
+
positionals: {
|
|
660
|
+
min: 0,
|
|
661
|
+
max: 0,
|
|
662
|
+
names: []
|
|
663
|
+
},
|
|
664
|
+
ref: "reject",
|
|
665
|
+
async run({ io, flags, values }) {
|
|
666
|
+
const templates = Array.isArray(values.template) ? values.template : void 0;
|
|
667
|
+
const given = typeof values["skill-dir"] === "string" ? values["skill-dir"] : null;
|
|
668
|
+
if (given !== null && values.skill !== true) throw new UsageError("--skill-dir needs --skill");
|
|
669
|
+
const log = await openForCli(io, flags);
|
|
670
|
+
const root = log.git.root ?? io.cwd;
|
|
671
|
+
const dir = values.skill !== true ? null : given === null ? DEFAULT_SKILL_DIR : await skillDir(root, io.cwd, given);
|
|
672
|
+
const doc = await initDecisionLog(log, {
|
|
673
|
+
...templates === void 0 ? {} : { templates },
|
|
674
|
+
...dir === null ? {} : { skill: {
|
|
675
|
+
dir,
|
|
676
|
+
version
|
|
677
|
+
} },
|
|
678
|
+
...commitChoice(values, { autoCommit: log.adr.autoCommit })
|
|
679
|
+
});
|
|
680
|
+
return {
|
|
681
|
+
exit: 0,
|
|
682
|
+
json: doc,
|
|
683
|
+
text: initText(doc),
|
|
684
|
+
notes: doc.notes
|
|
685
|
+
};
|
|
686
|
+
}
|
|
687
|
+
};
|
|
688
|
+
}
|
|
476
689
|
// src/cli/link.ts
|
|
477
690
|
/** `adr link <decision> <target> [--rel <rel>]` (design §6.7): a typed link, in any state. */
|
|
478
691
|
const linkCommand = {
|
|
@@ -524,15 +737,25 @@ function statuses(values) {
|
|
|
524
737
|
return s;
|
|
525
738
|
});
|
|
526
739
|
}
|
|
527
|
-
/** `
|
|
740
|
+
/** `--since`'s revision: non-empty and not an option. */
|
|
741
|
+
function sinceRev(values) {
|
|
742
|
+
const v = values.since;
|
|
743
|
+
if (typeof v !== "string") return null;
|
|
744
|
+
if (v.trim() === "" || v.startsWith("-")) throw new UsageError("--since must be a revision");
|
|
745
|
+
return v;
|
|
746
|
+
}
|
|
747
|
+
/** `adr list [--status <status>]… [--since <rev>]` (design §5.6, 2b D7). */
|
|
528
748
|
const listCommand = {
|
|
529
749
|
name: "list",
|
|
530
750
|
summary: "list decisions with their computed status",
|
|
531
|
-
usage: "list [--status <status>]…",
|
|
532
|
-
options: {
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
751
|
+
usage: "list [--status <status>]… [--since <rev>]",
|
|
752
|
+
options: {
|
|
753
|
+
status: {
|
|
754
|
+
type: "string",
|
|
755
|
+
multiple: true
|
|
756
|
+
},
|
|
757
|
+
since: { type: "string" }
|
|
758
|
+
},
|
|
536
759
|
positionals: {
|
|
537
760
|
min: 0,
|
|
538
761
|
max: 0,
|
|
@@ -541,10 +764,17 @@ const listCommand = {
|
|
|
541
764
|
ref: "read",
|
|
542
765
|
async run({ io, flags, values }) {
|
|
543
766
|
const status = statuses(values);
|
|
544
|
-
const
|
|
545
|
-
const
|
|
767
|
+
const rev = sinceRev(values);
|
|
768
|
+
const log = await openForCli(io, flags);
|
|
769
|
+
const since = rev === null ? null : await changedSince(log, rev);
|
|
770
|
+
const read = await readDecisions(log);
|
|
771
|
+
const doc = listDecisions(read, {
|
|
772
|
+
status,
|
|
773
|
+
since
|
|
774
|
+
});
|
|
546
775
|
const notes = [...doc.notes];
|
|
547
|
-
|
|
776
|
+
const numbering = numberingNote(read, { unavailable: true });
|
|
777
|
+
if (numbering !== null && doc.decisions.length > 0) notes.push(numbering);
|
|
548
778
|
if (doc.findings.length > 0) notes.push(`the decision log has ${plural(doc.findings.length, "problem", "problems")}; run adr verify`);
|
|
549
779
|
if (doc.unreadable.length > 0) notes.push(`${plural(doc.unreadable.length, "decision uses", "decisions use")} a newer format; upgrade adr`);
|
|
550
780
|
if (doc.unfoldable.length > 0) notes.push(`${plural(doc.unfoldable.length, "decision", "decisions")} can't be read; run adr verify`);
|
|
@@ -552,11 +782,47 @@ const listCommand = {
|
|
|
552
782
|
return {
|
|
553
783
|
exit: 0,
|
|
554
784
|
json: doc,
|
|
555
|
-
text: listText(doc, display),
|
|
785
|
+
text: doc.since === null ? listText(doc, display) : sinceText(doc, doc.since, display),
|
|
556
786
|
notes
|
|
557
787
|
};
|
|
558
788
|
}
|
|
559
789
|
};
|
|
790
|
+
// src/cli/mcp.ts
|
|
791
|
+
/**
|
|
792
|
+
* `adr mcp` (design §7.1): the MCP server over stdio. Loaded with a dynamic import, so no other
|
|
793
|
+
* command loads the MCP SDK.
|
|
794
|
+
*/
|
|
795
|
+
function mcpCommand(version) {
|
|
796
|
+
return {
|
|
797
|
+
name: "mcp",
|
|
798
|
+
summary: "start the MCP server over stdio",
|
|
799
|
+
usage: "mcp",
|
|
800
|
+
options: {},
|
|
801
|
+
positionals: {
|
|
802
|
+
min: 0,
|
|
803
|
+
max: 0,
|
|
804
|
+
names: []
|
|
805
|
+
},
|
|
806
|
+
ref: "reject",
|
|
807
|
+
async run({ io, flags }) {
|
|
808
|
+
if (flags.json) throw new UsageError("adr mcp speaks JSON-RPC on stdout; it takes no --json");
|
|
809
|
+
if (io.interactive) io.stderr.write("adr: adr mcp speaks MCP on stdin and stdout; start it from an MCP client\n");
|
|
810
|
+
const { serveAdrMcp } = await import("./server-CXCO5zfp.js");
|
|
811
|
+
await serveAdrMcp({
|
|
812
|
+
cwd: io.cwd,
|
|
813
|
+
root: flags.root,
|
|
814
|
+
env: io.env,
|
|
815
|
+
version,
|
|
816
|
+
stderr: io.stderr
|
|
817
|
+
});
|
|
818
|
+
return {
|
|
819
|
+
exit: 0,
|
|
820
|
+
json: null,
|
|
821
|
+
text: ""
|
|
822
|
+
};
|
|
823
|
+
}
|
|
824
|
+
};
|
|
825
|
+
}
|
|
560
826
|
// src/cli/editor.ts
|
|
561
827
|
/**
|
|
562
828
|
* The user's editor as an `edit` callback (design §5.7), or `null` when neither `VISUAL` nor
|
|
@@ -895,6 +1161,38 @@ const scopeCommand = {
|
|
|
895
1161
|
};
|
|
896
1162
|
}
|
|
897
1163
|
};
|
|
1164
|
+
// src/cli/search.ts
|
|
1165
|
+
/** `adr search <query>… [--all]` (design §8.2): words are joined, and every one must match. */
|
|
1166
|
+
const searchCommand = {
|
|
1167
|
+
name: "search",
|
|
1168
|
+
summary: "search decisions' ids, titles, bodies and notes",
|
|
1169
|
+
usage: "search <query>… [--all]",
|
|
1170
|
+
options: { all: { type: "boolean" } },
|
|
1171
|
+
positionals: {
|
|
1172
|
+
min: 1,
|
|
1173
|
+
max: 20,
|
|
1174
|
+
names: ["query"]
|
|
1175
|
+
},
|
|
1176
|
+
ref: "read",
|
|
1177
|
+
async run({ io, flags, values, positionals }) {
|
|
1178
|
+
const read = await readDecisions(await openForCli(io, flags));
|
|
1179
|
+
const doc = searchDecisions(read, {
|
|
1180
|
+
query: positionals.join(" "),
|
|
1181
|
+
includeInactive: values.all === true
|
|
1182
|
+
});
|
|
1183
|
+
const notes = [...notesWith(doc.notes, numberingNote(read, { unavailable: false }))];
|
|
1184
|
+
if (doc.inactive_omitted > 0) {
|
|
1185
|
+
const k = doc.inactive_omitted;
|
|
1186
|
+
notes.push(`${k === 1 ? "1 inactive decision also matches" : `${k} inactive decisions also match`}; add --all to see them`);
|
|
1187
|
+
}
|
|
1188
|
+
return {
|
|
1189
|
+
exit: 0,
|
|
1190
|
+
json: doc,
|
|
1191
|
+
text: searchText(doc),
|
|
1192
|
+
notes
|
|
1193
|
+
};
|
|
1194
|
+
}
|
|
1195
|
+
};
|
|
898
1196
|
// src/cli/show.ts
|
|
899
1197
|
/** `adr show <decision> [--history]` (design §5.6). */
|
|
900
1198
|
const showCommand = {
|
|
@@ -917,7 +1215,7 @@ const showCommand = {
|
|
|
917
1215
|
exit: 0,
|
|
918
1216
|
json: doc,
|
|
919
1217
|
text: showText(doc, display),
|
|
920
|
-
notes: doc.notes
|
|
1218
|
+
notes: notesWith(doc.notes, numberingNote(read, { unavailable: false }))
|
|
921
1219
|
};
|
|
922
1220
|
}
|
|
923
1221
|
};
|
|
@@ -1032,7 +1330,7 @@ function adrCli(version) {
|
|
|
1032
1330
|
version,
|
|
1033
1331
|
summary: "Architecture decision records kept as event logs in git.",
|
|
1034
1332
|
commands: [
|
|
1035
|
-
initCommand,
|
|
1333
|
+
initCommand(version),
|
|
1036
1334
|
newCommand,
|
|
1037
1335
|
acceptCommand,
|
|
1038
1336
|
rejectCommand,
|
|
@@ -1042,10 +1340,14 @@ function adrCli(version) {
|
|
|
1042
1340
|
noteCommand,
|
|
1043
1341
|
linkCommand,
|
|
1044
1342
|
scopeCommand,
|
|
1343
|
+
indexCommand,
|
|
1344
|
+
hooksCommand,
|
|
1045
1345
|
forCommand,
|
|
1046
1346
|
listCommand,
|
|
1047
1347
|
showCommand,
|
|
1048
|
-
|
|
1348
|
+
searchCommand,
|
|
1349
|
+
verify,
|
|
1350
|
+
mcpCommand(version)
|
|
1049
1351
|
]
|
|
1050
1352
|
};
|
|
1051
1353
|
}
|