airgen-cli 0.2.0 → 0.2.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/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,3 @@
1
+ import { Command } from "commander";
2
+ import type { AirgenClient } from "../client.js";
3
+ export declare function registerDiffCommand(program: Command, client: AirgenClient): void;
@@ -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
+ }
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}`);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "airgen-cli",
3
- "version": "0.2.0",
3
+ "version": "0.2.1",
4
4
  "description": "AIRGen CLI — requirements engineering from the command line",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",