@itsthecsguy/linework 0.0.0-stage → 0.1.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.
Files changed (223) hide show
  1. package/LICENSE +21 -0
  2. package/NOTICE +11 -0
  3. package/README.md +164 -2
  4. package/assets/drawio/LICENSE +201 -0
  5. package/assets/drawio/VERSION +1 -0
  6. package/assets/drawio/favicon.ico +0 -0
  7. package/assets/drawio/images/1x1.png +0 -0
  8. package/assets/drawio/images/2x2.png +0 -0
  9. package/assets/drawio/images/3x3.png +0 -0
  10. package/assets/drawio/images/ajax-loader.gif +0 -0
  11. package/assets/drawio/images/android-chrome-512x512.png +0 -0
  12. package/assets/drawio/images/apple-touch-icon.png +0 -0
  13. package/assets/drawio/images/aui-wait.gif +0 -0
  14. package/assets/drawio/images/broken.png +0 -0
  15. package/assets/drawio/images/browserconfig.xml +9 -0
  16. package/assets/drawio/images/close.png +0 -0
  17. package/assets/drawio/images/cloud-connector-atlas.png +0 -0
  18. package/assets/drawio/images/collapsed.gif +0 -0
  19. package/assets/drawio/images/delete.png +0 -0
  20. package/assets/drawio/images/document-google-drive-icon-80.png +0 -0
  21. package/assets/drawio/images/draw-search.png +0 -0
  22. package/assets/drawio/images/drawio.svg +19 -0
  23. package/assets/drawio/images/drawlogo-color.svg +50 -0
  24. package/assets/drawio/images/drawlogo-gray.svg +18 -0
  25. package/assets/drawio/images/drawlogo-text-bottom.svg +75 -0
  26. package/assets/drawio/images/drawlogo.svg +18 -0
  27. package/assets/drawio/images/drawlogo128.png +0 -0
  28. package/assets/drawio/images/drawlogo144.png +0 -0
  29. package/assets/drawio/images/drawlogo256.png +0 -0
  30. package/assets/drawio/images/drawlogo48-gray.png +0 -0
  31. package/assets/drawio/images/drawlogo48.png +0 -0
  32. package/assets/drawio/images/drawlogo80.png +0 -0
  33. package/assets/drawio/images/droptarget.png +0 -0
  34. package/assets/drawio/images/edit.gif +0 -0
  35. package/assets/drawio/images/edit16.png +0 -0
  36. package/assets/drawio/images/edit32.png +0 -0
  37. package/assets/drawio/images/edit80.png +0 -0
  38. package/assets/drawio/images/embed-icon.png +0 -0
  39. package/assets/drawio/images/expanded.gif +0 -0
  40. package/assets/drawio/images/facebook.png +0 -0
  41. package/assets/drawio/images/favicon-16x16.png +0 -0
  42. package/assets/drawio/images/favicon-32x32.png +0 -0
  43. package/assets/drawio/images/file.png +0 -0
  44. package/assets/drawio/images/folder.png +0 -0
  45. package/assets/drawio/images/gadget-screenshot.png +0 -0
  46. package/assets/drawio/images/gadget-thumb.png +0 -0
  47. package/assets/drawio/images/github-logo-white.svg +6 -0
  48. package/assets/drawio/images/github-logo.svg +17 -0
  49. package/assets/drawio/images/gitlab-logo-white.svg +1 -0
  50. package/assets/drawio/images/gitlab-logo.svg +1 -0
  51. package/assets/drawio/images/glyphicons_blogger.png +0 -0
  52. package/assets/drawio/images/glyphicons_facebook.png +0 -0
  53. package/assets/drawio/images/glyphicons_github.png +0 -0
  54. package/assets/drawio/images/glyphicons_google.png +0 -0
  55. package/assets/drawio/images/glyphicons_star.png +0 -0
  56. package/assets/drawio/images/glyphicons_twitter.png +0 -0
  57. package/assets/drawio/images/glyphicons_youtube.png +0 -0
  58. package/assets/drawio/images/google-drive-connector-atlas.png +0 -0
  59. package/assets/drawio/images/google-drive-connector-atlas.svg +121 -0
  60. package/assets/drawio/images/google-drive-logo-white.svg +9 -0
  61. package/assets/drawio/images/google-drive-logo.svg +17 -0
  62. package/assets/drawio/images/google-share.png +0 -0
  63. package/assets/drawio/images/icon-192-maskable.png +0 -0
  64. package/assets/drawio/images/icon-192.png +0 -0
  65. package/assets/drawio/images/icon-512-maskable.png +0 -0
  66. package/assets/drawio/images/icon-512.png +0 -0
  67. package/assets/drawio/images/icon-search.svg +21 -0
  68. package/assets/drawio/images/img-hi-res.png +0 -0
  69. package/assets/drawio/images/img-lo-res.png +0 -0
  70. package/assets/drawio/images/list.svg +16 -0
  71. package/assets/drawio/images/locked.png +0 -0
  72. package/assets/drawio/images/log.png +0 -0
  73. package/assets/drawio/images/logo-confluence.png +0 -0
  74. package/assets/drawio/images/logo-flat-small.png +0 -0
  75. package/assets/drawio/images/logo-flat.png +0 -0
  76. package/assets/drawio/images/logo-jira.png +0 -0
  77. package/assets/drawio/images/logo-small.gif +0 -0
  78. package/assets/drawio/images/logo-white.png +0 -0
  79. package/assets/drawio/images/logo.png +0 -0
  80. package/assets/drawio/images/manifest.json +25 -0
  81. package/assets/drawio/images/move.png +0 -0
  82. package/assets/drawio/images/notion-logo-white.svg +1 -0
  83. package/assets/drawio/images/notion-logo.svg +1 -0
  84. package/assets/drawio/images/onedrive-connector-atlas.png +0 -0
  85. package/assets/drawio/images/onedrive-connector-atlas.svg +248 -0
  86. package/assets/drawio/images/onedrive-logo-white.svg +29 -0
  87. package/assets/drawio/images/onedrive-logo.svg +29 -0
  88. package/assets/drawio/images/osa_database.png +0 -0
  89. package/assets/drawio/images/osa_drive-harddisk.png +0 -0
  90. package/assets/drawio/images/plant-uml-atlas.png +0 -0
  91. package/assets/drawio/images/samepage-icon-color.svg +37 -0
  92. package/assets/drawio/images/save.png +0 -0
  93. package/assets/drawio/images/sidebar-android.png +0 -0
  94. package/assets/drawio/images/sidebar-archimate.png +0 -0
  95. package/assets/drawio/images/sidebar-arrows.png +0 -0
  96. package/assets/drawio/images/sidebar-arrows2.png +0 -0
  97. package/assets/drawio/images/sidebar-basic.png +0 -0
  98. package/assets/drawio/images/sidebar-c4.png +0 -0
  99. package/assets/drawio/images/sidebar-cabinets.png +0 -0
  100. package/assets/drawio/images/sidebar-cumulus.png +0 -0
  101. package/assets/drawio/images/sidebar-dfd.png +0 -0
  102. package/assets/drawio/images/sidebar-dynamics365.png +0 -0
  103. package/assets/drawio/images/sidebar-eip.png +0 -0
  104. package/assets/drawio/images/sidebar-er.png +0 -0
  105. package/assets/drawio/images/sidebar-floorplans.png +0 -0
  106. package/assets/drawio/images/sidebar-flowchart.png +0 -0
  107. package/assets/drawio/images/sidebar-gcp3.png +0 -0
  108. package/assets/drawio/images/sidebar-general.png +0 -0
  109. package/assets/drawio/images/sidebar-kubernetes.png +0 -0
  110. package/assets/drawio/images/sidebar-leanmapping.png +0 -0
  111. package/assets/drawio/images/sidebar-mockups.png +0 -0
  112. package/assets/drawio/images/sidebar-network.png +0 -0
  113. package/assets/drawio/images/sidebar-network2.png +0 -0
  114. package/assets/drawio/images/sidebar-openstack.png +0 -0
  115. package/assets/drawio/images/sidebar-sitemap.png +0 -0
  116. package/assets/drawio/images/sidebar-threatmodeling.png +0 -0
  117. package/assets/drawio/images/sidebar-uml.png +0 -0
  118. package/assets/drawio/images/sketch-logo128.png +0 -0
  119. package/assets/drawio/images/spin.gif +0 -0
  120. package/assets/drawio/images/sprites.png +0 -0
  121. package/assets/drawio/images/stop-flat-icon-80.png +0 -0
  122. package/assets/drawio/images/submenu.gif +0 -0
  123. package/assets/drawio/images/touch-submenu.png +0 -0
  124. package/assets/drawio/images/transparent.gif +0 -0
  125. package/assets/drawio/images/trello-logo-white-orange.svg +6 -0
  126. package/assets/drawio/images/trello-logo-white.svg +6 -0
  127. package/assets/drawio/images/trello-logo.svg +1 -0
  128. package/assets/drawio/images/tweet.png +0 -0
  129. package/assets/drawio/images/update16.png +0 -0
  130. package/assets/drawio/images/update32.png +0 -0
  131. package/assets/drawio/images/update80.png +0 -0
  132. package/assets/drawio/images/youtube.png +0 -0
  133. package/assets/drawio/img/clipart/Battery_0_128x128.png +0 -0
  134. package/assets/drawio/img/clipart/Battery_100_128x128.png +0 -0
  135. package/assets/drawio/img/clipart/Battery_50_128x128.png +0 -0
  136. package/assets/drawio/img/clipart/Battery_75_128x128.png +0 -0
  137. package/assets/drawio/img/clipart/Battery_allstates_128x128.png +0 -0
  138. package/assets/drawio/img/clipart/Bluetooth_128x128.png +0 -0
  139. package/assets/drawio/img/clipart/Earth_globe_128x128.png +0 -0
  140. package/assets/drawio/img/clipart/Empty_Folder_128x128.png +0 -0
  141. package/assets/drawio/img/clipart/Full_Folder_128x128.png +0 -0
  142. package/assets/drawio/img/clipart/Gear_128x128.png +0 -0
  143. package/assets/drawio/img/clipart/Keys_128x128.png +0 -0
  144. package/assets/drawio/img/clipart/Lock_128x128.png +0 -0
  145. package/assets/drawio/img/clipart/Mouse_Pointer_128x128.png +0 -0
  146. package/assets/drawio/img/clipart/Plug_128x128.png +0 -0
  147. package/assets/drawio/img/clipart/Ships_Wheel_128x128.png +0 -0
  148. package/assets/drawio/img/clipart/Star_128x128.png +0 -0
  149. package/assets/drawio/img/clipart/Tire_128x128.png +0 -0
  150. package/assets/drawio/index.html +92 -0
  151. package/assets/drawio/js/PostConfig.js +7 -0
  152. package/assets/drawio/js/PreConfig.js +13 -0
  153. package/assets/drawio/js/app.min.js +18466 -0
  154. package/assets/drawio/js/bootstrap.js +428 -0
  155. package/assets/drawio/js/diagramly/LibavoidRouting.js +4459 -0
  156. package/assets/drawio/js/export-init.js +21 -0
  157. package/assets/drawio/js/extensions.min.js +10478 -0
  158. package/assets/drawio/js/libavoid-js/LICENSE +505 -0
  159. package/assets/drawio/js/libavoid-js/libavoid-routing.js +1633 -0
  160. package/assets/drawio/js/libavoid-js/libavoid.min.js +59 -0
  161. package/assets/drawio/js/main.js +22 -0
  162. package/assets/drawio/js/rough/rough.min.js +1 -0
  163. package/assets/drawio/js/shapes-14-6-5.min.js +3457 -0
  164. package/assets/drawio/js/stencils.min.js +447 -0
  165. package/assets/drawio/mxgraph/css/common.css +147 -0
  166. package/assets/drawio/resources/dia.txt +2901 -0
  167. package/assets/drawio/styles/dark-default.xml +109 -0
  168. package/assets/drawio/styles/default-old.xml +109 -0
  169. package/assets/drawio/styles/default.xml +203 -0
  170. package/assets/drawio/styles/fonts/ArchitectsDaughter-Regular.ttf +0 -0
  171. package/assets/drawio/styles/grapheditor.css +4501 -0
  172. package/assets/drawio/styles/high-contrast.css +128 -0
  173. package/assets/drawio/styles/thumb_horz.png +0 -0
  174. package/assets/drawio/styles/thumb_vertical.png +0 -0
  175. package/assets/fonts/ibm-plex-mono-LICENSE.txt +93 -0
  176. package/assets/fonts/ibm-plex-sans-LICENSE.txt +93 -0
  177. package/assets/fonts/kalam-400.woff2 +0 -0
  178. package/assets/fonts/kalam-700.woff2 +0 -0
  179. package/assets/fonts/kalam-LICENSE.txt +93 -0
  180. package/assets/fonts/plex-mono-400.woff2 +0 -0
  181. package/assets/fonts/plex-mono-600.woff2 +0 -0
  182. package/assets/fonts/plex-sans-400.woff2 +0 -0
  183. package/assets/fonts/plex-sans-600.woff2 +0 -0
  184. package/dist/api.js +181 -0
  185. package/dist/cli.js +262 -0
  186. package/dist/drawio/icons.js +121 -0
  187. package/dist/drawio/merge.js +309 -0
  188. package/dist/drawio/read.js +180 -0
  189. package/dist/drawio/show.js +501 -0
  190. package/dist/drawio/theme.js +425 -0
  191. package/dist/drawio/write.js +300 -0
  192. package/dist/edit/server.js +212 -0
  193. package/dist/git.js +46 -0
  194. package/dist/import/mermaid.js +569 -0
  195. package/dist/layout/layout.js +212 -0
  196. package/dist/layout/sequence.js +333 -0
  197. package/dist/layout/text.js +73 -0
  198. package/dist/lint/check.js +184 -0
  199. package/dist/lint/fixlabels.js +128 -0
  200. package/dist/mcp/server.js +239 -0
  201. package/dist/publish.js +121 -0
  202. package/dist/render/browser.js +210 -0
  203. package/dist/render/fonts.js +66 -0
  204. package/dist/report.js +36 -0
  205. package/dist/spec/model.js +53 -0
  206. package/dist/spec/parse.js +497 -0
  207. package/package.json +71 -4
  208. package/skills/linework/SKILL.md +214 -0
  209. package/skills/linework/examples/billing-er.drawio.svg +4 -0
  210. package/skills/linework/examples/billing-er.yaml +31 -0
  211. package/skills/linework/examples/checkout.drawio.svg +4 -0
  212. package/skills/linework/examples/checkout.yaml +28 -0
  213. package/skills/linework/examples/pay-sequence.drawio.svg +4 -0
  214. package/skills/linework/examples/pay-sequence.yaml +28 -0
  215. package/skills/linework/examples/payment-state.drawio.svg +4 -0
  216. package/skills/linework/examples/payment-state.yaml +22 -0
  217. package/skills/linework/examples/refund-flow.drawio.svg +4 -0
  218. package/skills/linework/examples/refund-flow.yaml +29 -0
  219. package/skills/linework/examples/serverless-aws.drawio.svg +4 -0
  220. package/skills/linework/examples/serverless-aws.yaml +23 -0
  221. package/skills/linework/references/google-docs.md +46 -0
  222. package/skills/linework/references/spec.md +312 -0
  223. package/skills/linework/references/style-guide.md +53 -0
