airgen-cli 0.8.0 → 0.10.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.
|
@@ -362,6 +362,50 @@ export function registerDiagramCommands(program, client) {
|
|
|
362
362
|
await client.delete(`/architecture/diagrams/${tenant}/${project}/${id}`);
|
|
363
363
|
console.log("Diagram deleted.");
|
|
364
364
|
});
|
|
365
|
+
cmd
|
|
366
|
+
.command("deduplicate")
|
|
367
|
+
.description("Find and remove duplicate-named diagrams (keeps the one with more blocks)")
|
|
368
|
+
.argument("<tenant>", "Tenant slug")
|
|
369
|
+
.argument("<project>", "Project slug")
|
|
370
|
+
.option("--dry-run", "Show duplicates without deleting")
|
|
371
|
+
.action(async (tenant, project, opts) => {
|
|
372
|
+
const data = await client.get(`/architecture/diagrams/${tenant}/${project}`);
|
|
373
|
+
const diagrams = data.diagrams ?? [];
|
|
374
|
+
// Group by name
|
|
375
|
+
const byName = new Map();
|
|
376
|
+
for (const d of diagrams) {
|
|
377
|
+
const name = (d.name ?? "").toLowerCase().trim();
|
|
378
|
+
const group = byName.get(name) ?? [];
|
|
379
|
+
group.push(d);
|
|
380
|
+
byName.set(name, group);
|
|
381
|
+
}
|
|
382
|
+
const duplicates = [...byName.entries()].filter(([, group]) => group.length > 1);
|
|
383
|
+
if (duplicates.length === 0) {
|
|
384
|
+
console.log("No duplicate diagrams found.");
|
|
385
|
+
return;
|
|
386
|
+
}
|
|
387
|
+
let removed = 0;
|
|
388
|
+
for (const [name, group] of duplicates) {
|
|
389
|
+
// Keep the one with the most blocks, or the newest
|
|
390
|
+
const sorted = group.sort((a, b) => (b.blockCount ?? 0) - (a.blockCount ?? 0) || (b.id > a.id ? 1 : -1));
|
|
391
|
+
const keep = sorted[0];
|
|
392
|
+
const toRemove = sorted.slice(1);
|
|
393
|
+
console.log(`"${keep.name ?? name}": keeping ${keep.id} (${keep.blockCount ?? 0} blocks), removing ${toRemove.length} duplicate(s)`);
|
|
394
|
+
if (!opts.dryRun) {
|
|
395
|
+
for (const dup of toRemove) {
|
|
396
|
+
await client.delete(`/architecture/diagrams/${tenant}/${project}/${dup.id}`);
|
|
397
|
+
removed++;
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
else {
|
|
401
|
+
for (const dup of toRemove) {
|
|
402
|
+
console.log(` [dry-run] would remove ${dup.id} (${dup.blockCount ?? 0} blocks)`);
|
|
403
|
+
}
|
|
404
|
+
removed += toRemove.length;
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
console.log(`${opts.dryRun ? "Would remove" : "Removed"} ${removed} duplicate diagram(s).`);
|
|
408
|
+
});
|
|
365
409
|
// Blocks sub-group
|
|
366
410
|
const blocks = cmd.command("blocks").description("Manage blocks in diagrams");
|
|
367
411
|
blocks
|
|
@@ -111,6 +111,70 @@ export function registerDocumentCommands(program, client) {
|
|
|
111
111
|
output(data);
|
|
112
112
|
}
|
|
113
113
|
});
|
|
114
|
+
sections
|
|
115
|
+
.command("list")
|
|
116
|
+
.description("List sections in a document with requirement counts")
|
|
117
|
+
.argument("<tenant>", "Tenant slug")
|
|
118
|
+
.argument("<project>", "Project slug")
|
|
119
|
+
.argument("<document>", "Document slug")
|
|
120
|
+
.action(async (tenant, project, document) => {
|
|
121
|
+
const data = await client.get(`/sections/${tenant}/${project}/${document}/full`);
|
|
122
|
+
const secs = data.sections ?? [];
|
|
123
|
+
if (isJsonMode()) {
|
|
124
|
+
output(secs);
|
|
125
|
+
}
|
|
126
|
+
else {
|
|
127
|
+
if (secs.length === 0) {
|
|
128
|
+
console.log("No sections found.");
|
|
129
|
+
return;
|
|
130
|
+
}
|
|
131
|
+
printTable(["ID", "Name", "Code", "Order", "Reqs"], secs.map(s => [
|
|
132
|
+
s.id,
|
|
133
|
+
s.name,
|
|
134
|
+
s.shortCode ?? "",
|
|
135
|
+
String(s.order ?? 0),
|
|
136
|
+
String(s.requirements?.length ?? 0),
|
|
137
|
+
]));
|
|
138
|
+
}
|
|
139
|
+
});
|
|
140
|
+
sections
|
|
141
|
+
.command("merge")
|
|
142
|
+
.description("Move all requirements from source section to target, then delete source")
|
|
143
|
+
.argument("<tenant>", "Tenant slug")
|
|
144
|
+
.argument("<project>", "Project slug")
|
|
145
|
+
.argument("<from-section-id>", "Source section ID")
|
|
146
|
+
.argument("<to-section-id>", "Target section ID")
|
|
147
|
+
.option("--dry-run", "Show what would be moved without executing")
|
|
148
|
+
.action(async (tenant, project, fromId, toId, opts) => {
|
|
149
|
+
// Get requirements in source section
|
|
150
|
+
const secData = await client.get(`/sections/${fromId}/requirements`);
|
|
151
|
+
const reqs = secData.requirements ?? [];
|
|
152
|
+
if (reqs.length === 0) {
|
|
153
|
+
console.log("Source section has no requirements.");
|
|
154
|
+
if (!opts.dryRun) {
|
|
155
|
+
await client.delete(`/sections/${fromId}`, { tenant });
|
|
156
|
+
console.log("Empty section deleted.");
|
|
157
|
+
}
|
|
158
|
+
return;
|
|
159
|
+
}
|
|
160
|
+
console.log(`Moving ${reqs.length} requirement(s) from ${fromId} to ${toId}...`);
|
|
161
|
+
if (opts.dryRun) {
|
|
162
|
+
for (const r of reqs) {
|
|
163
|
+
console.log(` [dry-run] ${r.ref}: would move to ${toId}`);
|
|
164
|
+
}
|
|
165
|
+
console.log(`[dry-run] Would delete source section ${fromId}`);
|
|
166
|
+
return;
|
|
167
|
+
}
|
|
168
|
+
// Move each requirement to the target section
|
|
169
|
+
let moved = 0;
|
|
170
|
+
for (const r of reqs) {
|
|
171
|
+
await client.patch(`/requirements/${tenant}/${project}/${r.id}`, { sectionId: toId });
|
|
172
|
+
moved++;
|
|
173
|
+
}
|
|
174
|
+
// Delete the now-empty source section
|
|
175
|
+
await client.delete(`/sections/${fromId}`, { tenant });
|
|
176
|
+
console.log(`Merged ${moved} requirement(s) and deleted source section.`);
|
|
177
|
+
});
|
|
114
178
|
sections
|
|
115
179
|
.command("delete")
|
|
116
180
|
.description("Delete a section")
|
|
@@ -47,6 +47,113 @@ export function registerProjectCommands(program, client) {
|
|
|
47
47
|
output(data);
|
|
48
48
|
}
|
|
49
49
|
});
|
|
50
|
+
cmd
|
|
51
|
+
.command("scaffold")
|
|
52
|
+
.description("Create a project with standard SE document structure, sections, and linksets")
|
|
53
|
+
.argument("<tenant>", "Tenant slug")
|
|
54
|
+
.requiredOption("--name <name>", "System name (e.g. 'Autonomous Vehicle')")
|
|
55
|
+
.option("--key <key>", "Project key (auto-generated from name)")
|
|
56
|
+
.option("--template <tpl>", "Template: se (default), safety", "se")
|
|
57
|
+
.action(async (tenant, opts) => {
|
|
58
|
+
const slug = opts.key ?? `se-${opts.name.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/(^-|-$)/g, "")}`;
|
|
59
|
+
const seDocuments = [
|
|
60
|
+
{ name: "Stakeholder Needs", code: "STK-NEEDS", sections: [
|
|
61
|
+
{ name: "Operational Needs", code: "OPS" },
|
|
62
|
+
{ name: "Performance Needs", code: "PERF" },
|
|
63
|
+
{ name: "Constraints", code: "CON" },
|
|
64
|
+
] },
|
|
65
|
+
{ name: "System Requirements", code: "SYS-REQS", sections: [
|
|
66
|
+
{ name: "Functional Requirements", code: "FUNC" },
|
|
67
|
+
{ name: "Performance Requirements", code: "PERF" },
|
|
68
|
+
{ name: "Interface Requirements", code: "IFC" },
|
|
69
|
+
{ name: "Environmental Requirements", code: "ENV" },
|
|
70
|
+
] },
|
|
71
|
+
{ name: "Subsystem Requirements", code: "SUB-REQS", sections: [
|
|
72
|
+
{ name: "Subsystem Functional", code: "FUNC" },
|
|
73
|
+
{ name: "Subsystem Performance", code: "PERF" },
|
|
74
|
+
{ name: "Subsystem Interface", code: "IFC" },
|
|
75
|
+
] },
|
|
76
|
+
{ name: "Architecture Description", code: "ARC", sections: [
|
|
77
|
+
{ name: "System Architecture", code: "SYS-ARC" },
|
|
78
|
+
{ name: "Subsystem Allocation", code: "ALLOC" },
|
|
79
|
+
{ name: "Interface Definitions", code: "IFC-DEF" },
|
|
80
|
+
] },
|
|
81
|
+
{ name: "Verification Plan", code: "VER", sections: [
|
|
82
|
+
{ name: "Test Cases", code: "TEST" },
|
|
83
|
+
{ name: "Analysis Cases", code: "ANAL" },
|
|
84
|
+
{ name: "Inspection Cases", code: "INSP" },
|
|
85
|
+
] },
|
|
86
|
+
{ name: "Traceability Matrix", code: "TRC", sections: [
|
|
87
|
+
{ name: "Coverage Summary", code: "COV" },
|
|
88
|
+
] },
|
|
89
|
+
];
|
|
90
|
+
const seLinksets = [
|
|
91
|
+
{ source: "stakeholder-needs", target: "system-requirements" },
|
|
92
|
+
{ source: "system-requirements", target: "subsystem-requirements" },
|
|
93
|
+
{ source: "architecture-description", target: "subsystem-requirements" },
|
|
94
|
+
{ source: "system-requirements", target: "verification-plan" },
|
|
95
|
+
{ source: "subsystem-requirements", target: "verification-plan" },
|
|
96
|
+
];
|
|
97
|
+
const safetyExtras = [
|
|
98
|
+
{ name: "Hazard Analysis", code: "HAZ", sections: [
|
|
99
|
+
{ name: "Hazard Identification", code: "HZID" },
|
|
100
|
+
{ name: "Risk Assessment", code: "RISK" },
|
|
101
|
+
{ name: "Safety Requirements", code: "SAFE" },
|
|
102
|
+
] },
|
|
103
|
+
];
|
|
104
|
+
const documents = [...seDocuments];
|
|
105
|
+
const linksets = [...seLinksets];
|
|
106
|
+
if (opts.template === "safety") {
|
|
107
|
+
documents.push(...safetyExtras);
|
|
108
|
+
linksets.push({ source: "hazard-analysis", target: "system-requirements" }, { source: "hazard-analysis", target: "verification-plan" });
|
|
109
|
+
}
|
|
110
|
+
console.error(`Scaffolding project "${opts.name}" (${slug})...`);
|
|
111
|
+
// 1. Create project
|
|
112
|
+
console.error(" Creating project...");
|
|
113
|
+
await client.post(`/tenants/${tenant}/projects`, {
|
|
114
|
+
slug,
|
|
115
|
+
name: opts.name,
|
|
116
|
+
key: opts.key,
|
|
117
|
+
});
|
|
118
|
+
// 2. Create documents and sections
|
|
119
|
+
const docSlugs = new Map();
|
|
120
|
+
for (const doc of documents) {
|
|
121
|
+
console.error(` Creating document: ${doc.name}...`);
|
|
122
|
+
const docData = await client.post("/documents", {
|
|
123
|
+
tenant,
|
|
124
|
+
projectKey: slug,
|
|
125
|
+
name: doc.name,
|
|
126
|
+
shortCode: doc.code,
|
|
127
|
+
});
|
|
128
|
+
const docSlug = docData.document.slug;
|
|
129
|
+
docSlugs.set(doc.name.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/(^-|-$)/g, ""), docSlug);
|
|
130
|
+
for (let i = 0; i < doc.sections.length; i++) {
|
|
131
|
+
const sec = doc.sections[i];
|
|
132
|
+
await client.post("/sections", {
|
|
133
|
+
tenant,
|
|
134
|
+
projectKey: slug,
|
|
135
|
+
documentSlug: docSlug,
|
|
136
|
+
name: sec.name,
|
|
137
|
+
shortCode: sec.code,
|
|
138
|
+
order: i,
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
// 3. Create linksets
|
|
143
|
+
for (const ls of linksets) {
|
|
144
|
+
const srcSlug = docSlugs.get(ls.source);
|
|
145
|
+
const tgtSlug = docSlugs.get(ls.target);
|
|
146
|
+
if (srcSlug && tgtSlug) {
|
|
147
|
+
console.error(` Creating linkset: ${ls.source} → ${ls.target}...`);
|
|
148
|
+
await client.post(`/linksets/${tenant}/${slug}`, {
|
|
149
|
+
sourceDocumentSlug: srcSlug,
|
|
150
|
+
targetDocumentSlug: tgtSlug,
|
|
151
|
+
});
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
const totalSections = documents.reduce((sum, d) => sum + d.sections.length, 0);
|
|
155
|
+
console.log(`Project scaffolded: ${documents.length} documents, ${totalSections} sections, ${linksets.length} linksets.`);
|
|
156
|
+
});
|
|
50
157
|
cmd
|
|
51
158
|
.command("delete")
|
|
52
159
|
.description("Delete a project")
|
|
@@ -34,7 +34,7 @@ export function registerTraceabilityCommands(program, client) {
|
|
|
34
34
|
.argument("<project-key>", "Project key")
|
|
35
35
|
.requiredOption("--source <id>", "Source requirement ID")
|
|
36
36
|
.requiredOption("--target <id>", "Target requirement ID")
|
|
37
|
-
.requiredOption("--type <type>", "Link type: satisfies, derives, verifies, implements, refines, conflicts")
|
|
37
|
+
.requiredOption("--type <type>", "Link type: satisfies, derives, verifies, implements, refines, conflicts, motivates")
|
|
38
38
|
.option("--description <desc>", "Short description for traceability matrices")
|
|
39
39
|
.option("--rationale <text>", "Engineering justification for why this link exists")
|
|
40
40
|
.action(async (tenant, projectKey, opts) => {
|