airgen-cli 0.4.1 → 0.5.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.
@@ -1,29 +1,41 @@
1
1
  import { writeFileSync } from "node:fs";
2
2
  import { output, printTable, isJsonMode } from "../output.js";
3
3
  // ── Mermaid rendering helpers ─────────────────────────────────
4
+ // Counter for generating short, readable Mermaid node IDs
5
+ let mermaidNodeCounter = 0;
6
+ const mermaidIdMap = new Map();
7
+ function resetMermaidIds() {
8
+ mermaidNodeCounter = 0;
9
+ mermaidIdMap.clear();
10
+ }
4
11
  function sanitizeId(id) {
5
- return id.replace(/[^a-zA-Z0-9_-]/g, "_");
12
+ let short = mermaidIdMap.get(id);
13
+ if (!short) {
14
+ short = `n${mermaidNodeCounter++}`;
15
+ mermaidIdMap.set(id, short);
16
+ }
17
+ return short;
6
18
  }
7
19
  function mermaidNodeShape(block) {
8
20
  const id = sanitizeId(block.id);
9
21
  const label = block.name.replace(/"/g, "'");
10
22
  const stereo = block.stereotype?.replace(/[«»<>]/g, "") ?? block.kind ?? "block";
11
- const display = `"«${stereo}»\\n${label}"`;
23
+ const display = `"<<${stereo}>><br/>${label}"`;
12
24
  switch (block.kind) {
13
25
  case "system": return `${id}[${display}]`;
14
26
  case "subsystem": return `${id}[${display}]`;
15
27
  case "actor": return `${id}([${display}])`;
16
- case "external": return `${id}>${display}]`;
28
+ case "external": return `${id}[${display}]`;
17
29
  case "interface": return `${id}{{${display}}}`;
18
- default: return `${id}[${display}]`; // component
30
+ default: return `${id}[${display}]`;
19
31
  }
20
32
  }
21
33
  function mermaidArrow(kind) {
22
34
  switch (kind) {
23
35
  case "flow": return "==>";
24
36
  case "dependency": return "-.->";
25
- case "composition": return "--*";
26
- default: return "-->"; // association
37
+ case "composition": return "-->";
38
+ default: return "-->";
27
39
  }
28
40
  }
29
41
  function mermaidStyle(block) {
@@ -174,6 +186,7 @@ function renderTerminal(blocks, connectors) {
174
186
  return lines.join("\n");
175
187
  }
176
188
  function renderMermaid(blocks, connectors, direction) {
189
+ resetMermaidIds();
177
190
  const lines = [`flowchart ${direction}`];
178
191
  // Nodes
179
192
  for (const b of blocks) {
@@ -185,7 +198,8 @@ function renderMermaid(blocks, connectors, direction) {
185
198
  const tgt = sanitizeId(c.target);
186
199
  const arrow = mermaidArrow(c.kind);
187
200
  if (c.label) {
188
- lines.push(` ${src} ${arrow}|"${c.label.replace(/"/g, "'")}"| ${tgt}`);
201
+ const edgeLabel = c.label.replace(/"/g, "'");
202
+ lines.push(` ${src} ${arrow}|${edgeLabel}| ${tgt}`);
189
203
  }
190
204
  else {
191
205
  lines.push(` ${src} ${arrow} ${tgt}`);
@@ -346,6 +360,32 @@ export function registerDiagramCommands(program, client) {
346
360
  });
347
361
  // Blocks sub-group
348
362
  const blocks = cmd.command("blocks").description("Manage blocks in diagrams");
363
+ blocks
364
+ .command("list")
365
+ .description("List blocks in a diagram")
366
+ .argument("<tenant>", "Tenant slug")
367
+ .argument("<project>", "Project slug")
368
+ .argument("<diagram-id>", "Diagram ID")
369
+ .action(async (tenant, project, diagramId) => {
370
+ const data = await client.get(`/architecture/blocks/${tenant}/${project}/${diagramId}`);
371
+ const list = data.blocks ?? [];
372
+ if (isJsonMode()) {
373
+ output(list);
374
+ }
375
+ else {
376
+ if (list.length === 0) {
377
+ console.log("No blocks in this diagram.");
378
+ return;
379
+ }
380
+ printTable(["ID", "Name", "Kind", "Stereotype", "Ports"], list.map(b => [
381
+ b.id,
382
+ b.name,
383
+ b.kind ?? "",
384
+ b.stereotype ?? "",
385
+ String(b.ports?.length ?? 0),
386
+ ]));
387
+ }
388
+ });
349
389
  blocks
350
390
  .command("library")
351
391
  .description("Get all block definitions in the project")
@@ -1,4 +1,4 @@
1
- import { writeFileSync } from "node:fs";
1
+ import { readFileSync, writeFileSync, existsSync } from "node:fs";
2
2
  import { UhtClient } from "../uht-client.js";
3
3
  import { isJsonMode } from "../output.js";
4
4
  // ── Constants ────────────────────────────────────────────────
@@ -320,6 +320,9 @@ export function registerLintCommands(program, client) {
320
320
  .option("--concepts <n>", "Max concepts to classify", "15")
321
321
  .option("--format <fmt>", "Output format: text, markdown, json", "text")
322
322
  .option("-o, --output <file>", "Write report to file")
323
+ .option("--suppress <titles...>", "Suppress findings by title substring (repeatable)")
324
+ .option("--baseline <file>", "Suppress findings listed in a baseline file (one title per line)")
325
+ .option("--save-baseline <file>", "Write current finding titles to a baseline file for future suppression")
323
326
  .action(async (tenant, project, opts) => {
324
327
  const uht = new UhtClient();
325
328
  if (!uht.isConfigured) {
@@ -391,7 +394,28 @@ export function registerLintCommands(program, client) {
391
394
  }
392
395
  // Step 5: Analyze findings
393
396
  console.error("Analyzing...");
394
- const findings = analyzeFindings(concepts, comparisons, requirements);
397
+ let findings = analyzeFindings(concepts, comparisons, requirements);
398
+ // Step 5b: Save baseline if requested (before suppression)
399
+ if (opts.saveBaseline) {
400
+ const titles = findings.map(f => f.title);
401
+ writeFileSync(opts.saveBaseline, titles.join("\n") + "\n", "utf-8");
402
+ console.error(`Baseline saved to ${opts.saveBaseline} (${titles.length} findings)`);
403
+ }
404
+ // Step 5c: Apply suppression
405
+ const suppressions = [...(opts.suppress ?? [])];
406
+ if (opts.baseline && existsSync(opts.baseline)) {
407
+ const baselineContent = readFileSync(opts.baseline, "utf-8");
408
+ const baselineLines = baselineContent.split("\n").map(l => l.trim()).filter(Boolean);
409
+ suppressions.push(...baselineLines);
410
+ }
411
+ if (suppressions.length > 0) {
412
+ const before = findings.length;
413
+ findings = findings.filter(f => !suppressions.some(s => f.title.includes(s)));
414
+ const suppressed = before - findings.length;
415
+ if (suppressed > 0) {
416
+ console.error(`Suppressed ${suppressed} known finding(s).`);
417
+ }
418
+ }
395
419
  // Step 6: Output report
396
420
  let report;
397
421
  if (opts.format === "json" || isJsonMode()) {
@@ -12,7 +12,7 @@ export function registerTraceabilityCommands(program, client) {
12
12
  ? `/trace-links/${tenant}/${project}/${opts.requirement}`
13
13
  : `/trace-links/${tenant}/${project}`;
14
14
  const data = await client.get(path);
15
- const links = data.links ?? [];
15
+ const links = data.traceLinks ?? [];
16
16
  if (isJsonMode()) {
17
17
  output(links);
18
18
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "airgen-cli",
3
- "version": "0.4.1",
3
+ "version": "0.5.0",
4
4
  "description": "AIRGen CLI — requirements engineering from the command line",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",