justskill-design 0.0.1 → 3.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.
Files changed (67) hide show
  1. package/README.en.md +235 -0
  2. package/README.md +184 -11
  3. package/SKILL.md +435 -0
  4. package/canvas/app.mjs +2870 -0
  5. package/canvas/groups.mjs +204 -0
  6. package/canvas/index.html +141 -0
  7. package/canvas/styles.css +1032 -0
  8. package/canvas/tree.mjs +249 -0
  9. package/cordis.patch.yml +8 -0
  10. package/design.mjs +172 -0
  11. package/docs/dsh-workbench.png +0 -0
  12. package/dsh/client.js +864 -0
  13. package/dsh/index.js +354 -0
  14. package/dsh/scan.js +139 -0
  15. package/dsh/skill.js +41 -0
  16. package/lib/base-css.mjs +92 -0
  17. package/lib/detect.mjs +123 -0
  18. package/lib/diagnostics.mjs +382 -0
  19. package/lib/export-html.mjs +309 -0
  20. package/lib/export-svg.mjs +111 -0
  21. package/lib/fragment.mjs +702 -0
  22. package/lib/geometry.mjs +827 -0
  23. package/lib/icons.mjs +106 -0
  24. package/lib/levenshtein.mjs +133 -0
  25. package/lib/node/cli.mjs +387 -0
  26. package/lib/node/export-svg.mjs +75 -0
  27. package/lib/node/fs.mjs +309 -0
  28. package/lib/node/op-json.mjs +75 -0
  29. package/lib/node/raster-run.mjs +115 -0
  30. package/lib/node/server.mjs +1264 -0
  31. package/lib/node/static.mjs +206 -0
  32. package/lib/node/verbs/add.mjs +220 -0
  33. package/lib/node/verbs/export.mjs +161 -0
  34. package/lib/node/verbs/format.mjs +106 -0
  35. package/lib/node/verbs/import.mjs +126 -0
  36. package/lib/node/verbs/ls.mjs +98 -0
  37. package/lib/node/verbs/migrate.mjs +236 -0
  38. package/lib/node/verbs/move.mjs +76 -0
  39. package/lib/node/verbs/new.mjs +284 -0
  40. package/lib/node/verbs/remove.mjs +41 -0
  41. package/lib/node/verbs/rename.mjs +87 -0
  42. package/lib/node/verbs/serve.mjs +164 -0
  43. package/lib/node/verbs/set.mjs +150 -0
  44. package/lib/node/verbs/show.mjs +122 -0
  45. package/lib/node/verbs/tokens.mjs +167 -0
  46. package/lib/node/verbs/validate.mjs +130 -0
  47. package/lib/node/write-verbs.mjs +215 -0
  48. package/lib/normalize.mjs +288 -0
  49. package/lib/ops.mjs +2063 -0
  50. package/lib/parse.mjs +997 -0
  51. package/lib/raster.mjs +186 -0
  52. package/lib/readers/pen.mjs +1684 -0
  53. package/lib/receipt.mjs +203 -0
  54. package/lib/render.mjs +503 -0
  55. package/lib/serialize.mjs +441 -0
  56. package/lib/spans.mjs +177 -0
  57. package/lib/svg-path.mjs +483 -0
  58. package/lib/table.mjs +1047 -0
  59. package/lib/tokens.mjs +295 -0
  60. package/lib/validate.mjs +929 -0
  61. package/package.json +68 -6
  62. package/references/cookbook.md +156 -0
  63. package/references/images.md +171 -0
  64. package/references/product-flow.md +408 -0
  65. package/references/schema.md +90 -0
  66. package/references/styling.md +181 -0
  67. package/scripts/install.mjs +95 -0
