airgen-cli 0.2.0 → 0.2.2
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 +8 -1
- package/dist/commands/diff.d.ts +3 -0
- package/dist/commands/diff.js +185 -0
- package/dist/commands/documents.js +1 -1
- package/dist/commands/projects.js +3 -0
- package/dist/index.js +2 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -128,14 +128,21 @@ airgen trace delete <tenant> <project> <link-id>
|
|
|
128
128
|
airgen trace linksets list <tenant> <project> # Document linksets
|
|
129
129
|
```
|
|
130
130
|
|
|
131
|
-
### Baselines
|
|
131
|
+
### Baselines & Diff
|
|
132
132
|
|
|
133
133
|
```bash
|
|
134
134
|
airgen bl list <tenant> <project>
|
|
135
135
|
airgen bl create <tenant> <project> --name "v1.0"
|
|
136
136
|
airgen bl compare <tenant> <project> --from <id1> --to <id2>
|
|
137
|
+
|
|
138
|
+
# Rich diff between baselines
|
|
139
|
+
airgen diff <tenant> <project> --from <bl1> --to <bl2> # Pretty terminal output
|
|
140
|
+
airgen diff <tenant> <project> --from <bl1> --to <bl2> --json # Structured JSON
|
|
141
|
+
airgen diff <tenant> <project> --from <bl1> --to <bl2> --format markdown -o diff.md
|
|
137
142
|
```
|
|
138
143
|
|
|
144
|
+
`diff` shows added, modified, and removed requirements with full text, plus a summary of changes to documents, trace links, diagrams, blocks, and connectors.
|
|
145
|
+
|
|
139
146
|
### Quality & AI
|
|
140
147
|
|
|
141
148
|
```bash
|
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
import { writeFileSync } from "node:fs";
|
|
2
|
+
import { isJsonMode, truncate } from "../output.js";
|
|
3
|
+
// ── Helpers ───────────────────────────────────────────────────
|
|
4
|
+
/** Extract short ref from "tenant:project:REQ-001" → "REQ-001" */
|
|
5
|
+
function shortRef(id) {
|
|
6
|
+
return id?.split(":").pop() ?? "?";
|
|
7
|
+
}
|
|
8
|
+
function counts(comp) {
|
|
9
|
+
return {
|
|
10
|
+
added: comp?.added?.length ?? 0,
|
|
11
|
+
removed: comp?.removed?.length ?? 0,
|
|
12
|
+
modified: comp?.modified?.length ?? 0,
|
|
13
|
+
unchanged: comp?.unchanged?.length ?? 0,
|
|
14
|
+
};
|
|
15
|
+
}
|
|
16
|
+
// ── Structured output ────────────────────────────────────────
|
|
17
|
+
function buildStructured(data) {
|
|
18
|
+
const reqs = data.requirements ?? { added: [], removed: [], modified: [], unchanged: [] };
|
|
19
|
+
const r = counts(reqs);
|
|
20
|
+
return {
|
|
21
|
+
summary: {
|
|
22
|
+
from: data.fromBaseline?.ref ?? "?",
|
|
23
|
+
to: data.toBaseline?.ref ?? "?",
|
|
24
|
+
requirements: r,
|
|
25
|
+
},
|
|
26
|
+
added: reqs.added.map(v => ({ ref: shortRef(v.requirementId), text: v.text ?? "" })),
|
|
27
|
+
removed: reqs.removed.map(v => ({ ref: shortRef(v.requirementId), text: v.text ?? "" })),
|
|
28
|
+
modified: reqs.modified.map(v => ({ ref: shortRef(v.requirementId), text: v.text ?? "" })),
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
// ── Pretty text ──────────────────────────────────────────────
|
|
32
|
+
function formatPretty(data) {
|
|
33
|
+
const from = data.fromBaseline?.ref ?? "?";
|
|
34
|
+
const to = data.toBaseline?.ref ?? "?";
|
|
35
|
+
const reqs = data.requirements ?? { added: [], removed: [], modified: [], unchanged: [] };
|
|
36
|
+
const r = counts(reqs);
|
|
37
|
+
const lines = [];
|
|
38
|
+
lines.push(` Baseline Diff: ${from} → ${to}`);
|
|
39
|
+
lines.push(` ${"═".repeat(20 + from.length + to.length)}`);
|
|
40
|
+
lines.push(` ${r.added} added, ${r.modified} modified, ${r.removed} removed, ${r.unchanged} unchanged`);
|
|
41
|
+
lines.push("");
|
|
42
|
+
if (reqs.added.length > 0) {
|
|
43
|
+
lines.push(" ┄┄ Added ┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄");
|
|
44
|
+
for (const v of reqs.added) {
|
|
45
|
+
lines.push(` + ${shortRef(v.requirementId)}`);
|
|
46
|
+
lines.push(` ${truncate(v.text ?? "", 100)}`);
|
|
47
|
+
}
|
|
48
|
+
lines.push("");
|
|
49
|
+
}
|
|
50
|
+
if (reqs.modified.length > 0) {
|
|
51
|
+
lines.push(" ┄┄ Modified ┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄");
|
|
52
|
+
for (const v of reqs.modified) {
|
|
53
|
+
lines.push(` ~ ${shortRef(v.requirementId)}`);
|
|
54
|
+
lines.push(` ${truncate(v.text ?? "", 100)}`);
|
|
55
|
+
}
|
|
56
|
+
lines.push("");
|
|
57
|
+
}
|
|
58
|
+
if (reqs.removed.length > 0) {
|
|
59
|
+
lines.push(" ┄┄ Removed ┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄");
|
|
60
|
+
for (const v of reqs.removed) {
|
|
61
|
+
lines.push(` - ${shortRef(v.requirementId)}`);
|
|
62
|
+
lines.push(` ${truncate(v.text ?? "", 100)}`);
|
|
63
|
+
}
|
|
64
|
+
lines.push("");
|
|
65
|
+
}
|
|
66
|
+
// Non-requirement entity summary
|
|
67
|
+
const others = [
|
|
68
|
+
["Documents", data.documents],
|
|
69
|
+
["Trace Links", data.traceLinks],
|
|
70
|
+
["Diagrams", data.diagrams],
|
|
71
|
+
["Blocks", data.blocks],
|
|
72
|
+
["Connectors", data.connectors],
|
|
73
|
+
];
|
|
74
|
+
const changed = others.filter(([, c]) => {
|
|
75
|
+
const n = counts(c);
|
|
76
|
+
return n.added + n.modified + n.removed > 0;
|
|
77
|
+
});
|
|
78
|
+
if (changed.length > 0) {
|
|
79
|
+
lines.push(" ┄┄ Other Changes ┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄");
|
|
80
|
+
for (const [label, comp] of changed) {
|
|
81
|
+
const n = counts(comp);
|
|
82
|
+
lines.push(` ${label}: +${n.added} ~${n.modified} -${n.removed}`);
|
|
83
|
+
}
|
|
84
|
+
lines.push("");
|
|
85
|
+
}
|
|
86
|
+
if (r.added + r.modified + r.removed === 0 && changed.length === 0) {
|
|
87
|
+
lines.push(" No changes between baselines.");
|
|
88
|
+
lines.push("");
|
|
89
|
+
}
|
|
90
|
+
return lines.join("\n");
|
|
91
|
+
}
|
|
92
|
+
// ── Markdown ─────────────────────────────────────────────────
|
|
93
|
+
function formatMarkdown(data) {
|
|
94
|
+
const from = data.fromBaseline?.ref ?? "?";
|
|
95
|
+
const to = data.toBaseline?.ref ?? "?";
|
|
96
|
+
const reqs = data.requirements ?? { added: [], removed: [], modified: [], unchanged: [] };
|
|
97
|
+
const r = counts(reqs);
|
|
98
|
+
const lines = [];
|
|
99
|
+
lines.push(`## Baseline Diff: ${from} → ${to}`);
|
|
100
|
+
lines.push("");
|
|
101
|
+
lines.push(`**${r.added}** added, **${r.modified}** modified, **${r.removed}** removed, **${r.unchanged}** unchanged`);
|
|
102
|
+
lines.push("");
|
|
103
|
+
if (reqs.added.length > 0) {
|
|
104
|
+
lines.push("### Added");
|
|
105
|
+
lines.push("| Ref | Text |");
|
|
106
|
+
lines.push("|---|---|");
|
|
107
|
+
for (const v of reqs.added) {
|
|
108
|
+
lines.push(`| ${shortRef(v.requirementId)} | ${truncate(v.text ?? "", 120)} |`);
|
|
109
|
+
}
|
|
110
|
+
lines.push("");
|
|
111
|
+
}
|
|
112
|
+
if (reqs.modified.length > 0) {
|
|
113
|
+
lines.push("### Modified");
|
|
114
|
+
lines.push("| Ref | Text (current) |");
|
|
115
|
+
lines.push("|---|---|");
|
|
116
|
+
for (const v of reqs.modified) {
|
|
117
|
+
lines.push(`| ${shortRef(v.requirementId)} | ${truncate(v.text ?? "", 120)} |`);
|
|
118
|
+
}
|
|
119
|
+
lines.push("");
|
|
120
|
+
}
|
|
121
|
+
if (reqs.removed.length > 0) {
|
|
122
|
+
lines.push("### Removed");
|
|
123
|
+
lines.push("| Ref | Text |");
|
|
124
|
+
lines.push("|---|---|");
|
|
125
|
+
for (const v of reqs.removed) {
|
|
126
|
+
lines.push(`| ${shortRef(v.requirementId)} | ${truncate(v.text ?? "", 120)} |`);
|
|
127
|
+
}
|
|
128
|
+
lines.push("");
|
|
129
|
+
}
|
|
130
|
+
// Non-requirement entity summary
|
|
131
|
+
const others = [
|
|
132
|
+
["Documents", data.documents],
|
|
133
|
+
["Trace Links", data.traceLinks],
|
|
134
|
+
["Diagrams", data.diagrams],
|
|
135
|
+
["Blocks", data.blocks],
|
|
136
|
+
["Connectors", data.connectors],
|
|
137
|
+
];
|
|
138
|
+
const changed = others.filter(([, c]) => {
|
|
139
|
+
const n = counts(c);
|
|
140
|
+
return n.added + n.modified + n.removed > 0;
|
|
141
|
+
});
|
|
142
|
+
if (changed.length > 0) {
|
|
143
|
+
lines.push("### Other Changes");
|
|
144
|
+
lines.push("| Entity | Added | Modified | Removed |");
|
|
145
|
+
lines.push("|---|---|---|---|");
|
|
146
|
+
for (const [label, comp] of changed) {
|
|
147
|
+
const n = counts(comp);
|
|
148
|
+
lines.push(`| ${label} | ${n.added} | ${n.modified} | ${n.removed} |`);
|
|
149
|
+
}
|
|
150
|
+
lines.push("");
|
|
151
|
+
}
|
|
152
|
+
return lines.join("\n");
|
|
153
|
+
}
|
|
154
|
+
// ── Command registration ─────────────────────────────────────
|
|
155
|
+
export function registerDiffCommand(program, client) {
|
|
156
|
+
program
|
|
157
|
+
.command("diff")
|
|
158
|
+
.description("Show what changed between two baselines")
|
|
159
|
+
.argument("<tenant>", "Tenant slug")
|
|
160
|
+
.argument("<project>", "Project slug")
|
|
161
|
+
.requiredOption("--from <ref>", "Source baseline ref (earlier)")
|
|
162
|
+
.requiredOption("--to <ref>", "Target baseline ref (later)")
|
|
163
|
+
.option("--format <fmt>", "Output format: text, markdown", "text")
|
|
164
|
+
.option("-o, --output <file>", "Write report to file")
|
|
165
|
+
.action(async (tenant, project, opts) => {
|
|
166
|
+
const data = await client.get(`/baselines/${tenant}/${project}/compare`, { from: opts.from, to: opts.to });
|
|
167
|
+
let result;
|
|
168
|
+
if (isJsonMode()) {
|
|
169
|
+
result = JSON.stringify(buildStructured(data), null, 2);
|
|
170
|
+
}
|
|
171
|
+
else if (opts.format === "markdown") {
|
|
172
|
+
result = formatMarkdown(data);
|
|
173
|
+
}
|
|
174
|
+
else {
|
|
175
|
+
result = formatPretty(data);
|
|
176
|
+
}
|
|
177
|
+
if (opts.output) {
|
|
178
|
+
writeFileSync(opts.output, result + "\n", "utf-8");
|
|
179
|
+
console.log(`Diff written to ${opts.output}`);
|
|
180
|
+
}
|
|
181
|
+
else {
|
|
182
|
+
console.log(result);
|
|
183
|
+
}
|
|
184
|
+
});
|
|
185
|
+
}
|
|
@@ -99,7 +99,7 @@ export function registerDocumentCommands(program, client) {
|
|
|
99
99
|
projectKey,
|
|
100
100
|
documentSlug: document,
|
|
101
101
|
name: opts.title,
|
|
102
|
-
order: opts.order ? parseInt(opts.order, 10) :
|
|
102
|
+
order: opts.order ? parseInt(opts.order, 10) : 0,
|
|
103
103
|
description: opts.description,
|
|
104
104
|
shortCode: opts.code,
|
|
105
105
|
});
|
|
@@ -28,9 +28,12 @@ export function registerProjectCommands(program, client) {
|
|
|
28
28
|
.requiredOption("--name <name>", "Project name")
|
|
29
29
|
.option("--key <key>", "Project key")
|
|
30
30
|
.option("--code <code>", "Project code")
|
|
31
|
+
.option("--slug <slug>", "Project slug (auto-generated from name if omitted)")
|
|
31
32
|
.option("--description <desc>", "Description")
|
|
32
33
|
.action(async (tenant, opts) => {
|
|
34
|
+
const slug = opts.slug ?? opts.name.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/(^-|-$)/g, "");
|
|
33
35
|
const data = await client.post(`/tenants/${tenant}/projects`, {
|
|
36
|
+
slug,
|
|
34
37
|
name: opts.name,
|
|
35
38
|
key: opts.key,
|
|
36
39
|
code: opts.code,
|
package/dist/index.js
CHANGED
|
@@ -20,6 +20,7 @@ import { registerImportExportCommands } from "./commands/import-export.js";
|
|
|
20
20
|
import { registerActivityCommands } from "./commands/activity.js";
|
|
21
21
|
import { registerImplementationCommands } from "./commands/implementation.js";
|
|
22
22
|
import { registerLintCommands } from "./commands/lint.js";
|
|
23
|
+
import { registerDiffCommand } from "./commands/diff.js";
|
|
23
24
|
const program = new Command();
|
|
24
25
|
// Lazy-init: only create client when a command actually runs
|
|
25
26
|
let client = null;
|
|
@@ -69,6 +70,7 @@ registerImportExportCommands(program, clientProxy);
|
|
|
69
70
|
registerActivityCommands(program, clientProxy);
|
|
70
71
|
registerImplementationCommands(program, clientProxy);
|
|
71
72
|
registerLintCommands(program, clientProxy);
|
|
73
|
+
registerDiffCommand(program, clientProxy);
|
|
72
74
|
// Handle async errors from Commander action handlers
|
|
73
75
|
process.on("uncaughtException", (err) => {
|
|
74
76
|
console.error(`Error: ${err.message}`);
|