@itsthecsguy/linework 0.1.4 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +21 -1
- package/dist/api.js +66 -9
- package/dist/cli.js +53 -286
- package/dist/commands.js +346 -0
- package/dist/daemon.js +193 -0
- package/dist/drawio/merge.js +47 -8
- package/dist/layout/layout.js +44 -1
- package/dist/lint/check.js +16 -0
- package/dist/mcp/server.js +4 -4
- package/dist/patch.js +167 -0
- package/dist/report.js +2 -0
- package/dist/spec/parse.js +2 -0
- package/package.json +1 -1
- package/skills/linework/SKILL.md +57 -200
- package/skills/linework/references/troubleshooting.md +11 -0
package/dist/layout/layout.js
CHANGED
|
@@ -156,7 +156,11 @@ function nodeSizes(d, tm, fonts) {
|
|
|
156
156
|
continue;
|
|
157
157
|
}
|
|
158
158
|
if (shape === 'diamond') {
|
|
159
|
-
|
|
159
|
+
// A diamond holds little text for its size. Past two lines, wrap wider:
|
|
160
|
+
// three lines in a wider diamond take less room than four in a tall one.
|
|
161
|
+
let t = textOf(n, fonts.maxText * 0.7);
|
|
162
|
+
if (t.lines > 2)
|
|
163
|
+
t = textOf(n, fonts.maxText);
|
|
160
164
|
const w = snap(Math.max(128, t.width * 1.5 + 40));
|
|
161
165
|
sizes.set(n.id, { w, h: snap(Math.max(72, w * 0.5, t.lines * fonts.line * 2 + 24)) });
|
|
162
166
|
continue;
|
|
@@ -274,6 +278,45 @@ async function run(d, dir, sizes, labels, groupMin, groupTop, order, pins = new
|
|
|
274
278
|
if (call)
|
|
275
279
|
flipped.add(pairs === 'reply' ? e.id : call.id);
|
|
276
280
|
});
|
|
281
|
+
// The edge that closes a loop (a retry, "new findings, investigate again")
|
|
282
|
+
// points back at a box that was declared earlier and already appears in an
|
|
283
|
+
// earlier edge. Lay it out reversed, so the main path keeps running forward
|
|
284
|
+
// whichever branch ELK happens to walk first.
|
|
285
|
+
const index = new Map(d.nodes.map((n, i) => [n.id, i]));
|
|
286
|
+
const firstUse = new Map();
|
|
287
|
+
d.edges.forEach((e, i) => {
|
|
288
|
+
for (const id of [e.from, e.to])
|
|
289
|
+
if (!firstUse.has(id))
|
|
290
|
+
firstUse.set(id, i);
|
|
291
|
+
});
|
|
292
|
+
const back = d.edges
|
|
293
|
+
.map((e, i) => ({ e, i }))
|
|
294
|
+
.filter(({ e, i }) => e.from !== e.to && index.get(e.to) < index.get(e.from) && firstUse.get(e.to) < i)
|
|
295
|
+
// Call-and-reply pairs are handled above.
|
|
296
|
+
.filter(({ e }) => !d.edges.some((x) => x.from === e.to && x.to === e.from))
|
|
297
|
+
// Last written first: once the loop's closing edge is cut, an earlier
|
|
298
|
+
// backward edge inside the same loop is no longer part of a cycle and stays.
|
|
299
|
+
.reverse()
|
|
300
|
+
.map(({ e }) => e);
|
|
301
|
+
const cut = new Set();
|
|
302
|
+
const reaches = (from, to) => {
|
|
303
|
+
const seen = new Set([from]);
|
|
304
|
+
for (const q = [from]; q.length;) {
|
|
305
|
+
const cur = q.pop();
|
|
306
|
+
if (cur === to)
|
|
307
|
+
return true;
|
|
308
|
+
for (const x of d.edges)
|
|
309
|
+
if (x.from === cur && !cut.has(x.id) && !seen.has(x.to))
|
|
310
|
+
(seen.add(x.to), q.push(x.to));
|
|
311
|
+
}
|
|
312
|
+
return false;
|
|
313
|
+
};
|
|
314
|
+
for (const e of back) {
|
|
315
|
+
if (!reaches(e.to, e.from))
|
|
316
|
+
continue;
|
|
317
|
+
cut.add(e.id);
|
|
318
|
+
flipped.add(e.id);
|
|
319
|
+
}
|
|
277
320
|
root.edges = d.edges.map((e, i) => {
|
|
278
321
|
const src = pinFor(pins, e.from, e.id, 'from') ?? e.from, dst = pinFor(pins, e.to, e.id, 'to') ?? e.to;
|
|
279
322
|
const flip = flipped.has(e.id);
|
package/dist/lint/check.js
CHANGED
|
@@ -63,9 +63,25 @@ export function check(cells) {
|
|
|
63
63
|
if (p?.container && !inside(n.box, p.box, 2))
|
|
64
64
|
findings.push({ level: 'warning', rule: 'outside-group', message: `${name(n)} sticks out of its group ${name(p)}` });
|
|
65
65
|
}
|
|
66
|
+
// A box drawn inside a group reads as a member of it.
|
|
67
|
+
const groups = containers.filter((g) => !scaffold(g));
|
|
68
|
+
for (const n of nodes) {
|
|
69
|
+
const mine = ancestors(n.id);
|
|
70
|
+
for (const g of groups)
|
|
71
|
+
if (!mine.has(g.id) && inter(n.box, g.box) > 0.25 * n.box.w * n.box.h)
|
|
72
|
+
findings.push({ level: 'warning', rule: 'in-other-group', message: `${name(n)} sits inside ${name(g)} but isn't in that group; move it out, or put it in the group` });
|
|
73
|
+
}
|
|
66
74
|
for (const e of edges) {
|
|
67
75
|
const pts = e.points;
|
|
68
76
|
const skip = new Set([e.source, e.target, ...ancestors(e.source), ...ancestors(e.target)]);
|
|
77
|
+
// A line through a group it has no end in looks like it connects to something there.
|
|
78
|
+
for (const g of groups) {
|
|
79
|
+
if (skip.has(g.id))
|
|
80
|
+
continue;
|
|
81
|
+
const r = shrink(g.box, 6);
|
|
82
|
+
if (pts.some((p, i) => i < pts.length - 1 && segHitsBox(p, pts[i + 1], r)))
|
|
83
|
+
findings.push({ level: 'warning', rule: 'edge-through-group', message: `edge ${edgeName(e, byId)} runs through ${name(g)}, which neither end is in` });
|
|
84
|
+
}
|
|
69
85
|
for (const n of nodes) {
|
|
70
86
|
if (skip.has(n.id))
|
|
71
87
|
continue;
|
package/dist/mcp/server.js
CHANGED
|
@@ -30,7 +30,7 @@ const PKG = (() => {
|
|
|
30
30
|
}
|
|
31
31
|
throw new Error('linework: bundled skill not found. Reinstall linework.');
|
|
32
32
|
})();
|
|
33
|
-
const TOOL_NAMES = `In this MCP server the CLI commands in the guide are tools: build -> build_diagram, show -> show_diagram, check -> check_diagram, render -> render_diagram, tidy -> tidy_diagram, import -> import_mermaid, icons -> list_icons, edit -> edit_diagram, publish -> publish_diagram. Pass the spec text to build_diagram directly instead of writing a file.\n\n`;
|
|
33
|
+
const TOOL_NAMES = `In this MCP server the CLI commands in the guide are tools: build -> build_diagram, show -> show_diagram, check -> check_diagram, render -> render_diagram, tidy -> tidy_diagram, import -> import_mermaid, icons -> list_icons, edit -> edit_diagram, publish -> publish_diagram. Pass the spec text to build_diagram directly instead of writing a file. There are no set, add or rm tools: for a small change, call show_diagram, edit its output and pass that to build_diagram, which keeps the layout the same way.\n\n`;
|
|
34
34
|
export function createServer(opts) {
|
|
35
35
|
let browser;
|
|
36
36
|
const getBrowser = () => (browser ??= DrawioBrowser.launch({ scale: 2 }));
|
|
@@ -66,13 +66,13 @@ export function createServer(opts) {
|
|
|
66
66
|
description: 'Returns the linework guide: diagram types, the YAML spec format with examples, and the style rules. Read it before your first diagram.',
|
|
67
67
|
inputSchema: z.object({
|
|
68
68
|
topic: z
|
|
69
|
-
.enum(['overview', 'spec', 'style', 'google-docs'])
|
|
69
|
+
.enum(['overview', 'spec', 'style', 'google-docs', 'troubleshooting'])
|
|
70
70
|
.default('overview')
|
|
71
|
-
.describe('overview: workflow and cheat sheet. spec: the full spec reference. style: what makes a good diagram. google-docs: getting diagrams into Google Docs.'),
|
|
71
|
+
.describe('overview: workflow and cheat sheet. spec: the full spec reference. style: what makes a good diagram. google-docs: getting diagrams into Google Docs. troubleshooting: what to do when a command fails.'),
|
|
72
72
|
}),
|
|
73
73
|
annotations: { readOnlyHint: true },
|
|
74
74
|
}, async ({ topic }) => {
|
|
75
|
-
const file = { overview: 'SKILL.md', spec: 'references/spec.md', style: 'references/style-guide.md', 'google-docs': 'references/google-docs.md' }[topic];
|
|
75
|
+
const file = { overview: 'SKILL.md', spec: 'references/spec.md', style: 'references/style-guide.md', 'google-docs': 'references/google-docs.md', troubleshooting: 'references/troubleshooting.md' }[topic];
|
|
76
76
|
const body = fs.readFileSync(path.join(PKG, 'skills', 'linework', file), 'utf8').replace(/^---\n[\s\S]*?\n---\n/, '');
|
|
77
77
|
return { content: [text(TOOL_NAMES + body)] };
|
|
78
78
|
});
|
package/dist/patch.js
ADDED
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
// One-line changes to an existing diagram: set a field, add or remove a box or
|
|
2
|
+
// an edge. Each reads the file's current spec (hand edits included), changes it
|
|
3
|
+
// and builds, so the layout merges the same way a show / edit / build round does.
|
|
4
|
+
import YAML, { isMap, isScalar, isSeq } from 'yaml';
|
|
5
|
+
import { build, show } from './api.js';
|
|
6
|
+
export class PatchError extends Error {
|
|
7
|
+
}
|
|
8
|
+
const TOP = new Set(['title', 'theme', 'direction']);
|
|
9
|
+
const TEXT = new Set(['label', 'tech', 'title']);
|
|
10
|
+
// "from ARROW to" or "from ARROW to: label". ER cardinalities count as arrows too.
|
|
11
|
+
const EDGE = /^(\S+)\s+(\S*(?:->|=>|--|\.\.)\S*|<->)\s+([^\s:]+)(?::\s*(.*))?$/;
|
|
12
|
+
function mapAt(doc, key, create = false) {
|
|
13
|
+
const m = doc.get(key);
|
|
14
|
+
if (isMap(m))
|
|
15
|
+
return m;
|
|
16
|
+
if (!create)
|
|
17
|
+
return undefined;
|
|
18
|
+
const made = doc.createNode({});
|
|
19
|
+
doc.set(key, made);
|
|
20
|
+
return made;
|
|
21
|
+
}
|
|
22
|
+
function seqAt(doc, key, create = false) {
|
|
23
|
+
const s = doc.get(key);
|
|
24
|
+
if (isSeq(s))
|
|
25
|
+
return s;
|
|
26
|
+
if (!create)
|
|
27
|
+
return undefined;
|
|
28
|
+
const made = doc.createNode([]);
|
|
29
|
+
doc.set(key, made);
|
|
30
|
+
return made;
|
|
31
|
+
}
|
|
32
|
+
const edgeEnds = (item) => {
|
|
33
|
+
if (isScalar(item) && typeof item.value === 'string') {
|
|
34
|
+
const m = EDGE.exec(item.value.trim());
|
|
35
|
+
return m ? { from: m[1], to: m[3] } : undefined;
|
|
36
|
+
}
|
|
37
|
+
// Unquoted, "a -> b: label" is a one-pair YAML map whose key is the edge.
|
|
38
|
+
if (isMap(item) && item.items.length === 1 && isScalar(item.items[0].key) && typeof item.items[0].key.value === 'string') {
|
|
39
|
+
const m = EDGE.exec(item.items[0].key.value.trim());
|
|
40
|
+
if (m)
|
|
41
|
+
return { from: m[1], to: m[3] };
|
|
42
|
+
}
|
|
43
|
+
if (isMap(item) && item.has('from') && item.has('to'))
|
|
44
|
+
return { from: String(item.get('from')), to: String(item.get('to')) };
|
|
45
|
+
return undefined;
|
|
46
|
+
};
|
|
47
|
+
/** Apply the ops to a spec's YAML text and return the new text. */
|
|
48
|
+
export function patchSpec(yaml, ops) {
|
|
49
|
+
const doc = YAML.parseDocument(yaml);
|
|
50
|
+
const sequence = doc.get('type') === 'sequence';
|
|
51
|
+
const nodesKey = sequence ? 'participants' : 'nodes';
|
|
52
|
+
const edgesKey = sequence ? 'messages' : 'edges';
|
|
53
|
+
const has = (key, id) => !!mapAt(doc, key)?.has(id);
|
|
54
|
+
const known = () => [...(mapAt(doc, nodesKey)?.items ?? []), ...(mapAt(doc, 'groups')?.items ?? [])].map((p) => String(isScalar(p.key) ? p.key.value : p.key)).join(', ');
|
|
55
|
+
for (const { op, args } of ops) {
|
|
56
|
+
if (!args.length)
|
|
57
|
+
throw new PatchError(`${op} needs at least one argument`);
|
|
58
|
+
for (const arg of args) {
|
|
59
|
+
if (op === 'set') {
|
|
60
|
+
const eq = arg.indexOf('=');
|
|
61
|
+
if (eq < 1)
|
|
62
|
+
throw new PatchError(`can't read "${arg}". Write it as id.field=value, for example api.label="Checkout API", or theme=dark.`);
|
|
63
|
+
const path = arg.slice(0, eq), raw = arg.slice(eq + 1);
|
|
64
|
+
const dot = path.lastIndexOf('.');
|
|
65
|
+
const field = dot < 0 ? path : path.slice(dot + 1);
|
|
66
|
+
const value = TEXT.has(field) ? raw : YAML.parse(raw);
|
|
67
|
+
if (dot < 0) {
|
|
68
|
+
if (!TOP.has(path))
|
|
69
|
+
throw new PatchError(`"${path}" isn't a diagram setting. Settings: title, theme, direction. For a box, write id.field=value.`);
|
|
70
|
+
if (raw === '')
|
|
71
|
+
doc.delete(path);
|
|
72
|
+
else
|
|
73
|
+
doc.set(path, value);
|
|
74
|
+
continue;
|
|
75
|
+
}
|
|
76
|
+
const id = path.slice(0, dot);
|
|
77
|
+
const key = has(nodesKey, id) ? nodesKey : has('groups', id) ? 'groups' : undefined;
|
|
78
|
+
if (!key)
|
|
79
|
+
throw new PatchError(`no box or group named "${id}". There are: ${known()}`);
|
|
80
|
+
const owner = mapAt(doc, key);
|
|
81
|
+
const found = owner.get(id, true);
|
|
82
|
+
let entry;
|
|
83
|
+
if (isMap(found))
|
|
84
|
+
entry = found;
|
|
85
|
+
else {
|
|
86
|
+
// "id: Label" and "id:" are short for a map.
|
|
87
|
+
const label = isScalar(found) && found.value != null ? { label: found.value } : {};
|
|
88
|
+
entry = doc.createNode(label, { flow: true });
|
|
89
|
+
owner.set(id, entry);
|
|
90
|
+
}
|
|
91
|
+
if (raw === '')
|
|
92
|
+
entry.delete(field);
|
|
93
|
+
else
|
|
94
|
+
entry.set(field, value);
|
|
95
|
+
}
|
|
96
|
+
else if (op === 'add') {
|
|
97
|
+
const text = arg.trim();
|
|
98
|
+
if (EDGE.test(text)) {
|
|
99
|
+
seqAt(doc, edgesKey, true).add(doc.createNode(text));
|
|
100
|
+
continue;
|
|
101
|
+
}
|
|
102
|
+
const group = /^group\s+/.test(text);
|
|
103
|
+
const parsed = YAML.parse(group ? text.replace(/^group\s+/, '') : text);
|
|
104
|
+
const entries = typeof parsed === 'string' ? [[parsed, {}]] : parsed && typeof parsed === 'object' ? Object.entries(parsed) : [];
|
|
105
|
+
if (!entries.length)
|
|
106
|
+
throw new PatchError(`can't read "${arg}". A box: "fraud: { label: Fraud check, in: vpc }". An edge: "api -> fraud: score". A group: "group vpc: { label: Production VPC }".`);
|
|
107
|
+
const key = group ? 'groups' : nodesKey;
|
|
108
|
+
for (const [id, v] of entries) {
|
|
109
|
+
if (has(nodesKey, id) || has('groups', id))
|
|
110
|
+
throw new PatchError(`"${id}" already exists. Use set to change it.`);
|
|
111
|
+
const val = typeof v === 'string' ? { label: v } : (v ?? {});
|
|
112
|
+
mapAt(doc, key, true).set(id, doc.createNode(val, { flow: true }));
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
else {
|
|
116
|
+
const text = arg.trim();
|
|
117
|
+
const m = EDGE.exec(text);
|
|
118
|
+
if (m) {
|
|
119
|
+
const list = seqAt(doc, edgesKey);
|
|
120
|
+
const i = list?.items.findIndex((it) => {
|
|
121
|
+
const e = edgeEnds(it);
|
|
122
|
+
return e && e.from === m[1] && e.to === m[3];
|
|
123
|
+
});
|
|
124
|
+
if (!list || i === undefined || i < 0)
|
|
125
|
+
throw new PatchError(`no edge from "${m[1]}" to "${m[3]}"`);
|
|
126
|
+
list.delete(i);
|
|
127
|
+
continue;
|
|
128
|
+
}
|
|
129
|
+
if (has(nodesKey, text)) {
|
|
130
|
+
mapAt(doc, nodesKey).delete(text);
|
|
131
|
+
const list = seqAt(doc, edgesKey);
|
|
132
|
+
if (list)
|
|
133
|
+
for (let i = list.items.length - 1; i >= 0; i--) {
|
|
134
|
+
const e = edgeEnds(list.items[i]);
|
|
135
|
+
if (e && (e.from === text || e.to === text))
|
|
136
|
+
list.delete(i);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
else if (has('groups', text)) {
|
|
140
|
+
// Its members move up to the group it was in, or out to the top level.
|
|
141
|
+
const groups = mapAt(doc, 'groups');
|
|
142
|
+
const self = groups.get(text);
|
|
143
|
+
const up = isMap(self) ? self.get('in') : undefined;
|
|
144
|
+
groups.delete(text);
|
|
145
|
+
for (const k of [nodesKey, 'groups'])
|
|
146
|
+
for (const p of mapAt(doc, k)?.items ?? [])
|
|
147
|
+
if (isMap(p.value) && p.value.get('in') === text) {
|
|
148
|
+
if (up)
|
|
149
|
+
p.value.set('in', up);
|
|
150
|
+
else
|
|
151
|
+
p.value.delete('in');
|
|
152
|
+
}
|
|
153
|
+
if (!groups.items.length)
|
|
154
|
+
doc.delete('groups');
|
|
155
|
+
}
|
|
156
|
+
else
|
|
157
|
+
throw new PatchError(`no box, group or edge matches "${arg}". There are: ${known()}`);
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
return doc.toString({ lineWidth: 0 });
|
|
162
|
+
}
|
|
163
|
+
/** Change a diagram file in place. The existing layout is kept. */
|
|
164
|
+
export async function patchFile(file, ops, opts = {}) {
|
|
165
|
+
const yaml = patchSpec(show(file).yaml, ops);
|
|
166
|
+
return build(yaml, { ...opts, out: file });
|
|
167
|
+
}
|
package/dist/report.js
CHANGED
|
@@ -8,6 +8,8 @@ export function buildLines(res, png) {
|
|
|
8
8
|
parts.push(`placed ${m.placed.join(', ')}`);
|
|
9
9
|
if (m.grown.length)
|
|
10
10
|
parts.push(`grew ${m.grown.join(', ')}`);
|
|
11
|
+
if (m.shrunk.length)
|
|
12
|
+
parts.push(`closed up ${m.shrunk.join(', ')}`);
|
|
11
13
|
if (m.removed.length)
|
|
12
14
|
parts.push(`removed ${m.removed.join(', ')}`);
|
|
13
15
|
if (m.rerouted)
|
package/dist/spec/parse.js
CHANGED
|
@@ -243,6 +243,8 @@ export function parseSpec(raw) {
|
|
|
243
243
|
const out = edges.filter((e) => e.from === n.id);
|
|
244
244
|
if (out.some((e) => !e.label))
|
|
245
245
|
warnings.push({ path: `nodes.${n.id}`, message: 'label every edge leaving a decision, e.g. "yes"/"no"' });
|
|
246
|
+
if (n.label.length > 40)
|
|
247
|
+
warnings.push({ path: `nodes.${n.id}`, message: `a diamond holds little text; shorten "${n.label.slice(0, 24)}..." to a short question and put the detail in the step before it` });
|
|
246
248
|
}
|
|
247
249
|
}
|
|
248
250
|
highlightWarning(real, edges, warnings);
|
package/package.json
CHANGED
package/skills/linework/SKILL.md
CHANGED
|
@@ -6,49 +6,49 @@ compatibility: Needs Node 20+, a Chromium-based browser (Chrome, Edge, or Playwr
|
|
|
6
6
|
|
|
7
7
|
# Linework
|
|
8
8
|
|
|
9
|
-
You describe the diagram as a small YAML spec. Linework does the layout, styling and rendering, and writes a `.drawio.svg
|
|
9
|
+
You describe the diagram as a small YAML spec. Linework does the layout, styling and rendering, and writes a `.drawio.svg`: a normal SVG for docs with the editable draw.io model inside it. Never write coordinates, SVG or draw.io XML yourself, and don't fall back to Mermaid, PlantUML or ASCII art unless the user asks for one.
|
|
10
10
|
|
|
11
|
-
|
|
11
|
+
`linework` below means `npx -y @itsthecsguy/linework` (plain `linework` if it's on the PATH). Keep specs and preview PNGs in your temp directory, not in the repo. If a command fails to download, find a browser or accept an option this guide names, read `references/troubleshooting.md`.
|
|
12
12
|
|
|
13
|
-
##
|
|
13
|
+
## New diagram
|
|
14
14
|
|
|
15
|
-
|
|
15
|
+
1. **Look for existing diagrams.** Search for `*.drawio.svg`. If one already shows what you need, change it (next section). Save new ones where the others live, else `docs/diagrams/`.
|
|
16
|
+
2. **Pick the type.** One diagram makes one point.
|
|
17
|
+
- `architecture`: components and how they talk.
|
|
18
|
+
- `flow`: a process with steps and decisions.
|
|
19
|
+
- `state`: the lifecycle of one thing.
|
|
20
|
+
- `sequence`: who calls whom, in order, for one scenario.
|
|
21
|
+
- `er`: tables, columns and relationships.
|
|
22
|
+
3. **Write the spec**, starting from the closest file in `examples/` next to this guide: `checkout.yaml` and `platform.yaml` (architecture), `serverless-aws.yaml` and `gcp-ingest.yaml` (cloud icons), `refund-flow.yaml` and `deploy-flow.yaml` (flow), `payment-state.yaml` and `job-state.yaml` (state), `pay-sequence.yaml` and `auth-seq.yaml` (sequence), `billing-er.yaml` and `shop-er.yaml` (ER). A Mermaid block converts directly: `linework import doc.md --block 1 -o out.drawio.svg`.
|
|
23
|
+
4. **Build:** `linework build /tmp/spec.yaml -o docs/diagrams/checkout.drawio.svg --png /tmp/checkout.png`
|
|
24
|
+
5. **Read the output.** `spec error` lines say exactly what to fix. `check:` lists layout problems. Fix every error and the warnings that matter, then rebuild. Look at the PNG if you can view images.
|
|
25
|
+
6. **Embed the `.drawio.svg`**, never the PNG: ``. Write alt text that says what the diagram shows in one sentence.
|
|
26
|
+
7. **Tell the user** where the file is and that they can edit it by hand in draw.io (`linework edit <file>` opens it in their browser).
|
|
16
27
|
|
|
17
|
-
|
|
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.
|
|
28
|
+
## Changing a diagram
|
|
22
29
|
|
|
23
|
-
|
|
30
|
+
The `.drawio.svg` is the source of truth. Every command below keeps what is already there in place, including boxes someone moved by hand in draw.io. Only what you change moves.
|
|
24
31
|
|
|
25
|
-
|
|
32
|
+
For one or two changes, edit the file directly:
|
|
26
33
|
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
```bash
|
|
46
|
-
linework build /tmp/spec.yaml -o docs/diagrams/checkout.drawio.svg --png /tmp/checkout.png
|
|
47
|
-
```
|
|
48
|
-
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.
|
|
49
|
-
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.
|
|
50
|
-
7. **Embed the `.drawio.svg`**, never the PNG (see "Putting it in a doc").
|
|
51
|
-
8. **Tell the user** where the file is, what it shows, and that they can edit it by hand in draw.io.
|
|
34
|
+
```bash
|
|
35
|
+
linework set d.drawio.svg api.label="Checkout API v2" api.highlight=true # id.field=value; empty value clears; also theme= title= direction=
|
|
36
|
+
linework add d.drawio.svg "fraud: { label: Fraud check, in: vpc }" "api -> fraud: score"
|
|
37
|
+
linework rm d.drawio.svg cache "api -> stripe" # a box goes with its edges
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
For a larger change, round-trip the spec. Always start from the file, never from an old copy of the spec:
|
|
41
|
+
|
|
42
|
+
```bash
|
|
43
|
+
linework show d.drawio.svg > /tmp/spec.yaml # current spec, hand edits included
|
|
44
|
+
# edit it; keep the "# linework-base:" line at the top
|
|
45
|
+
linework build /tmp/spec.yaml -o d.drawio.svg --png /tmp/d.png
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
- The `merged into the existing layout:` line says what was kept, placed, grown and rerouted. Pass it on to the user.
|
|
49
|
+
- If the user moved boxes by hand and left lines or labels in odd places, `linework tidy d.drawio.svg` reroutes those edges and re-places those labels. Boxes stay put.
|
|
50
|
+
- `--relayout` throws the layout away and starts over. Use it only if the user asks: it undoes their work.
|
|
51
|
+
- When the user says they finished editing in draw.io, run `show` and carry on from its output.
|
|
52
52
|
|
|
53
53
|
## Spec
|
|
54
54
|
|
|
@@ -57,184 +57,41 @@ type: architecture # architecture | flow | state | sequence | er
|
|
|
57
57
|
title: Checkout # optional
|
|
58
58
|
direction: auto # auto (default) | TB | LR | BT | RL
|
|
59
59
|
theme: clean # clean (default) | soft | dark | sketch | blueprint | c4
|
|
60
|
-
groups: # optional boxes around nodes;
|
|
60
|
+
groups: # optional boxes around nodes; nest with `in`
|
|
61
61
|
vpc: { label: Production VPC }
|
|
62
|
-
|
|
63
|
-
nodes: # id: { label, kind, tech, in } (all optional)
|
|
64
|
-
user: { label: Shopper, kind: person }
|
|
62
|
+
nodes: # id: { label, kind, tech, in }, all optional
|
|
65
63
|
web: { label: Web app, kind: client, tech: Next.js }
|
|
66
64
|
api: { label: Checkout API, tech: Go, in: vpc, highlight: true }
|
|
67
65
|
queue: { label: order-events, kind: queue, tech: Kafka, in: vpc }
|
|
68
|
-
db: { label: Orders, kind: database, tech: Postgres, in:
|
|
69
|
-
stripe: { kind: external
|
|
66
|
+
db: { label: Orders, kind: database, tech: Postgres, in: vpc }
|
|
67
|
+
stripe: { kind: external } # label defaults to "Stripe"
|
|
70
68
|
edges: # "from ARROW to: label"
|
|
71
|
-
-
|
|
72
|
-
- web ==> api: HTTPS # ==> highlighted: the path the doc is about
|
|
69
|
+
- web ==> api: HTTPS # ==> highlighted: the path the doc is about
|
|
73
70
|
- api -> db: SQL
|
|
74
|
-
- api -.-> queue: OrderPlaced
|
|
71
|
+
- api -.-> queue: OrderPlaced # -.-> async or event, dashed
|
|
75
72
|
- api -> stripe: charge
|
|
76
73
|
```
|
|
77
74
|
|
|
78
|
-
-
|
|
79
|
-
-
|
|
80
|
-
- `tech` is the
|
|
81
|
-
-
|
|
82
|
-
-
|
|
83
|
-
- 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.
|
|
84
|
-
- 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.
|
|
85
|
-
- `soft`: borderless pastel cards, tinted by kind. For slides and docs that want color coding.
|
|
86
|
-
- `dark`: for dark-mode docs and slides.
|
|
87
|
-
- `sketch`: hand-drawn lines and handwriting. For early proposals ("this is a draft") and whiteboard-style explainers.
|
|
88
|
-
- `blueprint`: blue ink on pale drafting paper, monospace. For technical deep dives that want a distinct look.
|
|
89
|
-
- `c4`: C4-model conventions; labels each box with its element type.
|
|
90
|
-
- Color is for meaning, so most nodes stay neutral:
|
|
91
|
-
- `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.
|
|
92
|
-
- `color: <name>` tints a node, group or edge to mark a category, like red for failure paths or green for what's new in this proposal. Say what the color means in the doc text. Names: `blue`, `violet`, `pink`, `red`, `orange`, `amber`, `green`, `teal`, `gray`.
|
|
93
|
-
- Kinds `error` and `success` are red and green in every style.
|
|
94
|
-
- An edge with a color uses the object form: `- { from: api, to: dlq, label: on failure, style: async, color: red }`.
|
|
95
|
-
|
|
96
|
-
Node kinds, also listed by `linework kinds`:
|
|
75
|
+
- Arrows: `->` call or flow, `==>` highlighted, `-.->` async, `<->` two-way, `--` no arrowhead. Spaces around the arrow. Quote an edge whose label contains `: ` or `#`.
|
|
76
|
+
- List nodes and edges in reading order. Layout follows it, and an edge that points back at an earlier node is drawn as the loop.
|
|
77
|
+
- `tech` is the quiet second line: the technology, not a description.
|
|
78
|
+
- Color is for meaning. `highlight: true` or `==>` marks the one path the text is about. `color: red|green|blue|...` marks a category; say what it means in the doc.
|
|
79
|
+
- Cloud icons on architecture nodes: `icon: aws.lambda`, `gcp.cloud_run`, `k8s.pod`, `azure.<name>`. Search with `linework icons aws lambda`.
|
|
97
80
|
|
|
98
81
|
| type | kinds (default first) |
|
|
99
82
|
|---|---|
|
|
100
83
|
| architecture | `service`, `person`, `client`, `function`, `database`, `cache`, `storage`, `queue`, `external` |
|
|
101
84
|
| flow | `step`, `decision`, `terminal`, `data`, `error` |
|
|
102
|
-
| state | `state`, `success`, `error
|
|
103
|
-
| sequence | `
|
|
104
|
-
| er | `
|
|
85
|
+
| state | `state`, `success`, `error`; start and end are `"[*] -> first"` and `"last -> [*]"`, quoted |
|
|
86
|
+
| sequence | `participants:` and `messages:` instead of nodes and edges; `-->` is a reply |
|
|
87
|
+
| er | `fields:` on nodes (`- id: uuid pk`), crow's-foot edges (`users }o--|| orgs: belongs to`) |
|
|
105
88
|
|
|
106
|
-
|
|
89
|
+
In a flow, keep decision text to a short question and label every edge leaving it (`window -> refund: "yes"`).
|
|
107
90
|
|
|
108
|
-
|
|
109
|
-
type: flow
|
|
110
|
-
nodes:
|
|
111
|
-
start: { label: Refund requested, kind: terminal }
|
|
112
|
-
window: { label: Within 30 days?, kind: decision }
|
|
113
|
-
refund: { label: Issue refund }
|
|
114
|
-
deny: { label: Deny with reason, kind: error }
|
|
115
|
-
edges:
|
|
116
|
-
- start -> window
|
|
117
|
-
- window -> refund: "yes" # label every edge leaving a decision
|
|
118
|
-
- window -> deny: "no"
|
|
119
|
-
```
|
|
91
|
+
## More
|
|
120
92
|
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
failed: { kind: error }
|
|
127
|
-
edges:
|
|
128
|
-
- "[*] -> pending" # quote edges that use [*]
|
|
129
|
-
- pending -> paid: payment_succeeded
|
|
130
|
-
- pending -> failed: declined
|
|
131
|
-
- "paid -> [*]"
|
|
132
|
-
```
|
|
133
|
-
|
|
134
|
-
Sequence diagrams use `participants` (in left-to-right order) and `messages`:
|
|
135
|
-
|
|
136
|
-
```yaml
|
|
137
|
-
type: sequence
|
|
138
|
-
participants:
|
|
139
|
-
web: { label: Web app, kind: client }
|
|
140
|
-
api: { label: Checkout API }
|
|
141
|
-
stripe: { kind: external }
|
|
142
|
-
messages:
|
|
143
|
-
- web ==> api: POST /checkout # -> call; ==> highlighted call
|
|
144
|
-
- api -> stripe: create PaymentIntent
|
|
145
|
-
- stripe --> api: client_secret # --> reply (dashed)
|
|
146
|
-
- api --> web: 200 OK
|
|
147
|
-
- note over api, stripe: retried with an idempotency key
|
|
148
|
-
- alt: # alt and par take branches
|
|
149
|
-
- when: payment succeeded
|
|
150
|
-
do:
|
|
151
|
-
- stripe -.-> api: webhook # -.-> async
|
|
152
|
-
- when: declined
|
|
153
|
-
do:
|
|
154
|
-
- api --> web: 402
|
|
155
|
-
- loop: every 30s # loop, opt, critical, break take one block
|
|
156
|
-
do:
|
|
157
|
-
- web -> api: poll status
|
|
158
|
-
```
|
|
159
|
-
|
|
160
|
-
Activation bars come from call/reply pairs automatically: each `a -> b` closed by a later `b --> a`. Participants take `color` and `highlight` like nodes.
|
|
161
|
-
|
|
162
|
-
ER diagrams put `fields` on nodes and use Mermaid's crow's-foot notation for edges:
|
|
163
|
-
|
|
164
|
-
```yaml
|
|
165
|
-
type: er
|
|
166
|
-
nodes:
|
|
167
|
-
users:
|
|
168
|
-
fields:
|
|
169
|
-
- id: uuid pk
|
|
170
|
-
- org_id: uuid fk
|
|
171
|
-
- email: text unique
|
|
172
|
-
orgs:
|
|
173
|
-
label: organizations
|
|
174
|
-
fields: [ "id: uuid pk", "name: text" ]
|
|
175
|
-
edges:
|
|
176
|
-
- users }o--|| orgs: belongs to # many users to exactly one org
|
|
177
|
-
```
|
|
178
|
-
|
|
179
|
-
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.
|
|
180
|
-
|
|
181
|
-
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.
|
|
182
|
-
|
|
183
|
-
## Editing an existing diagram
|
|
184
|
-
|
|
185
|
-
Always start from the file, never from an old copy of the spec:
|
|
186
|
-
|
|
187
|
-
```bash
|
|
188
|
-
linework show docs/diagrams/checkout.drawio.svg > /tmp/spec.yaml # current spec, hand edits included
|
|
189
|
-
# edit /tmp/spec.yaml; keep the "# linework-base:" line at the top
|
|
190
|
-
linework build /tmp/spec.yaml -o docs/diagrams/checkout.drawio.svg --png /tmp/checkout.png
|
|
191
|
-
```
|
|
192
|
-
|
|
193
|
-
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.)
|
|
194
|
-
- Existing nodes stay exactly where they are.
|
|
195
|
-
- New nodes go next to the node they connect to.
|
|
196
|
-
- Removed nodes disappear.
|
|
197
|
-
- Groups grow to fit new members.
|
|
198
|
-
- Only new edges, and edges whose routes went stale, get rerouted.
|
|
199
|
-
- Their colours, label edits and notes survive.
|
|
200
|
-
|
|
201
|
-
The `merged into the existing layout:` line tells you what happened. Mention it to the user.
|
|
202
|
-
|
|
203
|
-
The `# linework-base:` line tells `build` your spec includes the latest hand edits. Without it, `build` won't delete shapes someone added by hand that your spec doesn't mention. It keeps them and prints a note. If you see that note, run `show` again and redo your edit from its output.
|
|
204
|
-
|
|
205
|
-
`--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.
|
|
206
|
-
|
|
207
|
-
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.
|
|
208
|
-
|
|
209
|
-
## Putting it in a doc
|
|
210
|
-
|
|
211
|
-
- Markdown (GitHub, GitLab, most renderers): ``
|
|
212
|
-
- HTML: `<img src="diagrams/checkout.drawio.svg" alt="Checkout API calls Stripe to charge cards and writes orders to Postgres">`
|
|
213
|
-
- 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.
|
|
214
|
-
- If you used `color:` for categories, add a sentence under the diagram saying what each color means.
|
|
215
|
-
- 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.
|
|
216
|
-
- In a git repo, offer to run `linework git-setup` once, so `git diff` on diagrams shows readable spec changes.
|
|
217
|
-
- 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.
|
|
218
|
-
|
|
219
|
-
## Letting the user edit
|
|
220
|
-
|
|
221
|
-
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.
|
|
222
|
-
|
|
223
|
-
## Commands
|
|
224
|
-
|
|
225
|
-
| command | does |
|
|
226
|
-
|---|---|
|
|
227
|
-
| `build <spec\|-> -o out.drawio.svg [--png p.png] [--relayout]` | layout (or merge into an existing file), write, lint |
|
|
228
|
-
| `show <file> [--geometry]` | print the spec, stamped with `linework-base` |
|
|
229
|
-
| `tidy <file>` | reroute edges that pass through nodes; nothing else changes |
|
|
230
|
-
| `import <file.mmd\|doc.md> [--block N] [-o out.drawio.svg]` | convert Mermaid (flowchart, state, sequence, ER) |
|
|
231
|
-
| `icons <aws\|gcp\|k8s\|azure> [filter]` | list icon names |
|
|
232
|
-
| `edit <file>` | open the diagram in draw.io in the user's browser; saving writes the file |
|
|
233
|
-
| `publish <file...> [--to dir] [--pull]` | copy diagrams into Google Drive for the Docs add-on; `--pull` brings Drive edits back |
|
|
234
|
-
| `git-setup` | make `git diff` show diagram changes as spec changes |
|
|
235
|
-
| `render <file> -o out.png [--scale 2]` | preview image |
|
|
236
|
-
| `check <file>` | lint only; exit 1 on errors |
|
|
237
|
-
| `kinds [type]` | list node kinds |
|
|
238
|
-
| `schema` | JSON Schema of the spec |
|
|
239
|
-
| `skill` | print the path of the guide that matches the installed version |
|
|
240
|
-
| `install-browser` | download the headless Chromium this version uses |
|
|
93
|
+
- `references/spec.md`: every option, sequence blocks (`alt`, `loop`, notes), ER fields and cardinalities, themes, colors.
|
|
94
|
+
- `references/style-guide.md`: what makes a diagram read well. Read it before anything non-trivial.
|
|
95
|
+
- `references/troubleshooting.md`: download, browser and sandbox failures.
|
|
96
|
+
- `references/google-docs.md`: putting a diagram in Google Docs with `linework publish`.
|
|
97
|
+
- `linework help` lists every command, including `check`, `render`, `kinds`, `schema` and `git-setup` (readable `git diff` for diagrams).
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
# When a command fails
|
|
2
|
+
|
|
3
|
+
`linework` in the guide means `npx -y @itsthecsguy/linework`. Run plain `linework` only if `command -v linework` (or `where linework` on Windows) finds it.
|
|
4
|
+
|
|
5
|
+
- **The download fails** (`ENOTFOUND`, `EAI_AGAIN`, `ETIMEDOUT`, `403`): there is probably 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.
|
|
6
|
+
- **"none was found":** no browser is installed. Run `linework install-browser`, or ask the user to install Chrome.
|
|
7
|
+
- **"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.
|
|
8
|
+
- **An option or field is rejected that the guide says exists:** the 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.
|
|
9
|
+
- **A build hangs or behaves oddly after an upgrade:** builds reuse a background process that keeps the browser running. `linework daemon stop` ends it, and `LINEWORK_NO_DAEMON=1` or `--no-daemon` runs without it.
|
|
10
|
+
|
|
11
|
+
Never fall back to writing SVG or draw.io XML by hand. Tell the user what failed and what they can do about it.
|