@diagrammo/dgmo 0.6.0 → 0.6.2
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/.claude/commands/dgmo.md +76 -0
- package/dist/cli.cjs +164 -162
- package/dist/index.cjs +1146 -647
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +9 -21
- package/dist/index.d.ts +9 -21
- package/dist/index.js +1146 -647
- package/dist/index.js.map +1 -1
- package/docs/ai-integration.md +33 -50
- package/package.json +4 -3
- package/src/c4/layout.ts +75 -72
- package/src/c4/renderer.ts +122 -119
- package/src/cli.ts +130 -40
- package/src/d3.ts +55 -35
- package/src/echarts.ts +24 -24
- package/src/er/classify.ts +206 -0
- package/src/er/layout.ts +259 -94
- package/src/er/renderer.ts +246 -26
- package/src/index.ts +2 -2
- package/src/infra/compute.ts +1 -21
- package/src/infra/layout.ts +60 -13
- package/src/infra/parser.ts +5 -32
- package/src/infra/renderer.ts +403 -196
- package/src/infra/types.ts +1 -11
- package/src/initiative-status/layout.ts +46 -27
- package/src/kanban/renderer.ts +28 -24
- package/src/org/renderer.ts +24 -23
- package/src/render.ts +2 -2
- package/src/sequence/renderer.ts +24 -19
- package/src/sitemap/layout.ts +7 -14
- package/src/sitemap/renderer.ts +30 -29
- package/src/utils/legend-constants.ts +25 -0
- package/.claude/skills/dgmo-chart/SKILL.md +0 -141
- package/.claude/skills/dgmo-flowchart/SKILL.md +0 -61
- package/.claude/skills/dgmo-generate/SKILL.md +0 -59
- package/.claude/skills/dgmo-sequence/SKILL.md +0 -104
package/src/c4/renderer.ts
CHANGED
|
@@ -10,9 +10,22 @@ import { mix } from '../palettes/color-utils';
|
|
|
10
10
|
import { renderInlineText } from '../utils/inline-markdown';
|
|
11
11
|
import type { ParsedC4 } from './types';
|
|
12
12
|
import type { C4Shape } from './types';
|
|
13
|
-
import type { C4LayoutResult, C4LayoutNode, C4LayoutEdge, C4LayoutBoundary } from './layout';
|
|
13
|
+
import type { C4LayoutResult, C4LayoutNode, C4LayoutEdge, C4LayoutBoundary, C4LegendGroup } from './layout';
|
|
14
14
|
import { parseC4 } from './parser';
|
|
15
15
|
import { layoutC4Context, layoutC4Containers, layoutC4Components, layoutC4Deployment, collectCardMetadata } from './layout';
|
|
16
|
+
import {
|
|
17
|
+
LEGEND_HEIGHT,
|
|
18
|
+
LEGEND_PILL_FONT_SIZE,
|
|
19
|
+
LEGEND_PILL_FONT_W,
|
|
20
|
+
LEGEND_PILL_PAD,
|
|
21
|
+
LEGEND_DOT_R,
|
|
22
|
+
LEGEND_ENTRY_FONT_SIZE,
|
|
23
|
+
LEGEND_ENTRY_FONT_W,
|
|
24
|
+
LEGEND_ENTRY_DOT_GAP,
|
|
25
|
+
LEGEND_ENTRY_TRAIL,
|
|
26
|
+
LEGEND_CAPSULE_PAD,
|
|
27
|
+
LEGEND_GROUP_GAP,
|
|
28
|
+
} from '../utils/legend-constants';
|
|
16
29
|
|
|
17
30
|
// ============================================================
|
|
18
31
|
// Constants
|
|
@@ -59,16 +72,6 @@ const PERSON_ICON_W = PERSON_ARM_SPAN * 2; // total width including arms
|
|
|
59
72
|
const PERSON_SW = 1.5;
|
|
60
73
|
|
|
61
74
|
// Legend constants (match org)
|
|
62
|
-
const LEGEND_HEIGHT = 28;
|
|
63
|
-
const LEGEND_PILL_FONT_SIZE = 11;
|
|
64
|
-
const LEGEND_PILL_FONT_W = LEGEND_PILL_FONT_SIZE * 0.6;
|
|
65
|
-
const LEGEND_PILL_PAD = 16;
|
|
66
|
-
const LEGEND_DOT_R = 4;
|
|
67
|
-
const LEGEND_ENTRY_FONT_SIZE = 10;
|
|
68
|
-
const LEGEND_ENTRY_FONT_W = LEGEND_ENTRY_FONT_SIZE * 0.6;
|
|
69
|
-
const LEGEND_ENTRY_DOT_GAP = 4;
|
|
70
|
-
const LEGEND_ENTRY_TRAIL = 8;
|
|
71
|
-
const LEGEND_CAPSULE_PAD = 4;
|
|
72
75
|
|
|
73
76
|
// ============================================================
|
|
74
77
|
// Color helpers
|
|
@@ -239,8 +242,16 @@ export function renderC4Context(
|
|
|
239
242
|
|
|
240
243
|
const titleHeight = parsed.title ? TITLE_HEIGHT + 10 : 0;
|
|
241
244
|
const diagramW = layout.width;
|
|
242
|
-
const
|
|
243
|
-
|
|
245
|
+
const hasLegend = layout.legend.length > 0;
|
|
246
|
+
// In app mode, legend is a fixed overlay outside the scaled group.
|
|
247
|
+
// C4 layout adds MARGIN(40) + LEGEND_HEIGHT below content — remove that from diagramH.
|
|
248
|
+
const C4_LAYOUT_MARGIN = 40;
|
|
249
|
+
const LEGEND_FIXED_GAP = 8;
|
|
250
|
+
const fixedLegend = !exportDims && hasLegend;
|
|
251
|
+
const legendLayoutSpace = C4_LAYOUT_MARGIN + LEGEND_HEIGHT;
|
|
252
|
+
const legendReserveH = fixedLegend ? LEGEND_HEIGHT + LEGEND_FIXED_GAP : 0;
|
|
253
|
+
const diagramH = fixedLegend ? layout.height - legendLayoutSpace : layout.height;
|
|
254
|
+
const availH = height - titleHeight - legendReserveH;
|
|
244
255
|
const scaleX = (width - DIAGRAM_PADDING * 2) / diagramW;
|
|
245
256
|
const scaleY = (availH - DIAGRAM_PADDING * 2) / diagramH;
|
|
246
257
|
const scale = Math.min(MAX_SCALE, scaleX, scaleY);
|
|
@@ -428,6 +439,23 @@ export function renderC4Context(
|
|
|
428
439
|
.attr('data-line-number', String(node.lineNumber))
|
|
429
440
|
.attr('data-node-id', node.id);
|
|
430
441
|
|
|
442
|
+
if (activeTagGroup) {
|
|
443
|
+
const tagKey = activeTagGroup.toLowerCase();
|
|
444
|
+
const tagValue = node.metadata[tagKey];
|
|
445
|
+
if (tagValue) {
|
|
446
|
+
nodeG.attr(`data-tag-${tagKey}`, tagValue.toLowerCase());
|
|
447
|
+
} else {
|
|
448
|
+
// Fall back to the group's defaultValue so hover-dimming works for
|
|
449
|
+
// nodes that inherit the default (e.g. sc: Internal default).
|
|
450
|
+
const tagGroup = parsed.tagGroups.find(
|
|
451
|
+
(g) => g.name.toLowerCase() === tagKey || g.alias?.toLowerCase() === tagKey
|
|
452
|
+
);
|
|
453
|
+
if (tagGroup?.defaultValue) {
|
|
454
|
+
nodeG.attr(`data-tag-${tagKey}`, tagGroup.defaultValue.toLowerCase());
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
|
|
431
459
|
if (node.importPath) {
|
|
432
460
|
nodeG.attr('data-import-path', node.importPath);
|
|
433
461
|
}
|
|
@@ -566,101 +594,18 @@ export function renderC4Context(
|
|
|
566
594
|
}
|
|
567
595
|
|
|
568
596
|
// ── Legend ──
|
|
569
|
-
if (
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
: mix(palette.surface, palette.bg, 30);
|
|
580
|
-
|
|
581
|
-
const pillLabel = group.name;
|
|
582
|
-
const pillWidth = pillLabel.length * LEGEND_PILL_FONT_W + LEGEND_PILL_PAD;
|
|
583
|
-
|
|
584
|
-
const gEl = contentG
|
|
585
|
-
.append('g')
|
|
586
|
-
.attr('transform', `translate(${group.x}, ${group.y})`)
|
|
587
|
-
.attr('class', 'c4-legend-group')
|
|
588
|
-
.attr('data-legend-group', group.name.toLowerCase())
|
|
589
|
-
.style('cursor', 'pointer');
|
|
590
|
-
|
|
591
|
-
if (isActive) {
|
|
592
|
-
gEl
|
|
593
|
-
.append('rect')
|
|
594
|
-
.attr('width', group.width)
|
|
595
|
-
.attr('height', LEGEND_HEIGHT)
|
|
596
|
-
.attr('rx', LEGEND_HEIGHT / 2)
|
|
597
|
-
.attr('fill', groupBg);
|
|
598
|
-
}
|
|
599
|
-
|
|
600
|
-
const pillX = isActive ? LEGEND_CAPSULE_PAD : 0;
|
|
601
|
-
const pillY = isActive ? LEGEND_CAPSULE_PAD : 0;
|
|
602
|
-
const pillH = LEGEND_HEIGHT - (isActive ? LEGEND_CAPSULE_PAD * 2 : 0);
|
|
603
|
-
|
|
604
|
-
gEl
|
|
605
|
-
.append('rect')
|
|
606
|
-
.attr('x', pillX)
|
|
607
|
-
.attr('y', pillY)
|
|
608
|
-
.attr('width', pillWidth)
|
|
609
|
-
.attr('height', pillH)
|
|
610
|
-
.attr('rx', pillH / 2)
|
|
611
|
-
.attr('fill', isActive ? palette.bg : groupBg);
|
|
612
|
-
|
|
613
|
-
if (isActive) {
|
|
614
|
-
gEl
|
|
615
|
-
.append('rect')
|
|
616
|
-
.attr('x', pillX)
|
|
617
|
-
.attr('y', pillY)
|
|
618
|
-
.attr('width', pillWidth)
|
|
619
|
-
.attr('height', pillH)
|
|
620
|
-
.attr('rx', pillH / 2)
|
|
621
|
-
.attr('fill', 'none')
|
|
622
|
-
.attr('stroke', mix(palette.textMuted, palette.bg, 50))
|
|
623
|
-
.attr('stroke-width', 0.75);
|
|
624
|
-
}
|
|
625
|
-
|
|
626
|
-
gEl
|
|
627
|
-
.append('text')
|
|
628
|
-
.attr('x', pillX + pillWidth / 2)
|
|
629
|
-
.attr('y', LEGEND_HEIGHT / 2 + LEGEND_PILL_FONT_SIZE / 2 - 2)
|
|
630
|
-
.attr('font-size', LEGEND_PILL_FONT_SIZE)
|
|
631
|
-
.attr('font-weight', '500')
|
|
632
|
-
.attr('fill', isActive ? palette.text : palette.textMuted)
|
|
633
|
-
.attr('text-anchor', 'middle')
|
|
634
|
-
.text(pillLabel);
|
|
635
|
-
|
|
636
|
-
if (isActive) {
|
|
637
|
-
let entryX = pillX + pillWidth + 4;
|
|
638
|
-
for (const entry of group.entries) {
|
|
639
|
-
const entryG = gEl
|
|
640
|
-
.append('g')
|
|
641
|
-
.attr('data-legend-entry', entry.value.toLowerCase())
|
|
642
|
-
.style('cursor', 'pointer');
|
|
643
|
-
|
|
644
|
-
entryG
|
|
645
|
-
.append('circle')
|
|
646
|
-
.attr('cx', entryX + LEGEND_DOT_R)
|
|
647
|
-
.attr('cy', LEGEND_HEIGHT / 2)
|
|
648
|
-
.attr('r', LEGEND_DOT_R)
|
|
649
|
-
.attr('fill', entry.color);
|
|
650
|
-
|
|
651
|
-
const textX = entryX + LEGEND_DOT_R * 2 + LEGEND_ENTRY_DOT_GAP;
|
|
652
|
-
entryG
|
|
653
|
-
.append('text')
|
|
654
|
-
.attr('x', textX)
|
|
655
|
-
.attr('y', LEGEND_HEIGHT / 2 + LEGEND_ENTRY_FONT_SIZE / 2 - 1)
|
|
656
|
-
.attr('font-size', LEGEND_ENTRY_FONT_SIZE)
|
|
657
|
-
.attr('fill', palette.textMuted)
|
|
658
|
-
.text(entry.value);
|
|
659
|
-
|
|
660
|
-
entryX = textX + entry.value.length * LEGEND_ENTRY_FONT_W + LEGEND_ENTRY_TRAIL;
|
|
661
|
-
}
|
|
662
|
-
}
|
|
597
|
+
if (hasLegend) {
|
|
598
|
+
// App mode: fixed overlay at SVG bottom so it's always readable regardless of scale.
|
|
599
|
+
// Export mode: render inside scaled contentG at layout coordinates.
|
|
600
|
+
const legendParent = fixedLegend
|
|
601
|
+
? svg.append('g')
|
|
602
|
+
.attr('class', 'c4-legend-fixed')
|
|
603
|
+
.attr('transform', `translate(0, ${height - DIAGRAM_PADDING - LEGEND_HEIGHT})`)
|
|
604
|
+
: contentG.append('g').attr('class', 'c4-legend');
|
|
605
|
+
if (activeTagGroup) {
|
|
606
|
+
legendParent.attr('data-legend-active', activeTagGroup.toLowerCase());
|
|
663
607
|
}
|
|
608
|
+
renderLegend(legendParent as GSelection, layout, palette, isDark, activeTagGroup, fixedLegend ? width : null);
|
|
664
609
|
}
|
|
665
610
|
}
|
|
666
611
|
|
|
@@ -1189,29 +1134,52 @@ function placeEdgeLabels(
|
|
|
1189
1134
|
}
|
|
1190
1135
|
|
|
1191
1136
|
function renderLegend(
|
|
1192
|
-
|
|
1137
|
+
parent: GSelection,
|
|
1193
1138
|
layout: C4LayoutResult,
|
|
1194
1139
|
palette: PaletteColors,
|
|
1195
1140
|
isDark: boolean,
|
|
1196
|
-
activeTagGroup?: string | null
|
|
1141
|
+
activeTagGroup?: string | null,
|
|
1142
|
+
/** When set, center groups horizontally across this width (fixed overlay mode). */
|
|
1143
|
+
fixedWidth?: number | null
|
|
1197
1144
|
): void {
|
|
1198
|
-
|
|
1145
|
+
const visibleGroups = activeTagGroup != null
|
|
1146
|
+
? layout.legend.filter((g) => g.name.toLowerCase() === (activeTagGroup ?? '').toLowerCase())
|
|
1147
|
+
: layout.legend;
|
|
1148
|
+
|
|
1149
|
+
const pillWidthOf = (g: C4LegendGroup) => g.name.length * LEGEND_PILL_FONT_W + LEGEND_PILL_PAD;
|
|
1150
|
+
const effectiveW = (g: C4LegendGroup) => activeTagGroup != null ? g.width : pillWidthOf(g);
|
|
1151
|
+
|
|
1152
|
+
// In fixed mode, compute centered x-positions
|
|
1153
|
+
let fixedPositions: Map<string, number> | null = null;
|
|
1154
|
+
if (fixedWidth != null && visibleGroups.length > 0) {
|
|
1155
|
+
fixedPositions = new Map();
|
|
1156
|
+
const totalW = visibleGroups.reduce((s, g) => s + effectiveW(g), 0)
|
|
1157
|
+
+ (visibleGroups.length - 1) * LEGEND_GROUP_GAP;
|
|
1158
|
+
let cx = Math.max(DIAGRAM_PADDING, (fixedWidth - totalW) / 2);
|
|
1159
|
+
for (const g of visibleGroups) {
|
|
1160
|
+
fixedPositions.set(g.name, cx);
|
|
1161
|
+
cx += effectiveW(g) + LEGEND_GROUP_GAP;
|
|
1162
|
+
}
|
|
1163
|
+
}
|
|
1164
|
+
|
|
1165
|
+
for (const group of visibleGroups) {
|
|
1199
1166
|
const isActive =
|
|
1200
1167
|
activeTagGroup != null &&
|
|
1201
1168
|
group.name.toLowerCase() === (activeTagGroup ?? '').toLowerCase();
|
|
1202
1169
|
|
|
1203
|
-
if (activeTagGroup != null && !isActive) continue;
|
|
1204
|
-
|
|
1205
1170
|
const groupBg = isDark
|
|
1206
1171
|
? mix(palette.surface, palette.bg, 50)
|
|
1207
1172
|
: mix(palette.surface, palette.bg, 30);
|
|
1208
1173
|
|
|
1209
1174
|
const pillLabel = group.name;
|
|
1210
|
-
const pillWidth =
|
|
1175
|
+
const pillWidth = pillWidthOf(group);
|
|
1176
|
+
|
|
1177
|
+
const gX = fixedPositions?.get(group.name) ?? group.x;
|
|
1178
|
+
const gY = fixedPositions != null ? 0 : group.y;
|
|
1211
1179
|
|
|
1212
|
-
const gEl =
|
|
1180
|
+
const gEl = parent
|
|
1213
1181
|
.append('g')
|
|
1214
|
-
.attr('transform', `translate(${
|
|
1182
|
+
.attr('transform', `translate(${gX}, ${gY})`)
|
|
1215
1183
|
.attr('class', 'c4-legend-group')
|
|
1216
1184
|
.attr('data-legend-group', group.name.toLowerCase())
|
|
1217
1185
|
.style('cursor', 'pointer');
|
|
@@ -1317,8 +1285,16 @@ export function renderC4Containers(
|
|
|
1317
1285
|
|
|
1318
1286
|
const titleHeight = parsed.title ? TITLE_HEIGHT + 10 : 0;
|
|
1319
1287
|
const diagramW = layout.width;
|
|
1320
|
-
const
|
|
1321
|
-
|
|
1288
|
+
const hasLegend = layout.legend.length > 0;
|
|
1289
|
+
// In app mode, legend is a fixed overlay outside the scaled group.
|
|
1290
|
+
// C4 layout adds MARGIN(40) + LEGEND_HEIGHT below content — remove that from diagramH.
|
|
1291
|
+
const C4_LAYOUT_MARGIN = 40;
|
|
1292
|
+
const LEGEND_FIXED_GAP = 8;
|
|
1293
|
+
const fixedLegend = !exportDims && hasLegend;
|
|
1294
|
+
const legendLayoutSpace = C4_LAYOUT_MARGIN + LEGEND_HEIGHT;
|
|
1295
|
+
const legendReserveH = fixedLegend ? LEGEND_HEIGHT + LEGEND_FIXED_GAP : 0;
|
|
1296
|
+
const diagramH = fixedLegend ? layout.height - legendLayoutSpace : layout.height;
|
|
1297
|
+
const availH = height - titleHeight - legendReserveH;
|
|
1322
1298
|
const scaleX = (width - DIAGRAM_PADDING * 2) / diagramW;
|
|
1323
1299
|
const scaleY = (availH - DIAGRAM_PADDING * 2) / diagramH;
|
|
1324
1300
|
const scale = Math.min(MAX_SCALE, scaleX, scaleY);
|
|
@@ -1508,6 +1484,23 @@ export function renderC4Containers(
|
|
|
1508
1484
|
.attr('data-line-number', String(node.lineNumber))
|
|
1509
1485
|
.attr('data-node-id', node.id);
|
|
1510
1486
|
|
|
1487
|
+
if (activeTagGroup) {
|
|
1488
|
+
const tagKey = activeTagGroup.toLowerCase();
|
|
1489
|
+
const tagValue = node.metadata[tagKey];
|
|
1490
|
+
if (tagValue) {
|
|
1491
|
+
nodeG.attr(`data-tag-${tagKey}`, tagValue.toLowerCase());
|
|
1492
|
+
} else {
|
|
1493
|
+
// Fall back to the group's defaultValue so hover-dimming works for
|
|
1494
|
+
// nodes that inherit the default (e.g. sc: Internal default).
|
|
1495
|
+
const tagGroup = parsed.tagGroups.find(
|
|
1496
|
+
(g) => g.name.toLowerCase() === tagKey || g.alias?.toLowerCase() === tagKey
|
|
1497
|
+
);
|
|
1498
|
+
if (tagGroup?.defaultValue) {
|
|
1499
|
+
nodeG.attr(`data-tag-${tagKey}`, tagGroup.defaultValue.toLowerCase());
|
|
1500
|
+
}
|
|
1501
|
+
}
|
|
1502
|
+
}
|
|
1503
|
+
|
|
1511
1504
|
if (node.shape) {
|
|
1512
1505
|
nodeG.attr('data-shape', node.shape);
|
|
1513
1506
|
}
|
|
@@ -1717,8 +1710,18 @@ export function renderC4Containers(
|
|
|
1717
1710
|
}
|
|
1718
1711
|
|
|
1719
1712
|
// ── Legend ──
|
|
1720
|
-
if (
|
|
1721
|
-
|
|
1713
|
+
if (hasLegend) {
|
|
1714
|
+
// App mode: fixed overlay at SVG bottom so it's always readable regardless of scale.
|
|
1715
|
+
// Export mode: render inside scaled contentG at layout coordinates.
|
|
1716
|
+
const legendParent = fixedLegend
|
|
1717
|
+
? svg.append('g')
|
|
1718
|
+
.attr('class', 'c4-legend-fixed')
|
|
1719
|
+
.attr('transform', `translate(0, ${height - DIAGRAM_PADDING - LEGEND_HEIGHT})`)
|
|
1720
|
+
: contentG.append('g').attr('class', 'c4-legend');
|
|
1721
|
+
if (activeTagGroup) {
|
|
1722
|
+
legendParent.attr('data-legend-active', activeTagGroup.toLowerCase());
|
|
1723
|
+
}
|
|
1724
|
+
renderLegend(legendParent as GSelection, layout, palette, isDark, activeTagGroup, fixedLegend ? width : null);
|
|
1722
1725
|
}
|
|
1723
1726
|
}
|
|
1724
1727
|
|
package/src/cli.ts
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
|
-
import { existsSync, readFileSync, writeFileSync } from 'node:fs';
|
|
1
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
|
|
2
2
|
import { execSync } from 'node:child_process';
|
|
3
|
-
import {
|
|
3
|
+
import { homedir } from 'node:os';
|
|
4
|
+
import { resolve, join, basename, extname } from 'node:path';
|
|
5
|
+
import { createInterface } from 'node:readline';
|
|
4
6
|
import { Resvg } from '@resvg/resvg-js';
|
|
5
7
|
import { render } from './render';
|
|
6
8
|
import { parseDgmo, getAllChartTypes } from './dgmo-router';
|
|
@@ -57,6 +59,84 @@ const CHART_TYPE_DESCRIPTIONS: Record<string, string> = {
|
|
|
57
59
|
infra: 'Infra chart — infrastructure traffic flow with rps computation',
|
|
58
60
|
};
|
|
59
61
|
|
|
62
|
+
const CLAUDE_SKILL_CONTENT = `# dgmo — Diagrammo Diagram Assistant
|
|
63
|
+
|
|
64
|
+
You are helping the user author, render, and share diagrams using the \`dgmo\` CLI and \`.dgmo\` file format.
|
|
65
|
+
|
|
66
|
+
## What is dgmo?
|
|
67
|
+
|
|
68
|
+
\`dgmo\` is a CLI tool that renders \`.dgmo\` diagram files to PNG, SVG, or shareable URLs. Diagrams are written in a plain-text DSL.
|
|
69
|
+
|
|
70
|
+
## CLI Reference
|
|
71
|
+
|
|
72
|
+
\`\`\`
|
|
73
|
+
dgmo <input.dgmo> [options]
|
|
74
|
+
cat input.dgmo | dgmo [options]
|
|
75
|
+
\`\`\`
|
|
76
|
+
|
|
77
|
+
Key options:
|
|
78
|
+
- \`-o <file>\` — output file; format inferred from extension (\`.svg\` → SVG, else PNG)
|
|
79
|
+
- \`-o url\` — output a shareable diagrammo.app URL
|
|
80
|
+
- \`--theme <theme>\` — \`light\` (default), \`dark\`, \`transparent\`
|
|
81
|
+
- \`--palette <name>\` — \`nord\` (default), \`solarized\`, \`catppuccin\`, \`rose-pine\`, \`gruvbox\`, \`tokyo-night\`, \`one-dark\`, \`bold\`
|
|
82
|
+
- \`--copy\` — copy the URL to clipboard (use with \`-o url\`)
|
|
83
|
+
- \`--no-branding\` — omit diagrammo.app branding from exports
|
|
84
|
+
- \`--chart-types\` — list all supported chart types
|
|
85
|
+
|
|
86
|
+
## Supported Chart Types
|
|
87
|
+
|
|
88
|
+
| Type | Use case |
|
|
89
|
+
|------|----------|
|
|
90
|
+
| \`bar\` | Categorical comparisons |
|
|
91
|
+
| \`line\` / \`multi-line\` / \`area\` | Trends over time |
|
|
92
|
+
| \`pie\` / \`doughnut\` | Part-to-whole |
|
|
93
|
+
| \`radar\` / \`polar-area\` | Multi-dimensional metrics |
|
|
94
|
+
| \`bar-stacked\` | Multi-series categorical |
|
|
95
|
+
| \`scatter\` | 2D data points or bubble chart |
|
|
96
|
+
| \`sankey\` | Flow / allocation |
|
|
97
|
+
| \`chord\` | Circular flow relationships |
|
|
98
|
+
| \`function\` | Mathematical expressions |
|
|
99
|
+
| \`heatmap\` | Matrix intensity |
|
|
100
|
+
| \`funnel\` | Conversion pipeline |
|
|
101
|
+
| \`slope\` | Change between two periods |
|
|
102
|
+
| \`wordcloud\` | Term frequency |
|
|
103
|
+
| \`arc\` | Network relationships |
|
|
104
|
+
| \`timeline\` | Events, eras, date ranges |
|
|
105
|
+
| \`venn\` | Set overlaps |
|
|
106
|
+
| \`quadrant\` | 2x2 positioning matrix |
|
|
107
|
+
| \`sequence\` | Message / interaction flows |
|
|
108
|
+
| \`flowchart\` | Decision trees, process flows |
|
|
109
|
+
| \`class\` | UML class hierarchies |
|
|
110
|
+
| \`er\` | Database schemas |
|
|
111
|
+
| \`org\` | Hierarchical tree structures |
|
|
112
|
+
| \`kanban\` | Task / workflow columns |
|
|
113
|
+
| \`c4\` | System architecture (context → container → component → deployment) |
|
|
114
|
+
| \`initiative-status\` | Project roadmap with dependency tracking |
|
|
115
|
+
|
|
116
|
+
## Your Workflow
|
|
117
|
+
|
|
118
|
+
When the user asks you to create or edit a diagram:
|
|
119
|
+
|
|
120
|
+
1. **Write or edit the \`.dgmo\` file** with the appropriate chart type and data.
|
|
121
|
+
2. **Render it** with \`dgmo <file>.dgmo -o <file>.png\` to verify it produces output without errors.
|
|
122
|
+
3. **Show the user** what was created and suggest a shareable URL with \`dgmo <file>.dgmo -o url --copy\` if they want to share it.
|
|
123
|
+
|
|
124
|
+
When the user asks for a **shareable link**, run:
|
|
125
|
+
\`\`\`
|
|
126
|
+
dgmo <file>.dgmo -o url --copy
|
|
127
|
+
\`\`\`
|
|
128
|
+
|
|
129
|
+
## Getting Syntax Help
|
|
130
|
+
|
|
131
|
+
Run \`dgmo --chart-types\` to list types. For detailed syntax of a specific chart type, the best reference is the diagrammo.app documentation or existing \`.dgmo\` files in the project.
|
|
132
|
+
|
|
133
|
+
## Tips
|
|
134
|
+
|
|
135
|
+
- Default theme is \`light\` and default palette is \`nord\` — ask the user if they have a preference before rendering a final export.
|
|
136
|
+
- For C4 diagrams, use \`--c4-level\` to drill from context → containers → components → deployment.
|
|
137
|
+
- Stdin mode is useful for quick one-off renders: \`echo "..." | dgmo -o out.png\`
|
|
138
|
+
`;
|
|
139
|
+
|
|
60
140
|
function printHelp(): void {
|
|
61
141
|
console.log(`Usage: dgmo <input> [options]
|
|
62
142
|
cat input.dgmo | dgmo [options]
|
|
@@ -73,10 +153,12 @@ Options:
|
|
|
73
153
|
--c4-level <level> C4 render level: context (default), containers, components, deployment
|
|
74
154
|
--c4-system <name> System to drill into (with --c4-level containers or components)
|
|
75
155
|
--c4-container <name> Container to drill into (with --c4-level components)
|
|
156
|
+
--tag-group <name> Pre-select a tag group for static export coloring
|
|
76
157
|
--no-branding Omit diagrammo.app branding from exports
|
|
77
158
|
--copy Copy URL to clipboard (only with -o url)
|
|
78
159
|
--json Output structured JSON to stdout
|
|
79
160
|
--chart-types List all supported chart types
|
|
161
|
+
--install-claude-skill Install the dgmo Claude Code skill to ~/.claude/commands/
|
|
80
162
|
--help Show this help
|
|
81
163
|
--version Show version`);
|
|
82
164
|
}
|
|
@@ -99,11 +181,11 @@ function parseArgs(argv: string[]): {
|
|
|
99
181
|
copy: boolean;
|
|
100
182
|
json: boolean;
|
|
101
183
|
chartTypes: boolean;
|
|
184
|
+
installClaudeSkill: boolean;
|
|
102
185
|
c4Level: 'context' | 'containers' | 'components' | 'deployment';
|
|
103
186
|
c4System: string | undefined;
|
|
104
187
|
c4Container: string | undefined;
|
|
105
|
-
|
|
106
|
-
listScenarios: boolean;
|
|
188
|
+
tagGroup: string | undefined;
|
|
107
189
|
} {
|
|
108
190
|
const result = {
|
|
109
191
|
input: undefined as string | undefined,
|
|
@@ -116,11 +198,11 @@ function parseArgs(argv: string[]): {
|
|
|
116
198
|
copy: false,
|
|
117
199
|
json: false,
|
|
118
200
|
chartTypes: false,
|
|
201
|
+
installClaudeSkill: false,
|
|
119
202
|
c4Level: 'context' as 'context' | 'containers' | 'components' | 'deployment',
|
|
120
203
|
c4System: undefined as string | undefined,
|
|
121
204
|
c4Container: undefined as string | undefined,
|
|
122
|
-
|
|
123
|
-
listScenarios: false,
|
|
205
|
+
tagGroup: undefined as string | undefined,
|
|
124
206
|
};
|
|
125
207
|
|
|
126
208
|
const args = argv.slice(2); // skip node + script
|
|
@@ -174,11 +256,8 @@ function parseArgs(argv: string[]): {
|
|
|
174
256
|
} else if (arg === '--c4-container') {
|
|
175
257
|
result.c4Container = args[++i];
|
|
176
258
|
i++;
|
|
177
|
-
} else if (arg === '--
|
|
178
|
-
result.
|
|
179
|
-
i++;
|
|
180
|
-
} else if (arg === '--list-scenarios') {
|
|
181
|
-
result.listScenarios = true;
|
|
259
|
+
} else if (arg === '--tag-group') {
|
|
260
|
+
result.tagGroup = args[++i];
|
|
182
261
|
i++;
|
|
183
262
|
} else if (arg === '--no-branding') {
|
|
184
263
|
result.noBranding = true;
|
|
@@ -189,6 +268,9 @@ function parseArgs(argv: string[]): {
|
|
|
189
268
|
} else if (arg === '--chart-types') {
|
|
190
269
|
result.chartTypes = true;
|
|
191
270
|
i++;
|
|
271
|
+
} else if (arg === '--install-claude-skill') {
|
|
272
|
+
result.installClaudeSkill = true;
|
|
273
|
+
i++;
|
|
192
274
|
} else if (arg === '--copy') {
|
|
193
275
|
result.copy = true;
|
|
194
276
|
i++;
|
|
@@ -292,6 +374,42 @@ async function main(): Promise<void> {
|
|
|
292
374
|
return;
|
|
293
375
|
}
|
|
294
376
|
|
|
377
|
+
if (opts.installClaudeSkill) {
|
|
378
|
+
const claudeDir = join(homedir(), '.claude');
|
|
379
|
+
if (!existsSync(claudeDir)) {
|
|
380
|
+
console.error('~/.claude directory not found.');
|
|
381
|
+
console.error('Install Claude Code first: https://claude.ai/code');
|
|
382
|
+
process.exit(1);
|
|
383
|
+
}
|
|
384
|
+
const commandsDir = join(claudeDir, 'commands');
|
|
385
|
+
const destPath = join(commandsDir, 'dgmo.md');
|
|
386
|
+
const alreadyExists = existsSync(destPath);
|
|
387
|
+
const prompt = alreadyExists
|
|
388
|
+
? `~/.claude/commands/dgmo.md already exists. Overwrite? [y/N] `
|
|
389
|
+
: `Install dgmo Claude Code skill to ~/.claude/commands/dgmo.md? [Y/n] `;
|
|
390
|
+
await new Promise<void>((done) => {
|
|
391
|
+
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
392
|
+
rl.question(prompt, (answer) => {
|
|
393
|
+
rl.close();
|
|
394
|
+
const yes = alreadyExists
|
|
395
|
+
? answer.toLowerCase() === 'y' || answer.toLowerCase() === 'yes'
|
|
396
|
+
: answer === '' || answer.toLowerCase() === 'y' || answer.toLowerCase() === 'yes';
|
|
397
|
+
if (!yes) {
|
|
398
|
+
console.error('Aborted.');
|
|
399
|
+
process.exit(0);
|
|
400
|
+
}
|
|
401
|
+
done();
|
|
402
|
+
});
|
|
403
|
+
});
|
|
404
|
+
if (!existsSync(commandsDir)) {
|
|
405
|
+
mkdirSync(commandsDir, { recursive: true });
|
|
406
|
+
}
|
|
407
|
+
writeFileSync(destPath, CLAUDE_SKILL_CONTENT, 'utf-8');
|
|
408
|
+
console.log(`Installed: ${destPath}`);
|
|
409
|
+
console.log('Use /dgmo in Claude Code to activate the skill.');
|
|
410
|
+
return;
|
|
411
|
+
}
|
|
412
|
+
|
|
295
413
|
// Determine input source
|
|
296
414
|
let content: string;
|
|
297
415
|
let inputBasename: string | undefined;
|
|
@@ -440,34 +558,6 @@ async function main(): Promise<void> {
|
|
|
440
558
|
}
|
|
441
559
|
}
|
|
442
560
|
|
|
443
|
-
// List scenarios (infra diagrams)
|
|
444
|
-
if (opts.listScenarios) {
|
|
445
|
-
const { parseInfra } = await import('./infra/parser');
|
|
446
|
-
const infraParsed = parseInfra(content);
|
|
447
|
-
if (infraParsed.scenarios.length === 0) {
|
|
448
|
-
console.log('(no scenarios defined)');
|
|
449
|
-
} else {
|
|
450
|
-
for (const s of infraParsed.scenarios) {
|
|
451
|
-
console.log(s.name);
|
|
452
|
-
}
|
|
453
|
-
}
|
|
454
|
-
return;
|
|
455
|
-
}
|
|
456
|
-
|
|
457
|
-
// Validate --scenario name against defined scenarios
|
|
458
|
-
if (opts.scenario) {
|
|
459
|
-
const { parseInfra } = await import('./infra/parser');
|
|
460
|
-
const infraParsed = parseInfra(content);
|
|
461
|
-
const match = infraParsed.scenarios.find((s) => s.name.toLowerCase() === opts.scenario!.toLowerCase());
|
|
462
|
-
if (!match) {
|
|
463
|
-
const available = infraParsed.scenarios.map((s) => s.name);
|
|
464
|
-
const msg = available.length > 0
|
|
465
|
-
? `Error: Unknown scenario "${opts.scenario}". Available: ${available.join(', ')}`
|
|
466
|
-
: `Error: Unknown scenario "${opts.scenario}". No scenarios defined in this file.`;
|
|
467
|
-
exitWithJsonError(msg);
|
|
468
|
-
}
|
|
469
|
-
}
|
|
470
|
-
|
|
471
561
|
// Validate C4 options
|
|
472
562
|
if (opts.c4Level === 'containers' && !opts.c4System) {
|
|
473
563
|
exitWithJsonError('Error: --c4-system is required when --c4-level is containers');
|
|
@@ -488,7 +578,7 @@ async function main(): Promise<void> {
|
|
|
488
578
|
c4Level: opts.c4Level,
|
|
489
579
|
c4System: opts.c4System,
|
|
490
580
|
c4Container: opts.c4Container,
|
|
491
|
-
|
|
581
|
+
tagGroup: opts.tagGroup,
|
|
492
582
|
});
|
|
493
583
|
|
|
494
584
|
if (!svg) {
|