@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.
@@ -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 diagramH = layout.height;
243
- const availH = height - titleHeight;
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 (!exportDims) {
570
- for (const group of layout.legend) {
571
- const isActive =
572
- activeTagGroup != null &&
573
- group.name.toLowerCase() === (activeTagGroup ?? '').toLowerCase();
574
-
575
- if (activeTagGroup != null && !isActive) continue;
576
-
577
- const groupBg = isDark
578
- ? mix(palette.surface, palette.bg, 50)
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
- contentG: GSelection,
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
- for (const group of layout.legend) {
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 = pillLabel.length * LEGEND_PILL_FONT_W + LEGEND_PILL_PAD;
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 = contentG
1180
+ const gEl = parent
1213
1181
  .append('g')
1214
- .attr('transform', `translate(${group.x}, ${group.y})`)
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 diagramH = layout.height;
1321
- const availH = height - titleHeight;
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 (!exportDims) {
1721
- renderLegend(contentG as GSelection, layout, palette, isDark, activeTagGroup);
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 { resolve, basename, extname } from 'node:path';
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
- scenario: string | undefined;
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
- scenario: undefined as string | undefined,
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 === '--scenario') {
178
- result.scenario = args[++i];
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
- scenario: opts.scenario,
581
+ tagGroup: opts.tagGroup,
492
582
  });
493
583
 
494
584
  if (!svg) {