airgen-cli 0.1.1 → 0.1.3

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,75 +1,342 @@
1
- import { output } from "../output.js";
1
+ import { readFileSync } from "node:fs";
2
+ import { output, printTable, isJsonMode, truncate } from "../output.js";
3
+ import { resolveRequirementId } from "../resolve.js";
4
+ const IMPL_STATUSES = ["not_started", "in_progress", "implemented", "verified", "blocked"];
5
+ const IMPL_TAG_PREFIX = "impl:";
6
+ const PAGE_SIZE = 100;
7
+ const MAX_PAGES = 50;
8
+ // ── Helpers ──────────────────────────────────────────────────
9
+ async function fetchAllRequirements(client, tenant, project) {
10
+ const all = [];
11
+ for (let page = 1; page <= MAX_PAGES; page++) {
12
+ const data = await client.get(`/requirements/${tenant}/${project}`, {
13
+ page: String(page),
14
+ limit: String(PAGE_SIZE),
15
+ });
16
+ const items = data.data ?? [];
17
+ all.push(...items);
18
+ const totalPages = data.meta?.totalPages ?? 1;
19
+ if (page >= totalPages)
20
+ break;
21
+ }
22
+ return all.filter((r) => !r.deleted && !r.deletedAt);
23
+ }
24
+ function getImplStatus(tags) {
25
+ if (!tags)
26
+ return null;
27
+ const implTag = tags.find((t) => t.startsWith(IMPL_TAG_PREFIX));
28
+ if (!implTag)
29
+ return null;
30
+ const status = implTag.slice(IMPL_TAG_PREFIX.length);
31
+ return IMPL_STATUSES.includes(status) ? status : null;
32
+ }
33
+ function parseArtifacts(attributes) {
34
+ if (!attributes?.artifacts || typeof attributes.artifacts !== "string")
35
+ return [];
36
+ try {
37
+ const parsed = JSON.parse(attributes.artifacts);
38
+ return Array.isArray(parsed) ? parsed : [];
39
+ }
40
+ catch {
41
+ return [];
42
+ }
43
+ }
44
+ function statusEmoji(status) {
45
+ switch (status) {
46
+ case "not_started": return "[ ]";
47
+ case "in_progress": return "[~]";
48
+ case "implemented": return "[x]";
49
+ case "verified": return "[v]";
50
+ case "blocked": return "[!]";
51
+ default: return "[-]";
52
+ }
53
+ }
54
+ function pct(n, total) {
55
+ if (total === 0)
56
+ return "0.0%";
57
+ return ((n / total) * 100).toFixed(1) + "%";
58
+ }
59
+ // ── Command registration ─────────────────────────────────────
2
60
  export function registerImplementationCommands(program, client) {
3
61
  const cmd = program.command("impl").description("Implementation tracking");
62
+ // ── impl status ──────────────────────────────────────────
4
63
  cmd
5
64
  .command("status")
6
- .description("Set implementation status on a requirement")
65
+ .description("Set implementation status on a requirement (syncs tags + attributes)")
7
66
  .argument("<tenant>", "Tenant slug")
8
67
  .argument("<project>", "Project slug")
9
- .argument("<requirement-id>", "Requirement ID")
68
+ .argument("<requirement>", "Requirement ref, ID, or hashId")
10
69
  .requiredOption("--status <s>", "Status: not_started, in_progress, implemented, verified, blocked")
11
70
  .option("--notes <text>", "Notes")
12
- .action(async (tenant, project, requirementId, opts) => {
13
- // Implementation status is stored in requirement attributes
14
- const data = await client.patch(`/requirements/${tenant}/${project}/${requirementId}`, {
15
- attributes: {
16
- implementationStatus: opts.status,
17
- implementationNotes: opts.notes,
18
- },
71
+ .action(async (tenant, project, requirement, opts) => {
72
+ if (!IMPL_STATUSES.includes(opts.status)) {
73
+ console.error(`Invalid status: ${opts.status}. Must be one of: ${IMPL_STATUSES.join(", ")}`);
74
+ process.exit(1);
75
+ }
76
+ const resolvedId = await resolveRequirementId(client, tenant, project, requirement);
77
+ // Fetch current requirement to get existing tags + attributes
78
+ const reqData = await client.get(`/requirements/${tenant}/${project}`, { page: "1", limit: "100" });
79
+ const req = (reqData.data ?? []).find(r => r.id === resolvedId);
80
+ if (!req) {
81
+ console.error(`Requirement ${requirement} not found.`);
82
+ process.exit(1);
83
+ }
84
+ const now = new Date().toISOString();
85
+ // Update tags: remove existing impl:* tags, add new one
86
+ const currentTags = (req.tags ?? []).filter(t => !t.startsWith(IMPL_TAG_PREFIX));
87
+ currentTags.push(`${IMPL_TAG_PREFIX}${opts.status}`);
88
+ // Update attributes (matching MCP server's field names)
89
+ const updatedAttributes = {
90
+ ...(req.attributes ?? {}),
91
+ impl_status: opts.status,
92
+ impl_updated_at: now,
93
+ };
94
+ if (opts.notes !== undefined) {
95
+ updatedAttributes.impl_notes = opts.notes || null;
96
+ }
97
+ await client.patch(`/requirements/${tenant}/${project}/${resolvedId}`, {
98
+ tags: currentTags,
99
+ attributes: updatedAttributes,
19
100
  });
20
- output(data);
101
+ if (isJsonMode()) {
102
+ output({ ok: true, id: resolvedId, status: opts.status, tags: currentTags });
103
+ }
104
+ else {
105
+ console.log(`Implementation status set to ${opts.status} on ${req.ref ?? resolvedId}.`);
106
+ }
107
+ });
108
+ // ── impl summary ─────────────────────────────────────────
109
+ cmd
110
+ .command("summary")
111
+ .description("Implementation coverage report for a project")
112
+ .argument("<tenant>", "Tenant slug")
113
+ .argument("<project>", "Project slug")
114
+ .action(async (tenant, project) => {
115
+ const requirements = await fetchAllRequirements(client, tenant, project);
116
+ const total = requirements.length;
117
+ if (total === 0) {
118
+ console.log("No requirements found.");
119
+ return;
120
+ }
121
+ const statusCounts = {};
122
+ for (const s of IMPL_STATUSES)
123
+ statusCounts[s] = 0;
124
+ statusCounts["unset"] = 0;
125
+ let withArtifactCount = 0;
126
+ for (const req of requirements) {
127
+ const status = getImplStatus(req.tags);
128
+ if (status) {
129
+ statusCounts[status]++;
130
+ }
131
+ else {
132
+ statusCounts["unset"]++;
133
+ }
134
+ const artifacts = parseArtifacts(req.attributes);
135
+ if (artifacts.length > 0)
136
+ withArtifactCount++;
137
+ }
138
+ const done = (statusCounts["implemented"] ?? 0) + (statusCounts["verified"] ?? 0);
139
+ const inProgress = statusCounts["in_progress"] ?? 0;
140
+ const blocked = statusCounts["blocked"] ?? 0;
141
+ if (isJsonMode()) {
142
+ output({ total, statusCounts, done, inProgress, blocked, withArtifacts: withArtifactCount });
143
+ return;
144
+ }
145
+ console.log(`Implementation Report (${total} requirements)\n`);
146
+ console.log(`Coverage: ${pct(done, total)} | Artifacts: ${pct(withArtifactCount, total)}\n`);
147
+ printTable(["Status", "Count", "%", ""], [...IMPL_STATUSES, "unset"].map(s => {
148
+ const count = statusCounts[s] ?? 0;
149
+ const bar = "\u2588".repeat(Math.round((count / total) * 30));
150
+ const emoji = s === "unset" ? "[-]" : statusEmoji(s);
151
+ return [`${emoji} ${s}`, String(count), pct(count, total), bar];
152
+ }));
153
+ console.log(`\nDone (implemented + verified): ${done}`);
154
+ console.log(`In progress: ${inProgress}`);
155
+ console.log(`Blocked: ${blocked}`);
156
+ console.log(`Not started / unset: ${total - done - inProgress - blocked}`);
157
+ console.log(`With artifact links: ${withArtifactCount}`);
158
+ });
159
+ // ── impl list ────────────────────────────────────────────
160
+ cmd
161
+ .command("list")
162
+ .description("List requirements filtered by implementation status")
163
+ .argument("<tenant>", "Tenant slug")
164
+ .argument("<project>", "Project slug")
165
+ .option("--status <s>", "Filter by status: not_started, in_progress, implemented, verified, blocked, unset")
166
+ .action(async (tenant, project, opts) => {
167
+ const requirements = await fetchAllRequirements(client, tenant, project);
168
+ let filtered = requirements;
169
+ if (opts.status) {
170
+ if (opts.status === "unset") {
171
+ filtered = requirements.filter(r => getImplStatus(r.tags) === null);
172
+ }
173
+ else {
174
+ filtered = requirements.filter(r => getImplStatus(r.tags) === opts.status);
175
+ }
176
+ }
177
+ if (isJsonMode()) {
178
+ output(filtered.map(r => ({
179
+ ref: r.ref,
180
+ id: r.id,
181
+ text: r.text,
182
+ status: getImplStatus(r.tags) ?? "unset",
183
+ artifacts: parseArtifacts(r.attributes).length,
184
+ })));
185
+ return;
186
+ }
187
+ console.log(`Implementation status: ${opts.status ?? "all"} (${filtered.length} requirements)\n`);
188
+ if (filtered.length === 0) {
189
+ console.log("No matching requirements.");
190
+ return;
191
+ }
192
+ printTable(["Ref", "Status", "Text", "Artifacts"], filtered.map(r => [
193
+ r.ref ?? "?",
194
+ statusEmoji(getImplStatus(r.tags)) + " " + (getImplStatus(r.tags) ?? "unset"),
195
+ truncate(r.text ?? "", 50),
196
+ String(parseArtifacts(r.attributes).length),
197
+ ]));
198
+ });
199
+ // ── impl bulk-update ─────────────────────────────────────
200
+ cmd
201
+ .command("bulk-update")
202
+ .description("Bulk update implementation status from a JSON file")
203
+ .argument("<tenant>", "Tenant slug")
204
+ .argument("<project>", "Project slug")
205
+ .requiredOption("--file <path>", "JSON file with array of {ref, status, notes?}")
206
+ .action(async (tenant, project, opts) => {
207
+ let items;
208
+ try {
209
+ const raw = readFileSync(opts.file, "utf-8");
210
+ items = JSON.parse(raw);
211
+ if (!Array.isArray(items))
212
+ throw new Error("File must contain a JSON array");
213
+ }
214
+ catch (err) {
215
+ console.error(`Failed to read file: ${err.message}`);
216
+ process.exit(1);
217
+ }
218
+ let updated = 0;
219
+ let failed = 0;
220
+ const errors = [];
221
+ for (const item of items) {
222
+ if (!IMPL_STATUSES.includes(item.status)) {
223
+ errors.push(`${item.ref}: invalid status "${item.status}"`);
224
+ failed++;
225
+ continue;
226
+ }
227
+ try {
228
+ const resolvedId = await resolveRequirementId(client, tenant, project, item.ref);
229
+ // Fetch requirement for current tags/attributes
230
+ const reqData = await client.get(`/requirements/${tenant}/${project}`, { page: "1", limit: "100" });
231
+ const req = (reqData.data ?? []).find(r => r.id === resolvedId);
232
+ if (!req) {
233
+ errors.push(`${item.ref}: not found`);
234
+ failed++;
235
+ continue;
236
+ }
237
+ const now = new Date().toISOString();
238
+ const currentTags = (req.tags ?? []).filter(t => !t.startsWith(IMPL_TAG_PREFIX));
239
+ currentTags.push(`${IMPL_TAG_PREFIX}${item.status}`);
240
+ const updatedAttributes = {
241
+ ...(req.attributes ?? {}),
242
+ impl_status: item.status,
243
+ impl_updated_at: now,
244
+ };
245
+ if (item.notes !== undefined) {
246
+ updatedAttributes.impl_notes = item.notes || null;
247
+ }
248
+ await client.patch(`/requirements/${tenant}/${project}/${resolvedId}`, {
249
+ tags: currentTags,
250
+ attributes: updatedAttributes,
251
+ });
252
+ updated++;
253
+ }
254
+ catch (err) {
255
+ errors.push(`${item.ref}: ${err.message}`);
256
+ failed++;
257
+ }
258
+ }
259
+ if (isJsonMode()) {
260
+ output({ total: items.length, updated, failed, errors });
261
+ }
262
+ else {
263
+ console.log(`Bulk update: ${updated} updated, ${failed} failed (${items.length} total)`);
264
+ for (const e of errors) {
265
+ console.error(` - ${e}`);
266
+ }
267
+ }
21
268
  });
269
+ // ── impl link ────────────────────────────────────────────
22
270
  cmd
23
271
  .command("link")
24
272
  .description("Link a code artifact to a requirement")
25
273
  .argument("<tenant>", "Tenant slug")
26
274
  .argument("<project>", "Project slug")
27
- .argument("<requirement-id>", "Requirement ID")
275
+ .argument("<requirement>", "Requirement ref, ID, or hashId")
28
276
  .requiredOption("--type <type>", "Artifact type: file, commit, pr, issue, test, url")
29
277
  .requiredOption("--path <path>", "Artifact path/reference")
30
278
  .option("--label <text>", "Label")
31
279
  .option("--line <n>", "Line number (for file type)")
32
- .action(async (tenant, project, requirementId, opts) => {
33
- // Fetch current requirement to get existing artifacts
34
- const reqData = await client.get(`/requirements/${tenant}/${project}`, { page: "1", limit: "500" });
35
- const req = (reqData.data ?? []).find(r => r.id === requirementId);
280
+ .action(async (tenant, project, requirement, opts) => {
281
+ const resolvedId = await resolveRequirementId(client, tenant, project, requirement);
282
+ // Fetch current requirement
283
+ const reqData = await client.get(`/requirements/${tenant}/${project}`, { page: "1", limit: "100" });
284
+ const req = (reqData.data ?? []).find(r => r.id === resolvedId);
36
285
  if (!req) {
37
- console.error(`Requirement ${requirementId} not found.`);
286
+ console.error(`Requirement ${requirement} not found.`);
38
287
  process.exit(1);
39
288
  }
40
- const attrs = req.attributes ?? {};
41
- const artifacts = attrs.artifacts ?? [];
289
+ const artifacts = parseArtifacts(req.attributes);
42
290
  const newArtifact = { type: opts.type, path: opts.path };
43
291
  if (opts.label)
44
292
  newArtifact.label = opts.label;
45
293
  if (opts.line)
46
294
  newArtifact.line = parseInt(opts.line, 10);
295
+ // Check for duplicates
296
+ const exists = artifacts.some(a => a.type === newArtifact.type && a.path === newArtifact.path);
297
+ if (exists) {
298
+ console.log("Artifact already linked.");
299
+ return;
300
+ }
47
301
  artifacts.push(newArtifact);
48
- await client.patch(`/requirements/${tenant}/${project}/${requirementId}`, {
49
- attributes: { ...attrs, artifacts },
302
+ const updatedAttributes = {
303
+ ...(req.attributes ?? {}),
304
+ artifacts: JSON.stringify(artifacts),
305
+ };
306
+ await client.patch(`/requirements/${tenant}/${project}/${resolvedId}`, {
307
+ attributes: updatedAttributes,
50
308
  });
51
309
  console.log("Artifact linked.");
52
310
  });
311
+ // ── impl unlink ──────────────────────────────────────────
53
312
  cmd
54
313
  .command("unlink")
55
314
  .description("Remove an artifact link from a requirement")
56
315
  .argument("<tenant>", "Tenant slug")
57
316
  .argument("<project>", "Project slug")
58
- .argument("<requirement-id>", "Requirement ID")
317
+ .argument("<requirement>", "Requirement ref, ID, or hashId")
59
318
  .requiredOption("--type <type>", "Artifact type")
60
319
  .requiredOption("--path <path>", "Artifact path")
61
- .action(async (tenant, project, requirementId, opts) => {
62
- const reqData = await client.get(`/requirements/${tenant}/${project}`, { page: "1", limit: "500" });
63
- const req = (reqData.data ?? []).find(r => r.id === requirementId);
320
+ .action(async (tenant, project, requirement, opts) => {
321
+ const resolvedId = await resolveRequirementId(client, tenant, project, requirement);
322
+ const reqData = await client.get(`/requirements/${tenant}/${project}`, { page: "1", limit: "100" });
323
+ const req = (reqData.data ?? []).find(r => r.id === resolvedId);
64
324
  if (!req) {
65
- console.error(`Requirement ${requirementId} not found.`);
325
+ console.error(`Requirement ${requirement} not found.`);
66
326
  process.exit(1);
67
327
  }
68
- const attrs = req.attributes ?? {};
69
- const artifacts = attrs.artifacts ?? [];
328
+ const artifacts = parseArtifacts(req.attributes);
70
329
  const filtered = artifacts.filter(a => !(a.type === opts.type && a.path === opts.path));
71
- await client.patch(`/requirements/${tenant}/${project}/${requirementId}`, {
72
- attributes: { ...attrs, artifacts: filtered },
330
+ if (filtered.length === artifacts.length) {
331
+ console.log("No matching artifact found.");
332
+ return;
333
+ }
334
+ const updatedAttributes = {
335
+ ...(req.attributes ?? {}),
336
+ artifacts: filtered.length > 0 ? JSON.stringify(filtered) : null,
337
+ };
338
+ await client.patch(`/requirements/${tenant}/${project}/${resolvedId}`, {
339
+ attributes: updatedAttributes,
73
340
  });
74
341
  console.log("Artifact unlinked.");
75
342
  });
@@ -1,4 +1,15 @@
1
1
  import { output, printTable, isJsonMode, truncate } from "../output.js";
2
+ const IMPL_TAG_PREFIX = "impl:";
3
+ const IMPL_STATUSES = ["not_started", "in_progress", "implemented", "verified", "blocked"];
4
+ function getImplStatus(tags) {
5
+ if (!tags)
6
+ return null;
7
+ const implTag = tags.find(t => t.startsWith(IMPL_TAG_PREFIX));
8
+ if (!implTag)
9
+ return null;
10
+ const status = implTag.slice(IMPL_TAG_PREFIX.length);
11
+ return IMPL_STATUSES.includes(status) ? status : null;
12
+ }
2
13
  export function registerReportCommands(program, client) {
3
14
  const cmd = program.command("reports").alias("report").description("Project reports");
4
15
  cmd
@@ -68,16 +79,32 @@ export function registerReportCommands(program, client) {
68
79
  .action(async (tenant, project, opts) => {
69
80
  const data = await client.get(`/requirements/${tenant}/${project}`, { page: "1", limit: opts.limit });
70
81
  const reqs = data.data ?? [];
71
- const counts = {};
82
+ // Compliance status
83
+ const compCounts = {};
72
84
  for (const r of reqs) {
73
85
  const status = r.complianceStatus || "Unset";
74
- counts[status] = (counts[status] ?? 0) + 1;
86
+ compCounts[status] = (compCounts[status] ?? 0) + 1;
87
+ }
88
+ // Implementation status (read from tags, matching MCP server)
89
+ const implCounts = {};
90
+ for (const s of IMPL_STATUSES)
91
+ implCounts[s] = 0;
92
+ implCounts["unset"] = 0;
93
+ for (const r of reqs) {
94
+ const status = getImplStatus(r.tags);
95
+ if (status)
96
+ implCounts[status]++;
97
+ else
98
+ implCounts["unset"]++;
75
99
  }
76
100
  if (isJsonMode()) {
77
- output(counts);
101
+ output({ compliance: compCounts, implementation: implCounts });
78
102
  }
79
103
  else {
80
- printTable(["Status", "Count"], Object.entries(counts).map(([k, v]) => [k, String(v)]));
104
+ console.log("Compliance Status:");
105
+ printTable(["Status", "Count"], Object.entries(compCounts).map(([k, v]) => [k, String(v)]));
106
+ console.log("\nImplementation Status:");
107
+ printTable(["Status", "Count"], Object.entries(implCounts).filter(([, v]) => v > 0).map(([k, v]) => [k, String(v)]));
81
108
  }
82
109
  });
83
110
  cmd
@@ -1,4 +1,5 @@
1
1
  import { output, printTable, isJsonMode, truncate } from "../output.js";
2
+ import { resolveRequirementId } from "../resolve.js";
2
3
  export function registerRequirementCommands(program, client) {
3
4
  const cmd = program.command("requirements").alias("reqs").description("Manage requirements");
4
5
  cmd
@@ -96,7 +97,7 @@ export function registerRequirementCommands(program, client) {
96
97
  .description("Update a requirement")
97
98
  .argument("<tenant>", "Tenant slug")
98
99
  .argument("<project>", "Project slug")
99
- .argument("<id>", "Requirement ID")
100
+ .argument("<id>", "Requirement ref, ID, or hashId")
100
101
  .option("--text <text>", "Requirement text")
101
102
  .option("--pattern <p>", "Pattern")
102
103
  .option("--verification <v>", "Verification method")
@@ -105,6 +106,7 @@ export function registerRequirementCommands(program, client) {
105
106
  .option("--section <id>", "Section ID")
106
107
  .option("--tags <tags>", "Comma-separated tags")
107
108
  .action(async (tenant, project, id, opts) => {
109
+ const resolvedId = await resolveRequirementId(client, tenant, project, id);
108
110
  const body = {};
109
111
  if (opts.text)
110
112
  body.text = opts.text;
@@ -120,7 +122,7 @@ export function registerRequirementCommands(program, client) {
120
122
  body.sectionId = opts.section;
121
123
  if (opts.tags)
122
124
  body.tags = opts.tags.split(",").map(t => t.trim());
123
- await client.patch(`/requirements/${tenant}/${project}/${id}`, body);
125
+ await client.patch(`/requirements/${tenant}/${project}/${resolvedId}`, body);
124
126
  if (isJsonMode()) {
125
127
  output({ ok: true });
126
128
  }
@@ -133,9 +135,10 @@ export function registerRequirementCommands(program, client) {
133
135
  .description("Soft-delete a requirement")
134
136
  .argument("<tenant>", "Tenant slug")
135
137
  .argument("<project>", "Project slug")
136
- .argument("<id>", "Requirement ID")
138
+ .argument("<id>", "Requirement ref, ID, or hashId")
137
139
  .action(async (tenant, project, id) => {
138
- await client.delete(`/requirements/${tenant}/${project}/${id}`);
140
+ const resolvedId = await resolveRequirementId(client, tenant, project, id);
141
+ await client.delete(`/requirements/${tenant}/${project}/${resolvedId}`);
139
142
  console.log("Requirement deleted.");
140
143
  });
141
144
  cmd
@@ -143,9 +146,10 @@ export function registerRequirementCommands(program, client) {
143
146
  .description("Get version history of a requirement")
144
147
  .argument("<tenant>", "Tenant slug")
145
148
  .argument("<project>", "Project slug")
146
- .argument("<id>", "Requirement ID")
149
+ .argument("<id>", "Requirement ref, ID, or hashId")
147
150
  .action(async (tenant, project, id) => {
148
- const data = await client.get(`/requirements/${tenant}/${project}/${id}/history`);
151
+ const resolvedId = await resolveRequirementId(client, tenant, project, id);
152
+ const data = await client.get(`/requirements/${tenant}/${project}/${resolvedId}/history`);
149
153
  output(data);
150
154
  });
151
155
  cmd
@@ -0,0 +1,8 @@
1
+ /**
2
+ * Resolve any requirement identifier to its full ID.
3
+ *
4
+ * Accepts: full ID, ref, short ID (REQ-XXX), or hashId.
5
+ * Returns the full colon-separated ID (tenant:project:REQ-XXX).
6
+ */
7
+ import type { AirgenClient } from "./client.js";
8
+ export declare function resolveRequirementId(client: AirgenClient, tenant: string, project: string, identifier: string): Promise<string>;
@@ -0,0 +1,41 @@
1
+ /**
2
+ * Resolve any requirement identifier to its full ID.
3
+ *
4
+ * Accepts: full ID, ref, short ID (REQ-XXX), or hashId.
5
+ * Returns the full colon-separated ID (tenant:project:REQ-XXX).
6
+ */
7
+ export async function resolveRequirementId(client, tenant, project, identifier) {
8
+ // Already a full ID (contains colons)
9
+ if (identifier.includes(":")) {
10
+ return identifier;
11
+ }
12
+ // Try as ref first (GET /requirements/{tenant}/{project}/{ref})
13
+ try {
14
+ const data = await client.get(`/requirements/${tenant}/${project}/${identifier}`);
15
+ if (data.record?.id)
16
+ return data.record.id;
17
+ }
18
+ catch {
19
+ // Not found by ref — continue
20
+ }
21
+ // Search through requirements for short ID or hashId match
22
+ let page = 1;
23
+ const limit = 100;
24
+ while (true) {
25
+ const data = await client.get(`/requirements/${tenant}/${project}`, { page: String(page), limit: String(limit) });
26
+ const reqs = data.data ?? [];
27
+ for (const r of reqs) {
28
+ // Match by short ID (the REQ-XXX part of tenant:project:REQ-XXX)
29
+ if (r.id?.endsWith(`:${identifier}`))
30
+ return r.id;
31
+ // Match by hashId
32
+ if (r.hashId === identifier)
33
+ return r.id;
34
+ }
35
+ if (page >= (data.meta?.totalPages ?? 1))
36
+ break;
37
+ page++;
38
+ }
39
+ // Nothing found — return as-is and let the API error
40
+ return identifier;
41
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "airgen-cli",
3
- "version": "0.1.1",
3
+ "version": "0.1.3",
4
4
  "description": "AIRGen CLI — requirements engineering from the command line",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",