@diagrammo/dgmo 0.6.1 → 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 +160 -159
- package/dist/index.cjs +780 -147
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +4 -1
- package/dist/index.d.ts +4 -1
- package/dist/index.js +780 -147
- package/dist/index.js.map +1 -1
- package/docs/ai-integration.md +33 -50
- package/package.json +4 -3
- package/src/c4/layout.ts +68 -5
- package/src/cli.ts +124 -2
- package/src/er/classify.ts +206 -0
- package/src/er/layout.ts +259 -94
- package/src/er/renderer.ts +231 -17
- package/src/infra/layout.ts +60 -13
- package/src/infra/renderer.ts +375 -32
- package/src/initiative-status/layout.ts +46 -30
- 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/docs/ai-integration.md
CHANGED
|
@@ -1,14 +1,16 @@
|
|
|
1
1
|
# DGMO AI Integration Guide
|
|
2
2
|
|
|
3
|
-
Use AI coding tools to generate `.dgmo` diagrams. This guide covers Claude Code, Copilot, Cursor, and
|
|
3
|
+
Use AI coding tools to generate `.dgmo` diagrams. This guide covers Claude Code, Copilot, Cursor, Windsurf, and any tool with an MCP client.
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
---
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
## MCP Server (recommended for Claude)
|
|
8
8
|
|
|
9
|
-
|
|
9
|
+
`@diagrammo/dgmo-mcp` provides an MCP server that gives Claude the ability to render, share, and look up DGMO diagrams directly — no file management needed.
|
|
10
10
|
|
|
11
|
-
|
|
11
|
+
**5 tools:** `render_diagram`, `share_diagram`, `open_in_app`, `list_chart_types`, `get_language_reference`
|
|
12
|
+
|
|
13
|
+
Add to `~/.claude/settings.json` (global) or `.claude/settings.local.json` (project):
|
|
12
14
|
|
|
13
15
|
```json
|
|
14
16
|
{
|
|
@@ -23,60 +25,46 @@ Setup (Claude Code — add to `.claude/settings.local.json`):
|
|
|
23
25
|
|
|
24
26
|
See `dgmo-mcp/README.md` for full configuration options.
|
|
25
27
|
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
Copy the `.claude/skills/dgmo-*` directories from this repo into your project's `.claude/skills/` directory. This gives you four slash commands:
|
|
28
|
+
---
|
|
29
29
|
|
|
30
|
-
|
|
31
|
-
|---------|-------------|
|
|
32
|
-
| `/dgmo-generate <description>` | Picks the best diagram type automatically |
|
|
33
|
-
| `/dgmo-sequence <flow>` | Generates a sequence diagram |
|
|
34
|
-
| `/dgmo-flowchart <process>` | Generates a flowchart |
|
|
35
|
-
| `/dgmo-chart <data description>` | Generates a data chart |
|
|
30
|
+
## Claude Code — Skill (slash command)
|
|
36
31
|
|
|
37
|
-
|
|
32
|
+
Installs a `/dgmo` slash command that gives Claude full dgmo context — all chart types, CLI flags, workflow, and tips.
|
|
38
33
|
|
|
39
34
|
```bash
|
|
40
|
-
|
|
41
|
-
cp -r node_modules/@diagrammo/dgmo/.claude/skills/dgmo-* .claude/skills/
|
|
42
|
-
|
|
43
|
-
# Or if dgmo is installed globally
|
|
44
|
-
cp -r $(npm root -g)/@diagrammo/dgmo/.claude/skills/dgmo-* .claude/skills/
|
|
35
|
+
dgmo --install-claude-skill
|
|
45
36
|
```
|
|
46
37
|
|
|
47
|
-
|
|
38
|
+
This copies a skill file into `~/.claude/commands/`, making `/dgmo` available in every Claude Code session.
|
|
48
39
|
|
|
49
|
-
|
|
50
|
-
/dgmo-generate an ER diagram for a blog with users, posts, and comments
|
|
51
|
-
/dgmo-sequence the OAuth2 authorization code flow
|
|
52
|
-
/dgmo-flowchart CI/CD pipeline with build, test, and deploy stages
|
|
53
|
-
/dgmo-chart quarterly revenue: Q1 100, Q2 120, Q3 110, Q4 130
|
|
54
|
-
```
|
|
40
|
+
---
|
|
55
41
|
|
|
56
42
|
## Claude Code — CLAUDE.md snippet
|
|
57
43
|
|
|
58
|
-
|
|
44
|
+
To teach Claude about DGMO in a specific project without the global skill, add this to your `CLAUDE.md`:
|
|
59
45
|
|
|
60
46
|
```markdown
|
|
61
47
|
## DGMO Diagrams
|
|
62
48
|
|
|
63
|
-
When the user asks for a diagram, generate a `.dgmo` file.
|
|
49
|
+
When the user asks for a diagram, generate a `.dgmo` file.
|
|
64
50
|
|
|
65
51
|
Quick reference:
|
|
66
|
-
- Sequence: `A -> B
|
|
52
|
+
- Sequence: `A -message-> B` or `A <-response- B`
|
|
67
53
|
- Flowchart: `(Start) -> [Process] -> <Decision?> -yes-> (End)`
|
|
68
54
|
- Bar chart: `chart: bar` then `Label: value` lines
|
|
69
55
|
- ER diagram: `chart: er` then table definitions and `table1 1--* table2` relationships
|
|
70
56
|
- Org chart: `chart: org` then indented hierarchy
|
|
71
57
|
|
|
72
|
-
Full reference:
|
|
58
|
+
Full reference: `node_modules/@diagrammo/dgmo/docs/language-reference.md`
|
|
73
59
|
|
|
74
|
-
Render with: `dgmo file.dgmo
|
|
60
|
+
Render with: `dgmo file.dgmo` (PNG) or `dgmo file.dgmo -o url` (shareable link).
|
|
75
61
|
```
|
|
76
62
|
|
|
63
|
+
---
|
|
64
|
+
|
|
77
65
|
## Other AI Tools — Prompt Files
|
|
78
66
|
|
|
79
|
-
DGMO ships
|
|
67
|
+
DGMO ships context files for popular AI coding tools, included in the npm package and auto-loaded when present in a project root.
|
|
80
68
|
|
|
81
69
|
| File | Tool | How it works |
|
|
82
70
|
|------|------|-------------|
|
|
@@ -84,42 +72,37 @@ DGMO ships prompt files for popular AI coding tools. These are included in the n
|
|
|
84
72
|
| `.cursorrules` | Cursor | Auto-loaded when present in project root |
|
|
85
73
|
| `.windsurfrules` | Windsurf | Auto-loaded when present in project root |
|
|
86
74
|
|
|
87
|
-
### Setup
|
|
88
|
-
|
|
89
75
|
Copy the relevant file into your project root:
|
|
90
76
|
|
|
91
77
|
```bash
|
|
92
|
-
# From node_modules
|
|
78
|
+
# From node_modules (if installed as a dependency)
|
|
93
79
|
cp node_modules/@diagrammo/dgmo/.cursorrules .
|
|
94
80
|
cp node_modules/@diagrammo/dgmo/.windsurfrules .
|
|
95
81
|
mkdir -p .github && cp node_modules/@diagrammo/dgmo/.github/copilot-instructions.md .github/
|
|
96
82
|
|
|
97
|
-
#
|
|
83
|
+
# From global npm install
|
|
98
84
|
cp $(npm root -g)/@diagrammo/dgmo/.cursorrules .
|
|
99
85
|
```
|
|
100
86
|
|
|
101
|
-
Each file contains a condensed DGMO syntax reference with examples
|
|
87
|
+
Each file contains a condensed DGMO syntax reference with examples, all chart types listed, rendering commands, and common mistakes to avoid.
|
|
102
88
|
|
|
103
|
-
|
|
89
|
+
---
|
|
104
90
|
|
|
105
|
-
|
|
91
|
+
## Rendering commands
|
|
106
92
|
|
|
107
93
|
```bash
|
|
108
|
-
# Install
|
|
109
|
-
npm install -g @diagrammo/dgmo # or: brew install diagrammo/dgmo/dgmo
|
|
110
|
-
|
|
111
|
-
# Render
|
|
112
94
|
dgmo diagram.dgmo # PNG output
|
|
113
95
|
dgmo diagram.dgmo -o output.svg # SVG output
|
|
114
|
-
dgmo diagram.dgmo -o url # Shareable URL
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
dgmo
|
|
118
|
-
dgmo --chart-types --json # List all chart types
|
|
96
|
+
dgmo diagram.dgmo -o url # Shareable diagrammo.app URL
|
|
97
|
+
dgmo diagram.dgmo -o url --copy # URL copied to clipboard
|
|
98
|
+
dgmo --chart-types # List all supported chart types
|
|
99
|
+
dgmo --chart-types --json # Machine-readable chart type list
|
|
119
100
|
```
|
|
120
101
|
|
|
102
|
+
---
|
|
103
|
+
|
|
121
104
|
## Supported chart types
|
|
122
105
|
|
|
123
106
|
Run `dgmo --chart-types` for the full list, or see `docs/language-reference.md`.
|
|
124
107
|
|
|
125
|
-
|
|
108
|
+
29 types: bar, line, area, multi-line, pie, doughnut, radar, polar-area, bar-stacked, scatter, sankey, chord, function, heatmap, funnel, slope, wordcloud, arc, timeline, venn, quadrant, sequence, flowchart, class, er, org, kanban, c4, initiative-status, infra.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@diagrammo/dgmo",
|
|
3
|
-
"version": "0.6.
|
|
3
|
+
"version": "0.6.2",
|
|
4
4
|
"description": "DGMO diagram markup language — parser, renderer, and color system",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -26,7 +26,7 @@
|
|
|
26
26
|
"dist",
|
|
27
27
|
"src",
|
|
28
28
|
"docs",
|
|
29
|
-
".claude/
|
|
29
|
+
".claude/commands",
|
|
30
30
|
".github/copilot-instructions.md",
|
|
31
31
|
".cursorrules",
|
|
32
32
|
".windsurfrules"
|
|
@@ -39,7 +39,8 @@
|
|
|
39
39
|
"test": "vitest run",
|
|
40
40
|
"test:watch": "vitest",
|
|
41
41
|
"gallery": "pnpm build && node scripts/generate-gallery.mjs",
|
|
42
|
-
"check:duplication": "jscpd ./src"
|
|
42
|
+
"check:duplication": "jscpd ./src",
|
|
43
|
+
"postinstall": "node -e \"console.log('\\n💡 Claude Code user? Run: dgmo --install-claude-skill\\n')\""
|
|
43
44
|
},
|
|
44
45
|
"dependencies": {
|
|
45
46
|
"@dagrejs/dagre": "^2.0.4",
|
package/src/c4/layout.ts
CHANGED
|
@@ -109,17 +109,31 @@ const LEGEND_CAPSULE_PAD = 4;
|
|
|
109
109
|
// Post-Layout Crossing Reduction
|
|
110
110
|
// ============================================================
|
|
111
111
|
|
|
112
|
+
interface NodeGeometry {
|
|
113
|
+
y: number;
|
|
114
|
+
width: number;
|
|
115
|
+
height: number;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// Large penalty per edge-node collision — dominates the distance term so the
|
|
119
|
+
// sifter strongly prefers orderings where edges don't pass through other nodes.
|
|
120
|
+
const EDGE_NODE_COLLISION_WEIGHT = 5000;
|
|
121
|
+
|
|
112
122
|
/**
|
|
113
123
|
* Compute penalty for an edge ordering. Uses degree-weighted edge distance:
|
|
114
124
|
* long edges to high-degree nodes are penalized more than to low-degree nodes.
|
|
115
125
|
* This places shared/important nodes closer to their neighbors, reducing
|
|
116
126
|
* visual edge congestion.
|
|
117
127
|
*
|
|
128
|
+
* When nodeGeometry is provided, also adds a heavy penalty for each case where
|
|
129
|
+
* a straight-line edge bounding box overlaps another node — driving the sifter
|
|
130
|
+
* to prefer orderings that avoid edge-node collisions.
|
|
118
131
|
*/
|
|
119
132
|
function computeEdgePenalty(
|
|
120
133
|
edgeList: { source: string; target: string }[],
|
|
121
134
|
nodePositions: Map<string, number>,
|
|
122
|
-
degrees: Map<string, number
|
|
135
|
+
degrees: Map<string, number>,
|
|
136
|
+
nodeGeometry?: Map<string, NodeGeometry>
|
|
123
137
|
): number {
|
|
124
138
|
let penalty = 0;
|
|
125
139
|
|
|
@@ -135,6 +149,48 @@ function computeEdgePenalty(
|
|
|
135
149
|
penalty += dist * weight;
|
|
136
150
|
}
|
|
137
151
|
|
|
152
|
+
// Edge-node collision penalty: for each edge A→B, check if any other node C
|
|
153
|
+
// has its bounding box inside the straight-line bounding box of the edge.
|
|
154
|
+
// Uses x from nodePositions (updated per permutation) and y/size from nodeGeometry.
|
|
155
|
+
if (nodeGeometry) {
|
|
156
|
+
for (const edge of edgeList) {
|
|
157
|
+
const geomA = nodeGeometry.get(edge.source);
|
|
158
|
+
const geomB = nodeGeometry.get(edge.target);
|
|
159
|
+
if (!geomA || !geomB) continue;
|
|
160
|
+
|
|
161
|
+
const ax = nodePositions.get(edge.source) ?? 0;
|
|
162
|
+
const bx = nodePositions.get(edge.target) ?? 0;
|
|
163
|
+
const ay = geomA.y;
|
|
164
|
+
const by = geomB.y;
|
|
165
|
+
|
|
166
|
+
// Skip edges within the same rank — they have no vertical span to check
|
|
167
|
+
if (ay === by) continue;
|
|
168
|
+
|
|
169
|
+
const edgeMinX = Math.min(ax, bx);
|
|
170
|
+
const edgeMaxX = Math.max(ax, bx);
|
|
171
|
+
const edgeMinY = Math.min(ay, by);
|
|
172
|
+
const edgeMaxY = Math.max(ay, by);
|
|
173
|
+
|
|
174
|
+
for (const [name, geomC] of nodeGeometry) {
|
|
175
|
+
if (name === edge.source || name === edge.target) continue;
|
|
176
|
+
const cx = nodePositions.get(name) ?? 0;
|
|
177
|
+
const cy = geomC.y;
|
|
178
|
+
const hw = geomC.width / 2;
|
|
179
|
+
const hh = geomC.height / 2;
|
|
180
|
+
|
|
181
|
+
// AABB overlap: node C's box intersects the edge's straight-line bounding box
|
|
182
|
+
if (
|
|
183
|
+
cx + hw > edgeMinX &&
|
|
184
|
+
cx - hw < edgeMaxX &&
|
|
185
|
+
cy + hh > edgeMinY &&
|
|
186
|
+
cy - hh < edgeMaxY
|
|
187
|
+
) {
|
|
188
|
+
penalty += EDGE_NODE_COLLISION_WEIGHT;
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
138
194
|
return penalty;
|
|
139
195
|
}
|
|
140
196
|
|
|
@@ -160,6 +216,13 @@ function reduceCrossings(
|
|
|
160
216
|
degrees.set(edge.target, (degrees.get(edge.target) ?? 0) + 1);
|
|
161
217
|
}
|
|
162
218
|
|
|
219
|
+
// Build geometry map for edge-node collision scoring
|
|
220
|
+
const nodeGeometry = new Map<string, NodeGeometry>();
|
|
221
|
+
for (const name of g.nodes()) {
|
|
222
|
+
const pos = g.node(name);
|
|
223
|
+
if (pos) nodeGeometry.set(name, { y: pos.y, width: pos.width, height: pos.height });
|
|
224
|
+
}
|
|
225
|
+
|
|
163
226
|
// Group nodes by rank
|
|
164
227
|
const rankMap = new Map<number, string[]>();
|
|
165
228
|
for (const name of g.nodes()) {
|
|
@@ -216,7 +279,7 @@ function reduceCrossings(
|
|
|
216
279
|
}
|
|
217
280
|
|
|
218
281
|
// Current penalty
|
|
219
|
-
const currentPenalty = computeEdgePenalty(edgeList, basePositions, degrees);
|
|
282
|
+
const currentPenalty = computeEdgePenalty(edgeList, basePositions, degrees, nodeGeometry);
|
|
220
283
|
|
|
221
284
|
// Try permutations (feasible for partition sizes ≤ 8)
|
|
222
285
|
let bestPerm = [...partition];
|
|
@@ -229,7 +292,7 @@ function reduceCrossings(
|
|
|
229
292
|
for (let i = 0; i < perm.length; i++) {
|
|
230
293
|
testPositions.set(perm[i]!, xSlots[i]!);
|
|
231
294
|
}
|
|
232
|
-
const penalty = computeEdgePenalty(edgeList, testPositions, degrees);
|
|
295
|
+
const penalty = computeEdgePenalty(edgeList, testPositions, degrees, nodeGeometry);
|
|
233
296
|
if (penalty < bestPenalty) {
|
|
234
297
|
bestPenalty = penalty;
|
|
235
298
|
bestPerm = [...perm];
|
|
@@ -248,14 +311,14 @@ function reduceCrossings(
|
|
|
248
311
|
for (let k = 0; k < workingOrder.length; k++) {
|
|
249
312
|
testPositions.set(workingOrder[k]!, xSlots[k]!);
|
|
250
313
|
}
|
|
251
|
-
const before = computeEdgePenalty(edgeList, testPositions, degrees);
|
|
314
|
+
const before = computeEdgePenalty(edgeList, testPositions, degrees, nodeGeometry);
|
|
252
315
|
|
|
253
316
|
[workingOrder[i], workingOrder[i + 1]] = [workingOrder[i + 1]!, workingOrder[i]!];
|
|
254
317
|
const testPositions2 = new Map(basePositions);
|
|
255
318
|
for (let k = 0; k < workingOrder.length; k++) {
|
|
256
319
|
testPositions2.set(workingOrder[k]!, xSlots[k]!);
|
|
257
320
|
}
|
|
258
|
-
const after = computeEdgePenalty(edgeList, testPositions2, degrees);
|
|
321
|
+
const after = computeEdgePenalty(edgeList, testPositions2, degrees, nodeGeometry);
|
|
259
322
|
|
|
260
323
|
if (after < before) {
|
|
261
324
|
improved = true;
|
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]
|
|
@@ -78,6 +158,7 @@ Options:
|
|
|
78
158
|
--copy Copy URL to clipboard (only with -o url)
|
|
79
159
|
--json Output structured JSON to stdout
|
|
80
160
|
--chart-types List all supported chart types
|
|
161
|
+
--install-claude-skill Install the dgmo Claude Code skill to ~/.claude/commands/
|
|
81
162
|
--help Show this help
|
|
82
163
|
--version Show version`);
|
|
83
164
|
}
|
|
@@ -100,6 +181,7 @@ function parseArgs(argv: string[]): {
|
|
|
100
181
|
copy: boolean;
|
|
101
182
|
json: boolean;
|
|
102
183
|
chartTypes: boolean;
|
|
184
|
+
installClaudeSkill: boolean;
|
|
103
185
|
c4Level: 'context' | 'containers' | 'components' | 'deployment';
|
|
104
186
|
c4System: string | undefined;
|
|
105
187
|
c4Container: string | undefined;
|
|
@@ -116,6 +198,7 @@ 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,
|
|
@@ -185,6 +268,9 @@ function parseArgs(argv: string[]): {
|
|
|
185
268
|
} else if (arg === '--chart-types') {
|
|
186
269
|
result.chartTypes = true;
|
|
187
270
|
i++;
|
|
271
|
+
} else if (arg === '--install-claude-skill') {
|
|
272
|
+
result.installClaudeSkill = true;
|
|
273
|
+
i++;
|
|
188
274
|
} else if (arg === '--copy') {
|
|
189
275
|
result.copy = true;
|
|
190
276
|
i++;
|
|
@@ -288,6 +374,42 @@ async function main(): Promise<void> {
|
|
|
288
374
|
return;
|
|
289
375
|
}
|
|
290
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
|
+
|
|
291
413
|
// Determine input source
|
|
292
414
|
let content: string;
|
|
293
415
|
let inputBasename: string | undefined;
|
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
// ============================================================
|
|
2
|
+
// ER Diagram Entity Classification
|
|
3
|
+
// ============================================================
|
|
4
|
+
//
|
|
5
|
+
// Classifies each ER entity into a structural role using heuristics
|
|
6
|
+
// derived from column constraints, FK patterns, relationship in-degree,
|
|
7
|
+
// and naming conventions.
|
|
8
|
+
//
|
|
9
|
+
// Only used when no explicit colors or tag groups are defined.
|
|
10
|
+
|
|
11
|
+
import type { ERTable, ERRelationship } from './types';
|
|
12
|
+
import type { PaletteColors } from '../palettes/types';
|
|
13
|
+
|
|
14
|
+
export type EntityRole =
|
|
15
|
+
| 'core'
|
|
16
|
+
| 'dependent'
|
|
17
|
+
| 'junction'
|
|
18
|
+
| 'ambiguous'
|
|
19
|
+
| 'lookup'
|
|
20
|
+
| 'hub'
|
|
21
|
+
| 'self-referential'
|
|
22
|
+
| 'unclassified';
|
|
23
|
+
|
|
24
|
+
/** Maps each role to a key in PaletteColors['colors'] */
|
|
25
|
+
export const ROLE_COLORS: Record<EntityRole, keyof PaletteColors['colors']> = {
|
|
26
|
+
core: 'green',
|
|
27
|
+
dependent: 'blue',
|
|
28
|
+
junction: 'red',
|
|
29
|
+
ambiguous: 'purple',
|
|
30
|
+
lookup: 'yellow',
|
|
31
|
+
hub: 'orange',
|
|
32
|
+
'self-referential': 'teal',
|
|
33
|
+
unclassified: 'gray',
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
/** Human-readable legend labels per role */
|
|
37
|
+
export const ROLE_LABELS: Record<EntityRole, string> = {
|
|
38
|
+
core: 'Core entity',
|
|
39
|
+
dependent: 'Dependent',
|
|
40
|
+
junction: 'Junction / M:M',
|
|
41
|
+
ambiguous: 'Bridge',
|
|
42
|
+
lookup: 'Lookup / Reference',
|
|
43
|
+
hub: 'Hub',
|
|
44
|
+
'self-referential': 'Self-referential',
|
|
45
|
+
unclassified: 'Unclassified',
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
/** Stable display order for the semantic legend */
|
|
49
|
+
export const ROLE_ORDER: EntityRole[] = [
|
|
50
|
+
'core',
|
|
51
|
+
'dependent',
|
|
52
|
+
'junction',
|
|
53
|
+
'ambiguous',
|
|
54
|
+
'lookup',
|
|
55
|
+
'hub',
|
|
56
|
+
'self-referential',
|
|
57
|
+
'unclassified',
|
|
58
|
+
];
|
|
59
|
+
|
|
60
|
+
const LOOKUP_NAME_SUFFIXES = ['_type', '_status', '_code', '_category'];
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Classify each ER entity into a structural role.
|
|
64
|
+
*
|
|
65
|
+
* Returns a Map of tableId → EntityRole.
|
|
66
|
+
* Pure logic — no palette or DOM dependencies.
|
|
67
|
+
*/
|
|
68
|
+
export function classifyEREntities(
|
|
69
|
+
tables: ERTable[],
|
|
70
|
+
relationships: ERRelationship[]
|
|
71
|
+
): Map<string, EntityRole> {
|
|
72
|
+
const result = new Map<string, EntityRole>();
|
|
73
|
+
if (tables.length === 0) return result;
|
|
74
|
+
|
|
75
|
+
// ── Pre-compute graph metrics ─────────────────────────────────────────────
|
|
76
|
+
|
|
77
|
+
// indegreeMap: how many other tables have FK references pointing to this table.
|
|
78
|
+
// A table's indegree increments when it is on the exclusive '1' side of a 1:* or 1:?
|
|
79
|
+
// relationship. 1:1 relationships are skipped on both sides to avoid double-counting.
|
|
80
|
+
const indegreeMap: Record<string, number> = {};
|
|
81
|
+
for (const t of tables) indegreeMap[t.id] = 0;
|
|
82
|
+
for (const rel of relationships) {
|
|
83
|
+
if (rel.source === rel.target) continue; // skip self-loops
|
|
84
|
+
if (rel.cardinality.from === '1' && rel.cardinality.to !== '1') {
|
|
85
|
+
indegreeMap[rel.source] = (indegreeMap[rel.source] ?? 0) + 1;
|
|
86
|
+
}
|
|
87
|
+
if (rel.cardinality.to === '1' && rel.cardinality.from !== '1') {
|
|
88
|
+
indegreeMap[rel.target] = (indegreeMap[rel.target] ?? 0) + 1;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// mmParticipants: tables with '*' cardinality connecting to 2+ distinct other tables.
|
|
93
|
+
// Catches wide junction tables (e.g. with surrogate PKs) that the FK-ratio alone misses.
|
|
94
|
+
const tableStarNeighbors = new Map<string, Set<string>>();
|
|
95
|
+
for (const rel of relationships) {
|
|
96
|
+
if (rel.source === rel.target) continue;
|
|
97
|
+
if (rel.cardinality.from === '*') {
|
|
98
|
+
if (!tableStarNeighbors.has(rel.source)) tableStarNeighbors.set(rel.source, new Set());
|
|
99
|
+
tableStarNeighbors.get(rel.source)!.add(rel.target);
|
|
100
|
+
}
|
|
101
|
+
if (rel.cardinality.to === '*') {
|
|
102
|
+
if (!tableStarNeighbors.has(rel.target)) tableStarNeighbors.set(rel.target, new Set());
|
|
103
|
+
tableStarNeighbors.get(rel.target)!.add(rel.source);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
const mmParticipants = new Set<string>();
|
|
107
|
+
for (const [id, neighbors] of tableStarNeighbors) {
|
|
108
|
+
if (neighbors.size >= 2) mmParticipants.add(id);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// Hub outlier statistics
|
|
112
|
+
const indegreeValues = Object.values(indegreeMap);
|
|
113
|
+
const mean = indegreeValues.reduce((a, b) => a + b, 0) / indegreeValues.length;
|
|
114
|
+
const variance = indegreeValues.reduce((a, b) => a + (b - mean) ** 2, 0) / indegreeValues.length;
|
|
115
|
+
const stddev = Math.sqrt(variance);
|
|
116
|
+
|
|
117
|
+
// Median indegree (for lookup detection: table must be referenced above-median)
|
|
118
|
+
const sorted = [...indegreeValues].sort((a, b) => a - b);
|
|
119
|
+
const median =
|
|
120
|
+
sorted.length % 2 === 0
|
|
121
|
+
? (sorted[sorted.length / 2 - 1] + sorted[sorted.length / 2]) / 2
|
|
122
|
+
: sorted[Math.floor(sorted.length / 2)];
|
|
123
|
+
|
|
124
|
+
// ── Per-table classification ──────────────────────────────────────────────
|
|
125
|
+
|
|
126
|
+
for (const table of tables) {
|
|
127
|
+
const id = table.id;
|
|
128
|
+
const cols = table.columns;
|
|
129
|
+
const fkCols = cols.filter((c) => c.constraints.includes('fk'));
|
|
130
|
+
const pkFkCols = cols.filter(
|
|
131
|
+
(c) => c.constraints.includes('pk') && c.constraints.includes('fk')
|
|
132
|
+
);
|
|
133
|
+
const fkCount = fkCols.length;
|
|
134
|
+
const fkRatio = cols.length === 0 ? 0 : fkCount / cols.length;
|
|
135
|
+
const indegree = indegreeMap[id] ?? 0;
|
|
136
|
+
const nameLower = table.name.toLowerCase();
|
|
137
|
+
|
|
138
|
+
// External relationships (exclude self-loops)
|
|
139
|
+
const externalRels = relationships.filter(
|
|
140
|
+
(r) => (r.source === id || r.target === id) && r.source !== r.target
|
|
141
|
+
);
|
|
142
|
+
const hasSelfRef = relationships.some((r) => r.source === id && r.target === id);
|
|
143
|
+
|
|
144
|
+
// Distinct external tables this table relates to
|
|
145
|
+
const externalTargets = new Set<string>();
|
|
146
|
+
for (const rel of externalRels) {
|
|
147
|
+
externalTargets.add(rel.source === id ? rel.target : rel.source);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// ── Decision tree (priority order) ─────────────────────────────────────
|
|
151
|
+
|
|
152
|
+
// 1. Self-referential: has a self-loop relationship AND no external relationships
|
|
153
|
+
if (hasSelfRef && externalRels.length === 0) {
|
|
154
|
+
result.set(id, 'self-referential');
|
|
155
|
+
continue;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// 2. Junction: any one of three signals qualifies.
|
|
159
|
+
// Inheritance exception: composite PK FKs all pointing to a single parent → skip ratio signal.
|
|
160
|
+
const isInheritancePattern = pkFkCols.length >= 2 && externalTargets.size === 1;
|
|
161
|
+
const junctionByRatio = fkRatio >= 0.6 && !isInheritancePattern;
|
|
162
|
+
const junctionByCompositePk = pkFkCols.length >= 2 && externalTargets.size >= 2;
|
|
163
|
+
const junctionByMm = mmParticipants.has(id);
|
|
164
|
+
if (junctionByRatio || junctionByCompositePk || junctionByMm) {
|
|
165
|
+
result.set(id, 'junction');
|
|
166
|
+
continue;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// 3. Ambiguous: FK ratio 0.4–0.59, no composite PK FKs, not M:M participant
|
|
170
|
+
if (fkRatio >= 0.4 && fkRatio < 0.6 && pkFkCols.length < 2 && !mmParticipants.has(id)) {
|
|
171
|
+
result.set(id, 'ambiguous');
|
|
172
|
+
continue;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// 4. Lookup: naming convention match + structural guards
|
|
176
|
+
// Naming only applies when cols ≤ 6 AND fkCount ≤ 1; structure overrides for larger tables.
|
|
177
|
+
const nameMatchesLookup = LOOKUP_NAME_SUFFIXES.some((s) => nameLower.endsWith(s));
|
|
178
|
+
if (nameMatchesLookup && cols.length <= 6 && fkCount <= 1 && indegree > median) {
|
|
179
|
+
result.set(id, 'lookup');
|
|
180
|
+
continue;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
// 5. Hub: in-degree outlier in a schema of ≥ 6 tables.
|
|
184
|
+
// Dual condition: > mean + 1.5σ AND ≥ 2× mean (guards against near-zero mean edge cases).
|
|
185
|
+
if (
|
|
186
|
+
tables.length >= 6 &&
|
|
187
|
+
indegree > 0 &&
|
|
188
|
+
indegree > mean + 1.5 * stddev &&
|
|
189
|
+
indegree >= 2 * mean
|
|
190
|
+
) {
|
|
191
|
+
result.set(id, 'hub');
|
|
192
|
+
continue;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// 6. Dependent: has FK columns
|
|
196
|
+
if (fkCount > 0) {
|
|
197
|
+
result.set(id, 'dependent');
|
|
198
|
+
continue;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
// 7. Core: no FK columns (always true at this point)
|
|
202
|
+
result.set(id, 'core');
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
return result;
|
|
206
|
+
}
|