package/dist/cli.js ADDED
@@ -0,0 +1,262 @@
1
+ #!/usr/bin/env node
2
+ import fs from 'node:fs';
3
+ import path from 'node:path';
4
+ import { parseArgs } from 'node:util';
5
+ import { build, checkFile, listIcons, render, show, tidy } from './api.js';
6
+ import { SpecError, jsonSchema } from './spec/parse.js';
7
+ import { KINDS, DIAGRAM_TYPES } from './spec/model.js';
8
+ import { buildLines, checkLines } from './report.js';
9
+ const USAGE = `linework: AI-written, hand-editable diagrams (YAML spec -> .drawio.svg)
10
+
11
+ Usage:
12
+ linework build <spec.yaml|-> -o <out.drawio.svg> [--png preview.png] [--relayout]
13
+ Lay out and write the diagram, then lint it. "-" reads the spec from stdin.
14
+ If the file exists and someone edited it by hand, the spec is merged in:
15
+ existing positions stay, new nodes are placed next to their neighbours.
16
+ --relayout throws the existing layout away (hand edits included).
17
+ linework show <file.drawio.svg> [--geometry]
18
+ Print the spec of an existing diagram (including hand edits) as YAML.
19
+ --geometry appends node positions as comments.
20
+ linework render <file.drawio.svg> -o <out.png|out.svg> [--scale 2]
21
+ Render a preview image.
22
+ linework check <file.drawio.svg>
23
+ Lint layout: overlaps, edges through nodes, labels that don't fit.
24
+ linework tidy <file.drawio.svg>
25
+ Reroute edges that check flags as broken (e.g. after moving nodes by hand).
26
+ Nothing else in the file changes.
27
+ linework edit <file.drawio.svg> [--no-open] [--port N]
28
+ Open the diagram in draw.io (bundled, offline) in your browser. Saving in
29
+ the editor writes the file. Stops when you exit the editor or close the tab.
30
+ linework import <file.mmd|doc.md|-> [--block N] [-o out.drawio.svg] [--png p.png]
31
+ Convert a Mermaid diagram (flowchart, state, sequence, ER) to an linework spec.
32
+ Prints the spec, or builds it straight away with -o.
33
+ linework icons <aws|gcp|k8s|azure> [filter]
34
+ List icon names, for "icon: aws.lambda" on architecture nodes.
35
+ linework publish <file.drawio.svg...> [--to DIR] [--pull] [--force]
36
+ Copy diagrams into your Google Drive folder (Google Drive for desktop) so
37
+ draw.io's Google Docs add-on can insert them and refresh them later.
38
+ --pull copies back a diagram someone edited from Docs or Drive.
39
+ linework skill [install <dir>]
40
+ Print where the bundled agent skill is, or copy it to <dir>/linework
41
+ (e.g. .claude/skills, ~/.agents/skills).
42
+ linework mcp [--root DIR]
43
+ Run as an MCP server over stdio, for chat apps and agents without a shell.
44
+ Relative paths resolve against --root (default: the current folder).
45
+ linework git-setup
46
+ Make git diff show .drawio.svg changes as spec changes (this repo only).
47
+ linework kinds [architecture|flow|state]
48
+ List node kinds for each diagram type.
49
+ linework schema
50
+ Print the spec's JSON Schema.
51
+ `;
52
+ async function main(argv) {
53
+ const [cmd, ...rest] = argv;
54
+ const { values, positionals } = parseArgs({
55
+ args: rest,
56
+ allowPositionals: true,
57
+ options: {
58
+ out: { type: 'string', short: 'o' },
59
+ png: { type: 'string' },
60
+ force: { type: 'boolean', default: false },
61
+ relayout: { type: 'boolean', default: false },
62
+ geometry: { type: 'boolean', default: false },
63
+ quiet: { type: 'boolean', short: 'q', default: false },
64
+ block: { type: 'string' },
65
+ scale: { type: 'string' },
66
+ 'no-open': { type: 'boolean', default: false },
67
+ port: { type: 'string' },
68
+ root: { type: 'string' },
69
+ to: { type: 'string' },
70
+ pull: { type: 'boolean', default: false },
71
+ help: { type: 'boolean', short: 'h' },
72
+ },
73
+ });
74
+ if (!cmd || cmd === 'help' || cmd === '--help' || cmd === '-h' || values.help)
75
+ return void process.stdout.write(USAGE);
76
+ switch (cmd) {
77
+ case 'build': {
78
+ const src = positionals[0];
79
+ if (!src || !values.out)
80
+ fail('usage: linework build <spec.yaml|-> -o <out.drawio.svg>');
81
+ const text = src === '-' ? fs.readFileSync(0, 'utf8') : fs.readFileSync(src, 'utf8');
82
+ const res = await build(text, { out: values.out, png: values.png, force: values.force, relayout: values.relayout });
83
+ for (const line of buildLines(res, values.png))
84
+ console.log(line);
85
+ return;
86
+ }
87
+ case 'show': {
88
+ const file = positionals[0] ?? fail('usage: linework show <file.drawio.svg>');
89
+ const res = show(file, { geometry: values.geometry });
90
+ process.stdout.write(res.yaml);
91
+ if (values.quiet)
92
+ return;
93
+ for (const n of res.notes)
94
+ console.error(`note: ${n}`);
95
+ if (res.edits.length) {
96
+ const moved = res.edits.filter((e) => e.what !== 'added').length;
97
+ const added = res.edits.filter((e) => e.what === 'added').length;
98
+ console.error(`note: edited by hand since the last build (${moved} moved/resized, ${added} added). build keeps those positions and merges your changes in.`);
99
+ }
100
+ return;
101
+ }
102
+ case 'render': {
103
+ const file = positionals[0];
104
+ if (!file || !values.out)
105
+ fail('usage: linework render <file.drawio.svg> -o <out.png>');
106
+ const { DrawioBrowser } = await import('./render/browser.js');
107
+ const b = await DrawioBrowser.launch({ scale: values.scale ? Number(values.scale) : 2 });
108
+ try {
109
+ await render(file, values.out, b);
110
+ }
111
+ finally {
112
+ await b.close();
113
+ }
114
+ console.log(`wrote ${values.out}`);
115
+ return;
116
+ }
117
+ case 'check': {
118
+ const file = positionals[0] ?? fail('usage: linework check <file.drawio.svg>');
119
+ const res = await checkFile(file);
120
+ printCheck(res);
121
+ if (res.findings.some((f) => f.level === 'error'))
122
+ process.exitCode = 1;
123
+ return;
124
+ }
125
+ case 'tidy': {
126
+ const file = positionals[0] ?? fail('usage: linework tidy <file.drawio.svg>');
127
+ const res = await tidy(file);
128
+ console.log(res.fixed ? `rerouted ${res.fixed} edges in ${file}` : 'nothing to fix');
129
+ printCheck(res.check);
130
+ return;
131
+ }
132
+ case 'edit': {
133
+ const file = positionals[0] ?? fail('usage: linework edit <file.drawio.svg>');
134
+ const { startEditor, openInBrowser } = await import('./edit/server.js');
135
+ const session = await startEditor(file, { port: values.port ? Number(values.port) : undefined });
136
+ const opened = !values['no-open'] && openInBrowser(session.url);
137
+ console.log(`${opened ? 'opened' : 'open'} ${session.url}`);
138
+ console.log(`editing ${file}. Save in the editor (Ctrl/Cmd+S) to write the file. Exit the editor, close the tab or press Ctrl+C to stop.`);
139
+ process.once('SIGINT', () => void session.close());
140
+ const res = await session.done;
141
+ console.log(res.saves ? `saved ${file} ${res.saves - res.conflicts.length} time(s)` : 'closed without saving');
142
+ for (const c of res.conflicts)
143
+ console.log(`note: ${file} changed on disk while it was open, so one save went to ${c}`);
144
+ return;
145
+ }
146
+ case 'import': {
147
+ const src = positionals[0] ?? fail('usage: linework import <file.mmd|doc.md|-> [-o out.drawio.svg]');
148
+ const { importMermaid, mermaidBlocks } = await import('./import/mermaid.js');
149
+ const { toYaml } = await import('./drawio/show.js');
150
+ let text = src === '-' ? fs.readFileSync(0, 'utf8') : fs.readFileSync(src, 'utf8');
151
+ if (/\.(md|markdown|mdx)$/i.test(src)) {
152
+ const blocks = mermaidBlocks(text);
153
+ if (!blocks.length)
154
+ fail(`${src} has no \`\`\`mermaid blocks`);
155
+ const n = values.block ? Number(values.block) : blocks.length === 1 ? 1 : 0;
156
+ if (!n || !blocks[n - 1]) {
157
+ const first = blocks.map((b, i) => ` ${i + 1}: ${b.split('\n')[0]}`).join('\n');
158
+ fail(`${src} has ${blocks.length} mermaid blocks; pick one with --block N:\n${first}`);
159
+ }
160
+ text = blocks[n - 1];
161
+ }
162
+ const res = importMermaid(text);
163
+ for (const w of res.warnings)
164
+ console.error(`import: ${w}`);
165
+ const yaml = toYaml(res.diagram, 'imported from Mermaid');
166
+ if (!values.out)
167
+ return void process.stdout.write(yaml);
168
+ const built = await build(yaml, { out: values.out, png: values.png, relayout: values.relayout, force: values.force });
169
+ console.log(`wrote ${built.out} (${built.check.stats.nodes} nodes, ${built.check.stats.edges} edges, ${built.direction})`);
170
+ printCheck(built.check);
171
+ return;
172
+ }
173
+ case 'icons': {
174
+ const provider = positionals[0];
175
+ if (!provider || !['aws', 'gcp', 'k8s', 'azure'].includes(provider))
176
+ fail('usage: linework icons <aws|gcp|k8s|azure> [filter]');
177
+ const names = await listIcons(provider, positionals[1]);
178
+ console.log(names.map((n) => `${provider}.${n}`).join('\n') || 'no matches');
179
+ return;
180
+ }
181
+ case 'publish': {
182
+ if (!positionals.length)
183
+ fail('usage: linework publish <file.drawio.svg...> [--to DIR]');
184
+ const { publish } = await import('./publish.js');
185
+ const items = publish(positionals, { to: values.to, pull: values.pull, force: values.force });
186
+ for (const i of items) {
187
+ if (i.action === 'published')
188
+ console.log(`published ${path.relative(process.cwd(), i.file)} -> ${i.dest}`);
189
+ else if (i.action === 'unchanged')
190
+ console.log(`unchanged ${i.dest}`);
191
+ else if (i.action === 'pulled')
192
+ console.log(`pulled ${i.dest} -> ${path.relative(process.cwd(), i.file)}`);
193
+ else
194
+ console.log(`skipped ${path.relative(process.cwd(), i.file)}: the Drive copy was edited after the last publish. Run with --pull to take that version, or --force to overwrite it.`);
195
+ }
196
+ if (items.some((i) => i.action === 'published') && !values.pull) {
197
+ console.log('\nNot in the doc yet? In Google Docs: Extensions > draw.io > Insert diagrams, then pick the file.');
198
+ console.log('Already in the doc? After Drive syncs: Extensions > draw.io > Update diagrams.');
199
+ }
200
+ if (items.some((i) => i.action === 'conflict'))
201
+ process.exitCode = 1;
202
+ return;
203
+ }
204
+ case 'skill': {
205
+ const { fileURLToPath } = await import('node:url');
206
+ const src = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..', 'skills', 'linework');
207
+ if (positionals[0] !== 'install')
208
+ return void console.log(src);
209
+ const dir = positionals[1] ?? fail('usage: linework skill install <skills-dir> e.g. .claude/skills');
210
+ const dest = path.join(dir.replace(/^~(?=$|\/)/, process.env.HOME ?? '~'), 'linework');
211
+ fs.mkdirSync(dest, { recursive: true });
212
+ fs.cpSync(src, dest, { recursive: true });
213
+ console.log(`installed the linework skill to ${dest}`);
214
+ return;
215
+ }
216
+ case 'mcp': {
217
+ const { runStdio } = await import('./mcp/server.js');
218
+ await runStdio({ root: path.resolve(values.root ?? process.env.LINEWORK_ROOT ?? process.cwd()) });
219
+ return;
220
+ }
221
+ case 'git-setup': {
222
+ const { gitSetup } = await import('./git.js');
223
+ for (const line of gitSetup())
224
+ console.log(line);
225
+ return;
226
+ }
227
+ case 'kinds': {
228
+ const types = (positionals.length ? positionals : DIAGRAM_TYPES);
229
+ for (const t of types) {
230
+ if (!KINDS[t])
231
+ fail(`unknown type ${t}`);
232
+ console.log(`${t}:`);
233
+ for (const [k, d] of Object.entries(KINDS[t]))
234
+ console.log(` ${k.padEnd(10)} ${d.description}`);
235
+ }
236
+ return;
237
+ }
238
+ case 'schema':
239
+ console.log(JSON.stringify(jsonSchema(), null, 2));
240
+ return;
241
+ default:
242
+ fail(`unknown command "${cmd}"\n\n${USAGE}`);
243
+ }
244
+ }
245
+ function printCheck(res) {
246
+ for (const line of checkLines(res))
247
+ console.log(line);
248
+ }
249
+ function fail(msg) {
250
+ console.error(msg);
251
+ process.exit(2);
252
+ }
253
+ main(process.argv.slice(2)).catch((e) => {
254
+ if (e instanceof SpecError) {
255
+ console.error('spec error:');
256
+ for (const i of e.issues)
257
+ console.error(` ${i.path ? i.path + ': ' : ''}${i.message}`);
258
+ process.exit(1);
259
+ }
260
+ console.error(`error: ${e.message}`);
261
+ process.exit(1);
262
+ });
@@ -0,0 +1,121 @@
1
+ // Cloud icons from draw.io's bundled stencil sets. `icon: aws.lambda` puts AWS's
2
+ // Lambda icon at the left of the node; the node keeps its kind colours.
3
+ export const PROVIDERS = {
4
+ aws: { set: 'aws4.xml', prefix: 'mxgraph.aws4.' },
5
+ gcp: { set: 'gcp2.xml', prefix: 'mxgraph.gcp2.' },
6
+ k8s: { set: 'kubernetes2.xml', prefix: 'mxgraph.kubernetes2.' },
7
+ azure: { set: 'azure.xml', prefix: 'mxgraph.azure.' },
8
+ };
9
+ const ALIASES = {
10
+ aws: {
11
+ efs: 'elastic_file_system',
12
+ documentdb: 'documentdb_with_mongodb_compatibility',
13
+ memorydb: 'memorydb_for_redis',
14
+ msk: 'managed_streaming_for_kafka',
15
+ kafka: 'managed_streaming_for_kafka',
16
+ iam: 'identity_and_access_management',
17
+ kms: 'key_management_service',
18
+ alb: 'application_load_balancer',
19
+ elb: 'elastic_load_balancing',
20
+ route53: 'route_53',
21
+ apigateway: 'api_gateway',
22
+ stepfunctions: 'step_functions',
23
+ sfn: 'step_functions',
24
+ },
25
+ gcp: {
26
+ pubsub: 'cloud_pubsub',
27
+ firestore: 'cloud_firestore',
28
+ spanner: 'cloud_spanner',
29
+ memorystore: 'cloud_memorystore',
30
+ dataflow: 'cloud_dataflow',
31
+ bigtable: 'cloud_bigtable',
32
+ functions: 'cloud_functions',
33
+ gcs: 'cloud_storage',
34
+ storage: 'cloud_storage',
35
+ sql: 'cloud_sql',
36
+ run: 'cloud_run',
37
+ gce: 'compute_engine',
38
+ gke: 'container_engine',
39
+ kubernetes_engine: 'container_engine',
40
+ },
41
+ k8s: {
42
+ service: 'svc',
43
+ deployment: 'deploy',
44
+ ingress: 'ing',
45
+ statefulset: 'sts',
46
+ daemonset: 'ds',
47
+ configmap: 'cm',
48
+ namespace: 'ns',
49
+ persistentvolume: 'pv',
50
+ persistentvolumeclaim: 'pvc',
51
+ replicaset: 'rs',
52
+ serviceaccount: 'sa',
53
+ },
54
+ azure: {},
55
+ };
56
+ // AWS colours icons by service category.
57
+ const AWS_CATEGORY = [
58
+ ['#ED7100', ['lambda', 'ec2', 'ecs', 'eks', 'fargate', 'batch', 'app_runner', 'elastic_beanstalk', 'lightsail', 'ecr']],
59
+ ['#7AA116', ['s3', 'elastic_file_system', 'glacier', 'backup', 'fsx', 'storage_gateway']],
60
+ ['#C925D1', ['rds', 'aurora', 'dynamodb', 'elasticache', 'documentdb_with_mongodb_compatibility', 'neptune', 'memorydb_for_redis', 'timestream', 'keyspaces']],
61
+ ['#8C4FFF', ['api_gateway', 'cloudfront', 'route_53', 'elastic_load_balancing', 'application_load_balancer', 'vpc', 'direct_connect', 'global_accelerator', 'app_mesh', 'cloud_map', 'transit_gateway', 'kinesis', 'athena', 'glue', 'redshift', 'emr', 'quicksight', 'managed_streaming_for_kafka', 'lake_formation']],
62
+ ['#E7157B', ['sqs', 'sns', 'eventbridge', 'step_functions', 'mq', 'appsync', 'cloudwatch', 'cloudformation', 'systems_manager', 'cloudtrail', 'config']],
63
+ ['#DD344C', ['cognito', 'identity_and_access_management', 'key_management_service', 'secrets_manager', 'waf', 'shield', 'guardduty', 'certificate_manager']],
64
+ ['#01A88D', ['sagemaker', 'bedrock', 'rekognition', 'comprehend', 'textract', 'transcribe', 'polly', 'lex']],
65
+ ];
66
+ // Generic AWS resources are plain glyphs, not coloured tiles.
67
+ const AWS_GENERAL = new Set(['users', 'user', 'client', 'mobile_client', 'internet', 'traditional_server', 'generic_database', 'office_building', 'corporate_data_center']);
68
+ export function parseIcon(icon) {
69
+ const m = /^(aws|gcp|k8s|azure)\.([\w-]+)$/.exec(icon.trim().toLowerCase());
70
+ if (!m)
71
+ return { error: `icon "${icon}" should look like aws.lambda, gcp.cloud_run, k8s.pod or azure.<name>. Run "linework icons aws" to list names.` };
72
+ const provider = m[1];
73
+ const raw = m[2].replace(/-/g, '_');
74
+ return { provider, name: ALIASES[provider][raw] ?? raw };
75
+ }
76
+ /**
77
+ * The icon's draw.io style. Plain glyph icons (AWS generic resources, GCP and
78
+ * Azure) take their brand color only where it shows against the node's fill;
79
+ * on a dark or saturated fill they switch to the node's text color. Tiled icons
80
+ * (AWS services, Kubernetes) carry a white glyph on their own color, so they read
81
+ * on any fill and keep their colors.
82
+ */
83
+ export function resolveIcon(icon, on) {
84
+ const p = parseIcon(icon);
85
+ if ('error' in p)
86
+ return p;
87
+ const { provider, name } = p;
88
+ const stencil = PROVIDERS[provider].prefix + name;
89
+ const base = 'outlineConnect=0;aspect=fixed;html=1;sketch=0;';
90
+ const glyph = (brand) => (on && contrast(brand, on.fill) < 3 ? on.ink : brand);
91
+ let style;
92
+ if (provider === 'aws') {
93
+ style = AWS_GENERAL.has(name)
94
+ ? `${base}shape=${stencil};fillColor=${glyph('#232F3E')};strokeColor=none;`
95
+ : `${base}shape=mxgraph.aws4.resourceIcon;resIcon=${stencil};fillColor=${AWS_CATEGORY.find(([, names]) => names.includes(name))?.[0] ?? '#232F3E'};strokeColor=#ffffff;`;
96
+ }
97
+ else if (provider === 'gcp')
98
+ style = `${base}shape=${stencil};fillColor=${glyph('#4285F4')};strokeColor=none;`;
99
+ else if (provider === 'k8s')
100
+ style = `${base}shape=mxgraph.kubernetes.icon2;prIcon=${name};fillColor=#326CE5;strokeColor=#ffffff;`;
101
+ else
102
+ style = `${base}shape=${stencil};fillColor=${glyph('#0078D4')};strokeColor=none;`;
103
+ return { provider, name, stencil, style };
104
+ }
105
+ /** WCAG contrast ratio of two #RRGGBB colors. Anything else counts as white. */
106
+ export function contrast(a, b) {
107
+ const lum = (c) => {
108
+ const m = /^#([0-9a-f]{6})$/i.exec(c);
109
+ if (!m)
110
+ return 1;
111
+ const [r, g, b] = [0, 2, 4].map((i) => {
112
+ const v = parseInt(m[1].slice(i, i + 2), 16) / 255;
113
+ return v <= 0.03928 ? v / 12.92 : ((v + 0.055) / 1.055) ** 2.4;
114
+ });
115
+ return 0.2126 * r + 0.7152 * g + 0.0722 * b;
116
+ };
117
+ const [x, y] = [lum(a), lum(b)].sort((p, q) => q - p);
118
+ return (x + 0.05) / (y + 0.05);
119
+ }
120
+ export const ICON_SIZE = 28;
121
+ export const ICON_PAD = 44; // left space a node reserves for its icon