airgen-cli 0.1.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.
- package/dist/client.d.ts +51 -0
- package/dist/client.js +263 -0
- package/dist/commands/activity.d.ts +3 -0
- package/dist/commands/activity.js +33 -0
- package/dist/commands/ai.d.ts +3 -0
- package/dist/commands/ai.js +86 -0
- package/dist/commands/baselines.d.ts +3 -0
- package/dist/commands/baselines.js +55 -0
- package/dist/commands/diagrams.d.ts +3 -0
- package/dist/commands/diagrams.js +196 -0
- package/dist/commands/documents.d.ts +3 -0
- package/dist/commands/documents.js +123 -0
- package/dist/commands/implementation.d.ts +3 -0
- package/dist/commands/implementation.js +76 -0
- package/dist/commands/import-export.d.ts +3 -0
- package/dist/commands/import-export.js +66 -0
- package/dist/commands/projects.d.ts +3 -0
- package/dist/commands/projects.js +56 -0
- package/dist/commands/quality.d.ts +3 -0
- package/dist/commands/quality.js +61 -0
- package/dist/commands/reports.d.ts +3 -0
- package/dist/commands/reports.js +114 -0
- package/dist/commands/requirements.d.ts +3 -0
- package/dist/commands/requirements.js +231 -0
- package/dist/commands/tenants.d.ts +3 -0
- package/dist/commands/tenants.js +17 -0
- package/dist/commands/traceability.d.ts +3 -0
- package/dist/commands/traceability.js +106 -0
- package/dist/config.d.ts +9 -0
- package/dist/config.js +30 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +75 -0
- package/dist/output.d.ts +13 -0
- package/dist/output.js +46 -0
- package/package.json +39 -0
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import { output, printTable, isJsonMode, truncate } from "../output.js";
|
|
2
|
+
export function registerReportCommands(program, client) {
|
|
3
|
+
const cmd = program.command("reports").alias("report").description("Project reports");
|
|
4
|
+
cmd
|
|
5
|
+
.command("stats")
|
|
6
|
+
.description("Project statistics overview")
|
|
7
|
+
.argument("<tenant>", "Tenant slug")
|
|
8
|
+
.argument("<project>", "Project slug")
|
|
9
|
+
.action(async (tenant, project) => {
|
|
10
|
+
const [reqs, links, docs, diagrams, baselines] = await Promise.all([
|
|
11
|
+
client.get(`/requirements/${tenant}/${project}`, { page: "1", limit: "1" }),
|
|
12
|
+
client.get(`/trace-links/${tenant}/${project}`),
|
|
13
|
+
client.get(`/documents/${tenant}/${project}`),
|
|
14
|
+
client.get(`/architecture/diagrams/${tenant}/${project}`),
|
|
15
|
+
client.get(`/baselines/${tenant}/${project}`),
|
|
16
|
+
]);
|
|
17
|
+
const stats = {
|
|
18
|
+
requirements: reqs.meta?.totalItems ?? 0,
|
|
19
|
+
traceLinks: (links.links ?? []).length,
|
|
20
|
+
documents: (docs.documents ?? []).length,
|
|
21
|
+
diagrams: (diagrams.diagrams ?? []).length,
|
|
22
|
+
baselines: (baselines.baselines ?? []).length,
|
|
23
|
+
};
|
|
24
|
+
if (isJsonMode()) {
|
|
25
|
+
output(stats);
|
|
26
|
+
}
|
|
27
|
+
else {
|
|
28
|
+
printTable(["Metric", "Count"], Object.entries(stats).map(([k, v]) => [k, String(v)]));
|
|
29
|
+
}
|
|
30
|
+
});
|
|
31
|
+
cmd
|
|
32
|
+
.command("quality")
|
|
33
|
+
.description("Quality score summary")
|
|
34
|
+
.argument("<tenant>", "Tenant slug")
|
|
35
|
+
.argument("<project>", "Project slug")
|
|
36
|
+
.option("-l, --limit <n>", "Max requirements to fetch", "100")
|
|
37
|
+
.action(async (tenant, project, opts) => {
|
|
38
|
+
const data = await client.get(`/requirements/${tenant}/${project}`, { page: "1", limit: opts.limit });
|
|
39
|
+
const reqs = data.data ?? [];
|
|
40
|
+
const scored = reqs.filter(r => r.qaScore != null);
|
|
41
|
+
const avg = scored.length > 0
|
|
42
|
+
? scored.reduce((sum, r) => sum + (r.qaScore ?? 0), 0) / scored.length
|
|
43
|
+
: 0;
|
|
44
|
+
const below50 = scored.filter(r => (r.qaScore ?? 0) < 50);
|
|
45
|
+
if (isJsonMode()) {
|
|
46
|
+
output({ total: reqs.length, scored: scored.length, averageScore: Math.round(avg), belowThreshold: below50.length });
|
|
47
|
+
}
|
|
48
|
+
else {
|
|
49
|
+
console.log(`Total requirements: ${reqs.length}`);
|
|
50
|
+
console.log(`Scored: ${scored.length}`);
|
|
51
|
+
console.log(`Average QA score: ${Math.round(avg)}/100`);
|
|
52
|
+
console.log(`Below 50: ${below50.length}`);
|
|
53
|
+
if (below50.length > 0) {
|
|
54
|
+
console.log("\nLowest scoring:");
|
|
55
|
+
printTable(["Ref", "Score", "Text"], below50
|
|
56
|
+
.sort((a, b) => (a.qaScore ?? 0) - (b.qaScore ?? 0))
|
|
57
|
+
.slice(0, 10)
|
|
58
|
+
.map(r => [r.ref ?? "?", String(r.qaScore ?? 0), truncate(r.text ?? "", 60)]));
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
});
|
|
62
|
+
cmd
|
|
63
|
+
.command("compliance")
|
|
64
|
+
.description("Compliance status summary")
|
|
65
|
+
.argument("<tenant>", "Tenant slug")
|
|
66
|
+
.argument("<project>", "Project slug")
|
|
67
|
+
.option("-l, --limit <n>", "Max requirements to fetch", "100")
|
|
68
|
+
.action(async (tenant, project, opts) => {
|
|
69
|
+
const data = await client.get(`/requirements/${tenant}/${project}`, { page: "1", limit: opts.limit });
|
|
70
|
+
const reqs = data.data ?? [];
|
|
71
|
+
const counts = {};
|
|
72
|
+
for (const r of reqs) {
|
|
73
|
+
const status = r.complianceStatus || "Unset";
|
|
74
|
+
counts[status] = (counts[status] ?? 0) + 1;
|
|
75
|
+
}
|
|
76
|
+
if (isJsonMode()) {
|
|
77
|
+
output(counts);
|
|
78
|
+
}
|
|
79
|
+
else {
|
|
80
|
+
printTable(["Status", "Count"], Object.entries(counts).map(([k, v]) => [k, String(v)]));
|
|
81
|
+
}
|
|
82
|
+
});
|
|
83
|
+
cmd
|
|
84
|
+
.command("orphans")
|
|
85
|
+
.description("Find requirements with no trace links")
|
|
86
|
+
.argument("<tenant>", "Tenant slug")
|
|
87
|
+
.argument("<project>", "Project slug")
|
|
88
|
+
.option("-l, --limit <n>", "Max requirements to fetch", "100")
|
|
89
|
+
.action(async (tenant, project, opts) => {
|
|
90
|
+
const [reqData, linkData] = await Promise.all([
|
|
91
|
+
client.get(`/requirements/${tenant}/${project}`, { page: "1", limit: opts.limit }),
|
|
92
|
+
client.get(`/trace-links/${tenant}/${project}`),
|
|
93
|
+
]);
|
|
94
|
+
const reqs = reqData.data ?? [];
|
|
95
|
+
const links = linkData.links ?? [];
|
|
96
|
+
const linked = new Set();
|
|
97
|
+
for (const l of links) {
|
|
98
|
+
if (l.sourceRequirementId)
|
|
99
|
+
linked.add(l.sourceRequirementId);
|
|
100
|
+
if (l.targetRequirementId)
|
|
101
|
+
linked.add(l.targetRequirementId);
|
|
102
|
+
}
|
|
103
|
+
const orphans = reqs.filter(r => r.id && !linked.has(r.id));
|
|
104
|
+
if (isJsonMode()) {
|
|
105
|
+
output(orphans);
|
|
106
|
+
}
|
|
107
|
+
else {
|
|
108
|
+
console.log(`Orphan requirements (no trace links): ${orphans.length}/${reqs.length}\n`);
|
|
109
|
+
if (orphans.length > 0) {
|
|
110
|
+
printTable(["Ref", "Text"], orphans.slice(0, 30).map(r => [r.ref ?? "?", truncate(r.text ?? "", 70)]));
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
});
|
|
114
|
+
}
|
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
import { output, printTable, isJsonMode, truncate } from "../output.js";
|
|
2
|
+
export function registerRequirementCommands(program, client) {
|
|
3
|
+
const cmd = program.command("requirements").alias("reqs").description("Manage requirements");
|
|
4
|
+
cmd
|
|
5
|
+
.command("list")
|
|
6
|
+
.description("List requirements with pagination")
|
|
7
|
+
.argument("<tenant>", "Tenant slug")
|
|
8
|
+
.argument("<project>", "Project slug")
|
|
9
|
+
.option("-p, --page <n>", "Page number", "1")
|
|
10
|
+
.option("-l, --limit <n>", "Items per page", "25")
|
|
11
|
+
.option("--sort <field>", "Sort by: ref, createdAt, qaScore")
|
|
12
|
+
.option("--order <dir>", "Sort order: asc, desc")
|
|
13
|
+
.action(async (tenant, project, opts) => {
|
|
14
|
+
const data = await client.get(`/requirements/${tenant}/${project}`, { page: opts.page, limit: opts.limit, sortBy: opts.sort, sortOrder: opts.order });
|
|
15
|
+
const reqs = data.data ?? [];
|
|
16
|
+
const meta = data.meta;
|
|
17
|
+
if (isJsonMode()) {
|
|
18
|
+
output(data);
|
|
19
|
+
}
|
|
20
|
+
else {
|
|
21
|
+
if (reqs.length === 0) {
|
|
22
|
+
console.log("No requirements found.");
|
|
23
|
+
return;
|
|
24
|
+
}
|
|
25
|
+
console.log(`Requirements (${meta?.totalItems ?? reqs.length} total, page ${meta?.currentPage ?? 1}/${meta?.totalPages ?? 1})\n`);
|
|
26
|
+
printTable(["Ref", "Text", "Pattern", "QA", "Tags"], reqs.map(r => [
|
|
27
|
+
r.ref ?? "?",
|
|
28
|
+
truncate(r.text ?? "", 60),
|
|
29
|
+
r.pattern ?? "",
|
|
30
|
+
r.qaScore != null ? String(r.qaScore) : "-",
|
|
31
|
+
(r.tags ?? []).join(", "),
|
|
32
|
+
]));
|
|
33
|
+
}
|
|
34
|
+
});
|
|
35
|
+
cmd
|
|
36
|
+
.command("get")
|
|
37
|
+
.description("Get full details of a requirement")
|
|
38
|
+
.argument("<tenant>", "Tenant slug")
|
|
39
|
+
.argument("<project>", "Project slug")
|
|
40
|
+
.argument("<ref>", "Requirement reference (e.g. REQ-001)")
|
|
41
|
+
.action(async (tenant, project, ref) => {
|
|
42
|
+
const data = await client.get(`/requirements/${tenant}/${project}/${ref}`);
|
|
43
|
+
const r = data.record ?? data;
|
|
44
|
+
if (isJsonMode()) {
|
|
45
|
+
output(r);
|
|
46
|
+
}
|
|
47
|
+
else {
|
|
48
|
+
const lines = [
|
|
49
|
+
`${r.ref ?? ref}${r.qaScore != null ? ` (QA: ${r.qaScore}/100)` : ""}`,
|
|
50
|
+
"",
|
|
51
|
+
r.title ? `Title: ${r.title}` : null,
|
|
52
|
+
r.text ? `Text: ${r.text}` : null,
|
|
53
|
+
r.pattern ? `Pattern: ${r.pattern}` : null,
|
|
54
|
+
r.verification ? `Verification: ${r.verification}` : null,
|
|
55
|
+
r.rationale ? `Rationale: ${r.rationale}` : null,
|
|
56
|
+
r.complianceStatus ? `Compliance: ${r.complianceStatus}` : null,
|
|
57
|
+
r.documentSlug ? `Document: ${r.documentSlug}${r.sectionId ? ` / ${r.sectionId}` : ""}` : null,
|
|
58
|
+
r.tags?.length ? `Tags: ${r.tags.join(", ")}` : null,
|
|
59
|
+
r.id ? `ID: ${r.id}` : null,
|
|
60
|
+
].filter(Boolean);
|
|
61
|
+
console.log(lines.join("\n"));
|
|
62
|
+
}
|
|
63
|
+
});
|
|
64
|
+
cmd
|
|
65
|
+
.command("create")
|
|
66
|
+
.description("Create a new requirement")
|
|
67
|
+
.argument("<tenant>", "Tenant slug")
|
|
68
|
+
.argument("<project-key>", "Project key")
|
|
69
|
+
.requiredOption("--text <text>", "Requirement text")
|
|
70
|
+
.option("--pattern <p>", "Pattern: ubiquitous, event, state, unwanted, optional")
|
|
71
|
+
.option("--verification <v>", "Verification: Test, Analysis, Inspection, Demonstration")
|
|
72
|
+
.option("--document <slug>", "Document slug")
|
|
73
|
+
.option("--section <id>", "Section ID")
|
|
74
|
+
.option("--tags <tags>", "Comma-separated tags")
|
|
75
|
+
.action(async (tenant, projectKey, opts) => {
|
|
76
|
+
const data = await client.post("/requirements", {
|
|
77
|
+
tenant,
|
|
78
|
+
projectKey,
|
|
79
|
+
text: opts.text,
|
|
80
|
+
pattern: opts.pattern,
|
|
81
|
+
verification: opts.verification,
|
|
82
|
+
documentSlug: opts.document,
|
|
83
|
+
sectionId: opts.section,
|
|
84
|
+
tags: opts.tags?.split(",").map(t => t.trim()),
|
|
85
|
+
});
|
|
86
|
+
if (isJsonMode()) {
|
|
87
|
+
output(data);
|
|
88
|
+
}
|
|
89
|
+
else {
|
|
90
|
+
console.log("Requirement created.");
|
|
91
|
+
output(data);
|
|
92
|
+
}
|
|
93
|
+
});
|
|
94
|
+
cmd
|
|
95
|
+
.command("update")
|
|
96
|
+
.description("Update a requirement")
|
|
97
|
+
.argument("<tenant>", "Tenant slug")
|
|
98
|
+
.argument("<project>", "Project slug")
|
|
99
|
+
.argument("<id>", "Requirement ID")
|
|
100
|
+
.option("--text <text>", "Requirement text")
|
|
101
|
+
.option("--pattern <p>", "Pattern")
|
|
102
|
+
.option("--verification <v>", "Verification method")
|
|
103
|
+
.option("--rationale <r>", "Rationale")
|
|
104
|
+
.option("--compliance <status>", "Compliance status")
|
|
105
|
+
.option("--section <id>", "Section ID")
|
|
106
|
+
.option("--tags <tags>", "Comma-separated tags")
|
|
107
|
+
.action(async (tenant, project, id, opts) => {
|
|
108
|
+
const body = {};
|
|
109
|
+
if (opts.text)
|
|
110
|
+
body.text = opts.text;
|
|
111
|
+
if (opts.pattern)
|
|
112
|
+
body.pattern = opts.pattern;
|
|
113
|
+
if (opts.verification)
|
|
114
|
+
body.verification = opts.verification;
|
|
115
|
+
if (opts.rationale)
|
|
116
|
+
body.rationale = opts.rationale;
|
|
117
|
+
if (opts.compliance)
|
|
118
|
+
body.complianceStatus = opts.compliance;
|
|
119
|
+
if (opts.section)
|
|
120
|
+
body.sectionId = opts.section;
|
|
121
|
+
if (opts.tags)
|
|
122
|
+
body.tags = opts.tags.split(",").map(t => t.trim());
|
|
123
|
+
await client.patch(`/requirements/${tenant}/${project}/${id}`, body);
|
|
124
|
+
if (isJsonMode()) {
|
|
125
|
+
output({ ok: true });
|
|
126
|
+
}
|
|
127
|
+
else {
|
|
128
|
+
console.log("Requirement updated.");
|
|
129
|
+
}
|
|
130
|
+
});
|
|
131
|
+
cmd
|
|
132
|
+
.command("delete")
|
|
133
|
+
.description("Soft-delete a requirement")
|
|
134
|
+
.argument("<tenant>", "Tenant slug")
|
|
135
|
+
.argument("<project>", "Project slug")
|
|
136
|
+
.argument("<id>", "Requirement ID")
|
|
137
|
+
.action(async (tenant, project, id) => {
|
|
138
|
+
await client.delete(`/requirements/${tenant}/${project}/${id}`);
|
|
139
|
+
console.log("Requirement deleted.");
|
|
140
|
+
});
|
|
141
|
+
cmd
|
|
142
|
+
.command("history")
|
|
143
|
+
.description("Get version history of a requirement")
|
|
144
|
+
.argument("<tenant>", "Tenant slug")
|
|
145
|
+
.argument("<project>", "Project slug")
|
|
146
|
+
.argument("<id>", "Requirement ID")
|
|
147
|
+
.action(async (tenant, project, id) => {
|
|
148
|
+
const data = await client.get(`/requirements/${tenant}/${project}/${id}/history`);
|
|
149
|
+
output(data);
|
|
150
|
+
});
|
|
151
|
+
cmd
|
|
152
|
+
.command("search")
|
|
153
|
+
.description("Search requirements (semantic, similar, duplicates)")
|
|
154
|
+
.argument("<tenant>", "Tenant slug")
|
|
155
|
+
.argument("<project>", "Project slug")
|
|
156
|
+
.requiredOption("-q, --query <text>", "Search query")
|
|
157
|
+
.option("--mode <m>", "Mode: semantic, similar, duplicates", "semantic")
|
|
158
|
+
.option("-l, --limit <n>", "Max results")
|
|
159
|
+
.option("--min-similarity <n>", "Minimum similarity score")
|
|
160
|
+
.action(async (tenant, project, opts) => {
|
|
161
|
+
if (opts.mode === "semantic") {
|
|
162
|
+
const data = await client.post("/requirements/search/semantic", {
|
|
163
|
+
tenant,
|
|
164
|
+
project,
|
|
165
|
+
query: opts.query,
|
|
166
|
+
limit: opts.limit ? parseInt(opts.limit, 10) : undefined,
|
|
167
|
+
minSimilarity: opts.minSimilarity ? parseFloat(opts.minSimilarity) : undefined,
|
|
168
|
+
});
|
|
169
|
+
output(data);
|
|
170
|
+
}
|
|
171
|
+
else {
|
|
172
|
+
// similar/duplicates modes need a requirementId
|
|
173
|
+
console.error("Similar/duplicates modes require --requirement-id (not yet implemented in CLI).");
|
|
174
|
+
}
|
|
175
|
+
});
|
|
176
|
+
cmd
|
|
177
|
+
.command("filter")
|
|
178
|
+
.description("Advanced filtering with multiple criteria")
|
|
179
|
+
.argument("<tenant>", "Tenant slug")
|
|
180
|
+
.argument("<project>", "Project slug")
|
|
181
|
+
.option("--tags <tags>", "Comma-separated tags")
|
|
182
|
+
.option("--pattern <p>", "Pattern filter")
|
|
183
|
+
.option("--verification <v>", "Verification filter")
|
|
184
|
+
.option("--compliance <status>", "Compliance status filter")
|
|
185
|
+
.option("--qa-min <n>", "Min QA score")
|
|
186
|
+
.option("--qa-max <n>", "Max QA score")
|
|
187
|
+
.option("--document <slug>", "Document slug")
|
|
188
|
+
.option("--section <id>", "Section ID")
|
|
189
|
+
.option("--contains <text>", "Text contains")
|
|
190
|
+
.option("-p, --page <n>", "Page number", "1")
|
|
191
|
+
.option("-l, --limit <n>", "Items per page", "50")
|
|
192
|
+
.action(async (tenant, project, opts) => {
|
|
193
|
+
// Fetch requirements and filter client-side (matching MCP approach)
|
|
194
|
+
const data = await client.get(`/requirements/${tenant}/${project}`, { page: opts.page, limit: opts.limit });
|
|
195
|
+
let reqs = data.data ?? [];
|
|
196
|
+
if (opts.tags) {
|
|
197
|
+
const filterTags = opts.tags.split(",").map(t => t.trim().toLowerCase());
|
|
198
|
+
reqs = reqs.filter(r => r.tags?.some(t => filterTags.includes(t.toLowerCase())));
|
|
199
|
+
}
|
|
200
|
+
if (opts.pattern)
|
|
201
|
+
reqs = reqs.filter(r => r.pattern === opts.pattern);
|
|
202
|
+
if (opts.verification)
|
|
203
|
+
reqs = reqs.filter(r => r.verification === opts.verification);
|
|
204
|
+
if (opts.compliance)
|
|
205
|
+
reqs = reqs.filter(r => r.complianceStatus === opts.compliance);
|
|
206
|
+
if (opts.qaMin)
|
|
207
|
+
reqs = reqs.filter(r => (r.qaScore ?? 0) >= parseInt(opts.qaMin, 10));
|
|
208
|
+
if (opts.qaMax)
|
|
209
|
+
reqs = reqs.filter(r => (r.qaScore ?? 0) <= parseInt(opts.qaMax, 10));
|
|
210
|
+
if (opts.document)
|
|
211
|
+
reqs = reqs.filter(r => r.documentSlug === opts.document);
|
|
212
|
+
if (opts.section)
|
|
213
|
+
reqs = reqs.filter(r => r.sectionId === opts.section);
|
|
214
|
+
if (opts.contains) {
|
|
215
|
+
const term = opts.contains.toLowerCase();
|
|
216
|
+
reqs = reqs.filter(r => r.text?.toLowerCase().includes(term));
|
|
217
|
+
}
|
|
218
|
+
if (isJsonMode()) {
|
|
219
|
+
output(reqs);
|
|
220
|
+
}
|
|
221
|
+
else {
|
|
222
|
+
console.log(`Filtered: ${reqs.length} requirements\n`);
|
|
223
|
+
printTable(["Ref", "Text", "Pattern", "QA"], reqs.map(r => [
|
|
224
|
+
r.ref ?? "?",
|
|
225
|
+
truncate(r.text ?? "", 60),
|
|
226
|
+
r.pattern ?? "",
|
|
227
|
+
r.qaScore != null ? String(r.qaScore) : "-",
|
|
228
|
+
]));
|
|
229
|
+
}
|
|
230
|
+
});
|
|
231
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { output, printTable, isJsonMode } from "../output.js";
|
|
2
|
+
export function registerTenantCommands(program, client) {
|
|
3
|
+
const cmd = program.command("tenants").description("Manage tenants");
|
|
4
|
+
cmd
|
|
5
|
+
.command("list")
|
|
6
|
+
.description("List all tenants you have access to")
|
|
7
|
+
.action(async () => {
|
|
8
|
+
const data = await client.get("/tenants");
|
|
9
|
+
const tenants = data.tenants ?? [];
|
|
10
|
+
if (isJsonMode()) {
|
|
11
|
+
output(tenants);
|
|
12
|
+
}
|
|
13
|
+
else {
|
|
14
|
+
printTable(["Slug", "Name", "Created"], tenants.map(t => [t.slug, t.name ?? t.slug, t.createdAt ?? ""]));
|
|
15
|
+
}
|
|
16
|
+
});
|
|
17
|
+
}
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import { output, printTable, isJsonMode } from "../output.js";
|
|
2
|
+
export function registerTraceabilityCommands(program, client) {
|
|
3
|
+
const cmd = program.command("traces").alias("trace").description("Traceability links");
|
|
4
|
+
cmd
|
|
5
|
+
.command("list")
|
|
6
|
+
.description("List trace links in a project")
|
|
7
|
+
.argument("<tenant>", "Tenant slug")
|
|
8
|
+
.argument("<project>", "Project slug")
|
|
9
|
+
.option("--requirement <id>", "Filter by requirement ID")
|
|
10
|
+
.action(async (tenant, project, opts) => {
|
|
11
|
+
const path = opts.requirement
|
|
12
|
+
? `/trace-links/${tenant}/${project}/${opts.requirement}`
|
|
13
|
+
: `/trace-links/${tenant}/${project}`;
|
|
14
|
+
const data = await client.get(path);
|
|
15
|
+
const links = data.links ?? [];
|
|
16
|
+
if (isJsonMode()) {
|
|
17
|
+
output(links);
|
|
18
|
+
}
|
|
19
|
+
else {
|
|
20
|
+
printTable(["ID", "Source", "Target", "Type", "Description"], links.map(l => [
|
|
21
|
+
l.id,
|
|
22
|
+
l.sourceRef ?? l.sourceRequirementId ?? "",
|
|
23
|
+
l.targetRef ?? l.targetRequirementId ?? "",
|
|
24
|
+
l.linkType ?? "",
|
|
25
|
+
l.description ?? "",
|
|
26
|
+
]));
|
|
27
|
+
}
|
|
28
|
+
});
|
|
29
|
+
cmd
|
|
30
|
+
.command("create")
|
|
31
|
+
.description("Create a trace link between requirements")
|
|
32
|
+
.argument("<tenant>", "Tenant slug")
|
|
33
|
+
.argument("<project-key>", "Project key")
|
|
34
|
+
.requiredOption("--source <id>", "Source requirement ID")
|
|
35
|
+
.requiredOption("--target <id>", "Target requirement ID")
|
|
36
|
+
.requiredOption("--type <type>", "Link type: satisfies, derives, verifies, implements, refines, conflicts")
|
|
37
|
+
.option("--description <desc>", "Description")
|
|
38
|
+
.action(async (tenant, projectKey, opts) => {
|
|
39
|
+
const data = await client.post("/trace-links", {
|
|
40
|
+
tenant,
|
|
41
|
+
projectKey,
|
|
42
|
+
sourceRequirementId: opts.source,
|
|
43
|
+
targetRequirementId: opts.target,
|
|
44
|
+
linkType: opts.type,
|
|
45
|
+
description: opts.description,
|
|
46
|
+
});
|
|
47
|
+
if (isJsonMode()) {
|
|
48
|
+
output(data);
|
|
49
|
+
}
|
|
50
|
+
else {
|
|
51
|
+
console.log("Trace link created.");
|
|
52
|
+
}
|
|
53
|
+
});
|
|
54
|
+
cmd
|
|
55
|
+
.command("delete")
|
|
56
|
+
.description("Delete a trace link")
|
|
57
|
+
.argument("<tenant>", "Tenant slug")
|
|
58
|
+
.argument("<project>", "Project slug")
|
|
59
|
+
.argument("<link-id>", "Link ID")
|
|
60
|
+
.action(async (tenant, project, linkId) => {
|
|
61
|
+
await client.delete(`/trace-links/${tenant}/${project}/${linkId}`);
|
|
62
|
+
console.log("Trace link deleted.");
|
|
63
|
+
});
|
|
64
|
+
// Linksets
|
|
65
|
+
const linksets = cmd.command("linksets").description("Document linksets");
|
|
66
|
+
linksets
|
|
67
|
+
.command("list")
|
|
68
|
+
.description("List document linksets")
|
|
69
|
+
.argument("<tenant>", "Tenant slug")
|
|
70
|
+
.argument("<project>", "Project slug")
|
|
71
|
+
.action(async (tenant, project) => {
|
|
72
|
+
const data = await client.get(`/linksets/${tenant}/${project}`);
|
|
73
|
+
output(data);
|
|
74
|
+
});
|
|
75
|
+
linksets
|
|
76
|
+
.command("create")
|
|
77
|
+
.description("Create a document linkset")
|
|
78
|
+
.argument("<tenant>", "Tenant slug")
|
|
79
|
+
.argument("<project>", "Project slug")
|
|
80
|
+
.requiredOption("--source <slug>", "Source document slug")
|
|
81
|
+
.requiredOption("--target <slug>", "Target document slug")
|
|
82
|
+
.option("--link-type <type>", "Default link type")
|
|
83
|
+
.action(async (tenant, project, opts) => {
|
|
84
|
+
const data = await client.post(`/linksets/${tenant}/${project}`, {
|
|
85
|
+
sourceDocumentSlug: opts.source,
|
|
86
|
+
targetDocumentSlug: opts.target,
|
|
87
|
+
defaultLinkType: opts.linkType,
|
|
88
|
+
});
|
|
89
|
+
if (isJsonMode()) {
|
|
90
|
+
output(data);
|
|
91
|
+
}
|
|
92
|
+
else {
|
|
93
|
+
console.log("Linkset created.");
|
|
94
|
+
}
|
|
95
|
+
});
|
|
96
|
+
linksets
|
|
97
|
+
.command("delete")
|
|
98
|
+
.description("Delete a linkset")
|
|
99
|
+
.argument("<tenant>", "Tenant slug")
|
|
100
|
+
.argument("<project>", "Project slug")
|
|
101
|
+
.argument("<linkset-id>", "Linkset ID")
|
|
102
|
+
.action(async (tenant, project, linksetId) => {
|
|
103
|
+
await client.delete(`/linksets/${tenant}/${project}/${linksetId}`);
|
|
104
|
+
console.log("Linkset deleted.");
|
|
105
|
+
});
|
|
106
|
+
}
|
package/dist/config.d.ts
ADDED
package/dist/config.js
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CLI configuration — loads from env vars or ~/.airgenrc JSON file.
|
|
3
|
+
*/
|
|
4
|
+
import { readFileSync } from "node:fs";
|
|
5
|
+
import { join } from "node:path";
|
|
6
|
+
import { homedir } from "node:os";
|
|
7
|
+
function loadRcFile() {
|
|
8
|
+
try {
|
|
9
|
+
const raw = readFileSync(join(homedir(), ".airgenrc"), "utf-8");
|
|
10
|
+
return JSON.parse(raw);
|
|
11
|
+
}
|
|
12
|
+
catch {
|
|
13
|
+
return {};
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
export function loadConfig() {
|
|
17
|
+
const rc = loadRcFile();
|
|
18
|
+
const apiUrl = process.env.AIRGEN_API_URL ?? rc.apiUrl;
|
|
19
|
+
const email = process.env.AIRGEN_EMAIL ?? rc.email;
|
|
20
|
+
const password = process.env.AIRGEN_PASSWORD ?? rc.password;
|
|
21
|
+
if (!apiUrl) {
|
|
22
|
+
console.error("Missing AIRGEN_API_URL. Set it via environment variable or ~/.airgenrc");
|
|
23
|
+
process.exit(1);
|
|
24
|
+
}
|
|
25
|
+
if (!email || !password) {
|
|
26
|
+
console.error("Missing AIRGEN_EMAIL / AIRGEN_PASSWORD. Set via environment or ~/.airgenrc");
|
|
27
|
+
process.exit(1);
|
|
28
|
+
}
|
|
29
|
+
return { apiUrl, email, password };
|
|
30
|
+
}
|
package/dist/index.d.ts
ADDED
package/dist/index.js
ADDED
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { Command } from "commander";
|
|
3
|
+
import { AirgenClient } from "./client.js";
|
|
4
|
+
import { loadConfig } from "./config.js";
|
|
5
|
+
import { setJsonMode } from "./output.js";
|
|
6
|
+
import { registerTenantCommands } from "./commands/tenants.js";
|
|
7
|
+
import { registerProjectCommands } from "./commands/projects.js";
|
|
8
|
+
import { registerRequirementCommands } from "./commands/requirements.js";
|
|
9
|
+
import { registerDocumentCommands } from "./commands/documents.js";
|
|
10
|
+
import { registerDiagramCommands } from "./commands/diagrams.js";
|
|
11
|
+
import { registerTraceabilityCommands } from "./commands/traceability.js";
|
|
12
|
+
import { registerBaselineCommands } from "./commands/baselines.js";
|
|
13
|
+
import { registerQualityCommands } from "./commands/quality.js";
|
|
14
|
+
import { registerAiCommands } from "./commands/ai.js";
|
|
15
|
+
import { registerReportCommands } from "./commands/reports.js";
|
|
16
|
+
import { registerImportExportCommands } from "./commands/import-export.js";
|
|
17
|
+
import { registerActivityCommands } from "./commands/activity.js";
|
|
18
|
+
import { registerImplementationCommands } from "./commands/implementation.js";
|
|
19
|
+
const program = new Command();
|
|
20
|
+
// Lazy-init: only create client when a command actually runs
|
|
21
|
+
let client = null;
|
|
22
|
+
function getClient() {
|
|
23
|
+
if (!client) {
|
|
24
|
+
const config = loadConfig();
|
|
25
|
+
client = new AirgenClient(config);
|
|
26
|
+
}
|
|
27
|
+
return client;
|
|
28
|
+
}
|
|
29
|
+
// Proxy that defers client creation — commands receive this and call methods lazily
|
|
30
|
+
const clientProxy = new Proxy({}, {
|
|
31
|
+
get(_target, prop) {
|
|
32
|
+
const c = getClient();
|
|
33
|
+
const val = c[prop];
|
|
34
|
+
if (typeof val === "function")
|
|
35
|
+
return val.bind(c);
|
|
36
|
+
return val;
|
|
37
|
+
},
|
|
38
|
+
});
|
|
39
|
+
program
|
|
40
|
+
.name("airgen")
|
|
41
|
+
.description("AIRGen CLI — requirements engineering from the command line")
|
|
42
|
+
.version("0.1.0")
|
|
43
|
+
.option("--json", "Output as JSON")
|
|
44
|
+
.hook("preAction", (_thisCommand, actionCommand) => {
|
|
45
|
+
let cmd = actionCommand;
|
|
46
|
+
while (cmd) {
|
|
47
|
+
if (cmd.opts().json) {
|
|
48
|
+
setJsonMode(true);
|
|
49
|
+
break;
|
|
50
|
+
}
|
|
51
|
+
cmd = cmd.parent;
|
|
52
|
+
}
|
|
53
|
+
});
|
|
54
|
+
registerTenantCommands(program, clientProxy);
|
|
55
|
+
registerProjectCommands(program, clientProxy);
|
|
56
|
+
registerRequirementCommands(program, clientProxy);
|
|
57
|
+
registerDocumentCommands(program, clientProxy);
|
|
58
|
+
registerDiagramCommands(program, clientProxy);
|
|
59
|
+
registerTraceabilityCommands(program, clientProxy);
|
|
60
|
+
registerBaselineCommands(program, clientProxy);
|
|
61
|
+
registerQualityCommands(program, clientProxy);
|
|
62
|
+
registerAiCommands(program, clientProxy);
|
|
63
|
+
registerReportCommands(program, clientProxy);
|
|
64
|
+
registerImportExportCommands(program, clientProxy);
|
|
65
|
+
registerActivityCommands(program, clientProxy);
|
|
66
|
+
registerImplementationCommands(program, clientProxy);
|
|
67
|
+
// Handle async errors from Commander action handlers
|
|
68
|
+
process.on("uncaughtException", (err) => {
|
|
69
|
+
console.error(`Error: ${err.message}`);
|
|
70
|
+
process.exit(1);
|
|
71
|
+
});
|
|
72
|
+
program.parseAsync(process.argv).catch((err) => {
|
|
73
|
+
console.error(`Error: ${err.message}`);
|
|
74
|
+
process.exit(1);
|
|
75
|
+
});
|
package/dist/output.d.ts
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Output formatting for the CLI.
|
|
3
|
+
*/
|
|
4
|
+
export declare function setJsonMode(enabled: boolean): void;
|
|
5
|
+
export declare function isJsonMode(): boolean;
|
|
6
|
+
/** Print data — as JSON if --json flag, otherwise as formatted text. */
|
|
7
|
+
export declare function output(data: unknown, formatted?: string): void;
|
|
8
|
+
/** Print a simple table to the terminal. */
|
|
9
|
+
export declare function printTable(headers: string[], rows: string[][]): void;
|
|
10
|
+
/** Print an error and exit. */
|
|
11
|
+
export declare function die(msg: string): never;
|
|
12
|
+
/** Truncate a string. */
|
|
13
|
+
export declare function truncate(text: string, max: number): string;
|
package/dist/output.js
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Output formatting for the CLI.
|
|
3
|
+
*/
|
|
4
|
+
let jsonMode = false;
|
|
5
|
+
export function setJsonMode(enabled) {
|
|
6
|
+
jsonMode = enabled;
|
|
7
|
+
}
|
|
8
|
+
export function isJsonMode() {
|
|
9
|
+
return jsonMode;
|
|
10
|
+
}
|
|
11
|
+
/** Print data — as JSON if --json flag, otherwise as formatted text. */
|
|
12
|
+
export function output(data, formatted) {
|
|
13
|
+
if (jsonMode) {
|
|
14
|
+
console.log(JSON.stringify(data, null, 2));
|
|
15
|
+
}
|
|
16
|
+
else {
|
|
17
|
+
console.log(formatted ?? JSON.stringify(data, null, 2));
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
/** Print a simple table to the terminal. */
|
|
21
|
+
export function printTable(headers, rows) {
|
|
22
|
+
if (rows.length === 0) {
|
|
23
|
+
console.log("No results.");
|
|
24
|
+
return;
|
|
25
|
+
}
|
|
26
|
+
// Calculate column widths
|
|
27
|
+
const widths = headers.map((h, i) => Math.max(h.length, ...rows.map(r => (r[i] ?? "").length)));
|
|
28
|
+
const pad = (s, w) => s + " ".repeat(Math.max(0, w - s.length));
|
|
29
|
+
const sep = widths.map(w => "─".repeat(w)).join("──");
|
|
30
|
+
console.log(widths.map((w, i) => pad(headers[i], w)).join(" "));
|
|
31
|
+
console.log(sep);
|
|
32
|
+
for (const row of rows) {
|
|
33
|
+
console.log(widths.map((w, i) => pad(row[i] ?? "", w)).join(" "));
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
/** Print an error and exit. */
|
|
37
|
+
export function die(msg) {
|
|
38
|
+
console.error(`Error: ${msg}`);
|
|
39
|
+
process.exit(1);
|
|
40
|
+
}
|
|
41
|
+
/** Truncate a string. */
|
|
42
|
+
export function truncate(text, max) {
|
|
43
|
+
if (text.length <= max)
|
|
44
|
+
return text;
|
|
45
|
+
return text.slice(0, max - 3) + "...";
|
|
46
|
+
}
|