@acta-dev/cli 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +46 -0
- package/dist/index.d.ts +9 -0
- package/dist/index.js +1152 -0
- package/package.json +62 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Boris Khakhin
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
# @acta-dev/cli
|
|
2
|
+
|
|
3
|
+
The `acta` command-line tool — TypeScript-first docs-as-code for **ADR** and **spec** documents in a Git repository.
|
|
4
|
+
|
|
5
|
+
Markdown stays the source of truth; Acta adds a strict document model, validation, graph artifacts and CLI workflows so a team can understand why decisions were made and which specs depend on them.
|
|
6
|
+
|
|
7
|
+
## Install
|
|
8
|
+
|
|
9
|
+
```sh
|
|
10
|
+
npm install -g @acta-dev/cli
|
|
11
|
+
# or run without installing:
|
|
12
|
+
npx @acta-dev/cli init
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
The installed binary is `acta`.
|
|
16
|
+
|
|
17
|
+
## Quick start
|
|
18
|
+
|
|
19
|
+
```sh
|
|
20
|
+
acta init
|
|
21
|
+
acta new adr "Adopt Acta"
|
|
22
|
+
acta new spec "Document workflow"
|
|
23
|
+
acta validate
|
|
24
|
+
acta build
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
`acta init` creates `acta.config.ts`, `docs/decisions/`, `docs/specs/` and starter templates under `docs/templates/`.
|
|
28
|
+
|
|
29
|
+
## Commands
|
|
30
|
+
|
|
31
|
+
| Command | Purpose |
|
|
32
|
+
|---|---|
|
|
33
|
+
| `acta init` | Create config, document folders and templates. |
|
|
34
|
+
| `acta new adr\|spec <title>` | Create a document with the next available ID. |
|
|
35
|
+
| `acta list` | List documents, filterable by kind/status/tag. |
|
|
36
|
+
| `acta show <id>` | Show metadata, sections, links and backlinks. |
|
|
37
|
+
| `acta validate` | Validate frontmatter, IDs, links, sections and graph rules. |
|
|
38
|
+
| `acta graph` | Print the relationship graph as Mermaid or JSON. |
|
|
39
|
+
| `acta build` | Write `.acta/dist` JSON artifacts. |
|
|
40
|
+
| `acta renumber <from> <to>` | Rename an ID and update internal links. |
|
|
41
|
+
|
|
42
|
+
Full reference: [github.com/jentix/acta](https://github.com/jentix/acta).
|
|
43
|
+
|
|
44
|
+
## License
|
|
45
|
+
|
|
46
|
+
MIT
|
package/dist/index.d.ts
ADDED
package/dist/index.js
ADDED
|
@@ -0,0 +1,1152 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/index.ts
|
|
4
|
+
import { realpathSync } from "fs";
|
|
5
|
+
import { fileURLToPath } from "url";
|
|
6
|
+
import { defineCommand as defineCommand9, runMain } from "citty";
|
|
7
|
+
|
|
8
|
+
// src/commands/build.ts
|
|
9
|
+
import { buildArtifacts } from "@acta-dev/core";
|
|
10
|
+
import { defineCommand } from "citty";
|
|
11
|
+
import kleur2 from "kleur";
|
|
12
|
+
|
|
13
|
+
// src/context.ts
|
|
14
|
+
import { existsSync } from "fs";
|
|
15
|
+
import { join, resolve } from "path";
|
|
16
|
+
import { loadConfig, resolveConfig } from "@acta-dev/core";
|
|
17
|
+
async function resolveContext(opts) {
|
|
18
|
+
const cwd = resolve(opts.cwd ?? process.cwd());
|
|
19
|
+
if (opts.config) {
|
|
20
|
+
const config2 = await loadConfig(opts.config);
|
|
21
|
+
return { config: config2, cwd };
|
|
22
|
+
}
|
|
23
|
+
for (const dir of [cwd, join(cwd, ".."), join(cwd, "../..")]) {
|
|
24
|
+
const candidate = join(dir, "acta.config.ts");
|
|
25
|
+
if (existsSync(candidate)) {
|
|
26
|
+
const config2 = await loadConfig(candidate);
|
|
27
|
+
return { config: config2, cwd };
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
const config = resolveConfig({}, { rootDir: cwd });
|
|
31
|
+
return { config, cwd };
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// src/output.ts
|
|
35
|
+
import kleur from "kleur";
|
|
36
|
+
function printLine(msg = "") {
|
|
37
|
+
process.stdout.write(`${msg}
|
|
38
|
+
`);
|
|
39
|
+
}
|
|
40
|
+
function printError(msg) {
|
|
41
|
+
process.stderr.write(`${kleur.red("error")} ${msg}
|
|
42
|
+
`);
|
|
43
|
+
}
|
|
44
|
+
function printWarn(msg) {
|
|
45
|
+
process.stderr.write(`${kleur.yellow("warn")} ${msg}
|
|
46
|
+
`);
|
|
47
|
+
}
|
|
48
|
+
function printSuccess(msg) {
|
|
49
|
+
process.stdout.write(`${kleur.green("\u2713")} ${msg}
|
|
50
|
+
`);
|
|
51
|
+
}
|
|
52
|
+
function printJson(value) {
|
|
53
|
+
process.stdout.write(`${JSON.stringify(value, null, 2)}
|
|
54
|
+
`);
|
|
55
|
+
}
|
|
56
|
+
function exitFailure(msg) {
|
|
57
|
+
if (msg) printError(msg);
|
|
58
|
+
process.exit(1);
|
|
59
|
+
}
|
|
60
|
+
function exitUsage(msg) {
|
|
61
|
+
process.stderr.write(`${kleur.red("usage error")} ${msg}
|
|
62
|
+
`);
|
|
63
|
+
process.exit(2);
|
|
64
|
+
}
|
|
65
|
+
function printIssues(issues) {
|
|
66
|
+
for (const issue of issues) {
|
|
67
|
+
const prefix = issue.severity === "error" ? kleur.red("\u2717 error") : kleur.yellow("\u26A0 warn ");
|
|
68
|
+
const location = issue.documentId ? kleur.bold(issue.documentId) : issue.path ?? "";
|
|
69
|
+
printLine(` ${prefix} ${location} ${issue.message}`);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
function printValidationSummary(errorCount, warningCount, valid) {
|
|
73
|
+
if (valid) {
|
|
74
|
+
printSuccess(
|
|
75
|
+
`Validation passed ${kleur.dim(`(${warningCount} warning${warningCount !== 1 ? "s" : ""})`)}`
|
|
76
|
+
);
|
|
77
|
+
} else {
|
|
78
|
+
printLine(
|
|
79
|
+
` ${kleur.red(`${errorCount} error${errorCount !== 1 ? "s" : ""}`)} ${kleur.yellow(`${warningCount} warning${warningCount !== 1 ? "s" : ""}`)}`
|
|
80
|
+
);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
function printTable(rows) {
|
|
84
|
+
if (rows.length === 0) return;
|
|
85
|
+
const widths = rows[0]?.map((_, col) => Math.max(...rows.map((row) => (row[col] ?? "").length)));
|
|
86
|
+
for (const row of rows) {
|
|
87
|
+
printLine(row.map((cell, col) => cell.padEnd(widths[col] ?? 0)).join(" "));
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// src/commands/build.ts
|
|
92
|
+
var buildCommand = defineCommand({
|
|
93
|
+
meta: {
|
|
94
|
+
name: "build",
|
|
95
|
+
description: "Build normalized JSON artifacts for the web viewer, CI and integrations"
|
|
96
|
+
},
|
|
97
|
+
args: {
|
|
98
|
+
json: {
|
|
99
|
+
type: "boolean",
|
|
100
|
+
description: "Print the build manifest as JSON",
|
|
101
|
+
default: false
|
|
102
|
+
},
|
|
103
|
+
config: {
|
|
104
|
+
type: "string",
|
|
105
|
+
alias: "c",
|
|
106
|
+
description: "Path to acta.config.ts"
|
|
107
|
+
}
|
|
108
|
+
},
|
|
109
|
+
async run({ args }) {
|
|
110
|
+
const { config } = await resolveContext({ config: args.config });
|
|
111
|
+
if (!args.json) {
|
|
112
|
+
printLine("Building artifacts...");
|
|
113
|
+
}
|
|
114
|
+
const result = await buildArtifacts({ config });
|
|
115
|
+
const { manifest, validation } = result;
|
|
116
|
+
if (args.json) {
|
|
117
|
+
printJson({ ...manifest, outDir: config.resolvedBuild.outDir });
|
|
118
|
+
process.exit(validation.errorCount > 0 ? 1 : 0);
|
|
119
|
+
return;
|
|
120
|
+
}
|
|
121
|
+
printLine();
|
|
122
|
+
printSuccess(`Build complete`);
|
|
123
|
+
printLine(` ${kleur2.bold("Documents:")} ${manifest.documentCount}`);
|
|
124
|
+
printLine(
|
|
125
|
+
` ${kleur2.bold("Errors:")} ${manifest.errorCount === 0 ? kleur2.green("0") : kleur2.red(String(manifest.errorCount))}`
|
|
126
|
+
);
|
|
127
|
+
printLine(
|
|
128
|
+
` ${kleur2.bold("Warnings:")} ${manifest.warningCount === 0 ? kleur2.dim("0") : kleur2.yellow(String(manifest.warningCount))}`
|
|
129
|
+
);
|
|
130
|
+
printLine(` ${kleur2.bold("Output:")} ${config.resolvedBuild.outDir}`);
|
|
131
|
+
printLine(` ${kleur2.bold("Built at:")} ${manifest.builtAt}`);
|
|
132
|
+
if (validation.errorCount > 0) {
|
|
133
|
+
printLine();
|
|
134
|
+
printWarn(
|
|
135
|
+
`Build completed with ${validation.errorCount} validation error${validation.errorCount !== 1 ? "s" : ""}. Run \`acta validate\` for details.`
|
|
136
|
+
);
|
|
137
|
+
process.exit(1);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
// src/commands/graph.ts
|
|
143
|
+
import { buildGraph, loadProject } from "@acta-dev/core";
|
|
144
|
+
import { defineCommand as defineCommand2 } from "citty";
|
|
145
|
+
var STATUS_FILL = {
|
|
146
|
+
accepted: "#4ade80",
|
|
147
|
+
proposed: "#60a5fa",
|
|
148
|
+
rejected: "#f87171",
|
|
149
|
+
deprecated: "#a3a3a3",
|
|
150
|
+
superseded: "#d1d5db",
|
|
151
|
+
active: "#4ade80",
|
|
152
|
+
draft: "#60a5fa",
|
|
153
|
+
paused: "#fbbf24",
|
|
154
|
+
implemented: "#34d399",
|
|
155
|
+
obsolete: "#a3a3a3"
|
|
156
|
+
};
|
|
157
|
+
function nodeLabel(node) {
|
|
158
|
+
const safeTitle = node.title.replace(/"/g, "'");
|
|
159
|
+
return `${node.id}["${node.id}<br/>${safeTitle}"]:::${node.kind}_${sanitizeStatus(node.status)}`;
|
|
160
|
+
}
|
|
161
|
+
function sanitizeStatus(status) {
|
|
162
|
+
return status.replace(/[^a-z0-9]/g, "_");
|
|
163
|
+
}
|
|
164
|
+
function edgeLabel(edge) {
|
|
165
|
+
return ` ${edge.source} -->|${edge.type}| ${edge.target}`;
|
|
166
|
+
}
|
|
167
|
+
function toMermaid(graph) {
|
|
168
|
+
const lines = ["flowchart LR"];
|
|
169
|
+
const classNames = /* @__PURE__ */ new Set();
|
|
170
|
+
for (const node of graph.nodes) {
|
|
171
|
+
lines.push(` ${nodeLabel(node)}`);
|
|
172
|
+
classNames.add(`${node.kind}_${sanitizeStatus(node.status)}`);
|
|
173
|
+
}
|
|
174
|
+
if (graph.edges.length > 0) {
|
|
175
|
+
lines.push("");
|
|
176
|
+
for (const edge of graph.edges) {
|
|
177
|
+
lines.push(edgeLabel(edge));
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
if (classNames.size > 0) {
|
|
181
|
+
lines.push("");
|
|
182
|
+
for (const cls of classNames) {
|
|
183
|
+
const status = cls.replace(/^adr_|^spec_/, "").replace(/_/g, "");
|
|
184
|
+
const fill = STATUS_FILL[status] ?? "#e5e7eb";
|
|
185
|
+
lines.push(` classDef ${cls} fill:${fill},stroke:#6b7280,color:#111827`);
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
return lines.join("\n");
|
|
189
|
+
}
|
|
190
|
+
var graphCommand = defineCommand2({
|
|
191
|
+
meta: {
|
|
192
|
+
name: "graph",
|
|
193
|
+
description: "Print the document relationship graph as Mermaid or JSON"
|
|
194
|
+
},
|
|
195
|
+
args: {
|
|
196
|
+
format: {
|
|
197
|
+
type: "string",
|
|
198
|
+
alias: "f",
|
|
199
|
+
description: "Output format: mermaid | json (default: mermaid)",
|
|
200
|
+
default: "mermaid"
|
|
201
|
+
},
|
|
202
|
+
config: {
|
|
203
|
+
type: "string",
|
|
204
|
+
alias: "c",
|
|
205
|
+
description: "Path to acta.config.ts"
|
|
206
|
+
}
|
|
207
|
+
},
|
|
208
|
+
async run({ args }) {
|
|
209
|
+
const fmt = args.format ?? "mermaid";
|
|
210
|
+
if (fmt !== "mermaid" && fmt !== "json") {
|
|
211
|
+
exitUsage(`Unknown format "${fmt}". Use: mermaid, json`);
|
|
212
|
+
}
|
|
213
|
+
const { config } = await resolveContext({ config: args.config });
|
|
214
|
+
const project = await loadProject({ config });
|
|
215
|
+
const graph = buildGraph(project.documents);
|
|
216
|
+
if (fmt === "json") {
|
|
217
|
+
printJson(graph);
|
|
218
|
+
} else {
|
|
219
|
+
printLine(toMermaid(graph));
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
// src/commands/init.ts
|
|
225
|
+
import { existsSync as existsSync2 } from "fs";
|
|
226
|
+
import { mkdir, writeFile } from "fs/promises";
|
|
227
|
+
import { join as join2, resolve as resolve2 } from "path";
|
|
228
|
+
import { createInterface } from "readline";
|
|
229
|
+
import { resolveConfig as resolveConfig2 } from "@acta-dev/core";
|
|
230
|
+
import { defineCommand as defineCommand3 } from "citty";
|
|
231
|
+
var ADR_TEMPLATE = `---
|
|
232
|
+
id: ADR-0000
|
|
233
|
+
kind: adr
|
|
234
|
+
title: Template ADR
|
|
235
|
+
status: proposed
|
|
236
|
+
date: YYYY-MM-DD
|
|
237
|
+
tags: []
|
|
238
|
+
component: []
|
|
239
|
+
owners: []
|
|
240
|
+
summary: Short summary of the architectural decision.
|
|
241
|
+
links:
|
|
242
|
+
related: []
|
|
243
|
+
supersedes: []
|
|
244
|
+
replacedBy: []
|
|
245
|
+
decidedBy: []
|
|
246
|
+
dependsOn: []
|
|
247
|
+
validates: []
|
|
248
|
+
references: []
|
|
249
|
+
---
|
|
250
|
+
|
|
251
|
+
# Context
|
|
252
|
+
|
|
253
|
+
Describe the forces, constraints, and problem that make this decision necessary.
|
|
254
|
+
|
|
255
|
+
# Decision
|
|
256
|
+
|
|
257
|
+
State the decision clearly.
|
|
258
|
+
|
|
259
|
+
# Consequences
|
|
260
|
+
|
|
261
|
+
Describe expected tradeoffs, follow-up work, and operational impact.
|
|
262
|
+
|
|
263
|
+
# Alternatives
|
|
264
|
+
|
|
265
|
+
List the meaningful options considered and why they were not chosen.
|
|
266
|
+
`;
|
|
267
|
+
var SPEC_TEMPLATE = `---
|
|
268
|
+
id: SPEC-0000
|
|
269
|
+
kind: spec
|
|
270
|
+
title: Template Spec
|
|
271
|
+
status: draft
|
|
272
|
+
date: YYYY-MM-DD
|
|
273
|
+
tags: []
|
|
274
|
+
component: []
|
|
275
|
+
owners: []
|
|
276
|
+
summary: Short summary of the technical specification.
|
|
277
|
+
links:
|
|
278
|
+
related: []
|
|
279
|
+
decidedBy: []
|
|
280
|
+
dependsOn: []
|
|
281
|
+
validates: []
|
|
282
|
+
references: []
|
|
283
|
+
---
|
|
284
|
+
|
|
285
|
+
# Summary
|
|
286
|
+
|
|
287
|
+
Describe the feature, system, or technical change.
|
|
288
|
+
|
|
289
|
+
# Goals
|
|
290
|
+
|
|
291
|
+
List the outcomes this spec must achieve.
|
|
292
|
+
|
|
293
|
+
# Requirements
|
|
294
|
+
|
|
295
|
+
Define functional and non-functional requirements.
|
|
296
|
+
|
|
297
|
+
# Proposed design
|
|
298
|
+
|
|
299
|
+
Describe the planned design.
|
|
300
|
+
|
|
301
|
+
# Open questions
|
|
302
|
+
|
|
303
|
+
Track unresolved decisions.
|
|
304
|
+
`;
|
|
305
|
+
var CONFIG_TEMPLATE = `import { defineConfig } from "@acta-dev/core";
|
|
306
|
+
|
|
307
|
+
export default defineConfig({
|
|
308
|
+
docs: {
|
|
309
|
+
adrDir: "docs/decisions",
|
|
310
|
+
specDir: "docs/specs",
|
|
311
|
+
templatesDir: "docs/templates",
|
|
312
|
+
},
|
|
313
|
+
ids: {
|
|
314
|
+
adrPrefix: "ADR",
|
|
315
|
+
specPrefix: "SPEC",
|
|
316
|
+
width: 4,
|
|
317
|
+
},
|
|
318
|
+
validation: {
|
|
319
|
+
draftMaxAgeDays: 30,
|
|
320
|
+
requiredSections: {
|
|
321
|
+
adr: ["Context", "Decision", "Consequences"],
|
|
322
|
+
spec: ["Summary", "Goals", "Requirements"],
|
|
323
|
+
},
|
|
324
|
+
orphanDocuments: "warning",
|
|
325
|
+
asymmetricSupersedes: "error",
|
|
326
|
+
},
|
|
327
|
+
build: {
|
|
328
|
+
outDir: ".acta/dist",
|
|
329
|
+
cacheDir: ".acta/cache",
|
|
330
|
+
},
|
|
331
|
+
});
|
|
332
|
+
`;
|
|
333
|
+
var LEFTHOOK_TEMPLATE = `pre-commit:
|
|
334
|
+
commands:
|
|
335
|
+
biome:
|
|
336
|
+
glob: "*.{js,jsx,ts,tsx,json,jsonc,css,md,yml,yaml}"
|
|
337
|
+
run: pnpm exec biome check --write --files-ignore-unknown=true {staged_files}
|
|
338
|
+
stage_fixed: true
|
|
339
|
+
|
|
340
|
+
pre-push:
|
|
341
|
+
commands:
|
|
342
|
+
typecheck:
|
|
343
|
+
run: pnpm typecheck
|
|
344
|
+
test:
|
|
345
|
+
run: pnpm test
|
|
346
|
+
`;
|
|
347
|
+
var GITHUB_ACTION_TEMPLATE = `name: Acta CI
|
|
348
|
+
|
|
349
|
+
on:
|
|
350
|
+
pull_request:
|
|
351
|
+
push:
|
|
352
|
+
branches:
|
|
353
|
+
- main
|
|
354
|
+
|
|
355
|
+
jobs:
|
|
356
|
+
verify:
|
|
357
|
+
name: Verify
|
|
358
|
+
runs-on: ubuntu-latest
|
|
359
|
+
|
|
360
|
+
steps:
|
|
361
|
+
- name: Checkout
|
|
362
|
+
uses: actions/checkout@v4
|
|
363
|
+
|
|
364
|
+
- name: Setup pnpm
|
|
365
|
+
uses: pnpm/action-setup@v4
|
|
366
|
+
|
|
367
|
+
- name: Setup Node
|
|
368
|
+
uses: actions/setup-node@v4
|
|
369
|
+
with:
|
|
370
|
+
node-version-file: package.json
|
|
371
|
+
cache: pnpm
|
|
372
|
+
|
|
373
|
+
- name: Install dependencies
|
|
374
|
+
run: pnpm install --frozen-lockfile
|
|
375
|
+
|
|
376
|
+
- name: Lint
|
|
377
|
+
run: pnpm lint
|
|
378
|
+
|
|
379
|
+
- name: Check formatting
|
|
380
|
+
run: pnpm format:check
|
|
381
|
+
|
|
382
|
+
- name: Typecheck
|
|
383
|
+
run: pnpm typecheck
|
|
384
|
+
|
|
385
|
+
- name: Test
|
|
386
|
+
run: pnpm test
|
|
387
|
+
|
|
388
|
+
- name: Build
|
|
389
|
+
run: pnpm build
|
|
390
|
+
|
|
391
|
+
- name: Validate Acta docs
|
|
392
|
+
run: pnpm exec acta validate
|
|
393
|
+
|
|
394
|
+
- name: Build Acta artifacts
|
|
395
|
+
run: pnpm exec acta build
|
|
396
|
+
`;
|
|
397
|
+
async function confirm(message) {
|
|
398
|
+
return new Promise((resolvePromise) => {
|
|
399
|
+
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
400
|
+
rl.question(`${message} [y/N] `, (answer) => {
|
|
401
|
+
rl.close();
|
|
402
|
+
resolvePromise(answer.trim().toLowerCase() === "y");
|
|
403
|
+
});
|
|
404
|
+
});
|
|
405
|
+
}
|
|
406
|
+
async function safeWriteFile(filePath, content, yes) {
|
|
407
|
+
if (existsSync2(filePath)) {
|
|
408
|
+
if (!yes) {
|
|
409
|
+
const ok = await confirm(` Overwrite ${filePath}?`);
|
|
410
|
+
if (!ok) {
|
|
411
|
+
printWarn(`Skipped ${filePath}`);
|
|
412
|
+
return false;
|
|
413
|
+
}
|
|
414
|
+
} else {
|
|
415
|
+
printWarn(`Overwriting ${filePath}`);
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
await writeFile(filePath, content, "utf8");
|
|
419
|
+
return true;
|
|
420
|
+
}
|
|
421
|
+
var initCommand = defineCommand3({
|
|
422
|
+
meta: {
|
|
423
|
+
name: "init",
|
|
424
|
+
description: "Create Acta config, document folders and templates in the current repository"
|
|
425
|
+
},
|
|
426
|
+
args: {
|
|
427
|
+
yes: {
|
|
428
|
+
type: "boolean",
|
|
429
|
+
alias: "y",
|
|
430
|
+
description: "Skip prompts and overwrite existing files",
|
|
431
|
+
default: false
|
|
432
|
+
},
|
|
433
|
+
hooks: {
|
|
434
|
+
type: "boolean",
|
|
435
|
+
description: "Install Lefthook workflow template",
|
|
436
|
+
default: false
|
|
437
|
+
},
|
|
438
|
+
"github-action": {
|
|
439
|
+
type: "boolean",
|
|
440
|
+
description: "Install GitHub Actions workflow template",
|
|
441
|
+
default: false
|
|
442
|
+
},
|
|
443
|
+
config: {
|
|
444
|
+
type: "string",
|
|
445
|
+
alias: "c",
|
|
446
|
+
description: "Path to acta.config.ts (default: acta.config.ts)"
|
|
447
|
+
}
|
|
448
|
+
},
|
|
449
|
+
async run({ args }) {
|
|
450
|
+
const cwd = resolve2(process.cwd());
|
|
451
|
+
const yes = args.yes;
|
|
452
|
+
const config = resolveConfig2({}, { rootDir: cwd });
|
|
453
|
+
printLine("Initializing Acta docs structure...");
|
|
454
|
+
printLine();
|
|
455
|
+
const configPath = join2(cwd, "acta.config.ts");
|
|
456
|
+
const configWritten = await safeWriteFile(configPath, CONFIG_TEMPLATE, yes);
|
|
457
|
+
if (configWritten) printSuccess(`Created ${configPath}`);
|
|
458
|
+
const dirs = [
|
|
459
|
+
config.resolvedDocs.adrDir,
|
|
460
|
+
config.resolvedDocs.specDir,
|
|
461
|
+
config.resolvedDocs.templatesDir
|
|
462
|
+
];
|
|
463
|
+
for (const dir of dirs) {
|
|
464
|
+
await mkdir(dir, { recursive: true });
|
|
465
|
+
printSuccess(`Created dir ${dir}`);
|
|
466
|
+
}
|
|
467
|
+
const adrTplPath = join2(config.resolvedDocs.templatesDir, "adr.md");
|
|
468
|
+
const specTplPath = join2(config.resolvedDocs.templatesDir, "spec.md");
|
|
469
|
+
const adrWritten = await safeWriteFile(adrTplPath, ADR_TEMPLATE, yes);
|
|
470
|
+
if (adrWritten) printSuccess(`Created ${adrTplPath}`);
|
|
471
|
+
const specWritten = await safeWriteFile(specTplPath, SPEC_TEMPLATE, yes);
|
|
472
|
+
if (specWritten) printSuccess(`Created ${specTplPath}`);
|
|
473
|
+
const gitignorePath = join2(cwd, ".gitignore");
|
|
474
|
+
if (existsSync2(gitignorePath)) {
|
|
475
|
+
const { readFile: readFile3, appendFile } = await import("fs/promises");
|
|
476
|
+
const content = await readFile3(gitignorePath, "utf8");
|
|
477
|
+
if (!content.includes(".acta/")) {
|
|
478
|
+
await appendFile(gitignorePath, "\n# Acta build artifacts\n.acta/\n");
|
|
479
|
+
printSuccess(`Added .acta/ to .gitignore`);
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
if (args.hooks) {
|
|
483
|
+
const lefthookPath = join2(cwd, "lefthook.yml");
|
|
484
|
+
const lefthookWritten = await safeWriteFile(lefthookPath, LEFTHOOK_TEMPLATE, yes);
|
|
485
|
+
if (lefthookWritten) printSuccess(`Created ${lefthookPath}`);
|
|
486
|
+
}
|
|
487
|
+
if (args["github-action"]) {
|
|
488
|
+
const workflowsDir = join2(cwd, ".github", "workflows");
|
|
489
|
+
await mkdir(workflowsDir, { recursive: true });
|
|
490
|
+
const workflowPath = join2(workflowsDir, "acta-ci.yml");
|
|
491
|
+
const workflowWritten = await safeWriteFile(workflowPath, GITHUB_ACTION_TEMPLATE, yes);
|
|
492
|
+
if (workflowWritten) printSuccess(`Created ${workflowPath}`);
|
|
493
|
+
}
|
|
494
|
+
printLine();
|
|
495
|
+
printSuccess("Acta initialized. Run `acta validate` to check your documents.");
|
|
496
|
+
}
|
|
497
|
+
});
|
|
498
|
+
|
|
499
|
+
// src/commands/list.ts
|
|
500
|
+
import { loadProject as loadProject2 } from "@acta-dev/core";
|
|
501
|
+
import { defineCommand as defineCommand4 } from "citty";
|
|
502
|
+
import kleur3 from "kleur";
|
|
503
|
+
var STATUS_COLORS = {
|
|
504
|
+
// ADR
|
|
505
|
+
proposed: (s) => kleur3.cyan(s),
|
|
506
|
+
accepted: (s) => kleur3.green(s),
|
|
507
|
+
rejected: (s) => kleur3.red(s),
|
|
508
|
+
deprecated: (s) => kleur3.yellow(s),
|
|
509
|
+
superseded: (s) => kleur3.dim(s),
|
|
510
|
+
// Spec
|
|
511
|
+
draft: (s) => kleur3.cyan(s),
|
|
512
|
+
active: (s) => kleur3.green(s),
|
|
513
|
+
paused: (s) => kleur3.yellow(s),
|
|
514
|
+
implemented: (s) => kleur3.green(s),
|
|
515
|
+
obsolete: (s) => kleur3.dim(s)
|
|
516
|
+
};
|
|
517
|
+
function colorStatus(status) {
|
|
518
|
+
return (STATUS_COLORS[status] ?? ((s) => s))(status);
|
|
519
|
+
}
|
|
520
|
+
var listCommand = defineCommand4({
|
|
521
|
+
meta: {
|
|
522
|
+
name: "list",
|
|
523
|
+
description: "List ADR and spec documents with optional filters"
|
|
524
|
+
},
|
|
525
|
+
args: {
|
|
526
|
+
kind: {
|
|
527
|
+
type: "string",
|
|
528
|
+
alias: "k",
|
|
529
|
+
description: "Filter by kind: adr | spec"
|
|
530
|
+
},
|
|
531
|
+
status: {
|
|
532
|
+
type: "string",
|
|
533
|
+
alias: "s",
|
|
534
|
+
description: "Filter by status"
|
|
535
|
+
},
|
|
536
|
+
tag: {
|
|
537
|
+
type: "string",
|
|
538
|
+
alias: "t",
|
|
539
|
+
description: "Filter by tag"
|
|
540
|
+
},
|
|
541
|
+
json: {
|
|
542
|
+
type: "boolean",
|
|
543
|
+
description: "Output as JSON",
|
|
544
|
+
default: false
|
|
545
|
+
},
|
|
546
|
+
config: {
|
|
547
|
+
type: "string",
|
|
548
|
+
alias: "c",
|
|
549
|
+
description: "Path to acta.config.ts"
|
|
550
|
+
}
|
|
551
|
+
},
|
|
552
|
+
async run({ args }) {
|
|
553
|
+
const { config } = await resolveContext({ config: args.config });
|
|
554
|
+
const project = await loadProject2({ config });
|
|
555
|
+
let docs = project.documents;
|
|
556
|
+
if (args.kind) {
|
|
557
|
+
if (args.kind !== "adr" && args.kind !== "spec") {
|
|
558
|
+
process.stderr.write(`error: unknown kind "${args.kind}". Use: adr, spec
|
|
559
|
+
`);
|
|
560
|
+
process.exit(2);
|
|
561
|
+
}
|
|
562
|
+
docs = docs.filter((d) => d.kind === args.kind);
|
|
563
|
+
}
|
|
564
|
+
if (args.status) {
|
|
565
|
+
docs = docs.filter((d) => d.status === args.status);
|
|
566
|
+
}
|
|
567
|
+
if (args.tag) {
|
|
568
|
+
const tag = args.tag;
|
|
569
|
+
docs = docs.filter((d) => d.tags.includes(tag));
|
|
570
|
+
}
|
|
571
|
+
docs = docs.slice().sort((a, b) => {
|
|
572
|
+
if (a.kind !== b.kind) return a.kind.localeCompare(b.kind);
|
|
573
|
+
return a.id.localeCompare(b.id);
|
|
574
|
+
});
|
|
575
|
+
if (args.json) {
|
|
576
|
+
printJson(
|
|
577
|
+
docs.map((d) => ({
|
|
578
|
+
id: d.id,
|
|
579
|
+
kind: d.kind,
|
|
580
|
+
title: d.title,
|
|
581
|
+
status: d.status,
|
|
582
|
+
date: d.date,
|
|
583
|
+
tags: d.tags,
|
|
584
|
+
component: d.component,
|
|
585
|
+
owners: d.owners,
|
|
586
|
+
summary: d.summary
|
|
587
|
+
}))
|
|
588
|
+
);
|
|
589
|
+
return;
|
|
590
|
+
}
|
|
591
|
+
if (docs.length === 0) {
|
|
592
|
+
printLine("No documents found.");
|
|
593
|
+
return;
|
|
594
|
+
}
|
|
595
|
+
const rows = [
|
|
596
|
+
[kleur3.bold("ID"), kleur3.bold("KIND"), kleur3.bold("STATUS"), kleur3.bold("TITLE")],
|
|
597
|
+
...docs.map((d) => [kleur3.bold(d.id), d.kind, colorStatus(d.status), d.title])
|
|
598
|
+
];
|
|
599
|
+
printTable(rows);
|
|
600
|
+
printLine();
|
|
601
|
+
printLine(kleur3.dim(`${docs.length} document${docs.length !== 1 ? "s" : ""}`));
|
|
602
|
+
}
|
|
603
|
+
});
|
|
604
|
+
|
|
605
|
+
// src/commands/new.ts
|
|
606
|
+
import { existsSync as existsSync3 } from "fs";
|
|
607
|
+
import { writeFile as writeFile2 } from "fs/promises";
|
|
608
|
+
import { join as join4, relative } from "path";
|
|
609
|
+
import { adrStatuses, specStatuses } from "@acta-dev/core";
|
|
610
|
+
import { defineCommand as defineCommand5 } from "citty";
|
|
611
|
+
|
|
612
|
+
// src/id.ts
|
|
613
|
+
function allocateNextId(kind, documents, config) {
|
|
614
|
+
const prefix = kind === "adr" ? config.ids.adrPrefix : config.ids.specPrefix;
|
|
615
|
+
const existing = documents.filter((doc) => doc.kind === kind).map((doc) => {
|
|
616
|
+
const match = doc.id.match(/(\d+)$/);
|
|
617
|
+
return match?.[1] !== void 0 ? parseInt(match[1], 10) : 0;
|
|
618
|
+
});
|
|
619
|
+
const next = existing.length > 0 ? Math.max(...existing) + 1 : 1;
|
|
620
|
+
return `${prefix}-${String(next).padStart(config.ids.width, "0")}`;
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
// src/slug.ts
|
|
624
|
+
function titleToSlug(title) {
|
|
625
|
+
return title.toLowerCase().normalize("NFD").replace(/[̀-ͯ]/g, "").replace(/[^a-z0-9]+/g, "-").replace(/-{2,}/g, "-").replace(/^-+|-+$/g, "");
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
// src/template.ts
|
|
629
|
+
import { readFile } from "fs/promises";
|
|
630
|
+
import { join as join3 } from "path";
|
|
631
|
+
async function renderTemplate(kind, vars, config) {
|
|
632
|
+
const templateFile = join3(config.resolvedDocs.templatesDir, `${kind}.md`);
|
|
633
|
+
const raw = await readFile(templateFile, "utf8");
|
|
634
|
+
return interpolate(raw, vars);
|
|
635
|
+
}
|
|
636
|
+
function interpolate(raw, vars) {
|
|
637
|
+
let rendered = raw.replace(/^(id:\s*).*$/m, `$1${vars.id}`).replace(/^(title:\s*).*$/m, `$1${vars.title}`).replace(/^(date:\s*).*$/m, `$1${vars.date}`).replace(/^(status:\s*).*$/m, `$1${vars.status}`);
|
|
638
|
+
if (vars.tags) {
|
|
639
|
+
rendered = rendered.replace(/^(tags:\s*).*$/m, `$1[${vars.tags.join(", ")}]`);
|
|
640
|
+
}
|
|
641
|
+
return rendered;
|
|
642
|
+
}
|
|
643
|
+
function nowIsoDateTime() {
|
|
644
|
+
return (/* @__PURE__ */ new Date()).toISOString();
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
// src/commands/new.ts
|
|
648
|
+
async function createDocument(kind, title, opts) {
|
|
649
|
+
if (!title || title.trim() === "") {
|
|
650
|
+
exitUsage(`Title is required. Usage: acta new ${kind} "My title"`);
|
|
651
|
+
}
|
|
652
|
+
const { config } = await resolveContext({ config: opts.config });
|
|
653
|
+
const { loadProject: loadProject5 } = await import("@acta-dev/core");
|
|
654
|
+
const project = await loadProject5({ config });
|
|
655
|
+
let id;
|
|
656
|
+
if (opts.id) {
|
|
657
|
+
const prefix = kind === "adr" ? config.ids.adrPrefix : config.ids.specPrefix;
|
|
658
|
+
if (!opts.id.startsWith(`${prefix}-`)) {
|
|
659
|
+
exitUsage(`ID "${opts.id}" does not match ${kind} prefix "${prefix}-"`);
|
|
660
|
+
}
|
|
661
|
+
const existing = project.documents.find((d) => d.id === opts.id);
|
|
662
|
+
if (existing) {
|
|
663
|
+
exitFailure(`Document "${opts.id}" already exists at ${existing.file.path}`);
|
|
664
|
+
}
|
|
665
|
+
id = opts.id;
|
|
666
|
+
} else {
|
|
667
|
+
id = allocateNextId(kind, project.documents, config);
|
|
668
|
+
}
|
|
669
|
+
const defaultStatus = kind === "adr" ? "proposed" : "draft";
|
|
670
|
+
const status = opts.status ?? defaultStatus;
|
|
671
|
+
const validStatuses = kind === "adr" ? adrStatuses : specStatuses;
|
|
672
|
+
if (!validStatuses.includes(status)) {
|
|
673
|
+
exitUsage(`Invalid status "${status}" for ${kind}. Valid: ${validStatuses.join(", ")}`);
|
|
674
|
+
}
|
|
675
|
+
const slug = titleToSlug(title.trim());
|
|
676
|
+
const filename = `${id}-${slug}.md`;
|
|
677
|
+
const dir = kind === "adr" ? config.resolvedDocs.adrDir : config.resolvedDocs.specDir;
|
|
678
|
+
const destPath = join4(dir, filename);
|
|
679
|
+
if (existsSync3(destPath)) {
|
|
680
|
+
exitFailure(`File already exists: ${destPath}`);
|
|
681
|
+
}
|
|
682
|
+
const content = await renderTemplate(
|
|
683
|
+
kind,
|
|
684
|
+
{ id, title: title.trim(), date: nowIsoDateTime(), status, tags: parseTags(opts.tags) },
|
|
685
|
+
config
|
|
686
|
+
);
|
|
687
|
+
await writeFile2(destPath, content, "utf8");
|
|
688
|
+
if (opts.json) {
|
|
689
|
+
printJson({
|
|
690
|
+
id,
|
|
691
|
+
kind,
|
|
692
|
+
title: title.trim(),
|
|
693
|
+
status,
|
|
694
|
+
path: destPath,
|
|
695
|
+
relativePath: relative(process.cwd(), destPath)
|
|
696
|
+
});
|
|
697
|
+
return;
|
|
698
|
+
}
|
|
699
|
+
printSuccess(`Created ${destPath}`);
|
|
700
|
+
}
|
|
701
|
+
var newCommand = defineCommand5({
|
|
702
|
+
meta: {
|
|
703
|
+
name: "new",
|
|
704
|
+
description: "Create a new ADR or spec from the configured templates"
|
|
705
|
+
},
|
|
706
|
+
args: {
|
|
707
|
+
config: {
|
|
708
|
+
type: "string",
|
|
709
|
+
alias: "c",
|
|
710
|
+
description: "Path to acta.config.ts"
|
|
711
|
+
}
|
|
712
|
+
},
|
|
713
|
+
subCommands: {
|
|
714
|
+
adr: defineCommand5({
|
|
715
|
+
meta: { name: "adr", description: "Create a new ADR with the next available ID" },
|
|
716
|
+
args: {
|
|
717
|
+
title: {
|
|
718
|
+
type: "positional",
|
|
719
|
+
description: "Document title",
|
|
720
|
+
required: true
|
|
721
|
+
},
|
|
722
|
+
id: {
|
|
723
|
+
type: "string",
|
|
724
|
+
description: "Override auto-allocated ID (e.g. ADR-0007)"
|
|
725
|
+
},
|
|
726
|
+
status: {
|
|
727
|
+
type: "string",
|
|
728
|
+
description: "Initial status (default: proposed)"
|
|
729
|
+
},
|
|
730
|
+
tags: {
|
|
731
|
+
type: "string",
|
|
732
|
+
description: "Comma-separated tags"
|
|
733
|
+
},
|
|
734
|
+
json: {
|
|
735
|
+
type: "boolean",
|
|
736
|
+
description: "Print the created document id and path as JSON",
|
|
737
|
+
default: false
|
|
738
|
+
},
|
|
739
|
+
config: {
|
|
740
|
+
type: "string",
|
|
741
|
+
alias: "c",
|
|
742
|
+
description: "Path to acta.config.ts"
|
|
743
|
+
}
|
|
744
|
+
},
|
|
745
|
+
async run({ args }) {
|
|
746
|
+
await createDocument("adr", args.title, args);
|
|
747
|
+
}
|
|
748
|
+
}),
|
|
749
|
+
spec: defineCommand5({
|
|
750
|
+
meta: { name: "spec", description: "Create a new spec with the next available ID" },
|
|
751
|
+
args: {
|
|
752
|
+
title: {
|
|
753
|
+
type: "positional",
|
|
754
|
+
description: "Document title",
|
|
755
|
+
required: true
|
|
756
|
+
},
|
|
757
|
+
id: {
|
|
758
|
+
type: "string",
|
|
759
|
+
description: "Override auto-allocated ID (e.g. SPEC-0005)"
|
|
760
|
+
},
|
|
761
|
+
status: {
|
|
762
|
+
type: "string",
|
|
763
|
+
description: "Initial status (default: draft)"
|
|
764
|
+
},
|
|
765
|
+
tags: {
|
|
766
|
+
type: "string",
|
|
767
|
+
description: "Comma-separated tags"
|
|
768
|
+
},
|
|
769
|
+
json: {
|
|
770
|
+
type: "boolean",
|
|
771
|
+
description: "Print the created document id and path as JSON",
|
|
772
|
+
default: false
|
|
773
|
+
},
|
|
774
|
+
config: {
|
|
775
|
+
type: "string",
|
|
776
|
+
alias: "c",
|
|
777
|
+
description: "Path to acta.config.ts"
|
|
778
|
+
}
|
|
779
|
+
},
|
|
780
|
+
async run({ args }) {
|
|
781
|
+
await createDocument("spec", args.title, args);
|
|
782
|
+
}
|
|
783
|
+
})
|
|
784
|
+
}
|
|
785
|
+
});
|
|
786
|
+
function parseTags(value) {
|
|
787
|
+
if (!value) {
|
|
788
|
+
return void 0;
|
|
789
|
+
}
|
|
790
|
+
const tags = value.split(",").map((tag) => tag.trim()).filter(Boolean);
|
|
791
|
+
return tags.length > 0 ? tags : void 0;
|
|
792
|
+
}
|
|
793
|
+
|
|
794
|
+
// src/commands/renumber.ts
|
|
795
|
+
import { readFile as readFile2, rename, writeFile as writeFile3 } from "fs/promises";
|
|
796
|
+
import { basename, dirname, join as join5 } from "path";
|
|
797
|
+
import { internalLinkKeys, loadProject as loadProject3 } from "@acta-dev/core";
|
|
798
|
+
import { defineCommand as defineCommand6 } from "citty";
|
|
799
|
+
import kleur4 from "kleur";
|
|
800
|
+
import { parse as parseYaml, stringify as stringifyYaml } from "yaml";
|
|
801
|
+
function buildRenumberPlan(fromId, toId, project) {
|
|
802
|
+
const target = project.documents.find((d) => d.id.toLowerCase() === fromId.toLowerCase());
|
|
803
|
+
if (!target) {
|
|
804
|
+
exitFailure(`Document "${fromId}" not found.`);
|
|
805
|
+
}
|
|
806
|
+
const collision = project.documents.find((d) => d.id.toLowerCase() === toId.toLowerCase());
|
|
807
|
+
if (collision) {
|
|
808
|
+
exitFailure(`Target ID "${toId}" already exists at ${collision.file.path}.`);
|
|
809
|
+
}
|
|
810
|
+
const fromPrefix = fromId.replace(/-\d+$/, "");
|
|
811
|
+
const toPrefix = toId.replace(/-\d+$/, "");
|
|
812
|
+
if (fromPrefix !== toPrefix) {
|
|
813
|
+
exitUsage(
|
|
814
|
+
`Cannot renumber across kinds. FROM prefix "${fromPrefix}" \u2260 TO prefix "${toPrefix}".`
|
|
815
|
+
);
|
|
816
|
+
}
|
|
817
|
+
const oldFilename = basename(target.file.path);
|
|
818
|
+
const oldSlug = oldFilename.replace(`${target.id}-`, "").replace(/\.md$/, "");
|
|
819
|
+
const newFilename = `${toId}-${oldSlug}.md`;
|
|
820
|
+
const newPath = join5(dirname(target.file.path), newFilename);
|
|
821
|
+
const affectedDocs = project.documents.filter((d) => d.id !== target.id).filter((d) => internalLinkKeys.some((key) => d.links[key].includes(target.id))).map((d) => ({ doc: d, path: d.file.path }));
|
|
822
|
+
return {
|
|
823
|
+
target,
|
|
824
|
+
oldPath: target.file.path,
|
|
825
|
+
newPath,
|
|
826
|
+
newFilename,
|
|
827
|
+
affectedDocs
|
|
828
|
+
};
|
|
829
|
+
}
|
|
830
|
+
async function rewriteDocument(filePath, oldId, newId, isTarget) {
|
|
831
|
+
const raw = await readFile2(filePath, "utf8");
|
|
832
|
+
const match = raw.match(/^---\n([\s\S]*?)\n---\n([\s\S]*)$/);
|
|
833
|
+
if (!match) {
|
|
834
|
+
throw new Error(`Cannot parse frontmatter in ${filePath}`);
|
|
835
|
+
}
|
|
836
|
+
const [, frontmatterYaml, body] = match;
|
|
837
|
+
const fm = parseYaml(frontmatterYaml);
|
|
838
|
+
if (isTarget) {
|
|
839
|
+
fm.id = newId;
|
|
840
|
+
}
|
|
841
|
+
const linksObj = fm.links;
|
|
842
|
+
if (linksObj && typeof linksObj === "object") {
|
|
843
|
+
for (const key of internalLinkKeys) {
|
|
844
|
+
const arr = linksObj[key];
|
|
845
|
+
if (Array.isArray(arr)) {
|
|
846
|
+
linksObj[key] = arr.map((v) => v === oldId ? newId : v);
|
|
847
|
+
}
|
|
848
|
+
}
|
|
849
|
+
}
|
|
850
|
+
const newFrontmatter = stringifyYaml(fm, { lineWidth: 0 }).trimEnd();
|
|
851
|
+
return `---
|
|
852
|
+
${newFrontmatter}
|
|
853
|
+
---
|
|
854
|
+
${body}`;
|
|
855
|
+
}
|
|
856
|
+
var renumberCommand = defineCommand6({
|
|
857
|
+
meta: {
|
|
858
|
+
name: "renumber",
|
|
859
|
+
description: "Rename a document ID and update its filename plus internal links"
|
|
860
|
+
},
|
|
861
|
+
args: {
|
|
862
|
+
from: {
|
|
863
|
+
type: "positional",
|
|
864
|
+
description: "Current document ID (e.g. ADR-0001)",
|
|
865
|
+
required: true
|
|
866
|
+
},
|
|
867
|
+
to: {
|
|
868
|
+
type: "positional",
|
|
869
|
+
description: "New document ID (e.g. ADR-0007)",
|
|
870
|
+
required: true
|
|
871
|
+
},
|
|
872
|
+
"dry-run": {
|
|
873
|
+
type: "boolean",
|
|
874
|
+
description: "Print what would change without writing",
|
|
875
|
+
default: false
|
|
876
|
+
},
|
|
877
|
+
config: {
|
|
878
|
+
type: "string",
|
|
879
|
+
alias: "c",
|
|
880
|
+
description: "Path to acta.config.ts"
|
|
881
|
+
}
|
|
882
|
+
},
|
|
883
|
+
async run({ args }) {
|
|
884
|
+
const fromId = args.from;
|
|
885
|
+
const toId = args.to;
|
|
886
|
+
const dryRun = args["dry-run"];
|
|
887
|
+
if (!fromId || !toId) {
|
|
888
|
+
exitUsage("Usage: acta renumber <FROM> <TO>");
|
|
889
|
+
}
|
|
890
|
+
const { config } = await resolveContext({ config: args.config });
|
|
891
|
+
const project = await loadProject3({ config });
|
|
892
|
+
const plan = buildRenumberPlan(fromId, toId, project);
|
|
893
|
+
printLine();
|
|
894
|
+
printLine(kleur4.bold("Renumber plan:"));
|
|
895
|
+
printLine(` ${kleur4.dim("rename")} ${plan.target.id} \u2192 ${kleur4.bold(toId)}`);
|
|
896
|
+
printLine(
|
|
897
|
+
` ${kleur4.dim("file")} ${basename(plan.oldPath)} \u2192 ${kleur4.bold(plan.newFilename)}`
|
|
898
|
+
);
|
|
899
|
+
if (plan.affectedDocs.length > 0) {
|
|
900
|
+
printLine(` ${kleur4.dim("update links in:")}`);
|
|
901
|
+
for (const { doc } of plan.affectedDocs) {
|
|
902
|
+
printLine(` ${doc.id} ${doc.file.relativePath}`);
|
|
903
|
+
}
|
|
904
|
+
} else {
|
|
905
|
+
printLine(` ${kleur4.dim("no other documents reference this ID")}`);
|
|
906
|
+
}
|
|
907
|
+
if (dryRun) {
|
|
908
|
+
printLine();
|
|
909
|
+
printWarn("Dry run \u2014 no changes written.");
|
|
910
|
+
return;
|
|
911
|
+
}
|
|
912
|
+
printLine();
|
|
913
|
+
for (const { doc, path } of plan.affectedDocs) {
|
|
914
|
+
const rewritten = await rewriteDocument(path, fromId, toId, false);
|
|
915
|
+
await writeFile3(path, rewritten, "utf8");
|
|
916
|
+
printSuccess(`Updated links in ${doc.id}`);
|
|
917
|
+
}
|
|
918
|
+
const rewrittenTarget = await rewriteDocument(plan.oldPath, fromId, toId, true);
|
|
919
|
+
await writeFile3(plan.oldPath, rewrittenTarget, "utf8");
|
|
920
|
+
await rename(plan.oldPath, plan.newPath);
|
|
921
|
+
printSuccess(`Renamed ${basename(plan.oldPath)} \u2192 ${plan.newFilename}`);
|
|
922
|
+
printLine();
|
|
923
|
+
printSuccess(`Renumber complete: ${fromId} \u2192 ${toId}`);
|
|
924
|
+
const { validateLoadedProject: validateLoadedProject2 } = await import("@acta-dev/core");
|
|
925
|
+
const result = await validateLoadedProject2({ config });
|
|
926
|
+
if (!result.valid) {
|
|
927
|
+
printLine();
|
|
928
|
+
printWarn(
|
|
929
|
+
`Renumber introduced ${result.errorCount} validation error${result.errorCount !== 1 ? "s" : ""}. Run \`acta validate\` for details.`
|
|
930
|
+
);
|
|
931
|
+
process.exit(1);
|
|
932
|
+
}
|
|
933
|
+
}
|
|
934
|
+
});
|
|
935
|
+
|
|
936
|
+
// src/commands/show.ts
|
|
937
|
+
import { loadProject as loadProject4 } from "@acta-dev/core";
|
|
938
|
+
import { defineCommand as defineCommand7 } from "citty";
|
|
939
|
+
import kleur5 from "kleur";
|
|
940
|
+
var showCommand = defineCommand7({
|
|
941
|
+
meta: {
|
|
942
|
+
name: "show",
|
|
943
|
+
description: "Show metadata, sections, links and backlinks for one document"
|
|
944
|
+
},
|
|
945
|
+
args: {
|
|
946
|
+
id: {
|
|
947
|
+
type: "positional",
|
|
948
|
+
description: "Document ID (e.g. ADR-0001)",
|
|
949
|
+
required: true
|
|
950
|
+
},
|
|
951
|
+
json: {
|
|
952
|
+
type: "boolean",
|
|
953
|
+
description: "Output full document as JSON",
|
|
954
|
+
default: false
|
|
955
|
+
},
|
|
956
|
+
config: {
|
|
957
|
+
type: "string",
|
|
958
|
+
alias: "c",
|
|
959
|
+
description: "Path to acta.config.ts"
|
|
960
|
+
}
|
|
961
|
+
},
|
|
962
|
+
async run({ args }) {
|
|
963
|
+
if (!args.id) {
|
|
964
|
+
exitUsage("ID is required. Usage: acta show <ID>");
|
|
965
|
+
}
|
|
966
|
+
const { config } = await resolveContext({ config: args.config });
|
|
967
|
+
const project = await loadProject4({ config });
|
|
968
|
+
const doc = project.documents.find((d) => d.id.toLowerCase() === args.id.toLowerCase());
|
|
969
|
+
if (!doc) {
|
|
970
|
+
exitFailure(`Document "${args.id}" not found.`);
|
|
971
|
+
}
|
|
972
|
+
if (args.json) {
|
|
973
|
+
printJson(doc);
|
|
974
|
+
return;
|
|
975
|
+
}
|
|
976
|
+
printLine();
|
|
977
|
+
printLine(`${kleur5.bold(doc.id)} ${kleur5.dim(doc.kind)} ${doc.status}`);
|
|
978
|
+
printLine(kleur5.bold(doc.title));
|
|
979
|
+
printLine(kleur5.dim(doc.file.relativePath));
|
|
980
|
+
printLine();
|
|
981
|
+
if (doc.summary) {
|
|
982
|
+
printLine(doc.summary);
|
|
983
|
+
printLine();
|
|
984
|
+
}
|
|
985
|
+
printLine(
|
|
986
|
+
`${kleur5.bold("Date:")} ${formatShowDate(doc.date)}${doc.updated ? ` (updated ${formatShowDate(doc.updated)})` : ""}`
|
|
987
|
+
);
|
|
988
|
+
if (doc.tags.length > 0) {
|
|
989
|
+
printLine(`${kleur5.bold("Tags:")} ${doc.tags.join(", ")}`);
|
|
990
|
+
}
|
|
991
|
+
if (doc.component.length > 0) {
|
|
992
|
+
printLine(`${kleur5.bold("Component:")} ${doc.component.join(", ")}`);
|
|
993
|
+
}
|
|
994
|
+
if (doc.owners.length > 0) {
|
|
995
|
+
printLine(`${kleur5.bold("Owners:")} ${doc.owners.join(", ")}`);
|
|
996
|
+
}
|
|
997
|
+
if (doc.sections.length > 0) {
|
|
998
|
+
printLine();
|
|
999
|
+
printLine(kleur5.bold("Sections:"));
|
|
1000
|
+
for (const section of doc.sections) {
|
|
1001
|
+
printLine(` ${"#".repeat(section.level)} ${section.title}`);
|
|
1002
|
+
}
|
|
1003
|
+
}
|
|
1004
|
+
const linkEntries = Object.entries(doc.links).filter(([, ids]) => ids.length > 0);
|
|
1005
|
+
if (linkEntries.length > 0) {
|
|
1006
|
+
printLine();
|
|
1007
|
+
printLine(kleur5.bold("Links:"));
|
|
1008
|
+
for (const [key, ids] of linkEntries) {
|
|
1009
|
+
printLine(` ${kleur5.cyan(key)}: ${ids.join(", ")}`);
|
|
1010
|
+
}
|
|
1011
|
+
}
|
|
1012
|
+
const backlinkEntries = Object.entries(doc.backlinks).filter(
|
|
1013
|
+
([, ids]) => ids.length > 0
|
|
1014
|
+
);
|
|
1015
|
+
if (backlinkEntries.length > 0) {
|
|
1016
|
+
printLine();
|
|
1017
|
+
printLine(kleur5.bold("Backlinks:"));
|
|
1018
|
+
for (const [key, ids] of backlinkEntries) {
|
|
1019
|
+
printLine(` ${kleur5.cyan(key)}: ${ids.join(", ")}`);
|
|
1020
|
+
}
|
|
1021
|
+
}
|
|
1022
|
+
printLine();
|
|
1023
|
+
}
|
|
1024
|
+
});
|
|
1025
|
+
function formatShowDate(value) {
|
|
1026
|
+
if (!value) {
|
|
1027
|
+
return "";
|
|
1028
|
+
}
|
|
1029
|
+
const parsed = new Date(value);
|
|
1030
|
+
if (Number.isNaN(parsed.getTime())) {
|
|
1031
|
+
return value;
|
|
1032
|
+
}
|
|
1033
|
+
const year = parsed.getUTCFullYear();
|
|
1034
|
+
const month = String(parsed.getUTCMonth() + 1).padStart(2, "0");
|
|
1035
|
+
const day = String(parsed.getUTCDate()).padStart(2, "0");
|
|
1036
|
+
return `${year}-${month}-${day}`;
|
|
1037
|
+
}
|
|
1038
|
+
|
|
1039
|
+
// src/commands/validate.ts
|
|
1040
|
+
import { mkdir as mkdir2, writeFile as writeFile4 } from "fs/promises";
|
|
1041
|
+
import { join as join6 } from "path";
|
|
1042
|
+
import { validateLoadedProject } from "@acta-dev/core";
|
|
1043
|
+
import { defineCommand as defineCommand8 } from "citty";
|
|
1044
|
+
import kleur6 from "kleur";
|
|
1045
|
+
var validateCommand = defineCommand8({
|
|
1046
|
+
meta: {
|
|
1047
|
+
name: "validate",
|
|
1048
|
+
description: "Validate frontmatter, IDs, links, sections and repository rules"
|
|
1049
|
+
},
|
|
1050
|
+
args: {
|
|
1051
|
+
ci: {
|
|
1052
|
+
type: "boolean",
|
|
1053
|
+
description: "Write validation.json to outDir and use concise output",
|
|
1054
|
+
default: false
|
|
1055
|
+
},
|
|
1056
|
+
json: {
|
|
1057
|
+
type: "boolean",
|
|
1058
|
+
description: "Print machine-readable result to stdout",
|
|
1059
|
+
default: false
|
|
1060
|
+
},
|
|
1061
|
+
config: {
|
|
1062
|
+
type: "string",
|
|
1063
|
+
alias: "c",
|
|
1064
|
+
description: "Path to acta.config.ts"
|
|
1065
|
+
}
|
|
1066
|
+
},
|
|
1067
|
+
async run({ args }) {
|
|
1068
|
+
const { config } = await resolveContext({ config: args.config });
|
|
1069
|
+
const result = await validateLoadedProject({ config });
|
|
1070
|
+
if (args.json) {
|
|
1071
|
+
printJson(result);
|
|
1072
|
+
process.exit(result.valid ? 0 : 1);
|
|
1073
|
+
return;
|
|
1074
|
+
}
|
|
1075
|
+
if (args.ci) {
|
|
1076
|
+
await mkdir2(config.resolvedBuild.outDir, { recursive: true });
|
|
1077
|
+
const outPath = join6(config.resolvedBuild.outDir, "validation.json");
|
|
1078
|
+
await writeFile4(outPath, `${JSON.stringify(result, null, 2)}
|
|
1079
|
+
`, "utf8");
|
|
1080
|
+
if (result.errors.length > 0) {
|
|
1081
|
+
for (const issue of result.errors) {
|
|
1082
|
+
printLine(`${kleur6.red("error")} ${issue.documentId ?? ""} ${issue.message}`);
|
|
1083
|
+
}
|
|
1084
|
+
}
|
|
1085
|
+
for (const issue of result.warnings) {
|
|
1086
|
+
printLine(`${kleur6.yellow("warn")} ${issue.documentId ?? ""} ${issue.message}`);
|
|
1087
|
+
}
|
|
1088
|
+
printLine(`Written ${outPath}`);
|
|
1089
|
+
process.exit(result.valid ? 0 : 1);
|
|
1090
|
+
return;
|
|
1091
|
+
}
|
|
1092
|
+
if (result.issues.length === 0) {
|
|
1093
|
+
printValidationSummary(0, 0, true);
|
|
1094
|
+
} else {
|
|
1095
|
+
const errors = result.issues.filter((i) => i.severity === "error");
|
|
1096
|
+
const warnings = result.issues.filter((i) => i.severity === "warning");
|
|
1097
|
+
if (errors.length > 0) {
|
|
1098
|
+
printLine();
|
|
1099
|
+
printLine(kleur6.bold("Errors:"));
|
|
1100
|
+
printIssues(errors);
|
|
1101
|
+
}
|
|
1102
|
+
if (warnings.length > 0) {
|
|
1103
|
+
printLine();
|
|
1104
|
+
printLine(kleur6.bold("Warnings:"));
|
|
1105
|
+
printIssues(warnings);
|
|
1106
|
+
}
|
|
1107
|
+
printLine();
|
|
1108
|
+
printValidationSummary(result.errorCount, result.warningCount, result.valid);
|
|
1109
|
+
}
|
|
1110
|
+
process.exit(result.valid ? 0 : 1);
|
|
1111
|
+
}
|
|
1112
|
+
});
|
|
1113
|
+
|
|
1114
|
+
// src/index.ts
|
|
1115
|
+
var main = defineCommand9({
|
|
1116
|
+
meta: {
|
|
1117
|
+
name: "acta",
|
|
1118
|
+
version: "0.0.0",
|
|
1119
|
+
description: "Docs-as-code CLI for authoring, validating and building ADR/spec repositories"
|
|
1120
|
+
},
|
|
1121
|
+
subCommands: {
|
|
1122
|
+
init: initCommand,
|
|
1123
|
+
new: newCommand,
|
|
1124
|
+
list: listCommand,
|
|
1125
|
+
show: showCommand,
|
|
1126
|
+
validate: validateCommand,
|
|
1127
|
+
graph: graphCommand,
|
|
1128
|
+
build: buildCommand,
|
|
1129
|
+
renumber: renumberCommand
|
|
1130
|
+
}
|
|
1131
|
+
});
|
|
1132
|
+
var actaCliPackage = "@acta-dev/cli";
|
|
1133
|
+
function getCliBootstrapInfo() {
|
|
1134
|
+
return {
|
|
1135
|
+
name: "acta",
|
|
1136
|
+
packageName: actaCliPackage,
|
|
1137
|
+
version: "0.0.0"
|
|
1138
|
+
};
|
|
1139
|
+
}
|
|
1140
|
+
function isDirectExecution() {
|
|
1141
|
+
if (!process.argv[1]) {
|
|
1142
|
+
return false;
|
|
1143
|
+
}
|
|
1144
|
+
return realpathSync(process.argv[1]) === fileURLToPath(import.meta.url);
|
|
1145
|
+
}
|
|
1146
|
+
if (isDirectExecution()) {
|
|
1147
|
+
await runMain(main);
|
|
1148
|
+
}
|
|
1149
|
+
export {
|
|
1150
|
+
actaCliPackage,
|
|
1151
|
+
getCliBootstrapInfo
|
|
1152
|
+
};
|
package/package.json
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@acta-dev/cli",
|
|
3
|
+
"version": "0.1.1",
|
|
4
|
+
"description": "Acta CLI — TypeScript-first docs-as-code tooling for ADR and spec documents in Git. Provides the `acta` binary.",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"adr",
|
|
7
|
+
"spec",
|
|
8
|
+
"docs-as-code",
|
|
9
|
+
"architecture-decision-records",
|
|
10
|
+
"cli",
|
|
11
|
+
"documentation",
|
|
12
|
+
"markdown"
|
|
13
|
+
],
|
|
14
|
+
"license": "MIT",
|
|
15
|
+
"author": "jentix",
|
|
16
|
+
"homepage": "https://github.com/jentix/acta#readme",
|
|
17
|
+
"repository": {
|
|
18
|
+
"type": "git",
|
|
19
|
+
"url": "git+https://github.com/jentix/acta.git",
|
|
20
|
+
"directory": "packages/cli"
|
|
21
|
+
},
|
|
22
|
+
"bugs": {
|
|
23
|
+
"url": "https://github.com/jentix/acta/issues"
|
|
24
|
+
},
|
|
25
|
+
"type": "module",
|
|
26
|
+
"bin": {
|
|
27
|
+
"acta": "./dist/index.js"
|
|
28
|
+
},
|
|
29
|
+
"exports": {
|
|
30
|
+
".": {
|
|
31
|
+
"types": "./dist/index.d.ts",
|
|
32
|
+
"import": "./dist/index.js"
|
|
33
|
+
}
|
|
34
|
+
},
|
|
35
|
+
"files": [
|
|
36
|
+
"dist"
|
|
37
|
+
],
|
|
38
|
+
"engines": {
|
|
39
|
+
"node": ">=22.12 <26"
|
|
40
|
+
},
|
|
41
|
+
"publishConfig": {
|
|
42
|
+
"access": "public"
|
|
43
|
+
},
|
|
44
|
+
"dependencies": {
|
|
45
|
+
"citty": "^0.2.2",
|
|
46
|
+
"kleur": "^4.1.5",
|
|
47
|
+
"yaml": "^2.8.3",
|
|
48
|
+
"@acta-dev/core": "0.1.1"
|
|
49
|
+
},
|
|
50
|
+
"devDependencies": {
|
|
51
|
+
"execa": "^9.6.1",
|
|
52
|
+
"tsup": "^8.4.0",
|
|
53
|
+
"typescript": "^5.9.3",
|
|
54
|
+
"vitest": "^4.1.5"
|
|
55
|
+
},
|
|
56
|
+
"scripts": {
|
|
57
|
+
"build": "tsup src/index.ts --format esm --dts",
|
|
58
|
+
"dev": "tsup src/index.ts --format esm --dts --watch",
|
|
59
|
+
"test": "vitest run",
|
|
60
|
+
"typecheck": "tsc -p tsconfig.json"
|
|
61
|
+
}
|
|
62
|
+
}
|