airgen-cli 0.18.0 → 0.19.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.
@@ -180,7 +180,9 @@ export function registerRequirementCommands(program, client) {
180
180
  .option("--rationale <r>", "Rationale")
181
181
  .option("--compliance <status>", "Compliance status")
182
182
  .option("--section <id>", "Section ID")
183
- .option("--tags <tags>", "Comma-separated tags")
183
+ .option("--tags <tags>", "Comma-separated tags (replaces all)")
184
+ .option("--add-tags <tags>", "Comma-separated tags to add (keeps existing)")
185
+ .option("--remove-tags <tags>", "Comma-separated tags to remove")
184
186
  .action(async (tenant, project, id, opts) => {
185
187
  const resolvedId = await resolveRequirementId(client, tenant, project, id);
186
188
  const body = {};
@@ -196,8 +198,23 @@ export function registerRequirementCommands(program, client) {
196
198
  body.complianceStatus = opts.compliance;
197
199
  if (opts.section)
198
200
  body.sectionId = opts.section;
199
- if (opts.tags)
201
+ if (opts.addTags || opts.removeTags) {
202
+ // Read-modify-write for add/remove tags — use original ref for GET (resolvedId may be composite)
203
+ const existing = await client.get(`/requirements/${tenant}/${project}/${encodeURIComponent(id)}`);
204
+ let currentTags = existing.record?.tags ?? [];
205
+ if (opts.addTags) {
206
+ const toAdd = opts.addTags.split(",").map(t => t.trim());
207
+ currentTags = [...new Set([...currentTags, ...toAdd])];
208
+ }
209
+ if (opts.removeTags) {
210
+ const toRemove = new Set(opts.removeTags.split(",").map(t => t.trim()));
211
+ currentTags = currentTags.filter(t => !toRemove.has(t));
212
+ }
213
+ body.tags = currentTags;
214
+ }
215
+ else if (opts.tags) {
200
216
  body.tags = opts.tags.split(",").map(t => t.trim());
217
+ }
201
218
  await client.patch(`/requirements/${tenant}/${project}/${resolvedId}`, body);
202
219
  if (isJsonMode()) {
203
220
  output({ ok: true });
@@ -299,7 +316,7 @@ export function registerRequirementCommands(program, client) {
299
316
  const data = await client.get(`/requirements/${tenant}/${project}`, params);
300
317
  const reqs = data.data ?? [];
301
318
  if (isJsonMode()) {
302
- output(reqs);
319
+ output({ data: reqs, meta: { totalItems: reqs.length } });
303
320
  }
304
321
  else {
305
322
  console.log(`Filtered: ${reqs.length} requirements\n`);
@@ -396,22 +413,84 @@ export function registerRequirementCommands(program, client) {
396
413
  }
397
414
  console.log(`${opts.dryRun ? "Would create" : "Created"} ${created}. Skipped: ${skipped}. Errors: ${errors}.`);
398
415
  });
416
+ // ── bulk-update: update multiple requirements from JSON array ──
417
+ cmd
418
+ .command("bulk-update")
419
+ .description("Update multiple requirements from a JSON file (array of {ref, text?, rationale?, tags?, ...})")
420
+ .argument("<tenant>", "Tenant slug")
421
+ .argument("<project>", "Project slug")
422
+ .requiredOption("--file <path>", "Path to JSON file")
423
+ .option("--dry-run", "Validate without updating")
424
+ .action(async (tenant, project, opts) => {
425
+ const content = readFileSync(opts.file, "utf-8");
426
+ let items;
427
+ try {
428
+ items = JSON.parse(content);
429
+ if (!Array.isArray(items))
430
+ throw new Error("File must contain a JSON array");
431
+ }
432
+ catch (err) {
433
+ console.error(`Invalid JSON: ${err.message}`);
434
+ process.exit(1);
435
+ }
436
+ console.error(`Processing ${items.length} update(s)...`);
437
+ let updated = 0, errors = 0;
438
+ for (let i = 0; i < items.length; i++) {
439
+ const item = items[i];
440
+ const ref = String(item.ref ?? item.id ?? "");
441
+ if (!ref) {
442
+ console.error(` [${i}] Skipped: no ref or id`);
443
+ errors++;
444
+ continue;
445
+ }
446
+ if (opts.dryRun) {
447
+ console.log(` [dry-run] ${ref}: would update ${Object.keys(item).filter(k => k !== "ref" && k !== "id").join(", ")}`);
448
+ updated++;
449
+ continue;
450
+ }
451
+ try {
452
+ const resolvedId = await resolveRequirementId(client, tenant, project, ref);
453
+ const body = {};
454
+ if (item.text)
455
+ body.text = item.text;
456
+ if (item.pattern)
457
+ body.pattern = item.pattern;
458
+ if (item.verification)
459
+ body.verification = item.verification;
460
+ if (item.rationale)
461
+ body.rationale = item.rationale;
462
+ if (item.complianceStatus || item.compliance)
463
+ body.complianceStatus = item.complianceStatus ?? item.compliance;
464
+ if (item.sectionId || item.section)
465
+ body.sectionId = item.sectionId ?? item.section;
466
+ if (item.tags)
467
+ body.tags = Array.isArray(item.tags) ? item.tags : String(item.tags).split(",").map(t => t.trim());
468
+ await client.patch(`/requirements/${tenant}/${project}/${resolvedId}`, body);
469
+ updated++;
470
+ }
471
+ catch (err) {
472
+ console.error(` [${i}] ${ref}: ${err.message}`);
473
+ errors++;
474
+ }
475
+ }
476
+ console.log(`${opts.dryRun ? "Would update" : "Updated"} ${updated}. Errors: ${errors}.`);
477
+ });
399
478
  // ── text-search: search requirements by text content ──
400
479
  cmd
401
480
  .command("text-search")
402
481
  .description("Search requirements by text content (case-insensitive substring match)")
403
482
  .argument("<tenant>", "Tenant slug")
404
483
  .argument("<project>", "Project slug")
405
- .requiredOption("--text <query>", "Text to search for")
484
+ .requiredOption("-q, --query <text>", "Text to search for")
406
485
  .option("-l, --limit <n>", "Max results", "50")
407
486
  .action(async (tenant, project, opts) => {
408
- const data = await client.get(`/requirements/${tenant}/${project}`, { textContains: opts.text, limit: opts.limit, sortBy: "ref", sortOrder: "asc" });
487
+ const data = await client.get(`/requirements/${tenant}/${project}`, { textContains: opts.query, limit: opts.limit, sortBy: "ref", sortOrder: "asc" });
409
488
  const reqs = data.data ?? [];
410
489
  if (isJsonMode()) {
411
490
  output(reqs);
412
491
  }
413
492
  else {
414
- console.log(`Found ${reqs.length} requirement(s) matching "${opts.text}":\n`);
493
+ console.log(`Found ${reqs.length} requirement(s) matching "${opts.query}":\n`);
415
494
  printTable(["Ref", "Text", "Document", "QA"], reqs.map(r => [
416
495
  r.ref ?? "?",
417
496
  truncate(r.text ?? "", 55),
@@ -8,16 +8,24 @@ export function registerTraceabilityCommands(program, client) {
8
8
  .argument("<tenant>", "Tenant slug")
9
9
  .argument("<project>", "Project slug")
10
10
  .option("--requirement <id>", "Filter by requirement ID")
11
+ .option("-l, --limit <n>", "Max results to display")
12
+ .option("--type <type>", "Filter by link type")
11
13
  .action(async (tenant, project, opts) => {
12
14
  const path = opts.requirement
13
15
  ? `/trace-links/${tenant}/${project}/${opts.requirement}`
14
16
  : `/trace-links/${tenant}/${project}`;
15
17
  const data = await client.get(path);
16
- const links = data.traceLinks ?? [];
18
+ let links = data.traceLinks ?? [];
19
+ if (opts.type)
20
+ links = links.filter(l => l.linkType === opts.type);
21
+ const total = links.length;
22
+ if (opts.limit)
23
+ links = links.slice(0, parseInt(opts.limit, 10));
17
24
  if (isJsonMode()) {
18
- output(links);
25
+ output({ data: links, meta: { totalItems: total, showing: links.length } });
19
26
  }
20
27
  else {
28
+ console.log(`Trace links: ${links.length}${total > links.length ? ` of ${total}` : ""}\n`);
21
29
  printTable(["ID", "Source", "Target", "Type", "Description"], links.map(l => [
22
30
  l.id,
23
31
  l.sourceRef ?? l.sourceRequirementId ?? "",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "airgen-cli",
3
- "version": "0.18.0",
3
+ "version": "0.19.1",
4
4
  "description": "AIRGen CLI — requirements engineering from the command line",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",