doctrina-cli 0.12.0 → 0.14.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 +1 -1
- package/package.json +3 -2
- package/src/commands/adapter.js +205 -0
- package/src/commands/analyze.js +76 -19
- package/src/commands/change.js +341 -44
- package/src/commands/clarify.js +40 -19
- package/src/commands/close.js +119 -11
- package/src/commands/completion.js +6 -0
- package/src/commands/constitution.js +8 -1
- package/src/commands/context.js +359 -53
- package/src/commands/contract.js +17 -24
- package/src/commands/coverage.js +16 -4
- package/src/commands/decision.js +172 -40
- package/src/commands/doctor.js +38 -7
- package/src/commands/handoff.js +8 -1
- package/src/commands/hooks.js +6 -0
- package/src/commands/index-rebuild.js +8 -1
- package/src/commands/init.js +152 -6
- package/src/commands/intake.js +11 -2
- package/src/commands/intent.js +8 -1
- package/src/commands/metrics.js +80 -7
- package/src/commands/next.js +14 -2
- package/src/commands/prime.js +8 -1
- package/src/commands/report.js +8 -1
- package/src/commands/review.js +8 -1
- package/src/commands/search.js +8 -1
- package/src/commands/show.js +8 -1
- package/src/commands/skill.js +16 -6
- package/src/commands/spec.js +13 -6
- package/src/commands/status.js +14 -2
- package/src/commands/templates.js +277 -40
- package/src/commands/trace.js +15 -3
- package/src/commands/upgrade.js +23 -12
- package/src/commands/validate.js +93 -3
- package/src/commands/verify.js +12 -2
- package/src/commands/watch.js +8 -1
- package/src/commands/why.js +10 -24
- package/src/commands/work.js +69 -7
- package/src/index.js +60 -6
- package/src/lib/adapters.js +140 -0
- package/src/lib/adr-guard.js +47 -0
- package/src/lib/args.js +10 -0
- package/src/lib/clarity.js +1 -0
- package/src/lib/colors.js +1 -0
- package/src/lib/commands.js +314 -3
- package/src/lib/criteria.js +1 -0
- package/src/lib/dates.js +1 -0
- package/src/lib/diff.js +1 -0
- package/src/lib/doc-model.js +287 -0
- package/src/lib/docs-impact.js +167 -0
- package/src/lib/ears.js +1 -0
- package/src/lib/exit-codes.js +103 -0
- package/src/lib/flag-catalog.js +106 -0
- package/src/lib/fs-ops.js +1 -0
- package/src/lib/gates.js +123 -0
- package/src/lib/git.js +97 -0
- package/src/lib/index-json.js +14 -6
- package/src/lib/json-out.js +83 -0
- package/src/lib/prompt.js +18 -1
- package/src/lib/scan.js +81 -33
- package/src/lib/spec-ops.js +228 -3
- package/src/lib/suggest.js +1 -0
- package/src/lib/templates.js +89 -0
- package/src/lib/usage.js +89 -0
- package/src/lib/version.js +1 -0
- package/templates/.gitkeep +0 -0
- package/templates/AGENTS.md.template +71 -75
- package/templates/change/spec-delta.md.template +13 -7
package/README.md
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
> CLI for the Doctrina framework. The package is published as
|
|
4
4
|
> `doctrina-cli`; the executable it installs is `doctrina`.
|
|
5
5
|
|
|
6
|
-
**Status:** v0.
|
|
6
|
+
**Status:** v0.14.0 — current release. Zero runtime dependencies
|
|
7
7
|
(Node.js standard library only).
|
|
8
8
|
|
|
9
9
|
## Install
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "doctrina-cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.14.0",
|
|
4
4
|
"description": "Doctrina CLI — spec-driven, AGENTS.md-native framework for multi-agent AI development.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"keywords": [
|
|
@@ -19,7 +19,8 @@
|
|
|
19
19
|
},
|
|
20
20
|
"scripts": {
|
|
21
21
|
"test": "node --test",
|
|
22
|
-
"prepack": "node scripts/copy-templates.js"
|
|
22
|
+
"prepack": "node scripts/copy-templates.js",
|
|
23
|
+
"test:e2e": "node ../../scripts/e2e-packed.mjs"
|
|
23
24
|
},
|
|
24
25
|
"files": [
|
|
25
26
|
"src",
|
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
// @ts-check
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import process from "node:process";
|
|
4
|
+
import { exists, isFile, mkdirp, read, relPath, remove, write } from "../lib/fs-ops.js";
|
|
5
|
+
import {
|
|
6
|
+
ADAPTER_STATES, adapterFiles, describeAdapter, listAdapterNames, renderAdapterFile,
|
|
7
|
+
} from "../lib/adapters.js";
|
|
8
|
+
import { flagBool } from "../lib/args.js";
|
|
9
|
+
import { today } from "../lib/dates.js";
|
|
10
|
+
import { c } from "../lib/colors.js";
|
|
11
|
+
import { suggest } from "../lib/suggest.js";
|
|
12
|
+
import { notADoctrinaProject } from "../lib/exit-codes.js";
|
|
13
|
+
|
|
14
|
+
// `doctrina adapter` — add, remove, and inventory agent adapters (audit
|
|
15
|
+
// item C1).
|
|
16
|
+
//
|
|
17
|
+
// Before this command the only way to add an adapter to an existing project
|
|
18
|
+
// was `doctrina init --agent <name> --force`, because `init` refuses to run
|
|
19
|
+
// twice. That path regenerated AGENTS.md and .doctrina/product.md from
|
|
20
|
+
// blank templates, silently destroying every hand-authored rule and the
|
|
21
|
+
// product definition. `adapter add` is strictly additive: it writes that
|
|
22
|
+
// adapter's own files and never reads or writes AGENTS.md, product.md, or
|
|
23
|
+
// any other project artifact.
|
|
24
|
+
|
|
25
|
+
const SUBCOMMANDS = ["list", "add", "remove"];
|
|
26
|
+
|
|
27
|
+
export const flags = { boolean: ["json", "force"], string: [] };
|
|
28
|
+
|
|
29
|
+
export async function run(positional, cmdFlags) {
|
|
30
|
+
const sub = positional[0];
|
|
31
|
+
if (!SUBCOMMANDS.includes(sub)) {
|
|
32
|
+
console.error(c.red("error:") + ` unknown adapter subcommand "${sub ?? ""}"`);
|
|
33
|
+
const guess = suggest(sub, SUBCOMMANDS);
|
|
34
|
+
console.error(c.gray("hint: ") + (guess
|
|
35
|
+
? `did you mean \`doctrina adapter ${guess}\`?`
|
|
36
|
+
: `available: ${SUBCOMMANDS.join(", ")}`));
|
|
37
|
+
return 2;
|
|
38
|
+
}
|
|
39
|
+
const projectRoot = process.cwd();
|
|
40
|
+
ensureDoctrinaProject(projectRoot);
|
|
41
|
+
if (sub === "list") return adapterList(projectRoot);
|
|
42
|
+
if (sub === "add") return adapterAdd(projectRoot, positional[1], cmdFlags);
|
|
43
|
+
return adapterRemove(projectRoot, positional[1], cmdFlags);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function adapterList(projectRoot) {
|
|
47
|
+
const names = listAdapterNames(projectRoot);
|
|
48
|
+
if (names.length === 0) {
|
|
49
|
+
console.log(c.gray("no adapters available (no templates/adapters directory found)"));
|
|
50
|
+
return 0;
|
|
51
|
+
}
|
|
52
|
+
const rows = names.map((n) => describeAdapter(projectRoot, n));
|
|
53
|
+
const label = {
|
|
54
|
+
[ADAPTER_STATES.INSTALLED]: c.green("installed"),
|
|
55
|
+
[ADAPTER_STATES.AVAILABLE]: c.gray("available"),
|
|
56
|
+
[ADAPTER_STATES.NATIVE]: c.cyan("native "),
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
console.log(c.bold("Agent adapters:"));
|
|
60
|
+
console.log("");
|
|
61
|
+
for (const r of rows) {
|
|
62
|
+
const suffix = r.state === ADAPTER_STATES.NATIVE
|
|
63
|
+
? c.gray("reads AGENTS.md directly — no file needed")
|
|
64
|
+
: c.gray(`${r.installed.length}/${r.files.length} file${r.files.length === 1 ? "" : "s"}` +
|
|
65
|
+
(r.source === "project" ? " · project template" : ""));
|
|
66
|
+
console.log(` ${label[r.state]} ${c.cyan(r.name.padEnd(10))} ${suffix}`);
|
|
67
|
+
}
|
|
68
|
+
console.log("");
|
|
69
|
+
const installed = rows.filter((r) => r.state === ADAPTER_STATES.INSTALLED).length;
|
|
70
|
+
const native = rows.filter((r) => r.state === ADAPTER_STATES.NATIVE).length;
|
|
71
|
+
console.log(c.gray(
|
|
72
|
+
`${installed} installed · ${rows.length - installed - native} available · ${native} native. ` +
|
|
73
|
+
"Add one: ") + c.cyan("doctrina adapter add <name>"));
|
|
74
|
+
return 0;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function adapterAdd(projectRoot, name, cmdFlags) {
|
|
78
|
+
if (!name) {
|
|
79
|
+
console.error(c.red("error:") + " adapter add requires a name (see `doctrina adapter list`)");
|
|
80
|
+
return 2;
|
|
81
|
+
}
|
|
82
|
+
const spec = adapterFiles(projectRoot, name);
|
|
83
|
+
if (!spec) {
|
|
84
|
+
console.error(c.red("error:") + ` unknown adapter "${name}"`);
|
|
85
|
+
const guess = suggest(name, listAdapterNames(projectRoot));
|
|
86
|
+
console.error(c.gray("hint: ") + (guess
|
|
87
|
+
? `did you mean \`doctrina adapter add ${guess}\`?`
|
|
88
|
+
: "run `doctrina adapter list` for the available adapters"));
|
|
89
|
+
return 2;
|
|
90
|
+
}
|
|
91
|
+
if (spec.files.length === 0) {
|
|
92
|
+
console.log(c.cyan("native ") + ` ${name} reads AGENTS.md directly — nothing to install`);
|
|
93
|
+
console.log(c.gray("Its rules already reach the agent through the hub at AGENTS.md."));
|
|
94
|
+
return 0;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const force = flagBool(cmdFlags, "force", false);
|
|
98
|
+
const tokens = {
|
|
99
|
+
PROJECT_NAME: path.basename(projectRoot),
|
|
100
|
+
PROJECT_DESCRIPTION: "",
|
|
101
|
+
DATE: today(),
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
// Additive by construction: only this adapter's own files are touched,
|
|
105
|
+
// and an existing one is left alone unless --force is given. AGENTS.md
|
|
106
|
+
// and product.md are never read or written here.
|
|
107
|
+
let written = 0;
|
|
108
|
+
let skipped = 0;
|
|
109
|
+
for (const file of spec.files) {
|
|
110
|
+
const dest = path.join(projectRoot, file.relativePath);
|
|
111
|
+
if (exists(dest) && !force) {
|
|
112
|
+
console.log(c.yellow("skip ") + ` ${file.relativePath} (already present; --force overwrites)`);
|
|
113
|
+
skipped += 1;
|
|
114
|
+
continue;
|
|
115
|
+
}
|
|
116
|
+
mkdirp(path.dirname(dest));
|
|
117
|
+
write(dest, renderAdapterFile(file, tokens), { force: true });
|
|
118
|
+
console.log(c.green("created") + ` ${relPath(projectRoot, dest)}`);
|
|
119
|
+
written += 1;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
console.log("");
|
|
123
|
+
console.log(`${c.bold("Adapter " + name)} — ${written} file${written === 1 ? "" : "s"} written` +
|
|
124
|
+
(skipped > 0 ? `, ${skipped} left alone` : "") +
|
|
125
|
+
(spec.source === "project" ? c.gray(" (from .doctrina/templates/adapters/)") : ""));
|
|
126
|
+
console.log(c.gray("Additive only: AGENTS.md and .doctrina/product.md were not touched."));
|
|
127
|
+
return 0;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function adapterRemove(projectRoot, name, cmdFlags) {
|
|
131
|
+
if (!name) {
|
|
132
|
+
console.error(c.red("error:") + " adapter remove requires a name (see `doctrina adapter list`)");
|
|
133
|
+
return 2;
|
|
134
|
+
}
|
|
135
|
+
const spec = adapterFiles(projectRoot, name);
|
|
136
|
+
if (!spec) {
|
|
137
|
+
console.error(c.red("error:") + ` unknown adapter "${name}"`);
|
|
138
|
+
return 2;
|
|
139
|
+
}
|
|
140
|
+
if (spec.files.length === 0) {
|
|
141
|
+
console.log(c.cyan("native ") + ` ${name} installs no files — nothing to remove`);
|
|
142
|
+
return 0;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// Only delete files this adapter created AND that still match what it
|
|
146
|
+
// would write. A file the user edited is theirs now; --force overrides.
|
|
147
|
+
const force = flagBool(cmdFlags, "force", false);
|
|
148
|
+
const tokens = { PROJECT_NAME: path.basename(projectRoot), PROJECT_DESCRIPTION: "", DATE: today() };
|
|
149
|
+
let removed = 0;
|
|
150
|
+
let kept = 0;
|
|
151
|
+
for (const file of spec.files) {
|
|
152
|
+
const dest = path.join(projectRoot, file.relativePath);
|
|
153
|
+
if (!isFile(dest)) continue;
|
|
154
|
+
const pristine = normalise(read(dest)) === normalise(renderAdapterFile(file, tokens));
|
|
155
|
+
if (!pristine && !force) {
|
|
156
|
+
console.log(c.yellow("kept ") + ` ${file.relativePath} (edited since install; --force removes it)`);
|
|
157
|
+
kept += 1;
|
|
158
|
+
continue;
|
|
159
|
+
}
|
|
160
|
+
remove(dest);
|
|
161
|
+
console.log(c.green("removed") + ` ${file.relativePath}`);
|
|
162
|
+
removed += 1;
|
|
163
|
+
}
|
|
164
|
+
console.log("");
|
|
165
|
+
console.log(`${c.bold("Adapter " + name)} — ${removed} file${removed === 1 ? "" : "s"} removed` +
|
|
166
|
+
(kept > 0 ? `, ${kept} kept (edited)` : ""));
|
|
167
|
+
return 0;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
function normalise(text) {
|
|
171
|
+
return text.replace(/\r\n/g, "\n").trim();
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
function ensureDoctrinaProject(projectRoot) {
|
|
175
|
+
if (!exists(path.join(projectRoot, ".doctrina"))) {
|
|
176
|
+
throw notADoctrinaProject();
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
export const help = `
|
|
181
|
+
Usage: doctrina adapter <list|add|remove> [name] [--force]
|
|
182
|
+
|
|
183
|
+
Add, remove, and inventory the per-agent adapter files that point at
|
|
184
|
+
AGENTS.md. Adding an adapter is strictly additive: only that adapter's own
|
|
185
|
+
files are written, and AGENTS.md / .doctrina/product.md are never touched.
|
|
186
|
+
|
|
187
|
+
Subcommands:
|
|
188
|
+
list Every adapter with its state:
|
|
189
|
+
installed — its files are present
|
|
190
|
+
available — it ships files and none are installed
|
|
191
|
+
native — the agent reads AGENTS.md directly and
|
|
192
|
+
needs no file at all
|
|
193
|
+
add <name> Install one adapter's files. An existing file is left
|
|
194
|
+
alone unless --force is given.
|
|
195
|
+
remove <name> Delete the files this adapter created. A file edited
|
|
196
|
+
since install is kept unless --force is given.
|
|
197
|
+
|
|
198
|
+
Custom adapters: a directory at .doctrina/templates/adapters/<name>/ is
|
|
199
|
+
installable by name and takes precedence over a bundled adapter of the
|
|
200
|
+
same name.
|
|
201
|
+
|
|
202
|
+
Options:
|
|
203
|
+
--force With add, overwrite an existing file; with remove,
|
|
204
|
+
delete a file that was edited after install.
|
|
205
|
+
`;
|
package/src/commands/analyze.js
CHANGED
|
@@ -1,9 +1,15 @@
|
|
|
1
|
+
// @ts-check
|
|
1
2
|
import path from "node:path";
|
|
2
3
|
import process from "node:process";
|
|
3
4
|
import { exists, isDir, isFile, read, relPath, walk } from "../lib/fs-ops.js";
|
|
4
5
|
import { c } from "../lib/colors.js";
|
|
5
6
|
import { isUntouchedScaffold } from "./change.js";
|
|
6
7
|
|
|
8
|
+
// Flags this command accepts. Declared HERE, with the command, so
|
|
9
|
+
// adding a command never requires editing the entrypoint — the gap that
|
|
10
|
+
// let six flags ship undeclared and silently swallow a positional (C3).
|
|
11
|
+
export const flags = { boolean: ["json"], string: [] };
|
|
12
|
+
|
|
7
13
|
export async function run(positional, _flags) {
|
|
8
14
|
const id = positional[0];
|
|
9
15
|
if (!id) {
|
|
@@ -21,6 +27,25 @@ export async function run(positional, _flags) {
|
|
|
21
27
|
console.log(`analyzing ${relPath(projectRoot, changeDir)}/`);
|
|
22
28
|
console.log("");
|
|
23
29
|
|
|
30
|
+
const results = collectAnalysis(projectRoot, changeDir);
|
|
31
|
+
for (const r of results) console.log(r.line);
|
|
32
|
+
console.log("");
|
|
33
|
+
|
|
34
|
+
const failed = results.filter((r) => r.kind === "fail").length;
|
|
35
|
+
if (failed === 0) {
|
|
36
|
+
console.log(c.green("ok") + ` ready to apply`);
|
|
37
|
+
} else {
|
|
38
|
+
console.log(c.red("fail") + ` ${failed} issue${failed === 1 ? "" : "s"}`);
|
|
39
|
+
}
|
|
40
|
+
return failed === 0 ? 0 : 1;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// The structural findings for a change, as data. Exported so the shared
|
|
44
|
+
// gate map (`lib/gates.js`) can enforce the SAME checks on every
|
|
45
|
+
// transition that needs them, instead of each command deciding for itself
|
|
46
|
+
// — `change apply` used to mutate a change that `analyze` had just
|
|
47
|
+
// refused (audit item C6).
|
|
48
|
+
export function collectAnalysis(projectRoot, changeDir) {
|
|
24
49
|
const results = [];
|
|
25
50
|
|
|
26
51
|
// proposal.md
|
|
@@ -32,6 +57,37 @@ export async function run(positional, _flags) {
|
|
|
32
57
|
const text = read(proposalPath);
|
|
33
58
|
if (/^##\s+Why\b/m.test(text)) results.push(pass(`proposal.md has a "## Why" section`));
|
|
34
59
|
else results.push(fail(`proposal.md missing "## Why" section`));
|
|
60
|
+
|
|
61
|
+
// A section that still holds only its scaffold comment was never
|
|
62
|
+
// written. The check above only proved the HEADING survived, which is
|
|
63
|
+
// why six changes closed in one session with every rationale section
|
|
64
|
+
// empty: the planning step wrote to the file with a pattern that did
|
|
65
|
+
// not match its line endings, and nothing downstream looked inside.
|
|
66
|
+
// For a framework whose whole premise is recoverable provenance, an
|
|
67
|
+
// archived change that cannot say why it happened is the defect.
|
|
68
|
+
// Only `Why` and `What` are required. They are what makes a change
|
|
69
|
+
// recoverable a year later — the reason it exists and the shape it
|
|
70
|
+
// took. `Scope boundaries` and `Open questions` are legitimately empty
|
|
71
|
+
// on a change that has neither, and demanding the word "None." there
|
|
72
|
+
// is friction that buys nothing.
|
|
73
|
+
const hollow = [];
|
|
74
|
+
for (const m of text.matchAll(/^##\s+(.+?)[ \t]*\r?$/gm)) {
|
|
75
|
+
const heading = m[1].trim();
|
|
76
|
+
if (!/^(Why|What)$/i.test(heading)) continue;
|
|
77
|
+
const start = m.index + m[0].length;
|
|
78
|
+
const next = text.slice(start).search(/^##\s+/m);
|
|
79
|
+
const body = (next < 0 ? text.slice(start) : text.slice(start, start + next));
|
|
80
|
+
// Strip comments; anything left that is not whitespace is real prose.
|
|
81
|
+
if (body.replace(/<!--[\s\S]*?-->/g, "").trim() === "") hollow.push(heading);
|
|
82
|
+
}
|
|
83
|
+
if (hollow.length > 0) {
|
|
84
|
+
results.push(fail(
|
|
85
|
+
`proposal.md has ${hollow.length} unwritten section${hollow.length === 1 ? "" : "s"} ` +
|
|
86
|
+
`(${hollow.join(", ")}) — a heading that survived is not a section that was written`,
|
|
87
|
+
));
|
|
88
|
+
} else {
|
|
89
|
+
results.push(pass("proposal.md states why and what"));
|
|
90
|
+
}
|
|
35
91
|
}
|
|
36
92
|
|
|
37
93
|
// tasks.md
|
|
@@ -41,6 +97,16 @@ export async function run(positional, _flags) {
|
|
|
41
97
|
} else {
|
|
42
98
|
results.push(pass("tasks.md present"));
|
|
43
99
|
const text = read(tasksPath);
|
|
100
|
+
// Scaffold placeholders never replaced ("- [ ]" with no text, checked or
|
|
101
|
+
// not) mean the change was OPENED but never PLANNED — the agent went
|
|
102
|
+
// straight to implementing on a hollow change and nothing barked
|
|
103
|
+
// (operator report 2026-07-19). A hard failure here blocks `change
|
|
104
|
+
// check` and `close` until the plan is real; ticking an empty box does
|
|
105
|
+
// not help, `change tick` refuses those too.
|
|
106
|
+
const placeholders = (text.match(/^\s*-\s*\[[ xX]\]\s*$/gm) ?? []).length;
|
|
107
|
+
if (placeholders > 0) {
|
|
108
|
+
results.push(fail(`tasks.md still carries ${placeholders} scaffold placeholder task${placeholders === 1 ? "" : "s"} ("- [ ]" with no text) — plan the change before implementing: replace them with real tasks (or delete the lines)`));
|
|
109
|
+
}
|
|
44
110
|
if (/^\s*-\s*\[\s\]/m.test(text)) results.push(pass("tasks.md has at least one unchecked task"));
|
|
45
111
|
else results.push(info("tasks.md has no unchecked tasks (already done?)"));
|
|
46
112
|
}
|
|
@@ -65,11 +131,11 @@ export async function run(positional, _flags) {
|
|
|
65
131
|
const cap = capMatch ? capMatch[1] : path.basename(path.dirname(deltaPath));
|
|
66
132
|
|
|
67
133
|
if (!op) {
|
|
68
|
-
results.push(fail(` ${rel}: Operation header missing or malformed
|
|
134
|
+
results.push(fail(` ${rel}: Operation header missing or malformed`));
|
|
69
135
|
continue;
|
|
70
136
|
}
|
|
71
137
|
if (!["ADDED", "MODIFIED", "REMOVED"].includes(op)) {
|
|
72
|
-
results.push(fail(` ${rel}: Operation "${op}" is not one of ADDED|MODIFIED|REMOVED
|
|
138
|
+
results.push(fail(` ${rel}: Operation "${op}" is not one of ADDED|MODIFIED|REMOVED`));
|
|
73
139
|
continue;
|
|
74
140
|
}
|
|
75
141
|
const targetSpec = path.join(projectRoot, ".doctrina", "specs", cap, "spec.md");
|
|
@@ -79,38 +145,29 @@ export async function run(positional, _flags) {
|
|
|
79
145
|
// untouched `spec new` scaffold is the canonical flow (spec new →
|
|
80
146
|
// ADDED delta), so it passes as a replacement; only real content fails.
|
|
81
147
|
if (exists(targetSpec) && !isUntouchedScaffold(read(targetSpec), cap)) {
|
|
82
|
-
results.push(fail(` ${cap} (ADDED) but target ${targetRel} has real content — use MODIFIED or remove it first
|
|
148
|
+
results.push(fail(` ${cap} (ADDED) but target ${targetRel} has real content — use MODIFIED or remove it first`));
|
|
83
149
|
} else if (exists(targetSpec)) {
|
|
84
|
-
results.push(pass(` ${cap} (ADDED) → ${targetRel} (replaces the untouched scaffold)
|
|
150
|
+
results.push(pass(` ${cap} (ADDED) → ${targetRel} (replaces the untouched scaffold)`));
|
|
85
151
|
} else {
|
|
86
|
-
results.push(pass(` ${cap} (ADDED) → ${targetRel} (new)
|
|
152
|
+
results.push(pass(` ${cap} (ADDED) → ${targetRel} (new)`));
|
|
87
153
|
}
|
|
88
154
|
} else if (op === "MODIFIED") {
|
|
89
155
|
if (!exists(targetSpec)) {
|
|
90
|
-
results.push(fail(` ${cap} (MODIFIED) but target ${targetRel} does not exist
|
|
156
|
+
results.push(fail(` ${cap} (MODIFIED) but target ${targetRel} does not exist`));
|
|
91
157
|
} else {
|
|
92
|
-
results.push(pass(` ${cap} (MODIFIED) → ${targetRel}
|
|
158
|
+
results.push(pass(` ${cap} (MODIFIED) → ${targetRel}`));
|
|
93
159
|
}
|
|
94
160
|
} else {
|
|
95
161
|
if (!exists(targetSpec)) {
|
|
96
|
-
results.push(fail(` ${cap} (REMOVED) but target ${targetRel} does not exist
|
|
162
|
+
results.push(fail(` ${cap} (REMOVED) but target ${targetRel} does not exist`));
|
|
97
163
|
} else {
|
|
98
|
-
results.push(pass(` ${cap} (REMOVED) → ${targetRel}
|
|
164
|
+
results.push(pass(` ${cap} (REMOVED) → ${targetRel}`));
|
|
99
165
|
}
|
|
100
166
|
}
|
|
101
167
|
}
|
|
102
168
|
}
|
|
103
169
|
|
|
104
|
-
|
|
105
|
-
console.log("");
|
|
106
|
-
|
|
107
|
-
const failed = results.filter((r) => r.kind === "fail").length;
|
|
108
|
-
if (failed === 0) {
|
|
109
|
-
console.log(c.green("ok") + ` ready to apply`);
|
|
110
|
-
} else {
|
|
111
|
-
console.log(c.red("fail") + ` ${failed} issue${failed === 1 ? "" : "s"}`);
|
|
112
|
-
}
|
|
113
|
-
return failed === 0 ? 0 : 1;
|
|
170
|
+
return results;
|
|
114
171
|
}
|
|
115
172
|
|
|
116
173
|
function pass(msg) {
|