@1agh/maude 0.42.0 → 0.44.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 (332) hide show
  1. package/apps/studio/.ai/cache/_stats.json +7 -0
  2. package/apps/studio/acp/bootstrap-brief.ts +12 -2
  3. package/apps/studio/acp/bridge.ts +44 -10
  4. package/apps/studio/acp/env.ts +23 -0
  5. package/apps/studio/acp/login-state.ts +263 -0
  6. package/apps/studio/acp/plugin-bootstrap.ts +31 -25
  7. package/apps/studio/acp/probe.ts +152 -1
  8. package/apps/studio/annotations-layer.tsx +1451 -324
  9. package/apps/studio/annotations-model.ts +58 -0
  10. package/apps/studio/api.ts +568 -24
  11. package/apps/studio/bin/_audio-search.mjs +140 -0
  12. package/apps/studio/bin/_import-asset-pdf-worker.mjs +34 -0
  13. package/apps/studio/bin/_import-asset.mjs +815 -0
  14. package/apps/studio/bin/_import-brand.mjs +635 -0
  15. package/apps/studio/bin/_import-tokens-alias-resolver.mjs +95 -0
  16. package/apps/studio/bin/_import-tokens.mjs +1201 -0
  17. package/apps/studio/bin/_ingest-footage.mjs +386 -0
  18. package/apps/studio/bin/_probe-footage-playwright.mjs +269 -0
  19. package/apps/studio/bin/_transcribe.mjs +419 -0
  20. package/apps/studio/bin/_transcribe.test.mjs +80 -0
  21. package/apps/studio/bin/_video-playwright.mjs +141 -71
  22. package/apps/studio/bin/audio-search.sh +28 -0
  23. package/apps/studio/bin/generate.sh +154 -0
  24. package/apps/studio/bin/import-asset.sh +34 -0
  25. package/apps/studio/bin/import-brand.sh +29 -0
  26. package/apps/studio/bin/import-tokens.sh +33 -0
  27. package/apps/studio/bin/ingest-footage.sh +27 -0
  28. package/apps/studio/bin/photo-adjust.sh +163 -0
  29. package/apps/studio/bin/photo-bg-remove.sh +258 -0
  30. package/apps/studio/bin/probe-footage.sh +28 -0
  31. package/apps/studio/bin/read-annotations.mjs +125 -2
  32. package/apps/studio/bin/transcribe.sh +39 -0
  33. package/apps/studio/canvas-edit.ts +656 -12
  34. package/apps/studio/canvas-lib.tsx +637 -10
  35. package/apps/studio/canvas-pipeline.ts +65 -7
  36. package/apps/studio/canvas-shell.tsx +156 -16
  37. package/apps/studio/client/app.jsx +2385 -699
  38. package/apps/studio/client/export-center.jsx +35 -1
  39. package/apps/studio/client/generate-dialog.jsx +352 -0
  40. package/apps/studio/client/github.js +18 -0
  41. package/apps/studio/client/inspector-controls.jsx +781 -0
  42. package/apps/studio/client/panels/BrandUploadPanel.jsx +233 -0
  43. package/apps/studio/client/panels/ChatPanel.jsx +15 -33
  44. package/apps/studio/client/panels/GitPanel.jsx +8 -0
  45. package/apps/studio/client/panels/IntroVideoDialog.jsx +44 -0
  46. package/apps/studio/client/panels/OnboardingWizard.jsx +12 -0
  47. package/apps/studio/client/panels/ReadinessList.jsx +227 -7
  48. package/apps/studio/client/panels/RepoBranchSwitcher.jsx +133 -21
  49. package/apps/studio/client/panels/SettingsPanel.jsx +828 -0
  50. package/apps/studio/client/panels/SetupChecklist.jsx +223 -0
  51. package/apps/studio/client/panels/StickerPicker.jsx +160 -0
  52. package/apps/studio/client/panels/TimelinePanel.jsx +15 -2
  53. package/apps/studio/client/photo-knobs.jsx +432 -0
  54. package/apps/studio/client/styles/3-shell-maude.css +376 -28
  55. package/apps/studio/client/styles/4-components.css +223 -0
  56. package/apps/studio/client/styles/6-acp-chat.css +86 -0
  57. package/apps/studio/client/tour/quick-setup-tour.js +39 -0
  58. package/apps/studio/commands/annotation-strokes-command.ts +13 -3
  59. package/apps/studio/commands/edit-source-command.ts +31 -4
  60. package/apps/studio/config.schema.json +70 -0
  61. package/apps/studio/context-menu.tsx +42 -8
  62. package/apps/studio/context.ts +16 -0
  63. package/apps/studio/design-setup-readiness.ts +115 -0
  64. package/apps/studio/dist/client.bundle.js +5572 -21
  65. package/apps/studio/dist/comment-mount.js +2 -2
  66. package/apps/studio/dist/runtime/.min-sizes.json +1 -0
  67. package/apps/studio/dist/runtime/@imgly_background-removal.js +5543 -0
  68. package/apps/studio/dist/runtime/pixi-js.js +519 -519
  69. package/apps/studio/dist/styles.css +1 -1
  70. package/apps/studio/dom-selection.ts +25 -0
  71. package/apps/studio/draw/palette.ts +34 -0
  72. package/apps/studio/exporters/jobs.ts +47 -5
  73. package/apps/studio/exporters/video.ts +24 -4
  74. package/apps/studio/footage/schema.test.ts +287 -0
  75. package/apps/studio/footage/schema.ts +662 -0
  76. package/apps/studio/footage-store.ts +235 -0
  77. package/apps/studio/fs-watch.ts +10 -0
  78. package/apps/studio/generation/adapters/elevenlabs.ts +370 -0
  79. package/apps/studio/generation/adapters/gemini.ts +542 -0
  80. package/apps/studio/generation/adapters/groq.test.ts +80 -0
  81. package/apps/studio/generation/adapters/groq.ts +247 -0
  82. package/apps/studio/generation/audio-library.test.ts +95 -0
  83. package/apps/studio/generation/audio-library.ts +156 -0
  84. package/apps/studio/generation/captions.test.ts +78 -0
  85. package/apps/studio/generation/captions.ts +132 -0
  86. package/apps/studio/generation/download.ts +76 -0
  87. package/apps/studio/generation/elevenlabs.test.ts +223 -0
  88. package/apps/studio/generation/gemini.test.ts +350 -0
  89. package/apps/studio/generation/jobs.test.ts +128 -0
  90. package/apps/studio/generation/jobs.ts +243 -0
  91. package/apps/studio/generation/keys.test.ts +75 -0
  92. package/apps/studio/generation/keys.ts +162 -0
  93. package/apps/studio/generation/prefs.test.ts +95 -0
  94. package/apps/studio/generation/prefs.ts +82 -0
  95. package/apps/studio/generation/registry.ts +88 -0
  96. package/apps/studio/generation/types.test.ts +123 -0
  97. package/apps/studio/generation/types.ts +266 -0
  98. package/apps/studio/generation/whisper-models.test.ts +152 -0
  99. package/apps/studio/generation/whisper-models.ts +257 -0
  100. package/apps/studio/generation/whisper-spike-results.md +61 -0
  101. package/apps/studio/git/endpoints.ts +68 -7
  102. package/apps/studio/git/service.ts +199 -21
  103. package/apps/studio/github/service.ts +67 -0
  104. package/apps/studio/handoff.ts +3 -2
  105. package/apps/studio/http.ts +1039 -22
  106. package/apps/studio/input-router.tsx +10 -0
  107. package/apps/studio/inspect.ts +53 -1
  108. package/apps/studio/media/intro.mp4 +0 -0
  109. package/apps/studio/media-commit-chain.ts +119 -0
  110. package/apps/studio/paths.ts +20 -0
  111. package/apps/studio/photo/filters.ts +285 -0
  112. package/apps/studio/photo/pipeline.ts +607 -0
  113. package/apps/studio/photo/schema.ts +367 -0
  114. package/apps/studio/photo-store.ts +154 -0
  115. package/apps/studio/readiness.ts +108 -20
  116. package/apps/studio/runtime-bundle.ts +29 -6
  117. package/apps/studio/scaffold-design.ts +1145 -0
  118. package/apps/studio/server.ts +18 -2
  119. package/apps/studio/stickers/figjam-doodle/manifest.json +104 -0
  120. package/apps/studio/stickers/figjam-doodle/slice1.png +0 -0
  121. package/apps/studio/stickers/figjam-doodle/slice10.png +0 -0
  122. package/apps/studio/stickers/figjam-doodle/slice11.png +0 -0
  123. package/apps/studio/stickers/figjam-doodle/slice12.png +0 -0
  124. package/apps/studio/stickers/figjam-doodle/slice13.png +0 -0
  125. package/apps/studio/stickers/figjam-doodle/slice14.png +0 -0
  126. package/apps/studio/stickers/figjam-doodle/slice15.png +0 -0
  127. package/apps/studio/stickers/figjam-doodle/slice16.png +0 -0
  128. package/apps/studio/stickers/figjam-doodle/slice17.png +0 -0
  129. package/apps/studio/stickers/figjam-doodle/slice18.png +0 -0
  130. package/apps/studio/stickers/figjam-doodle/slice19.png +0 -0
  131. package/apps/studio/stickers/figjam-doodle/slice2.png +0 -0
  132. package/apps/studio/stickers/figjam-doodle/slice20.png +0 -0
  133. package/apps/studio/stickers/figjam-doodle/slice21.png +0 -0
  134. package/apps/studio/stickers/figjam-doodle/slice22.png +0 -0
  135. package/apps/studio/stickers/figjam-doodle/slice23.png +0 -0
  136. package/apps/studio/stickers/figjam-doodle/slice24.png +0 -0
  137. package/apps/studio/stickers/figjam-doodle/slice3.png +0 -0
  138. package/apps/studio/stickers/figjam-doodle/slice4.png +0 -0
  139. package/apps/studio/stickers/figjam-doodle/slice5.png +0 -0
  140. package/apps/studio/stickers/figjam-doodle/slice6.png +0 -0
  141. package/apps/studio/stickers/figjam-doodle/slice7.png +0 -0
  142. package/apps/studio/stickers/figjam-doodle/slice8.png +0 -0
  143. package/apps/studio/stickers/figjam-doodle/slice9.png +0 -0
  144. package/apps/studio/stickers/life-style/best.png +0 -0
  145. package/apps/studio/stickers/life-style/come-on.png +0 -0
  146. package/apps/studio/stickers/life-style/cut.png +0 -0
  147. package/apps/studio/stickers/life-style/detox.png +0 -0
  148. package/apps/studio/stickers/life-style/just.png +0 -0
  149. package/apps/studio/stickers/life-style/manifest.json +68 -0
  150. package/apps/studio/stickers/life-style/mirror.png +0 -0
  151. package/apps/studio/stickers/life-style/nice.png +0 -0
  152. package/apps/studio/stickers/life-style/objects.png +0 -0
  153. package/apps/studio/stickers/life-style/ok.png +0 -0
  154. package/apps/studio/stickers/life-style/queen.png +0 -0
  155. package/apps/studio/stickers/life-style/star.png +0 -0
  156. package/apps/studio/stickers/life-style/sun.png +0 -0
  157. package/apps/studio/stickers/life-style/water.png +0 -0
  158. package/apps/studio/stickers/life-style/yeah.png +0 -0
  159. package/apps/studio/stickers/life-style/you-can.png +0 -0
  160. package/apps/studio/stickers/opposing-thoughts/check-the-deets.png +0 -0
  161. package/apps/studio/stickers/opposing-thoughts/cut-it.png +0 -0
  162. package/apps/studio/stickers/opposing-thoughts/dope.png +0 -0
  163. package/apps/studio/stickers/opposing-thoughts/fresh.png +0 -0
  164. package/apps/studio/stickers/opposing-thoughts/hot.png +0 -0
  165. package/apps/studio/stickers/opposing-thoughts/idea.png +0 -0
  166. package/apps/studio/stickers/opposing-thoughts/keep-exploring.png +0 -0
  167. package/apps/studio/stickers/opposing-thoughts/killed-it.png +0 -0
  168. package/apps/studio/stickers/opposing-thoughts/love-it.png +0 -0
  169. package/apps/studio/stickers/opposing-thoughts/manifest.json +88 -0
  170. package/apps/studio/stickers/opposing-thoughts/not-sure.png +0 -0
  171. package/apps/studio/stickers/opposing-thoughts/ok.png +0 -0
  172. package/apps/studio/stickers/opposing-thoughts/pure-gold.png +0 -0
  173. package/apps/studio/stickers/opposing-thoughts/save-for-later.png +0 -0
  174. package/apps/studio/stickers/opposing-thoughts/steal-this.png +0 -0
  175. package/apps/studio/stickers/opposing-thoughts/take-a-peek.png +0 -0
  176. package/apps/studio/stickers/opposing-thoughts/this-or-that.png +0 -0
  177. package/apps/studio/stickers/opposing-thoughts/thoughts.png +0 -0
  178. package/apps/studio/stickers/opposing-thoughts/vibes.png +0 -0
  179. package/apps/studio/stickers/opposing-thoughts/winner.png +0 -0
  180. package/apps/studio/stickers/opposing-thoughts/wip.png +0 -0
  181. package/apps/studio/stickers/project-status/group-100.png +0 -0
  182. package/apps/studio/stickers/project-status/group-101.png +0 -0
  183. package/apps/studio/stickers/project-status/group-102.png +0 -0
  184. package/apps/studio/stickers/project-status/group-103.png +0 -0
  185. package/apps/studio/stickers/project-status/group-104.png +0 -0
  186. package/apps/studio/stickers/project-status/group-105.png +0 -0
  187. package/apps/studio/stickers/project-status/group-106.png +0 -0
  188. package/apps/studio/stickers/project-status/group-107.png +0 -0
  189. package/apps/studio/stickers/project-status/group-108.png +0 -0
  190. package/apps/studio/stickers/project-status/group-109.png +0 -0
  191. package/apps/studio/stickers/project-status/group-110.png +0 -0
  192. package/apps/studio/stickers/project-status/group-111.png +0 -0
  193. package/apps/studio/stickers/project-status/group-112.png +0 -0
  194. package/apps/studio/stickers/project-status/group-113.png +0 -0
  195. package/apps/studio/stickers/project-status/group-114.png +0 -0
  196. package/apps/studio/stickers/project-status/group-115.png +0 -0
  197. package/apps/studio/stickers/project-status/group-117.png +0 -0
  198. package/apps/studio/stickers/project-status/group-118.png +0 -0
  199. package/apps/studio/stickers/project-status/group-119.png +0 -0
  200. package/apps/studio/stickers/project-status/group-120.png +0 -0
  201. package/apps/studio/stickers/project-status/group-121.png +0 -0
  202. package/apps/studio/stickers/project-status/group-122.png +0 -0
  203. package/apps/studio/stickers/project-status/group-41.png +0 -0
  204. package/apps/studio/stickers/project-status/group-42.png +0 -0
  205. package/apps/studio/stickers/project-status/group-43.png +0 -0
  206. package/apps/studio/stickers/project-status/group-44.png +0 -0
  207. package/apps/studio/stickers/project-status/group-45.png +0 -0
  208. package/apps/studio/stickers/project-status/group-46.png +0 -0
  209. package/apps/studio/stickers/project-status/group-47.png +0 -0
  210. package/apps/studio/stickers/project-status/group-48.png +0 -0
  211. package/apps/studio/stickers/project-status/group-49.png +0 -0
  212. package/apps/studio/stickers/project-status/group-50.png +0 -0
  213. package/apps/studio/stickers/project-status/group-51.png +0 -0
  214. package/apps/studio/stickers/project-status/group-52.png +0 -0
  215. package/apps/studio/stickers/project-status/group-53.png +0 -0
  216. package/apps/studio/stickers/project-status/group-54.png +0 -0
  217. package/apps/studio/stickers/project-status/group-55.png +0 -0
  218. package/apps/studio/stickers/project-status/group-56.png +0 -0
  219. package/apps/studio/stickers/project-status/group-57.png +0 -0
  220. package/apps/studio/stickers/project-status/group-58.png +0 -0
  221. package/apps/studio/stickers/project-status/group-59.png +0 -0
  222. package/apps/studio/stickers/project-status/group-60.png +0 -0
  223. package/apps/studio/stickers/project-status/group-61.png +0 -0
  224. package/apps/studio/stickers/project-status/group-62.png +0 -0
  225. package/apps/studio/stickers/project-status/group-63.png +0 -0
  226. package/apps/studio/stickers/project-status/group-64.png +0 -0
  227. package/apps/studio/stickers/project-status/group-65.png +0 -0
  228. package/apps/studio/stickers/project-status/group-66.png +0 -0
  229. package/apps/studio/stickers/project-status/group-67.png +0 -0
  230. package/apps/studio/stickers/project-status/group-68.png +0 -0
  231. package/apps/studio/stickers/project-status/group-69.png +0 -0
  232. package/apps/studio/stickers/project-status/group-70.png +0 -0
  233. package/apps/studio/stickers/project-status/group-71.png +0 -0
  234. package/apps/studio/stickers/project-status/group-72.png +0 -0
  235. package/apps/studio/stickers/project-status/group-73.png +0 -0
  236. package/apps/studio/stickers/project-status/group-74.png +0 -0
  237. package/apps/studio/stickers/project-status/group-75.png +0 -0
  238. package/apps/studio/stickers/project-status/group-76.png +0 -0
  239. package/apps/studio/stickers/project-status/group-77.png +0 -0
  240. package/apps/studio/stickers/project-status/group-78.png +0 -0
  241. package/apps/studio/stickers/project-status/group-79.png +0 -0
  242. package/apps/studio/stickers/project-status/group-80.png +0 -0
  243. package/apps/studio/stickers/project-status/group-81.png +0 -0
  244. package/apps/studio/stickers/project-status/group-82.png +0 -0
  245. package/apps/studio/stickers/project-status/group-83.png +0 -0
  246. package/apps/studio/stickers/project-status/group-84.png +0 -0
  247. package/apps/studio/stickers/project-status/group-85.png +0 -0
  248. package/apps/studio/stickers/project-status/group-86.png +0 -0
  249. package/apps/studio/stickers/project-status/group-87.png +0 -0
  250. package/apps/studio/stickers/project-status/group-88.png +0 -0
  251. package/apps/studio/stickers/project-status/group-89.png +0 -0
  252. package/apps/studio/stickers/project-status/group-90.png +0 -0
  253. package/apps/studio/stickers/project-status/group-91.png +0 -0
  254. package/apps/studio/stickers/project-status/group-92.png +0 -0
  255. package/apps/studio/stickers/project-status/group-93.png +0 -0
  256. package/apps/studio/stickers/project-status/group-94.png +0 -0
  257. package/apps/studio/stickers/project-status/group-96.png +0 -0
  258. package/apps/studio/stickers/project-status/group-97.png +0 -0
  259. package/apps/studio/stickers/project-status/group-98.png +0 -0
  260. package/apps/studio/stickers/project-status/group-99.png +0 -0
  261. package/apps/studio/stickers/project-status/manifest.json +328 -0
  262. package/apps/studio/test/_helpers.ts +15 -2
  263. package/apps/studio/test/acp-bootstrap-brief.test.ts +11 -0
  264. package/apps/studio/test/acp-bridge.test.ts +34 -1
  265. package/apps/studio/test/acp-commands.test.ts +2 -1
  266. package/apps/studio/test/acp-env.test.ts +30 -0
  267. package/apps/studio/test/acp-plugin-bootstrap.test.ts +23 -57
  268. package/apps/studio/test/acp-session-plugins.test.ts +72 -1
  269. package/apps/studio/test/annotation-strokes-command.test.ts +17 -0
  270. package/apps/studio/test/annotations-layer.test.ts +174 -0
  271. package/apps/studio/test/asset-api.test.ts +3 -3
  272. package/apps/studio/test/canvas-edit.test.ts +17 -6
  273. package/apps/studio/test/canvas-origin-gate.test.ts +50 -0
  274. package/apps/studio/test/csp-canvas-shell.test.ts +11 -0
  275. package/apps/studio/test/csrf-write-guard.test.ts +23 -1
  276. package/apps/studio/test/design-setup-readiness.test.ts +155 -0
  277. package/apps/studio/test/dynamic-text-edit.test.ts +162 -0
  278. package/apps/studio/test/edit-source-command.test.ts +23 -0
  279. package/apps/studio/test/element-structural-edit.test.ts +154 -0
  280. package/apps/studio/test/fixtures/fake-claude-auth.mjs +13 -0
  281. package/apps/studio/test/footage-store.test.ts +100 -0
  282. package/apps/studio/test/generate-route.test.ts +106 -0
  283. package/apps/studio/test/git-branches.test.ts +97 -0
  284. package/apps/studio/test/github-api.test.ts +56 -1
  285. package/apps/studio/test/import-asset-browser.test.ts +64 -0
  286. package/apps/studio/test/import-asset-route.test.ts +71 -0
  287. package/apps/studio/test/import-asset.test.ts +399 -0
  288. package/apps/studio/test/import-brand-browser.test.ts +88 -0
  289. package/apps/studio/test/import-brand-route.test.ts +120 -0
  290. package/apps/studio/test/import-brand.test.ts +206 -0
  291. package/apps/studio/test/import-tokens.test.ts +722 -0
  292. package/apps/studio/test/input-router.test.ts +33 -0
  293. package/apps/studio/test/inspect-script-syntax.test.ts +53 -0
  294. package/apps/studio/test/media-commit-chain.test.ts +210 -0
  295. package/apps/studio/test/photo-bg-remove-validation.test.ts +71 -0
  296. package/apps/studio/test/photo-canvas-bundle.test.ts +61 -0
  297. package/apps/studio/test/photo-edit-api.test.ts +201 -0
  298. package/apps/studio/test/photo-filters.test.ts +149 -0
  299. package/apps/studio/test/photo-pipeline.test.ts +106 -0
  300. package/apps/studio/test/photo-taxonomy.test.ts +96 -0
  301. package/apps/studio/test/read-annotations.test.ts +164 -0
  302. package/apps/studio/test/readiness.test.ts +13 -11
  303. package/apps/studio/test/scaffold-design.test.ts +122 -0
  304. package/apps/studio/test/shell-importmap.test.ts +49 -0
  305. package/apps/studio/test/text-editability-stamp.test.ts +131 -0
  306. package/apps/studio/test/timeline-parse.test.ts +57 -0
  307. package/apps/studio/test/tool-palette-insert-anchor.test.ts +5 -3
  308. package/apps/studio/test/tour-overlay.test.tsx +16 -0
  309. package/apps/studio/test/undo-stack.test.ts +33 -0
  310. package/apps/studio/test/video-asset.test.ts +5 -5
  311. package/apps/studio/test/video-render-bridge.test.ts +23 -1
  312. package/apps/studio/text-caret.ts +239 -0
  313. package/apps/studio/tool-palette.tsx +49 -21
  314. package/apps/studio/ui-prefs.ts +130 -0
  315. package/apps/studio/undo-stack.ts +94 -2
  316. package/apps/studio/use-annotation-resize.tsx +4 -4
  317. package/apps/studio/use-canvas-media-drop.tsx +40 -1
  318. package/apps/studio/use-chrome-visibility.tsx +8 -2
  319. package/apps/studio/use-selection-set.tsx +21 -0
  320. package/apps/studio/video-comp.tsx +48 -7
  321. package/apps/studio/whats-new.json +149 -0
  322. package/apps/studio/ws.ts +5 -0
  323. package/cli/bin/maude.mjs +6 -6
  324. package/cli/commands/design.mjs +67 -3
  325. package/cli/lib/gitignore-block.mjs +2 -0
  326. package/cli/lib/pkg-root.mjs +107 -0
  327. package/cli/lib/pkg-root.test.mjs +79 -0
  328. package/cli/lib/reconstruct-toolset.test.mjs +194 -0
  329. package/cli/lib/update-check.mjs +8 -0
  330. package/package.json +13 -11
  331. package/plugins/design/dependencies.json +18 -0
  332. package/plugins/design/templates/_shell.html +1 -0