package/lib/icons.mjs ADDED
@@ -0,0 +1,106 @@
1
+ /**
2
+ * The icon set. Generated by `scripts/gen-icons.mjs` — do not edit by hand.
3
+ *
4
+ * Lucide (ISC): each value is the inner markup of one
5
+ * glyph, drawn on a 24×24 viewBox and stroked with `currentColor`, so a node's
6
+ * `icon.color` tints it.
7
+ */
8
+
9
+ export const ICON_LIBRARY = "lucide";
10
+
11
+ /**
12
+ * The coordinate system every glyph in this set is drawn in.
13
+ *
14
+ * A `<svg>` with no `viewBox` has no coordinate system: the geometry is drawn in
15
+ * the element's own pixel box. A 24×24 glyph inside a 15×15 element is then not
16
+ * scaled down, it is CLIPPED — and `target`'s three rings become one arc, while
17
+ * `signal`'s five bars become a single dot. The drawing has to say what grid it
18
+ * was drawn on, so every icon element carries this.
19
+ */
20
+ export const ICON_VIEW_BOX = "0 0 24 24";
21
+
22
+ /** Glyph name -> SVG markup, alphabetically. */
23
+ export const ICONS = {
24
+ "activity": "<path d=\"M22 12h-2.48a2 2 0 0 0-1.93 1.46l-2.35 8.36a.25.25 0 0 1-.48 0L9.24 2.18a.25.25 0 0 0-.48 0l-2.35 8.36A2 2 0 0 1 4.49 12H2\" />",
25
+ "arrow-right": "<path d=\"M5 12h14\" /><path d=\"m12 5 7 7-7 7\" />",
26
+ "arrow-up-right": "<path d=\"M7 7h10v10\" /><path d=\"M7 17 17 7\" />",
27
+ "award": "<path d=\"m15.477 12.89 1.515 8.526a.5.5 0 0 1-.81.47l-3.58-2.687a1 1 0 0 0-1.197 0l-3.586 2.686a.5.5 0 0 1-.81-.469l1.514-8.526\" /><circle cx=\"12\" cy=\"8\" r=\"6\" />",
28
+ "badge-check": "<path d=\"M3.85 8.62a4 4 0 0 1 4.78-4.77 4 4 0 0 1 6.74 0 4 4 0 0 1 4.78 4.78 4 4 0 0 1 0 6.74 4 4 0 0 1-4.77 4.78 4 4 0 0 1-6.75 0 4 4 0 0 1-4.78-4.77 4 4 0 0 1 0-6.76Z\" /><path d=\"m16 9-5.5 5.5L8 12\" />",
29
+ "battery-full": "<path d=\"M10 10v4\" /><path d=\"M14 10v4\" /><path d=\"M22 14v-4\" /><path d=\"M6 10v4\" /><rect x=\"2\" y=\"6\" width=\"16\" height=\"12\" rx=\"2\" />",
30
+ "bell": "<path d=\"M10.268 21a2 2 0 0 0 3.464 0\" /><path d=\"M3.262 15.326A1 1 0 0 0 4 17h16a1 1 0 0 0 .74-1.673C19.41 13.956 18 12.499 18 8A6 6 0 0 0 6 8c0 4.499-1.411 5.956-2.738 7.326\" />",
31
+ "building-2": "<path d=\"M10 12h4\" /><path d=\"M10 8h4\" /><path d=\"M14 21v-3a2 2 0 0 0-4 0v3\" /><path d=\"M6 10H4a2 2 0 0 0-2 2v7a2 2 0 0 0 2 2h16a2 2 0 0 0 2-2V9a2 2 0 0 0-2-2h-2\" /><path d=\"M6 21V5a2 2 0 0 1 2-2h8a2 2 0 0 1 2 2v16\" />",
32
+ "calendar": "<path d=\"M8 2v3\" /><path d=\"M16 2v3\" /><rect x=\"3\" y=\"3\" width=\"18\" height=\"18\" rx=\"2\" /><path d=\"M3 9h18\" />",
33
+ "chart-bar": "<path d=\"M3 3v16a2 2 0 0 0 2 2h16\" /><path d=\"M7 16h8\" /><path d=\"M7 11h12\" /><path d=\"M7 6h3\" />",
34
+ "chart-line": "<path d=\"M3 3v16a2 2 0 0 0 2 2h16\" /><path d=\"m19 9-5 5-4-4-3 3\" />",
35
+ "check": "<path d=\"M20 6 9 17l-5-5\" />",
36
+ "chevron-down": "<path d=\"m6 9 6 6 6-6\" />",
37
+ "chevron-left": "<path d=\"m15 18-6-6 6-6\" />",
38
+ "chevron-right": "<path d=\"m9 18 6-6-6-6\" />",
39
+ "chevron-up": "<path d=\"m18 15-6-6-6 6\" />",
40
+ "circle-check": "<circle cx=\"12\" cy=\"12\" r=\"10\" /><path d=\"m16 9-5.5 5.5L8 12\" />",
41
+ "clipboard-check": "<rect width=\"8\" height=\"4\" x=\"8\" y=\"2\" rx=\"1\" ry=\"1\" /><path d=\"M16 4h2a2 2 0 0 1 2 2v14a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2V6a2 2 0 0 1 2-2h2\" /><path d=\"m9 14 2 2 4-4\" />",
42
+ "compass": "<circle cx=\"12\" cy=\"12\" r=\"10\" /><path d=\"m16.24 7.76-1.804 5.411a2 2 0 0 1-1.265 1.265L7.76 16.24l1.804-5.411a2 2 0 0 1 1.265-1.265z\" />",
43
+ "credit-card": "<rect width=\"20\" height=\"14\" x=\"2\" y=\"5\" rx=\"2\" /><line x1=\"2\" x2=\"22\" y1=\"10\" y2=\"10\" /><path d=\"M6 14h2\" />",
44
+ "crown": "<path d=\"M11.562 3.266a.5.5 0 0 1 .876 0L15.39 8.87a1 1 0 0 0 1.516.294L21.183 5.5a.5.5 0 0 1 .798.519l-2.834 10.246a1 1 0 0 1-.956.734H5.81a1 1 0 0 1-.957-.734L2.02 6.02a.5.5 0 0 1 .798-.519l4.276 3.664a1 1 0 0 0 1.516-.294z\" /><path d=\"M5 21h14\" />",
45
+ "download": "<path d=\"M12 15V3\" /><path d=\"M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4\" /><path d=\"m7 10 5 5 5-5\" />",
46
+ "ellipsis": "<circle cx=\"12\" cy=\"12\" r=\"1\" /><circle cx=\"19\" cy=\"12\" r=\"1\" /><circle cx=\"5\" cy=\"12\" r=\"1\" />",
47
+ "file-text": "<path d=\"M6 22a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h8a2.4 2.4 0 0 1 1.704.706l3.588 3.588A2.4 2.4 0 0 1 20 8v12a2 2 0 0 1-2 2z\" /><path d=\"M14 2v5a1 1 0 0 0 1 1h5\" /><path d=\"M10 9H8\" /><path d=\"M16 13H8\" /><path d=\"M16 17H8\" />",
48
+ "focus": "<circle cx=\"12\" cy=\"12\" r=\"3\" /><path d=\"M3 7V5a2 2 0 0 1 2-2h2\" /><path d=\"M17 3h2a2 2 0 0 1 2 2v2\" /><path d=\"M21 17v2a2 2 0 0 1-2 2h-2\" /><path d=\"M7 21H5a2 2 0 0 1-2-2v-2\" />",
49
+ "gem": "<path d=\"M10.5 3 8 9l4 13 4-13-2.5-6\" /><path d=\"M17 3a2 2 0 0 1 1.6.8l3 4a2 2 0 0 1 .013 2.382l-7.99 10.986a2 2 0 0 1-3.247 0l-7.99-10.986A2 2 0 0 1 2.4 7.8l2.998-3.997A2 2 0 0 1 7 3z\" /><path d=\"M2 9h20\" />",
50
+ "graduation-cap": "<path d=\"M21.42 10.922a1 1 0 0 0-.019-1.838L12.83 5.18a2 2 0 0 0-1.66 0L2.6 9.08a1 1 0 0 0 0 1.832l8.57 3.908a2 2 0 0 0 1.66 0z\" /><path d=\"M22 10v6\" /><path d=\"M6 12.5V16a6 3 0 0 0 12 0v-3.5\" />",
51
+ "heart": "<path d=\"M2 9.5a5.5 5.5 0 0 1 9.591-3.676.56.56 0 0 0 .818 0A5.49 5.49 0 0 1 22 9.5c0 2.29-1.5 4-3 5.5l-5.492 5.313a2 2 0 0 1-3 .019L5 15c-1.5-1.5-3-3.2-3-5.5\" />",
52
+ "heart-handshake": "<path d=\"M19.414 14.414C21 12.828 22 11.5 22 9.5a5.5 5.5 0 0 0-9.591-3.676.6.6 0 0 1-.818.001A5.5 5.5 0 0 0 2 9.5c0 2.3 1.5 4 3 5.5l5.535 5.362a2 2 0 0 0 2.879.052 2.12 2.12 0 0 0-.004-3 2.124 2.124 0 1 0 3-3 2.124 2.124 0 0 0 3.004 0 2 2 0 0 0 0-2.828l-1.881-1.882a2.41 2.41 0 0 0-3.409 0l-1.71 1.71a2 2 0 0 1-2.828 0 2 2 0 0 1 0-2.828l2.823-2.762\" />",
53
+ "history": "<path d=\"M3 12a9 9 0 1 0 9-9 9.75 9.75 0 0 0-6.74 2.74L3 8\" /><path d=\"M3 3v5h5\" /><path d=\"M12 7v5l4 2\" />",
54
+ "house": "<path d=\"M15 21v-8a1 1 0 0 0-1-1h-4a1 1 0 0 0-1 1v8\" /><path d=\"M3 10a2 2 0 0 1 .709-1.528l7-6a2 2 0 0 1 2.582 0l7 6A2 2 0 0 1 21 10v9a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z\" />",
55
+ "image": "<rect width=\"18\" height=\"18\" x=\"3\" y=\"3\" rx=\"2\" ry=\"2\" /><circle cx=\"9\" cy=\"9\" r=\"2\" /><path d=\"m21 15-3.086-3.086a2 2 0 0 0-2.828 0L6 21\" />",
56
+ "info": "<circle cx=\"12\" cy=\"12\" r=\"10\" /><path d=\"M12 16v-4\" /><path d=\"M12 8h.01\" />",
57
+ "layout-dashboard": "<rect width=\"7\" height=\"9\" x=\"3\" y=\"3\" rx=\"1\" /><rect width=\"7\" height=\"5\" x=\"14\" y=\"3\" rx=\"1\" /><rect width=\"7\" height=\"9\" x=\"14\" y=\"12\" rx=\"1\" /><rect width=\"7\" height=\"5\" x=\"3\" y=\"16\" rx=\"1\" />",
58
+ "loader": "<path d=\"M12 2v4\" /><path d=\"m16.2 7.8 2.9-2.9\" /><path d=\"M18 12h4\" /><path d=\"m16.2 16.2 2.9 2.9\" /><path d=\"M12 18v4\" /><path d=\"m4.9 19.1 2.9-2.9\" /><path d=\"M2 12h4\" /><path d=\"m4.9 4.9 2.9 2.9\" />",
59
+ "lock": "<rect width=\"18\" height=\"11\" x=\"3\" y=\"11\" rx=\"2\" ry=\"2\" /><path d=\"M7 11V7a5 5 0 0 1 10 0v4\" />",
60
+ "map-pin": "<path d=\"M20 10c0 4.993-5.539 10.193-7.399 11.799a1 1 0 0 1-1.202 0C9.539 20.193 4 14.993 4 10a8 8 0 0 1 16 0\" /><circle cx=\"12\" cy=\"10\" r=\"3\" />",
61
+ "medal": "<path d=\"M7.21 15 2.66 7.14a2 2 0 0 1 .13-2.2L4.4 2.8A2 2 0 0 1 6 2h12a2 2 0 0 1 1.6.8l1.6 2.14a2 2 0 0 1 .14 2.2L16.79 15\" /><path d=\"M11 12 5.12 2.2\" /><path d=\"m13 12 5.88-9.8\" /><path d=\"M8 7h8\" /><circle cx=\"12\" cy=\"17\" r=\"5\" /><path d=\"M12 18v-2h-.5\" />",
62
+ "package": "<path d=\"M11 21.73a2 2 0 0 0 2 0l7-4A2 2 0 0 0 21 16V8a2 2 0 0 0-1-1.73l-7-4a2 2 0 0 0-2 0l-7 4A2 2 0 0 0 3 8v8a2 2 0 0 0 1 1.73z\" /><path d=\"M12 22V12\" /><polyline points=\"3.29 7 12 12 20.71 7\" /><path d=\"m7.5 4.27 9 5.15\" />",
63
+ "pen-line": "<path d=\"M13 21h8\" /><path d=\"M21.174 6.812a1 1 0 0 0-3.986-3.987L3.842 16.174a2 2 0 0 0-.5.83l-1.321 4.352a.5.5 0 0 0 .623.622l4.353-1.32a2 2 0 0 0 .83-.497z\" />",
64
+ "phone": "<path d=\"M13.832 16.568a1 1 0 0 0 1.213-.303l.355-.465A2 2 0 0 1 17 15h3a2 2 0 0 1 2 2v3a2 2 0 0 1-2 2A18 18 0 0 1 2 4a2 2 0 0 1 2-2h3a2 2 0 0 1 2 2v3a2 2 0 0 1-.8 1.6l-.468.351a1 1 0 0 0-.292 1.233 14 14 0 0 0 6.392 6.384\" />",
65
+ "plus": "<path d=\"M5 12h14\" /><path d=\"M12 5v14\" />",
66
+ "refresh-ccw": "<path d=\"M21 12a9 9 0 0 0-9-9 9.75 9.75 0 0 0-6.74 2.74L3 8\" /><path d=\"M3 3v5h5\" /><path d=\"M3 12a9 9 0 0 0 9 9 9.75 9.75 0 0 0 6.74-2.74L21 16\" /><path d=\"M16 16h5v5\" />",
67
+ "refresh-cw": "<path d=\"M3 12a9 9 0 0 1 9-9 9.75 9.75 0 0 1 6.74 2.74L21 8\" /><path d=\"M21 3v5h-5\" /><path d=\"M21 12a9 9 0 0 1-9 9 9.75 9.75 0 0 1-6.74-2.74L3 16\" /><path d=\"M8 16H3v5\" />",
68
+ "route": "<circle cx=\"6\" cy=\"19\" r=\"3\" /><path d=\"M9 19h8.5a3.5 3.5 0 0 0 0-7h-11a3.5 3.5 0 0 1 0-7H15\" /><circle cx=\"18\" cy=\"5\" r=\"3\" />",
69
+ "search": "<path d=\"m21 21-4.34-4.34\" /><circle cx=\"11\" cy=\"11\" r=\"8\" />",
70
+ "settings": "<path d=\"M9.671 4.136a2.34 2.34 0 0 1 4.659 0 2.34 2.34 0 0 0 3.319 1.915 2.34 2.34 0 0 1 2.33 4.033 2.34 2.34 0 0 0 0 3.831 2.34 2.34 0 0 1-2.33 4.033 2.34 2.34 0 0 0-3.319 1.915 2.34 2.34 0 0 1-4.659 0 2.34 2.34 0 0 0-3.32-1.915 2.34 2.34 0 0 1-2.33-4.033 2.34 2.34 0 0 0 0-3.831A2.34 2.34 0 0 1 6.35 6.051a2.34 2.34 0 0 0 3.319-1.915\" /><circle cx=\"12\" cy=\"12\" r=\"3\" />",
71
+ "settings-2": "<path d=\"M14 17H5\" /><path d=\"M19 7h-9\" /><circle cx=\"17\" cy=\"17\" r=\"3\" /><circle cx=\"7\" cy=\"7\" r=\"3\" />",
72
+ "share-2": "<circle cx=\"18\" cy=\"5\" r=\"3\" /><circle cx=\"6\" cy=\"12\" r=\"3\" /><circle cx=\"18\" cy=\"19\" r=\"3\" /><line x1=\"8.59\" x2=\"15.42\" y1=\"13.51\" y2=\"17.49\" /><line x1=\"15.41\" x2=\"8.59\" y1=\"6.51\" y2=\"10.49\" />",
73
+ "shield-check": "<path d=\"M20 13c0 5-3.5 7.5-7.66 8.95a1 1 0 0 1-.67-.01C7.5 20.5 4 18 4 13V6a1 1 0 0 1 1-1c2 0 4.5-1.2 6.24-2.72a1.17 1.17 0 0 1 1.52 0C14.51 3.81 17 5 19 5a1 1 0 0 1 1 1z\" /><path d=\"m9 12 2 2 4-4\" />",
74
+ "signal": "<path d=\"M2 20h.01\" /><path d=\"M7 20v-4\" /><path d=\"M12 20v-8\" /><path d=\"M17 20V8\" /><path d=\"M22 4v16\" />",
75
+ "sliders-horizontal": "<path d=\"M10 5H3\" /><path d=\"M12 19H3\" /><path d=\"M14 3v4\" /><path d=\"M16 17v4\" /><path d=\"M21 12h-9\" /><path d=\"M21 19h-5\" /><path d=\"M21 5h-7\" /><path d=\"M8 10v4\" /><path d=\"M8 12H3\" />",
76
+ "sparkles": "<path d=\"M11.017 2.814a1 1 0 0 1 1.966 0l1.051 5.558a2 2 0 0 0 1.594 1.594l5.558 1.051a1 1 0 0 1 0 1.966l-5.558 1.051a2 2 0 0 0-1.594 1.594l-1.051 5.558a1 1 0 0 1-1.966 0l-1.051-5.558a2 2 0 0 0-1.594-1.594l-5.558-1.051a1 1 0 0 1 0-1.966l5.558-1.051a2 2 0 0 0 1.594-1.594z\" /><path d=\"M20 2v4\" /><path d=\"M22 4h-4\" /><circle cx=\"4\" cy=\"20\" r=\"2\" />",
77
+ "star": "<path d=\"M11.525 2.295a.53.53 0 0 1 .95 0l2.31 4.679a2.123 2.123 0 0 0 1.595 1.16l5.166.756a.53.53 0 0 1 .294.904l-3.736 3.638a2.123 2.123 0 0 0-.611 1.878l.882 5.14a.53.53 0 0 1-.771.56l-4.618-2.428a2.122 2.122 0 0 0-1.973 0L6.396 21.01a.53.53 0 0 1-.77-.56l.881-5.139a2.122 2.122 0 0 0-.611-1.879L2.16 9.795a.53.53 0 0 1 .294-.906l5.165-.755a2.122 2.122 0 0 0 1.597-1.16z\" />",
78
+ "sun": "<circle cx=\"12\" cy=\"12\" r=\"4\" /><path d=\"M12 2v2\" /><path d=\"M12 20v2\" /><path d=\"m4.93 4.93 1.41 1.41\" /><path d=\"m17.66 17.66 1.41 1.41\" /><path d=\"M2 12h2\" /><path d=\"M20 12h2\" /><path d=\"m6.34 17.66-1.41 1.41\" /><path d=\"m19.07 4.93-1.41 1.41\" />",
79
+ "target": "<circle cx=\"12\" cy=\"12\" r=\"10\" /><circle cx=\"12\" cy=\"12\" r=\"6\" /><circle cx=\"12\" cy=\"12\" r=\"2\" />",
80
+ "thumbs-up": "<path d=\"M15 5.88 14 10h5.83a2 2 0 0 1 1.92 2.56l-2.33 8A2 2 0 0 1 17.5 22H4a2 2 0 0 1-2-2v-8a2 2 0 0 1 2-2h2.76a2 2 0 0 0 1.79-1.11L12 2a3.13 3.13 0 0 1 3 3.88Z\" /><path d=\"M7 10v12\" />",
81
+ "trending-down": "<path d=\"M16 17h6v-6\" /><path d=\"m22 17-8.5-8.5-5 5L2 7\" />",
82
+ "trending-up": "<path d=\"M16 7h6v6\" /><path d=\"m22 7-8.5 8.5-5-5L2 17\" />",
83
+ "umbrella": "<path d=\"M12 13v7a2 2 0 0 0 4 0\" /><path d=\"M12 2v2\" /><path d=\"M20.992 13a1 1 0 0 0 .97-1.274 10.284 10.284 0 0 0-19.923 0A1 1 0 0 0 3 13z\" />",
84
+ "user": "<path d=\"M19 21v-2a4 4 0 0 0-4-4H9a4 4 0 0 0-4 4v2\" /><circle cx=\"12\" cy=\"7\" r=\"4\" />",
85
+ "user-check": "<path d=\"m16 11 2 2 4-4\" /><path d=\"M16 21v-2a4 4 0 0 0-4-4H6a4 4 0 0 0-4 4v2\" /><circle cx=\"9\" cy=\"7\" r=\"4\" />",
86
+ "user-round": "<circle cx=\"12\" cy=\"8\" r=\"5\" /><path d=\"M20 21a8 8 0 0 0-16 0\" />",
87
+ "users": "<path d=\"M16 21v-2a4 4 0 0 0-4-4H6a4 4 0 0 0-4 4v2\" /><path d=\"M16 3.128a4 4 0 0 1 0 7.744\" /><path d=\"M22 21v-2a4 4 0 0 0-3-3.87\" /><circle cx=\"9\" cy=\"7\" r=\"4\" />",
88
+ "wifi": "<path d=\"M12 20h.01\" /><path d=\"M2 8.82a15 15 0 0 1 20 0\" /><path d=\"M5 12.859a10 10 0 0 1 14 0\" /><path d=\"M8.5 16.429a5 5 0 0 1 7 0\" />",
89
+ "x": "<path d=\"M18 6 6 18\" /><path d=\"m6 6 12 12\" />",
90
+ "zap": "<path d=\"M15.914 4a1.5 1.5 0 00-2.474-1.561l-9 9A1.5 1.5 0 005.5 14h4.002a.5.5 0 01.471.666L8.086 20a1.5 1.5 0 002.475 1.56l9-9A1.5 1.5 0 0018.5 10h-3.997a.5.5 0 01-.472-.667z\" />",
91
+ };
92
+
93
+ /** The markup for a glyph, or undefined when the set does not have it. */
94
+ export function iconBody(name) {
95
+ return Object.prototype.hasOwnProperty.call(ICONS, name) ? ICONS[name] : undefined;
96
+ }
97
+
98
+ /** Is this a glyph the set carries? */
99
+ export function hasIcon(name) {
100
+ return Object.prototype.hasOwnProperty.call(ICONS, name);
101
+ }
102
+
103
+ /** Every glyph name in the set, alphabetically. */
104
+ export function iconNames() {
105
+ return Object.keys(ICONS);
106
+ }
@@ -0,0 +1,133 @@
1
+ /**
2
+ * "Did you mean ...?" support.
3
+ * PURE ESM — no `node:` imports.
4
+ */
5
+
6
+ /** Levenshtein edit distance, two-row iterative. */
7
+ export function distance(a, b) {
8
+ const s = String(a);
9
+ const t = String(b);
10
+ if (s === t) return 0;
11
+ if (s.length === 0) return t.length;
12
+ if (t.length === 0) return s.length;
13
+
14
+ let prev = new Array(t.length + 1);
15
+ let curr = new Array(t.length + 1);
16
+ for (let j = 0; j <= t.length; j += 1) prev[j] = j;
17
+
18
+ for (let i = 1; i <= s.length; i += 1) {
19
+ curr[0] = i;
20
+ const sc = s.charCodeAt(i - 1);
21
+ for (let j = 1; j <= t.length; j += 1) {
22
+ const cost = sc === t.charCodeAt(j - 1) ? 0 : 1;
23
+ curr[j] = Math.min(curr[j - 1] + 1, prev[j] + 1, prev[j - 1] + cost);
24
+ }
25
+ const swap = prev;
26
+ prev = curr;
27
+ curr = swap;
28
+ }
29
+ return prev[t.length];
30
+ }
31
+
32
+ /**
33
+ * Return the closest candidate, or null when nothing is close enough.
34
+ * `maxDistance` defaults to a third of the input length (min 2, max 4).
35
+ */
36
+ export function suggest(input, candidates, maxDistance) {
37
+ const list = Array.from(candidates || []);
38
+ if (list.length === 0) return null;
39
+
40
+ const s = String(input);
41
+ const limit =
42
+ typeof maxDistance === "number"
43
+ ? maxDistance
44
+ : Math.min(4, Math.max(2, Math.floor(s.length / 3)));
45
+
46
+ let best = null;
47
+ let bestScore = Infinity;
48
+ for (const candidate of list) {
49
+ const c = String(candidate);
50
+ // An identical candidate is never a suggestion. "Did you mean home__title"
51
+ // when the author already wrote home__title teaches nothing, and callers
52
+ // that ask about a duplicate rely on getting the OTHER similar name back.
53
+ if (c === s) continue;
54
+ const d = distance(s, c);
55
+ if (d < bestScore || (d === bestScore && best !== null && c < best)) {
56
+ bestScore = d;
57
+ best = c;
58
+ }
59
+ }
60
+ return bestScore <= limit ? best : null;
61
+ }
62
+
63
+ /**
64
+ * Suggest from a list of token references, comparing only the name part so that
65
+ * `$color.surafce` still finds `$color.surface`.
66
+ */
67
+ export function suggestTokenRef(ref, candidates, maxDistance) {
68
+ const list = Array.from(candidates || []);
69
+ if (list.length === 0) return null;
70
+
71
+ const parts = String(ref).split(".");
72
+ const name = parts.length > 1 ? parts.slice(1).join(".") : String(ref);
73
+ const group = parts.length > 1 ? parts[0] : null;
74
+
75
+ const sameGroup = group ? list.filter((c) => String(c).startsWith(`${group}.`)) : [];
76
+ const pool = sameGroup.length > 0 ? sameGroup : list;
77
+
78
+ const stripped = pool.map((c) => {
79
+ const p = String(c).split(".");
80
+ return { full: c, name: p.length > 1 ? p.slice(1).join(".") : String(c) };
81
+ });
82
+
83
+ const hit = suggest(name, stripped.map((x) => x.name), maxDistance);
84
+ if (hit === null) return null;
85
+ const match = stripped.find((x) => x.name === hit);
86
+ return match ? match.full : null;
87
+ }
88
+
89
+ function bareName(value) {
90
+ const lower = String(value).toLowerCase();
91
+ return lower.startsWith("data-") ? lower.slice(5) : lower;
92
+ }
93
+
94
+ /**
95
+ * Suggest an attribute name for a typo: `backgound-color` -> `background-color`.
96
+ *
97
+ * Tried in order, so the answer never gets worse than an exact answer:
98
+ * the name itself, a case-insensitive match, the name without its `data-`
99
+ * prefix (`kind` -> `data-kind`), then a fuzzy match on either form.
100
+ *
101
+ * @param {string} name what the file wrote
102
+ * @param {Iterable<string>} allowedNames the attribute names the table allows
103
+ * @returns {string|null} the allowed name, or null when nothing is close
104
+ */
105
+ export function suggestAttr(name, allowedNames) {
106
+ const list = Array.from(allowedNames || [], (c) => String(c));
107
+ if (list.length === 0) return null;
108
+
109
+ const input = String(name ?? "");
110
+ if (input === "") return null;
111
+
112
+ const exact = list.find((c) => c === input);
113
+ if (exact) return exact;
114
+
115
+ const lower = input.toLowerCase();
116
+ const insensitive = list.find((c) => c.toLowerCase() === lower);
117
+ if (insensitive) return insensitive;
118
+
119
+ const pairs = list.map((c) => ({ full: c, bare: bareName(c) }));
120
+ const want = bareName(input);
121
+
122
+ const bareHit = pairs.find((p) => p.bare === want);
123
+ if (bareHit) return bareHit.full;
124
+
125
+ const fuzzy = suggest(want, pairs.map((p) => p.bare));
126
+ if (fuzzy !== null) {
127
+ const match = pairs.find((p) => p.bare === fuzzy);
128
+ if (match) return match.full;
129
+ }
130
+
131
+ const fuzzyFull = suggest(lower, pairs.map((p) => p.full));
132
+ return fuzzyFull;
133
+ }
@@ -0,0 +1,387 @@
1
+ /**
2
+ * The Node side of the command line: entry-point detection, argument parsing,
3
+ * the shared flags every verb accepts, and reading a design file once.
4
+ *
5
+ * A verb's `run()` returns the exit code: 0 success, 1 refused or invalid,
6
+ * 2 unreadable file or bad arguments (docs/format-v3.md §10).
7
+ */
8
+
9
+ import fs from "node:fs";
10
+ import path from "node:path";
11
+ import process from "node:process";
12
+ import { fileURLToPath } from "node:url";
13
+
14
+ import { detectFormat, readerFor } from "../detect.mjs";
15
+ import { readText, revisionOf } from "./fs.mjs";
16
+
17
+ export const EXIT_OK = 0;
18
+ export const EXIT_INVALID = 1;
19
+ export const EXIT_UNREADABLE = 2;
20
+
21
+ /** An error a verb raises to stop with a specific exit code and message. */
22
+ export class CliError extends Error {
23
+ /**
24
+ * @param {string} message
25
+ * @param {number} [exitCode] defaults to 2 — bad arguments
26
+ * @param {string} [code]
27
+ * @param {string} [hint] "here is what to write instead", for a refusal
28
+ * @param {object} [extra] extra fields, such as the offending property
29
+ */
30
+ constructor(message, exitCode = EXIT_UNREADABLE, code = "cli/error", hint = "", extra = {}) {
31
+ super(message);
32
+ this.name = "CliError";
33
+ this.exitCode = exitCode;
34
+ this.code = code;
35
+ // A refusal without a hint is a dead end for a caller that cannot see the
36
+ // file, so the hint is carried rather than dropped.
37
+ this.hint = hint;
38
+ Object.assign(this, extra);
39
+ }
40
+ }
41
+
42
+ /**
43
+ * Was this module the script Node was asked to run?
44
+ *
45
+ * The obvious test — `process.argv[1] === fileURLToPath(import.meta.url)` — is
46
+ * wrong for a skill, because that is exactly how a skill gets invoked:
47
+ *
48
+ * ~/.claude/skills/justskill-design -> ~/.agents/skills/justskill-design -> the repo
49
+ *
50
+ * Node's ESM loader resolves symlinks, so `import.meta.url` is the real
51
+ * repository path while `process.argv[1]` is whatever the caller typed. The two
52
+ * never match, `main()` never runs, and the process still exits 0 with no
53
+ * output. A silent success is the worst possible failure, so both sides are
54
+ * resolved properly here.
55
+ *
56
+ * @param {string} importMetaUrl pass `import.meta.url`
57
+ * @param {string} [argv1] defaults to `process.argv[1]`
58
+ * @returns {boolean}
59
+ */
60
+ export function isMainModule(importMetaUrl, argv1 = process.argv[1]) {
61
+ if (!argv1) return false;
62
+
63
+ const self = fileURLToPath(importMetaUrl);
64
+
65
+ let realSelf;
66
+ let realArgv;
67
+ try {
68
+ realSelf = fs.realpathSync(self);
69
+ realArgv = fs.realpathSync(argv1);
70
+ } catch {
71
+ // One of them does not exist, which happens in a bundled or piped
72
+ // invocation. Fall back to a plain comparison, still right when unlinked.
73
+ return path.resolve(argv1) === path.resolve(self);
74
+ }
75
+
76
+ return realArgv === realSelf;
77
+ }
78
+
79
+ /**
80
+ * A path to show a human: relative when that is shorter and still meaningful,
81
+ * absolute otherwise.
82
+ *
83
+ * `path.relative` happily produces `../../../../../tmp/x.html` for a file
84
+ * outside the working directory, which tells the reader nothing.
85
+ */
86
+ export function relativeOrAbsolute(target, cwd = process.cwd()) {
87
+ const relative = path.relative(cwd, target);
88
+ if (!relative) return target;
89
+ return relative.startsWith("..") ? target : relative;
90
+ }
91
+
92
+ /** The flags every verb accepts. Merge into a verb's own `flags`. */
93
+ export const SHARED_FLAGS = {
94
+ json: { type: "boolean", help: "print JSON instead of a human report" },
95
+ quiet: { type: "boolean", alias: ["q"], help: "print nothing on success" },
96
+ "dry-run": { type: "boolean", help: "print the result and write nothing" },
97
+ force: { type: "boolean", help: "proceed when the file already exists" },
98
+ help: { type: "boolean", alias: ["h"], help: "print this usage" },
99
+ };
100
+
101
+ function aliasesOf(definition) {
102
+ const alias = definition && definition.alias;
103
+ if (!alias) return [];
104
+ return Array.isArray(alias) ? alias.map(String) : [String(alias)];
105
+ }
106
+
107
+ function parseBoolean(raw, token) {
108
+ const value = String(raw).toLowerCase();
109
+ if (value === "true" || value === "1" || value === "yes") return true;
110
+ if (value === "false" || value === "0" || value === "no") return false;
111
+ throw new CliError(`Option ${token} takes true or false, got ${JSON.stringify(raw)}`);
112
+ }
113
+
114
+ /**
115
+ * Parse `argv` against a spec.
116
+ *
117
+ * ```js
118
+ * parseArgs(argv, {
119
+ * flags: { ...SHARED_FLAGS, name: { type: "string" }, depth: { type: "number" } },
120
+ * positionals: { min: 1, max: 1 },
121
+ * usage: USAGE,
122
+ * })
123
+ * ```
124
+ *
125
+ * Supported forms: `--flag`, `--flag=value`, `--flag value`, `--no-flag`, and
126
+ * single-letter aliases (`-q`). `--` ends flag parsing; everything after it is a
127
+ * positional. An unknown option is an error unless `spec.allowUnknown`.
128
+ *
129
+ * @param {string[]} argv
130
+ * @param {{flags?: object, positionals?: {min?: number, max?: number}, usage?: string, allowUnknown?: boolean}} [spec]
131
+ * @returns {{values: Record<string, any>, positionals: string[], provided: Set<string>, has: (name: string) => boolean}}
132
+ * @throws {CliError} exit code 2 when the arguments are wrong
133
+ */
134
+ export function parseArgs(argv, spec = {}) {
135
+ const flags = { ...SHARED_FLAGS, ...(spec.flags || {}) };
136
+ const lookup = new Map();
137
+ for (const [name, definition] of Object.entries(flags)) {
138
+ lookup.set(`--${name}`, { name, definition: definition || {} });
139
+ for (const alias of aliasesOf(definition)) {
140
+ lookup.set(alias.length === 1 ? `-${alias}` : `--${alias}`, { name, definition: definition || {} });
141
+ }
142
+ }
143
+
144
+ const values = {};
145
+ for (const [name, definition] of Object.entries(flags)) {
146
+ if (definition && "default" in definition) values[name] = definition.default;
147
+ }
148
+
149
+ const args = Array.isArray(argv) ? argv.map(String) : [];
150
+ const positionals = [];
151
+ const provided = new Set();
152
+ let index = 0;
153
+ let stopped = false;
154
+
155
+ const usageSuffix = spec.usage ? `\n\n${String(spec.usage).trim()}` : "";
156
+
157
+ while (index < args.length) {
158
+ const arg = args[index];
159
+
160
+ if (stopped || arg === "-" || !arg.startsWith("-")) {
161
+ positionals.push(arg);
162
+ index += 1;
163
+ continue;
164
+ }
165
+ if (arg === "--") {
166
+ stopped = true;
167
+ index += 1;
168
+ continue;
169
+ }
170
+
171
+ let token = arg;
172
+ let inline = null;
173
+ const equals = arg.startsWith("--") ? arg.indexOf("=") : -1;
174
+ if (equals !== -1) {
175
+ token = arg.slice(0, equals);
176
+ inline = arg.slice(equals + 1);
177
+ }
178
+
179
+ let entry = lookup.get(token);
180
+ let negated = false;
181
+ if (!entry && token.startsWith("--no-")) {
182
+ entry = lookup.get(`--${token.slice(5)}`);
183
+ negated = true;
184
+ }
185
+ if (!entry) {
186
+ if (spec.allowUnknown) {
187
+ values[token.replace(/^--?/, "")] = inline === null ? true : inline;
188
+ index += 1;
189
+ continue;
190
+ }
191
+ throw new CliError(`Unknown option: ${arg}${usageSuffix}`);
192
+ }
193
+
194
+ const { name, definition } = entry;
195
+ const type = definition.type || "boolean";
196
+ index += 1;
197
+
198
+ if (type === "boolean") {
199
+ values[name] = negated ? false : inline === null ? true : parseBoolean(inline, token);
200
+ provided.add(name);
201
+ continue;
202
+ }
203
+
204
+ let raw = inline;
205
+ if (raw === null) {
206
+ if (index >= args.length) throw new CliError(`Option ${token} needs a value${usageSuffix}`);
207
+ raw = args[index];
208
+ index += 1;
209
+ }
210
+
211
+ if (type === "number") {
212
+ const number = Number(raw);
213
+ if (!Number.isFinite(number)) {
214
+ throw new CliError(`Option ${token} needs a number, got ${JSON.stringify(raw)}${usageSuffix}`);
215
+ }
216
+ values[name] = number;
217
+ } else if (type === "list") {
218
+ if (!Array.isArray(values[name])) values[name] = [];
219
+ values[name].push(raw);
220
+ } else {
221
+ values[name] = raw;
222
+ }
223
+ provided.add(name);
224
+ }
225
+
226
+ // `--help` wins over a missing argument: `validate --help` must print the
227
+ // usage, not complain that no file was given.
228
+ if (values.help === true) {
229
+ return { values, positionals, provided, has: (name) => provided.has(name) };
230
+ }
231
+
232
+ const minimum = spec.positionals && spec.positionals.min;
233
+ const maximum = spec.positionals && spec.positionals.max;
234
+ if (Number.isInteger(minimum) && positionals.length < minimum) {
235
+ throw new CliError(`Expected ${minimum} argument${minimum === 1 ? "" : "s"}, got ${positionals.length}${usageSuffix}`);
236
+ }
237
+ if (Number.isInteger(maximum) && positionals.length > maximum) {
238
+ throw new CliError(`Unexpected extra argument: ${positionals[maximum]}${usageSuffix}`);
239
+ }
240
+
241
+ return { values, positionals, provided, has: (name) => provided.has(name) };
242
+ }
243
+
244
+ /**
245
+ * Print to stderr and exit.
246
+ *
247
+ * @param {string} message
248
+ * @param {number} [code] defaults to 1 — refused or invalid
249
+ * @returns {never}
250
+ */
251
+ export function fail(message, code = EXIT_INVALID) {
252
+ const text = message === undefined || message === null ? "" : String(message);
253
+ process.stderr.write(text.endsWith("\n") ? text : `${text}\n`);
254
+ process.exitCode = code;
255
+ process.exit(code);
256
+ }
257
+
258
+ /**
259
+ * Print one line to stdout, unless the caller asked for quiet.
260
+ *
261
+ * @param {string} text
262
+ * @param {{quiet?: boolean, stream?: NodeJS.WriteStream, newline?: boolean}} [options]
263
+ * @returns {boolean} true when something was written
264
+ */
265
+ export function out(text, options = {}) {
266
+ if (options && options.quiet) return false;
267
+ const stream = (options && options.stream) || process.stdout;
268
+ const value = text === undefined || text === null ? "" : String(text);
269
+ stream.write(options && options.newline === false ? value : `${value}\n`);
270
+ return true;
271
+ }
272
+
273
+ function pickExport(mod, names) {
274
+ if (!mod) return null;
275
+ for (const name of names) {
276
+ if (name === "default") {
277
+ if (typeof mod.default === "function") return mod.default;
278
+ continue;
279
+ }
280
+ if (typeof mod[name] === "function") return mod[name];
281
+ }
282
+ return null;
283
+ }
284
+
285
+ function unwrapDocument(value) {
286
+ if (!value || typeof value !== "object" || Array.isArray(value)) return null;
287
+ if (value.kind === "document") return value;
288
+ if (value.doc && typeof value.doc === "object") {
289
+ const doc = unwrapDocument(value.doc);
290
+ if (doc && !Array.isArray(doc.diagnostics) && Array.isArray(value.diagnostics)) {
291
+ doc.diagnostics = value.diagnostics;
292
+ }
293
+ return doc;
294
+ }
295
+ if (value.value && typeof value.value === "object") return unwrapDocument(value.value);
296
+ if (Array.isArray(value.children) || value.html || value.body) return value;
297
+ return null;
298
+ }
299
+
300
+ /**
301
+ * Read a design file and parse it with the reader its format selects.
302
+ *
303
+ * This is the shared front door of every verb. It uses `detectFormat` then the
304
+ * reader — never a hard-coded path — so a `.pen` file and an `.html` file are
305
+ * one code path.
306
+ *
307
+ * @param {string} file
308
+ * @returns {Promise<{file: string, text: string, format: string, doc: object, revision: string}>}
309
+ * @throws {FsError} `fs/not-found` / `fs/unreadable` — the caller maps these to exit 2
310
+ * @throws {CliError} exit 2 for an unknown format or a reader that throws
311
+ */
312
+ export async function readDesignFile(file) {
313
+ const target = String(file === undefined || file === null ? "" : file);
314
+ if (target === "") throw new CliError("No file given");
315
+
316
+ const text = readText(target);
317
+ const format = detectFormat(target, text);
318
+ if (format === "unknown") {
319
+ throw new CliError(
320
+ `Cannot tell what ${target} is: expected .html markup or .pen JSON`,
321
+ EXIT_UNREADABLE,
322
+ "detect/unknown-format",
323
+ );
324
+ }
325
+
326
+ const load = readerFor(format);
327
+ let mod;
328
+ try {
329
+ mod = await load();
330
+ } catch (error) {
331
+ throw new CliError(`Cannot load the ${format} reader: ${error.message}`, EXIT_UNREADABLE, "cli/reader");
332
+ }
333
+
334
+ const parse = pickExport(mod, ["parseHtml", "parse", "parseDocument", "read", "default"]);
335
+ if (!parse) {
336
+ throw new CliError(`The ${format} reader (lib/parse.mjs) exports no parse function`, EXIT_UNREADABLE, "cli/reader");
337
+ }
338
+
339
+ let produced;
340
+ try {
341
+ produced = parse(text, { file: target, format, source: text });
342
+ if (produced && typeof produced.then === "function") produced = await produced;
343
+ } catch (error) {
344
+ throw new CliError(`${target}: ${error.message}`, EXIT_UNREADABLE, "cli/parse");
345
+ }
346
+
347
+ const doc = unwrapDocument(produced);
348
+ if (!doc) {
349
+ throw new CliError(`${target}: the ${format} reader returned no document`, EXIT_UNREADABLE, "cli/parse");
350
+ }
351
+ if (typeof doc.source !== "string") {
352
+ try {
353
+ doc.source = text;
354
+ } catch {
355
+ /* a frozen document still works: `ls` and `show` fall back to the text */
356
+ }
357
+ }
358
+
359
+ return { file: target, text, format, doc, revision: revisionOf(text) };
360
+ }
361
+
362
+ /** `lib/` — every module a verb may need lives here, whatever the verb's directory. */
363
+ const LIB_ROOT = new URL("../", import.meta.url);
364
+
365
+ /**
366
+ * Import a `lib/` module and take the first function it exports under one of
367
+ * `names`.
368
+ *
369
+ * The specifier is a bare file name inside `lib/` — `"validate.mjs"` — so a verb
370
+ * in `lib/node/verbs/` never has to work out how many `../` it needs.
371
+ *
372
+ * @param {string} specifier a path inside `lib/`, for example `"validate.mjs"`
373
+ * @param {string[]} names export names to try, in order
374
+ * @param {string} what human name of the export, for the error message
375
+ * @returns {Promise<Function>}
376
+ */
377
+ export async function loadExport(specifier, names, what) {
378
+ let mod;
379
+ try {
380
+ mod = await import(new URL(specifier, LIB_ROOT).href);
381
+ } catch (error) {
382
+ throw new CliError(`Cannot load lib/${specifier}: ${error.message}`, EXIT_UNREADABLE, "cli/module");
383
+ }
384
+ const fn = pickExport(mod, names);
385
+ if (!fn) throw new CliError(`lib/${specifier} exports no ${what}()`, EXIT_UNREADABLE, "cli/module");
386
+ return fn;
387
+ }