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.
@@ -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,3 @@
1
+ import { Command } from "commander";
2
+ import type { AirgenClient } from "../client.js";
3
+ export declare function registerRequirementCommands(program: Command, client: AirgenClient): void;
@@ -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,3 @@
1
+ import { Command } from "commander";
2
+ import type { AirgenClient } from "../client.js";
3
+ export declare function registerTenantCommands(program: Command, client: AirgenClient): void;
@@ -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,3 @@
1
+ import { Command } from "commander";
2
+ import type { AirgenClient } from "../client.js";
3
+ export declare function registerTraceabilityCommands(program: Command, client: AirgenClient): void;
@@ -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
+ }
@@ -0,0 +1,9 @@
1
+ /**
2
+ * CLI configuration — loads from env vars or ~/.airgenrc JSON file.
3
+ */
4
+ export interface CliConfig {
5
+ apiUrl: string;
6
+ email: string;
7
+ password: string;
8
+ }
9
+ export declare function loadConfig(): CliConfig;
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
+ }
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};
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
+ });
@@ -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
+ }