@@ -0,0 +1,1201 @@
1
+ // _import-tokens.mjs — token-file import: W3C design-tokens / Style-Dictionary
2
+ // JSON / raw CSS custom-properties → Maude's DS CSS-variable contract.
3
+ // Reached via `maude design import-tokens` (DDR-062), never a raw bin path.
4
+ //
5
+ // See DDR-172 for the full mapping contract + security posture. Every
6
+ // numbered "Decision N" comment below points at the exact DDR clause it
7
+ // implements. CLI-only (no HTTP route, no in-app panel — DDR-172 Instruments).
8
+
9
+ import { createHash } from 'node:crypto';
10
+ import {
11
+ existsSync,
12
+ lstatSync,
13
+ mkdirSync,
14
+ readFileSync,
15
+ realpathSync,
16
+ renameSync,
17
+ writeFileSync,
18
+ } from 'node:fs';
19
+ import { join, resolve, sep } from 'node:path';
20
+ import { pathToFileURL } from 'node:url';
21
+ import { oklchToHex, parseColor, parseOklch, rgbToOklch } from '../draw/palette.ts';
22
+ import { DESIGN_PLUGIN_DIR } from '../paths.ts';
23
+ import { resolveAliases } from './_import-tokens-alias-resolver.mjs';
24
+
25
+ export class ImportTokensError extends Error {
26
+ constructor(code, message) {
27
+ super(message);
28
+ this.code = code;
29
+ }
30
+ }
31
+
32
+ // Exit codes: 0 ok · 2 usage · 3 validation/mapping reject · 4 read/parse
33
+ // error · 5 unsupported format · 6 write/containment error · 1 other.
34
+
35
+ // ============================================================================
36
+ // Decision 1 — input read (mirrors DDR-167 Decision 2 step 1: realpath-then-
37
+ // open, symlink-component rejection, single capped read) + format-appropriate
38
+ // parsers (native JSON.parse; a bespoke tokenizer for raw CSS — NOT
39
+ // lightningcss, see the DDR's Round-1 revision, and NOT a regex).
40
+ // ============================================================================
41
+
42
+ export const TOKENS_MAX_BYTES = 2 * 1024 * 1024;
43
+
44
+ /** Decision 1 — realpath-then-open, symlink-component rejection, single capped read. */
45
+ export function readTokenFileCapped(inputPath, maxBytes = TOKENS_MAX_BYTES) {
46
+ // Reject when the LEAF (the file itself, pre-resolution) is a symlink —
47
+ // the classic check-then-read substitution window: the file getting
48
+ // swapped out from under this read between validation and open. Checked
49
+ // against the ORIGINAL path (never the realpath-resolved one, which by
50
+ // definition can no longer show as a symlink). Deliberately NOT walking
51
+ // every ANCESTOR directory component: on macOS, `/tmp`/`/var` are
52
+ // themselves OS-level symlinks (`-> /private/tmp`/`-> /private/var`), so a
53
+ // whole-chain check rejects essentially every legitimate local path and
54
+ // isn't the threat this closes anyway — an attacker who can plant a
55
+ // symlink in a PARENT directory the operator already trusts and navigated
56
+ // into has an easier path to the same data than racing this read (the
57
+ // same residual DDR-167 Decision 2 step 1 already accepts explicitly).
58
+ const absOriginal = resolve(inputPath);
59
+ let leafStat;
60
+ try {
61
+ leafStat = lstatSync(absOriginal);
62
+ } catch (err) {
63
+ throw new ImportTokensError(4, `could not resolve path: ${err?.message ?? err}`);
64
+ }
65
+ if (leafStat.isSymbolicLink()) {
66
+ throw new ImportTokensError(3, `refusing a path whose target is a symlink: ${absOriginal}`);
67
+ }
68
+ let real;
69
+ try {
70
+ real = realpathSync(inputPath);
71
+ } catch (err) {
72
+ throw new ImportTokensError(4, `could not resolve path: ${err?.message ?? err}`);
73
+ }
74
+ let buf;
75
+ try {
76
+ buf = readFileSync(real);
77
+ } catch (err) {
78
+ throw new ImportTokensError(4, `could not read file: ${err?.message ?? err}`);
79
+ }
80
+ if (buf.length === 0) throw new ImportTokensError(4, 'file is empty');
81
+ if (buf.length > maxBytes) {
82
+ throw new ImportTokensError(
83
+ 4,
84
+ `token file exceeds the ${Math.round(maxBytes / (1024 * 1024))} MB cap`
85
+ );
86
+ }
87
+ let text;
88
+ try {
89
+ text = buf.toString('utf8');
90
+ } catch (err) {
91
+ throw new ImportTokensError(4, `could not decode as UTF-8: ${err?.message ?? err}`);
92
+ }
93
+ return text;
94
+ }
95
+
96
+ // ---- Bespoke CSS structural scanner (comment/string-aware, no regex-over-
97
+ // adversarial-text) — shared by Decision 1's raw-CSS input tokenizer AND
98
+ // Decision 7's theme-block locator on the trusted OUTPUT file. ----
99
+
100
+ /** Walk `text`, returning every `{...}` rule as `{selector, bodyStart, bodyEnd, nestedInAtRule}`. */
101
+ function scanCssRules(text) {
102
+ const rules = [];
103
+ const n = text.length;
104
+ let i = 0;
105
+ let selectorStart = 0;
106
+ const stack = [];
107
+ while (i < n) {
108
+ const ch = text[i];
109
+ if (ch === '/' && text[i + 1] === '*') {
110
+ const end = text.indexOf('*/', i + 2);
111
+ i = end === -1 ? n : end + 2;
112
+ continue;
113
+ }
114
+ if (ch === '"' || ch === "'") {
115
+ const quote = ch;
116
+ i += 1;
117
+ while (i < n) {
118
+ if (text[i] === '\\') {
119
+ i += 2;
120
+ continue;
121
+ }
122
+ if (text[i] === quote) {
123
+ i += 1;
124
+ break;
125
+ }
126
+ i += 1;
127
+ }
128
+ continue;
129
+ }
130
+ if (ch === '{') {
131
+ const selector = text.slice(selectorStart, i);
132
+ const parentAtRule = stack.length ? stack[stack.length - 1].nestedInAtRule : false;
133
+ const isAtRule = /^\s*@/.test(selector);
134
+ stack.push({ selector, bodyStart: i + 1, nestedInAtRule: parentAtRule || isAtRule });
135
+ i += 1;
136
+ selectorStart = i;
137
+ continue;
138
+ }
139
+ if (ch === '}') {
140
+ const top = stack.pop();
141
+ if (top) {
142
+ rules.push({
143
+ selector: top.selector,
144
+ bodyStart: top.bodyStart,
145
+ bodyEnd: i,
146
+ nestedInAtRule: top.nestedInAtRule,
147
+ });
148
+ }
149
+ i += 1;
150
+ selectorStart = i;
151
+ continue;
152
+ }
153
+ i += 1;
154
+ }
155
+ return rules;
156
+ }
157
+
158
+ /** Extract `--name: value;` declarations from a rule body, comment/string-aware. `offset` shifts returned positions into the ORIGINAL text's coordinate space. */
159
+ function extractDeclarations(bodyText, offset = 0) {
160
+ const out = [];
161
+ const n = bodyText.length;
162
+ let i = 0;
163
+ let declStart = 0;
164
+ while (i < n) {
165
+ const ch = bodyText[i];
166
+ if (ch === '/' && bodyText[i + 1] === '*') {
167
+ const end = bodyText.indexOf('*/', i + 2);
168
+ i = end === -1 ? n : end + 2;
169
+ continue;
170
+ }
171
+ if (ch === '"' || ch === "'") {
172
+ const quote = ch;
173
+ i += 1;
174
+ while (i < n) {
175
+ if (bodyText[i] === '\\') {
176
+ i += 2;
177
+ continue;
178
+ }
179
+ if (bodyText[i] === quote) {
180
+ i += 1;
181
+ break;
182
+ }
183
+ i += 1;
184
+ }
185
+ continue;
186
+ }
187
+ if (ch === ';') {
188
+ const decl = bodyText.slice(declStart, i);
189
+ // A trailing `/* ... */` comment from the PREVIOUS declaration (or a
190
+ // section-header comment) commonly lands at the START of this slice —
191
+ // tolerate any number of interleaved whitespace/comment spans before
192
+ // the actual `--name:`, not just leading whitespace.
193
+ const m = /^(?:\s|\/\*[\s\S]*?\*\/)*(--[A-Za-z0-9-]+)\s*:/.exec(decl);
194
+ if (m) {
195
+ const afterColon = decl.slice(m[0].length);
196
+ const leadingWs = afterColon.length - afterColon.trimStart().length;
197
+ const trailingWs = afterColon.length - afterColon.trimEnd().length;
198
+ const valueStart = offset + declStart + m[0].length + leadingWs;
199
+ const valueEnd = offset + declStart + decl.length - trailingWs;
200
+ out.push({
201
+ name: m[1],
202
+ rawValue: afterColon.trim(),
203
+ declStart: offset + declStart,
204
+ declEnd: offset + i + 1,
205
+ valueStart,
206
+ valueEnd,
207
+ });
208
+ }
209
+ declStart = i + 1;
210
+ i += 1;
211
+ continue;
212
+ }
213
+ i += 1;
214
+ }
215
+ return out;
216
+ }
217
+
218
+ /**
219
+ * Decision 1 — raw-CSS-custom-properties input tokenizer. Extracts every
220
+ * `--name: value;` declaration from every rule body in the file (regardless
221
+ * of nesting — the INPUT side has no reason to exclude @media-nested
222
+ * declarations; that exclusion is specific to Decision 7's locator on
223
+ * Maude's own OUTPUT file, to avoid matching a duplicate selector there).
224
+ */
225
+ export function tokenizeCssCustomProperties(text) {
226
+ const tokens = Object.create(null);
227
+ for (const rule of scanCssRules(text)) {
228
+ for (const decl of extractDeclarations(
229
+ text.slice(rule.bodyStart, rule.bodyEnd),
230
+ rule.bodyStart
231
+ )) {
232
+ tokens[decl.name] = decl.rawValue;
233
+ }
234
+ }
235
+ return { tokens, types: Object.create(null) };
236
+ }
237
+
238
+ /**
239
+ * Decision 1 — parse the input as JSON (W3C design-tokens / Style-Dictionary)
240
+ * or, on JSON failure, as raw CSS custom properties. A failure on BOTH is a
241
+ * hard reject. Error messages report structural position only, never a
242
+ * verbatim excerpt of the input content.
243
+ */
244
+ export function parseTokenFile(text) {
245
+ const trimmed = text.trimStart();
246
+ if (trimmed.startsWith('{')) {
247
+ let obj;
248
+ try {
249
+ obj = JSON.parse(text);
250
+ } catch (err) {
251
+ const pos = /position (\d+)/.exec(String(err?.message ?? ''))?.[1];
252
+ throw new ImportTokensError(
253
+ 4,
254
+ `could not parse as JSON (design-tokens/Style-Dictionary)${pos ? ` — malformed at byte offset ${pos}` : ''}`
255
+ );
256
+ }
257
+ if (obj === null || typeof obj !== 'object' || Array.isArray(obj)) {
258
+ throw new ImportTokensError(4, 'could not parse as JSON — expected a top-level object');
259
+ }
260
+ return { format: 'json', root: obj };
261
+ }
262
+ // Raw CSS: parse-failure here just means "no declarations found" (the
263
+ // tokenizer never throws on malformed input — it best-effort extracts what
264
+ // it can scan safely) — an empty result is itself the "could not parse"
265
+ // signal the caller checks for.
266
+ const { tokens } = tokenizeCssCustomProperties(text);
267
+ if (Object.keys(tokens).length === 0) {
268
+ throw new ImportTokensError(
269
+ 4,
270
+ 'could not parse as JSON or as CSS custom properties — no recognized token declarations found'
271
+ );
272
+ }
273
+ return { format: 'css', tokens };
274
+ }
275
+
276
+ // ============================================================================
277
+ // Decision 3 — prototype-pollution-safe, structurally depth-capped flatten of
278
+ // the parsed JSON into {path -> rawValue} + {path -> detectedType}.
279
+ // ============================================================================
280
+
281
+ const BANNED_KEYS = new Set(['__proto__', 'constructor', 'prototype']);
282
+ const STRUCTURE_MAX_DEPTH = 32;
283
+
284
+ function isLeafToken(node) {
285
+ return (
286
+ node &&
287
+ typeof node === 'object' &&
288
+ !Array.isArray(node) &&
289
+ ('$value' in node || 'value' in node)
290
+ );
291
+ }
292
+
293
+ /** Decision 3 — flatten W3C/Style-Dictionary JSON into path->rawValue string maps. */
294
+ export function flattenJsonTokens(root, { maxDepth = STRUCTURE_MAX_DEPTH } = {}) {
295
+ const tokens = Object.create(null);
296
+ const types = Object.create(null);
297
+
298
+ function walk(node, pathParts, depth, inheritedType) {
299
+ if (depth > maxDepth) {
300
+ throw new ImportTokensError(3, 'structure-too-deep (nesting exceeds the 32-level cap)');
301
+ }
302
+ if (node === null || typeof node !== 'object') return;
303
+ if (Array.isArray(node)) {
304
+ for (let i = 0; i < node.length; i += 1) {
305
+ walk(node[i], [...pathParts, String(i)], depth + 1, inheritedType);
306
+ }
307
+ return;
308
+ }
309
+ if (isLeafToken(node)) {
310
+ const raw = '$value' in node ? node.$value : node.value;
311
+ const type =
312
+ typeof node.$type === 'string'
313
+ ? node.$type
314
+ : typeof node.type === 'string'
315
+ ? node.type
316
+ : inheritedType;
317
+ if (typeof raw === 'string') {
318
+ const path = pathParts.join('.');
319
+ tokens[path] = raw;
320
+ if (type) types[path] = type;
321
+ }
322
+ // Non-string leaf $value (composite shadow/typography/border objects)
323
+ // is intentionally left unflattened — Decision 5: unmapped, not guessed.
324
+ return;
325
+ }
326
+ const groupType = typeof node.$type === 'string' ? node.$type : inheritedType;
327
+ for (const key of Object.keys(node)) {
328
+ if (BANNED_KEYS.has(key)) continue;
329
+ if (key.startsWith('$')) continue; // $description/$extensions/etc — metadata, never consumed (Decision 8)
330
+ walk(node[key], [...pathParts, key], depth + 1, groupType);
331
+ }
332
+ }
333
+
334
+ walk(root, [], 0, undefined);
335
+ return { tokens, types };
336
+ }
337
+
338
+ // ============================================================================
339
+ // Decision 4 — value-grammar allowlist (the CSS-injection closure).
340
+ // Every grammar: printable-ASCII-only, no unbounded free-text capture, a
341
+ // literal space (never `\s`), a magnitude bound paired with every numeric
342
+ // grammar. See DDR-172 Decision 4 for the full Round-1/Round-2 history.
343
+ // ============================================================================
344
+
345
+ const ASCII_ONLY_RE = /^[\x20-\x7E]*$/;
346
+
347
+ const HEX_RES = [
348
+ /^#[0-9a-fA-F]{3}$/,
349
+ /^#[0-9a-fA-F]{4}$/,
350
+ /^#[0-9a-fA-F]{6}$/,
351
+ /^#[0-9a-fA-F]{8}$/,
352
+ ];
353
+ const OKLCH_RE = /^oklch\([\d.]+%? [\d.]+ [\d.]+( ?\/ ?[\d.]+%?)?\)$/;
354
+ const RGB_RE = /^rgba?\( ?[\d.]+[ ,]+[\d.]+[ ,]+[\d.]+( ?[,/] ?[\d.]+%?)? ?\)$/;
355
+ const HSL_RE = /^hsla?\( ?[\d.]+ [\d.]+% [\d.]+%( ?\/ ?[\d.]+%?)?\)$/;
356
+
357
+ /** DDR-172 Decision 4's color-function grammar. Exported for reuse by DDR-173 Decision 1/6 (T12's brand extraction). */
358
+ export function isValidColorValue(v) {
359
+ return HEX_RES.some((re) => re.test(v)) || OKLCH_RE.test(v) || RGB_RE.test(v) || HSL_RE.test(v);
360
+ }
361
+
362
+ const DIMENSION_RE = /^-?[\d.]{1,6}(px|rem|em|%)$/;
363
+ const DIMENSION_BOUNDS = { space: 4096, radius: 512, type: 512, lh: 512 };
364
+ function isValidDimension(v, family) {
365
+ if (v === '0') return true;
366
+ if (!DIMENSION_RE.test(v)) return false;
367
+ const n = Math.abs(Number.parseFloat(v));
368
+ const max = DIMENSION_BOUNDS[family] ?? 512;
369
+ return Number.isFinite(n) && n <= max;
370
+ }
371
+
372
+ const DURATION_RE = /^[\d.]{1,6}m?s$/;
373
+ function isValidDuration(v) {
374
+ if (!DURATION_RE.test(v)) return false;
375
+ const m = /^([\d.]{1,6})(m?s)$/.exec(v);
376
+ const n = Number.parseFloat(m[1]);
377
+ const ms = m[2] === 'ms' ? n : n * 1000;
378
+ return Number.isFinite(ms) && ms >= 0 && ms <= 60000;
379
+ }
380
+
381
+ const EASING_KEYWORDS = new Set(['linear', 'ease', 'ease-in', 'ease-out', 'ease-in-out']);
382
+ const CUBIC_BEZIER_RE = /^cubic-bezier\( ?-?[\d.]+ ?, ?-?[\d.]+ ?, ?-?[\d.]+ ?, ?-?[\d.]+ ?\)$/;
383
+ function isValidEasing(v) {
384
+ return EASING_KEYWORDS.has(v) || CUBIC_BEZIER_RE.test(v);
385
+ }
386
+
387
+ const FONT_GENERIC = new Set([
388
+ 'serif',
389
+ 'sans-serif',
390
+ 'monospace',
391
+ 'system-ui',
392
+ 'ui-monospace',
393
+ 'ui-sans-serif',
394
+ 'ui-serif',
395
+ 'cursive',
396
+ 'fantasy',
397
+ ]);
398
+ const FONT_QUOTED_RE = /^"[A-Za-z0-9 _-]{1,64}"$/;
399
+ function isValidFontStack(v) {
400
+ const parts = v.split(',').map((s) => s.trim());
401
+ return parts.length > 0 && parts.every((p) => FONT_GENERIC.has(p) || FONT_QUOTED_RE.test(p));
402
+ }
403
+
404
+ const SHADOW_RGBA = 'rgba?\\( ?[\\d.]+[ ,]+[\\d.]+[ ,]+[\\d.]+( ?[,/] ?[\\d.]+%?)? ?\\)';
405
+ // Each length is either a bare `0` (valid CSS shorthand — no unit needed) or
406
+ // a strict `px` number — matches the real maude DS's own shadow shape
407
+ // (`0 1px 2px rgba(...)`), never a free-text/unitless-non-zero capture.
408
+ const SHADOW_LENGTH = '(?:0|-?[\\d.]{1,5}px)';
409
+ const SHADOW_LAYER = `( ?${SHADOW_LENGTH} ){2,4}${SHADOW_RGBA}`;
410
+ const SHADOW_RE = new RegExp(`^${SHADOW_LAYER}( ?, ?${SHADOW_LAYER})*$`);
411
+ function isValidShadow(v) {
412
+ return SHADOW_RE.test(v);
413
+ }
414
+
415
+ function familyOfVariable(varName) {
416
+ return varName.replace(/^--/, '').split('-')[0];
417
+ }
418
+
419
+ /** Decision 4 — validate `value` against the grammar for `varName`'s family. */
420
+ export function validateTokenValue(varName, value) {
421
+ if (typeof value !== 'string' || !ASCII_ONLY_RE.test(value)) {
422
+ return { ok: false, reason: 'invalid-value-shape (non-ASCII or non-string)' };
423
+ }
424
+ const family = familyOfVariable(varName);
425
+ if (['bg', 'fg', 'border', 'accent', 'status'].includes(family)) {
426
+ return isValidColorValue(value)
427
+ ? { ok: true }
428
+ : { ok: false, reason: 'invalid-value-shape (color)' };
429
+ }
430
+ if (['space', 'radius', 'type', 'lh'].includes(family)) {
431
+ return isValidDimension(value, family)
432
+ ? { ok: true }
433
+ : { ok: false, reason: 'invalid-value-shape (dimension/magnitude)' };
434
+ }
435
+ if (family === 'dur') {
436
+ return isValidDuration(value)
437
+ ? { ok: true }
438
+ : { ok: false, reason: 'invalid-value-shape (duration)' };
439
+ }
440
+ if (family === 'ease') {
441
+ return isValidEasing(value)
442
+ ? { ok: true }
443
+ : { ok: false, reason: 'invalid-value-shape (easing)' };
444
+ }
445
+ if (family === 'font') {
446
+ return isValidFontStack(value)
447
+ ? { ok: true }
448
+ : { ok: false, reason: 'invalid-value-shape (font stack)' };
449
+ }
450
+ if (family === 'shadow') {
451
+ return isValidShadow(value)
452
+ ? { ok: true }
453
+ : { ok: false, reason: 'invalid-value-shape (shadow)' };
454
+ }
455
+ return { ok: false, reason: `ungoverned family: ${family}` };
456
+ }
457
+
458
+ // ============================================================================
459
+ // Decision 5 — heuristic name-to-variable mapping, never silent.
460
+ // ============================================================================
461
+
462
+ export const KNOWN_VARIABLES = new Set([
463
+ '--bg-0',
464
+ '--bg-1',
465
+ '--bg-2',
466
+ '--bg-3',
467
+ '--bg-4',
468
+ '--fg-0',
469
+ '--fg-1',
470
+ '--fg-2',
471
+ '--fg-3',
472
+ '--border-subtle',
473
+ '--border-default',
474
+ '--border-strong',
475
+ '--accent',
476
+ '--accent-hover',
477
+ '--accent-active',
478
+ '--accent-fg',
479
+ '--accent-muted',
480
+ '--accent-tint',
481
+ '--status-success',
482
+ '--status-warn',
483
+ '--status-error',
484
+ '--status-info',
485
+ '--space-0',
486
+ '--space-1',
487
+ '--space-2',
488
+ '--space-3',
489
+ '--space-4',
490
+ '--space-5',
491
+ '--space-6',
492
+ '--space-7',
493
+ '--space-8',
494
+ '--radius-xs',
495
+ '--radius-sm',
496
+ '--radius-md',
497
+ '--radius-lg',
498
+ '--radius-xl',
499
+ '--radius-pill',
500
+ '--type-xs',
501
+ '--type-sm',
502
+ '--type-base',
503
+ '--type-md',
504
+ '--type-lg',
505
+ '--type-xl',
506
+ '--type-2xl',
507
+ '--type-3xl',
508
+ '--lh-xs',
509
+ '--lh-sm',
510
+ '--lh-base',
511
+ '--lh-md',
512
+ '--lh-lg',
513
+ '--lh-xl',
514
+ '--lh-2xl',
515
+ '--lh-3xl',
516
+ '--font-display',
517
+ '--font-body',
518
+ '--font-mono',
519
+ '--dur-flip',
520
+ '--dur-panel',
521
+ '--dur-route',
522
+ '--dur-soft',
523
+ '--ease-out',
524
+ '--ease-in-out',
525
+ '--shadow-sm',
526
+ '--shadow-md',
527
+ '--shadow-lg',
528
+ ]);
529
+
530
+ function normalizeTokenPath(path) {
531
+ return path
532
+ .replace(/([a-z0-9])([A-Z])/g, '$1 $2')
533
+ .replace(/[/_.-]+/g, ' ')
534
+ .toLowerCase()
535
+ .trim()
536
+ .split(/\s+/)
537
+ .filter(Boolean);
538
+ }
539
+
540
+ function has(words, ...any) {
541
+ return any.some((w) => words.includes(w));
542
+ }
543
+
544
+ /** Decision 5 — map a flattened token path to a KNOWN_VARIABLES member, or null (unmapped, never guessed). */
545
+ export function mapTokenNameToVariable(path, detectedType) {
546
+ const words = normalizeTokenPath(path);
547
+ const isColor = detectedType === 'color' || detectedType == null;
548
+ const isStatusWord = has(words, 'success', 'warn', 'warning', 'error', 'danger', 'info');
549
+
550
+ if (isColor && has(words, 'bg', 'background', 'surface') && !isStatusWord) {
551
+ if (has(words, '4', 'pressed')) return '--bg-4';
552
+ if (has(words, '3', 'input')) return '--bg-3';
553
+ if (has(words, '2', 'nested', 'popover', 'inspector')) return '--bg-2';
554
+ if (has(words, '1', 'card', 'panel')) return '--bg-1';
555
+ return '--bg-0';
556
+ }
557
+ if (isColor && has(words, 'fg', 'text', 'foreground') && !has(words, 'accent')) {
558
+ if (has(words, '3', 'disabled')) return '--fg-3';
559
+ if (has(words, '2', 'tertiary', 'muted')) return '--fg-2';
560
+ if (has(words, '1', 'secondary')) return '--fg-1';
561
+ return '--fg-0';
562
+ }
563
+ if (isColor && has(words, 'border') && !has(words, 'radius', 'width', 'corner', 'rounding')) {
564
+ if (has(words, 'strong')) return '--border-strong';
565
+ if (has(words, 'subtle')) return '--border-subtle';
566
+ return '--border-default';
567
+ }
568
+ if (isColor && has(words, 'success')) return '--status-success';
569
+ if (isColor && has(words, 'warn', 'warning')) return '--status-warn';
570
+ if (isColor && has(words, 'error', 'danger')) return '--status-error';
571
+ if (isColor && has(words, 'info')) return '--status-info';
572
+ if (isColor && has(words, 'accent', 'brand', 'primary')) {
573
+ if (has(words, 'hover')) return '--accent-hover';
574
+ if (has(words, 'active', 'pressed')) return '--accent-active';
575
+ if (has(words, 'fg', 'foreground', 'on')) return '--accent-fg';
576
+ if (has(words, 'muted')) return '--accent-muted';
577
+ if (has(words, 'tint')) return '--accent-tint';
578
+ return '--accent';
579
+ }
580
+ if (has(words, 'space', 'spacing', 'gap')) {
581
+ const idx = words.find((w) => /^\d+$/.test(w));
582
+ if (idx == null) return null;
583
+ return `--space-${Math.max(0, Math.min(8, Number(idx)))}`;
584
+ }
585
+ if (has(words, 'radius', 'corner', 'rounding')) {
586
+ if (has(words, 'xs')) return '--radius-xs';
587
+ if (has(words, 'sm', 'small')) return '--radius-sm';
588
+ if (has(words, 'lg', 'large')) return '--radius-lg';
589
+ if (has(words, 'xl')) return '--radius-xl';
590
+ if (has(words, 'pill', 'full', 'round')) return '--radius-pill';
591
+ return '--radius-md';
592
+ }
593
+ if (has(words, 'lineheight', 'leading') || (has(words, 'line') && has(words, 'height'))) {
594
+ for (const s of ['xs', 'sm', 'base', 'md', 'lg', 'xl']) if (has(words, s)) return `--lh-${s}`;
595
+ if (has(words, '2xl')) return '--lh-2xl';
596
+ if (has(words, '3xl')) return '--lh-3xl';
597
+ return null;
598
+ }
599
+ if (has(words, 'fontsize') || (has(words, 'font') && has(words, 'size'))) {
600
+ for (const s of ['xs', 'sm', 'base', 'md', 'lg', 'xl']) if (has(words, s)) return `--type-${s}`;
601
+ if (has(words, '2xl')) return '--type-2xl';
602
+ if (has(words, '3xl')) return '--type-3xl';
603
+ return null;
604
+ }
605
+ if (has(words, 'font', 'typeface', 'fontfamily') || detectedType === 'fontFamily') {
606
+ if (has(words, 'display', 'heading', 'headline')) return '--font-display';
607
+ if (has(words, 'mono', 'code', 'monospace')) return '--font-mono';
608
+ return '--font-body';
609
+ }
610
+ if (has(words, 'duration', 'motion') || detectedType === 'duration') {
611
+ if (has(words, 'flip')) return '--dur-flip';
612
+ if (has(words, 'panel')) return '--dur-panel';
613
+ if (has(words, 'route', 'page')) return '--dur-route';
614
+ return '--dur-soft';
615
+ }
616
+ if (has(words, 'easing', 'ease', 'curve') || detectedType === 'cubicBezier') {
617
+ return has(words, 'inout') || (has(words, 'in') && has(words, 'out'))
618
+ ? '--ease-in-out'
619
+ : '--ease-out';
620
+ }
621
+ if (has(words, 'shadow', 'elevation')) {
622
+ if (has(words, 'sm', 'small')) return '--shadow-sm';
623
+ if (has(words, 'lg', 'large')) return '--shadow-lg';
624
+ return '--shadow-md';
625
+ }
626
+ return null;
627
+ }
628
+
629
+ // ============================================================================
630
+ // Decision 6 — colorspace normalization: convert when confident, report when not.
631
+ // ============================================================================
632
+
633
+ function detectColorFormat(v) {
634
+ if (typeof v !== 'string') return null;
635
+ if (HEX_RES.some((re) => re.test(v))) return 'hex';
636
+ if (RGB_RE.test(v)) return 'rgb';
637
+ if (OKLCH_RE.test(v)) return 'oklch';
638
+ if (HSL_RE.test(v)) return 'hsl';
639
+ return null;
640
+ }
641
+
642
+ /** Decision 6 — normalize a color value into `targetColorSpace`. Returns `{ value }` or `{ skip: reason }`. */
643
+ export function normalizeColorspace(value, targetColorSpace) {
644
+ const inputFormat = detectColorFormat(value);
645
+ if (inputFormat === null) return { skip: 'unparseable-color-value' };
646
+ if (targetColorSpace === 'hex') {
647
+ if (inputFormat === 'hex') return { value };
648
+ if (inputFormat === 'rgb') {
649
+ const rgb = parseColor(value);
650
+ return {
651
+ value: `#${[rgb.r, rgb.g, rgb.b].map((n) => n.toString(16).padStart(2, '0')).join('')}`,
652
+ };
653
+ }
654
+ if (inputFormat === 'oklch') return { value: oklchToHex(parseOklch(value)) };
655
+ return { skip: `unsupported-colorspace-conversion (${inputFormat}->hex)` };
656
+ }
657
+ if (targetColorSpace === 'oklch' || targetColorSpace == null) {
658
+ if (inputFormat === 'oklch') return { value };
659
+ if (inputFormat === 'hex' || inputFormat === 'rgb') {
660
+ const rgb = parseColor(value);
661
+ const o = rgbToOklch(rgb);
662
+ return { value: `oklch(${o.l.toFixed(3)} ${o.c.toFixed(3)} ${o.h.toFixed(1)})` };
663
+ }
664
+ return { skip: `unsupported-colorspace-conversion (${inputFormat}->oklch)` };
665
+ }
666
+ // hsl/lab targets — no converter in this pass (Decision 6, named gap).
667
+ if (inputFormat === targetColorSpace) return { value };
668
+ return { skip: `unsupported-colorspace-conversion (${inputFormat}->${targetColorSpace})` };
669
+ }
670
+
671
+ // ============================================================================
672
+ // Decision 7 — write-path: theme-block-scoped patch, atomic, contained.
673
+ // ============================================================================
674
+
675
+ const SELECTOR_ROOT_RE = /(^|,)\s*:root\s*(,|$)/;
676
+
677
+ function selectorMatchesTheme(selector, rootClass, theme) {
678
+ // Tolerant of whitespace/quote-style variants a hand-edited file might
679
+ // legitimately carry — `[ data-theme = "dark" ]`, `[data-theme='dark']`.
680
+ const escapedClass = rootClass.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
681
+ const escapedTheme = theme.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
682
+ const re = new RegExp(
683
+ `\\.${escapedClass}\\s*\\[\\s*data-theme\\s*=\\s*["']${escapedTheme}["']\\s*\\]`
684
+ );
685
+ return re.test(selector);
686
+ }
687
+
688
+ /**
689
+ * Decision 7 — locate the target theme's top-level (non-`@media`-nested)
690
+ * rule block. Returns `{ bodyStart, bodyEnd }` or `null` (caller MUST hard
691
+ * reject on null — never fall back to `:root`, the first match, or a
692
+ * whole-file scan).
693
+ */
694
+ export function locateThemeBlock(cssText, { rootClass, theme, singleTheme }) {
695
+ const rules = scanCssRules(cssText).filter((r) => !r.nestedInAtRule);
696
+ const themed = rules.find((r) => selectorMatchesTheme(r.selector, rootClass, theme));
697
+ if (themed) return { bodyStart: themed.bodyStart, bodyEnd: themed.bodyEnd };
698
+ if (singleTheme) {
699
+ const rootRule = rules.find((r) => SELECTOR_ROOT_RE.test(r.selector));
700
+ if (rootRule) return { bodyStart: rootRule.bodyStart, bodyEnd: rootRule.bodyEnd };
701
+ }
702
+ return null;
703
+ }
704
+
705
+ /**
706
+ * Decision 7 — patch (or, with `forceInsert`, append) ONE declaration inside
707
+ * the given block span. Never touches text outside `[bodyStart, bodyEnd)`.
708
+ */
709
+ export function patchDeclarationInBlock(
710
+ cssText,
711
+ block,
712
+ varName,
713
+ newValue,
714
+ { forceInsert = false } = {}
715
+ ) {
716
+ const body = cssText.slice(block.bodyStart, block.bodyEnd);
717
+ const decls = extractDeclarations(body, block.bodyStart);
718
+ const existing = decls.find((d) => d.name === varName);
719
+ if (existing) {
720
+ // Replace ONLY the value span — name, colon spacing, and the trailing
721
+ // `;` are left byte-for-byte untouched, exactly the "replaces ONLY that
722
+ // declaration's value" contract (Decision 7). Returns an UPDATED block
723
+ // span (bodyEnd shifted by the length delta) — callers patching multiple
724
+ // variables in the same block in sequence must use the returned block,
725
+ // not the original, or subsequent lookups mis-locate against stale
726
+ // offsets once a value's length changes.
727
+ const css = cssText.slice(0, existing.valueStart) + newValue + cssText.slice(existing.valueEnd);
728
+ const delta = newValue.length - (existing.valueEnd - existing.valueStart);
729
+ return {
730
+ css,
731
+ patched: true,
732
+ block: { bodyStart: block.bodyStart, bodyEnd: block.bodyEnd + delta },
733
+ };
734
+ }
735
+ if (!forceInsert) return { css: cssText, patched: false, block };
736
+ // Force-insert anchor: immediately before the located block's OWN closing
737
+ // `}` — never at file scope (Decision 7).
738
+ const insertion = ` ${varName}: ${newValue};\n`;
739
+ const css = `${cssText.slice(0, block.bodyEnd)}${insertion}${cssText.slice(block.bodyEnd)}`;
740
+ return {
741
+ css,
742
+ patched: true,
743
+ block: { bodyStart: block.bodyStart, bodyEnd: block.bodyEnd + insertion.length },
744
+ };
745
+ }
746
+
747
+ function atomicWrite(filePath, content) {
748
+ const dir = filePath.slice(0, filePath.lastIndexOf(sep));
749
+ const tmp = join(
750
+ dir,
751
+ `.tmp-import-tokens-${createHash('sha256')
752
+ .update(filePath + Date.now())
753
+ .digest('hex')
754
+ .slice(0, 12)}`
755
+ );
756
+ writeFileSync(tmp, content);
757
+ renameSync(tmp, filePath);
758
+ }
759
+
760
+ function containSystemPath(root, designRootRel, tokensCssRel) {
761
+ const systemDir = resolve(root, designRootRel, 'system');
762
+ const target = resolve(root, designRootRel, tokensCssRel);
763
+ if (target !== systemDir && !target.startsWith(systemDir + sep)) {
764
+ throw new ImportTokensError(6, `tokensCssRel escapes system/: ${target}`);
765
+ }
766
+ return target;
767
+ }
768
+
769
+ function containConfigPath(root, designRootRel) {
770
+ const expected = resolve(root, designRootRel, 'config.json');
771
+ return expected;
772
+ }
773
+
774
+ // ============================================================================
775
+ // Decision 8 — new-DS metadata: sink eliminated, not bounded.
776
+ // ============================================================================
777
+
778
+ const DS_NAME_RE = /^[a-z0-9-]{1,64}$/;
779
+
780
+ export function assertValidDsName(name) {
781
+ if (!ASCII_ONLY_RE.test(name) || !DS_NAME_RE.test(name)) {
782
+ throw new ImportTokensError(3, `invalid-ds-name: must match ${DS_NAME_RE} — got "${name}"`);
783
+ }
784
+ }
785
+
786
+ // ============================================================================
787
+ // Orchestration
788
+ // ============================================================================
789
+
790
+ function readConfig(root, designRootRel) {
791
+ const path = containConfigPath(root, designRootRel);
792
+ if (!existsSync(path)) throw new ImportTokensError(4, `config.json not found at ${path}`);
793
+ try {
794
+ return { path, config: JSON.parse(readFileSync(path, 'utf8')) };
795
+ } catch (err) {
796
+ throw new ImportTokensError(4, `could not parse config.json: ${err?.message ?? err}`);
797
+ }
798
+ }
799
+
800
+ function scaffoldMinimalDs(root, designRootRel, dsName) {
801
+ if (!DESIGN_PLUGIN_DIR) {
802
+ throw new ImportTokensError(
803
+ 4,
804
+ 'scaffold template not found — this install layout does not bundle the design plugin templates; patch an existing DS instead, or run /design:setup-ds'
805
+ );
806
+ }
807
+ const tplPath = join(
808
+ DESIGN_PLUGIN_DIR,
809
+ 'templates/design-system-inspiration/core/colors_and_type.css.tpl'
810
+ );
811
+ if (!existsSync(tplPath)) {
812
+ throw new ImportTokensError(4, `scaffold template not found at ${tplPath}`);
813
+ }
814
+ const tpl = readFileSync(tplPath, 'utf8');
815
+ // DDR-043 neutral-skeleton posture — grayscale, system fonts, no shadow/
816
+ // motion personality. Every placeholder gets a literal, inert default; the
817
+ // import pass immediately after this overwrites whatever it successfully
818
+ // mapped.
819
+ const NEUTRAL = {
820
+ project_label: dsName,
821
+ root_class: dsName,
822
+ theme_default: 'dark',
823
+ bg_0: '#0d0d0f',
824
+ bg_1: '#151517',
825
+ bg_2: '#1b1b1e',
826
+ bg_3: '#232326',
827
+ bg_4: '#2b2b2f',
828
+ border_subtle: '#232326',
829
+ border_default: '#333338',
830
+ border_strong: '#48484f',
831
+ fg_0: '#f2f2f3',
832
+ fg_1: '#c7c7cb',
833
+ fg_2: '#9a9aa0',
834
+ fg_3: '#6b6b70',
835
+ accent_strategy_summary: 'single',
836
+ accent_block:
837
+ ' --accent: #6b6bf0;\n --accent-hover: #8080f5;\n --accent-active: #5555d0;\n --accent-fg: #0d0d0f;\n --accent-muted: #3d3d8a;\n --accent-tint: color-mix(in oklab, var(--accent) 16%, transparent);',
838
+ status_success: '#3fb950',
839
+ status_warn: '#d29922',
840
+ status_error: '#f85149',
841
+ status_info: '#58a6ff',
842
+ presence_online: '#3fb950',
843
+ presence_away: '#d29922',
844
+ presence_offline: '#6b6b70',
845
+ shadow_sm: '0 1px 2px rgba(0,0,0,0.4)',
846
+ shadow_md: '0 4px 14px rgba(0,0,0,0.46)',
847
+ shadow_lg: '0 14px 38px rgba(0,0,0,0.56)',
848
+ radius_xs: '3px',
849
+ radius_sm: '5px',
850
+ radius_md: '7px',
851
+ radius_lg: '10px',
852
+ radius_xl: '14px',
853
+ radius_pill: '999px',
854
+ space_0: '0',
855
+ space_1: '2px',
856
+ space_2: '4px',
857
+ space_3: '8px',
858
+ space_4: '12px',
859
+ space_5: '16px',
860
+ space_6: '24px',
861
+ space_7: '32px',
862
+ space_8: '48px',
863
+ font_display: 'system-ui, sans-serif',
864
+ font_body: 'system-ui, sans-serif',
865
+ font_mono: 'ui-monospace, monospace',
866
+ type_xs: '11px',
867
+ lh_xs: '16px',
868
+ type_sm: '12px',
869
+ lh_sm: '18px',
870
+ type_base: '14px',
871
+ lh_base: '20px',
872
+ type_md: '16px',
873
+ lh_md: '24px',
874
+ type_lg: '19px',
875
+ lh_lg: '26px',
876
+ type_xl: '23px',
877
+ lh_xl: '30px',
878
+ type_2xl: '28px',
879
+ lh_2xl: '34px',
880
+ type_3xl: '34px',
881
+ lh_3xl: '40px',
882
+ dur_flip: '140ms',
883
+ dur_panel: '220ms',
884
+ dur_route: '280ms',
885
+ dur_soft: '120ms',
886
+ ease_out_curve: 'cubic-bezier(0.2,0,0,1)',
887
+ ease_in_out_curve: 'cubic-bezier(0.4,0,0.2,1)',
888
+ layout_max_w: 'none',
889
+ layout_gutter: 'var(--space-5)',
890
+ };
891
+ const filled = tpl.replace(/\{\{(\w+)\}\}/g, (m, key) => (key in NEUTRAL ? NEUTRAL[key] : m));
892
+ const dsDir = resolve(root, designRootRel, 'system', dsName);
893
+ mkdirSync(dsDir, { recursive: true });
894
+ const tokensCssRel = `system/${dsName}/colors_and_type.css`;
895
+ atomicWrite(resolve(root, designRootRel, tokensCssRel), filled);
896
+ return { tokensCssRel, rootClass: dsName, themeDefault: 'dark', themes: ['dark'] };
897
+ }
898
+
899
+ function upsertDesignSystemConfig(root, designRootRel, dsName, scaffold, tokenCount) {
900
+ const { path, config } = readConfig(root, designRootRel);
901
+ config.designSystems = Array.isArray(config.designSystems) ? config.designSystems : [];
902
+ const description = `Imported via maude design import-tokens — ${tokenCount} tokens mapped on ${new Date().toISOString().slice(0, 10)}. Refine via /design:setup-ds.`;
903
+ const entry = {
904
+ name: dsName,
905
+ path: `system/${dsName}`,
906
+ description,
907
+ tokensCssRel: scaffold.tokensCssRel,
908
+ rootClass: scaffold.rootClass,
909
+ themeDefault: scaffold.themeDefault,
910
+ themes: scaffold.themes,
911
+ newCanvasDir: 'ui',
912
+ newComponentDir: `system/${dsName}/preview`,
913
+ };
914
+ const idx = config.designSystems.findIndex((d) => d?.name === dsName);
915
+ if (idx >= 0) config.designSystems[idx] = entry;
916
+ else config.designSystems.push(entry);
917
+ if (!config.defaultDesignSystem) config.defaultDesignSystem = dsName;
918
+ atomicWrite(path, `${JSON.stringify(config, null, 2)}\n`);
919
+ }
920
+
921
+ /**
922
+ * Full import: parse → flatten → resolve aliases → map → validate → normalize
923
+ * colorspace → theme-block-scoped patch → mapping report.
924
+ */
925
+ export async function importTokens({
926
+ inputPath,
927
+ root,
928
+ designRootRel = '.design',
929
+ dsName = null,
930
+ newDs = null,
931
+ theme = null,
932
+ forceInsert = false,
933
+ }) {
934
+ if (newDs) assertValidDsName(newDs);
935
+
936
+ const text = readTokenFileCapped(inputPath);
937
+ const parsed = parseTokenFile(text);
938
+ const { tokens: rawTokens, types } =
939
+ parsed.format === 'json'
940
+ ? flattenJsonTokens(parsed.root)
941
+ : { tokens: parsed.tokens, types: Object.create(null) };
942
+
943
+ const { resolved, statuses } = resolveAliases(rawTokens);
944
+
945
+ let targetName = newDs || dsName;
946
+ let scaffold = null;
947
+ let targetColorSpace = 'oklch';
948
+ if (newDs) {
949
+ scaffold = scaffoldMinimalDs(root, designRootRel, newDs);
950
+ targetColorSpace = 'hex'; // matches the neutral scaffold template's own literal values
951
+ } else {
952
+ const { config } = readConfig(root, designRootRel);
953
+ targetName = dsName || config.defaultDesignSystem;
954
+ const dsEntry = (config.designSystems || []).find((d) => d?.name === targetName);
955
+ if (!dsEntry)
956
+ throw new ImportTokensError(4, `design system "${targetName}" not found in config.json`);
957
+ scaffold = {
958
+ tokensCssRel: dsEntry.tokensCssRel,
959
+ rootClass: dsEntry.rootClass,
960
+ themeDefault: dsEntry.themeDefault,
961
+ themes: dsEntry.themes || [dsEntry.themeDefault],
962
+ };
963
+ targetColorSpace = config.colorSpace || 'oklch';
964
+ }
965
+
966
+ const targetTheme = theme || scaffold.themeDefault;
967
+ const singleTheme = (scaffold.themes || []).length <= 1;
968
+ const cssPath = containSystemPath(root, designRootRel, scaffold.tokensCssRel);
969
+ if (!existsSync(cssPath)) throw new ImportTokensError(4, `target CSS file not found: ${cssPath}`);
970
+ let cssText = readFileSync(cssPath, 'utf8');
971
+
972
+ let block = locateThemeBlock(cssText, {
973
+ rootClass: scaffold.rootClass,
974
+ theme: targetTheme,
975
+ singleTheme,
976
+ });
977
+ if (!block) {
978
+ throw new ImportTokensError(
979
+ 3,
980
+ `could not locate the "${targetTheme}" theme block in ${cssPath} — this file's theme structure isn't recognized`
981
+ );
982
+ }
983
+
984
+ const report = {
985
+ generatedAt: new Date().toISOString(),
986
+ inputPath,
987
+ targetDs: targetName,
988
+ targetTheme,
989
+ tokens: [],
990
+ };
991
+ let mappedCount = 0;
992
+
993
+ for (const path of Object.keys(rawTokens)) {
994
+ const entry = { path, detectedType: types[path] || null };
995
+ if (statuses[path]) {
996
+ entry.status = 'unmapped';
997
+ entry.reason = statuses[path];
998
+ report.tokens.push(entry);
999
+ continue;
1000
+ }
1001
+ const value = resolved[path];
1002
+ const varName = mapTokenNameToVariable(path, types[path] || null);
1003
+ if (!varName || !KNOWN_VARIABLES.has(varName)) {
1004
+ entry.status = 'unmapped';
1005
+ entry.reason = 'no-recognized-pattern';
1006
+ report.tokens.push(entry);
1007
+ continue;
1008
+ }
1009
+ entry.targetVariable = varName;
1010
+
1011
+ let finalValue = value;
1012
+ const family = familyOfVariable(varName);
1013
+ if (['bg', 'fg', 'border', 'accent', 'status'].includes(family) && typeof value === 'string') {
1014
+ const norm = normalizeColorspace(value, targetColorSpace);
1015
+ if (norm.skip) {
1016
+ entry.status = 'skipped';
1017
+ entry.reason = norm.skip;
1018
+ report.tokens.push(entry);
1019
+ continue;
1020
+ }
1021
+ finalValue = norm.value;
1022
+ }
1023
+
1024
+ const validation = validateTokenValue(varName, finalValue);
1025
+ if (!validation.ok) {
1026
+ entry.status = 'rejected';
1027
+ entry.reason = validation.reason;
1028
+ report.tokens.push(entry);
1029
+ continue;
1030
+ }
1031
+
1032
+ const patchResult = patchDeclarationInBlock(cssText, block, varName, finalValue, {
1033
+ forceInsert,
1034
+ });
1035
+ if (!patchResult.patched) {
1036
+ entry.status = 'skipped';
1037
+ entry.reason = 'family not present in target file (pass --force-insert to add it)';
1038
+ report.tokens.push(entry);
1039
+ continue;
1040
+ }
1041
+ cssText = patchResult.css;
1042
+ block = patchResult.block; // stale-offset fix: later patches in this same block must use the shifted span
1043
+ entry.status = 'mapped';
1044
+ entry.value = finalValue;
1045
+ mappedCount += 1;
1046
+ report.tokens.push(entry);
1047
+ }
1048
+
1049
+ // Twin-declaration policy — report the untouched theme(s) for every mapped
1050
+ // variable, so the user knows those weren't silently synced (Decision 7).
1051
+ // Snapshot BEFORE looping — report.tokens is pushed into below, and a
1052
+ // live for-of over a growing array would re-visit newly pushed entries.
1053
+ const mappedEntries = report.tokens.filter((e) => e.status === 'mapped');
1054
+ for (const t of scaffold.themes || []) {
1055
+ if (t === targetTheme) continue;
1056
+ for (const entry of mappedEntries) {
1057
+ report.tokens.push({
1058
+ path: entry.path,
1059
+ targetVariable: entry.targetVariable,
1060
+ status: 'skipped',
1061
+ reason: `themed-token-not-patched (theme=${t}, value unchanged)`,
1062
+ });
1063
+ }
1064
+ }
1065
+
1066
+ if (mappedCount > 0) {
1067
+ const backupDir = resolve(root, designRootRel, '_history', '_system');
1068
+ mkdirSync(backupDir, { recursive: true });
1069
+ const ts = Date.now();
1070
+ atomicWrite(join(backupDir, `import-tokens-backup-${ts}.css`), readFileSync(cssPath, 'utf8'));
1071
+ atomicWrite(cssPath, cssText);
1072
+ atomicWrite(
1073
+ join(backupDir, `import-tokens-report-${ts}.json`),
1074
+ `${JSON.stringify(report, null, 2)}\n`
1075
+ );
1076
+ }
1077
+
1078
+ if (newDs) upsertDesignSystemConfig(root, designRootRel, newDs, scaffold, mappedCount);
1079
+
1080
+ return { ...report, mappedCount, cssPath };
1081
+ }
1082
+
1083
+ // ============================================================================
1084
+ // CLI entry
1085
+ // ============================================================================
1086
+
1087
+ function parseArgv(argv) {
1088
+ const out = {
1089
+ file: null,
1090
+ root: null,
1091
+ designRoot: '.design',
1092
+ ds: null,
1093
+ newDs: null,
1094
+ theme: null,
1095
+ forceInsert: false,
1096
+ json: false,
1097
+ };
1098
+ for (let i = 0; i < argv.length; i += 1) {
1099
+ const a = argv[i];
1100
+ switch (a) {
1101
+ case '--root':
1102
+ out.root = argv[++i];
1103
+ break;
1104
+ case '--design-root':
1105
+ out.designRoot = argv[++i];
1106
+ break;
1107
+ case '--ds':
1108
+ out.ds = argv[++i];
1109
+ break;
1110
+ case '--new-ds':
1111
+ out.newDs = argv[++i];
1112
+ break;
1113
+ case '--theme':
1114
+ out.theme = argv[++i];
1115
+ break;
1116
+ case '--force-insert':
1117
+ out.forceInsert = true;
1118
+ break;
1119
+ case '--json':
1120
+ out.json = true;
1121
+ break;
1122
+ case '--help':
1123
+ case '-h':
1124
+ out.help = true;
1125
+ break;
1126
+ default:
1127
+ if (a.startsWith('-')) throw new ImportTokensError(2, `unknown flag ${a}`);
1128
+ if (out.file === null) out.file = a;
1129
+ else throw new ImportTokensError(2, `unexpected extra arg ${a}`);
1130
+ }
1131
+ }
1132
+ return out;
1133
+ }
1134
+
1135
+ const HELP = `import-tokens — token-file import (reached via \`maude design import-tokens\`)
1136
+
1137
+ Usage:
1138
+ maude design import-tokens <token-file> --root <repo> [--design-root .design]
1139
+ [--ds <name> | --new-ds <name>] [--theme <name>]
1140
+ [--force-insert] [--json]
1141
+
1142
+ Parses a W3C design-tokens JSON, Style-Dictionary JSON, or raw CSS custom-
1143
+ properties file, maps recognized tokens onto the DS CSS-variable contract,
1144
+ and patches (or scaffolds, with --new-ds) the target design system. Emits a
1145
+ mapping report accounting for every input token (mapped/unmapped/rejected/
1146
+ skipped) — never silently drops one. See DDR-172 for the full contract.
1147
+
1148
+ Exit: 0 ok · 2 usage · 3 validation/mapping reject · 4 read/parse error ·
1149
+ 5 unsupported format · 6 write/containment error · 1 other.`;
1150
+
1151
+ async function main() {
1152
+ let opts;
1153
+ try {
1154
+ opts = parseArgv(process.argv.slice(2));
1155
+ } catch (err) {
1156
+ process.stderr.write(`import-tokens: ${err.message}\n`);
1157
+ process.exit(err instanceof ImportTokensError ? err.code : 2);
1158
+ }
1159
+ if (opts.help) {
1160
+ process.stdout.write(`${HELP}\n`);
1161
+ return;
1162
+ }
1163
+ if (!opts.file) {
1164
+ process.stderr.write('import-tokens: <token-file> required\n');
1165
+ process.exit(2);
1166
+ }
1167
+ const root = opts.root || process.env.CLAUDE_PROJECT_DIR || process.cwd();
1168
+ try {
1169
+ const result = await importTokens({
1170
+ inputPath: opts.file,
1171
+ root,
1172
+ designRootRel: opts.designRoot,
1173
+ dsName: opts.ds,
1174
+ newDs: opts.newDs,
1175
+ theme: opts.theme,
1176
+ forceInsert: opts.forceInsert,
1177
+ });
1178
+ if (opts.json) {
1179
+ process.stdout.write(`${JSON.stringify(result)}\n`);
1180
+ } else {
1181
+ process.stdout.write(
1182
+ `import-tokens: ${result.mappedCount} mapped, ${result.tokens.length - result.mappedCount} not mapped -> ${result.cssPath}\n`
1183
+ );
1184
+ }
1185
+ } catch (err) {
1186
+ process.stderr.write(`import-tokens: ${err.message}\n`);
1187
+ process.exit(err instanceof ImportTokensError ? err.code : 1);
1188
+ }
1189
+ }
1190
+
1191
+ // import.meta.main is the reliable "am I the entry module?" flag under bun
1192
+ // --compile — the argv/url compare below falsely matches inside a standalone
1193
+ // binary (every bundled module's import.meta.url collapses to the binary's
1194
+ // own path), which would hijack the process before Bun.serve ever runs (the
1195
+ // v0.38.0 "Starting…" hang class of bug). Fall back to the argv compare only
1196
+ // for the plain-`node` CLI path (Node <24 leaves import.meta.main undefined).
1197
+ const runDirectly =
1198
+ import.meta.main ?? (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href);
1199
+ if (runDirectly) {
1200
+ main();
1201
+ }