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.
- package/dist/commands/implementation.js +297 -30
- package/dist/commands/reports.js +31 -4
- package/dist/commands/requirements.js +10 -6
- package/dist/resolve.d.ts +8 -0
- package/dist/resolve.js +41 -0
- package/package.json +1 -1
|
@@ -1,75 +1,342 @@
|
|
|
1
|
-
import {
|
|
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
|
|
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,
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
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
|
-
|
|
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
|
|
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,
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
const
|
|
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 ${
|
|
286
|
+
console.error(`Requirement ${requirement} not found.`);
|
|
38
287
|
process.exit(1);
|
|
39
288
|
}
|
|
40
|
-
const
|
|
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
|
-
|
|
49
|
-
attributes
|
|
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
|
|
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,
|
|
62
|
-
const
|
|
63
|
-
const
|
|
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 ${
|
|
325
|
+
console.error(`Requirement ${requirement} not found.`);
|
|
66
326
|
process.exit(1);
|
|
67
327
|
}
|
|
68
|
-
const
|
|
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
|
-
|
|
72
|
-
|
|
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
|
});
|
package/dist/commands/reports.js
CHANGED
|
@@ -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
|
-
|
|
82
|
+
// Compliance status
|
|
83
|
+
const compCounts = {};
|
|
72
84
|
for (const r of reqs) {
|
|
73
85
|
const status = r.complianceStatus || "Unset";
|
|
74
|
-
|
|
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(
|
|
101
|
+
output({ compliance: compCounts, implementation: implCounts });
|
|
78
102
|
}
|
|
79
103
|
else {
|
|
80
|
-
|
|
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}/${
|
|
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
|
|
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
|
|
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>;
|
package/dist/resolve.js
ADDED
|
@@ -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
|
+
}
|