airgen-cli 0.17.1 → 0.18.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/commands/diagrams.js +188 -13
- package/dist/commands/projects.js +1 -1
- package/package.json +1 -1
|
@@ -18,15 +18,14 @@ function sanitizeId(id) {
|
|
|
18
18
|
}
|
|
19
19
|
function mermaidNodeShape(block) {
|
|
20
20
|
const id = sanitizeId(block.id);
|
|
21
|
-
const label = block.name.replace(/"/g, "'");
|
|
22
|
-
const stereo = block.stereotype?.replace(/[«»<>]/g, "") ?? block.kind ?? "
|
|
23
|
-
|
|
21
|
+
const label = block.name.replace(/"/g, "'").replace(/[<>]/g, "");
|
|
22
|
+
const stereo = block.stereotype?.replace(/[«»<>]/g, "") ?? block.kind ?? "";
|
|
23
|
+
// Use Mermaid-safe label: stereotype on separate line via <br>, no << >> delimiters
|
|
24
|
+
const display = stereo ? `"${stereo}<br>${label}"` : `"${label}"`;
|
|
25
|
+
// Use only universally supported shapes: [] for most, {} for interface
|
|
24
26
|
switch (block.kind) {
|
|
25
|
-
case "
|
|
26
|
-
case "
|
|
27
|
-
case "actor": return `${id}([${display}])`;
|
|
28
|
-
case "external": return `${id}[${display}]`;
|
|
29
|
-
case "interface": return `${id}{{${display}}}`;
|
|
27
|
+
case "actor": return `${id}[${display}]`;
|
|
28
|
+
case "interface": return `${id}[${display}]`;
|
|
30
29
|
default: return `${id}[${display}]`;
|
|
31
30
|
}
|
|
32
31
|
}
|
|
@@ -177,17 +176,20 @@ function renderTerminal(blocks, connectors) {
|
|
|
177
176
|
function renderMermaid(blocks, connectors, direction) {
|
|
178
177
|
resetMermaidIds();
|
|
179
178
|
const lines = [`flowchart ${direction}`];
|
|
179
|
+
const blockIds = new Set(blocks.map(b => b.id));
|
|
180
180
|
// Nodes
|
|
181
181
|
for (const b of blocks) {
|
|
182
182
|
lines.push(` ${mermaidNodeShape(b)}`);
|
|
183
183
|
}
|
|
184
|
-
// Edges
|
|
184
|
+
// Edges (skip orphan connectors whose source or target block is missing)
|
|
185
185
|
for (const c of connectors) {
|
|
186
|
+
if (!blockIds.has(c.source) || !blockIds.has(c.target))
|
|
187
|
+
continue;
|
|
186
188
|
const src = sanitizeId(c.source);
|
|
187
189
|
const tgt = sanitizeId(c.target);
|
|
188
190
|
const arrow = mermaidArrow(c.kind);
|
|
189
191
|
if (c.label) {
|
|
190
|
-
const edgeLabel = c.label.replace(/"/g, "'");
|
|
192
|
+
const edgeLabel = c.label.replace(/"/g, "'").replace(/[<>|]/g, "");
|
|
191
193
|
lines.push(` ${src} ${arrow}|${edgeLabel}| ${tgt}`);
|
|
192
194
|
}
|
|
193
195
|
else {
|
|
@@ -289,8 +291,14 @@ export function registerDiagramCommands(program, client) {
|
|
|
289
291
|
if (opts.clean) {
|
|
290
292
|
rendered = rendered
|
|
291
293
|
.split("\n")
|
|
292
|
-
.filter(l =>
|
|
293
|
-
|
|
294
|
+
.filter(l => {
|
|
295
|
+
const t = l.trim();
|
|
296
|
+
return !t.startsWith("classDef ") && !t.startsWith("class ") && !t.startsWith("style ") && t !== "";
|
|
297
|
+
})
|
|
298
|
+
.map(l => l
|
|
299
|
+
.replace(/<<[^>]+>><br\/?>/g, "") // Strip <<stereotype>><br> or <<stereotype>><br/>
|
|
300
|
+
.replace(/[a-z]+<br>/gi, (m) => m) // Keep stereo<br>label (valid Mermaid)
|
|
301
|
+
)
|
|
294
302
|
.join("\n");
|
|
295
303
|
}
|
|
296
304
|
if (isJsonMode()) {
|
|
@@ -457,7 +465,7 @@ export function registerDiagramCommands(program, client) {
|
|
|
457
465
|
});
|
|
458
466
|
blocks
|
|
459
467
|
.command("create")
|
|
460
|
-
.description("Create a new block")
|
|
468
|
+
.description("Create a new block (use --upsert to return existing if name matches)")
|
|
461
469
|
.argument("<tenant>", "Tenant slug")
|
|
462
470
|
.argument("<project-key>", "Project key")
|
|
463
471
|
.requiredOption("--diagram <id>", "Diagram ID")
|
|
@@ -469,7 +477,22 @@ export function registerDiagramCommands(program, client) {
|
|
|
469
477
|
.option("--height <n>", "Height")
|
|
470
478
|
.option("--stereotype <s>", "Stereotype")
|
|
471
479
|
.option("--description <desc>", "Description")
|
|
480
|
+
.option("--upsert", "Return existing block if name matches on this diagram instead of creating duplicate")
|
|
472
481
|
.action(async (tenant, projectKey, opts) => {
|
|
482
|
+
// Upsert: check for existing block with same name on this diagram
|
|
483
|
+
if (opts.upsert) {
|
|
484
|
+
const existing = await client.get(`/architecture/blocks/${tenant}/${projectKey}/${opts.diagram}`);
|
|
485
|
+
const match = (existing.blocks ?? []).find(b => b.name.toLowerCase() === opts.name.toLowerCase());
|
|
486
|
+
if (match) {
|
|
487
|
+
if (isJsonMode()) {
|
|
488
|
+
output({ block: match, deduplicated: true });
|
|
489
|
+
}
|
|
490
|
+
else {
|
|
491
|
+
console.log(`Block already exists: ${match.id} (${match.name})`);
|
|
492
|
+
}
|
|
493
|
+
return;
|
|
494
|
+
}
|
|
495
|
+
}
|
|
473
496
|
const data = await client.post("/architecture/blocks", {
|
|
474
497
|
tenant,
|
|
475
498
|
projectKey,
|
|
@@ -491,6 +514,85 @@ export function registerDiagramCommands(program, client) {
|
|
|
491
514
|
output(data);
|
|
492
515
|
}
|
|
493
516
|
});
|
|
517
|
+
blocks
|
|
518
|
+
.command("deduplicate")
|
|
519
|
+
.description("Find and remove duplicate-named blocks on a diagram")
|
|
520
|
+
.argument("<tenant>", "Tenant slug")
|
|
521
|
+
.argument("<project>", "Project slug")
|
|
522
|
+
.option("--diagram <id>", "Specific diagram ID (otherwise checks all)")
|
|
523
|
+
.option("--dry-run", "Show duplicates without deleting (default)")
|
|
524
|
+
.option("--apply", "Actually delete duplicates")
|
|
525
|
+
.action(async (tenant, project, opts) => {
|
|
526
|
+
const diagramIds = [];
|
|
527
|
+
if (opts.diagram) {
|
|
528
|
+
diagramIds.push(opts.diagram);
|
|
529
|
+
}
|
|
530
|
+
else {
|
|
531
|
+
const data = await client.get(`/architecture/diagrams/${tenant}/${project}`);
|
|
532
|
+
diagramIds.push(...(data.diagrams ?? []).map(d => d.id));
|
|
533
|
+
}
|
|
534
|
+
let totalRemoved = 0;
|
|
535
|
+
for (const diagId of diagramIds) {
|
|
536
|
+
const data = await client.get(`/architecture/blocks/${tenant}/${project}/${diagId}`);
|
|
537
|
+
const allBlocks = data.blocks ?? [];
|
|
538
|
+
const byName = new Map();
|
|
539
|
+
for (const b of allBlocks) {
|
|
540
|
+
const name = b.name.toLowerCase().trim();
|
|
541
|
+
const group = byName.get(name) ?? [];
|
|
542
|
+
group.push(b);
|
|
543
|
+
byName.set(name, group);
|
|
544
|
+
}
|
|
545
|
+
for (const [, group] of byName) {
|
|
546
|
+
if (group.length <= 1)
|
|
547
|
+
continue;
|
|
548
|
+
// Keep oldest (first created)
|
|
549
|
+
const sorted = group.sort((a, b) => (a.id < b.id ? -1 : 1));
|
|
550
|
+
const keep = sorted[0];
|
|
551
|
+
const dupes = sorted.slice(1);
|
|
552
|
+
console.log(` "${keep.name}" on ${diagId}: keeping ${keep.id}, ${dupes.length} duplicate(s)`);
|
|
553
|
+
if (opts.apply) {
|
|
554
|
+
for (const dup of dupes) {
|
|
555
|
+
await client.delete(`/architecture/blocks/${tenant}/${project}/${dup.id}`);
|
|
556
|
+
totalRemoved++;
|
|
557
|
+
}
|
|
558
|
+
}
|
|
559
|
+
else {
|
|
560
|
+
for (const dup of dupes) {
|
|
561
|
+
console.log(` [dry-run] would remove ${dup.id}`);
|
|
562
|
+
}
|
|
563
|
+
totalRemoved += dupes.length;
|
|
564
|
+
}
|
|
565
|
+
}
|
|
566
|
+
}
|
|
567
|
+
console.log(`${opts.apply ? "Removed" : "Would remove"} ${totalRemoved} duplicate block(s).`);
|
|
568
|
+
});
|
|
569
|
+
blocks
|
|
570
|
+
.command("find")
|
|
571
|
+
.description("Search blocks by name across project or diagram")
|
|
572
|
+
.argument("<tenant>", "Tenant slug")
|
|
573
|
+
.argument("<project>", "Project slug")
|
|
574
|
+
.requiredOption("--name <name>", "Block name to search (case-insensitive substring)")
|
|
575
|
+
.option("--diagram <id>", "Limit to specific diagram")
|
|
576
|
+
.action(async (tenant, project, opts) => {
|
|
577
|
+
if (opts.diagram) {
|
|
578
|
+
const data = await client.get(`/architecture/blocks/${tenant}/${project}/${opts.diagram}`);
|
|
579
|
+
const matches = (data.blocks ?? []).filter(b => b.name.toLowerCase().includes(opts.name.toLowerCase()));
|
|
580
|
+
if (isJsonMode()) {
|
|
581
|
+
output(matches);
|
|
582
|
+
return;
|
|
583
|
+
}
|
|
584
|
+
printTable(["ID", "Name", "Kind"], matches.map(b => [b.id, b.name, b.kind ?? ""]));
|
|
585
|
+
}
|
|
586
|
+
else {
|
|
587
|
+
const libData = await client.get(`/architecture/block-library/${tenant}/${project}`);
|
|
588
|
+
const matches = (libData.blocks ?? []).filter(b => b.name.toLowerCase().includes(opts.name.toLowerCase()));
|
|
589
|
+
if (isJsonMode()) {
|
|
590
|
+
output(matches);
|
|
591
|
+
return;
|
|
592
|
+
}
|
|
593
|
+
printTable(["ID", "Name", "Kind"], matches.map(b => [b.id, b.name, b.kind ?? ""]));
|
|
594
|
+
}
|
|
595
|
+
});
|
|
494
596
|
blocks
|
|
495
597
|
.command("delete")
|
|
496
598
|
.description("Delete a block from the project")
|
|
@@ -503,6 +605,65 @@ export function registerDiagramCommands(program, client) {
|
|
|
503
605
|
});
|
|
504
606
|
// Connectors sub-group
|
|
505
607
|
const connectors = cmd.command("connectors").alias("conn").description("Manage connectors");
|
|
608
|
+
connectors
|
|
609
|
+
.command("list")
|
|
610
|
+
.description("List connectors in a diagram")
|
|
611
|
+
.argument("<tenant>", "Tenant slug")
|
|
612
|
+
.argument("<project>", "Project slug")
|
|
613
|
+
.argument("<diagram-id>", "Diagram ID")
|
|
614
|
+
.action(async (tenant, project, diagramId) => {
|
|
615
|
+
const data = await client.get(`/architecture/connectors/${tenant}/${project}/${diagramId}`);
|
|
616
|
+
const list = data.connectors ?? [];
|
|
617
|
+
if (isJsonMode()) {
|
|
618
|
+
output(list);
|
|
619
|
+
}
|
|
620
|
+
else {
|
|
621
|
+
if (list.length === 0) {
|
|
622
|
+
console.log("No connectors in this diagram.");
|
|
623
|
+
return;
|
|
624
|
+
}
|
|
625
|
+
printTable(["ID", "Source", "Target", "Kind", "Label"], list.map(c => [c.id, c.source, c.target, c.kind ?? "", c.label ?? ""]));
|
|
626
|
+
}
|
|
627
|
+
});
|
|
628
|
+
connectors
|
|
629
|
+
.command("cleanup")
|
|
630
|
+
.description("Find and remove orphan connectors (source or target block missing)")
|
|
631
|
+
.argument("<tenant>", "Tenant slug")
|
|
632
|
+
.argument("<project>", "Project slug")
|
|
633
|
+
.option("--diagram <id>", "Specific diagram ID (otherwise checks all)")
|
|
634
|
+
.option("--apply", "Actually delete orphans (default is dry-run)")
|
|
635
|
+
.action(async (tenant, project, opts) => {
|
|
636
|
+
const diagramIds = [];
|
|
637
|
+
if (opts.diagram) {
|
|
638
|
+
diagramIds.push(opts.diagram);
|
|
639
|
+
}
|
|
640
|
+
else {
|
|
641
|
+
const data = await client.get(`/architecture/diagrams/${tenant}/${project}`);
|
|
642
|
+
diagramIds.push(...(data.diagrams ?? []).map(d => d.id));
|
|
643
|
+
}
|
|
644
|
+
let totalOrphans = 0;
|
|
645
|
+
for (const diagId of diagramIds) {
|
|
646
|
+
const [blockData, connData] = await Promise.all([
|
|
647
|
+
client.get(`/architecture/blocks/${tenant}/${project}/${diagId}`),
|
|
648
|
+
client.get(`/architecture/connectors/${tenant}/${project}/${diagId}`),
|
|
649
|
+
]);
|
|
650
|
+
const blockIds = new Set((blockData.blocks ?? []).map(b => b.id));
|
|
651
|
+
const orphans = (connData.connectors ?? []).filter(c => !blockIds.has(c.source) || !blockIds.has(c.target));
|
|
652
|
+
if (orphans.length === 0)
|
|
653
|
+
continue;
|
|
654
|
+
console.log(` ${diagId}: ${orphans.length} orphan connector(s)`);
|
|
655
|
+
for (const c of orphans) {
|
|
656
|
+
if (opts.apply) {
|
|
657
|
+
await client.delete(`/architecture/connectors/${tenant}/${project}/${c.id}?diagramId=${diagId}`);
|
|
658
|
+
}
|
|
659
|
+
else {
|
|
660
|
+
console.log(` [dry-run] ${c.id}: ${c.source} → ${c.target} (${c.kind ?? "?"})`);
|
|
661
|
+
}
|
|
662
|
+
}
|
|
663
|
+
totalOrphans += orphans.length;
|
|
664
|
+
}
|
|
665
|
+
console.log(`${opts.apply ? "Removed" : "Would remove"} ${totalOrphans} orphan connector(s).`);
|
|
666
|
+
});
|
|
506
667
|
connectors
|
|
507
668
|
.command("create")
|
|
508
669
|
.description("Create a connector between blocks")
|
|
@@ -516,7 +677,21 @@ export function registerDiagramCommands(program, client) {
|
|
|
516
677
|
.option("--source-port <id>", "Source port ID")
|
|
517
678
|
.option("--target-port <id>", "Target port ID")
|
|
518
679
|
.option("--line-style <style>", "Line style: straight, smoothstep, step, polyline, bezier")
|
|
680
|
+
.option("--upsert", "Return existing connector if source+target+diagram matches")
|
|
519
681
|
.action(async (tenant, projectKey, opts) => {
|
|
682
|
+
if (opts.upsert) {
|
|
683
|
+
const existing = await client.get(`/architecture/connectors/${tenant}/${projectKey}/${opts.diagram}`);
|
|
684
|
+
const match = (existing.connectors ?? []).find(c => c.source === opts.source && c.target === opts.target);
|
|
685
|
+
if (match) {
|
|
686
|
+
if (isJsonMode()) {
|
|
687
|
+
output({ connector: match, deduplicated: true });
|
|
688
|
+
}
|
|
689
|
+
else {
|
|
690
|
+
console.log(`Connector already exists: ${match.id} (${match.source} → ${match.target})`);
|
|
691
|
+
}
|
|
692
|
+
return;
|
|
693
|
+
}
|
|
694
|
+
}
|
|
520
695
|
const data = await client.post("/architecture/connectors", {
|
|
521
696
|
tenant,
|
|
522
697
|
projectKey,
|
|
@@ -147,7 +147,7 @@ export function registerProjectCommands(program, client) {
|
|
|
147
147
|
});
|
|
148
148
|
cmd
|
|
149
149
|
.command("delete")
|
|
150
|
-
.description("Delete a project")
|
|
150
|
+
.description("Delete a project by slug")
|
|
151
151
|
.argument("<tenant>", "Tenant slug")
|
|
152
152
|
.argument("<project>", "Project slug")
|
|
153
153
|
.action(async (tenant, project) => {
|