@itsthecsguy/linework 0.1.0 → 0.1.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/README.md +59 -12
- package/dist/api.js +39 -2
- package/dist/cli.js +13 -0
- package/dist/drawio/theme.js +39 -34
- package/dist/lint/check.js +4 -0
- package/dist/render/browser.js +29 -3
- package/package.json +1 -1
- package/skills/linework/SKILL.md +41 -22
- package/skills/linework/examples/checkout.yaml +4 -4
- package/skills/linework/examples/pay-sequence.yaml +2 -2
- package/skills/linework/examples/refund-flow.yaml +1 -1
- package/skills/linework/references/google-docs.md +5 -5
- package/skills/linework/references/spec.md +4 -4
- package/skills/linework/examples/billing-er.drawio.svg +0 -4
- package/skills/linework/examples/checkout.drawio.svg +0 -4
- package/skills/linework/examples/pay-sequence.drawio.svg +0 -4
- package/skills/linework/examples/payment-state.drawio.svg +0 -4
- package/skills/linework/examples/refund-flow.drawio.svg +0 -4
- package/skills/linework/examples/serverless-aws.drawio.svg +0 -4
package/README.md
CHANGED
|
@@ -1,4 +1,9 @@
|
|
|
1
|
-
|
|
1
|
+
<h1>
|
|
2
|
+
<picture>
|
|
3
|
+
<source media="(prefers-color-scheme: dark)" srcset="docs/brand/logo-dark.svg">
|
|
4
|
+
<img alt="Linework" src="docs/brand/logo-light.svg" height="64">
|
|
5
|
+
</picture>
|
|
6
|
+
</h1>
|
|
2
7
|
|
|
3
8
|
Software diagrams your AI agent writes and you can edit by hand, in Claude Code, Codex, Cursor, Antigravity, Copilot, Grok Build or any other agent.
|
|
4
9
|
|
|
@@ -11,28 +16,69 @@ Your agent writes a short YAML spec. Linework lays it out, styles it and writes
|
|
|
11
16
|
|
|
12
17
|
## Quick start
|
|
13
18
|
|
|
14
|
-
|
|
19
|
+
### 1. Check what you need
|
|
20
|
+
|
|
21
|
+
- **Node 22.20 or newer** for the skills installer in step 2. Linework itself runs on Node 20+. Check with `node -v`.
|
|
22
|
+
- **Chrome, Edge or another Chromium browser.** Linework uses it in the background to measure text and draw. If you have neither, run `npx -y @itsthecsguy/linework install-browser`.
|
|
23
|
+
- **Using Codex?** Its sandbox stops Chrome from starting. Run `npx -y @itsthecsguy/linework install-browser` once, outside Codex. Linework's own headless Chromium does start inside the sandbox.
|
|
24
|
+
|
|
25
|
+
### 2. Give your agent the skill
|
|
15
26
|
|
|
16
27
|
```bash
|
|
17
28
|
npx skills add itsthecsguy/linework
|
|
18
29
|
```
|
|
19
30
|
|
|
20
|
-
|
|
31
|
+
The [skills](https://github.com/vercel-labs/skills) installer finds the agents on your machine, asks which ones to set up, and puts the skill where each one looks. It covers Claude Code, Codex, Cursor, Antigravity, Gemini CLI, GitHub Copilot, Grok Build, OpenCode, Windsurf and about 70 more.
|
|
32
|
+
|
|
33
|
+
It installs for the current project by default. Add `-g` to install it for every project. To skip the prompts, name the agents:
|
|
34
|
+
|
|
35
|
+
```bash
|
|
36
|
+
npx skills add itsthecsguy/linework -g -a claude-code -a codex -y
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
### 3. Check that it works
|
|
40
|
+
|
|
41
|
+
This builds a two-box diagram without an agent:
|
|
42
|
+
|
|
43
|
+
```bash
|
|
44
|
+
printf 'type: flow\nnodes: { a: Hello, b: World }\nedges:\n - a -> b\n' | npx -y @itsthecsguy/linework build - -o hello.drawio.svg --png hello.png
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
It should print `check: 0 errors, 0 warnings` and write `hello.drawio.svg` and `hello.png`. The first run takes a few seconds longer while `npx` downloads the package. If it says no browser was found, run `npx -y @itsthecsguy/linework install-browser` and try again.
|
|
48
|
+
|
|
49
|
+
### 4. Ask for a diagram
|
|
21
50
|
|
|
22
|
-
|
|
51
|
+
Start a new agent session so it loads the skill, then ask:
|
|
23
52
|
|
|
24
53
|
> Add an architecture diagram of the checkout flow to docs/design.md.
|
|
25
54
|
|
|
26
|
-
The agent
|
|
55
|
+
The agent writes the spec, runs Linework, looks at the PNG preview, fixes what's off and embeds the `.drawio.svg` in the doc. You don't install anything else. The skill runs Linework through `npx`.
|
|
27
56
|
|
|
28
|
-
|
|
57
|
+
### Optional: install the command
|
|
29
58
|
|
|
30
|
-
|
|
59
|
+
The skill works without this, but a global install skips the `npx` download check on every run and puts `linework` on your PATH:
|
|
31
60
|
|
|
32
|
-
```
|
|
33
|
-
|
|
61
|
+
```bash
|
|
62
|
+
npm install -g @itsthecsguy/linework
|
|
34
63
|
```
|
|
35
64
|
|
|
65
|
+
### Updating
|
|
66
|
+
|
|
67
|
+
```bash
|
|
68
|
+
npx skills update linework
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
If you installed the command globally, update it with `npm install -g @itsthecsguy/linework@latest`.
|
|
72
|
+
|
|
73
|
+
### Other setups
|
|
74
|
+
|
|
75
|
+
- **Agent can't run shell commands?** Use the MCP server. See [Connect over MCP](#connect-over-mcp).
|
|
76
|
+
- **Agent doesn't load skills?** Add this line to your `AGENTS.md`, `CLAUDE.md` or `GEMINI.md`:
|
|
77
|
+
|
|
78
|
+
```markdown
|
|
79
|
+
For diagrams, use Linework: run `npx -y @itsthecsguy/linework skill` to get the guide's path, and read SKILL.md there before your first diagram.
|
|
80
|
+
```
|
|
81
|
+
|
|
36
82
|
## How it works
|
|
37
83
|
|
|
38
84
|
A spec is a few lines of YAML:
|
|
@@ -66,14 +112,14 @@ Set `theme:` in the spec.
|
|
|
66
112
|
| `soft` | slides. Borderless pastel cards, tinted by kind. |
|
|
67
113
|
| `dark` | dark-mode pages |
|
|
68
114
|
| `sketch` | early drafts. Hand-drawn lines and a handwriting font. |
|
|
69
|
-
| `blueprint` | technical deep dives.
|
|
70
|
-
| `c4` | C4-model diagrams |
|
|
115
|
+
| `blueprint` | technical deep dives. Blue ink on pale drafting paper, monospace. |
|
|
116
|
+
| `c4` | C4-model diagrams, in quiet blues and grays |
|
|
71
117
|
|
|
72
118
|
Color is for meaning. Nodes stay neutral by default, and errors and successes are red and green in every style. Mark the part the doc is about with `highlight: true` on a node or `==>` on an edge, and it's drawn in the style's accent color. Use `color: red` (or blue, violet, pink, orange, amber, green, teal, gray) for categories.
|
|
73
119
|
|
|
74
120
|
Linework sizes every box from the real width of its text in the font it renders with, wrapping long labels instead of widening boxes. Each SVG carries a subset of that font (about 10 to 15KB), so text fits the same way on GitHub, in a browser or in a doc. `check` flags labels that overflow, overlap a box, another label, another edge or a group border, and `build` moves edge labels until they're clear.
|
|
75
121
|
|
|
76
|
-
Linework needs Node 20+ and a Chromium-based browser, which it uses headless to measure text and render. It tries Playwright's Chromium, then Chrome, then Edge. If none is found, run `
|
|
122
|
+
Linework needs Node 20+ and a Chromium-based browser, which it uses headless to measure text and render. It tries Playwright's Chromium, then Chrome, then Edge. If none is found, run `linework install-browser`, or set `LINEWORK_CHROME` to a browser binary. If a browser is found but crashes on launch, an agent's sandbox is usually blocking it. Linework then retries Playwright's Chromium in single-process mode, which starts inside Codex's sandbox.
|
|
77
123
|
|
|
78
124
|
## Connect over MCP
|
|
79
125
|
|
|
@@ -148,6 +194,7 @@ Docs can't hold an SVG, so the diagram lives in Google Drive and draw.io's Googl
|
|
|
148
194
|
| `linework mcp [--root dir]` | MCP server over stdio |
|
|
149
195
|
| `linework kinds [type]` | node kinds per diagram type |
|
|
150
196
|
| `linework schema` | JSON Schema of the spec |
|
|
197
|
+
| `linework install-browser` | download the headless Chromium build this version uses |
|
|
151
198
|
|
|
152
199
|
## Status
|
|
153
200
|
|
package/dist/api.js
CHANGED
|
@@ -3,7 +3,7 @@ import fs from 'node:fs';
|
|
|
3
3
|
import path from 'node:path';
|
|
4
4
|
import { parseSpecText } from './spec/parse.js';
|
|
5
5
|
import { layout } from './layout/layout.js';
|
|
6
|
-
import { patchEdges, serialize, stampEdgeRoutes, toMxfile } from './drawio/write.js';
|
|
6
|
+
import { edgeCellId, patchEdges, serialize, stampEdgeRoutes, toMxfile } from './drawio/write.js';
|
|
7
7
|
import { absBox, fingerprint, readDrawio, specBase } from './drawio/read.js';
|
|
8
8
|
import { manualEdits, toDiagram, toYaml } from './drawio/show.js';
|
|
9
9
|
import { merge } from './drawio/merge.js';
|
|
@@ -96,7 +96,7 @@ export async function build(specText, opts) {
|
|
|
96
96
|
}
|
|
97
97
|
}
|
|
98
98
|
else
|
|
99
|
-
xml = toMxfile(diagram, l);
|
|
99
|
+
xml = await straightenDetours(b, toMxfile(diagram, l), detourEdges(diagram, l));
|
|
100
100
|
xml = (await fixLabels(b, xml)).xml;
|
|
101
101
|
xml = stampEdgeRoutes(xml);
|
|
102
102
|
return finish(b, xml, { mode, warnings, direction: mode === 'merge' ? (old.meta.aiviz_direction ?? l.direction) : l.direction, edits, old, stats });
|
|
@@ -117,6 +117,43 @@ export async function build(specText, opts) {
|
|
|
117
117
|
};
|
|
118
118
|
}
|
|
119
119
|
}
|
|
120
|
+
/**
|
|
121
|
+
* Edges whose route is far longer than the gap between their ends. Layered layout
|
|
122
|
+
* draws one edge of every cycle backwards, so the reply in a call-and-callback pair
|
|
123
|
+
* (api -> stripe, stripe -> api) can loop around a whole group.
|
|
124
|
+
*/
|
|
125
|
+
export function detourEdges(d, l) {
|
|
126
|
+
const out = [];
|
|
127
|
+
for (const e of d.edges) {
|
|
128
|
+
const r = l.edges.get(e.id), a = l.nodes.get(e.from), z = l.nodes.get(e.to);
|
|
129
|
+
if (!r || !a || !z || r.points.length < 2)
|
|
130
|
+
continue;
|
|
131
|
+
let len = 0;
|
|
132
|
+
for (let i = 0; i < r.points.length - 1; i++)
|
|
133
|
+
len += Math.abs(r.points[i + 1].x - r.points[i].x) + Math.abs(r.points[i + 1].y - r.points[i].y);
|
|
134
|
+
const gap = Math.max(0, Math.abs(a.x + a.w / 2 - (z.x + z.w / 2)) - (a.w + z.w) / 2) +
|
|
135
|
+
Math.max(0, Math.abs(a.y + a.h / 2 - (z.y + z.h / 2)) - (a.h + z.h) / 2);
|
|
136
|
+
if (len > 2 * gap + 240)
|
|
137
|
+
out.push(edgeCellId(e.id));
|
|
138
|
+
}
|
|
139
|
+
return out;
|
|
140
|
+
}
|
|
141
|
+
/**
|
|
142
|
+
* Reroute detours with libavoid. Keep the new routes when they're much shorter and
|
|
143
|
+
* cost at most a couple of crossings each: a short hop with a crossing reads better
|
|
144
|
+
* than a line wrapped around half the diagram.
|
|
145
|
+
*/
|
|
146
|
+
async function straightenDetours(b, xml, ids) {
|
|
147
|
+
if (!ids.length)
|
|
148
|
+
return xml;
|
|
149
|
+
const candidate = await rerouteInto(b, xml, ids);
|
|
150
|
+
const [before, after] = [await b.inspect(xml), await b.inspect(candidate)];
|
|
151
|
+
const length = (cells) => cells.filter((c) => ids.includes(c.id)).reduce((sum, c) => sum + (c.points ?? []).reduce((s, p, i, a) => (i ? s + Math.abs(p.x - a[i - 1].x) + Math.abs(p.y - a[i - 1].y) : 0), 0), 0);
|
|
152
|
+
const [c0, c1] = [check(before), check(after)];
|
|
153
|
+
const errors = (c) => c.findings.filter((f) => f.level === 'error').length;
|
|
154
|
+
const keep = errors(c1) <= errors(c0) && c1.stats.crossings <= c0.stats.crossings + 2 * ids.length && length(after) < 0.6 * length(before);
|
|
155
|
+
return keep ? candidate : xml;
|
|
156
|
+
}
|
|
120
157
|
async function rerouteInto(b, xml, edgeIds) {
|
|
121
158
|
return patchEdges(xml, await b.reroute(xml, edgeIds), edgeIds);
|
|
122
159
|
}
|
package/dist/cli.js
CHANGED
|
@@ -48,6 +48,10 @@ Usage:
|
|
|
48
48
|
List node kinds for each diagram type.
|
|
49
49
|
linework schema
|
|
50
50
|
Print the spec's JSON Schema.
|
|
51
|
+
|
|
52
|
+
linework install-browser
|
|
53
|
+
Download the headless Chromium build that matches this version. Needed only
|
|
54
|
+
when no Chrome or Edge is installed, or to run inside an agent's sandbox.
|
|
51
55
|
`;
|
|
52
56
|
async function main(argv) {
|
|
53
57
|
const [cmd, ...rest] = argv;
|
|
@@ -238,6 +242,15 @@ async function main(argv) {
|
|
|
238
242
|
case 'schema':
|
|
239
243
|
console.log(JSON.stringify(jsonSchema(), null, 2));
|
|
240
244
|
return;
|
|
245
|
+
case 'install-browser': {
|
|
246
|
+
const { browserInstallCommand } = await import('./render/browser.js');
|
|
247
|
+
const { spawnSync } = await import('node:child_process');
|
|
248
|
+
const [bin, ...args] = browserInstallCommand();
|
|
249
|
+
console.log('downloading headless Chromium for linework...');
|
|
250
|
+
const r = spawnSync(bin, args, { stdio: 'inherit' });
|
|
251
|
+
process.exitCode = r.status ?? 1;
|
|
252
|
+
return;
|
|
253
|
+
}
|
|
241
254
|
default:
|
|
242
255
|
fail(`unknown command "${cmd}"\n\n${USAGE}`);
|
|
243
256
|
}
|
package/dist/drawio/theme.js
CHANGED
|
@@ -31,7 +31,8 @@ const LIGHT_SW = {
|
|
|
31
31
|
pink: { tint: '#FDF2F8', line: '#F9A8D4', ink: '#9D174D', solid: '#DB2777', edge: '#DB2777' },
|
|
32
32
|
red: { tint: '#FEF2F2', line: '#FCA5A5', ink: '#991B1B', solid: '#DC2626', edge: '#DC2626' },
|
|
33
33
|
orange: { tint: '#FFF7ED', line: '#FDBA74', ink: '#9A3412', solid: '#C2410C', edge: '#EA580C' },
|
|
34
|
-
|
|
34
|
+
// Ochre, not orange: the two read as one color at line weight otherwise
|
|
35
|
+
amber: { tint: '#FFFBEB', line: '#FCD34D', ink: '#92400E', solid: '#B45309', edge: '#A16207' },
|
|
35
36
|
green: { tint: '#F0FDF4', line: '#86EFAC', ink: '#166534', solid: '#15803D', edge: '#16A34A' },
|
|
36
37
|
teal: { tint: '#F0FDFA', line: '#5EEAD4', ink: '#115E59', solid: '#0F766E', edge: '#0D9488' },
|
|
37
38
|
gray: { tint: '#F4F4F5', line: '#D4D4D8', ink: '#3F3F46', solid: '#52525B', edge: '#71717A' },
|
|
@@ -59,17 +60,17 @@ const DARK_SW = {
|
|
|
59
60
|
teal: { tint: '#042F2E', line: '#115E59', ink: '#99F6E4', solid: '#0F766E', edge: '#2DD4BF' },
|
|
60
61
|
gray: { tint: '#27272A', line: '#52525B', ink: '#E4E4E7', solid: '#52525B', edge: '#A1A1AA' },
|
|
61
62
|
};
|
|
62
|
-
// Blueprint:
|
|
63
|
+
// Blueprint: a drafting sheet. Blue ink on pale paper; color only changes the line.
|
|
63
64
|
const BP_LINE = {
|
|
64
|
-
blue: '#
|
|
65
|
-
violet: '#
|
|
66
|
-
pink: '#
|
|
67
|
-
red: '#
|
|
68
|
-
orange: '#
|
|
69
|
-
amber: '#
|
|
70
|
-
green: '#
|
|
71
|
-
teal: '#
|
|
72
|
-
gray: '#
|
|
65
|
+
blue: '#1D4ED8',
|
|
66
|
+
violet: '#6D28D9',
|
|
67
|
+
pink: '#BE185D',
|
|
68
|
+
red: '#B91C1C',
|
|
69
|
+
orange: '#C2410C',
|
|
70
|
+
amber: '#A16207',
|
|
71
|
+
green: '#15803D',
|
|
72
|
+
teal: '#0F766E',
|
|
73
|
+
gray: '#64748B',
|
|
73
74
|
};
|
|
74
75
|
function lightPaint(sw, neutral, text, muted, accent) {
|
|
75
76
|
return (color, highlight) => {
|
|
@@ -167,7 +168,7 @@ const SKETCH = {
|
|
|
167
168
|
edge: '#3F3F46',
|
|
168
169
|
edgeText: '#3F3F46',
|
|
169
170
|
edgeWidth: 1.5,
|
|
170
|
-
accent: '
|
|
171
|
+
accent: 'blue',
|
|
171
172
|
kindColors: true,
|
|
172
173
|
radius: 8,
|
|
173
174
|
group: { fill: 'none', stroke: '#71717A', dashed: true, text: '#3F3F46' },
|
|
@@ -179,8 +180,9 @@ const SKETCH = {
|
|
|
179
180
|
if (color === 'ink')
|
|
180
181
|
return { fill: SKETCH_INK, stroke: SKETCH_INK, text: '#FFFFFF', sub: '#5C5C66', strokeWidth: 1.5, extra: solid };
|
|
181
182
|
if (hl) {
|
|
182
|
-
const
|
|
183
|
-
|
|
183
|
+
const c = color === 'neutral' ? 'blue' : color;
|
|
184
|
+
// A marker pass: a stronger tint than any kind's, under the accent's line
|
|
185
|
+
return { fill: c === 'blue' ? '#B9C6FF' : LIGHT_SW[c].line, stroke: LIGHT_SW[c].solid, text: SKETCH_INK, sub: LIGHT_SW[c].ink, strokeWidth: 2.5, extra: solid };
|
|
184
186
|
}
|
|
185
187
|
if (color === 'neutral')
|
|
186
188
|
return { fill: '#FFFFFF', stroke: SKETCH_INK, text: SKETCH_INK, sub: '#5C5C66', strokeWidth: 1.5, extra: solid };
|
|
@@ -189,37 +191,38 @@ const SKETCH = {
|
|
|
189
191
|
edgePaint: (color, hl) => {
|
|
190
192
|
if (!color && !hl)
|
|
191
193
|
return { stroke: SKETCH.edge, text: SKETCH.edgeText, width: 1.5 };
|
|
192
|
-
const s = LIGHT_SW[color ?? '
|
|
194
|
+
const s = LIGHT_SW[color ?? 'blue'];
|
|
193
195
|
return { stroke: s.solid, text: s.ink, width: hl ? 2.5 : 1.5 };
|
|
194
196
|
},
|
|
195
197
|
extra: 'sketch=1;curveFitting=1;jiggle=2;',
|
|
196
198
|
};
|
|
197
|
-
const
|
|
198
|
-
const BP_INK = '#
|
|
199
|
-
const
|
|
199
|
+
const BP_PAPER = '#F6F8FC';
|
|
200
|
+
const BP_INK = '#1B2F55';
|
|
201
|
+
const BP_LINE_INK = '#3F5F96';
|
|
202
|
+
const BP_HI = '#1D4ED8';
|
|
200
203
|
const BLUEPRINT = {
|
|
201
204
|
name: 'blueprint',
|
|
202
205
|
type: MONO,
|
|
203
|
-
canvas:
|
|
206
|
+
canvas: BP_PAPER,
|
|
204
207
|
text: BP_INK,
|
|
205
|
-
muted: '#
|
|
206
|
-
edge: '#
|
|
207
|
-
edgeText: '#
|
|
208
|
+
muted: '#5A6F92',
|
|
209
|
+
edge: '#6F86AD',
|
|
210
|
+
edgeText: '#4A5F84',
|
|
208
211
|
edgeWidth: 1.25,
|
|
209
|
-
accent: '
|
|
212
|
+
accent: 'blue',
|
|
210
213
|
kindColors: false,
|
|
211
214
|
radius: 4,
|
|
212
|
-
group: { fill: 'none', stroke: '#
|
|
213
|
-
note: { fill: '#
|
|
214
|
-
well: '#
|
|
215
|
+
group: { fill: 'none', stroke: '#9DB0D0', dashed: true, text: '#5A6F92' },
|
|
216
|
+
note: { fill: '#EAF0FA', stroke: '#9DB0D0', text: BP_INK },
|
|
217
|
+
well: '#EAF0FA',
|
|
215
218
|
paint: (color, hl) => {
|
|
216
219
|
if (hl)
|
|
217
|
-
return { fill: BP_HI, stroke: BP_HI, text:
|
|
220
|
+
return { fill: BP_HI, stroke: BP_HI, text: '#FFFFFF', sub: 'rgba(255,255,255,0.8)', strokeWidth: 1 };
|
|
218
221
|
if (color === 'ink')
|
|
219
|
-
return { fill: BP_INK, stroke: BP_INK, text:
|
|
222
|
+
return { fill: BP_INK, stroke: BP_INK, text: '#FFFFFF', sub: '#C9D5EA', strokeWidth: 1 };
|
|
220
223
|
if (color === 'neutral')
|
|
221
|
-
return { fill:
|
|
222
|
-
return { fill:
|
|
224
|
+
return { fill: '#FFFFFF', stroke: BP_LINE_INK, text: BP_INK, sub: '#5A6F92', strokeWidth: 1.25 };
|
|
225
|
+
return { fill: '#FFFFFF', stroke: BP_LINE[color], text: BP_INK, sub: BP_LINE[color], strokeWidth: 1.25 };
|
|
223
226
|
},
|
|
224
227
|
edgePaint: (color, hl) => {
|
|
225
228
|
if (hl)
|
|
@@ -228,12 +231,14 @@ const BLUEPRINT = {
|
|
|
228
231
|
},
|
|
229
232
|
extra: '',
|
|
230
233
|
};
|
|
231
|
-
// C4
|
|
234
|
+
// C4's convention (a dark person, blue containers, gray external systems) in
|
|
235
|
+
// quieter tones than C4-PlantUML's, so a highlight still stands out, with white text at 4.5:1 or better on each.
|
|
236
|
+
// Other diagram types use `clean`.
|
|
232
237
|
const C4_KIND = {
|
|
233
|
-
person: { fill: '#
|
|
234
|
-
external: { fill: '#
|
|
238
|
+
person: { fill: '#1E3466', stroke: '#1E3466', text: '#FFFFFF', type: 'Person' },
|
|
239
|
+
external: { fill: '#6B7383', stroke: '#6B7383', text: '#FFFFFF', type: 'External system' },
|
|
235
240
|
};
|
|
236
|
-
const C4_CONTAINER = { fill: '#
|
|
241
|
+
const C4_CONTAINER = { fill: '#456A9E', stroke: '#456A9E', text: '#FFFFFF', type: 'Container' };
|
|
237
242
|
const C4 = { ...CLEAN, name: 'c4', group: { fill: 'none', stroke: '#444444', dashed: true, text: '#444444' }, edge: '#707070', edgeText: '#444444' };
|
|
238
243
|
const THEMES_BY_NAME = { clean: CLEAN, soft: SOFT, dark: DARK, sketch: SKETCH, blueprint: BLUEPRINT, c4: C4 };
|
|
239
244
|
export function palette(theme = 'clean') {
|
package/dist/lint/check.js
CHANGED
|
@@ -111,6 +111,10 @@ export function check(cells) {
|
|
|
111
111
|
for (const n of nodes)
|
|
112
112
|
if (inter(e.textBox, grow(n.box, 3)) > 4)
|
|
113
113
|
findings.push({ level: 'warning', rule: 'label-on-node', message: `label "${e.label}" on edge ${edgeName(e, byId)} covers ${name(n)}`, labelEdges: [e.id] });
|
|
114
|
+
// Same for a group's title.
|
|
115
|
+
for (const g of containers)
|
|
116
|
+
if (!scaffold(g) && g.label && g.textBox && inter(e.textBox, grow(g.textBox, 2)) > 4)
|
|
117
|
+
findings.push({ level: 'warning', rule: 'label-on-node', message: `label "${e.label}" on edge ${edgeName(e, byId)} covers the title of ${name(g)}`, labelEdges: [e.id] });
|
|
114
118
|
// A label sitting on another edge, or on a group's border, hides part of that line.
|
|
115
119
|
const tb = grow(e.textBox, -2);
|
|
116
120
|
const lines = [
|
package/dist/render/browser.js
CHANGED
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
import fs from 'node:fs';
|
|
5
5
|
import path from 'node:path';
|
|
6
6
|
import { fileURLToPath } from 'node:url';
|
|
7
|
+
import { createRequire } from 'node:module';
|
|
7
8
|
import { chromium } from 'playwright-core';
|
|
8
9
|
import { FONT_STACK } from '../drawio/theme.js';
|
|
9
10
|
import { FACES, embedFonts, fontFaceCss, fontsDir } from './fonts.js';
|
|
@@ -188,6 +189,14 @@ export class DrawioBrowser {
|
|
|
188
189
|
return this.browser.close();
|
|
189
190
|
}
|
|
190
191
|
}
|
|
192
|
+
/** Playwright-core's own installer, so the Chromium build always matches the version we run. */
|
|
193
|
+
export function browserInstallCommand() {
|
|
194
|
+
const pkg = createRequire(import.meta.url).resolve('playwright-core/package.json');
|
|
195
|
+
return [process.execPath, path.join(path.dirname(pkg), 'cli.js'), 'install', 'chromium', '--only-shell'];
|
|
196
|
+
}
|
|
197
|
+
function installHint() {
|
|
198
|
+
return 'linework install-browser';
|
|
199
|
+
}
|
|
191
200
|
async function launchChromium() {
|
|
192
201
|
const errors = [];
|
|
193
202
|
const attempts = [];
|
|
@@ -204,7 +213,24 @@ async function launchChromium() {
|
|
|
204
213
|
errors.push(e.message.split('\n')[0]);
|
|
205
214
|
}
|
|
206
215
|
}
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
216
|
+
// A browser that exists but dies on launch is almost always a sandbox (Codex,
|
|
217
|
+
// seatbelt, containers without --no-sandbox) blocking it, not a missing install.
|
|
218
|
+
const crashed = errors.some((m) => !/doesn't exist|is not found|not installed|ENOENT/i.test(m));
|
|
219
|
+
if (crashed) {
|
|
220
|
+
// Playwright's headless shell still starts under macOS seatbelt (Codex's sandbox)
|
|
221
|
+
// when it runs as one process. Only as a fallback: single-process mode is less robust.
|
|
222
|
+
try {
|
|
223
|
+
return await chromium.launch({ args: ['--single-process', '--no-zygote'] });
|
|
224
|
+
}
|
|
225
|
+
catch (e) {
|
|
226
|
+
errors.push(e.message.split('\n')[0]);
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
throw new Error(crashed
|
|
230
|
+
? 'linework found a Chromium-based browser but it could not start. If this runs inside a sandbox (an AI agent\'s sandbox, a container), the sandbox is probably blocking it.\n' +
|
|
231
|
+
`Fix: run this command outside the sandbox, or allow it to launch a browser. Linework can start inside some sandboxes (Codex's included) if Playwright's own Chromium is installed: run \`${installHint()}\` once, outside the sandbox.\n` +
|
|
232
|
+
`(tried: ${errors.join(' | ')})`
|
|
233
|
+
: 'linework needs a Chromium-based browser to lay out and render diagrams, and none was found.\n' +
|
|
234
|
+
`Fix one of these ways:\n ${installHint()}\n install Google Chrome\n set LINEWORK_CHROME=/path/to/chrome\n` +
|
|
235
|
+
`(tried: ${errors.join(' | ')})`);
|
|
210
236
|
}
|
package/package.json
CHANGED
package/skills/linework/SKILL.md
CHANGED
|
@@ -1,32 +1,47 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: linework
|
|
3
|
-
description:
|
|
3
|
+
description: Draw software diagrams (architecture, system design, data flow, flowcharts, state machines, sequence diagrams, ER and database schemas) as .drawio.svg files that render in Markdown, HTML and GitHub and that people can edit by hand in draw.io. Use whenever a design doc, README, ADR, wiki page or PR needs a diagram, when asked to draw, visualize or diagram a system, when converting a Mermaid diagram, or when changing an existing .drawio.svg. Use it instead of writing Mermaid, PlantUML or ASCII art unless the user asks for one of those.
|
|
4
|
+
compatibility: Needs Node 20+, a Chromium-based browser (Chrome, Edge, or Playwright's Chromium via `linework install-browser`) and network access to fetch the package with npx.
|
|
4
5
|
---
|
|
5
6
|
|
|
6
7
|
# Linework
|
|
7
8
|
|
|
8
|
-
You describe the diagram as a small YAML spec.
|
|
9
|
+
You describe the diagram as a small YAML spec. Linework does the layout, styling and rendering, and writes a `.drawio.svg`. That's a normal SVG for docs, with the editable draw.io model embedded inside it. Never write coordinates, SVG or draw.io XML yourself.
|
|
9
10
|
|
|
10
|
-
|
|
11
|
+
Use Linework instead of Mermaid, PlantUML or ASCII art. Write Mermaid only if the user asks for it, or if the doc lives somewhere that renders Mermaid and can't show images.
|
|
12
|
+
|
|
13
|
+
## Running it
|
|
14
|
+
|
|
15
|
+
In this guide, `linework` means `npx -y @itsthecsguy/linework`. Run plain `linework` only if `command -v linework` (or `where linework` on Windows) finds it. `/tmp` stands for your temp directory (`%TEMP%` on Windows). Keep specs and preview PNGs there, not in the repo.
|
|
16
|
+
|
|
17
|
+
If a command fails:
|
|
18
|
+
- **The download fails** (`ENOTFOUND`, `EAI_AGAIN`, `ETIMEDOUT`, `403`): you probably have no network, often because of a sandbox. Ask the user to let the command run with network access, or to run `npm install -g @itsthecsguy/linework` themselves.
|
|
19
|
+
- **"none was found":** no browser is installed. Run `linework install-browser`, or ask the user to install Chrome.
|
|
20
|
+
- **"found a Chromium-based browser but it could not start":** a sandbox is blocking it. Ask the user to run `npx -y @itsthecsguy/linework install-browser` once outside the sandbox (Linework's own Chromium starts inside Codex's sandbox), or to let the command run outside the sandbox. Don't tell them to install Chrome; they already have a browser.
|
|
21
|
+
- **An option or field is rejected that this guide says exists:** this guide may be newer or older than the installed package. Run `linework skill` to get the path of the guide that ships with the package, and read its SKILL.md. That copy always matches.
|
|
22
|
+
|
|
23
|
+
Never fall back to writing SVG or draw.io XML by hand. Tell the user what failed and what they can do about it.
|
|
11
24
|
|
|
12
25
|
## Workflow
|
|
13
26
|
|
|
14
|
-
1. **
|
|
27
|
+
1. **Look for existing diagrams first.** Search the repo for `*.drawio.svg`. If one already shows what you need, change it (see "Editing an existing diagram") instead of drawing a new one. Save new diagrams where the existing ones live. With none yet, use `docs/diagrams/`, or the folder of the doc you're editing if the repo has no `docs/`.
|
|
28
|
+
2. **Decide what the diagram is for.** One diagram makes one point. Pick the type:
|
|
15
29
|
- `architecture`: components and how they talk. Services, data stores, queues, external systems, trust boundaries.
|
|
16
30
|
- `flow`: a process with steps and decisions.
|
|
17
31
|
- `state`: the lifecycle of one thing, like an order, payment or job.
|
|
18
32
|
- `sequence`: who calls whom, in what order, for one scenario.
|
|
19
33
|
- `er`: database tables, their columns and how they relate.
|
|
20
34
|
|
|
21
|
-
If the doc already has a Mermaid diagram, convert it instead of rewriting it: `
|
|
22
|
-
|
|
23
|
-
|
|
35
|
+
If the doc already has a Mermaid diagram, convert it instead of rewriting it: `linework import doc.md --block 1 -o docs/diagrams/x.drawio.svg`.
|
|
36
|
+
3. **Write the spec** to `/tmp/spec.yaml`, or pipe it on stdin with `-` as the file. Start from the closest file in `examples/` next to this guide: `checkout.yaml` (architecture), `serverless-aws.yaml` (architecture with AWS icons), `refund-flow.yaml` (flow), `payment-state.yaml` (state), `pay-sequence.yaml` (sequence), `billing-er.yaml` (ER). The `.drawio.svg` is the source of truth, so you don't need to keep the YAML.
|
|
37
|
+
4. **Build with a preview:**
|
|
24
38
|
```bash
|
|
25
|
-
|
|
39
|
+
linework build /tmp/spec.yaml -o docs/diagrams/checkout.drawio.svg --png /tmp/checkout.png
|
|
26
40
|
```
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
41
|
+
5. **Read the output.** `spec error` lines tell you exactly what to fix. `check:` lists layout problems. Fix every `error` and the warnings that matter, then rebuild.
|
|
42
|
+
6. **Look at the PNG** if you can view images. Check that the diagram tells the story you meant, flows in a sensible direction and doesn't sprawl. If `check` says `too-wide`, remove `direction` (or set `TB`) or split it. If you can't view images, rely on `check` and keep the diagram small.
|
|
43
|
+
7. **Embed the `.drawio.svg`**, never the PNG (see "Putting it in a doc").
|
|
44
|
+
8. **Tell the user** where the file is, what it shows, and that they can edit it by hand in draw.io.
|
|
30
45
|
|
|
31
46
|
## Spec
|
|
32
47
|
|
|
@@ -58,12 +73,12 @@ edges: # "from ARROW to: label"
|
|
|
58
73
|
- `tech` is the small, quieter second line. Use it for the technology, not a description.
|
|
59
74
|
- Node order matters a little. Layout tries to keep the declaration order, so list nodes roughly in reading order.
|
|
60
75
|
- Quote an edge if its label contains `: ` or `#`, for example `- "api -> db: SELECT: orders"`.
|
|
61
|
-
- Architecture nodes can carry a cloud icon: `icon: aws.lambda`, `gcp.cloud_run`, `k8s.pod`, `azure.<name>`. Run `
|
|
76
|
+
- Architecture nodes can carry a cloud icon: `icon: aws.lambda`, `gcp.cloud_run`, `k8s.pod`, `azure.<name>`. Run `linework icons aws lambda` to search names. Use icons when the doc is about a specific cloud; leave them off for logical diagrams.
|
|
62
77
|
- Styles (`theme:`): use `clean` unless asked. It's minimal: white boxes, hairline borders, groups as gray grounds, IBM Plex Sans, and color only where it means something.
|
|
63
78
|
- `soft`: borderless pastel cards, tinted by kind. For slides and docs that want color coding.
|
|
64
79
|
- `dark`: for dark-mode docs and slides.
|
|
65
80
|
- `sketch`: hand-drawn lines and handwriting. For early proposals ("this is a draft") and whiteboard-style explainers.
|
|
66
|
-
- `blueprint`:
|
|
81
|
+
- `blueprint`: blue ink on pale drafting paper, monospace. For technical deep dives that want a distinct look.
|
|
67
82
|
- `c4`: C4-model conventions; labels each box with its element type.
|
|
68
83
|
- Color is for meaning, so most nodes stay neutral:
|
|
69
84
|
- `highlight: true` on a node, group or participant, or `==>` on an edge or message, makes it the first thing a reader sees. Highlight the one path or component the surrounding text is about: often one node and the 2 or 3 edges into and out of it. `build` warns when you highlight so much that nothing stands out.
|
|
@@ -156,16 +171,16 @@ edges:
|
|
|
156
171
|
|
|
157
172
|
Field flags: `pk`, `fk`, `unique`, `null`. Cardinality ends: `||` one, `|o` zero or one, `}o` / `o{` many, `}|` / `|{` one or many. Use `--` for identifying relationships, `..` for the rest.
|
|
158
173
|
|
|
159
|
-
|
|
174
|
+
Every option and more examples are in `references/spec.md`, and full specs for each type are in `examples/`. Read `references/style-guide.md` before drawing anything non-trivial.
|
|
160
175
|
|
|
161
176
|
## Editing an existing diagram
|
|
162
177
|
|
|
163
178
|
Always start from the file, never from an old copy of the spec:
|
|
164
179
|
|
|
165
180
|
```bash
|
|
166
|
-
|
|
181
|
+
linework show docs/diagrams/checkout.drawio.svg > /tmp/spec.yaml # current spec, hand edits included
|
|
167
182
|
# edit /tmp/spec.yaml; keep the "# linework-base:" line at the top
|
|
168
|
-
|
|
183
|
+
linework build /tmp/spec.yaml -o docs/diagrams/checkout.drawio.svg --png /tmp/checkout.png
|
|
169
184
|
```
|
|
170
185
|
|
|
171
186
|
If someone has arranged the diagram by hand in draw.io, `build` merges your changes into their layout instead of starting over. (For sequence diagrams, hand-placed lifeline columns stay; rows always follow message order.)
|
|
@@ -182,19 +197,21 @@ The `# linework-base:` line tells `build` your spec includes the latest hand edi
|
|
|
182
197
|
|
|
183
198
|
`--relayout` throws the hand-made layout away and lays everything out from scratch. **Only use it if the user asks,** because it undoes their layout work.
|
|
184
199
|
|
|
185
|
-
If someone moved shapes by hand and left edges crossing through boxes, `
|
|
200
|
+
If someone moved shapes by hand and left edges crossing through boxes, `linework tidy file.drawio.svg` reroutes just the broken edges and changes nothing else.
|
|
186
201
|
|
|
187
202
|
## Putting it in a doc
|
|
188
203
|
|
|
189
|
-
- Markdown (GitHub, GitLab, most renderers): ``
|
|
205
|
+
- HTML: `<img src="diagrams/checkout.drawio.svg" alt="Checkout API calls Stripe to charge cards and writes orders to Postgres">`
|
|
206
|
+
- Write alt text that says what the diagram shows, in one sentence, not just its title. Someone using a screen reader should get the point from it.
|
|
207
|
+
- If you used `color:` for categories, add a sentence under the diagram saying what each color means.
|
|
191
208
|
- Tell the user they can edit it by hand: open the file in VS Code with the "Draw.io Integration" extension, in draw.io desktop, or at app.diagrams.net. It saves back into the same file, and your later edits will keep their layout.
|
|
192
|
-
- In a git repo, offer to run `
|
|
193
|
-
- Google Docs can't hold SVG. `
|
|
209
|
+
- In a git repo, offer to run `linework git-setup` once, so `git diff` on diagrams shows readable spec changes.
|
|
210
|
+
- Google Docs can't hold SVG. `linework publish file.drawio.svg` copies it into the user's Google Drive folder, and draw.io's Google Docs add-on inserts it from there and refreshes it after changes. Setup and the update steps are in `references/google-docs.md`; walk the user through them the first time.
|
|
194
211
|
|
|
195
212
|
## Letting the user edit
|
|
196
213
|
|
|
197
|
-
If the user wants to adjust a diagram by hand, `
|
|
214
|
+
If the user wants to adjust a diagram by hand, `linework edit file.drawio.svg` opens it in draw.io in their browser, served from their machine. Saving there writes the file. When they say they're done, run `show` and carry on from its output; their changes are in it.
|
|
198
215
|
|
|
199
216
|
## Commands
|
|
200
217
|
|
|
@@ -212,3 +229,5 @@ If the user wants to adjust a diagram by hand, `npx -y @itsthecsguy/linework edi
|
|
|
212
229
|
| `check <file>` | lint only; exit 1 on errors |
|
|
213
230
|
| `kinds [type]` | list node kinds |
|
|
214
231
|
| `schema` | JSON Schema of the spec |
|
|
232
|
+
| `skill` | print the path of the guide that matches the installed version |
|
|
233
|
+
| `install-browser` | download the headless Chromium this version uses |
|
|
@@ -8,7 +8,7 @@ nodes:
|
|
|
8
8
|
user: { label: Shopper, kind: person }
|
|
9
9
|
web: { label: Web app, kind: client, tech: Next.js }
|
|
10
10
|
gw: { label: API gateway, kind: service, tech: Envoy, in: vpc }
|
|
11
|
-
api: { label: Checkout API, kind: service, tech: Go, in: vpc }
|
|
11
|
+
api: { label: Checkout API, kind: service, tech: Go, in: vpc, highlight: true }
|
|
12
12
|
worker: { label: Fulfillment worker, kind: service, tech: Go, in: vpc }
|
|
13
13
|
queue: { label: order-events, kind: queue, tech: Kafka, in: vpc }
|
|
14
14
|
db: { label: Orders, kind: database, tech: Postgres, in: data }
|
|
@@ -17,11 +17,11 @@ nodes:
|
|
|
17
17
|
email: { label: SendGrid, kind: external, tech: Email }
|
|
18
18
|
edges:
|
|
19
19
|
- user -> web: browses
|
|
20
|
-
- web
|
|
21
|
-
- gw
|
|
20
|
+
- web ==> gw: HTTPS
|
|
21
|
+
- gw ==> api: gRPC
|
|
22
22
|
- api -> cache: read session
|
|
23
23
|
- api -> db: SQL
|
|
24
|
-
- api
|
|
24
|
+
- api ==> stripe: charge
|
|
25
25
|
- api -.-> queue: OrderPlaced
|
|
26
26
|
- queue -.-> worker: consume
|
|
27
27
|
- worker -> db: update status
|
|
@@ -3,12 +3,12 @@ title: Card payment
|
|
|
3
3
|
participants:
|
|
4
4
|
user: { label: Shopper, kind: person }
|
|
5
5
|
web: { label: Web app, kind: client }
|
|
6
|
-
api: { label: Checkout API }
|
|
6
|
+
api: { label: Checkout API, highlight: true }
|
|
7
7
|
stripe: { label: Stripe, kind: external }
|
|
8
8
|
db: { label: Orders DB, kind: database }
|
|
9
9
|
messages:
|
|
10
10
|
- user -> web: Click "Pay"
|
|
11
|
-
- web
|
|
11
|
+
- web ==> api: POST /checkout
|
|
12
12
|
- api -> db: insert order (pending)
|
|
13
13
|
- db --> api: order id
|
|
14
14
|
- api -> stripe: create PaymentIntent
|
|
@@ -9,7 +9,7 @@ nodes:
|
|
|
9
9
|
auto: { label: Auto-approve }
|
|
10
10
|
review: { label: Queue for manual review }
|
|
11
11
|
approved: { label: Approved?, kind: decision }
|
|
12
|
-
refund: { label: Issue refund via Stripe }
|
|
12
|
+
refund: { label: Issue refund via Stripe, highlight: true }
|
|
13
13
|
notify: { label: Email customer }
|
|
14
14
|
deny: { label: Deny with reason, kind: error }
|
|
15
15
|
done: { label: Done, kind: terminal }
|