@itsthecsguy/linework 0.0.0-stage → 0.1.1
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/LICENSE +21 -0
- package/NOTICE +11 -0
- package/README.md +206 -2
- package/assets/drawio/LICENSE +201 -0
- package/assets/drawio/VERSION +1 -0
- package/assets/drawio/favicon.ico +0 -0
- package/assets/drawio/images/1x1.png +0 -0
- package/assets/drawio/images/2x2.png +0 -0
- package/assets/drawio/images/3x3.png +0 -0
- package/assets/drawio/images/ajax-loader.gif +0 -0
- package/assets/drawio/images/android-chrome-512x512.png +0 -0
- package/assets/drawio/images/apple-touch-icon.png +0 -0
- package/assets/drawio/images/aui-wait.gif +0 -0
- package/assets/drawio/images/broken.png +0 -0
- package/assets/drawio/images/browserconfig.xml +9 -0
- package/assets/drawio/images/close.png +0 -0
- package/assets/drawio/images/cloud-connector-atlas.png +0 -0
- package/assets/drawio/images/collapsed.gif +0 -0
- package/assets/drawio/images/delete.png +0 -0
- package/assets/drawio/images/document-google-drive-icon-80.png +0 -0
- package/assets/drawio/images/draw-search.png +0 -0
- package/assets/drawio/images/drawio.svg +19 -0
- package/assets/drawio/images/drawlogo-color.svg +50 -0
- package/assets/drawio/images/drawlogo-gray.svg +18 -0
- package/assets/drawio/images/drawlogo-text-bottom.svg +75 -0
- package/assets/drawio/images/drawlogo.svg +18 -0
- package/assets/drawio/images/drawlogo128.png +0 -0
- package/assets/drawio/images/drawlogo144.png +0 -0
- package/assets/drawio/images/drawlogo256.png +0 -0
- package/assets/drawio/images/drawlogo48-gray.png +0 -0
- package/assets/drawio/images/drawlogo48.png +0 -0
- package/assets/drawio/images/drawlogo80.png +0 -0
- package/assets/drawio/images/droptarget.png +0 -0
- package/assets/drawio/images/edit.gif +0 -0
- package/assets/drawio/images/edit16.png +0 -0
- package/assets/drawio/images/edit32.png +0 -0
- package/assets/drawio/images/edit80.png +0 -0
- package/assets/drawio/images/embed-icon.png +0 -0
- package/assets/drawio/images/expanded.gif +0 -0
- package/assets/drawio/images/facebook.png +0 -0
- package/assets/drawio/images/favicon-16x16.png +0 -0
- package/assets/drawio/images/favicon-32x32.png +0 -0
- package/assets/drawio/images/file.png +0 -0
- package/assets/drawio/images/folder.png +0 -0
- package/assets/drawio/images/gadget-screenshot.png +0 -0
- package/assets/drawio/images/gadget-thumb.png +0 -0
- package/assets/drawio/images/github-logo-white.svg +6 -0
- package/assets/drawio/images/github-logo.svg +17 -0
- package/assets/drawio/images/gitlab-logo-white.svg +1 -0
- package/assets/drawio/images/gitlab-logo.svg +1 -0
- package/assets/drawio/images/glyphicons_blogger.png +0 -0
- package/assets/drawio/images/glyphicons_facebook.png +0 -0
- package/assets/drawio/images/glyphicons_github.png +0 -0
- package/assets/drawio/images/glyphicons_google.png +0 -0
- package/assets/drawio/images/glyphicons_star.png +0 -0
- package/assets/drawio/images/glyphicons_twitter.png +0 -0
- package/assets/drawio/images/glyphicons_youtube.png +0 -0
- package/assets/drawio/images/google-drive-connector-atlas.png +0 -0
- package/assets/drawio/images/google-drive-connector-atlas.svg +121 -0
- package/assets/drawio/images/google-drive-logo-white.svg +9 -0
- package/assets/drawio/images/google-drive-logo.svg +17 -0
- package/assets/drawio/images/google-share.png +0 -0
- package/assets/drawio/images/icon-192-maskable.png +0 -0
- package/assets/drawio/images/icon-192.png +0 -0
- package/assets/drawio/images/icon-512-maskable.png +0 -0
- package/assets/drawio/images/icon-512.png +0 -0
- package/assets/drawio/images/icon-search.svg +21 -0
- package/assets/drawio/images/img-hi-res.png +0 -0
- package/assets/drawio/images/img-lo-res.png +0 -0
- package/assets/drawio/images/list.svg +16 -0
- package/assets/drawio/images/locked.png +0 -0
- package/assets/drawio/images/log.png +0 -0
- package/assets/drawio/images/logo-confluence.png +0 -0
- package/assets/drawio/images/logo-flat-small.png +0 -0
- package/assets/drawio/images/logo-flat.png +0 -0
- package/assets/drawio/images/logo-jira.png +0 -0
- package/assets/drawio/images/logo-small.gif +0 -0
- package/assets/drawio/images/logo-white.png +0 -0
- package/assets/drawio/images/logo.png +0 -0
- package/assets/drawio/images/manifest.json +25 -0
- package/assets/drawio/images/move.png +0 -0
- package/assets/drawio/images/notion-logo-white.svg +1 -0
- package/assets/drawio/images/notion-logo.svg +1 -0
- package/assets/drawio/images/onedrive-connector-atlas.png +0 -0
- package/assets/drawio/images/onedrive-connector-atlas.svg +248 -0
- package/assets/drawio/images/onedrive-logo-white.svg +29 -0
- package/assets/drawio/images/onedrive-logo.svg +29 -0
- package/assets/drawio/images/osa_database.png +0 -0
- package/assets/drawio/images/osa_drive-harddisk.png +0 -0
- package/assets/drawio/images/plant-uml-atlas.png +0 -0
- package/assets/drawio/images/samepage-icon-color.svg +37 -0
- package/assets/drawio/images/save.png +0 -0
- package/assets/drawio/images/sidebar-android.png +0 -0
- package/assets/drawio/images/sidebar-archimate.png +0 -0
- package/assets/drawio/images/sidebar-arrows.png +0 -0
- package/assets/drawio/images/sidebar-arrows2.png +0 -0
- package/assets/drawio/images/sidebar-basic.png +0 -0
- package/assets/drawio/images/sidebar-c4.png +0 -0
- package/assets/drawio/images/sidebar-cabinets.png +0 -0
- package/assets/drawio/images/sidebar-cumulus.png +0 -0
- package/assets/drawio/images/sidebar-dfd.png +0 -0
- package/assets/drawio/images/sidebar-dynamics365.png +0 -0
- package/assets/drawio/images/sidebar-eip.png +0 -0
- package/assets/drawio/images/sidebar-er.png +0 -0
- package/assets/drawio/images/sidebar-floorplans.png +0 -0
- package/assets/drawio/images/sidebar-flowchart.png +0 -0
- package/assets/drawio/images/sidebar-gcp3.png +0 -0
- package/assets/drawio/images/sidebar-general.png +0 -0
- package/assets/drawio/images/sidebar-kubernetes.png +0 -0
- package/assets/drawio/images/sidebar-leanmapping.png +0 -0
- package/assets/drawio/images/sidebar-mockups.png +0 -0
- package/assets/drawio/images/sidebar-network.png +0 -0
- package/assets/drawio/images/sidebar-network2.png +0 -0
- package/assets/drawio/images/sidebar-openstack.png +0 -0
- package/assets/drawio/images/sidebar-sitemap.png +0 -0
- package/assets/drawio/images/sidebar-threatmodeling.png +0 -0
- package/assets/drawio/images/sidebar-uml.png +0 -0
- package/assets/drawio/images/sketch-logo128.png +0 -0
- package/assets/drawio/images/spin.gif +0 -0
- package/assets/drawio/images/sprites.png +0 -0
- package/assets/drawio/images/stop-flat-icon-80.png +0 -0
- package/assets/drawio/images/submenu.gif +0 -0
- package/assets/drawio/images/touch-submenu.png +0 -0
- package/assets/drawio/images/transparent.gif +0 -0
- package/assets/drawio/images/trello-logo-white-orange.svg +6 -0
- package/assets/drawio/images/trello-logo-white.svg +6 -0
- package/assets/drawio/images/trello-logo.svg +1 -0
- package/assets/drawio/images/tweet.png +0 -0
- package/assets/drawio/images/update16.png +0 -0
- package/assets/drawio/images/update32.png +0 -0
- package/assets/drawio/images/update80.png +0 -0
- package/assets/drawio/images/youtube.png +0 -0
- package/assets/drawio/img/clipart/Battery_0_128x128.png +0 -0
- package/assets/drawio/img/clipart/Battery_100_128x128.png +0 -0
- package/assets/drawio/img/clipart/Battery_50_128x128.png +0 -0
- package/assets/drawio/img/clipart/Battery_75_128x128.png +0 -0
- package/assets/drawio/img/clipart/Battery_allstates_128x128.png +0 -0
- package/assets/drawio/img/clipart/Bluetooth_128x128.png +0 -0
- package/assets/drawio/img/clipart/Earth_globe_128x128.png +0 -0
- package/assets/drawio/img/clipart/Empty_Folder_128x128.png +0 -0
- package/assets/drawio/img/clipart/Full_Folder_128x128.png +0 -0
- package/assets/drawio/img/clipart/Gear_128x128.png +0 -0
- package/assets/drawio/img/clipart/Keys_128x128.png +0 -0
- package/assets/drawio/img/clipart/Lock_128x128.png +0 -0
- package/assets/drawio/img/clipart/Mouse_Pointer_128x128.png +0 -0
- package/assets/drawio/img/clipart/Plug_128x128.png +0 -0
- package/assets/drawio/img/clipart/Ships_Wheel_128x128.png +0 -0
- package/assets/drawio/img/clipart/Star_128x128.png +0 -0
- package/assets/drawio/img/clipart/Tire_128x128.png +0 -0
- package/assets/drawio/index.html +92 -0
- package/assets/drawio/js/PostConfig.js +7 -0
- package/assets/drawio/js/PreConfig.js +13 -0
- package/assets/drawio/js/app.min.js +18466 -0
- package/assets/drawio/js/bootstrap.js +428 -0
- package/assets/drawio/js/diagramly/LibavoidRouting.js +4459 -0
- package/assets/drawio/js/export-init.js +21 -0
- package/assets/drawio/js/extensions.min.js +10478 -0
- package/assets/drawio/js/libavoid-js/LICENSE +505 -0
- package/assets/drawio/js/libavoid-js/libavoid-routing.js +1633 -0
- package/assets/drawio/js/libavoid-js/libavoid.min.js +59 -0
- package/assets/drawio/js/main.js +22 -0
- package/assets/drawio/js/rough/rough.min.js +1 -0
- package/assets/drawio/js/shapes-14-6-5.min.js +3457 -0
- package/assets/drawio/js/stencils.min.js +447 -0
- package/assets/drawio/mxgraph/css/common.css +147 -0
- package/assets/drawio/resources/dia.txt +2901 -0
- package/assets/drawio/styles/dark-default.xml +109 -0
- package/assets/drawio/styles/default-old.xml +109 -0
- package/assets/drawio/styles/default.xml +203 -0
- package/assets/drawio/styles/fonts/ArchitectsDaughter-Regular.ttf +0 -0
- package/assets/drawio/styles/grapheditor.css +4501 -0
- package/assets/drawio/styles/high-contrast.css +128 -0
- package/assets/drawio/styles/thumb_horz.png +0 -0
- package/assets/drawio/styles/thumb_vertical.png +0 -0
- package/assets/fonts/ibm-plex-mono-LICENSE.txt +93 -0
- package/assets/fonts/ibm-plex-sans-LICENSE.txt +93 -0
- package/assets/fonts/kalam-400.woff2 +0 -0
- package/assets/fonts/kalam-700.woff2 +0 -0
- package/assets/fonts/kalam-LICENSE.txt +93 -0
- package/assets/fonts/plex-mono-400.woff2 +0 -0
- package/assets/fonts/plex-mono-600.woff2 +0 -0
- package/assets/fonts/plex-sans-400.woff2 +0 -0
- package/assets/fonts/plex-sans-600.woff2 +0 -0
- package/dist/api.js +218 -0
- package/dist/cli.js +275 -0
- package/dist/drawio/icons.js +121 -0
- package/dist/drawio/merge.js +309 -0
- package/dist/drawio/read.js +180 -0
- package/dist/drawio/show.js +501 -0
- package/dist/drawio/theme.js +425 -0
- package/dist/drawio/write.js +300 -0
- package/dist/edit/server.js +212 -0
- package/dist/git.js +46 -0
- package/dist/import/mermaid.js +569 -0
- package/dist/layout/layout.js +212 -0
- package/dist/layout/sequence.js +333 -0
- package/dist/layout/text.js +73 -0
- package/dist/lint/check.js +188 -0
- package/dist/lint/fixlabels.js +128 -0
- package/dist/mcp/server.js +239 -0
- package/dist/publish.js +121 -0
- package/dist/render/browser.js +236 -0
- package/dist/render/fonts.js +66 -0
- package/dist/report.js +36 -0
- package/dist/spec/model.js +53 -0
- package/dist/spec/parse.js +497 -0
- package/package.json +71 -4
- package/skills/linework/SKILL.md +233 -0
- package/skills/linework/examples/billing-er.yaml +31 -0
- package/skills/linework/examples/checkout.yaml +28 -0
- package/skills/linework/examples/pay-sequence.yaml +28 -0
- package/skills/linework/examples/payment-state.yaml +22 -0
- package/skills/linework/examples/refund-flow.yaml +29 -0
- package/skills/linework/examples/serverless-aws.yaml +23 -0
- package/skills/linework/references/google-docs.md +46 -0
- package/skills/linework/references/spec.md +312 -0
- package/skills/linework/references/style-guide.md +53 -0
|
@@ -0,0 +1,497 @@
|
|
|
1
|
+
// YAML spec -> Diagram. Every error says where it is and how to fix it,
|
|
2
|
+
// because the reader is usually a model that will retry.
|
|
3
|
+
import YAML from 'yaml';
|
|
4
|
+
import { parseIcon } from '../drawio/icons.js';
|
|
5
|
+
import { z } from 'zod';
|
|
6
|
+
import { DEFAULT_KIND, DIAGRAM_TYPES, DIRECTIONS, FRAME_OPS, KINDS, THEMES, COLORS, } from './model.js';
|
|
7
|
+
const scalar = z.union([z.string(), z.number(), z.boolean()]).transform(String);
|
|
8
|
+
const FieldInput = z.union([
|
|
9
|
+
scalar,
|
|
10
|
+
z.record(z.string(), z.union([scalar, z.null()])),
|
|
11
|
+
z.strictObject({ name: scalar, type: scalar.optional(), flags: z.array(z.string()).optional() }),
|
|
12
|
+
]);
|
|
13
|
+
// Checked by hand so a wrong name gets a helpful message.
|
|
14
|
+
const ColorInput = z.string();
|
|
15
|
+
const HighlightInput = z.boolean();
|
|
16
|
+
const NodeInput = z.union([
|
|
17
|
+
scalar,
|
|
18
|
+
z.strictObject({
|
|
19
|
+
label: scalar.optional(),
|
|
20
|
+
kind: z.string().optional(),
|
|
21
|
+
tech: scalar.optional(),
|
|
22
|
+
in: z.string().optional(),
|
|
23
|
+
icon: z.string().optional(),
|
|
24
|
+
fields: z.array(FieldInput).optional(),
|
|
25
|
+
color: ColorInput.optional(),
|
|
26
|
+
highlight: HighlightInput.optional(),
|
|
27
|
+
}),
|
|
28
|
+
]);
|
|
29
|
+
const GroupInput = z.union([
|
|
30
|
+
scalar,
|
|
31
|
+
z.strictObject({
|
|
32
|
+
label: scalar.optional(),
|
|
33
|
+
in: z.string().optional(),
|
|
34
|
+
color: ColorInput.optional(),
|
|
35
|
+
highlight: HighlightInput.optional(),
|
|
36
|
+
}),
|
|
37
|
+
]);
|
|
38
|
+
const EdgeObject = z.strictObject({
|
|
39
|
+
from: z.string(),
|
|
40
|
+
to: z.string(),
|
|
41
|
+
label: scalar.optional(),
|
|
42
|
+
style: z.enum(['sync', 'async', 'bidir', 'line']).optional(),
|
|
43
|
+
color: ColorInput.optional(),
|
|
44
|
+
highlight: HighlightInput.optional(),
|
|
45
|
+
});
|
|
46
|
+
const EdgeInput = z.union([z.string(), EdgeObject, z.record(z.string(), z.union([scalar, z.null()]))]);
|
|
47
|
+
// Messages: "a -> b: text", "note over a, b: text", or a frame object.
|
|
48
|
+
const MessageInput = z.lazy(() => z.union([z.string(), z.record(z.string(), z.unknown())]));
|
|
49
|
+
export const SpecInput = z.strictObject({
|
|
50
|
+
type: z.enum(DIAGRAM_TYPES),
|
|
51
|
+
title: scalar.optional(),
|
|
52
|
+
direction: z.enum(DIRECTIONS).optional(),
|
|
53
|
+
theme: z.enum(THEMES).optional(),
|
|
54
|
+
groups: z.record(z.string(), GroupInput).optional(),
|
|
55
|
+
nodes: z.record(z.string(), NodeInput).optional(),
|
|
56
|
+
edges: z.array(EdgeInput).optional(),
|
|
57
|
+
participants: z.record(z.string(), NodeInput).optional(),
|
|
58
|
+
messages: z.array(MessageInput).optional(),
|
|
59
|
+
});
|
|
60
|
+
export class SpecError extends Error {
|
|
61
|
+
issues;
|
|
62
|
+
constructor(issues) {
|
|
63
|
+
super(issues.map((i) => `${i.path ? i.path + ': ' : ''}${i.message}`).join('\n'));
|
|
64
|
+
this.issues = issues;
|
|
65
|
+
this.name = 'SpecError';
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
const ID_RE = /^[A-Za-z_][\w.-]*$/;
|
|
69
|
+
const MSG_ARROWS = { '->': 'sync', '==>': 'sync', '-->': 'reply', '-.->': 'async' };
|
|
70
|
+
const MSG_RE = /^\s*(\S+)\s+(-->|-\.->|==>|->)\s+(\S+)\s*$/;
|
|
71
|
+
const NOTE_RE = /^\s*note\s+(over|left of|right of)\s+(.+?)\s*$/i;
|
|
72
|
+
const ER_RE = /^\s*(\S+)\s+([|}][o|])(--|\.\.)([o|][|{])\s+(\S+)\s*$/;
|
|
73
|
+
const CARD_LEFT = { '|o': 'zero_or_one', '||': 'one', '}o': 'many', '}|': 'one_or_many' };
|
|
74
|
+
const CARD_RIGHT = { 'o|': 'zero_or_one', '||': 'one', 'o{': 'many', '|{': 'one_or_many' };
|
|
75
|
+
const FIELD_FLAGS = new Set(['pk', 'fk', 'uk', 'unique', 'null', 'nullable', 'not_null']);
|
|
76
|
+
const ARROWS = { '->': 'sync', '==>': 'sync', '-.->': 'async', '<->': 'bidir', '--': 'line' };
|
|
77
|
+
const EDGE_RE = /^\s*(\S+)\s+(<->|-\.->|==>|->|--)\s+(\S+)\s*$/;
|
|
78
|
+
export const START_ID = '_start';
|
|
79
|
+
export const END_ID = '_end';
|
|
80
|
+
export function parseSpecText(text) {
|
|
81
|
+
let raw;
|
|
82
|
+
try {
|
|
83
|
+
raw = YAML.parse(text);
|
|
84
|
+
}
|
|
85
|
+
catch (e) {
|
|
86
|
+
throw new SpecError([{ path: '', message: `invalid YAML: ${e.message}` }]);
|
|
87
|
+
}
|
|
88
|
+
return parseSpec(raw);
|
|
89
|
+
}
|
|
90
|
+
export function parseSpec(raw) {
|
|
91
|
+
const res = SpecInput.safeParse(raw);
|
|
92
|
+
if (!res.success)
|
|
93
|
+
throw new SpecError(res.error.issues.map(zodIssue));
|
|
94
|
+
const spec = res.data;
|
|
95
|
+
const type = spec.type;
|
|
96
|
+
const issues = [];
|
|
97
|
+
const warnings = [];
|
|
98
|
+
if (type === 'sequence')
|
|
99
|
+
return parseSequence(spec);
|
|
100
|
+
if (spec.participants || spec.messages) {
|
|
101
|
+
throw new SpecError([{ path: spec.participants ? 'participants' : 'messages', message: `participants and messages are for type: sequence. A ${type} diagram uses nodes and edges.` }]);
|
|
102
|
+
}
|
|
103
|
+
if (!spec.nodes)
|
|
104
|
+
throw new SpecError([{ path: 'nodes', message: 'nodes is required: a map of id -> { label, kind, tech, in }' }]);
|
|
105
|
+
const groups = [];
|
|
106
|
+
for (const [id, g] of Object.entries(spec.groups ?? {})) {
|
|
107
|
+
if (!ID_RE.test(id))
|
|
108
|
+
issues.push({ path: `groups.${id}`, message: badId(id) });
|
|
109
|
+
const obj = typeof g === 'string' ? { label: g } : g;
|
|
110
|
+
groups.push({ id, label: obj.label ?? humanize(id), parent: obj.in, ...look(obj, `groups.${id}`, issues) });
|
|
111
|
+
}
|
|
112
|
+
const groupIds = new Set(groups.map((g) => g.id));
|
|
113
|
+
const nodes = [];
|
|
114
|
+
for (const [id, n] of Object.entries(spec.nodes)) {
|
|
115
|
+
const path = `nodes.${id}`;
|
|
116
|
+
if (!ID_RE.test(id))
|
|
117
|
+
issues.push({ path, message: badId(id) });
|
|
118
|
+
if (groupIds.has(id))
|
|
119
|
+
issues.push({ path, message: `"${id}" is already a group id. Node and group ids must be unique.` });
|
|
120
|
+
const obj = typeof n === 'string' ? { label: n } : n;
|
|
121
|
+
const kind = obj.kind ?? DEFAULT_KIND[type];
|
|
122
|
+
if (!KINDS[type][kind]) {
|
|
123
|
+
issues.push({ path: `${path}.kind`, message: `unknown kind "${kind}" for a ${type} diagram. Use one of: ${Object.keys(KINDS[type]).join(', ')}.` });
|
|
124
|
+
}
|
|
125
|
+
const node = { id, label: obj.label ?? humanize(id), kind, tech: obj.tech, parent: obj.in, ...look(obj, path, issues) };
|
|
126
|
+
if ('icon' in obj && obj.icon) {
|
|
127
|
+
const ic = parseIcon(obj.icon);
|
|
128
|
+
if (type !== 'architecture')
|
|
129
|
+
warnings.push({ path: `${path}.icon`, message: 'icons only show on architecture diagrams' });
|
|
130
|
+
else if ('error' in ic)
|
|
131
|
+
issues.push({ path: `${path}.icon`, message: ic.error });
|
|
132
|
+
else
|
|
133
|
+
node.icon = `${ic.provider}.${ic.name}`;
|
|
134
|
+
}
|
|
135
|
+
if ('fields' in obj && obj.fields) {
|
|
136
|
+
if (type !== 'er')
|
|
137
|
+
issues.push({ path: `${path}.fields`, message: 'fields are for type: er' });
|
|
138
|
+
else
|
|
139
|
+
node.fields = obj.fields.map((f, i) => parseField(f, `${path}.fields[${i}]`, issues)).filter((f) => !!f);
|
|
140
|
+
}
|
|
141
|
+
nodes.push(node);
|
|
142
|
+
}
|
|
143
|
+
const nodeIds = new Set(nodes.map((n) => n.id));
|
|
144
|
+
if (nodes.length === 0)
|
|
145
|
+
issues.push({ path: 'nodes', message: 'add at least one node' });
|
|
146
|
+
for (const item of [...groups, ...nodes]) {
|
|
147
|
+
if (item.parent && !groupIds.has(item.parent)) {
|
|
148
|
+
const where = groupIds.has(item.id) ? 'groups' : 'nodes';
|
|
149
|
+
issues.push({ path: `${where}.${item.id}.in`, message: `group "${item.parent}" doesn't exist.${suggest(item.parent, groupIds)}` });
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
for (const g of groups) {
|
|
153
|
+
const seen = new Set();
|
|
154
|
+
for (let p = g.id; p; p = groups.find((x) => x.id === p)?.parent) {
|
|
155
|
+
if (seen.has(p)) {
|
|
156
|
+
issues.push({ path: `groups.${g.id}.in`, message: 'groups are nested in a cycle' });
|
|
157
|
+
break;
|
|
158
|
+
}
|
|
159
|
+
seen.add(p);
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
// Edges
|
|
163
|
+
const edges = [];
|
|
164
|
+
const counts = new Map();
|
|
165
|
+
let needStart = false;
|
|
166
|
+
const endFor = new Set(); // one end dot per source keeps edges from piling up
|
|
167
|
+
(spec.edges ?? []).forEach((item, i) => {
|
|
168
|
+
const path = `edges[${i}]`;
|
|
169
|
+
let parsed = null;
|
|
170
|
+
if (type === 'er') {
|
|
171
|
+
let text = null;
|
|
172
|
+
if (typeof item === 'string')
|
|
173
|
+
text = item;
|
|
174
|
+
else if (!('from' in item)) {
|
|
175
|
+
const entries = Object.entries(item);
|
|
176
|
+
if (entries.length === 1)
|
|
177
|
+
text = entries[0][1] == null ? entries[0][0] : `${entries[0][0]}: ${entries[0][1]}`;
|
|
178
|
+
}
|
|
179
|
+
if (text == null) {
|
|
180
|
+
issues.push({ path, message: 'write ER relationships like "- users }o--|| orgs: belongs to"' });
|
|
181
|
+
return;
|
|
182
|
+
}
|
|
183
|
+
parsed = parseErEdge(text, path, issues);
|
|
184
|
+
}
|
|
185
|
+
else if (typeof item === 'string')
|
|
186
|
+
parsed = parseEdgeString(item, path, issues);
|
|
187
|
+
else if ('from' in item && 'to' in item && typeof item.from === 'string') {
|
|
188
|
+
const o = item;
|
|
189
|
+
parsed = { from: o.from, to: o.to, label: o.label, style: o.style ?? 'sync', ...look(o, path, issues) };
|
|
190
|
+
}
|
|
191
|
+
else {
|
|
192
|
+
const entries = Object.entries(item);
|
|
193
|
+
if (entries.length !== 1) {
|
|
194
|
+
issues.push({ path, message: 'write each edge as one line, like "- api -> db: SQL"' });
|
|
195
|
+
return;
|
|
196
|
+
}
|
|
197
|
+
const [head, label] = entries[0];
|
|
198
|
+
parsed = parseEdgeString(label == null ? head : `${head}: ${label}`, path, issues);
|
|
199
|
+
}
|
|
200
|
+
if (!parsed)
|
|
201
|
+
return;
|
|
202
|
+
for (const end of ['from', 'to']) {
|
|
203
|
+
const id = parsed[end];
|
|
204
|
+
if (id === '[*]') {
|
|
205
|
+
if (type !== 'state') {
|
|
206
|
+
issues.push({ path, message: '[*] is only for state diagrams. Add a terminal node instead.' });
|
|
207
|
+
return;
|
|
208
|
+
}
|
|
209
|
+
if (end === 'from') {
|
|
210
|
+
parsed.from = START_ID;
|
|
211
|
+
needStart = true;
|
|
212
|
+
}
|
|
213
|
+
else {
|
|
214
|
+
endFor.add(parsed.from);
|
|
215
|
+
parsed.to = `${END_ID}_${parsed.from === START_ID ? 'start' : parsed.from}`;
|
|
216
|
+
}
|
|
217
|
+
continue;
|
|
218
|
+
}
|
|
219
|
+
if (groupIds.has(id)) {
|
|
220
|
+
issues.push({ path, message: `"${id}" is a group. Connect edges to nodes inside it instead.` });
|
|
221
|
+
return;
|
|
222
|
+
}
|
|
223
|
+
if (!nodeIds.has(id))
|
|
224
|
+
issues.push({ path, message: `node "${id}" doesn't exist.${suggest(id, nodeIds)} Declare it under nodes: first.` });
|
|
225
|
+
}
|
|
226
|
+
const base = `${parsed.from}->${parsed.to}`;
|
|
227
|
+
const n = (counts.get(base) ?? 0) + 1;
|
|
228
|
+
counts.set(base, n);
|
|
229
|
+
edges.push({ id: n === 1 ? base : `${base}#${n}`, ...parsed });
|
|
230
|
+
});
|
|
231
|
+
if (needStart && !nodeIds.has(START_ID))
|
|
232
|
+
nodes.unshift({ id: START_ID, label: '', kind: 'start' });
|
|
233
|
+
for (const from of endFor)
|
|
234
|
+
nodes.push({ id: `${END_ID}_${from === START_ID ? 'start' : from}`, label: '', kind: 'end' });
|
|
235
|
+
if (issues.length)
|
|
236
|
+
throw new SpecError(issues);
|
|
237
|
+
// Warnings: things that produce worse diagrams but still render.
|
|
238
|
+
const real = nodes.filter((n) => n.kind !== 'start' && n.kind !== 'end');
|
|
239
|
+
if (real.length > 20)
|
|
240
|
+
warnings.push({ path: 'nodes', message: `${real.length} nodes is a lot for one diagram. Consider splitting it; 12 or fewer reads best.` });
|
|
241
|
+
if (type === 'flow') {
|
|
242
|
+
for (const n of nodes.filter((n) => n.kind === 'decision')) {
|
|
243
|
+
const out = edges.filter((e) => e.from === n.id);
|
|
244
|
+
if (out.some((e) => !e.label))
|
|
245
|
+
warnings.push({ path: `nodes.${n.id}`, message: 'label every edge leaving a decision, e.g. "yes"/"no"' });
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
highlightWarning(real, edges, warnings);
|
|
249
|
+
const linked = new Set(edges.flatMap((e) => [e.from, e.to]));
|
|
250
|
+
for (const n of real)
|
|
251
|
+
if (edges.length && !linked.has(n.id))
|
|
252
|
+
warnings.push({ path: `nodes.${n.id}`, message: 'not connected to anything' });
|
|
253
|
+
return {
|
|
254
|
+
diagram: { type, title: spec.title, direction: spec.direction ?? 'auto', theme: spec.theme ?? 'clean', groups, nodes, edges },
|
|
255
|
+
warnings,
|
|
256
|
+
};
|
|
257
|
+
}
|
|
258
|
+
function parseErEdge(s, path, issues) {
|
|
259
|
+
const colon = s.indexOf(':');
|
|
260
|
+
const head = colon >= 0 ? s.slice(0, colon) : s;
|
|
261
|
+
const label = colon >= 0 ? s.slice(colon + 1).trim() : '';
|
|
262
|
+
const m = ER_RE.exec(head);
|
|
263
|
+
if (!m) {
|
|
264
|
+
issues.push({
|
|
265
|
+
path,
|
|
266
|
+
message: `can't read relationship "${s}". Use Mermaid crow's-foot notation with spaces around it, e.g. "orders }o--|| users: placed by". Ends: || one, |o zero or one, }o / o{ many, }| / |{ one or many. Middle: -- identifying, .. non-identifying.`,
|
|
267
|
+
});
|
|
268
|
+
return null;
|
|
269
|
+
}
|
|
270
|
+
return {
|
|
271
|
+
from: m[1],
|
|
272
|
+
to: m[5],
|
|
273
|
+
label: label || undefined,
|
|
274
|
+
style: 'line',
|
|
275
|
+
card: { from: CARD_LEFT[m[2]], to: CARD_RIGHT[m[4]], identifying: m[3] === '--' },
|
|
276
|
+
};
|
|
277
|
+
}
|
|
278
|
+
function parseField(f, path, issues) {
|
|
279
|
+
let name, rest;
|
|
280
|
+
if (typeof f === 'string' || typeof f === 'number' || typeof f === 'boolean') {
|
|
281
|
+
const parts = String(f).trim().split(/\s+/);
|
|
282
|
+
name = parts.shift() ?? '';
|
|
283
|
+
rest = parts.join(' ');
|
|
284
|
+
}
|
|
285
|
+
else if (f && typeof f === 'object' && Object.keys(f).length === 1) {
|
|
286
|
+
// "- name: text" is a field called "name" of type text, not the long form.
|
|
287
|
+
const [k, v] = Object.entries(f)[0];
|
|
288
|
+
name = k;
|
|
289
|
+
rest = v == null ? '' : String(v);
|
|
290
|
+
}
|
|
291
|
+
else if (f && typeof f === 'object' && 'name' in f) {
|
|
292
|
+
const o = f;
|
|
293
|
+
return { name: String(o.name), type: o.type, flags: (o.flags ?? []).map((x) => x.toLowerCase()) };
|
|
294
|
+
}
|
|
295
|
+
else {
|
|
296
|
+
issues.push({ path, message: 'write a field as "name: type flags", e.g. "- id: uuid pk"' });
|
|
297
|
+
return null;
|
|
298
|
+
}
|
|
299
|
+
const flags = [];
|
|
300
|
+
const type = [];
|
|
301
|
+
for (const tok of rest.split(/\s+/).filter(Boolean))
|
|
302
|
+
(FIELD_FLAGS.has(tok.toLowerCase()) ? flags : type).push(tok.toLowerCase() === 'nullable' ? 'null' : FIELD_FLAGS.has(tok.toLowerCase()) ? tok.toLowerCase() : tok);
|
|
303
|
+
return { name, type: type.join(' ') || undefined, flags };
|
|
304
|
+
}
|
|
305
|
+
// ---------- sequence ----------
|
|
306
|
+
function parseSequence(spec) {
|
|
307
|
+
const issues = [];
|
|
308
|
+
const warnings = [];
|
|
309
|
+
for (const k of ['nodes', 'edges', 'groups', 'direction'])
|
|
310
|
+
if (spec[k] !== undefined)
|
|
311
|
+
issues.push({ path: k, message: `a sequence diagram uses participants and messages, not ${k}` });
|
|
312
|
+
if (!spec.participants)
|
|
313
|
+
issues.push({ path: 'participants', message: 'participants is required: an ordered map of id -> { label, kind }' });
|
|
314
|
+
const nodes = [];
|
|
315
|
+
for (const [id, n] of Object.entries(spec.participants ?? {})) {
|
|
316
|
+
const path = `participants.${id}`;
|
|
317
|
+
if (!ID_RE.test(id))
|
|
318
|
+
issues.push({ path, message: badId(id) });
|
|
319
|
+
const obj = typeof n === 'string' ? { label: n } : n;
|
|
320
|
+
const kind = obj.kind ?? 'service';
|
|
321
|
+
if (!KINDS.sequence[kind])
|
|
322
|
+
issues.push({ path: `${path}.kind`, message: `unknown kind "${kind}" for a sequence diagram. Use one of: ${Object.keys(KINDS.sequence).join(', ')}.` });
|
|
323
|
+
if ('fields' in obj && obj.fields)
|
|
324
|
+
issues.push({ path: `${path}.fields`, message: 'fields are for type: er' });
|
|
325
|
+
nodes.push({ id, label: obj.label ?? humanize(id), kind, tech: obj.tech, ...look(obj, path, issues) });
|
|
326
|
+
}
|
|
327
|
+
const ids = new Set(nodes.map((n) => n.id));
|
|
328
|
+
const need = (id, path) => {
|
|
329
|
+
if (!ids.has(id))
|
|
330
|
+
issues.push({ path, message: `participant "${id}" doesn't exist.${suggest(id, ids)} Declare it under participants: first.` });
|
|
331
|
+
};
|
|
332
|
+
const items = (list, path) => list.flatMap((item, i) => {
|
|
333
|
+
const p = `${path}[${i}]`;
|
|
334
|
+
const r = seqItem(item, p);
|
|
335
|
+
return r ? [r] : [];
|
|
336
|
+
});
|
|
337
|
+
const seqItem = (item, path) => {
|
|
338
|
+
if (typeof item === 'string')
|
|
339
|
+
return seqString(item, path);
|
|
340
|
+
if (!item || typeof item !== 'object') {
|
|
341
|
+
issues.push({ path, message: 'expected a message like "a -> b: text"' });
|
|
342
|
+
return null;
|
|
343
|
+
}
|
|
344
|
+
const o = item;
|
|
345
|
+
const op = Object.keys(o).find((k) => FRAME_OPS.includes(k));
|
|
346
|
+
if (op) {
|
|
347
|
+
const v = o[op];
|
|
348
|
+
if (op === 'alt' || op === 'par') {
|
|
349
|
+
if (!Array.isArray(v)) {
|
|
350
|
+
issues.push({ path, message: `${op} takes a list of branches: - ${op}: [ { when: condition, do: [messages] }, ... ]` });
|
|
351
|
+
return null;
|
|
352
|
+
}
|
|
353
|
+
const branches = v.map((b, j) => {
|
|
354
|
+
const bo = (b ?? {});
|
|
355
|
+
if (!Array.isArray(bo.do))
|
|
356
|
+
issues.push({ path: `${path}.${op}[${j}]`, message: 'each branch needs do: [messages]' });
|
|
357
|
+
return { when: bo.when == null ? undefined : String(bo.when), items: items(Array.isArray(bo.do) ? bo.do : [], `${path}.${op}[${j}].do`) };
|
|
358
|
+
});
|
|
359
|
+
return { kind: 'frame', op, branches };
|
|
360
|
+
}
|
|
361
|
+
if (!Array.isArray(o.do)) {
|
|
362
|
+
issues.push({ path, message: `${op} needs do: [messages], e.g. - ${op}: every 5s\n do:\n - a -> b: poll` });
|
|
363
|
+
return null;
|
|
364
|
+
}
|
|
365
|
+
return { kind: 'frame', op, branches: [{ when: v == null ? undefined : String(v), items: items(o.do, `${path}.do`) }] };
|
|
366
|
+
}
|
|
367
|
+
const entries = Object.entries(o);
|
|
368
|
+
if (entries.length === 1) {
|
|
369
|
+
const [head, label] = entries[0];
|
|
370
|
+
return seqString(label == null ? head : `${head}: ${label}`, path);
|
|
371
|
+
}
|
|
372
|
+
issues.push({ path, message: `unknown item. Use a message "a -> b: text", a note "note over a: text", or a frame (${FRAME_OPS.join(', ')}).` });
|
|
373
|
+
return null;
|
|
374
|
+
};
|
|
375
|
+
const seqString = (s, path) => {
|
|
376
|
+
const colon = s.indexOf(':');
|
|
377
|
+
const head = colon >= 0 ? s.slice(0, colon) : s;
|
|
378
|
+
const text = colon >= 0 ? s.slice(colon + 1).trim() : '';
|
|
379
|
+
const note = NOTE_RE.exec(head);
|
|
380
|
+
if (note) {
|
|
381
|
+
const over = note[2].split(',').map((x) => x.trim()).filter(Boolean);
|
|
382
|
+
over.forEach((id) => need(id, path));
|
|
383
|
+
const side = note[1].toLowerCase().startsWith('left') ? 'left' : note[1].toLowerCase().startsWith('right') ? 'right' : 'over';
|
|
384
|
+
return { kind: 'note', over, side, text };
|
|
385
|
+
}
|
|
386
|
+
const m = MSG_RE.exec(head);
|
|
387
|
+
if (!m) {
|
|
388
|
+
issues.push({ path, message: `can't read "${s}". Messages: "a -> b: call", "b --> a: reply", "a -.-> b: async", "a ==> b: highlighted call". Notes: "note over a: text". Spaces around the arrow.` });
|
|
389
|
+
return null;
|
|
390
|
+
}
|
|
391
|
+
need(m[1], path);
|
|
392
|
+
need(m[3], path);
|
|
393
|
+
return { kind: 'msg', from: m[1], to: m[3], label: text || undefined, style: MSG_ARROWS[m[2]], ...(m[2] === '==>' ? { highlight: true } : {}) };
|
|
394
|
+
};
|
|
395
|
+
const seq = items(spec.messages ?? [], 'messages');
|
|
396
|
+
if (issues.length)
|
|
397
|
+
throw new SpecError(issues);
|
|
398
|
+
const msgs = [];
|
|
399
|
+
const flat = (xs) => xs.forEach((x) => (x.kind === 'msg' ? msgs.push(x) : x.kind === 'frame' ? x.branches.forEach((b) => flat(b.items)) : 0));
|
|
400
|
+
flat(seq);
|
|
401
|
+
highlightWarning(nodes, msgs, warnings);
|
|
402
|
+
if (nodes.length > 8)
|
|
403
|
+
warnings.push({ path: 'participants', message: `${nodes.length} participants is wide; 6 or fewer fits a doc page` });
|
|
404
|
+
return { diagram: { type: 'sequence', title: spec.title, direction: 'auto', theme: spec.theme ?? 'clean', groups: [], nodes, edges: [], seq }, warnings };
|
|
405
|
+
}
|
|
406
|
+
function parseEdgeString(s, path, issues) {
|
|
407
|
+
const colon = s.indexOf(':');
|
|
408
|
+
const head = colon >= 0 ? s.slice(0, colon) : s;
|
|
409
|
+
const label = colon >= 0 ? s.slice(colon + 1).trim() : '';
|
|
410
|
+
const m = EDGE_RE.exec(head);
|
|
411
|
+
if (!m) {
|
|
412
|
+
issues.push({ path, message: `can't read edge "${s}". Write it as "from -> to: label", with spaces around the arrow. Arrows: -> (call), ==> (highlighted call), -.-> (async), <-> (two-way), -- (no arrow).` });
|
|
413
|
+
return null;
|
|
414
|
+
}
|
|
415
|
+
return { from: m[1], to: m[3], label: label || undefined, style: ARROWS[m[2]], ...(m[2] === '==>' ? { highlight: true } : {}) };
|
|
416
|
+
}
|
|
417
|
+
function look(o, path, issues) {
|
|
418
|
+
const out = {};
|
|
419
|
+
if (o.color != null) {
|
|
420
|
+
const c = o.color.toLowerCase();
|
|
421
|
+
if (COLORS.includes(c))
|
|
422
|
+
out.color = c;
|
|
423
|
+
else
|
|
424
|
+
issues.push({ path: `${path}.color`, message: `unknown color "${o.color}". Use one of: ${COLORS.join(', ')}.` });
|
|
425
|
+
}
|
|
426
|
+
if (o.highlight)
|
|
427
|
+
out.highlight = true;
|
|
428
|
+
return out;
|
|
429
|
+
}
|
|
430
|
+
/** Emphasis only works when it's rare. */
|
|
431
|
+
function highlightWarning(nodes, edges, warnings) {
|
|
432
|
+
const hn = nodes.filter((i) => i.highlight).length, he = edges.filter((i) => i.highlight).length;
|
|
433
|
+
if (hn > Math.max(2, nodes.length / 4) || he > Math.max(4, edges.length / 3))
|
|
434
|
+
warnings.push({
|
|
435
|
+
path: 'highlight',
|
|
436
|
+
message: `${hn} nodes and ${he} edges are highlighted. Highlight the one path or part the reader should see first; with this many, nothing stands out. Use color: for categories instead.`,
|
|
437
|
+
});
|
|
438
|
+
}
|
|
439
|
+
function zodIssue(i) {
|
|
440
|
+
const path = i.path.map((p) => (typeof p === 'number' ? `[${p}]` : `.${String(p)}`)).join('').replace(/^\./, '');
|
|
441
|
+
if (i.code === 'unrecognized_keys') {
|
|
442
|
+
const known = {
|
|
443
|
+
'': 'type, title, direction, theme, groups, nodes, edges (or participants, messages for sequence)',
|
|
444
|
+
};
|
|
445
|
+
const where = path.startsWith('nodes') || path.startsWith('participants')
|
|
446
|
+
? 'label, kind, tech, in, icon, fields, color, highlight'
|
|
447
|
+
: path.startsWith('groups')
|
|
448
|
+
? 'label, in, color, highlight'
|
|
449
|
+
: path.startsWith('edges')
|
|
450
|
+
? 'from, to, label, style, color, highlight'
|
|
451
|
+
: known[''];
|
|
452
|
+
return { path, message: `unknown key(s) ${i.keys.map((k) => `"${k}"`).join(', ')}. Allowed here: ${where}.` };
|
|
453
|
+
}
|
|
454
|
+
if (i.code === 'invalid_value' && path === 'type')
|
|
455
|
+
return { path, message: `type must be one of: ${DIAGRAM_TYPES.join(', ')}` };
|
|
456
|
+
if (i.code === 'invalid_value' && path === 'theme')
|
|
457
|
+
return { path, message: `theme must be one of: ${THEMES.join(', ')}` };
|
|
458
|
+
if (i.code === 'invalid_value' && path === 'direction')
|
|
459
|
+
return { path, message: `direction must be one of: ${DIRECTIONS.join(', ')}` };
|
|
460
|
+
if (i.code === 'invalid_type' && path === 'nodes')
|
|
461
|
+
return { path, message: 'nodes is required: a map of id -> { label, kind, tech, in }' };
|
|
462
|
+
if (i.code === 'invalid_union')
|
|
463
|
+
return { path, message: path.startsWith('edges') ? 'each edge is a string like "a -> b: label"' : 'expected a label string or an object' };
|
|
464
|
+
return { path, message: i.message };
|
|
465
|
+
}
|
|
466
|
+
function badId(id) {
|
|
467
|
+
return `"${id}" isn't a valid id. Use letters, digits, _ . or - and start with a letter, e.g. order_db.`;
|
|
468
|
+
}
|
|
469
|
+
export function humanize(id) {
|
|
470
|
+
const s = id.replace(/[_-]+/g, ' ').trim();
|
|
471
|
+
return s.charAt(0).toUpperCase() + s.slice(1);
|
|
472
|
+
}
|
|
473
|
+
function suggest(id, pool) {
|
|
474
|
+
let best, bestD = Infinity;
|
|
475
|
+
for (const p of pool) {
|
|
476
|
+
const d = lev(id.toLowerCase(), p.toLowerCase());
|
|
477
|
+
if (d < bestD)
|
|
478
|
+
[best, bestD] = [p, d];
|
|
479
|
+
}
|
|
480
|
+
return best && bestD <= Math.max(2, Math.floor(id.length / 3)) ? ` Did you mean "${best}"?` : '';
|
|
481
|
+
}
|
|
482
|
+
function lev(a, b) {
|
|
483
|
+
const dp = Array.from({ length: b.length + 1 }, (_, j) => j);
|
|
484
|
+
for (let i = 1; i <= a.length; i++) {
|
|
485
|
+
let prev = dp[0];
|
|
486
|
+
dp[0] = i;
|
|
487
|
+
for (let j = 1; j <= b.length; j++) {
|
|
488
|
+
const tmp = dp[j];
|
|
489
|
+
dp[j] = Math.min(dp[j] + 1, dp[j - 1] + 1, prev + (a[i - 1] === b[j - 1] ? 0 : 1));
|
|
490
|
+
prev = tmp;
|
|
491
|
+
}
|
|
492
|
+
}
|
|
493
|
+
return dp[b.length];
|
|
494
|
+
}
|
|
495
|
+
export function jsonSchema() {
|
|
496
|
+
return z.toJSONSchema(SpecInput, { io: 'input' });
|
|
497
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,73 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@itsthecsguy/linework",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
3
|
+
"version": "0.1.1",
|
|
4
|
+
"description": "Software diagrams an AI can write and a person can edit. YAML spec in, .drawio.svg out, editable in draw.io.",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"diagram",
|
|
7
|
+
"drawio",
|
|
8
|
+
"draw.io",
|
|
9
|
+
"architecture",
|
|
10
|
+
"flowchart",
|
|
11
|
+
"sequence-diagram",
|
|
12
|
+
"er-diagram",
|
|
13
|
+
"mcp",
|
|
14
|
+
"agent-skill",
|
|
15
|
+
"ai",
|
|
16
|
+
"svg",
|
|
17
|
+
"mermaid"
|
|
18
|
+
],
|
|
19
|
+
"author": "Nick Salerni",
|
|
20
|
+
"license": "MIT",
|
|
21
|
+
"type": "module",
|
|
22
|
+
"bin": {
|
|
23
|
+
"linework": "dist/cli.js",
|
|
24
|
+
"aiviz": "dist/cli.js"
|
|
25
|
+
},
|
|
26
|
+
"files": [
|
|
27
|
+
"dist",
|
|
28
|
+
"assets",
|
|
29
|
+
"skills",
|
|
30
|
+
"LICENSE",
|
|
31
|
+
"NOTICE",
|
|
32
|
+
"README.md"
|
|
33
|
+
],
|
|
34
|
+
"engines": {
|
|
35
|
+
"node": ">=20"
|
|
36
|
+
},
|
|
37
|
+
"scripts": {
|
|
38
|
+
"build": "tsc -p tsconfig.json",
|
|
39
|
+
"dev": "tsx src/cli.ts",
|
|
40
|
+
"test": "vitest run",
|
|
41
|
+
"assets": "node scripts/copy-drawio-assets.mjs",
|
|
42
|
+
"typecheck": "tsc -p tsconfig.json --noEmit",
|
|
43
|
+
"prepack": "npm run build",
|
|
44
|
+
"fonts": "node scripts/copy-fonts.mjs"
|
|
45
|
+
},
|
|
46
|
+
"dependencies": {
|
|
47
|
+
"@modelcontextprotocol/server": "^2.4.0",
|
|
48
|
+
"@xmldom/xmldom": "^0.9.12",
|
|
49
|
+
"elkjs": "^0.12.1",
|
|
50
|
+
"playwright-core": "^1.64.1",
|
|
51
|
+
"subset-font": "^2.9.0",
|
|
52
|
+
"yaml": "^2.9.1",
|
|
53
|
+
"zod": "^4.6.7"
|
|
54
|
+
},
|
|
55
|
+
"devDependencies": {
|
|
56
|
+
"@fontsource/ibm-plex-mono": "^5.3.0",
|
|
57
|
+
"@fontsource/ibm-plex-sans": "^5.3.0",
|
|
58
|
+
"@fontsource/kalam": "^5.3.0",
|
|
59
|
+
"@modelcontextprotocol/client": "^2.4.0",
|
|
60
|
+
"@types/node": "^26.6.6",
|
|
61
|
+
"tsx": "^4.23.15",
|
|
62
|
+
"typescript": "^7.0.2",
|
|
63
|
+
"vitest": "^5.1.0"
|
|
64
|
+
},
|
|
65
|
+
"repository": {
|
|
66
|
+
"type": "git",
|
|
67
|
+
"url": "git+https://github.com/itsthecsguy/linework.git"
|
|
68
|
+
},
|
|
69
|
+
"homepage": "https://github.com/itsthecsguy/linework#readme",
|
|
70
|
+
"publishConfig": {
|
|
71
|
+
"access": "public"
|
|
72
|
+
}
|
|
73
|
+
}
|