airgen-cli 0.13.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.
@@ -264,6 +264,7 @@ export function registerDiagramCommands(program, client) {
264
264
  .option("--direction <dir>", "Layout direction for mermaid: TB, LR, BT, RL", "TB")
265
265
  .option("-o, --output <file>", "Write to file instead of stdout")
266
266
  .option("--wrap", "Wrap mermaid in markdown fenced code block")
267
+ .option("--clean", "Strip classDef/class/style lines from mermaid output (maximum compatibility)")
267
268
  .action(async (tenant, project, id, opts) => {
268
269
  // Fetch diagram metadata + blocks + connectors in parallel
269
270
  const [diagramData, blocksData, connectorsData] = await Promise.all([
@@ -281,6 +282,9 @@ export function registerDiagramCommands(program, client) {
281
282
  let rendered;
282
283
  if (opts.format === "mermaid") {
283
284
  rendered = renderMermaid(blocks, connectors, opts.direction);
285
+ if (opts.clean) {
286
+ rendered = rendered.split("\n").filter(l => !l.trim().startsWith("classDef ") && !l.trim().startsWith("class ") && !l.trim().startsWith("style ")).join("\n");
287
+ }
284
288
  if (isJsonMode()) {
285
289
  output({ mermaid: rendered, blocks: blocks.length, connectors: connectors.length });
286
290
  return;
@@ -638,6 +638,7 @@ export function registerLintCommands(program, client) {
638
638
  .option("--save-baseline <file>", "Write current finding titles to a baseline file for future suppression")
639
639
  .option("--threshold <n>", "Jaccard similarity threshold (0.0-1.0)", "0.6")
640
640
  .option("--spray-threshold <n>", "Outgoing trace link count for spray pattern detection", "8")
641
+ .option("--min-severity <level>", "Only show findings at this severity or above: low, medium, high", "low")
641
642
  .action(async (tenant, project, opts) => {
642
643
  const uht = new UhtClient();
643
644
  if (!uht.isConfigured) {
@@ -744,6 +745,16 @@ export function registerLintCommands(program, client) {
744
745
  if (suppressed > 0)
745
746
  console.error(`Suppressed ${suppressed} known finding(s).`);
746
747
  }
748
+ // Step 5d: Apply severity filter
749
+ const sevOrder = { high: 0, medium: 1, low: 2 };
750
+ const minSev = sevOrder[opts.minSeverity] ?? 2;
751
+ if (minSev < 2) {
752
+ const before = findings.length;
753
+ findings = findings.filter(f => sevOrder[f.severity] <= minSev);
754
+ const filtered = before - findings.length;
755
+ if (filtered > 0)
756
+ console.error(`Filtered ${filtered} finding(s) below ${opts.minSeverity} severity.`);
757
+ }
747
758
  // Step 6: Output report
748
759
  let report;
749
760
  if (opts.format === "json" || isJsonMode()) {
@@ -1,3 +1,4 @@
1
+ import { readFileSync } from "node:fs";
1
2
  import { output, printTable, isJsonMode, truncate } from "../output.js";
2
3
  import { resolveRequirementId } from "../resolve.js";
3
4
  export function registerRequirementCommands(program, client) {
@@ -8,12 +9,38 @@ export function registerRequirementCommands(program, client) {
8
9
  .argument("<tenant>", "Tenant slug")
9
10
  .argument("<project>", "Project slug")
10
11
  .option("-p, --page <n>", "Page number", "1")
11
- .option("-l, --limit <n>", "Items per page", "25")
12
+ .option("-l, --limit <n>", "Items per page (use 'all' to fetch everything)", "50")
12
13
  .option("--sort <field>", "Sort by: ref, createdAt, qaScore")
13
14
  .option("--order <dir>", "Sort order: asc, desc")
14
15
  .option("--tags <tags>", "Comma-separated tags to filter by (server-side)")
15
16
  .option("--document <slug>", "Filter by document slug (server-side)")
16
17
  .action(async (tenant, project, opts) => {
18
+ // Handle --limit all: fetch all pages
19
+ if (opts.limit.toLowerCase() === "all") {
20
+ const all = [];
21
+ for (let page = 1; page <= 50; page++) {
22
+ const params = {
23
+ page: String(page), limit: "500",
24
+ sortBy: opts.sort, sortOrder: opts.order,
25
+ };
26
+ if (opts.tags)
27
+ params.tags = opts.tags;
28
+ if (opts.document)
29
+ params.documentSlug = opts.document;
30
+ const data = await client.get(`/requirements/${tenant}/${project}`, params);
31
+ all.push(...(data.data ?? []));
32
+ if (page >= (data.meta?.totalPages ?? 1))
33
+ break;
34
+ }
35
+ if (isJsonMode()) {
36
+ output({ data: all, meta: { totalItems: all.length } });
37
+ }
38
+ else {
39
+ console.log(`All requirements: ${all.length}\n`);
40
+ printTable(["Ref", "Text", "Pattern", "QA", "Tags"], all.map(r => [r.ref ?? "?", truncate(r.text ?? "", 60), r.pattern ?? "", r.qaScore != null ? String(r.qaScore) : "-", (r.tags ?? []).join(", ")]));
41
+ }
42
+ return;
43
+ }
17
44
  const params = {
18
45
  page: opts.page,
19
46
  limit: opts.limit,
@@ -258,4 +285,113 @@ export function registerRequirementCommands(program, client) {
258
285
  ]));
259
286
  }
260
287
  });
288
+ // ── reassign: move requirement to a different document/section ──
289
+ cmd
290
+ .command("reassign")
291
+ .description("Move a requirement to a different document/section (preserves ID and trace links)")
292
+ .argument("<tenant>", "Tenant slug")
293
+ .argument("<project>", "Project slug")
294
+ .argument("<id>", "Requirement ref, ID, or hashId")
295
+ .requiredOption("--section <id>", "Target section ID (determines the target document)")
296
+ .action(async (tenant, project, id, opts) => {
297
+ const resolvedId = await resolveRequirementId(client, tenant, project, id);
298
+ await client.patch(`/requirements/${tenant}/${project}/${resolvedId}`, {
299
+ sectionId: opts.section,
300
+ });
301
+ if (isJsonMode()) {
302
+ output({ ok: true, movedTo: opts.section });
303
+ }
304
+ else {
305
+ console.log(`Requirement reassigned to section ${opts.section}. Ref will update to match new document prefix.`);
306
+ }
307
+ });
308
+ // ── bulk-create: create multiple requirements from JSON array ──
309
+ cmd
310
+ .command("bulk-create")
311
+ .description("Create multiple requirements from a JSON file (array of objects)")
312
+ .argument("<tenant>", "Tenant slug")
313
+ .argument("<project-key>", "Project key")
314
+ .requiredOption("--file <path>", "Path to JSON file (array of {text, document?, section?, verification?, rationale?, tags?, idempotencyKey?})")
315
+ .option("--dry-run", "Validate without creating")
316
+ .action(async (tenant, projectKey, opts) => {
317
+ const content = readFileSync(opts.file, "utf-8");
318
+ let items;
319
+ try {
320
+ items = JSON.parse(content);
321
+ if (!Array.isArray(items))
322
+ throw new Error("File must contain a JSON array");
323
+ }
324
+ catch (err) {
325
+ console.error(`Invalid JSON: ${err.message}`);
326
+ process.exit(1);
327
+ }
328
+ console.error(`Processing ${items.length} requirement(s)...`);
329
+ let created = 0, skipped = 0, errors = 0;
330
+ for (let i = 0; i < items.length; i++) {
331
+ const item = items[i];
332
+ const text = String(item.text ?? "").trim();
333
+ if (!text || text.length < 10) {
334
+ console.error(` [${i}] Skipped: text too short`);
335
+ skipped++;
336
+ continue;
337
+ }
338
+ if (opts.dryRun) {
339
+ console.log(` [dry-run] ${truncate(text, 80)}`);
340
+ created++;
341
+ continue;
342
+ }
343
+ try {
344
+ const body = { tenant, projectKey, text };
345
+ if (item.document || item.documentSlug)
346
+ body.documentSlug = item.document ?? item.documentSlug;
347
+ if (item.section || item.sectionId)
348
+ body.sectionId = item.section ?? item.sectionId;
349
+ if (item.verification)
350
+ body.verification = item.verification;
351
+ if (item.rationale)
352
+ body.rationale = item.rationale;
353
+ if (item.pattern)
354
+ body.pattern = item.pattern;
355
+ if (item.compliance)
356
+ body.complianceStatus = item.compliance;
357
+ if (item.idempotencyKey)
358
+ body.idempotencyKey = item.idempotencyKey;
359
+ if (Array.isArray(item.tags))
360
+ body.tags = item.tags;
361
+ else if (typeof item.tags === "string")
362
+ body.tags = item.tags.split(",").map(t => t.trim());
363
+ await client.post("/requirements", body);
364
+ created++;
365
+ }
366
+ catch (err) {
367
+ console.error(` [${i}] Error: ${err.message}`);
368
+ errors++;
369
+ }
370
+ }
371
+ console.log(`${opts.dryRun ? "Would create" : "Created"} ${created}. Skipped: ${skipped}. Errors: ${errors}.`);
372
+ });
373
+ // ── text-search: search requirements by text content ──
374
+ cmd
375
+ .command("text-search")
376
+ .description("Search requirements by text content (case-insensitive substring match)")
377
+ .argument("<tenant>", "Tenant slug")
378
+ .argument("<project>", "Project slug")
379
+ .requiredOption("--text <query>", "Text to search for")
380
+ .option("-l, --limit <n>", "Max results", "50")
381
+ .action(async (tenant, project, opts) => {
382
+ const data = await client.get(`/requirements/${tenant}/${project}`, { textContains: opts.text, limit: opts.limit, sortBy: "ref", sortOrder: "asc" });
383
+ const reqs = data.data ?? [];
384
+ if (isJsonMode()) {
385
+ output(reqs);
386
+ }
387
+ else {
388
+ console.log(`Found ${reqs.length} requirement(s) matching "${opts.text}":\n`);
389
+ printTable(["Ref", "Text", "Document", "QA"], reqs.map(r => [
390
+ r.ref ?? "?",
391
+ truncate(r.text ?? "", 55),
392
+ r.documentSlug ?? "",
393
+ r.qaScore != null ? String(r.qaScore) : "-",
394
+ ]));
395
+ }
396
+ });
261
397
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "airgen-cli",
3
- "version": "0.13.0",
3
+ "version": "0.14.0",
4
4
  "description": "AIRGen CLI — requirements engineering from the command line",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",