youiyoux 0.1.2 → 0.1.4

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 (212) hide show
  1. package/README.md +10 -0
  2. package/dist/chunks/{chunk-S7UV34H5.js → chunk-42YJX3W5.js} +1 -3
  3. package/dist/chunks/{chunk-FUB7B3VK.js → chunk-4BBYLTZX.js} +10 -10
  4. package/dist/chunks/chunk-52KOKYY6.js +8710 -0
  5. package/dist/chunks/chunk-FJZFCAL5.js +129 -0
  6. package/dist/chunks/chunk-FVVL5JCY.js +24 -0
  7. package/dist/chunks/{chunk-UHPK62Q5.js → chunk-M5XZWCPS.js} +240 -60
  8. package/dist/chunks/{chunk-ZLRFD5JI.js → chunk-NYDJYMID.js} +263 -16
  9. package/dist/chunks/{src-7ENJHYG4.js → chunk-RDTHHRZN.js} +319 -165
  10. package/dist/chunks/{chunk-E4HNK7VF.js → chunk-WNTS7ZO4.js} +1 -1
  11. package/dist/chunks/chunk-XAEMVEYX.js +188 -0
  12. package/dist/chunks/{chunk-CLNMVQZJ.js → chunk-YFCMTTXS.js} +708 -494
  13. package/dist/chunks/chunk-YJ7WMBTT.js +13761 -0
  14. package/dist/chunks/src-5IP4NGKT.js +115 -0
  15. package/dist/chunks/src-6CJR547Y.js +124 -0
  16. package/dist/chunks/{src-WQWYJ6IQ.js → src-BPETAFLW.js} +175 -76
  17. package/dist/chunks/{src-B742AYFC.js → src-LLV7IY6W.js} +55 -16
  18. package/dist/chunks/src-RHNZELGE.js +306 -0
  19. package/dist/chunks/{src-4KFNL3MY.js → src-SFKFERZ2.js} +18 -8
  20. package/dist/chunks/{src-VEHGMKYV.js → src-VL2ZYO6X.js} +4 -4
  21. package/dist/hub/assets/Badge-CUJUit4d.js +1 -0
  22. package/dist/hub/assets/CandidatePanel-CnbK4S0j.js +1 -0
  23. package/dist/hub/assets/CatalogScreen-B5OVL-AR.js +1 -0
  24. package/dist/hub/assets/ChangesScreen-GwGqJwxN.js +1 -0
  25. package/dist/hub/assets/CheatSheet-BLSOq-xa.js +1 -0
  26. package/dist/hub/assets/CodeBlock-CsiGFgvI.js +1 -0
  27. package/dist/hub/assets/CommandPalette-CAWQfdXJ.js +1 -0
  28. package/dist/hub/assets/CopyButton-DE8drl4P.js +1 -0
  29. package/dist/hub/assets/Disclosure-B1kTgPgk.js +1 -0
  30. package/dist/hub/assets/DiscoverScreen-Tl_Z6mgY.js +2 -0
  31. package/dist/hub/assets/DraftEditor-yIDapXTs.js +5 -0
  32. package/dist/hub/assets/Field-LwrJXoOs.js +1 -0
  33. package/dist/hub/assets/GapsScreen-DXZAlm6v.js +1 -0
  34. package/dist/hub/assets/HomeScreen-CEpiVhOa.js +1 -0
  35. package/dist/hub/assets/InventoryScreen-CEG3Pmw_.js +1 -0
  36. package/dist/hub/assets/KindIcon-BJ6bz-ny.js +1 -0
  37. package/dist/hub/assets/LibraryScreen-IaVnxrX9.js +1 -0
  38. package/dist/hub/assets/LicenceBadge-CLCUMMHE.js +1 -0
  39. package/dist/hub/assets/Opened-DJyf4Hnj.js +1 -0
  40. package/dist/hub/assets/Preview-C069zVMP.js +1 -0
  41. package/dist/hub/assets/RuleDetail-B4g2-TRe.js +3 -0
  42. package/dist/hub/assets/RulesScreen-CAaXIWjO.js +3 -0
  43. package/dist/hub/assets/ScreenImage-DcI7_G3O.js +1 -0
  44. package/dist/hub/assets/ScreensScreen-DwczCjZD.js +1 -0
  45. package/dist/hub/assets/SearchField-DaCpNAj6.js +1 -0
  46. package/dist/hub/assets/Section-BTnvUq_W.js +1 -0
  47. package/dist/hub/assets/Segmented-Dzi1x49S.js +1 -0
  48. package/dist/hub/assets/ServiceDown-Bv-BPXro.js +1 -0
  49. package/dist/hub/assets/SpecimenCapture-DlsjshVE.js +1 -0
  50. package/dist/hub/assets/Specimens-C-lHp7sr.js +2 -0
  51. package/dist/hub/assets/StackScreen-DkmELsqR.js +1 -0
  52. package/dist/hub/assets/StubScreens-Bt7lWLVK.js +1 -0
  53. package/dist/hub/assets/StyleScreen-tq1tbRBo.js +1 -0
  54. package/dist/hub/assets/Swatch-KDz4qxdM.js +1 -0
  55. package/dist/hub/assets/Tabs-1QQDS0I6.js +1 -0
  56. package/dist/hub/assets/TokenDetail-DvCw6GqC.js +1 -0
  57. package/dist/hub/assets/TokenValue-CsPRc2ai.js +1 -0
  58. package/dist/hub/assets/TokensScreen-DoENV8Xy.js +1 -0
  59. package/dist/hub/assets/arrow-right-DcbNDzqs.js +1 -0
  60. package/dist/hub/assets/bot-B3gC9oUR.js +1 -0
  61. package/dist/hub/assets/button-6mvMVtpb.js +1 -0
  62. package/dist/hub/assets/button.demo-DuU0DQbv.js +1 -0
  63. package/dist/hub/assets/card.demo-CufAxMAA.js +1 -0
  64. package/dist/hub/assets/chevron-down-CzN07vMi.js +1 -0
  65. package/dist/hub/assets/circle-alert-DT2LC4yf.js +1 -0
  66. package/dist/hub/assets/circle-check-DkwBMT62.js +1 -0
  67. package/dist/hub/assets/client-D7whEyep.js +1 -0
  68. package/dist/hub/assets/clsx-DB0hHKMi.js +1 -0
  69. package/dist/hub/assets/compass-BkKOZPYe.js +1 -0
  70. package/dist/hub/assets/contrast-BkNHUAPa.js +1 -0
  71. package/dist/hub/assets/createLucideIcon-CGDJWtKO.js +1 -0
  72. package/dist/hub/assets/discover-XjJEViwk.js +1 -0
  73. package/dist/hub/assets/dist-BYQYaPBV.js +1 -0
  74. package/dist/hub/assets/dist-Bk4srtDn.js +41 -0
  75. package/dist/hub/assets/dist-BogNzeK9.js +1 -0
  76. package/dist/hub/assets/dist-D0z7Bvsy.js +1 -0
  77. package/dist/hub/assets/download-BQI-Si9R.js +1 -0
  78. package/dist/hub/assets/engine-CMmt9yEz.js +1 -0
  79. package/dist/hub/assets/esm-NPpg23M8.js +1 -0
  80. package/dist/hub/assets/family-tiles-CyTdy_cP.js +1 -0
  81. package/dist/hub/assets/format-DxAYNTN_.js +1 -0
  82. package/dist/hub/assets/geist-cyrillic-ext-wght-normal-DjL33-gN.woff2 +0 -0
  83. package/dist/hub/assets/geist-cyrillic-wght-normal-BEAKL7Jp.woff2 +0 -0
  84. package/dist/hub/assets/geist-latin-core-vkBVTr4s.woff2 +0 -0
  85. package/dist/hub/assets/geist-latin-ext-wght-normal-DC-KSUi6.woff2 +0 -0
  86. package/dist/hub/assets/geist-latin-wght-normal-BgDaEnEv.woff2 +0 -0
  87. package/dist/hub/assets/geist-mono-cyrillic-ext-wght-normal-X_5orZeX.woff2 +0 -0
  88. package/dist/hub/assets/geist-mono-cyrillic-wght-normal-DiZS0aHC.woff2 +0 -0
  89. package/dist/hub/assets/geist-mono-latin-core-DccGGzmg.woff2 +0 -0
  90. package/dist/hub/assets/geist-mono-latin-ext-wght-normal-Bwz-egvJ.woff2 +0 -0
  91. package/dist/hub/assets/geist-mono-latin-wght-normal-XN7g48iV.woff2 +0 -0
  92. package/dist/hub/assets/geist-mono-symbols2-wght-normal-CO5SzqOn.woff2 +0 -0
  93. package/dist/hub/assets/geist-mono-vietnamese-wght-normal-DadHysG0.woff2 +0 -0
  94. package/dist/hub/assets/geist-vietnamese-wght-normal-6IgcOCM7.woff2 +0 -0
  95. package/dist/hub/assets/git-pull-request-arrow-RqUUIRCU.js +1 -0
  96. package/dist/hub/assets/input.demo-238gqWpz.js +1 -0
  97. package/dist/hub/assets/inventory-BI8QBdar.js +1 -0
  98. package/dist/hub/assets/inventory-source-lZYzcA8O.js +1 -0
  99. package/dist/hub/assets/jsx-runtime-D6zNwnaE.js +9 -0
  100. package/dist/hub/assets/layers-DfzdK_wG.js +1 -0
  101. package/dist/hub/assets/layout-grid-xFY9L9Oc.js +1 -0
  102. package/dist/hub/assets/library-W5R8GwvG.js +1 -0
  103. package/dist/hub/assets/main-7snyIdoi.css +2 -0
  104. package/dist/hub/assets/main-B9CIkk5m.js +2 -0
  105. package/dist/hub/assets/minus-BDifFhOo.js +1 -0
  106. package/dist/hub/assets/monitor-X_lELkcU.js +1 -0
  107. package/dist/hub/assets/mouse-pointer-click-CVJ6hWg8.js +1 -0
  108. package/dist/hub/assets/paintbrush-DOkEgSTO.js +1 -0
  109. package/dist/hub/assets/plural-BbGQPQKA.js +1 -0
  110. package/dist/hub/assets/preview-BA8CPsKb.css +2 -0
  111. package/dist/hub/assets/preview-DUm13vr1.js +3 -0
  112. package/dist/hub/assets/protocol-caP6umNL.js +1 -0
  113. package/dist/hub/assets/react-DB-4Zxce.js +1 -0
  114. package/dist/hub/assets/shield-check-0ka0kQct.js +1 -0
  115. package/dist/hub/assets/trash-B7aEfJps.js +1 -0
  116. package/dist/hub/assets/triangle-alert-Dkdi9eDR.js +1 -0
  117. package/dist/hub/assets/try-live-zX3CSd3P.js +1 -0
  118. package/dist/hub/assets/utils-BoaXfs8y.js +1 -0
  119. package/dist/hub/assets/values-DzxR_TlI.js +1 -0
  120. package/dist/hub/assets/worker-Bh-wSp42.js +1 -0
  121. package/dist/hub/assets/workflow-jpV8wh-U.js +1 -0
  122. package/dist/hub/assets/x-MLbFVa4v.js +1 -0
  123. package/dist/hub/favicon.svg +1 -0
  124. package/dist/hub/index.html +35 -0
  125. package/dist/hub/kit/candidates.json +1 -0
  126. package/dist/hub/kit/chunks/chunk-2NYFUOIV.js +14 -0
  127. package/dist/hub/kit/chunks/chunk-DCANE7XH.js +1 -0
  128. package/dist/hub/kit/chunks/chunk-FTGHCY4I.js +14 -0
  129. package/dist/hub/kit/chunks/chunk-IBF3UTJF.js +33 -0
  130. package/dist/hub/kit/chunks/chunk-LLXY4VSH.js +1 -0
  131. package/dist/hub/kit/chunks/chunk-PUPZMW4P.js +14 -0
  132. package/dist/hub/kit/chunks/chunk-PWLNZQET.js +1 -0
  133. package/dist/hub/kit/chunks/chunk-ULXWVOKJ.js +1 -0
  134. package/dist/hub/kit/chunks/chunk-WM4BGGMK.js +1 -0
  135. package/dist/hub/kit/chunks/chunk-ZQUNRK72.js +1 -0
  136. package/dist/hub/kit/class-variance-authority.js +1 -0
  137. package/dist/hub/kit/clsx.js +1 -0
  138. package/dist/hub/kit/fonts/geist-cyrillic-ext-wght-normal.woff2 +0 -0
  139. package/dist/hub/kit/fonts/geist-cyrillic-wght-normal.woff2 +0 -0
  140. package/dist/hub/kit/fonts/geist-latin-ext-wght-normal.woff2 +0 -0
  141. package/dist/hub/kit/fonts/geist-latin-wght-normal.woff2 +0 -0
  142. package/dist/hub/kit/fonts/geist-vietnamese-wght-normal.woff2 +0 -0
  143. package/dist/hub/kit/frame.css.src +73 -0
  144. package/dist/hub/kit/frame.js +2 -0
  145. package/dist/hub/kit/lucide-react.js +1878 -0
  146. package/dist/hub/kit/preview.html +13 -0
  147. package/dist/hub/kit/radix-ui-react-dialog.js +41 -0
  148. package/dist/hub/kit/radix-ui-react-label.js +1 -0
  149. package/dist/hub/kit/radix-ui-react-separator.js +1 -0
  150. package/dist/hub/kit/radix-ui-react-slot.js +1 -0
  151. package/dist/hub/kit/radix-ui-react-switch.js +1 -0
  152. package/dist/hub/kit/radix-ui-react-tabs.js +1 -0
  153. package/dist/hub/kit/react-dom-client.js +1 -0
  154. package/dist/hub/kit/react-dom.js +1 -0
  155. package/dist/hub/kit/react-jsx-runtime.js +1 -0
  156. package/dist/hub/kit/react.js +1 -0
  157. package/dist/hub/kit/tailwind-merge.js +1 -0
  158. package/dist/hub/kit/tailwindcss/index.css +950 -0
  159. package/dist/hub/kit/tailwindcss/preflight.css +398 -0
  160. package/dist/hub/kit/tailwindcss/theme.css +510 -0
  161. package/dist/hub/kit/tailwindcss/utilities.css +1 -0
  162. package/dist/hub-demo/frame/button.demo.js +1 -0
  163. package/dist/hub-demo/frame/card.demo.js +1 -0
  164. package/dist/hub-demo/frame/demos.js +6 -0
  165. package/dist/hub-demo/frame/frame.css +770 -0
  166. package/dist/hub-demo/frame/input.demo.js +1 -0
  167. package/dist/hub-demo/preview/components/button.demo.tsx +47 -0
  168. package/dist/hub-demo/preview/components/button.tsx +31 -0
  169. package/dist/hub-demo/preview/components/card.demo.tsx +26 -0
  170. package/dist/hub-demo/preview/components/card.tsx +20 -0
  171. package/dist/hub-demo/preview/components/input.demo.tsx +39 -0
  172. package/dist/hub-demo/preview/components/input.tsx +19 -0
  173. package/dist/hub-demo/preview/components/utils.ts +5 -0
  174. package/dist/hub-demo/preview/system.css +161 -0
  175. package/dist/hub-demo/public/data/demo-app.usage.bedd9c82fe.json +1 -0
  176. package/dist/hub-demo/public/data/demo.catalog.a605de2281.json +1 -0
  177. package/dist/hub-demo/public/data/gaps.129a93538d.json +1 -0
  178. package/dist/hub-demo/public/data/inventory.165a4f6408.json +1 -0
  179. package/dist/hub-demo/public/data/inventory.console.badge.ad0238784e.json +1 -0
  180. package/dist/hub-demo/public/data/inventory.console.button.3249daff35.json +1 -0
  181. package/dist/hub-demo/public/data/inventory.console.card.9ea88708ac.json +1 -0
  182. package/dist/hub-demo/public/data/inventory.console.dialog.8b6574d93a.json +1 -0
  183. package/dist/hub-demo/public/data/inventory.console.input.bdffb756a7.json +1 -0
  184. package/dist/hub-demo/public/data/inventory.console.link-as-button.1ce7faa1f3.json +1 -0
  185. package/dist/hub-demo/public/data/inventory.console.tabs.813e694c44.json +1 -0
  186. package/dist/hub-demo/public/data/inventory.extras.e5d06c6ff4.json +1 -0
  187. package/dist/hub-demo/public/data/inventory.site.badge.047a68f644.json +1 -0
  188. package/dist/hub-demo/public/data/inventory.site.button.720938384e.json +1 -0
  189. package/dist/hub-demo/public/data/inventory.site.card.d8df5f6873.json +1 -0
  190. package/dist/hub-demo/public/data/inventory.site.link-as-button.9c1f8a55bf.json +1 -0
  191. package/dist/hub-demo/public/data/inventory.site.tabs.59bd0200b9.json +1 -0
  192. package/dist/hub-demo/public/data/manifest.json +39 -0
  193. package/dist/hub-demo/public/data/specimens.console.dark.046471907d.css +786 -0
  194. package/dist/hub-demo/public/data/specimens.console.light.aaffa2d213.css +786 -0
  195. package/dist/hub-demo/public/data/specimens.site.dark.94a6706908.css +92 -0
  196. package/dist/hub-demo/public/data/specimens.site.light.3ddbc8d28d.css +92 -0
  197. package/dist/hub-demo/public/previews/button.dark.webp +0 -0
  198. package/dist/hub-demo/public/previews/button.light.webp +0 -0
  199. package/dist/hub-demo/public/previews/card.dark.webp +0 -0
  200. package/dist/hub-demo/public/previews/card.light.webp +0 -0
  201. package/dist/hub-demo/public/previews/input.dark.webp +0 -0
  202. package/dist/hub-demo/public/previews/input.light.webp +0 -0
  203. package/dist/hub-previews.js +173 -0
  204. package/dist/hub-server.js +6329 -0
  205. package/dist/hub-snapshot.js +573 -0
  206. package/dist/schema/youi.gaps.schema.json +235 -0
  207. package/dist/schema/youi.request.schema.json +229 -0
  208. package/dist/tailwind-worker.js +3 -3
  209. package/dist/youi.js +10 -8
  210. package/dist/youiyoux.js +10 -8
  211. package/package.json +4 -4
  212. package/dist/chunks/chunk-3ZHGONNA.js +0 -21797
@@ -1,35 +1,4 @@
1
- import { createRequire as __youiRequire } from "node:module"; const require = __youiRequire(import.meta.url);
2
- import {
3
- EXPORT_TYPES,
4
- LIBRARY_BY_ID,
5
- buildReport,
6
- detectRepo,
7
- elementTypes,
8
- libraryOfPackage,
9
- packageOf,
10
- playwrightCli
11
- } from "./chunk-FUB7B3VK.js";
12
- import {
13
- candidateSource,
14
- checkpointSqlite,
15
- cleanData,
16
- discoverCacheDir,
17
- discoverIndexPath,
18
- expandAll,
19
- indexHome,
20
- ingestCatalog,
21
- inventoryRun,
22
- linkCandidates,
23
- openSqlite,
24
- projectGraph,
25
- readCatalog,
26
- searchCatalog,
27
- selectSources,
28
- sharedStore,
29
- showFromCatalog,
30
- storeLocation,
31
- syncIndex
32
- } from "./chunk-UHPK62Q5.js";
1
+ import { createRequire as __youiRequire } from "node:module"; import { dirname as __youiDir } from "node:path"; import { fileURLToPath as __youiPath } from "node:url"; const require = __youiRequire(import.meta.url); const __youiDirname = __youiDir(__youiPath(import.meta.url));
33
2
  import {
34
3
  BASELINE_FILE,
35
4
  DEFAULT_EXCLUDE,
@@ -40,13 +9,13 @@ import {
40
9
  adoptionRequest,
41
10
  buildInventory,
42
11
  buildTheme,
43
- buildTokens,
12
+ captureScreens,
13
+ captureText,
44
14
  checkCapabilities,
45
- compareParity,
15
+ chosenSession,
46
16
  compareToBaseline,
47
17
  componentCard,
48
18
  createBaseline,
49
- cssValue,
50
19
  describeIdentity,
51
20
  evaluateRule,
52
21
  failedChecks,
@@ -60,26 +29,68 @@ import {
60
29
  loadProject,
61
30
  nearDuplicates,
62
31
  offLibraryImports,
63
- parseCssColor,
32
+ openLoginBrowser,
64
33
  parseModule,
65
34
  planChange,
66
35
  ratchetBaseline,
67
36
  readStylesheet,
37
+ recordSession,
68
38
  renderShots,
69
39
  ruleCapability,
70
40
  runMigration,
71
41
  scanProject,
72
42
  screenBom,
73
43
  searchLibrary,
44
+ sessionFile,
74
45
  shotsNext,
75
46
  shotsText,
76
47
  substituteVars,
77
48
  vendoredPath,
78
49
  whereUsed
79
- } from "./chunk-3ZHGONNA.js";
50
+ } from "./chunk-52KOKYY6.js";
80
51
  import {
81
52
  within
82
- } from "./chunk-S7UV34H5.js";
53
+ } from "./chunk-42YJX3W5.js";
54
+ import {
55
+ EXPORT_TYPES,
56
+ LIBRARY_BY_ID,
57
+ buildReport,
58
+ detectRepo,
59
+ elementTypes,
60
+ libraryOfPackage,
61
+ packageOf,
62
+ playwrightCli
63
+ } from "./chunk-4BBYLTZX.js";
64
+ import {
65
+ MissingIndexError,
66
+ buildGaps,
67
+ candidateSource,
68
+ checkpointSqlite,
69
+ cleanData,
70
+ declaredPackages,
71
+ discoverCacheDir,
72
+ discoverIndexPath,
73
+ expandAll,
74
+ indexHome,
75
+ ingestCatalog,
76
+ inventoryRun,
77
+ linkCandidates,
78
+ openSqlite,
79
+ projectGraph,
80
+ readCatalog,
81
+ searchCatalog,
82
+ selectSources,
83
+ sharedStore,
84
+ showFromCatalog,
85
+ storeLocation,
86
+ syncIndex
87
+ } from "./chunk-M5XZWCPS.js";
88
+ import {
89
+ buildTokens,
90
+ compareParity,
91
+ cssValue,
92
+ parseCssColor
93
+ } from "./chunk-YJ7WMBTT.js";
83
94
  import {
84
95
  Baseline,
85
96
  DefinitionError,
@@ -91,6 +102,7 @@ import {
91
102
  OutputConfig,
92
103
  ProjectConfig,
93
104
  ScanReport,
105
+ copyleft,
94
106
  executionLabel,
95
107
  findFiles,
96
108
  getSystem,
@@ -103,48 +115,27 @@ import {
103
115
  relPath,
104
116
  renderJsonSchemas,
105
117
  require_dist,
118
+ ruleSlug,
106
119
  safeId,
107
120
  sha256,
108
121
  shortHash,
122
+ toPosix,
109
123
  writeFileAtomic
110
- } from "./chunk-ZLRFD5JI.js";
124
+ } from "./chunk-NYDJYMID.js";
111
125
  import {
112
126
  __toESM
113
- } from "./chunk-E4HNK7VF.js";
114
-
115
- // src/node20.js
116
- if (!Map.groupBy) {
117
- Map.groupBy = (items, key2) => {
118
- const groups = /* @__PURE__ */ new Map();
119
- let i = 0;
120
- for (const item of items) {
121
- const k = key2(item, i++);
122
- const list = groups.get(k);
123
- if (list) list.push(item);
124
- else groups.set(k, [item]);
125
- }
126
- return groups;
127
- };
128
- }
129
- if (!Object.groupBy) Object.groupBy = (items, key2) => Object.fromEntries(Map.groupBy(items, key2));
130
- if (!Promise.withResolvers) {
131
- Promise.withResolvers = () => {
132
- let resolve13, reject;
133
- const promise = new Promise((yes, no) => (resolve13 = yes, reject = no));
134
- return { promise, resolve: resolve13, reject };
135
- };
136
- }
127
+ } from "./chunk-WNTS7ZO4.js";
137
128
 
138
129
  // ../cli/src/main.ts
139
130
  var import_yaml3 = __toESM(require_dist(), 1);
140
- import { execFile as execFile6 } from "node:child_process";
141
- import { existsSync as existsSync4 } from "node:fs";
131
+ import { execFile as execFile5 } from "node:child_process";
132
+ import { existsSync as existsSync5 } from "node:fs";
142
133
  import { appendFile as appendFile2, readFile as readFile8 } from "node:fs/promises";
143
- import { join as join22, relative as relative9, resolve as resolve12 } from "node:path";
144
- import { parseArgs, promisify as promisify6 } from "node:util";
134
+ import { basename as basename3, dirname as dirname11, join as join22, relative as relative8, resolve as resolve12 } from "node:path";
135
+ import { parseArgs, promisify as promisify5 } from "node:util";
145
136
 
146
137
  // ../cli/src/check.ts
147
- import { dirname as dirname3, join as join7, relative as relative4, resolve as resolve2, sep as sep2 } from "node:path";
138
+ import { dirname as dirname3, join as join7, resolve as resolve2 } from "node:path";
148
139
 
149
140
  // ../cli/src/check-diff.ts
150
141
  import { execFile } from "node:child_process";
@@ -158,7 +149,7 @@ import { join as join2, relative, resolve } from "node:path";
158
149
 
159
150
  // ../cli/src/command.ts
160
151
  var COMMAND = "youiyoux";
161
- var SUBCOMMANDS = ["show", "where", "bom", "check", "baseline", "rules", "plan", "migrate", "init", "sync", "scan", "watch", "hooks", "agents", "mcp", "ui", "library", "discover", "index", "report", "render", "detect", "build", "parity", "schema", "inventory", "help"];
152
+ var SUBCOMMANDS = ["show", "where", "bom", "check", "baseline", "rules", "plan", "migrate", "init", "sync", "scan", "watch", "hooks", "agents", "mcp", "ui", "library", "discover", "index", "report", "render", "screens", "requests", "detect", "build", "parity", "schema", "inventory", "gaps", "help"];
162
153
 
163
154
  // ../cli/src/agents.ts
164
155
  var import_yaml = __toESM(require_dist(), 1);
@@ -188,10 +179,9 @@ function viaNpx(text, root) {
188
179
  return text.replace(BARE, `${portableYoui(root).run} `);
189
180
  }
190
181
  function diffInstruction(root) {
191
- const { run: run8, setup } = portableYoui(root);
192
- return `- ${setup} After UI edits, run \`${run8} check --diff\` (\`--json\` for data): it lists what your change adds that near-duplicates an existing component, hardcodes a value a token holds, or imports an element from a library other than the project's choice, each with its fix. Apply each fix.`;
182
+ const { run: run7, setup } = portableYoui(root);
183
+ return `- ${setup} After UI edits, run \`${run7} check --diff\` (\`--json\` for data): it lists what your change adds that near-duplicates an existing component, hardcodes a value a token holds, or imports an element from a library other than the project's choice, each with its fix. Apply each fix.`;
193
184
  }
194
- var DIFF_INSTRUCTION = diffInstruction();
195
185
  function repoAgentsBlock(root) {
196
186
  return viaNpx([BLOCK_BEGIN, REPO_BLOCK_HEADING, "", diffInstruction(root), "- Run `youiyoux init --design <dir>` to connect design-system rules; repository inference works without it.", BLOCK_END].join("\n"), root);
197
187
  }
@@ -231,6 +221,7 @@ function agentsBlock(project2) {
231
221
  `- Never edit files marked \`${GENERATED_MARK}\` (\`${config.outputs.dir}/*\`, ${config.agents.designMd}, the youiyoux skill). Change the design language and run \`youiyoux sync\`.`,
232
222
  `- Owned component files in \`${config.componentsDir}\` may be edited; \`youiyoux sync\` 3-way merges upstream changes into them and reports drift.`,
233
223
  "- After changing a component, call `ui_render` with its file (or run `youiyoux render <file>`) and look at the images: light and dark, desktop and phone, its accessibility issues and the diff against the base. Fix what it reports.",
224
+ "- People pin comments and change requests to components on captured screens. Before UI work, call `requests_list` (or run `youiyoux requests`); after making a requested change, capture its screen again (`screens_capture` with its route, or `youiyoux screens capture --base <app> --route <route>`) and call `request_addressed` with what you changed (file:line). Ask with `request_reply` when one is unclear. A person closes requests, never an agent.",
234
225
  "- Before you finish, run `youiyoux check`; it must report 0 errors.",
235
226
  diffInstruction(project2.root),
236
227
  "- To change the System itself (a token, a prop, a rule), run `youiyoux plan` (or `system_plan`) and propose it. Agents never approve, publish or merge design changes.",
@@ -298,7 +289,7 @@ function frontmatter(project2, build) {
298
289
  colors,
299
290
  typography,
300
291
  rounded: dims("radius"),
301
- spacing: Object.fromEntries(Object.entries(dims("space"))),
292
+ spacing: dims("space"),
302
293
  components
303
294
  };
304
295
  }
@@ -342,7 +333,7 @@ function skillMd(project2, uncontracted = []) {
342
333
  const from = importHint(config);
343
334
  const description = `Build and fix UI with the ${system.config.name} design language (YouIyouX): System components, tokens, rules, where-used lookups, change proposals and Library adoption requests. Use for any UI, styling, colour or component work in this project.`;
344
335
  const components = system.components.map(({ contract }) => `- **${contract.name}**: ${contract.summary}${enumSummary(contract) ? ` (${enumSummary(contract)})` : ""}`);
345
- const fixes = enforced(system.rules).map((r) => `| \`${r.id.split("/rule/")[1]}\` | ${r.severity} | ${r.agent} |`);
336
+ const fixes = enforced(system.rules).map((r) => `| \`${ruleSlug(r.id)}\` | ${r.severity} | ${r.agent} |`);
346
337
  return viaNpx([
347
338
  "---",
348
339
  "name: youiyoux",
@@ -408,22 +399,15 @@ function astGrepRules(project2) {
408
399
  # Portable subset: youiyoux check also resolves imports; these run with plain \`ast-grep scan -r <file>\`.
409
400
  `;
410
401
  for (const rule2 of enforced(project2.system.rules)) {
411
- if (rule2.check.engine === "ast-grep") {
412
- out.push({ file: `${rule2.id.split("/rule/")[1]}.yml`, content: header + (0, import_yaml.stringify)({ id: rule2.id, language: rule2.check.language, severity: rule2.severity === "error" ? "error" : "warning", message: rule2.title, note: rule2.agent, rule: rule2.check.rule }) });
413
- }
402
+ const entry = (language, rest) => ({ file: `${ruleSlug(rule2.id)}.yml`, content: header + (0, import_yaml.stringify)({ id: rule2.id, language, severity: rule2.severity === "error" ? "error" : "warning", message: rule2.title, note: rule2.agent, ...rest }) });
403
+ if (rule2.check.engine === "ast-grep") out.push(entry(rule2.check.language, { rule: rule2.check.rule }));
414
404
  if (rule2.check.engine === "youi" && rule2.check.id === "native-element") {
415
405
  const tags = Object.keys(rule2.check.options.elements ?? {});
416
406
  const regex = `^(${tags.join("|")})$`;
417
- out.push({
418
- file: `${rule2.id.split("/rule/")[1]}.yml`,
419
- content: header + (0, import_yaml.stringify)({ id: rule2.id, language: "tsx", severity: rule2.severity === "error" ? "error" : "warning", message: rule2.title, note: rule2.agent, ignores: [`${project2.config.componentsDir}/**`], rule: { any: [{ kind: "jsx_opening_element", has: { field: "name", regex } }, { kind: "jsx_self_closing_element", has: { field: "name", regex } }] } })
420
- });
407
+ out.push(entry("tsx", { ignores: [`${project2.config.componentsDir}/**`], rule: { any: [{ kind: "jsx_opening_element", has: { field: "name", regex } }, { kind: "jsx_self_closing_element", has: { field: "name", regex } }] } }));
421
408
  }
422
409
  if (rule2.check.engine === "youi" && rule2.check.id === "raw-color") {
423
- out.push({
424
- file: `${rule2.id.split("/rule/")[1]}.yml`,
425
- content: header + (0, import_yaml.stringify)({ id: rule2.id, language: "tsx", severity: rule2.severity === "error" ? "error" : "warning", message: rule2.title, note: rule2.agent, rule: { kind: "string_fragment", regex: "(\\[#[0-9a-fA-F]{3,8}\\]|^#[0-9a-fA-F]{3,8}$|rgba?\\()", inside: { kind: "jsx_attribute", stopBy: "end" } } })
426
- });
410
+ out.push(entry("tsx", { rule: { kind: "string_fragment", regex: "(\\[#[0-9a-fA-F]{3,8}\\]|^#[0-9a-fA-F]{3,8}$|rgba?\\()", inside: { kind: "jsx_attribute", stopBy: "end" } } }));
427
411
  }
428
412
  }
429
413
  return out;
@@ -474,8 +458,7 @@ async function initProject(options) {
474
458
  }
475
459
  }
476
460
  notes.push(`owned components: ${componentsDir}`);
477
- const tsconfig = await exists(join2(root, "tsconfig.json")) ? "tsconfig.json" : "tsconfig.json";
478
- const alias = await aliasRoot(root, tsconfig);
461
+ const alias = await aliasRoot(root);
479
462
  const importFrom = alias !== void 0 && componentsDir.startsWith(alias) ? `@/${componentsDir.slice(alias.length)}` : void 0;
480
463
  if (importFrom) notes.push(`imports use the @/ alias: ${importFrom}`);
481
464
  const include = (await Promise.all(
@@ -623,7 +606,6 @@ async function stagedChange(project2) {
623
606
  for (const file of added.keys()) sources[file] = await git(root, ["show", `:${prefix}${file}`]);
624
607
  return { base: "HEAD (staged changes)", commit: head, added, sources };
625
608
  }
626
- var slugOf = (rule2) => rule2.slice(rule2.indexOf("/rule/") + 6);
627
609
  var DRIFT_SEVERITY = { "near-duplicate": "warn", "hardcoded-value": "error", "off-library": "error" };
628
610
  async function importOf(project2) {
629
611
  const alias = await aliasRoot(project2.root, project2.config.tsconfig);
@@ -654,9 +636,9 @@ async function chosenLibraries(project2, added, notes) {
654
636
  notes.push(`imports were compared only with the System's components (${error instanceof Error ? error.message : String(error)})`);
655
637
  return chosen;
656
638
  }
657
- const outside = hits.filter((hit) => (hit.where ?? []).some((where) => {
658
- const at = where.lastIndexOf(":");
659
- return !isAdded(added, where.slice(0, at), Number(where.slice(at + 1)));
639
+ const outside = hits.filter((hit) => (hit.where ?? []).some((where2) => {
640
+ const at = where2.lastIndexOf(":");
641
+ return !isAdded(added, where2.slice(0, at), Number(where2.slice(at + 1)));
660
642
  }));
661
643
  const byType = /* @__PURE__ */ new Map();
662
644
  for (const hit of outside) {
@@ -726,11 +708,11 @@ async function analyseChange(project2, scan, change, options) {
726
708
  const merged = findings2.findIndex((f, i) => f.kind !== "rule" && samePlace(f, violation) && sameText(drift[i]?.text ?? "", violation.text));
727
709
  if (merged >= 0) {
728
710
  const into = findings2[merged];
729
- findings2[merged] = { ...into, rule: slugOf(violation.rule), severity: violation.severity === "error" ? "error" : into.severity };
711
+ findings2[merged] = { ...into, rule: ruleSlug(violation.rule), severity: violation.severity === "error" ? "error" : into.severity };
730
712
  continue;
731
713
  }
732
- const fix = violation.suggestion ?? rules.get(violation.rule)?.agent ?? `see \`youiyoux rules\` for ${slugOf(violation.rule)}`;
733
- findings2.push({ kind: "rule", file: violation.file, line: violation.line, column: violation.column, message: violation.message, fix, severity: violation.severity, rule: slugOf(violation.rule) });
714
+ const fix = violation.suggestion ?? rules.get(violation.rule)?.agent ?? `see \`youiyoux rules\` for ${ruleSlug(violation.rule)}`;
715
+ findings2.push({ kind: "rule", file: violation.file, line: violation.line, column: violation.column, message: violation.message, fix, severity: violation.severity, rule: ruleSlug(violation.rule) });
734
716
  }
735
717
  findings2.sort((a, b) => a.file.localeCompare(b.file) || a.line - b.line || a.column - b.column);
736
718
  return { base: change.base, commit: change.commit, files, findings: findings2, notes, incomplete };
@@ -747,7 +729,6 @@ import { promisify as promisify2 } from "node:util";
747
729
  import { dirname, join as join4 } from "node:path";
748
730
  import { createRequire } from "node:module";
749
731
  var TOOL_ROOT = join4(dirname(createRequire(import.meta.url).resolve("youiyoux/package.json")), "dist");
750
- var CLI_BIN = join4(TOOL_ROOT, "packages/cli/bin/youi.js");
751
732
 
752
733
  // ../cli/src/tool-command.ts
753
734
  import { join as join5, relative as relative2, isAbsolute as isAbsolute2 } from "node:path";
@@ -1035,7 +1016,7 @@ async function verifyWiring(project2) {
1035
1016
  }
1036
1017
 
1037
1018
  // ../cli/src/check.ts
1038
- var toProjectPath = (project2, file) => relative4(project2.root, resolve2(file)).split(sep2).join("/");
1019
+ var toProjectPath = (project2, file) => relPath(project2.root, resolve2(file));
1039
1020
  function baselinePath(project2, configPath) {
1040
1021
  return join7(dirname3(resolve2(configPath ?? project2.configFile ?? join7(project2.root, PROJECT_FILE))), BASELINE_FILE);
1041
1022
  }
@@ -1055,7 +1036,7 @@ async function runCheck(project2, options = {}) {
1055
1036
  const wantedRule = (id) => !options.rule || id.endsWith(`/rule/${options.rule}`);
1056
1037
  const violations = report.violations.filter((v) => wantedRule(v.rule) && (!options.quiet || v.severity === "error"));
1057
1038
  const notRun = failedChecks(report.checks).filter((c) => wantedRule(c.rule));
1058
- const lock = options.lock === false || diff && options.lock !== true ? null : await lockIssues(project2);
1039
+ const lock = options.lock === false || diff ? null : await lockIssues(project2);
1059
1040
  const recorded = options.baseline ? await readBaseline(options.baseline) : null;
1060
1041
  if (options.baseline && !recorded) throw new Error(`no baseline at ${options.baseline}; record one with \`youiyoux baseline\``);
1061
1042
  const baseline = recorded && options.baseline ? { file: options.baseline, recorded: recorded.identity, comparison: compareToBaseline(recorded, violations, scope ?? void 0) } : null;
@@ -1073,7 +1054,7 @@ var diffExitCodeOf = (result, strict) => strict && ((result.diff?.findings.lengt
1073
1054
  async function writeBaseline(project2, file, options = {}) {
1074
1055
  const scan = await scanProject(project2, options.cache ? { cache: options.cache } : {});
1075
1056
  if (scan.report.status !== "complete" && !options.force) {
1076
- const gaps = [...scan.report.coverage.failed.map((f) => f.file), ...failedChecks(scan.report.checks).map((c) => `rule ${c.rule.split("/rule/")[1]}`)];
1057
+ const gaps = [...scan.report.coverage.failed.map((f) => f.file), ...failedChecks(scan.report.checks).map((c) => `rule ${ruleSlug(c.rule)}`)];
1077
1058
  throw new Error(`the scan is partial (${gaps.join(", ")}): findings there would later look new. Fix what could not be analysed or run, or record anyway with --force`);
1078
1059
  }
1079
1060
  const existing = await readBaseline(file);
@@ -1087,23 +1068,208 @@ async function writeBaseline(project2, file, options = {}) {
1087
1068
  }
1088
1069
 
1089
1070
  // ../cli/src/check-repo.ts
1071
+ import { readFile as readFile3 } from "node:fs/promises";
1072
+ import { dirname as dirname5, join as join9, resolve as resolve4 } from "node:path";
1073
+
1074
+ // ../cli/src/hooks-install.ts
1090
1075
  import { execFile as execFile3 } from "node:child_process";
1091
- import { access as access2, readFile as readFile3 } from "node:fs/promises";
1092
- import { dirname as dirname4, join as join8, resolve as resolve3 } from "node:path";
1076
+ import { chmod, mkdir as mkdir2, realpath as realpath2 } from "node:fs/promises";
1077
+ import { dirname as dirname4, join as join8, relative as relative4, resolve as resolve3 } from "node:path";
1093
1078
  import { promisify as promisify3 } from "node:util";
1094
1079
  var run3 = promisify3(execFile3);
1095
- var exists2 = (file) => access2(file).then(() => true, () => false);
1080
+ var CLAUDE_MARKER = "hooks run claude";
1081
+ var BLOCK_START = "# >>> youi pre-commit >>>";
1082
+ var BLOCK_END2 = "# <<< youi pre-commit <<<";
1083
+ var WORKFLOW = ".github/workflows/youi-check.yml";
1084
+ var WORKFLOW_HEADER = "# Generated by `youiyoux hooks install --ci`.";
1085
+ var LEGACY_WORKFLOW_HEADER = WORKFLOW_HEADER.replace("youiyoux", "youi");
1086
+ var managedWorkflow = (text) => text.startsWith(WORKFLOW_HEADER) || text.startsWith(LEGACY_WORKFLOW_HEADER);
1087
+ var DIFF_STEP = "name: youiyoux check --diff";
1088
+ var CLAUDE_MATCHER = "Edit|Write|MultiEdit";
1089
+ var CLAUDE_TIMEOUT_S = 60;
1090
+ var shellQuote = (arg) => /^[\w@%+=:,./-]+$/.test(arg) ? arg : `'${arg.replace(/'/g, `'\\''`)}'`;
1091
+ async function gitOut(cwd, args) {
1092
+ return (await run3("git", [...args], { cwd })).stdout.trim();
1093
+ }
1094
+ async function repoRootOf(dir) {
1095
+ return gitOut(dir, ["rev-parse", "--show-toplevel"]).catch(() => dir);
1096
+ }
1097
+ function hookCommand(toolRoot, kind, target, extra = []) {
1098
+ const server = toolCommand(toolRoot);
1099
+ const args = [server.command, ...server.args, "hooks", "run", kind];
1100
+ for (const key2 of ["project", "config", "design", "root"]) {
1101
+ const value = target[key2];
1102
+ if (value) args.push(`--${key2}`, resolve3(value));
1103
+ }
1104
+ return [...args, ...extra].map(shellQuote).join(" ");
1105
+ }
1106
+ async function write(file, content, before, guarantee, mode) {
1107
+ if (before === content) return { file, action: "unchanged", guarantee };
1108
+ await mkdir2(dirname4(file), { recursive: true });
1109
+ await writeFileAtomic(file, content);
1110
+ if (mode !== void 0) await chmod(file, mode);
1111
+ return { file, action: before === null ? "created" : "updated", guarantee };
1112
+ }
1113
+ var isYouiEntry = (entry) => (entry.hooks ?? []).some((h) => typeof h.command === "string" && h.command.includes(CLAUDE_MARKER));
1114
+ async function installClaudeHook(repoRoot, command2) {
1115
+ const file = join8(repoRoot, ".claude/settings.local.json");
1116
+ const before = await readTextOrNull(file);
1117
+ let settings;
1118
+ try {
1119
+ settings = before === null ? {} : JSON.parse(before);
1120
+ } catch {
1121
+ throw new Error(`${file} is not valid JSON; fix it first (youi will not overwrite it)`);
1122
+ }
1123
+ const hooks = settings.hooks ?? {};
1124
+ const existing = hooks.PostToolUse ?? [];
1125
+ const entry = { matcher: CLAUDE_MATCHER, hooks: [{ type: "command", command: command2, timeout: CLAUDE_TIMEOUT_S }] };
1126
+ const postToolUse = existing.some(isYouiEntry) ? existing.map((e) => isYouiEntry(e) ? entry : e) : [...existing, entry];
1127
+ const next2 = { ...settings, hooks: { ...hooks, PostToolUse: postToolUse } };
1128
+ return write(file, `${JSON.stringify(next2, null, 2)}
1129
+ `, before, "advisory: after each Claude Code Edit/Write/MultiEdit, that file's new violations go back to Claude; never blocks; edits made any other way are not seen");
1130
+ }
1131
+ function withBlock(script, block) {
1132
+ if (script === null) return `#!/bin/sh
1133
+ ${block}
1134
+ `;
1135
+ const start = script.indexOf(BLOCK_START);
1136
+ const end = script.indexOf(BLOCK_END2);
1137
+ if (start >= 0 && end > start) return `${script.slice(0, start)}${block}${script.slice(end + BLOCK_END2.length)}`;
1138
+ return `${script.endsWith("\n") ? script : `${script}
1139
+ `}
1140
+ ${block}
1141
+ `;
1142
+ }
1143
+ async function installPreCommit(projectRoot, command2, strict) {
1144
+ const hooksDir = resolve3(projectRoot, await gitOut(projectRoot, ["rev-parse", "--git-path", "hooks"]));
1145
+ const file = join8(hooksDir, "pre-commit");
1146
+ const before = await readTextOrNull(file);
1147
+ const shebang = before?.split("\n", 1)[0] ?? "";
1148
+ if (before !== null && shebang.startsWith("#!") && !/\b(?:sh|bash|zsh|dash)\b/.test(shebang)) {
1149
+ throw new Error(`${file} is not a shell script (${shebang}); add this line to it yourself: ${command2}`);
1150
+ }
1151
+ if (before !== null && /^\s*exec\s/m.test(before) && !before.includes(BLOCK_START)) {
1152
+ throw new Error(`${file} hands off with exec, so an appended check would never run; add this line before the exec yourself: ${command2}`);
1153
+ }
1154
+ const what = strict ? "Blocks commits with new design-system errors" : "Advisory, never blocks";
1155
+ const block = `${BLOCK_START}
1156
+ # ${what}: checks the staged content of staged files only; CI is authoritative.
1157
+ ${command2} || exit $?
1158
+ ${BLOCK_END2}`;
1159
+ const guarantee = strict ? "blocks commits with new errors in staged files (any agent or person that commits; --no-verify skips it)" : "advisory at commit time, staged files only (any agent or person that commits; not every edit)";
1160
+ return write(file, withBlock(before, block), before, guarantee, 493);
1161
+ }
1162
+ async function installRepoAgentInstructions(root) {
1163
+ const file = join8(root, "AGENTS.md");
1164
+ const before = await readTextOrNull(file);
1165
+ const guarantee = "agents run youiyoux check --diff after UI edits; pre-commit also checks staged edits";
1166
+ const ruled = before !== null && before.indexOf(RULE_END) > before.indexOf(RULE_BEGIN) && before.includes(RULE_BEGIN);
1167
+ return write(file, ruled ? before : upsertBlock(before, repoAgentsBlock(root)), before, guarantee);
1168
+ }
1169
+ function codexFallback(projectRoot, agentsFile) {
1170
+ return {
1171
+ file: join8(projectRoot, agentsFile),
1172
+ action: "skipped",
1173
+ guarantee: "no post-edit hook installed (Codex documents none): Codex follows AGENTS.md, where `youiyoux sync` tells it to run `youiyoux check --diff` after UI edits, and the git pre-commit hook checks what it commits"
1174
+ };
1175
+ }
1176
+ function githubSlug(url) {
1177
+ const match = /github\.com[:/]([^/\s]+)\/([^/\s]+?)(?:\.git)?\/?$/.exec(url.trim());
1178
+ return match ? `${match[1]}/${match[2]}` : null;
1179
+ }
1180
+ async function repoAt(root) {
1181
+ const slug = githubSlug(await gitOut(root, ["remote", "get-url", "origin"]).catch(() => ""));
1182
+ if (!slug) throw new Error(`${root} has no GitHub origin remote; the workflow needs one to check it out`);
1183
+ return { slug, commit: await gitOut(root, ["rev-parse", "HEAD"]) };
1184
+ }
1185
+ var posix2 = (path) => toPosix(path) || ".";
1186
+ var inside = (root, path) => !relative4(root, path).startsWith("..");
1187
+ function workflowText(input) {
1188
+ const dir = input.projectDir || ".";
1189
+ const target = ["--project", dir, "--design", ".youi-ci/design", ...input.configPath ? ["--config", input.configPath, "--root", dir] : []];
1190
+ const checkArgs = ["--baseline", "--format", "github", ...target].map(shellQuote).join(" ");
1191
+ const diffArgs2 = ["--strict", "--format", "github", ...target].map(shellQuote).join(" ");
1192
+ const diffStep = input.diff ? ` - ${DIFF_STEP} (drift this pull request adds)
1193
+ if: \${{ !cancelled() && github.event_name == 'pull_request' }}
1194
+ env:
1195
+ YOUI_BASE: \${{ github.base_ref }}
1196
+ run: node .youi-ci/tool/packages/cli/bin/youi.js check --diff "origin/$YOUI_BASE" ${diffArgs2}
1197
+ ` : "";
1198
+ return `${WORKFLOW_HEADER}
1199
+ # The authoritative design-system check: it fails on violations the baseline does not
1200
+ # accept, annotates them on the PR's lines, and states the rules version (design commit
1201
+ # and definitions digest) in the job summary. Raise the rules version by changing the
1202
+ # design ref below; YOUI_REPO_TOKEN must be able to read both repositories.
1203
+ name: youiyoux check
1204
+ on:
1205
+ pull_request:
1206
+ push:
1207
+ branches: [main]
1208
+ permissions:
1209
+ contents: read
1210
+ jobs:
1211
+ youi-check:
1212
+ runs-on: ubuntu-latest
1213
+ timeout-minutes: 10
1214
+ steps:
1215
+ - uses: actions/checkout@v4${input.diff ? "\n with:\n fetch-depth: 0" : ""}
1216
+ - name: Check out youi
1217
+ uses: actions/checkout@v4
1218
+ with:
1219
+ repository: ${input.tool.slug}
1220
+ ref: ${input.tool.commit}
1221
+ path: .youi-ci/tool
1222
+ token: \${{ secrets.YOUI_REPO_TOKEN }}
1223
+ - name: Check out the design rules (the rules version)
1224
+ uses: actions/checkout@v4
1225
+ with:
1226
+ repository: ${input.design.slug}
1227
+ ref: ${input.design.commit}
1228
+ path: .youi-ci/design
1229
+ token: \${{ secrets.YOUI_REPO_TOKEN }}
1230
+ - uses: pnpm/action-setup@v4
1231
+ with:
1232
+ version: 10.13.1
1233
+ - uses: actions/setup-node@v4
1234
+ with:
1235
+ node-version: 24
1236
+ - run: pnpm install --frozen-lockfile --ignore-scripts
1237
+ working-directory: .youi-ci/tool
1238
+ - name: youiyoux check
1239
+ run: node .youi-ci/tool/packages/cli/bin/youi.js check ${checkArgs}
1240
+ ${diffStep}`;
1241
+ }
1242
+ async function installWorkflow(project2, toolRoot, target, force, options = {}) {
1243
+ const top = await gitOut(project2.root, ["rev-parse", "--show-toplevel"]);
1244
+ const published = ephemeralTool(toolRoot);
1245
+ const [tool, design] = await Promise.all([published ? Promise.resolve({ slug: "npm/youiyoux", commit: "published" }) : repoAt(toolRoot), repoAt(project2.repo.root)]);
1246
+ const configFile = resolve3(target.config ?? project2.configFile ?? join8(project2.root, PROJECT_FILE));
1247
+ const configPath = !target.config ? null : inside(project2.repo.root, configFile) ? `.youi-ci/design/${posix2(relative4(project2.repo.root, configFile))}` : inside(top, configFile) ? posix2(relative4(top, configFile)) : null;
1248
+ if (target.config && !configPath) throw new Error(`${configFile} is in neither this repo nor the design repo, so CI cannot read it`);
1249
+ const file = join8(top, WORKFLOW);
1250
+ const before = await readTextOrNull(file);
1251
+ if (before !== null && !managedWorkflow(before) && !force) throw new Error(`${file} exists and was not written by youi; pass --force to replace it`);
1252
+ const diff = options.diff === true || before !== null && managedWorkflow(before) && (before.includes(DIFF_STEP) || before.includes(DIFF_STEP.replace("youiyoux", "youi")));
1253
+ const projectDir = posix2(relative4(top, await realpath2(project2.root).catch(() => project2.root)));
1254
+ let text = workflowText({ tool, design, projectDir, configPath, diff });
1255
+ if (published) {
1256
+ text = text.replace(/ - name: Check out youi\n[\s\S]*?(?= - name: Check out the design)/, "").replace(/ - run: pnpm install --frozen-lockfile --ignore-scripts\n working-directory: .youi-ci\/tool\n/, "").replaceAll("node .youi-ci/tool/packages/cli/bin/youi.js", "npx -y youiyoux");
1257
+ }
1258
+ return write(file, text, before, `authoritative: ${published ? "runs the published youiyoux package; pinned to" : `pinned to ${tool.slug}@${tool.commit.slice(0, 7)} and`} rules ${design.slug}@${design.commit.slice(0, 7)}${diff ? "; pull requests also fail on drift they add (youiyoux check --diff --strict)" : ""}`);
1259
+ }
1260
+
1261
+ // ../cli/src/check-repo.ts
1096
1262
  async function loadCheckProject(root, options = {}, inferTop = false) {
1097
- let dir = resolve3(root);
1263
+ let dir = resolve4(root);
1098
1264
  if (options.configPath) return loadProject(dir, options);
1099
1265
  if (inferTop) {
1100
- for (let ancestor = dir; ; ancestor = dirname4(ancestor)) {
1101
- if (await exists2(join8(ancestor, PROJECT_FILE))) return loadProject(ancestor, options);
1102
- if (dirname4(ancestor) === ancestor) break;
1266
+ for (let ancestor = dir; ; ancestor = dirname5(ancestor)) {
1267
+ if (await exists(join9(ancestor, PROJECT_FILE))) return loadProject(ancestor, options);
1268
+ if (dirname5(ancestor) === ancestor) break;
1103
1269
  }
1104
- dir = await run3("git", ["rev-parse", "--show-toplevel"], { cwd: dir }).then((r) => r.stdout.trim(), () => dir);
1270
+ dir = await repoRootOf(dir);
1105
1271
  }
1106
- if (await exists2(join8(dir, PROJECT_FILE))) return loadProject(dir, options);
1272
+ if (await exists(join9(dir, PROJECT_FILE))) return loadProject(dir, options);
1107
1273
  return inferProject(options.productRoot ?? dir);
1108
1274
  }
1109
1275
  async function inferProject(dir, loaded) {
@@ -1139,7 +1305,7 @@ async function runRepoCheck(project2, change) {
1139
1305
  const changedUi = (file) => change.added.has(file) && (/\.(tsx|jsx|astro|css)$/.test(file) || modules.some((m) => m.file === file && m.jsx.length > 0));
1140
1306
  for (const file of /* @__PURE__ */ new Set([...target.files, ...change.added.keys()])) {
1141
1307
  try {
1142
- const module = await parseModule(file, change.sources?.[file] ?? await readFile3(join8(project2.root, file), "utf8"));
1308
+ const module = await parseModule(file, change.sources?.[file] ?? await readFile3(join9(project2.root, file), "utf8"));
1143
1309
  if (module) {
1144
1310
  modules.push(module);
1145
1311
  if (changedUi(file) && module.parseErrors > 0) incomplete.push(`${file}: fix the syntax errors, then run youiyoux check --diff again`);
@@ -1164,7 +1330,7 @@ async function runRepoCheck(project2, change) {
1164
1330
  } catch (error) {
1165
1331
  incomplete.push(`Components could not be compared (${String(error)}); run youiyoux inventory --project ${project2.root} to diagnose`);
1166
1332
  }
1167
- const findings2 = drift.map((d) => ({ ...d, severity: d.kind === "near-duplicate" ? "warn" : "error" }));
1333
+ const findings2 = drift.map((d) => ({ ...d, severity: DRIFT_SEVERITY[d.kind] }));
1168
1334
  findings2.sort((a, b) => a.file.localeCompare(b.file) || a.line - b.line || a.column - b.column);
1169
1335
  return { identity: null, unmatched: [], failed: [], notRun: [], incomplete: incomplete.length > 0, diff: { base: change.base, commit: change.commit, files: [...change.added.keys()].sort(), findings: findings2, notes, incomplete } };
1170
1336
  }
@@ -1243,25 +1409,26 @@ function renderScan(report, outFile) {
1243
1409
  coverageLine(report),
1244
1410
  ...report.coverage.failed.map((f) => style.warn(` ! ${f.file}: ${f.reason}`)),
1245
1411
  ...ruleProblemLines(report),
1246
- ...outFile ? [style.dim(`usage map \u2192 ${outFile}`)] : []
1412
+ style.dim(`usage map \u2192 ${outFile}`)
1247
1413
  ];
1248
1414
  return lines.join("\n");
1249
1415
  }
1250
- function renderWhere(where, report, all) {
1251
- const max = Math.max(...where.byRoute.map((r) => r.sites), 1);
1252
- const routeWidth = Math.max(...where.byRoute.map((r) => r.route.length), 6);
1253
- const unknown = where.unknown.length > 0 ? style.warn(plural(where.unknown.length, "unknown")) : style.dim("0 unknown");
1254
- const lookalikes = where.lookalikes.reduce((n, l) => n + l.sites, 0);
1416
+ function renderWhere(where2, report) {
1417
+ const all = where2.sites;
1418
+ const max = Math.max(...where2.byRoute.map((r) => r.sites), 1);
1419
+ const routeWidth = Math.max(...where2.byRoute.map((r) => r.route.length), 6);
1420
+ const unknown = where2.unknown.length > 0 ? style.warn(plural(where2.unknown.length, "unknown")) : style.dim("0 unknown");
1421
+ const lookalikes = where2.lookalikes.reduce((n, l) => n + l.sites, 0);
1255
1422
  const lines = [
1256
- `${style.accent(where.name)} ${style.dim(`${where.element} \xB7 v${where.version} \xB7 ${where.status}`)}`,
1257
- style.dim(where.import),
1423
+ `${style.accent(where2.name)} ${style.dim(`${where2.element} \xB7 v${where2.version} \xB7 ${where2.status}`)}`,
1424
+ style.dim(where2.import),
1258
1425
  "",
1259
- `${style.bold(plural(where.total, "site"))} in ${plural(where.files, "file")} across ${plural(where.screens, "screen")} ${unknown} ${lookalikes > 0 ? style.warn(`${plural(lookalikes, "lookalike")} not counted`) : style.dim("0 lookalikes")}`,
1426
+ `${style.bold(plural(where2.total, "site"))} in ${plural(where2.files, "file")} across ${plural(where2.screens, "screen")} ${unknown} ${lookalikes > 0 ? style.warn(`${plural(lookalikes, "lookalike")} not counted`) : style.dim("0 lookalikes")}`,
1260
1427
  rule(),
1261
- ...where.byRoute.map((r) => `${padEnd(r.route, routeWidth)} ${padStart(String(r.sites), 4)} ${bar(r.sites, max)}${r.route === "*" ? ` ${SHELL_NOTE}` : ""}`),
1262
- ...where.unrouted > 0 ? [`${padEnd(style.warn("(unrouted)"), routeWidth)} ${padStart(String(where.unrouted), 4)} ${style.dim("no screen imports these files")}`] : [],
1428
+ ...where2.byRoute.map((r) => `${padEnd(r.route, routeWidth)} ${padStart(String(r.sites), 4)} ${bar(r.sites, max)}${r.route === "*" ? ` ${SHELL_NOTE}` : ""}`),
1429
+ ...where2.unrouted > 0 ? [`${padEnd(style.warn("(unrouted)"), routeWidth)} ${padStart(String(where2.unrouted), 4)} ${style.dim("no screen imports these files")}`] : [],
1263
1430
  "",
1264
- ...Object.entries(where.props).map(([prop, counts]) => `${style.dim(padEnd(prop, 9))}${Object.entries(counts).map(([value, n]) => `${value.startsWith("(") ? style.dim(value) : value} ${style.dim(String(n))}`).join(style.dim(" \xB7 "))}`),
1431
+ ...Object.entries(where2.props).map(([prop, counts]) => `${style.dim(padEnd(prop, 9))}${Object.entries(counts).map(([value, n]) => `${value.startsWith("(") ? style.dim(value) : value} ${style.dim(String(n))}`).join(style.dim(" \xB7 "))}`),
1265
1432
  ""
1266
1433
  ];
1267
1434
  const byRoute = Map.groupBy(all, (s) => s.routes[0] ?? "(unrouted)");
@@ -1269,16 +1436,14 @@ function renderWhere(where, report, all) {
1269
1436
  lines.push(style.bold(route));
1270
1437
  for (const site of sites) lines.push(` ${padEnd(style.dim(site.at), 46)} ${padEnd(site.use, 14)} ${site.props}`);
1271
1438
  }
1272
- if (where.nextOffset !== null) lines.push(style.dim(` \u2026 ${where.total - all.length} more (--all, or --route <route>)`));
1273
- for (const l of where.lookalikes) lines.push("", style.warn(`lookalike: ${plural(l.sites, "site")} render ${l.resolved}, a different ${where.name}`), style.dim(" not counted above \xB7 youiyoux check --rule components.canonical-import lists them"));
1274
- for (const u of where.unknown) lines.push(style.warn(`unknown: ${u.at} <${u.name}> (${u.reason})`));
1439
+ if (where2.nextOffset !== null) lines.push(style.dim(` \u2026 ${where2.total - all.length} more (--all, or --route <route>)`));
1440
+ for (const l of where2.lookalikes) lines.push("", style.warn(`lookalike: ${plural(l.sites, "site")} render ${l.resolved}, a different ${where2.name}`), style.dim(" not counted above \xB7 youiyoux check --rule components.canonical-import lists them"));
1441
+ for (const u of where2.unknown) lines.push(style.warn(`unknown: ${u.at} <${u.name}> (${u.reason})`));
1275
1442
  lines.push("", coverageLine(report));
1276
1443
  return lines.join("\n");
1277
1444
  }
1278
- function ruleSlug(id) {
1279
- return id.slice(id.indexOf("/rule/") + 6);
1280
- }
1281
- function renderCheck(violations, lockIssues2, report, suppressed) {
1445
+ function renderCheck(violations, lockIssues2, report) {
1446
+ const { suppressed } = report;
1282
1447
  const lines = [];
1283
1448
  for (const [file, list] of Map.groupBy(violations, (v) => v.file)) {
1284
1449
  lines.push(style.bold(file));
@@ -1298,7 +1463,7 @@ function renderCheck(violations, lockIssues2, report, suppressed) {
1298
1463
  lines.push(
1299
1464
  "",
1300
1465
  errors > 0 ? style.error(`\u2716 ${plural(errors, "error")} \xB7 ${plural(warnings, "warning")} in ${plural(files, "file")}`) : warnings > 0 ? style.warn(`\u2713 0 errors \xB7 ${plural(warnings, "warning")} in ${plural(files, "file")}`) : style.ok("\u2713 0 errors \xB7 0 warnings"),
1301
- ...[...byRule].sort((a, b) => b[1].length - a[1].length).map(([slug2, list]) => style.dim(` ${padStart(String(list.length), 4)} ${slug2}`)),
1466
+ ...[...byRule].sort((a, b) => b[1].length - a[1].length).map(([slug, list]) => style.dim(` ${padStart(String(list.length), 4)} ${slug}`)),
1302
1467
  ...suppressed > 0 ? [style.dim(` ${padStart(String(suppressed), 4)} suppressed by youi-ignore`)] : [],
1303
1468
  ...lockIssues2 === null ? [style.dim(" no youi.lock.json yet: generated-file and drift checks start after `youiyoux sync`")] : [],
1304
1469
  coverageLine(report),
@@ -1425,14 +1590,13 @@ function renderShots2(report) {
1425
1590
  }
1426
1591
 
1427
1592
  // ../cli/src/check-render.ts
1428
- var slug = (rule2) => rule2.slice(rule2.indexOf("/rule/") + 6);
1429
1593
  function actionable(result) {
1430
1594
  if (!result.baseline) return result.violations;
1431
1595
  const { comparison } = result.baseline;
1432
1596
  return [...comparison.new, ...comparison.regressed].sort((a, b) => a.file.localeCompare(b.file) || a.line - b.line);
1433
1597
  }
1434
1598
  function renderCheckText(result) {
1435
- const lines = [renderCheck(actionable(result), result.lock, result.scan.report, result.scan.report.suppressed)];
1599
+ const lines = [renderCheck(actionable(result), result.lock, result.scan.report)];
1436
1600
  if (result.baseline) {
1437
1601
  const { comparison, file, recorded } = result.baseline;
1438
1602
  const gone = comparison.fixed.reduce((n, f) => n + f.gone, 0);
@@ -1460,13 +1624,13 @@ function githubAnnotations(result, prefix) {
1460
1624
  file: `${prefix}${v.file}`,
1461
1625
  line: v.line,
1462
1626
  col: v.column,
1463
- title: `youi ${slug(v.rule)}`
1627
+ title: `youi ${ruleSlug(v.rule)}`
1464
1628
  })
1465
1629
  );
1466
1630
  for (const issue of result.lock ?? []) lines.push(command(issue.severity === "error" ? "error" : "warning", issue.message, { file: `${prefix}${issue.file}`, title: "youi setup" }));
1467
1631
  for (const file of result.unmatched) lines.push(command("error", `${file} was not checked: not in this project's include globs`, { title: "youi incomplete" }));
1468
1632
  for (const failure of result.failed) lines.push(command("error", `could not be fully analysed (${failure.reason}); a clean result here is not a pass`, { file: `${prefix}${failure.file}`, title: "youi incomplete" }));
1469
- for (const run8 of result.notRun) lines.push(command("error", `rule ${slug(run8.rule)} did not run (${run8.reason ?? run8.state}); its findings are unknown`, { title: "youi incomplete" }));
1633
+ for (const run7 of result.notRun) lines.push(command("error", `rule ${ruleSlug(run7.rule)} did not run (${run7.reason ?? run7.state}); its findings are unknown`, { title: "youi incomplete" }));
1470
1634
  return lines;
1471
1635
  }
1472
1636
  var revision = (state) => state.commit ? `\`${state.commit}\`${state.dirty ? " (dirty)" : ""}` : "not a git checkout";
@@ -1488,7 +1652,7 @@ function diffGaps(result) {
1488
1652
  return [
1489
1653
  ...result.unmatched.map((f) => `${f} was not checked: it is not in this project's include globs`),
1490
1654
  ...result.failed.map((f) => `${f.file} could not be fully analysed (${f.reason})`),
1491
- ...result.notRun.map((r) => `rule ${slug(r.rule)} did not run (${r.reason ?? r.state})`),
1655
+ ...result.notRun.map((r) => `rule ${ruleSlug(r.rule)} did not run (${r.reason ?? r.state})`),
1492
1656
  ...result.diff?.incomplete ?? []
1493
1657
  ];
1494
1658
  }
@@ -1542,33 +1706,27 @@ function githubDiffSummary(result) {
1542
1706
 
1543
1707
  // ../cli/src/hooks.ts
1544
1708
  import { execFile as execFile4 } from "node:child_process";
1545
- import { access as access3, realpath as realpath2 } from "node:fs/promises";
1546
- import { dirname as dirname5, join as join9, resolve as resolve4 } from "node:path";
1709
+ import { realpath as realpath3 } from "node:fs/promises";
1710
+ import { dirname as dirname6, join as join10, resolve as resolve5 } from "node:path";
1547
1711
  import { promisify as promisify4 } from "node:util";
1548
1712
  var run4 = promisify4(execFile4);
1549
1713
  var MAX_LISTED = 20;
1550
1714
  var COMPONENT_FILE = /\.(?:tsx|jsx)$/;
1551
1715
  var quoted = (path) => /^[\w./-]+$/.test(path) ? path : `'${path.replace(/'/g, "'\\''")}'`;
1552
1716
  var SILENT = { stdout: "", exitCode: 0 };
1553
- async function exists3(file) {
1554
- return access3(file).then(
1555
- () => true,
1556
- () => false
1557
- );
1558
- }
1559
1717
  async function load(target, near) {
1560
1718
  return loadCheckProject(target.project ?? near, { ...target.config ? { configPath: target.config } : {}, ...target.design ? { designRoot: target.design } : {}, ...target.root ? { productRoot: target.root } : {} }, !target.project);
1561
1719
  }
1562
1720
  async function baselineFor(project2, target) {
1563
1721
  const file = baselinePath(project2, target.config);
1564
- return await exists3(file) ? file : null;
1722
+ return await exists(file) ? file : null;
1565
1723
  }
1566
1724
  function findingLines(result, root) {
1567
1725
  const findings2 = result.diff?.findings ?? [];
1568
1726
  const lines = findings2.slice(0, MAX_LISTED).map((f) => `- ${diffFindingLine(f)}`);
1569
1727
  if (findings2.length > MAX_LISTED) {
1570
- const { run: run8, setup } = portableYoui(root);
1571
- lines.push(`- \u2026and ${findings2.length - MAX_LISTED} more. ${setup} Run \`${run8} check --diff\`.`);
1728
+ const { run: run7, setup } = portableYoui(root);
1729
+ lines.push(`- \u2026and ${findings2.length - MAX_LISTED} more. ${setup} Run \`${run7} check --diff\`.`);
1572
1730
  }
1573
1731
  for (const gap of diffGaps(result)) lines.push(`- incomplete: ${gap}; 0 findings there is not a pass`);
1574
1732
  return lines;
@@ -1593,21 +1751,22 @@ async function runClaudeHook(input, target = {}) {
1593
1751
  }
1594
1752
  const raw = payload.tool_input?.file_path;
1595
1753
  if (typeof raw !== "string" || raw.length === 0) return SILENT;
1596
- const file = resolve4(payload.cwd ?? process.cwd(), raw);
1754
+ const file = resolve5(payload.cwd ?? process.cwd(), raw);
1597
1755
  try {
1598
- const project2 = await load(target, dirname5(file));
1599
- const real = (path) => realpath2(path).catch(() => path);
1600
- const relative10 = relPath(await real(project2.root), await real(file));
1601
- if (!isChecked(project2, relative10)) return SILENT;
1602
- const { change, problem } = await changeFor(() => workingTreeChange(project2, void 0, [relative10]));
1603
- if (problem) return claudeContext(problem, project2.root);
1604
- const result = configured(project2) ? await runCheck(project2, { files: [file], lock: false, baseline: await baselineFor(project2, target), ...change ? { diff: change } : {} }) : await runRepoCheck(project2, change);
1756
+ const project2 = await load(target, dirname6(file));
1757
+ const real = (path) => realpath3(path).catch(() => path);
1758
+ const relative9 = relPath(await real(project2.root), await real(file));
1759
+ if (!isChecked(project2, relative9)) return SILENT;
1760
+ const found = await changeFor(() => workingTreeChange(project2, void 0, [relative9]));
1761
+ if (found.problem !== void 0) return claudeContext(found.problem, project2.root);
1762
+ const { change } = found;
1763
+ const result = configured(project2) ? await runCheck(project2, { files: [file], lock: false, baseline: await baselineFor(project2, target), diff: change }) : await runRepoCheck(project2, change);
1605
1764
  const lines = findingLines(result, project2.root);
1606
- const see = COMPONENT_FILE.test(relative10) ? `See it: youiyoux render ${quoted(relative10)} (or the MCP tool ui_render), then open the images: light and dark, desktop and phone, accessibility issues and the diff against the base.` : null;
1607
- if (lines.length === 0) return see ? claudeContext(`youiyoux: ${relative10} passes the design-system check. ${see}`, project2.root) : SILENT;
1608
- const older = change && "violations" in result ? result.violations.filter((v) => !isAdded(change.added, v.file, v.line)).length : 0;
1765
+ const see = COMPONENT_FILE.test(relative9) ? `See it: youiyoux render ${quoted(relative9)} (or the MCP tool ui_render), then open the images: light and dark, desktop and phone, accessibility issues and the diff against the base.` : null;
1766
+ if (lines.length === 0) return see ? claudeContext(`youiyoux: ${relative9} passes the design-system check. ${see}`, project2.root) : SILENT;
1767
+ const older = "violations" in result ? result.violations.filter((v) => !isAdded(change.added, v.file, v.line)).length : 0;
1609
1768
  const known = older > 0 ? ` (${older} older findings on unchanged lines omitted)` : "";
1610
- return claudeContext(`youiyoux design-system check (advisory) on ${relative10}${known}:
1769
+ return claudeContext(`youiyoux design-system check (advisory) on ${relative9}${known}:
1611
1770
  ${lines.join("\n")}${!configured(project2) ? `
1612
1771
  ${result.diff?.notes.join("\n")}` : ""}
1613
1772
  Fix these before finishing (each "Fix:" says what to write); \`youiyoux show <component>\` and the youiyoux MCP tools give the legal values. CI runs the authoritative check.${see ? `
@@ -1635,10 +1794,11 @@ async function runPreCommitHook(target = {}, options = {}) {
1635
1794
  const staged = await stagedSources(project2);
1636
1795
  const files = Object.keys(staged);
1637
1796
  if (files.length === 0) return SILENT;
1638
- const { change, problem } = await changeFor(() => stagedChange(project2));
1639
- if (problem) return { stdout: `${viaNpx(problem, project2.root)}
1797
+ const found = await changeFor(() => stagedChange(project2));
1798
+ if (found.problem !== void 0) return { stdout: `${viaNpx(found.problem, project2.root)}
1640
1799
  `, exitCode: options.strict ? 1 : 0 };
1641
- const result = configured(project2) ? await runCheck(project2, { files: files.map((f) => join9(project2.root, f)), overrides: staged, lock: false, baseline: await baselineFor(project2, target), ...change ? { diff: change } : {} }) : await runRepoCheck(project2, change);
1800
+ const { change } = found;
1801
+ const result = configured(project2) ? await runCheck(project2, { files: files.map((f) => join10(project2.root, f)), overrides: staged, lock: false, baseline: await baselineFor(project2, target), diff: change }) : await runRepoCheck(project2, change);
1642
1802
  const lines = findingLines(result, project2.root);
1643
1803
  const failing = result.incomplete || (result.diff?.findings.length ?? 0) > 0;
1644
1804
  if (lines.length === 0) return { stdout: `${label}: ${files.length} staged file(s) clean, no drift
@@ -1653,195 +1813,9 @@ ${viaNpx(lines.join("\n"), project2.root)}
1653
1813
  }
1654
1814
  }
1655
1815
 
1656
- // ../cli/src/hooks-install.ts
1657
- import { execFile as execFile5 } from "node:child_process";
1658
- import { chmod, mkdir as mkdir2, realpath as realpath3 } from "node:fs/promises";
1659
- import { dirname as dirname6, join as join10, relative as relative5, resolve as resolve5, sep as sep3 } from "node:path";
1660
- import { promisify as promisify5 } from "node:util";
1661
- var run5 = promisify5(execFile5);
1662
- var CLAUDE_MARKER = "hooks run claude";
1663
- var BLOCK_START = "# >>> youi pre-commit >>>";
1664
- var BLOCK_END2 = "# <<< youi pre-commit <<<";
1665
- var WORKFLOW = ".github/workflows/youi-check.yml";
1666
- var WORKFLOW_HEADER = "# Generated by `youiyoux hooks install --ci`.";
1667
- var LEGACY_WORKFLOW_HEADER = WORKFLOW_HEADER.replace("youiyoux", "youi");
1668
- var managedWorkflow = (text) => text.startsWith(WORKFLOW_HEADER) || text.startsWith(LEGACY_WORKFLOW_HEADER);
1669
- var DIFF_STEP = "name: youiyoux check --diff";
1670
- var CLAUDE_MATCHER = "Edit|Write|MultiEdit";
1671
- var CLAUDE_TIMEOUT_S = 60;
1672
- var shellQuote = (arg) => /^[\w@%+=:,./-]+$/.test(arg) ? arg : `'${arg.replace(/'/g, `'\\''`)}'`;
1673
- async function gitOut(cwd, args) {
1674
- return (await run5("git", [...args], { cwd })).stdout.trim();
1675
- }
1676
- async function repoRootOf(dir) {
1677
- return gitOut(dir, ["rev-parse", "--show-toplevel"]).catch(() => dir);
1678
- }
1679
- function hookCommand(toolRoot, kind, target, extra = []) {
1680
- const server = toolCommand(toolRoot);
1681
- const args = [server.command, ...server.args, "hooks", "run", kind];
1682
- for (const key2 of ["project", "config", "design", "root"]) {
1683
- const value = target[key2];
1684
- if (value) args.push(`--${key2}`, resolve5(value));
1685
- }
1686
- return [...args, ...extra].map(shellQuote).join(" ");
1687
- }
1688
- async function write(file, content, before, guarantee, mode) {
1689
- if (before === content) return { file, action: "unchanged", guarantee };
1690
- await mkdir2(dirname6(file), { recursive: true });
1691
- await writeFileAtomic(file, content);
1692
- if (mode !== void 0) await chmod(file, mode);
1693
- return { file, action: before === null ? "created" : "updated", guarantee };
1694
- }
1695
- var isYouiEntry = (entry) => (entry.hooks ?? []).some((h) => typeof h.command === "string" && h.command.includes(CLAUDE_MARKER));
1696
- async function installClaudeHook(repoRoot, command2) {
1697
- const file = join10(repoRoot, ".claude/settings.local.json");
1698
- const before = await readTextOrNull(file);
1699
- let settings;
1700
- try {
1701
- settings = before === null ? {} : JSON.parse(before);
1702
- } catch {
1703
- throw new Error(`${file} is not valid JSON; fix it first (youi will not overwrite it)`);
1704
- }
1705
- const hooks = settings.hooks ?? {};
1706
- const existing = hooks.PostToolUse ?? [];
1707
- const entry = { matcher: CLAUDE_MATCHER, hooks: [{ type: "command", command: command2, timeout: CLAUDE_TIMEOUT_S }] };
1708
- const postToolUse = existing.some(isYouiEntry) ? existing.map((e) => isYouiEntry(e) ? entry : e) : [...existing, entry];
1709
- const next2 = { ...settings, hooks: { ...hooks, PostToolUse: postToolUse } };
1710
- return write(file, `${JSON.stringify(next2, null, 2)}
1711
- `, before, "advisory: after each Claude Code Edit/Write/MultiEdit, that file's new violations go back to Claude; never blocks; edits made any other way are not seen");
1712
- }
1713
- function withBlock(script, block) {
1714
- if (script === null) return `#!/bin/sh
1715
- ${block}
1716
- `;
1717
- const start = script.indexOf(BLOCK_START);
1718
- const end = script.indexOf(BLOCK_END2);
1719
- if (start >= 0 && end > start) return `${script.slice(0, start)}${block}${script.slice(end + BLOCK_END2.length)}`;
1720
- return `${script.endsWith("\n") ? script : `${script}
1721
- `}
1722
- ${block}
1723
- `;
1724
- }
1725
- async function installPreCommit(projectRoot, command2, strict) {
1726
- const hooksDir = resolve5(projectRoot, await gitOut(projectRoot, ["rev-parse", "--git-path", "hooks"]));
1727
- const file = join10(hooksDir, "pre-commit");
1728
- const before = await readTextOrNull(file);
1729
- const shebang = before?.split("\n", 1)[0] ?? "";
1730
- if (before !== null && shebang.startsWith("#!") && !/\b(?:sh|bash|zsh|dash)\b/.test(shebang)) {
1731
- throw new Error(`${file} is not a shell script (${shebang}); add this line to it yourself: ${command2}`);
1732
- }
1733
- if (before !== null && /^\s*exec\s/m.test(before) && !before.includes(BLOCK_START)) {
1734
- throw new Error(`${file} hands off with exec, so an appended check would never run; add this line before the exec yourself: ${command2}`);
1735
- }
1736
- const what = strict ? "Blocks commits with new design-system errors" : "Advisory, never blocks";
1737
- const block = `${BLOCK_START}
1738
- # ${what}: checks the staged content of staged files only; CI is authoritative.
1739
- ${command2} || exit $?
1740
- ${BLOCK_END2}`;
1741
- const guarantee = strict ? "blocks commits with new errors in staged files (any agent or person that commits; --no-verify skips it)" : "advisory at commit time, staged files only (any agent or person that commits; not every edit)";
1742
- return write(file, withBlock(before, block), before, guarantee, 493);
1743
- }
1744
- async function installRepoAgentInstructions(root) {
1745
- const file = join10(root, "AGENTS.md");
1746
- const before = await readTextOrNull(file);
1747
- const guarantee = "agents run youiyoux check --diff after UI edits; pre-commit also checks staged edits";
1748
- const ruled = before !== null && before.indexOf(RULE_END) > before.indexOf(RULE_BEGIN) && before.includes(RULE_BEGIN);
1749
- return write(file, ruled ? before : upsertBlock(before, repoAgentsBlock(root)), before, guarantee);
1750
- }
1751
- function codexFallback(projectRoot, agentsFile) {
1752
- return {
1753
- file: join10(projectRoot, agentsFile),
1754
- action: "skipped",
1755
- guarantee: "no post-edit hook installed (Codex documents none): Codex follows AGENTS.md, where `youiyoux sync` tells it to run `youiyoux check --diff` after UI edits, and the git pre-commit hook checks what it commits"
1756
- };
1757
- }
1758
- function githubSlug(url) {
1759
- const match = /github\.com[:/]([^/\s]+)\/([^/\s]+?)(?:\.git)?\/?$/.exec(url.trim());
1760
- return match ? `${match[1]}/${match[2]}` : null;
1761
- }
1762
- async function repoAt(root) {
1763
- const slug2 = githubSlug(await gitOut(root, ["remote", "get-url", "origin"]).catch(() => ""));
1764
- if (!slug2) throw new Error(`${root} has no GitHub origin remote; the workflow needs one to check it out`);
1765
- return { slug: slug2, commit: await gitOut(root, ["rev-parse", "HEAD"]) };
1766
- }
1767
- var posix2 = (path) => path.split(sep3).join("/") || ".";
1768
- var inside = (root, path) => !relative5(root, path).startsWith("..");
1769
- function workflowText(input) {
1770
- const dir = input.projectDir || ".";
1771
- const target = ["--project", dir, "--design", ".youi-ci/design", ...input.configPath ? ["--config", input.configPath, "--root", dir] : []];
1772
- const checkArgs = ["--baseline", "--format", "github", ...target].map(shellQuote).join(" ");
1773
- const diffArgs2 = ["--strict", "--format", "github", ...target].map(shellQuote).join(" ");
1774
- const diffStep = input.diff ? ` - ${DIFF_STEP} (drift this pull request adds)
1775
- if: \${{ !cancelled() && github.event_name == 'pull_request' }}
1776
- env:
1777
- YOUI_BASE: \${{ github.base_ref }}
1778
- run: node .youi-ci/tool/packages/cli/bin/youi.js check --diff "origin/$YOUI_BASE" ${diffArgs2}
1779
- ` : "";
1780
- return `${WORKFLOW_HEADER}
1781
- # The authoritative design-system check: it fails on violations the baseline does not
1782
- # accept, annotates them on the PR's lines, and states the rules version (design commit
1783
- # and definitions digest) in the job summary. Raise the rules version by changing the
1784
- # design ref below; YOUI_REPO_TOKEN must be able to read both repositories.
1785
- name: youiyoux check
1786
- on:
1787
- pull_request:
1788
- push:
1789
- branches: [main]
1790
- permissions:
1791
- contents: read
1792
- jobs:
1793
- youi-check:
1794
- runs-on: ubuntu-latest
1795
- timeout-minutes: 10
1796
- steps:
1797
- - uses: actions/checkout@v4${input.diff ? "\n with:\n fetch-depth: 0" : ""}
1798
- - name: Check out youi
1799
- uses: actions/checkout@v4
1800
- with:
1801
- repository: ${input.tool.slug}
1802
- ref: ${input.tool.commit}
1803
- path: .youi-ci/tool
1804
- token: \${{ secrets.YOUI_REPO_TOKEN }}
1805
- - name: Check out the design rules (the rules version)
1806
- uses: actions/checkout@v4
1807
- with:
1808
- repository: ${input.design.slug}
1809
- ref: ${input.design.commit}
1810
- path: .youi-ci/design
1811
- token: \${{ secrets.YOUI_REPO_TOKEN }}
1812
- - uses: pnpm/action-setup@v4
1813
- with:
1814
- version: 10.13.1
1815
- - uses: actions/setup-node@v4
1816
- with:
1817
- node-version: 24
1818
- - run: pnpm install --frozen-lockfile --ignore-scripts
1819
- working-directory: .youi-ci/tool
1820
- - name: youiyoux check
1821
- run: node .youi-ci/tool/packages/cli/bin/youi.js check ${checkArgs}
1822
- ${diffStep}`;
1823
- }
1824
- async function installWorkflow(project2, toolRoot, target, force, options = {}) {
1825
- const top = await gitOut(project2.root, ["rev-parse", "--show-toplevel"]);
1826
- const [tool, design] = await Promise.all([ephemeralTool(toolRoot) ? Promise.resolve({ slug: "npm/youiyoux", commit: "published" }) : repoAt(toolRoot), repoAt(project2.repo.root)]);
1827
- const configFile = resolve5(target.config ?? project2.configFile ?? join10(project2.root, PROJECT_FILE));
1828
- const configPath = !target.config ? null : inside(project2.repo.root, configFile) ? `.youi-ci/design/${posix2(relative5(project2.repo.root, configFile))}` : inside(top, configFile) ? posix2(relative5(top, configFile)) : null;
1829
- if (target.config && !configPath) throw new Error(`${configFile} is in neither this repo nor the design repo, so CI cannot read it`);
1830
- const file = join10(top, WORKFLOW);
1831
- const before = await readTextOrNull(file);
1832
- if (before !== null && !managedWorkflow(before) && !force) throw new Error(`${file} exists and was not written by youi; pass --force to replace it`);
1833
- const diff = options.diff === true || before !== null && managedWorkflow(before) && (before.includes(DIFF_STEP) || before.includes(DIFF_STEP.replace("youiyoux", "youi")));
1834
- const projectDir = posix2(relative5(top, await realpath3(project2.root).catch(() => project2.root)));
1835
- let text = workflowText({ tool, design, projectDir, configPath, diff });
1836
- if (ephemeralTool(toolRoot)) {
1837
- text = text.replace(/ - name: Check out youi\n[\s\S]*?(?= - name: Check out the design)/, "").replace(/ - run: pnpm install --frozen-lockfile --ignore-scripts\n working-directory: .youi-ci\/tool\n/, "").replaceAll("node .youi-ci/tool/packages/cli/bin/youi.js", "npx -y youiyoux");
1838
- }
1839
- return write(file, text, before, `authoritative: ${ephemeralTool(toolRoot) ? "runs the published youiyoux package; pinned to" : `pinned to ${tool.slug}@${tool.commit.slice(0, 7)} and`} rules ${design.slug}@${design.commit.slice(0, 7)}${diff ? "; pull requests also fail on drift they add (youiyoux check --diff --strict)" : ""}`);
1840
- }
1841
-
1842
1816
  // ../cli/src/agents-install.ts
1843
1817
  import { readdir as readdir2, rm as rm2, rmdir } from "node:fs/promises";
1844
- import { dirname as dirname7, isAbsolute as isAbsolute4, join as join11, relative as relative6 } from "node:path";
1818
+ import { dirname as dirname7, isAbsolute as isAbsolute4, join as join11, relative as relative5 } from "node:path";
1845
1819
  var TOML_BEGIN = "# >>> youi mcp >>>";
1846
1820
  var TOML_END = "# <<< youi mcp <<<";
1847
1821
  var MCP_NAME = "youiyoux";
@@ -1858,7 +1832,7 @@ function mcpCommand(toolRoot, target) {
1858
1832
  return { command: server.command, args };
1859
1833
  }
1860
1834
  function relativeTo(repoRoot, server) {
1861
- return { command: server.command, args: server.args.map((arg) => isAbsolute4(arg) ? relative6(repoRoot, arg) || "." : arg) };
1835
+ return { command: server.command, args: server.args.map((arg) => isAbsolute4(arg) ? relative5(repoRoot, arg) || "." : arg) };
1862
1836
  }
1863
1837
  var shellLine = ({ command: command2, args }) => [command2, ...args].map(shellQuote).join(" ");
1864
1838
  function withBlock2(text, block, begin, end) {
@@ -2047,7 +2021,7 @@ function renderDiscoverSearch(result) {
2047
2021
  const width = Math.min(40, Math.max(...result.hits.map((h) => (h.source.ref ?? h.name).length)));
2048
2022
  const rows = result.hits.flatMap((h) => {
2049
2023
  const s = h.signals;
2050
- const facts = [h.license === "unknown" ? style.warn("licence unknown") : h.license, s.via ? `${compact(s.via.weeklyDownloads)}/wk via ${s.via.package}` : `${compact(s.weeklyDownloads)}/wk`, `${compact(s.stars)}\u2605`, ...s.archived ? [style.error("archived")] : []].join(" \xB7 ");
2024
+ const facts = [h.license === "unknown" ? style.warn("licence unknown") : copyleft(h.license) ? style.warn(`${h.license} (copyleft)`) : h.license, s.via ? `${compact(s.via.weeklyDownloads)}/wk via ${s.via.package}` : `${compact(s.weeklyDownloads)}/wk`, `${compact(s.stars)}\u2605`, ...s.archived ? [style.error("archived")] : []].join(" \xB7 ");
2051
2025
  return [` ${padEnd(truncate(h.source.ref ?? h.name, 40), width)} ${truncate(h.summary, 72)}`, ` ${" ".repeat(width)} ${style.dim(facts)} ${h.install ? h.install.command : ""}`];
2052
2026
  });
2053
2027
  const from = result.offset ?? 0;
@@ -2087,7 +2061,7 @@ import { homedir, userInfo } from "node:os";
2087
2061
  import { join as join12, resolve as resolve6 } from "node:path";
2088
2062
  async function swapImpact(candidate, family) {
2089
2063
  try {
2090
- const { discoverIndexPath: discoverIndexPath2, sharedStore: sharedStore2 } = await import("./src-4KFNL3MY.js");
2064
+ const { discoverIndexPath: discoverIndexPath2, sharedStore: sharedStore2 } = await import("./src-SFKFERZ2.js");
2091
2065
  const path = discoverIndexPath2();
2092
2066
  if (!process.env.DATABASE_URL && !existsSync(path)) return null;
2093
2067
  const impact = await (await sharedStore2(path, process.env, { readOnly: true })).swapImpact(candidate, family);
@@ -2098,7 +2072,7 @@ async function swapImpact(candidate, family) {
2098
2072
  }
2099
2073
  async function libraryLicence(id) {
2100
2074
  try {
2101
- const { discoverCacheDir: discoverCacheDir2, findListing } = await import("./src-4KFNL3MY.js");
2075
+ const { discoverCacheDir: discoverCacheDir2, findListing } = await import("./src-SFKFERZ2.js");
2102
2076
  const { entry } = await findListing(discoverCacheDir2(void 0, process.env), id);
2103
2077
  return entry.id === id ? entry.license : null;
2104
2078
  } catch {
@@ -2106,7 +2080,7 @@ async function libraryLicence(id) {
2106
2080
  }
2107
2081
  }
2108
2082
  async function startRuleEditor(options) {
2109
- const { FileStore, RuleWorkflow, serveWorkflow } = await import("./src-7ENJHYG4.js");
2083
+ const { FileStore, RuleWorkflow, serveWorkflow } = await import("./src-6CJR547Y.js");
2110
2084
  const projectRoot = resolve6(options.project);
2111
2085
  const load2 = { ...options.config ? { configPath: resolve6(options.config) } : {}, ...options.design ? { designRoot: resolve6(options.design) } : {}, ...options.root ? { productRoot: resolve6(options.root) } : {} };
2112
2086
  const project2 = await loadProject(projectRoot, load2);
@@ -2121,48 +2095,125 @@ async function startRuleEditor(options) {
2121
2095
 
2122
2096
  // ../cli/src/hub.ts
2123
2097
  import { spawn } from "node:child_process";
2124
- import { access as access4 } from "node:fs/promises";
2098
+ import { existsSync as existsSync2 } from "node:fs";
2099
+ import { access as access2, mkdtemp as mkdtemp2, rm as rm3 } from "node:fs/promises";
2100
+ import { tmpdir as tmpdir2 } from "node:os";
2125
2101
  import { join as join13, resolve as resolve7 } from "node:path";
2126
2102
  var HUB_SNAPSHOT_DIR = ".hub";
2127
2103
  var DEFAULT_HUB_PORT = "5173";
2104
+ var named = (options) => ["design", "project", "config", "root", "inventory", "detect"].flatMap((key2) => options[key2] ? [`--${key2}`, resolve7(options[key2])] : []);
2105
+ var namesSystem = (options) => ["design", "project", "config", "root"].some((key2) => options[key2]);
2128
2106
  function hubPlan(toolRoot, options) {
2129
2107
  const web = join13(toolRoot, "apps/web");
2130
2108
  const vite = [join13(web, "node_modules/vite/bin/vite.js"), ...options.port ? ["--port", options.port, "--strictPort"] : []];
2131
- const named = ["design", "project", "config", "root", "inventory", "detect"].flatMap((key2) => options[key2] ? [`--${key2}`, resolve7(options[key2])] : []);
2132
- const watchesDemo = options.watch && !["design", "project", "config", "root"].some((key2) => options[key2]);
2133
- const flags = watchesDemo ? [...named, "--project", join13(toolRoot, "examples/demo-app")] : named;
2109
+ const watchesDemo = options.watch && !namesSystem(options);
2110
+ const flags = watchesDemo ? [...named(options), "--project", join13(toolRoot, "examples/demo-app")] : named(options);
2134
2111
  const api = options.api ? { YOUI_API: options.api } : {};
2135
2112
  if (flags.length === 0) return { web, snapshot: null, env: api, vite, watch: null };
2136
2113
  const out = join13(web, HUB_SNAPSHOT_DIR);
2137
2114
  const snapshot = [join13(web, "scripts/snapshot.ts"), "--out", out, ...flags];
2138
2115
  return { web, snapshot, env: { YOUI_HUB_SNAPSHOT: out, ...api }, vite, watch: options.watch ? [...snapshot, "--watch"] : null };
2139
2116
  }
2140
- function run6(args, cwd, env) {
2117
+ var BUILT_HUB = "hub";
2118
+ var BUILT_DEMO = "hub-demo";
2119
+ function builtHubPlan(toolRoot, options, out) {
2120
+ const flags = named(options);
2121
+ if (options.watch && !namesSystem(options)) throw new Error("--watch keeps a system's snapshot live: name it with --design or --project (the demo system is not watched)");
2122
+ const hub = join13(toolRoot, BUILT_HUB);
2123
+ const snapshot = flags.length > 0 ? [join13(toolRoot, "hub-snapshot.js"), "--out", out, ...flags] : null;
2124
+ const server = [join13(toolRoot, "hub-server.js"), "--dist", hub, "--kit", join13(hub, "kit"), "--snapshot", snapshot ? out : join13(toolRoot, BUILT_DEMO), ...snapshot ? ["--build-frame"] : [], ...options.port ? ["--port", options.port] : [], ...options.api ? ["--api", options.api] : [], ...options.watch ? ["--live"] : []];
2125
+ return { snapshot, server, watch: options.watch && snapshot ? [...snapshot, "--watch"] : null };
2126
+ }
2127
+ function run5(args, cwd, env, started) {
2141
2128
  return new Promise((done, fail) => {
2142
2129
  const child = spawn(process.execPath, args, { cwd, env: { ...process.env, ...env }, stdio: "inherit" });
2130
+ started?.(child);
2143
2131
  child.on("error", fail);
2144
2132
  child.on("exit", (code, signal) => done(code ?? (signal === "SIGINT" || signal === "SIGTERM" ? 0 : 1)));
2145
2133
  });
2146
2134
  }
2135
+ async function runBuiltHub(toolRoot, options) {
2136
+ const out = await mkdtemp2(join13(tmpdir2(), "youiyoux-hub-"));
2137
+ const children = /* @__PURE__ */ new Set();
2138
+ const stop = () => {
2139
+ for (const child of children) child.kill("SIGTERM");
2140
+ };
2141
+ process.on("SIGINT", stop);
2142
+ process.on("SIGTERM", stop);
2143
+ try {
2144
+ const plan = builtHubPlan(toolRoot, options, out);
2145
+ if (plan.snapshot) {
2146
+ const code = await run5(plan.snapshot, process.cwd(), {}, (child) => children.add(child));
2147
+ if (code !== 0) return code;
2148
+ }
2149
+ if (plan.watch) children.add(spawn(process.execPath, plan.watch, { cwd: process.cwd(), stdio: "inherit" }));
2150
+ return await run5(plan.server, process.cwd(), {}, (child) => children.add(child));
2151
+ } finally {
2152
+ stop();
2153
+ process.off("SIGINT", stop);
2154
+ process.off("SIGTERM", stop);
2155
+ await rm3(out, { recursive: true, force: true });
2156
+ }
2157
+ }
2147
2158
  async function runHub(toolRoot, options) {
2159
+ if (existsSync2(join13(toolRoot, BUILT_HUB, "index.html"))) return runBuiltHub(toolRoot, options);
2148
2160
  const plan = hubPlan(toolRoot, options);
2149
- await access4(plan.vite[0]).catch(() => {
2161
+ await access2(plan.vite[0]).catch(() => {
2150
2162
  throw new Error(`the hub's dependencies are not installed: run \`pnpm install\` in ${toolRoot}`);
2151
2163
  });
2152
2164
  if (plan.snapshot) {
2153
- const code = await run6(plan.snapshot, process.cwd(), {});
2165
+ const code = await run5(plan.snapshot, process.cwd(), {});
2154
2166
  if (code !== 0) return code;
2155
2167
  }
2156
2168
  const watcher = plan.watch ? spawn(process.execPath, plan.watch, { cwd: process.cwd(), stdio: "inherit" }) : null;
2157
2169
  try {
2158
- return await run6(plan.vite, plan.web, plan.env);
2170
+ return await run5(plan.vite, plan.web, plan.env);
2159
2171
  } finally {
2160
2172
  watcher?.kill("SIGTERM");
2161
2173
  }
2162
2174
  }
2175
+ function previewsPlan(toolRoot, options) {
2176
+ const web = join13(toolRoot, "apps/web");
2177
+ const registry = options.registry ?? "@shadcn";
2178
+ const args = [join13(web, "scripts/capture-discover-thumbs.ts"), "--hub", `http://localhost:${options.port ?? DEFAULT_HUB_PORT}`];
2179
+ if (registry !== "all") args.push("--registry", registry);
2180
+ for (const key2 of ["role", "limit"]) if (options[key2]) args.push(`--${key2}`, options[key2]);
2181
+ if (options.cache) args.push("--cache", resolve7(options.cache));
2182
+ if (options.force) args.push("--force");
2183
+ return { web, args };
2184
+ }
2185
+ async function runPreviews(toolRoot, options) {
2186
+ const plan = previewsPlan(toolRoot, options);
2187
+ if (existsSync2(join13(toolRoot, BUILT_HUB, "index.html"))) return run5([join13(toolRoot, "hub-previews.js"), ...plan.args.slice(1)], process.cwd(), {});
2188
+ await access2(join13(plan.web, "node_modules/playwright-core")).catch(() => {
2189
+ throw new Error(`the hub's dependencies are not installed: run \`pnpm install\` in ${toolRoot}`);
2190
+ });
2191
+ return run5(plan.args, plan.web, {});
2192
+ }
2193
+
2194
+ // ../cli/src/requests.ts
2195
+ function cliNext(brief) {
2196
+ return brief.next.replace(/call request_addressed with what you changed \(file:line\)/, `run youiyoux requests addressed ${brief.id} --message "<what you changed, with file:line>"`).replace(/request_reply/g, `youiyoux requests reply ${brief.id} --message "\u2026"`);
2197
+ }
2198
+ function renderBrief(brief, full = false) {
2199
+ const lines = [
2200
+ `${style.bold(brief.id)} ${brief.kind} ${brief.status === "open" ? style.warn(brief.status) : style.dim(brief.status)} ${brief.route} ${brief.component}${brief.lookalike ? style.dim(" (hand-built lookalike)") : ""}${brief.at ? ` ${brief.at}` : ""}`,
2201
+ ` \u201C${brief.ask}\u201D ${style.dim(`\xB7 ${brief.from} \xB7 ${brief.scope}`)}`
2202
+ ];
2203
+ if (brief.picture) lines.push(` ${style.dim("picture")} ${brief.picture}${brief.box ? style.dim(` (box ${brief.box.x},${brief.box.y} ${brief.box.width}\xD7${brief.box.height} px)`) : ""}`);
2204
+ for (const entry of full ? brief.thread : brief.thread.slice(-2)) lines.push(` ${style.dim("\xB7")} ${entry}`);
2205
+ if (!full && brief.thread.length > 2) lines.push(style.dim(` \u2026 ${brief.thread.length - 2} earlier in the thread: youiyoux requests show ${brief.id}`));
2206
+ lines.push(` ${style.dim("next")} ${cliNext(brief)}`);
2207
+ return lines.join("\n");
2208
+ }
2209
+ function renderRequests(briefs, status, unreadable) {
2210
+ const lines = briefs.length ? briefs.map((b) => renderBrief(b)) : [`No ${status === "all" ? "" : `${status} `}requests on the screens.`];
2211
+ for (const u of unreadable) lines.push(style.error(`\u2716 ${u.file} is not a youi.request/v1: ${u.reason}`));
2212
+ return lines.join("\n\n");
2213
+ }
2163
2214
 
2164
2215
  // ../cli/src/index-store.ts
2165
- import { appendFile, mkdir as mkdir3, readFile as readFile4, stat, rename, mkdtemp as mkdtemp2, rm as rm3 } from "node:fs/promises";
2216
+ import { appendFile, mkdir as mkdir3, readFile as readFile4, stat, rename, mkdtemp as mkdtemp3, rm as rm4 } from "node:fs/promises";
2166
2217
  import { dirname as dirname8, join as join14, resolve as resolve8 } from "node:path";
2167
2218
  var SOURCES_FILE = "index-sources.json";
2168
2219
  var SCANS_LOG = "scans.jsonl";
@@ -2186,26 +2237,19 @@ async function noteSource(home, change) {
2186
2237
  `);
2187
2238
  }
2188
2239
  async function recordInventory(store, home, report, file) {
2189
- const run8 = inventoryRun(report, resolve8(file));
2190
- const logged = { run: run8.run, source: run8.source, scannedAt: run8.scannedAt, projects: run8.projects, families: run8.families };
2240
+ const run7 = inventoryRun(report, resolve8(file));
2241
+ const logged = { run: run7.run, source: run7.source, scannedAt: run7.scannedAt, projects: run7.projects, families: run7.families };
2191
2242
  await mkdir3(home, { recursive: true });
2192
2243
  await appendFile(join14(home, SCANS_LOG), `${JSON.stringify({ schema: "youi.scan-log/v1", ...logged })}
2193
2244
  `);
2194
- await store.recordInventory(run8);
2195
- await noteSource(home, { inventory: run8.source });
2245
+ await store.recordInventory(run7);
2246
+ await noteSource(home, { inventory: run7.source });
2196
2247
  await linkCandidates(store);
2197
- return run8;
2248
+ return run7;
2198
2249
  }
2199
2250
  async function recordProject(store, project2, usage) {
2200
2251
  await store.replaceGraph(`project:${project2.root}`, projectGraph({ project: project2.config.id, rules: project2.system.rules, components: project2.system.components.map((c) => ({ id: c.contract.id, name: c.contract.name })), usage }));
2201
2252
  }
2202
- async function readJsonFile(file) {
2203
- try {
2204
- return JSON.parse(await readFile4(file, "utf8"));
2205
- } catch {
2206
- return null;
2207
- }
2208
- }
2209
2253
  async function readLog(home) {
2210
2254
  const text = await readFile4(join14(home, SCANS_LOG), "utf8").catch(() => "");
2211
2255
  return text.split("\n").flatMap((line) => {
@@ -2223,7 +2267,7 @@ async function rebuildIndex(path, options = {}) {
2223
2267
  const env = options.env ?? process.env;
2224
2268
  const home = indexHome(path, env);
2225
2269
  await mkdir3(dirname8(path), { recursive: true });
2226
- const scratch = !env.DATABASE_URL ? await mkdtemp2(`${path}.rebuild-`) : null;
2270
+ const scratch = !env.DATABASE_URL ? await mkdtemp3(`${path}.rebuild-`) : null;
2227
2271
  const target = scratch ? join14(scratch, "index.db") : path;
2228
2272
  const store = scratch ? await openSqlite(target) : await sharedStore(path, env);
2229
2273
  try {
@@ -2241,7 +2285,7 @@ async function rebuildIndex(path, options = {}) {
2241
2285
  const missing = [];
2242
2286
  let replayed = 0;
2243
2287
  for (const file of sources.inventories) {
2244
- const parsed = InventoryReport.safeParse(await readJsonFile(file));
2288
+ const parsed = InventoryReport.safeParse(await readJson(file));
2245
2289
  if (!parsed.success) {
2246
2290
  missing.push(file);
2247
2291
  continue;
@@ -2260,7 +2304,7 @@ async function rebuildIndex(path, options = {}) {
2260
2304
  }
2261
2305
  try {
2262
2306
  const project2 = await loadProject(source.root, source.options);
2263
- const usage = ScanReport.safeParse(await readJsonFile(source.usage ?? join14(project2.root, ".youi/usage.json")));
2307
+ const usage = ScanReport.safeParse(await readJson(source.usage ?? join14(project2.root, ".youi/usage.json")));
2264
2308
  await recordProject(store, project2, usage.success ? usage.data : null);
2265
2309
  projects++;
2266
2310
  } catch (error) {
@@ -2295,7 +2339,7 @@ async function rebuildIndex(path, options = {}) {
2295
2339
  } finally {
2296
2340
  if (scratch) {
2297
2341
  await store.close();
2298
- await rm3(scratch, { recursive: true, force: true });
2342
+ await rm4(scratch, { recursive: true, force: true });
2299
2343
  }
2300
2344
  }
2301
2345
  }
@@ -2327,6 +2371,40 @@ function renderHistory(scans) {
2327
2371
  return lines.join("\n");
2328
2372
  }
2329
2373
 
2374
+ // ../cli/src/gaps.ts
2375
+ var where = (c) => {
2376
+ const first = c.definitions.map((d) => `${d.file}:${d.line}`).slice(0, 2).join(", ");
2377
+ const parts = [c.count ? `${c.count} ${c.count === 1 ? "component" : "components"} named for it` : null, c.uses ? `${c.uses} ${c.uses === 1 ? "use" : "uses"}` : null, `in ${c.files} ${c.files === 1 ? "file" : "files"}`].filter(Boolean);
2378
+ return `${parts.join(", ")}${first ? ` (${first}${c.definitions.length > 2 ? ", \u2026" : ""})` : ""}`;
2379
+ };
2380
+ var offered = (c) => c.candidates === null ? style.dim("no candidates: the Discover index is not synced") : c.candidates.length === 0 ? style.dim("no licensed candidate in the index") : c.candidates.map((x) => `${x.name} ${style.dim(`(${x.source}, ${x.licence}${x.copyleft ? ", copyleft" : ""})`)}`).join(" \xB7 ");
2381
+ function renderGaps(report) {
2382
+ const lines = [];
2383
+ for (const project2 of report.projects) {
2384
+ const by = (status) => project2.concepts.filter((c) => c.status === status);
2385
+ const hand = by("hand-built");
2386
+ const missing = by("missing");
2387
+ lines.push(`${style.bold(project2.id)}${project2.complete ? "" : style.warn(" (some files could not be read: counts are a lower bound)")}${project2.dependencies === "unread" ? style.dim(" (package.json not read: libraries only from imports)") : ""}`);
2388
+ if (hand.length) {
2389
+ lines.push(" Built by hand, with what could stand in for it:");
2390
+ for (const c of hand) lines.push(` ${c.name.padEnd(16)} ${where(c)}`, ` ${"".padEnd(16)} ${offered(c)}`);
2391
+ }
2392
+ if (missing.length) lines.push(` Not in it, from the standard kit (${missing.length}): ${missing.map((c) => c.name).join(", ")}`);
2393
+ const library = by("library");
2394
+ if (library.length) lines.push(` From libraries: ${library.map((c) => `${c.name} (${c.packages.join(", ")})`).join(", ")}`);
2395
+ const system = by("system");
2396
+ if (system.length) lines.push(` In the System${report.system ? ` (${report.system})` : ""}: ${system.map((c) => c.name).join(", ")}`);
2397
+ lines.push("");
2398
+ }
2399
+ if (report.across.length) {
2400
+ lines.push("Built by hand in more than one project, worth adding to the System once:");
2401
+ for (const a of report.across) lines.push(` ${a.name}: ${a.projects.join(", ")} (${a.uses} uses)`);
2402
+ lines.push("");
2403
+ }
2404
+ lines.push(report.discover === "synced" ? "Next: candidates for every gap are in youiyoux gaps --json and on the Gaps page of youiyoux ui; youiyoux discover show <id> for one. Nothing is installed." : "Next: youiyoux discover sync, then run youiyoux gaps again for candidates.");
2405
+ return lines.join("\n");
2406
+ }
2407
+
2330
2408
  // ../cli/src/inventory.ts
2331
2409
  var FAMILIES_PER_ROLE = 6;
2332
2410
  function shapeLabel(shape) {
@@ -2363,7 +2441,7 @@ function storyLines(report) {
2363
2441
  function storyNext(report, outFile) {
2364
2442
  const stories = report.projects.flatMap((p) => p.stories ? [p.stories] : []);
2365
2443
  if (stories.length === 0) return null;
2366
- const open = `youiyoux ui --inventory ${outFile ?? ".youi/inventory.json"}`;
2444
+ const open = `youiyoux ui --inventory ${outFile}`;
2367
2445
  const skipped = stories.reduce((n, s) => n + s.skipped.length, 0);
2368
2446
  if (skipped > 0) return `Next: rewrite the skipped stories as CSF 3 args stories of a component in this project (or fix the file), run youiyoux inventory again, then see them beside their real uses with ${open}`;
2369
2447
  return `Next: see the stories beside their real uses with ${open}`;
@@ -2411,7 +2489,7 @@ function renderInventory(report, outFile) {
2411
2489
  }
2412
2490
  const stories = storyLines(report);
2413
2491
  if (stories.length > 0) lines.push("", ...stories);
2414
- if (outFile) lines.push(style.dim(`inventory \u2192 ${outFile}`));
2492
+ lines.push(style.dim(`inventory \u2192 ${outFile}`));
2415
2493
  const next2 = storyNext(report, outFile);
2416
2494
  if (next2) lines.push(next2);
2417
2495
  return lines.join("\n");
@@ -2445,7 +2523,7 @@ function renderAdoptionRequest(request) {
2445
2523
  // ../cli/src/report/run.ts
2446
2524
  import { randomBytes as randomBytes2 } from "node:crypto";
2447
2525
  import { mkdir as mkdir4, rename as rename2, stat as stat2, writeFile as writeFile2 } from "node:fs/promises";
2448
- import { join as join21, relative as relative8, resolve as resolve11 } from "node:path";
2526
+ import { join as join21, relative as relative7, resolve as resolve11 } from "node:path";
2449
2527
 
2450
2528
  // ../cli/src/report/card.ts
2451
2529
  import { deflateSync } from "node:zlib";
@@ -2759,7 +2837,7 @@ function prefixOf(tokens) {
2759
2837
  const [best, n] = [...counts].sort((a, b) => b[1] - a[1])[0] ?? [null, 0];
2760
2838
  return best && n >= tokens.length / 4 ? best : null;
2761
2839
  }
2762
- function scaleTokens(index, file, usage = { count: /* @__PURE__ */ new Map(), byFile: /* @__PURE__ */ new Map() }) {
2840
+ function scaleTokens(index, file, usage) {
2763
2841
  const value = (t) => `${t.id.split(".")[0]}\0${JSON.stringify(t.light)}`;
2764
2842
  const prefix = prefixOf(index.tokens);
2765
2843
  const rank = new Map(index.tokens.map((t) => [t, genericity(t, prefix)]));
@@ -2773,7 +2851,7 @@ function scaleTokens(index, file, usage = { count: /* @__PURE__ */ new Map(), by
2773
2851
  const component = componentOf(t);
2774
2852
  return component !== null && !general.has(value(t)) && new RegExp(`(?:^|[/._-])${component}s?(?:[/._-]|$)`).test(path);
2775
2853
  });
2776
- const score = (t) => [rank.get(t), Number(local.has(cssName(t))), Number(!usage.aliases?.has(cssName(t))), usage.count.get(cssName(t)) ?? 0, -t.cssName.length];
2854
+ const score = (t) => [rank.get(t), Number(local.has(cssName(t))), Number(!usage.aliases.has(cssName(t))), usage.count.get(cssName(t)) ?? 0, -t.cssName.length];
2777
2855
  const higher = (a, b) => {
2778
2856
  for (let i = 0; i < a.length; i++) if (a[i] !== b[i]) return b[i] - a[i];
2779
2857
  return 0;
@@ -2784,21 +2862,20 @@ function scaleTokens(index, file, usage = { count: /* @__PURE__ */ new Map(), by
2784
2862
  var escape = (text) => text.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
2785
2863
  async function reportTokens(module, tokens, usage) {
2786
2864
  if (isShowcase(module.file) || module.lang !== "css" && module.jsx.length === 0) return [];
2787
- const { parseModule: parseModule2 } = await import("./src-B742AYFC.js");
2788
2865
  let source = withoutVariables(module.source);
2789
2866
  if (module.lang !== "css") {
2790
2867
  source = maskFunctions(source, /(?:repeating-)?(?:linear|radial|conic)-gradient\s*\(/gi);
2791
2868
  source = source.replace(/(?<=[[,]\s*)(["'`])(?:#[0-9a-fA-F]{3,8}|(?:rgba?|hsla?)\([^)\n]*\))\1(?=\s*[,\]])/g, (literal) => " ".repeat(literal.length));
2792
2869
  }
2793
- const clean = source === module.source ? module : await parseModule2(module.file, source);
2870
+ const clean = source === module.source ? module : await parseModule(module.file, source);
2794
2871
  if (!clean) return [];
2795
2872
  const index = scaleTokens(tokens, module.file, usage);
2796
2873
  return hardcodedValues(clean, index, { isTokenSource: module.lang === "css", added: () => true }).filter((f) => {
2797
2874
  if (/\bvar\(/.test(f.text)) return false;
2798
2875
  const kind = /^Hardcoded (colour|radius|spacing|type)\b/.exec(f.message)?.[1];
2799
2876
  const groups = kind === "colour" ? /^(color|colour)\./ : kind === "radius" ? /^(radius|radii|rounded)\./ : kind === "spacing" ? /^(space|spacing)\./ : /^(type|typography|fontSize)\./;
2800
- const named = index.tokens.filter((t) => new RegExp(`[\\s(]${escape(t.id)}(?=[\\s,)]|$)`).test(f.fix));
2801
- return named.length > 0 && named.every((t) => groups.test(t.id));
2877
+ const named2 = index.tokens.filter((t) => new RegExp(`[\\s(]${escape(t.id)}(?=[\\s,)]|$)`).test(f.fix));
2878
+ return named2.length > 0 && named2.every((t) => groups.test(t.id));
2802
2879
  });
2803
2880
  }
2804
2881
  function publicLook(caption) {
@@ -2808,12 +2885,12 @@ function publicLook(caption) {
2808
2885
  }
2809
2886
 
2810
2887
  // ../cli/src/report/groups.ts
2811
- import { existsSync as existsSync2 } from "node:fs";
2888
+ import { existsSync as existsSync3 } from "node:fs";
2812
2889
  import { join as join16 } from "node:path";
2813
2890
 
2814
2891
  // ../cli/src/report/imports.ts
2815
2892
  import { realpathSync } from "node:fs";
2816
- import { dirname as dirname9, join as join15, relative as relative7, resolve as resolve9 } from "node:path";
2893
+ import { dirname as dirname9, join as join15, relative as relative6, resolve as resolve9 } from "node:path";
2817
2894
  import ts from "typescript";
2818
2895
  var resolvers = /* @__PURE__ */ new Map();
2819
2896
  function resolverOf(root) {
@@ -2832,7 +2909,7 @@ function clearImportCache() {
2832
2909
  }
2833
2910
  async function verifiedImport(root, keeper, from, suggested) {
2834
2911
  const { options, cache } = resolverOf(root);
2835
- let local = relative7(dirname9(join15(root, from)), join15(root, keeper)).replaceAll("\\", "/").replace(/\.(tsx?|jsx?|mjs)$/, "");
2912
+ let local = relative6(dirname9(join15(root, from)), join15(root, keeper)).replaceAll("\\", "/").replace(/\.(tsx?|jsx?|mjs)$/, "");
2836
2913
  if (!local.startsWith(".")) local = `./${local}`;
2837
2914
  for (const specifier of /* @__PURE__ */ new Set([suggested, local])) {
2838
2915
  const found = ts.resolveModuleName(specifier, resolve9(root, from), options, ts.sys, cache).resolvedModule?.resolvedFileName;
@@ -2950,7 +3027,6 @@ var SVG_TAGS = /* @__PURE__ */ new Set(["svg", "path", "g", "circle", "rect", "l
2950
3027
  var NEAR_IDENTICAL = 0.5;
2951
3028
  var MIN_STRUCTURE = 6;
2952
3029
  var GENERIC_IDENTICAL = 0.85;
2953
- var plural2 = (count, word, many = `${word}s`) => `${count} ${count === 1 ? word : many}`;
2954
3030
  var repoPath = (dir, file) => dir === "." ? file : `${dir}/${file}`;
2955
3031
  var kebab = (name) => name.replace(/[a-z0-9](?=[A-Z])/g, "$&-").toLowerCase();
2956
3032
  function shapeWords(shape) {
@@ -3116,8 +3192,8 @@ async function nearDuplicateGroups(input) {
3116
3192
  actions.push(`replace the hand-styled ${tag}${at.length > 1 ? "s" : ""} at line${at.length > 1 ? "s" : ""} ${at.join(", ")} with ${keeperName}`);
3117
3193
  }
3118
3194
  for (const v of list.filter((c) => c.kind === "component")) {
3119
- const named = v.name === "default" ? "its default export" : v.name;
3120
- actions.push(v.name === keeperName ? `remove its own ${v.name} and use the kept one` : `build ${named} on ${keeperName} instead of restyling its own element`);
3195
+ const named2 = v.name === "default" ? "its default export" : v.name;
3196
+ actions.push(v.name === keeperName ? `remove its own ${v.name} and use the kept one` : `build ${named2} on ${keeperName} instead of restyling its own element`);
3121
3197
  }
3122
3198
  if (actions.length > 0) steps.push(`In ${file}, ${actions.join("; ")} (${use})`);
3123
3199
  for (const v of list.filter((c) => c.kind === "class")) steps.push(`In ${file}, ${v.name} copies ${keeperName}'s shape: use ${keeperName} where ${v.name} is used`);
@@ -3126,13 +3202,13 @@ async function nearDuplicateGroups(input) {
3126
3202
  const copyIds = new Set(copies.map((v) => v.id));
3127
3203
  const counted = members.filter((m) => copyIds.has(m.version.id));
3128
3204
  const home = [...counted.reduce((n, m) => n.set(m.version.facts, (n.get(m.version.facts) ?? 0) + 1), /* @__PURE__ */ new Map())].sort((a, b) => b[1] - a[1])[0]?.[0] ?? keeperFacts;
3129
- const where = keeperFacts.project.name && importedBy(keeperFacts) > 0 ? `${keeperFacts.project.name}, ${keeperFile}` : keeperFile;
3130
- const lead = kept ? `Keep ${keeperName} (${where})` : `Create ${keeperName} in ${keeperFile} (a new file in ${extract.where}) with this shape`;
3205
+ const where2 = keeperFacts.project.name && importedBy(keeperFacts) > 0 ? `${keeperFacts.project.name}, ${keeperFile}` : keeperFile;
3206
+ const lead = kept ? `Keep ${keeperName} (${where2})` : `Create ${keeperName} in ${keeperFile} (a new file in ${extract.where}) with this shape`;
3131
3207
  const [one, many] = KIND_WORDS[kind];
3132
3208
  const components = new Set([kept, ...copies].flatMap((v) => v?.kind === "component" ? [v.id] : [])).size;
3133
3209
  findings2.push(finding(
3134
3210
  home.project.dir,
3135
- `${plural2(members.length, one, many)} look the same (${shapeWords(family.shape)}) but are built ${sides.length} ways`,
3211
+ `${plural(members.length, one, many)} look the same (${shapeWords(family.shape)}) but are built ${sides.length} ways`,
3136
3212
  counted.length,
3137
3213
  [...copies].sort((a, b) => path(a).localeCompare(path(b)) || a.line - b.line).slice(0, SITES_PER_FINDING).map((v) => ({ at: `${path(v)}:${v.line}`, file: path(v), line: v.line })),
3138
3214
  lead,
@@ -3145,7 +3221,7 @@ async function nearDuplicateGroups(input) {
3145
3221
  }
3146
3222
  function finding(project2, title, count, sites, lead, steps, exported) {
3147
3223
  const shown2 = steps.slice(0, STEPS_SHOWN);
3148
- const more = steps.length > shown2.length ? `; and ${plural2(steps.length - shown2.length, "more file")} (listed in the report)` : "";
3224
+ const more = steps.length > shown2.length ? `; and ${plural(steps.length - shown2.length, "more file")} (listed in the report)` : "";
3149
3225
  return { kind: "near-duplicate", project: project2, title, detail: null, count, sites, lead, steps, next: `${lead}. ${shown2.join(". ")}${more}`, exported };
3150
3226
  }
3151
3227
  function commonDir(files) {
@@ -3247,7 +3323,7 @@ function factsOf(project2, id, modules, eligible, workspaceNames, dependencies,
3247
3323
  if (dirs.has(file.slice(0, at))) break;
3248
3324
  dirs.add(file.slice(0, at));
3249
3325
  }
3250
- const roots = [...dirs].filter((dir) => existsSync2(join16(project2.root, dir, "package.json"))).sort((a, b) => b.length - a.length);
3326
+ const roots = [...dirs].filter((dir) => existsSync3(join16(project2.root, dir, "package.json"))).sort((a, b) => b.length - a.length);
3251
3327
  const nested = (file) => roots.find((dir) => file.startsWith(`${dir}/`)) ?? "";
3252
3328
  return { project: project2, id, dependencies, packageImports, jsxCounts, eligible, twins, dirs, importer, packageOf: nested };
3253
3329
  }
@@ -3257,13 +3333,6 @@ var import_yaml2 = __toESM(require_dist(), 1);
3257
3333
  import { readFile as readFile5 } from "node:fs/promises";
3258
3334
  import { basename as basename2, join as join17, resolve as resolve10 } from "node:path";
3259
3335
  var UI_FILES = ["**/*.{tsx,jsx,astro,vue,svelte}"];
3260
- async function readJson3(file) {
3261
- try {
3262
- return JSON.parse(await readFile5(file, "utf8"));
3263
- } catch {
3264
- return null;
3265
- }
3266
- }
3267
3336
  var strings = (value) => Array.isArray(value) ? value.filter((v) => typeof v === "string") : [];
3268
3337
  async function workspaceGlobs(root, pkg) {
3269
3338
  const pnpm = await readFile5(join17(root, "pnpm-workspace.yaml"), "utf8").catch(() => null);
@@ -3281,7 +3350,7 @@ async function workspaceGlobs(root, pkg) {
3281
3350
  var hasUi = async (dir) => (await findFiles(dir, UI_FILES, DEFAULT_EXCLUDE)).length > 0;
3282
3351
  async function findProjects(dir) {
3283
3352
  const root = resolve10(dir);
3284
- const pkg = await readJson3(join17(root, "package.json"));
3353
+ const pkg = await readJson(join17(root, "package.json"));
3285
3354
  const name = typeof pkg?.name === "string" && pkg.name.trim() ? pkg.name.trim() : basename2(root);
3286
3355
  const single = { dir: ".", root, name: typeof pkg?.name === "string" ? pkg.name : null };
3287
3356
  const workspace = await workspaceGlobs(root, pkg);
@@ -3297,7 +3366,7 @@ async function findProjects(dir) {
3297
3366
  skipped.push(member);
3298
3367
  continue;
3299
3368
  }
3300
- const memberPkg = await readJson3(join17(memberRoot, "package.json"));
3369
+ const memberPkg = await readJson(join17(memberRoot, "package.json"));
3301
3370
  projects.push({ dir: member, root: memberRoot, name: typeof memberPkg?.name === "string" ? memberPkg.name : null });
3302
3371
  }
3303
3372
  if (projects.length === 0) return { root, name, layout: `one project (${workspace.source} names no package with UI files)`, projects: [single], skipped };
@@ -3307,13 +3376,10 @@ async function findProjects(dir) {
3307
3376
  // ../cli/src/report/model.ts
3308
3377
  var REPORT_SCHEMA = "youiyoux.report/1";
3309
3378
  var NPX = `npx ${COMMAND}`;
3310
- var SITES_PER_FINDING2 = 6;
3311
3379
  var TOP_COMPONENTS = 10;
3312
3380
  var TOP_FIXES = 5;
3313
3381
  var AGENT_LINE = `Set up youiyoux for your agent: run ${NPX} agents install, and your coding agent looks up these components before it writes a new one.`;
3314
- var plural3 = (count, word, many = `${word}s`) => `${count} ${count === 1 ? word : many}`;
3315
3382
  var sentence = (text) => text.charAt(0).toUpperCase() + text.slice(1);
3316
- var repoPath2 = (dir, file) => dir === "." ? file : `${dir}/${file}`;
3317
3383
  var errorText = (error) => error instanceof Error ? error.message : String(error);
3318
3384
  var Lines = class {
3319
3385
  files = /* @__PURE__ */ new Map();
@@ -3350,7 +3416,7 @@ async function modulesOf(project2, target, failed) {
3350
3416
  const module = await parseModule(file, await readFile6(join18(project2.root, file), "utf8"));
3351
3417
  if (module) modules.push(module);
3352
3418
  } catch (error) {
3353
- failed.push(`${repoPath2(project2.dir, file)}: ${errorText(error)}`);
3419
+ failed.push(`${repoPath(project2.dir, file)}: ${errorText(error)}`);
3354
3420
  }
3355
3421
  }
3356
3422
  return modules;
@@ -3364,8 +3430,9 @@ async function grouped(project2, found, lines, describe) {
3364
3430
  const findings2 = [];
3365
3431
  for (const list of groups.values()) {
3366
3432
  const first = list[0];
3367
- const sites = await Promise.all(list.slice(0, SITES_PER_FINDING2).map((f) => lines.site(`${repoPath2(project2.dir, f.file)}:${f.line}`, repoPath2(project2.dir, f.file), f.line)));
3368
- findings2.push({ kind: first.kind, project: project2.dir, count: list.length, sites, ...describe(first, list.length) });
3433
+ const sites = await Promise.all(list.slice(0, SITES_PER_FINDING).map((f) => lines.site(`${repoPath(project2.dir, f.file)}:${f.line}`, repoPath(project2.dir, f.file), f.line)));
3434
+ const { title, next: next2 } = describe(first, list.length);
3435
+ findings2.push({ kind: first.kind, project: project2.dir, count: list.length, sites, title, detail: null, next: next2 });
3369
3436
  }
3370
3437
  return findings2.sort((a, b) => b.count - a.count || a.title.localeCompare(b.title));
3371
3438
  }
@@ -3395,8 +3462,8 @@ async function analyseProject(project2, index, inventory, inventoryError, worksp
3395
3462
  if (isShowcase(at)) showcaseFiles.add(at.replace(/:\d+(?::\d+)?$/, ""));
3396
3463
  }
3397
3464
  const live = (items) => items.flatMap((item) => {
3398
- const where = item.where?.filter((at) => !isShowcase(at));
3399
- return item.where?.length && !where?.length ? [] : [{ ...item, ...where ? { where, count: Math.min(item.count, where.length) } : {} }];
3465
+ const where2 = item.where?.filter((at) => !isShowcase(at));
3466
+ return item.where?.length && !where2?.length ? [] : [{ ...item, ...where2 ? { where: where2, count: Math.min(item.count, where2.length) } : {} }];
3400
3467
  });
3401
3468
  const signals = live(scan.signals);
3402
3469
  const used = new Set(signals.filter((s) => s.kind !== "package" && s.kind !== "lockfile").map((s) => s.library));
@@ -3414,14 +3481,14 @@ async function analyseProject(project2, index, inventory, inventoryError, worksp
3414
3481
  for (const look of looks) {
3415
3482
  if (look.recipe?.kind !== "component") continue;
3416
3483
  const key2 = `${look.recipe.file}#${look.recipe.export}`;
3417
- const entry = byComponent.get(key2) ?? { name: look.recipe.export, file: repoPath2(project2.dir, look.recipe.file), roles: [] };
3484
+ const entry = byComponent.get(key2) ?? { name: look.recipe.export, file: repoPath(project2.dir, look.recipe.file), roles: [] };
3418
3485
  entry.roles.push(look.role);
3419
3486
  byComponent.set(key2, entry);
3420
3487
  }
3421
3488
  const components = [...byComponent.values()].map((c) => ({ name: c.name, file: c.file, uses: c.roles.length, role: [...new Set(c.roles)].sort((a, b) => c.roles.filter((r) => r === b).length - c.roles.filter((r) => r === a).length)[0] })).sort((a, b) => b.uses - a.uses || a.name.localeCompare(b.name)).slice(0, TOP_COMPONENTS);
3422
3489
  const inventoryProject = inventory?.projects[index];
3423
3490
  if (inventoryError) incomplete.push(`The component inventory could not be built (${inventoryError}); run ${NPX} inventory --project ${project2.dir} to see why`);
3424
- for (const f of inventoryProject?.failed ?? []) failed.push(`${repoPath2(project2.dir, f.file)}: ${f.reason}`);
3491
+ for (const f of inventoryProject?.failed ?? []) failed.push(`${repoPath(project2.dir, f.file)}: ${f.reason}`);
3425
3492
  let hardcoded = [];
3426
3493
  let offLibrary = [];
3427
3494
  const everyLine = () => true;
@@ -3433,7 +3500,7 @@ async function analyseProject(project2, index, inventory, inventoryError, worksp
3433
3500
  const found = (await Promise.all(modules.map((module) => reportTokens(module, tokens, usage)))).flat();
3434
3501
  hardcoded = await grouped(project2, found, lines, (first, count) => {
3435
3502
  const { word, value, tokens: names } = hardcodedValue(first);
3436
- return { title: `${sentence(word)} ${value} is hardcoded in ${plural3(count, "place")}; the theme already has it as ${names}`, detail: null, next: `${sentence(first.fix)} in each place` };
3503
+ return { title: `${sentence(word)} ${value} is hardcoded in ${plural(count, "place")}; the theme already has it as ${names}`, next: `${sentence(first.fix)} in each place` };
3437
3504
  });
3438
3505
  } catch (error) {
3439
3506
  incomplete.push(`Hardcoded values were not checked (${errorText(error)}); run ${NPX} check --diff to see why`);
@@ -3445,7 +3512,7 @@ async function analyseProject(project2, index, inventory, inventoryError, worksp
3445
3512
  const kitDirs = [changeProject.config.componentsDir, ...inventory?.kits.filter((k) => k.project === projectId && k.kind === "cva-folder").map((k) => k.path) ?? []];
3446
3513
  const exempt = (file) => kitDirs.some((dir) => file === dir || file.startsWith(`${dir}/`));
3447
3514
  const found = modules.flatMap((module) => offLibraryImports(module, { added: everyLine, classify, chosen, exempt }));
3448
- offLibrary = await grouped(project2, found, lines, (first) => ({ title: first.message.replace(/^This imports/, "Imports"), detail: null, next: `${sentence(first.fix)}` }));
3515
+ offLibrary = await grouped(project2, found, lines, (first) => ({ title: first.message.replace(/^This imports/, "Imports"), next: sentence(first.fix) }));
3449
3516
  } catch (error) {
3450
3517
  incomplete.push(`Library choices were not compared (${errorText(error)}); run ${NPX} detect ${project2.dir} to see why`);
3451
3518
  }
@@ -3462,7 +3529,7 @@ async function analyseProject(project2, index, inventory, inventoryError, worksp
3462
3529
  libraries,
3463
3530
  unused,
3464
3531
  definitions: inventory ? inventory.definitions.filter((d) => d.project === projectId && !isShowcase(d.file)).length : 0,
3465
- kits: inventory ? inventory.kits.filter((k) => k.project === projectId).map((k) => ({ path: repoPath2(project2.dir, k.path), kind: k.kind === "cva-folder" ? "CVA components" : "CSS classes", members: k.members.length })) : [],
3532
+ kits: inventory ? inventory.kits.filter((k) => k.project === projectId).map((k) => ({ path: repoPath(project2.dir, k.path), kind: k.kind === "cva-folder" ? "CVA components" : "CSS classes", members: k.members.length })) : [],
3466
3533
  roles,
3467
3534
  uses: looks.length,
3468
3535
  components,
@@ -3475,7 +3542,7 @@ async function analyseProject(project2, index, inventory, inventoryError, worksp
3475
3542
  return { section, facts };
3476
3543
  }
3477
3544
  function topNext(f) {
3478
- const text = f.lead && f.steps ? `${f.lead}${f.steps.length > 1 ? `, then change ${plural3(f.steps.length, "file")} (each listed in the report)` : `; ${f.steps[0].charAt(0).toLowerCase()}${f.steps[0].slice(1)}`}` : f.next;
3545
+ const text = f.lead && f.steps ? `${f.lead}${f.steps.length > 1 ? `, then change ${plural(f.steps.length, "file")} (each listed in the report)` : `; ${f.steps[0].charAt(0).toLowerCase()}${f.steps[0].slice(1)}`}` : f.next;
3479
3546
  return `${text.charAt(0).toLowerCase()}${text.slice(1).replace(/\.$/, "")}`;
3480
3547
  }
3481
3548
  var DUPLICATE_RULE = "Duplicated components: groups of 2+ exported components that look alike or share a name.";
@@ -3485,10 +3552,10 @@ function summaryLines(model) {
3485
3552
  const top = model.projects.flatMap((p) => p.components).sort((a, b) => b.uses - a.uses)[0];
3486
3553
  const first = model.topFixes[0];
3487
3554
  return [
3488
- `${model.repo}: ${plural3(t.projects, "project")}, ${plural3(t.files, "file")} scanned; UI libraries: ${t.libraries.length > 0 ? t.libraries.join(", ") : "none found"}.`,
3489
- `Components: ${t.definitions} defined, ${plural3(t.uses, "button, input, card or other UI element", "buttons, inputs, cards and other UI elements")} in use${top ? `; most used: ${top.name} (${plural3(top.uses, "use")})` : ""}.`,
3490
- `Near-duplicates: ${plural3(t.nearDuplicates, "group")} of UI that looks the same but is built more than once; ${t.componentDuplicates} of them between exported components.`,
3491
- `Hardcoded values: ${plural3(t.hardcoded, "colour, spacing or radius", "colours, spacings and radii")} a theme token already holds; off-library imports: ${t.offLibrary}.`,
3555
+ `${model.repo}: ${plural(t.projects, "project")}, ${plural(t.files, "file")} scanned; UI libraries: ${t.libraries.length > 0 ? t.libraries.join(", ") : "none found"}.`,
3556
+ `Components: ${t.definitions} defined, ${plural(t.uses, "button, input, card or other UI element", "buttons, inputs, cards and other UI elements")} in use${top ? `; most used: ${top.name} (${plural(top.uses, "use")})` : ""}.`,
3557
+ `Near-duplicates: ${plural(t.nearDuplicates, "group")} of UI that looks the same but is built more than once; ${t.componentDuplicates} of them between exported components.`,
3558
+ `Hardcoded values: ${plural(t.hardcoded, "colour, spacing or radius", "colours, spacings and radii")} a theme token already holds; off-library imports: ${t.offLibrary}.`,
3492
3559
  first ? `Top fix: ${first.title.replace(/\.$/, "")}; ${topNext(first)}.` : `Nothing to fix first; next: ${NPX} agents install, so your agent keeps it that way.`
3493
3560
  ];
3494
3561
  }
@@ -3523,7 +3590,7 @@ async function buildReportModel(dir, options = {}) {
3523
3590
  offLibrary: projects.reduce((n, p) => n + p.offLibrary.reduce((m, f) => m + f.count, 0), 0)
3524
3591
  };
3525
3592
  const topFixes = [...near, ...projects.flatMap((p) => [...p.hardcoded, ...p.offLibrary])].sort((a, b) => b.count - a.count || a.kind.localeCompare(b.kind) || a.title.localeCompare(b.title)).slice(0, TOP_FIXES);
3526
- const layout = repo.skipped.length > 0 ? `${repo.layout}; ${plural3(repo.skipped.length, "package")} without UI files left out (${repo.skipped.join(", ")})` : repo.layout;
3593
+ const layout = repo.skipped.length > 0 ? `${repo.layout}; ${plural(repo.skipped.length, "package")} without UI files left out (${repo.skipped.join(", ")})` : repo.layout;
3527
3594
  const model = {
3528
3595
  schema: REPORT_SCHEMA,
3529
3596
  repo: repo.name,
@@ -3545,7 +3612,6 @@ async function buildReportModel(dir, options = {}) {
3545
3612
 
3546
3613
  // ../cli/src/report/html.ts
3547
3614
  var escape2 = (text) => String(text).replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#39;");
3548
- var plural4 = (count, word, many = `${word}s`) => `${count} ${count === 1 ? word : many}`;
3549
3615
  var anchor = (dir) => `p-${dir === "." ? "root" : dir.replace(/[^a-zA-Z0-9]+/g, "-").replace(/^-|-$/g, "")}`;
3550
3616
  var CSS = `
3551
3617
  :root{color-scheme:light dark;--canvas:#f4f5f7;--surface:#fcfcfd;--raised:#f6f7f9;--sunken:#f8f9fa;--line:#e3e4e8;--line-strong:#d0d2d8;--ink:#16171a;--ink-2:#3d4049;--muted:#6b6f78;--faint:#9ea2ab;--accent:#5e6ad2;--accent-soft:#eceefa;--success:#4f7a69;--success-soft:#eaf1ee;--danger:#ad5a43;--danger-soft:#f7ece8;--neutral-soft:#eef0f3;--k-dup:var(--accent);--k-hard:color-mix(in oklch,var(--accent) 55%,var(--surface));--k-off:color-mix(in oklch,var(--accent) 25%,var(--surface));--shadow:0 1px 2px rgba(16,17,20,.04)}
@@ -3723,7 +3789,6 @@ var rich = (text) => escape2(text).replace(FACT, (fact) => `<span class="mono">$
3723
3789
  function next(text) {
3724
3790
  return `<p class="next"><strong>Next:</strong> ${rich(text.replace(/\.$/, ""))}.</p>`;
3725
3791
  }
3726
- var STEPS_SHOWN2 = 5;
3727
3792
  var KIND = { "near-duplicate": "Duplicate", "hardcoded-value": "Hardcoded", "off-library": "Off-library" };
3728
3793
  function splitMeasures(title) {
3729
3794
  const m = /\s*\(([^()]*\d[^()]*)\)/.exec(title);
@@ -3747,18 +3812,18 @@ function shots(f, looks) {
3747
3812
  }
3748
3813
  function finding2(f, showProject, looks) {
3749
3814
  const { text, measures } = f.kind === "near-duplicate" ? splitMeasures(f.title) : { text: f.title, measures: [] };
3750
- const where = showProject && f.project !== "." ? `<p class="muted">in <span class="mono">${escape2(f.project)}</span></p>` : "";
3751
- const head = `<div class="head"><span class="count">${f.count}<small>${f.count === 1 ? "place" : "places"}</small></span><div class="what"><p class="title">${rich(text)}</p>${f.detail ? `<p class="muted">${escape2(f.detail)}</p>` : ""}${where}</div><span class="chip">${f.kind === "hardcoded-value" ? swatch(f.title) : ""}${KIND[f.kind]}</span></div>`;
3815
+ const where2 = showProject && f.project !== "." ? `<p class="muted">in <span class="mono">${escape2(f.project)}</span></p>` : "";
3816
+ const head = `<div class="head"><span class="count">${f.count}<small>${f.count === 1 ? "place" : "places"}</small></span><div class="what"><p class="title">${rich(text)}</p>${where2}</div><span class="chip">${f.kind === "hardcoded-value" ? swatch(f.title) : ""}${KIND[f.kind]}</span></div>`;
3752
3817
  const specs = measures.length > 0 ? `<ul class="specs">${measures.map((m) => `<li>${escape2(m)}</li>`).join("")}</ul>` : "";
3753
3818
  if (f.lead && f.steps && f.steps.length > 0) {
3754
3819
  const item = (step) => `<li>${rich(step.replace(/\.$/, ""))}</li>`;
3755
- const rest = f.steps.slice(STEPS_SHOWN2);
3756
- const more2 = rest.length > 0 ? `<details class="more"><summary>and ${plural4(rest.length, "more file")}</summary><ol class="steps" start="${STEPS_SHOWN2 + 1}">${rest.map(item).join("")}</ol></details>` : "";
3757
- return `<li class="finding" data-kind="${f.kind}">${head}${specs}<div class="compare">${next(f.lead)}<ol class="steps">${f.steps.slice(0, STEPS_SHOWN2).map(item).join("")}</ol>${more2}${shots(f, looks)}</div></li>`;
3820
+ const rest = f.steps.slice(STEPS_SHOWN);
3821
+ const more2 = rest.length > 0 ? `<details class="more"><summary>and ${plural(rest.length, "more file")}</summary><ol class="steps" start="${STEPS_SHOWN + 1}">${rest.map(item).join("")}</ol></details>` : "";
3822
+ return `<li class="finding" data-kind="${f.kind}">${head}${specs}<div class="compare">${next(f.lead)}<ol class="steps">${f.steps.slice(0, STEPS_SHOWN).map(item).join("")}</ol>${more2}${shots(f, looks)}</div></li>`;
3758
3823
  }
3759
3824
  const code = f.sites.some((s) => s.code);
3760
3825
  const sites = f.sites.map((s) => `<li><code>${escape2(s.at)}</code>${s.code ? `<pre>${escape2(s.code)}</pre>` : ""}</li>`).join("");
3761
- const more = f.count > f.sites.length ? `<li>+ ${plural4(f.count - f.sites.length, "more place")}</li>` : "";
3826
+ const more = f.count > f.sites.length ? `<li>+ ${plural(f.count - f.sites.length, "more place")}</li>` : "";
3762
3827
  return `<li class="finding" data-kind="${f.kind}">${head}${specs}<ul class="sites${code ? " code" : ""}">${sites}${more}</ul>${next(f.next)}</li>`;
3763
3828
  }
3764
3829
  function findings(list, none, after, looks) {
@@ -3770,29 +3835,29 @@ function panel(name, id, title, aside, body, level = "h3") {
3770
3835
  }
3771
3836
  function projectSection(p, many, looks) {
3772
3837
  const id = anchor(p.dir);
3773
- const where = p.dir === "." ? "" : ` --project ${p.dir}`;
3838
+ const where2 = p.dir === "." ? "" : ` --project ${p.dir}`;
3774
3839
  const libraries = p.libraries.length === 0 ? `<div class="body"><p class="none">No UI library found.</p>${next(`if it uses one, run ${NPX} detect ${p.dir} to see what was read`)}</div>` : `<div class="body"><ul class="rows">${p.libraries.map((l) => `<li><span>${escape2(l.name)}${l.for.length > 0 ? ` <small>${escape2(l.for.join(", "))}</small>` : ""}</span><span>${escape2(l.confidence)} confidence</span></li>`).join("")}</ul>${p.unused.length > 0 ? `<p class="muted">Declared but never imported (not counted): ${escape2(p.unused.join(", "))}</p>` : ""}${next(`build new UI with these, not a new library: ${NPX} agents install tells your agent so`)}</div>`;
3775
- const kits = p.kits.length > 0 ? `<p class="muted">Kits: ${p.kits.map((k) => `<code>${escape2(k.path)}</code> (${escape2(k.kind)}, ${plural4(k.members, "member")})`).join(", ")}</p>` : "";
3776
- const components = p.components.length > 0 ? `<h4>Most used</h4><ul class="rows">${p.components.map((c) => `<li><span>${escape2(c.name)}<small>${escape2(c.file)}</small></span><span>${plural4(c.uses, "use")}</span></li>`).join("")}</ul>` : "";
3777
- const inventory = `<div class="body"><p>${plural4(p.definitions, "component")} defined in ${plural4(p.files, "file")}; ${plural4(p.uses, "UI element")} in use (buttons, links styled as buttons, inputs, cards, badges, dialogs, tabs).</p>${p.roles.length > 0 ? `<ul class="rows">${p.roles.map((r) => `<li><span class="mono">${escape2(r.role)}</span><span>${plural4(r.uses, "use")} \xB7 ${plural4(r.families, "shape")}</span></li>`).join("")}</ul>` : ""}${kits}${components}${next(`see every look grouped by shape with ${NPX} inventory${where}`)}</div>`;
3778
- const unread = p.failed.length > 0 ? `<details class="incomplete more"><summary>${plural4(p.failed.length, "file")} not fully read; what is in them is unknown</summary><ul class="unread">${p.failed.map((f) => `<li>${rich(f)}</li>`).join("")}</ul></details>` : "";
3779
- const count = (list) => list.length === 0 ? "None" : `${plural4(list.length, "finding")} \xB7 ${plural4(list.reduce((n, f) => n + f.count, 0), "place")}`;
3840
+ const kits = p.kits.length > 0 ? `<p class="muted">Kits: ${p.kits.map((k) => `<code>${escape2(k.path)}</code> (${escape2(k.kind)}, ${plural(k.members, "member")})`).join(", ")}</p>` : "";
3841
+ const components = p.components.length > 0 ? `<h4>Most used</h4><ul class="rows">${p.components.map((c) => `<li><span>${escape2(c.name)}<small>${escape2(c.file)}</small></span><span>${plural(c.uses, "use")}</span></li>`).join("")}</ul>` : "";
3842
+ const inventory = `<div class="body"><p>${plural(p.definitions, "component")} defined in ${plural(p.files, "file")}; ${plural(p.uses, "UI element")} in use (buttons, links styled as buttons, inputs, cards, badges, dialogs, tabs).</p>${p.roles.length > 0 ? `<ul class="rows">${p.roles.map((r) => `<li><span class="mono">${escape2(r.role)}</span><span>${plural(r.uses, "use")} \xB7 ${plural(r.families, "shape")}</span></li>`).join("")}</ul>` : ""}${kits}${components}${next(`see every look grouped by shape with ${NPX} inventory${where2}`)}</div>`;
3843
+ const unread = p.failed.length > 0 ? `<details class="incomplete more"><summary>${plural(p.failed.length, "file")} not fully read; what is in them is unknown</summary><ul class="unread">${p.failed.map((f) => `<li>${rich(f)}</li>`).join("")}</ul></details>` : "";
3844
+ const count = (list) => list.length === 0 ? "None" : `${plural(list.length, "finding")} \xB7 ${plural(list.reduce((n, f) => n + f.count, 0), "place")}`;
3780
3845
  return `<section class="project" data-section="project" data-project="${escape2(p.dir)}" id="${id}">
3781
3846
  <h2>${escape2(many || p.dir !== "." ? p.dir : "This repo")}${p.name && p.name !== p.dir ? ` <small>${escape2(p.name)}</small>` : ""}</h2>
3782
3847
  ${p.incomplete.length > 0 || p.failed.length > 0 ? `<div class="unread-at" id="${id}-unread">${p.incomplete.map((i) => `<p class="incomplete">${escape2(i)}</p>`).join("")}${unread}</div>` : ""}
3783
3848
  ${panel("near-duplicates", `${id}-near`, "Near-duplicate groups", count(p.nearDuplicates), `<div class="body"><p class="muted">Components that look the same (height, padding, corners, text size and weight) but are built more than once.</p></div>${findings(p.nearDuplicates, "None found: each look is built once.", `keep it that way: ${NPX} agents install has your agent reuse these components`, looks)}`)}
3784
3849
  ${panel("hardcoded", `${id}-hardcoded`, "Hardcoded values that should be tokens", count(p.hardcoded), `<div class="body"><p class="muted">Colours, spacing and radii written as literals although a theme variable holds the same value.</p></div>${findings(p.hardcoded, "None found.", `run ${NPX} check --diff on each change to keep new literals out`, looks)}`)}
3785
3850
  ${panel("off-library", `${id}-off`, "Off-library imports", count(p.offLibrary), `<div class="body"><p class="muted">Imports of an element from a library other than the one the rest of the code uses for it.</p></div>${findings(p.offLibrary, "None found.", `run ${NPX} check --diff on each change to catch a second library early`, looks)}`)}
3786
- ${panel("inventory", `${id}-inventory`, "Component inventory", `${plural4(p.definitions, "component")} \xB7 ${plural4(p.uses, "use")}`, inventory)}
3787
- ${panel("libraries", `${id}-libraries`, "UI libraries", p.libraries.length > 0 ? plural4(p.libraries.length, "library", "libraries") : "", libraries)}
3851
+ ${panel("inventory", `${id}-inventory`, "Component inventory", `${plural(p.definitions, "component")} \xB7 ${plural(p.uses, "use")}`, inventory)}
3852
+ ${panel("libraries", `${id}-libraries`, "UI libraries", p.libraries.length > 0 ? plural(p.libraries.length, "library", "libraries") : "", libraries)}
3788
3853
  ${p.notes.map((n) => `<p class="muted">${escape2(n.replace(/\.?$/, "."))}</p>`).join("")}
3789
3854
  </section>`;
3790
3855
  }
3791
3856
  function looksSection(model) {
3792
3857
  const drawn = model.projects.flatMap((p) => p.rendered.map((r) => ({ ...r, project: p.dir })));
3793
- const body = drawn.length > 0 ? `<div class="looks">${drawn.map((r) => `<figure data-component="${escape2(r.component)}"><figcaption>${escape2(r.component)} <small>${escape2(r.file)}</small></figcaption><div class="pair">${r.light ? `<img alt="${escape2(r.component)}, light" src="${r.light}">` : ""}${r.dark ? `<img alt="${escape2(r.component)}, dark" src="${r.dark}">` : ""}</div>${r.looks.length > 0 ? `<p class="muted">${plural4(r.looks.length, "look")} drawn, light and dark</p>` : ""}</figure>`).join("")}</div>` : "";
3858
+ const body = drawn.length > 0 ? `<div class="looks">${drawn.map((r) => `<figure data-component="${escape2(r.component)}"><figcaption>${escape2(r.component)} <small>${escape2(r.file)}</small></figcaption><div class="pair">${r.light ? `<img alt="${escape2(r.component)}, light" src="${r.light}">` : ""}${r.dark ? `<img alt="${escape2(r.component)}, dark" src="${r.dark}">` : ""}</div>${r.looks.length > 0 ? `<p class="muted">${plural(r.looks.length, "look")} drawn, light and dark</p>` : ""}</figure>`).join("")}</div>` : "";
3794
3859
  const after = model.render.next ? next(model.render.next) : drawn.length > 0 ? next(`see a component after you change it with ${NPX} render <file>`) : next(model.render.state === "off" ? `run ${NPX} --render to draw the most-used components (it asks before downloading the ~100 MB headless Chromium, once)` : `run ${NPX} render <file> to see why a component could not be drawn`);
3795
- return `<section class="panel" data-section="looks" data-render="${model.render.state}" id="looks"><header><h2>Rendered looks</h2><span class="aside">${drawn.length > 0 ? plural4(drawn.length, "component") : ""}</span></header><div class="body">${body}<p class="${drawn.length > 0 ? "muted" : ""}">${escape2(model.render.note)}</p>${after}</div></section>`;
3860
+ return `<section class="panel" data-section="looks" data-render="${model.render.state}" id="looks"><header><h2>Rendered looks</h2><span class="aside">${drawn.length > 0 ? plural(drawn.length, "component") : ""}</span></header><div class="body">${body}<p class="${drawn.length > 0 ? "muted" : ""}">${escape2(model.render.note)}</p>${after}</div></section>`;
3796
3861
  }
3797
3862
  function placesByKind(model) {
3798
3863
  const places = (pick) => model.projects.reduce((n, p) => n + pick(p).reduce((m, f) => m + f.count, 0), 0);
@@ -3804,25 +3869,21 @@ function placesByKind(model) {
3804
3869
  }
3805
3870
  function headline(model) {
3806
3871
  const t = model.totals;
3807
- const [near, hard, off] = placesByKind(model);
3808
- const parts = [
3809
- { label: "Near-duplicates", places: near.places, href: model.projects.length > 1 ? "top-fixes" : `${anchor(model.projects[0]?.dir ?? ".")}-near` },
3810
- { label: "Hardcoded values", places: hard.places, href: model.projects.length > 1 ? "top-fixes" : `${anchor(model.projects[0]?.dir ?? ".")}-hardcoded` },
3811
- { label: "Off-library imports", places: off.places, href: model.projects.length > 1 ? "top-fixes" : `${anchor(model.projects[0]?.dir ?? ".")}-off` }
3812
- ];
3872
+ const panels = ["near", "hardcoded", "off"];
3873
+ const parts = placesByKind(model).map((part, i) => ({ ...part, href: model.projects.length > 1 ? "top-fixes" : `${anchor(model.projects[0]?.dir ?? ".")}-${panels[i]}` }));
3813
3874
  const total = parts.reduce((n2, part) => n2 + part.places, 0);
3814
3875
  const n = (value) => `<b class="${value > 0 ? "warn" : "ok"}">${value}</b>`;
3815
3876
  const unknown = model.projects.some((p) => p.incomplete.length > 0 || p.failed.length > 0);
3816
3877
  const analyses = model.projects.reduce((n2, p) => n2 + p.incomplete.length, 0);
3817
3878
  const unread = model.projects.reduce((n2, p) => n2 + p.failed.length, 0);
3818
- const caveat = [analyses > 0 ? `${plural4(analyses, "analysis", "analyses")} could not run` : "", unread > 0 ? `${plural4(unread, "file")} could not be fully read` : ""].filter(Boolean).join(" and ");
3879
+ const caveat = [analyses > 0 ? `${plural(analyses, "analysis", "analyses")} could not run` : "", unread > 0 ? `${plural(unread, "file")} could not be fully read` : ""].filter(Boolean).join(" and ");
3819
3880
  const said = [];
3820
3881
  if (t.nearDuplicates > 0) said.push(`${n(t.nearDuplicates)} ${t.nearDuplicates === 1 ? "look is" : "looks are"} built more than once`);
3821
3882
  if (t.hardcoded > 0) said.push(`${n(t.hardcoded)} ${t.hardcoded === 1 ? "value skips" : "values skip"} the theme`);
3822
3883
  if (t.offLibrary > 0) said.push(`${n(t.offLibrary)} ${t.offLibrary === 1 ? "import comes" : "imports come"} from a second library`);
3823
3884
  const sentence2 = said.length === 0 ? `Every look is built once and every value comes from the theme${unknown ? ", in what could be read" : ""}.` : `${said.length > 1 ? `${said.slice(0, -1).join(", ")} and ${said.at(-1)}` : said[0]}: <b class="warn">${total}</b> ${total === 1 ? "place" : "places"} to fix.`;
3824
3885
  const bar2 = total > 0 ? `<div class="split" role="img" aria-label="${parts.map((part) => `${part.label} ${part.places}`).join(", ")}">${parts.map((part, i) => part.places > 0 ? `<span class="seg s${i}" style="width:${(part.places / total * 100).toFixed(2)}%"></span>` : "").join("")}</div>` : "";
3825
- const legend = `<ul class="legend">${parts.map((part, i) => `<li><a href="#${part.href}"><span class="key s${i}${part.places === 0 ? " zero" : ""}"></span>${escape2(part.label)} <b>${part.places}</b>${part.places === 0 ? ` <span class="clean">Clean</span>` : ""}</a></li>`).join("")}${unknown ? `<li class="alert"><a href="#${anchor(model.projects.find((p) => p.failed.length > 0 || p.incomplete.length > 0)?.dir ?? ".")}-unread" aria-describedby="unread-tip"><i aria-hidden="true">!</i>${analyses + unread} not read</a><span class="tip" id="unread-tip" role="tooltip">${escape2(caveat.charAt(0).toUpperCase() + caveat.slice(1))}: what is in them is unknown, not zero.</span></li>` : ""}<li class="of">${plural4(t.definitions, "component")} \xB7 ${plural4(t.uses, "UI element")} in use \xB7 ${plural4(t.files, "file")}</li></ul>`;
3886
+ const legend = `<ul class="legend">${parts.map((part, i) => `<li><a href="#${part.href}"><span class="key s${i}${part.places === 0 ? " zero" : ""}"></span>${escape2(part.label)} <b>${part.places}</b>${part.places === 0 ? ` <span class="clean">Clean</span>` : ""}</a></li>`).join("")}${unknown ? `<li class="alert"><a href="#${anchor(model.projects.find((p) => p.failed.length > 0 || p.incomplete.length > 0)?.dir ?? ".")}-unread" aria-describedby="unread-tip"><i aria-hidden="true">!</i>${analyses + unread} not read</a><span class="tip" id="unread-tip" role="tooltip">${escape2(caveat.charAt(0).toUpperCase() + caveat.slice(1))}: what is in them is unknown, not zero.</span></li>` : ""}<li class="of">${plural(t.definitions, "component")} \xB7 ${plural(t.uses, "UI element")} in use \xB7 ${plural(t.files, "file")}</li></ul>`;
3826
3887
  return `<section class="headline" data-section="headline"><p class="say">${sentence2}</p>${bar2}${legend}</section>`;
3827
3888
  }
3828
3889
  var SQUARES = 40;
@@ -3836,7 +3897,7 @@ function shapeMap(model) {
3836
3897
  const rows = [...roles].map(([role, { uses, families }]) => {
3837
3898
  const shown2 = Math.min(families, SQUARES);
3838
3899
  const squares = families === 0 ? `<span class="unmeasured">shape not measured</span>` : `${Array.from({ length: shown2 }, (_, i) => `<i class="${i === 0 ? "one" : "more"}"></i>`).join("")}${families > shown2 ? `<span class="over">+${families - shown2}</span>` : ""}`;
3839
- return `<li class="${families > 1 ? "drift" : families === 1 ? "even" : ""}"><span class="role mono">${escape2(role)}</span><span class="sq">${squares}</span><span class="val"><b>${families}</b> ${families === 1 ? "shape" : "shapes"} \xB7 ${plural4(uses, "use")}</span></li>`;
3900
+ return `<li class="${families > 1 ? "drift" : families === 1 ? "even" : ""}"><span class="role mono">${escape2(role)}</span><span class="sq">${squares}</span><span class="val"><b>${families}</b> ${families === 1 ? "shape" : "shapes"} \xB7 ${plural(uses, "use")}</span></li>`;
3840
3901
  }).join("");
3841
3902
  const many = model.projects.length > 1 ? ", summed over the projects" : "";
3842
3903
  return `<section class="panel" data-section="shapes" id="shapes"><header><h2>Shapes per element</h2><span class="aside"><i class="k one"></i>first shape <i class="k more"></i>each extra shape</span></header><div class="body"><p class="muted">One square per distinct look of each kind of element${many}. One square is consistent; every extra square is a copy someone built.</p><ul class="shapes">${rows}</ul></div></section>`;
@@ -3883,7 +3944,7 @@ ${shapeMap(model)}
3883
3944
  ${panel("top-fixes", "top-fixes", "Top fixes", "Ranked by how many places each one touches", top, "h2")}
3884
3945
  <aside class="agent" data-section="agent"><p><strong>${escape2(model.agent)}</strong></p>${next(`${NPX} agents install, then restart your agent`)}</aside>
3885
3946
  ${looksSection(model)}
3886
- <section class="panel projects" data-section="projects" id="projects"><header><h2>Projects</h2><span class="aside">${plural4(model.projects.length, "project")}</span></header><div class="body"><p>${escape2(model.layout.charAt(0).toUpperCase() + model.layout.slice(1))}.</p>${many ? `<nav><ul>${model.projects.map((p) => `<li><a href="#${anchor(p.dir)}">${escape2(p.dir)}</a>${p.name && p.name !== p.dir ? ` <small>${escape2(p.name)}</small>` : ""}</li>`).join("")}</ul></nav>` : ""}${next(many ? "open each project below; each has its own fixes" : "read this repo's section below")}</div></section>
3947
+ <section class="panel projects" data-section="projects" id="projects"><header><h2>Projects</h2><span class="aside">${plural(model.projects.length, "project")}</span></header><div class="body"><p>${escape2(model.layout.charAt(0).toUpperCase() + model.layout.slice(1))}.</p>${many ? `<nav><ul>${model.projects.map((p) => `<li><a href="#${anchor(p.dir)}">${escape2(p.dir)}</a>${p.name && p.name !== p.dir ? ` <small>${escape2(p.name)}</small>` : ""}</li>`).join("")}</ul></nav>` : ""}${next(many ? "open each project below; each has its own fixes" : "read this repo's section below")}</div></section>
3887
3948
  ${model.projects.map((p) => projectSection(p, many, looks)).join("\n")}
3888
3949
  <footer>
3889
3950
  <p>Made with ${escape2(NPX)} on ${escape2(date)} in ${(model.durationMs / 1e3).toFixed(1)} s. This file is self-contained: it loads nothing from the network, and nothing was uploaded.</p>
@@ -3898,12 +3959,13 @@ ${model.projects.map((p) => projectSection(p, many, looks)).join("\n")}
3898
3959
 
3899
3960
  // ../cli/src/report/looks.ts
3900
3961
  import { spawn as spawn2 } from "node:child_process";
3901
- import { existsSync as existsSync3, readFileSync as readFileSync2 } from "node:fs";
3962
+ import { existsSync as existsSync4, readFileSync as readFileSync2 } from "node:fs";
3902
3963
  import { homedir as homedir2 } from "node:os";
3903
3964
  import { dirname as dirname10, join as join19 } from "node:path";
3904
3965
  var LOOKS_PER_PROJECT = 4;
3905
3966
  var DOWNLOAD_SIZE = "~100 MB";
3906
3967
  var DEPENDENCIES = "Install the repo's dependencies to see rendered looks";
3968
+ var UNDRAWABLE = /not installed|loads no stylesheet|drawn from (?:its|their) (?:tag and )?classes|did not render|bundle failed|unstyled|stub fallback/i;
3907
3969
  var playwrightDir = () => dirname10(playwrightCli());
3908
3970
  function browsersPath(env) {
3909
3971
  const set = env.PLAYWRIGHT_BROWSERS_PATH;
@@ -3914,12 +3976,12 @@ function browsersPath(env) {
3914
3976
  return join19(env.XDG_CACHE_HOME ?? join19(homedir2(), ".cache"), "ms-playwright");
3915
3977
  }
3916
3978
  function chromiumStatus(env = process.env) {
3917
- if (env.YOUI_CHROMIUM) return { available: existsSync3(env.YOUI_CHROMIUM), where: env.YOUI_CHROMIUM };
3979
+ if (env.YOUI_CHROMIUM) return { available: existsSync4(env.YOUI_CHROMIUM), where: env.YOUI_CHROMIUM };
3918
3980
  try {
3919
3981
  const browsers = JSON.parse(readFileSync2(join19(playwrightDir(), "browsers.json"), "utf8"));
3920
3982
  const revision2 = browsers.browsers.find((b) => b.name === "chromium-headless-shell")?.revision;
3921
3983
  const dir = join19(browsersPath(env), `chromium_headless_shell-${revision2}`);
3922
- return { available: revision2 !== void 0 && existsSync3(join19(dir, "INSTALLATION_COMPLETE")), where: dir };
3984
+ return { available: revision2 !== void 0 && existsSync4(join19(dir, "INSTALLATION_COMPLETE")), where: dir };
3923
3985
  } catch {
3924
3986
  return { available: false, where: "playwright-core is not installed" };
3925
3987
  }
@@ -3931,7 +3993,7 @@ function downloadChromium() {
3931
3993
  child.on("close", (code) => resolve13(code === 0));
3932
3994
  });
3933
3995
  }
3934
- var dataUri = (file) => file && existsSync3(file) ? `data:image/png;base64,${readFileSync2(file).toString("base64")}` : null;
3996
+ var dataUri = (file) => file && existsSync4(file) ? `data:image/png;base64,${readFileSync2(file).toString("base64")}` : null;
3935
3997
  async function addLooks(model, repoRoot, options = {}) {
3936
3998
  const drawn = [];
3937
3999
  const problems = [];
@@ -3939,34 +4001,34 @@ async function addLooks(model, repoRoot, options = {}) {
3939
4001
  for (const section of model.projects) {
3940
4002
  const names = [...new Set(section.components.map((c) => c.name))].slice(0, LOOKS_PER_PROJECT);
3941
4003
  if (names.length === 0) continue;
3942
- const where = section.dir === "." ? "this repo" : section.dir;
4004
+ const where2 = section.dir === "." ? "this repo" : section.dir;
3943
4005
  try {
3944
4006
  const installed = (name) => {
3945
4007
  let dir = join19(repoRoot, section.dir);
3946
4008
  while (true) {
3947
- if (existsSync3(join19(dir, "node_modules", name, "package.json"))) return true;
4009
+ if (existsSync4(join19(dir, "node_modules", name, "package.json"))) return true;
3948
4010
  if (dir === repoRoot || dirname10(dir) === dir) return false;
3949
4011
  dir = dirname10(dir);
3950
4012
  }
3951
4013
  };
3952
4014
  if (!installed("react") || !installed("react-dom")) {
3953
- missing.push(where);
4015
+ missing.push(where2);
3954
4016
  continue;
3955
4017
  }
3956
4018
  const projectRoot = join19(repoRoot, section.dir);
3957
4019
  const target = await loadInventoryTarget(projectRoot);
3958
4020
  if (findStylesheets(projectRoot, target.files, { tsconfig: target.tsconfig, override: target.specimenCss, templates: target.templates }).found.length === 0) {
3959
- missing.push(where);
4021
+ missing.push(where2);
3960
4022
  continue;
3961
4023
  }
3962
4024
  const report = await renderShots(join19(repoRoot, section.dir), names, { download: false, base: "HEAD", ...options.executablePath ? { executablePath: options.executablePath } : {}, ...options.cache ? { cache: options.cache } : {}, ...options.log ? { log: options.log } : {} });
3963
- const unavailable = report.notes.some((note) => /not installed|loads no stylesheet|drawn from (?:its|their) (?:tag and )?classes|did not render|bundle failed|unstyled|stub fallback/i.test(note));
4025
+ const unavailable = report.notes.some((note) => UNDRAWABLE.test(note));
3964
4026
  if (unavailable) {
3965
- missing.push(where);
4027
+ missing.push(where2);
3966
4028
  continue;
3967
4029
  }
3968
4030
  for (const component of report.components) {
3969
- if (component.notes.some((note) => /not installed|loads no stylesheet|drawn from (?:its|their) (?:tag and )?classes|did not render|bundle failed|unstyled|stub fallback/i.test(note))) {
4031
+ if (component.notes.some((note) => UNDRAWABLE.test(note))) {
3970
4032
  missing.push(component.component);
3971
4033
  continue;
3972
4034
  }
@@ -3982,7 +4044,7 @@ async function addLooks(model, repoRoot, options = {}) {
3982
4044
  drawn.push(component.component);
3983
4045
  }
3984
4046
  } catch (error) {
3985
- problems.push(`${where}: ${error instanceof Error ? error.message : String(error)}`);
4047
+ problems.push(`${where2}: ${error instanceof Error ? error.message : String(error)}`);
3986
4048
  if (error instanceof RenderUnavailableError) break;
3987
4049
  }
3988
4050
  }
@@ -3996,7 +4058,6 @@ function looksOff(model) {
3996
4058
  }
3997
4059
 
3998
4060
  // ../cli/src/report/markdown.ts
3999
- var STEPS_SHOWN3 = 5;
4000
4061
  var md = (text) => text.replace(/([\\`*_[\]<>|#])/g, "\\$1");
4001
4062
  function renderMarkdown(model, reportFile) {
4002
4063
  const t = model.totals;
@@ -4017,8 +4078,8 @@ function renderMarkdown(model, reportFile) {
4017
4078
  model.topFixes.forEach((fix, i) => {
4018
4079
  lines.push(`${i + 1}. ${md(fix.title)}${model.projects.length > 1 && fix.project !== "." ? ` (${md(fix.project)})` : ""} `);
4019
4080
  if (fix.lead && fix.steps && fix.steps.length > 0) {
4020
- lines.push(` Next: ${md(fix.lead)}.`, ...fix.steps.slice(0, STEPS_SHOWN3).map((step) => ` - ${md(step.replace(/\.$/, ""))}`));
4021
- if (fix.steps.length > STEPS_SHOWN3) lines.push(` - and ${fix.steps.length - STEPS_SHOWN3} more ${fix.steps.length - STEPS_SHOWN3 === 1 ? "file" : "files"}, listed in the report`);
4081
+ lines.push(` Next: ${md(fix.lead)}.`, ...fix.steps.slice(0, STEPS_SHOWN).map((step) => ` - ${md(step.replace(/\.$/, ""))}`));
4082
+ if (fix.steps.length > STEPS_SHOWN) lines.push(` - and ${fix.steps.length - STEPS_SHOWN} more ${fix.steps.length - STEPS_SHOWN === 1 ? "file" : "files"}, listed in the report`);
4022
4083
  } else lines.push(` Next: ${md(fix.next.replace(/\.$/, ""))}.`);
4023
4084
  });
4024
4085
  lines.push("");
@@ -4176,7 +4237,7 @@ async function writeBinaryAtomic(file, bytes) {
4176
4237
  await rename2(temp, file);
4177
4238
  }
4178
4239
  var shown = (file) => {
4179
- const rel = relative8(process.cwd(), file);
4240
+ const rel = relative7(process.cwd(), file);
4180
4241
  return rel && !rel.startsWith("..") ? rel : file;
4181
4242
  };
4182
4243
  async function runReport(options = {}, io = {}) {
@@ -4233,8 +4294,8 @@ async function runReport(options = {}, io = {}) {
4233
4294
  print2(JSON.stringify({ ...clean, projects: clean.projects.map((p) => ({ ...p, rendered: p.rendered.map(({ light: _l, dark: _d, ...r }) => r) })), files }, null, 2));
4234
4295
  return { code: 0, files, model: clean };
4235
4296
  }
4236
- const width = Math.max(...["Report", "Card", "Summary"].map((w) => w.length)) + 2;
4237
- const looks = clean.render.state === "drawn" ? "" : clean.render.state === "off" && !renderNote ? `; to add rendered looks, run ${NPX} --render` : "";
4297
+ const width = 9;
4298
+ const looks = clean.render.state === "off" && !renderNote ? `; to add rendered looks, run ${NPX} --render` : "";
4238
4299
  print2([
4239
4300
  ...clean.summary,
4240
4301
  "",
@@ -4276,6 +4337,22 @@ ${style.bold("Anywhere")} ${style.dim("no design system needed")}
4276
4337
  <first project>/.youi/inventory.json unless --out, and records the scan
4277
4338
  (project, commit, time, counts, families) in the local index for trends
4278
4339
  inventory --history every recorded scan per project, newest first, with what changed
4340
+ gaps [--project <dir>\u2026] [--inventory <file>] [--json] [--out <file>]
4341
+ per project, each kind of element: in the System, from a library, built
4342
+ by hand or missing from the standard kit, with licensed candidates from
4343
+ the Discover index, and what is built by hand in more than one project
4344
+ requests [--status open|addressed|closed|all] [--route r] [--json]
4345
+ comments and change requests people pinned to components on captured
4346
+ screens: what is asked, the file:line, the picture, and what to do next
4347
+ requests show <id> | reply <id> --message <text> | addressed <id> --message <what you changed>
4348
+ read one, reply, or mark it done (as an agent; a person closes it in the hub)
4349
+ screens capture --base <url> [--route r] [--project dir] [--out dir] [--storage-state file] [--json]
4350
+ photograph the routes in <project>/.youi/screens/screens.json, light and
4351
+ dark, from the app running in development at <url>, and place each
4352
+ component on them with the line that rendered it (for youiyoux ui)
4353
+ screens login --base <url> [--project dir] [--storage-state file]
4354
+ open the app in a browser window to sign in once; capture then uses the
4355
+ session (kept outside the product, readable only by you)
4279
4356
  render [file|component\u2026] [--base ref] [--json]
4280
4357
  see a component you changed: its looks drawn with the project's own code
4281
4358
  and CSS in headless Chromium, light and dark at 1280 and 390 px, axe's
@@ -4347,6 +4424,9 @@ ${style.bold("Discover")} ${style.dim("UI components by need, across shadcn reg
4347
4424
  and facets (role, source, licence, headless/styled); --all also lists
4348
4425
  every other registry, kept apart from the default search
4349
4426
  discover show <id|ref> [--source] [--json] one candidate: provenance, licence evidence, dependencies
4427
+ discover previews [--port n] [--registry @shadcn|all] [--role r] [--limit n] [--force]
4428
+ photograph candidates in your theme for the hub's gallery, from a hub
4429
+ running on your project (youiyoux ui --project <dir>)
4350
4430
  (cache: --cache, else YOUI_DISCOVER_CACHE, else ~/.cache/youiyoux/discover)
4351
4431
 
4352
4432
  ${style.bold("Index")} ${style.dim("the local store of derived data: rebuildable, never the authority")}
@@ -4439,6 +4519,10 @@ var OPTIONS = {
4439
4519
  license: { type: "string" },
4440
4520
  icons: { type: "boolean" },
4441
4521
  source: { type: "boolean" },
4522
+ registry: { type: "string" },
4523
+ "storage-state": { type: "string" },
4524
+ message: { type: "string" },
4525
+ status: { type: "string" },
4442
4526
  "max-registries": { type: "string" },
4443
4527
  offset: { type: "string" },
4444
4528
  history: { type: "boolean", default: false },
@@ -4450,7 +4534,7 @@ var OPTIONS = {
4450
4534
  var INVENTORY_OPTIONS = { ...OPTIONS, project: { type: "string", multiple: true }, include: { type: "string", multiple: true }, exclude: { type: "string", multiple: true } };
4451
4535
  var UsageError = class extends Error {
4452
4536
  };
4453
- var run7 = promisify6(execFile6);
4537
+ var run6 = promisify5(execFile5);
4454
4538
  var print = (text) => void process.stdout.write(`${text}
4455
4539
  `);
4456
4540
  var json = (value) => print(JSON.stringify(value, null, 2));
@@ -4482,23 +4566,44 @@ async function recordUsage(values, p, report, usage) {
4482
4566
  await noteSource(home, { project: { root: resolve12(values.project ?? process.cwd()), options: Object.fromEntries(Object.entries(loadOptions(values)).map(([key2, value]) => [key2, resolve12(value)])), usage } });
4483
4567
  return null;
4484
4568
  } catch (error) {
4485
- return style.warn(`! not recorded in the local index: ${error instanceof Error ? error.message : String(error)}`);
4569
+ return notRecorded(error);
4486
4570
  }
4487
4571
  }
4572
+ var notRecorded = (error) => style.warn(`! not recorded in the local index: ${error instanceof Error ? error.message : String(error)}`);
4573
+ var inventoryDirs = (values) => values.project?.length ? values.project : [process.cwd()];
4574
+ var scanInventory = (values, dirs) => buildInventory(dirs, { ...values.include ? { include: values.include } : {}, ...values.exclude ? { exclude: values.exclude } : {} });
4488
4575
  function indexOf(values) {
4489
4576
  const path = discoverIndexPath(values.cache);
4490
4577
  return { path, home: indexHome(path) };
4491
4578
  }
4579
+ async function cmdGaps(values) {
4580
+ const dirs = inventoryDirs(values);
4581
+ const inventory = values.inventory ? InventoryReport.parse(JSON.parse(await readFile8(resolve12(values.inventory), "utf8"))) : await scanInventory(values, dirs);
4582
+ const cwd = values.inventory ? dirname11(resolve12(values.inventory)) : process.cwd();
4583
+ const project2 = await loadProject(resolve12(dirs[0]), loadOptions(values)).catch(() => null);
4584
+ const system = project2 ? { name: project2.system.config.name, components: project2.system.components.map((c) => c.contract.name) } : null;
4585
+ const store = await readCatalog(resolve12(discoverCacheDir(values.cache)), indexOf(values).path).catch((error) => {
4586
+ if (error instanceof MissingIndexError) return null;
4587
+ throw error;
4588
+ });
4589
+ const roots = new Map(inventory.projects.map((p) => [p.id, resolve12(cwd, p.root)]));
4590
+ const report = await buildGaps({ inventory, system, dependencies: declaredPackages(roots) }, store);
4591
+ if (values.out) await writeFileAtomic(resolve12(values.out), `${JSON.stringify(report, null, 2)}
4592
+ `);
4593
+ if (values.json) json(report);
4594
+ else print(renderGaps(report));
4595
+ return 0;
4596
+ }
4492
4597
  async function cmdInventory(values) {
4493
4598
  if (values.history) return cmdHistory(values, []);
4494
- const dirs = values.project?.length ? values.project : [process.cwd()];
4495
- const report = await buildInventory(dirs, { ...values.include ? { include: values.include } : {}, ...values.exclude ? { exclude: values.exclude } : {} });
4599
+ const dirs = inventoryDirs(values);
4600
+ const report = await scanInventory(values, dirs);
4496
4601
  const out = resolve12(values.out ?? join22(dirs[0], ".youi/inventory.json"));
4497
4602
  await writeFileAtomic(out, `${JSON.stringify(report, null, 2)}
4498
4603
  `);
4499
4604
  const recorded = await recordScan(values, report, out);
4500
4605
  if (values.json) json(report);
4501
- else print([renderInventory(report, relative9(process.cwd(), out).startsWith("..") ? out : relative9(process.cwd(), out)), recorded].join("\n"));
4606
+ else print([renderInventory(report, relative8(process.cwd(), out).startsWith("..") ? out : relative8(process.cwd(), out)), recorded].join("\n"));
4502
4607
  return 0;
4503
4608
  }
4504
4609
  async function recordScan(values, report, out) {
@@ -4509,7 +4614,7 @@ async function recordScan(values, report, out) {
4509
4614
  const scans = (await store.history({ limit: 500 })).filter((s) => report.projects.some((p) => p.id === s.project)).length;
4510
4615
  return style.dim(`history \u2192 ${store.location} (${plural(scans, "scan")} of these projects; youiyoux inventory --history)`);
4511
4616
  } catch (error) {
4512
- return style.warn(`! not recorded in the local index: ${error instanceof Error ? error.message : String(error)}`);
4617
+ return notRecorded(error);
4513
4618
  }
4514
4619
  }
4515
4620
  async function cmdHistory(values, positionals) {
@@ -4548,9 +4653,9 @@ async function cmdWhere(values, positionals) {
4548
4653
  const p = await project(values);
4549
4654
  const { report } = await scanProject(p);
4550
4655
  const limit = values.all ? Number.MAX_SAFE_INTEGER : Number(values.limit ?? (values.json ? 20 : 24));
4551
- const where = whereUsed(p, report, query, { ...values.route ? { route: values.route } : {}, limit });
4552
- if (values.json) json(where);
4553
- else print(renderWhere(where, report, where.sites));
4656
+ const where2 = whereUsed(p, report, query, { ...values.route ? { route: values.route } : {}, limit });
4657
+ if (values.json) json(where2);
4658
+ else print(renderWhere(where2, report));
4554
4659
  return 0;
4555
4660
  }
4556
4661
  async function cmdShow(values, positionals) {
@@ -4577,9 +4682,10 @@ function baselineFile(values, p) {
4577
4682
  if (values["baseline-file"]) return resolve12(values["baseline-file"]);
4578
4683
  return values.baseline ? baselinePath(p, values.config) : null;
4579
4684
  }
4685
+ var checkScope = (values, positionals) => ({ ...positionals.length > 0 ? { files: positionals } : {}, ...values.rule ? { rule: values.rule } : {}, quiet: values.quiet });
4580
4686
  async function repoPrefix(root) {
4581
4687
  try {
4582
- return (await run7("git", ["rev-parse", "--show-prefix"], { cwd: root })).stdout.trim();
4688
+ return (await run6("git", ["rev-parse", "--show-prefix"], { cwd: root })).stdout.trim();
4583
4689
  } catch {
4584
4690
  return "";
4585
4691
  }
@@ -4589,7 +4695,7 @@ async function cmdCheck(values, positionals) {
4589
4695
  if (!["text", "json", "github"].includes(format)) throw new UsageError("youiyoux check --format text|json|github");
4590
4696
  if (values.diff || values.base !== void 0) return cmdCheckDiff(values, positionals, await loadCheckProject(values.project ?? process.cwd(), loadOptions(values), !values.project), format);
4591
4697
  const p = await project(values);
4592
- const result = await runCheck(p, { ...positionals.length > 0 ? { files: positionals } : {}, ...values.rule ? { rule: values.rule } : {}, quiet: values.quiet, baseline: baselineFile(values, p) });
4698
+ const result = await runCheck(p, { ...checkScope(values, positionals), baseline: baselineFile(values, p) });
4593
4699
  if (format === "json") {
4594
4700
  const { report } = result.scan;
4595
4701
  json({ errors: result.errors, incomplete: result.incomplete, identity: result.identity, unmatched: result.unmatched, notRun: result.notRun, violations: result.violations, baseline: result.baseline, lock: result.lock, suppressed: report.suppressed, coverage: report.coverage, status: report.status, checks: report.checks });
@@ -4603,7 +4709,7 @@ async function cmdCheck(values, positionals) {
4603
4709
  async function cmdCheckDiff(values, positionals, p, format) {
4604
4710
  if (values.baseline) throw new UsageError("youiyoux check --diff looks only at added lines, so a baseline does not apply; drop --baseline");
4605
4711
  const change = await workingTreeChange(p, values.base);
4606
- const result = configured(p) ? await runCheck(p, { ...positionals.length > 0 ? { files: positionals } : {}, ...values.rule ? { rule: values.rule } : {}, quiet: values.quiet, diff: change }) : await runRepoCheck(p, change);
4712
+ const result = configured(p) ? await runCheck(p, { ...checkScope(values, positionals), diff: change }) : await runRepoCheck(p, change);
4607
4713
  if (format === "json") json(diffJson(result));
4608
4714
  else if (format === "github") {
4609
4715
  for (const line of githubDiffAnnotations(result, await repoPrefix(p.root))) print(line);
@@ -4630,7 +4736,7 @@ async function cmdBaseline(values) {
4630
4736
  const { totals } = written.baseline;
4631
4737
  if (values.json) json({ file: written.file, action: written.action, totals, notAdded: written.notAdded, identity: written.baseline.identity });
4632
4738
  else {
4633
- print(`${style.ok(written.action)} ${relative9(process.cwd(), file) || file}: ${plural(totals.errors, "error")} \xB7 ${plural(totals.warnings, "warning")} accepted`);
4739
+ print(`${style.ok(written.action)} ${relative8(process.cwd(), file) || file}: ${plural(totals.errors, "error")} \xB7 ${plural(totals.warnings, "warning")} accepted`);
4634
4740
  print(style.dim(` rules ${describeIdentity(written.baseline.identity)}`));
4635
4741
  if (written.notAdded > 0) print(style.warn(` ${plural(written.notAdded, "current violation")} not added: fix them, or accept them deliberately with --force`));
4636
4742
  }
@@ -4794,7 +4900,7 @@ async function cmdAgents(values, positionals) {
4794
4900
  const repoRoot = await repoRootOf(projectRoot);
4795
4901
  const target = { project: projectRoot, ...values.config ? { config: resolve12(values.config) } : {}, ...values.design ? { design: resolve12(values.design) } : {}, ...values.root !== void 0 ? { root: resolve12(values.root || ".") } : {} };
4796
4902
  const server = mcpCommand(TOOL_ROOT, target);
4797
- const system = existsSync4(resolve12(values.config ?? join22(projectRoot, PROJECT_FILE))) ? await project(values) : null;
4903
+ const system = existsSync5(resolve12(values.config ?? join22(projectRoot, PROJECT_FILE))) ? await project(values) : null;
4798
4904
  const claudeServer = system && system.root === repoRoot ? { command: "node", args: mcpArgs(system) } : relativeTo(repoRoot, server);
4799
4905
  const both = !values.claude && !values.codex;
4800
4906
  const results = [];
@@ -4852,10 +4958,10 @@ async function startEditor(values, port) {
4852
4958
  if (!values.project) return null;
4853
4959
  try {
4854
4960
  const editor = await startRuleEditor({ project: values.project, ...values.config ? { config: values.config } : {}, ...values.design ? { design: values.design } : {}, ...values.root ? { root: values.root } : {}, ...values.state ? { state: values.state } : {} });
4855
- const { design, project: product } = editor.info;
4961
+ const { design, project: product2 } = editor.info;
4856
4962
  print(`${style.ok("rule editor")} http://localhost:${port}/#key=${editor.sessionKey}`);
4857
4963
  print(style.dim(` design ${design.root} (${design.branch} @ ${design.head.slice(0, 7)}${design.dirty ? ", working tree differs" : ""})`));
4858
- print(style.dim(` project ${product.id} at ${product.root}`));
4964
+ print(style.dim(` project ${product2.id} at ${product2.root}`));
4859
4965
  print(style.dim(` drafts ${editor.stateDir}`));
4860
4966
  print(style.dim(" open the link above: it carries this run's key, which every change needs (nothing else can approve)"));
4861
4967
  print(style.dim(" approved changes become a local review branch; nothing is pushed"));
@@ -4877,20 +4983,20 @@ async function cmdUi(values) {
4877
4983
  async function cmdDetect(values, positionals) {
4878
4984
  const target = detectTarget(positionals);
4879
4985
  if (!target) throw new UsageError("youiyoux detect <repo-path> or youiyoux detect <url\u2026> [--expose] (one repo, or one or more URLs; not both)");
4880
- const { repoReport, siteReport, writeReport } = await import("./src-VEHGMKYV.js");
4986
+ const { repoReport, siteReport, writeReport } = await import("./src-VL2ZYO6X.js");
4881
4987
  const out = resolve12(values.out ?? ".youi/detect");
4882
4988
  const report = target.kind === "repo" ? await repoReport(target.path) : await siteReport(target.urls, { out, expose: values.expose, ...process.env.YOUI_CHROMIUM ? { executablePath: process.env.YOUI_CHROMIUM } : {} });
4883
4989
  const file = await writeReport(report, out);
4884
4990
  if (values.json) json(report);
4885
4991
  else {
4886
- const shown2 = relative9(process.cwd(), file);
4992
+ const shown2 = relative8(process.cwd(), file);
4887
4993
  print(renderDetect(report, shown2 && !shown2.startsWith("..") ? shown2 : file));
4888
4994
  }
4889
4995
  return 0;
4890
4996
  }
4891
4997
  async function cmdRender(values, positionals, io) {
4892
4998
  const root = resolve12(values.root ?? values.project ?? process.cwd());
4893
- const targets = positionals.map((t) => existsSync4(resolve12(t)) ? resolve12(t) : t);
4999
+ const targets = positionals.map((t) => existsSync5(resolve12(t)) ? resolve12(t) : t);
4894
5000
  try {
4895
5001
  const report = await renderShots(root, targets, { ...values.base ? { base: values.base } : {}, ...process.env.YOUI_CHROMIUM ? { executablePath: process.env.YOUI_CHROMIUM } : {}, ...io.render });
4896
5002
  if (values.json) json({ ...report, next: shotsNext(report, (t) => `run youiyoux render${t ? ` ${t}` : ""} again`) });
@@ -4903,13 +5009,114 @@ async function cmdRender(values, positionals, io) {
4903
5009
  return 1;
4904
5010
  }
4905
5011
  }
5012
+ async function product(values) {
5013
+ const project2 = await loadProject(resolve12(values.project ?? process.cwd()), loadOptions(values)).catch(() => null);
5014
+ return { project: project2, root: project2?.root ?? resolve12(values.root ?? values.project ?? process.cwd()) };
5015
+ }
5016
+ async function cmdRequests(values, positionals) {
5017
+ const usage = "youiyoux requests [--status open|addressed|closed|all] [--route r] or youiyoux requests show <id> | reply <id> --message <text> | addressed <id> --message <what you changed>";
5018
+ const [action, id, ...rest] = positionals;
5019
+ if (rest.length > 0) throw new UsageError(usage);
5020
+ const { RequestStore, requestBrief } = await import("./src-6CJR547Y.js");
5021
+ const { project: project2, root } = await product(values);
5022
+ const store = new RequestStore(root, project2?.config.id ?? basename3(root));
5023
+ const agent = { kind: "agent", name: "youiyoux CLI" };
5024
+ const one = (request) => {
5025
+ const brief = requestBrief(request, root);
5026
+ if (values.json) json(brief);
5027
+ else print(renderBrief(brief, true));
5028
+ return 0;
5029
+ };
5030
+ if (!action || action === "list") {
5031
+ if (id) throw new UsageError(usage);
5032
+ const status = values.status ?? "open";
5033
+ if (!["open", "addressed", "closed", "all"].includes(status)) throw new UsageError("--status takes open, addressed, closed or all");
5034
+ const { requests, unreadable } = await store.list();
5035
+ const briefs = requests.filter((r) => (status === "all" || r.status === status) && (!values.route || r.target.route === values.route)).map((r) => requestBrief(r, root));
5036
+ if (values.json) json({ requests: briefs, unreadable });
5037
+ else print(renderRequests(briefs, status, unreadable));
5038
+ return unreadable.length ? 1 : 0;
5039
+ }
5040
+ if (!id) throw new UsageError(usage);
5041
+ if (action === "show") return one(await store.get(id));
5042
+ if (action === "reply" || action === "addressed") {
5043
+ if (!values.message?.trim()) throw new UsageError(`youiyoux requests ${action} ${id} --message <${action === "reply" ? "text" : "what you changed, with file:line"}>`);
5044
+ return one(action === "reply" ? await store.reply(agent, id, values.message) : await store.setStatus(agent, id, "addressed", values.message));
5045
+ }
5046
+ throw new UsageError(usage);
5047
+ }
5048
+ async function cmdScreens(values, positionals, io) {
5049
+ const usage = "youiyoux screens capture --base <url of the app in development> [--route <one route>] [--project dir] [--out <screens dir>] [--storage-state <signed-in session>] [--json] or youiyoux screens login --base <url> [--project dir] [--storage-state <file>]";
5050
+ if (!["capture", "login"].includes(positionals[0] ?? "") || positionals.length > 1 || !values.base) throw new UsageError(usage);
5051
+ const { root } = await product(values);
5052
+ if (positionals[0] === "login") return screensLogin(values.base, values["storage-state"] ? resolve12(values["storage-state"]) : sessionFile(root), io);
5053
+ const dir = resolve12(values.out ?? join22(root, ".youi/screens"));
5054
+ if (!existsSync5(join22(dir, "screens.json"))) {
5055
+ process.stderr.write(`${style.error("\u2716")} no screens.json in ${dir}: list the routes to capture there, as {"schema":"youiyoux.screens/v1","project":"<name>","source":"<what the app is>","capturedAt":"","viewport":{"width":1440,"height":900},"screens":[{"route":"/","path":"/"}]}
5056
+ `);
5057
+ return 1;
5058
+ }
5059
+ const session = chosenSession(root, values["storage-state"] ? resolve12(values["storage-state"]) : void 0);
5060
+ if (session?.saved) process.stderr.write(`${style.dim(`signed in with the session youiyoux screens login saved (${session.file}); delete it to capture signed out`)}
5061
+ `);
5062
+ const { launchBrowser } = await import("./src-VL2ZYO6X.js");
5063
+ const browser = await launchBrowser(process.env.YOUI_CHROMIUM, { purpose: { what: "youiyoux screens capture", command: "youiyoux screens capture" } });
5064
+ try {
5065
+ const report = await captureScreens(dir, { base: values.base, root, browser, ...values.route ? { routes: [values.route] } : {}, ...session ? { storageState: session.file } : {}, log: (line) => process.stderr.write(`${style.dim(line)}
5066
+ `) });
5067
+ if (values.json) json(report);
5068
+ else print(captureText(report));
5069
+ return report.screens.every((s) => s.error) ? 1 : 0;
5070
+ } finally {
5071
+ await browser.close();
5072
+ }
5073
+ }
5074
+ async function screensLogin(base, file, io) {
5075
+ const login = io.login ?? {};
5076
+ if (!login.browser && !process.stdin.isTTY) {
5077
+ process.stderr.write(`${style.error("\u2716")} youiyoux screens login waits for you to sign in and press Enter: run it in a terminal
5078
+ `);
5079
+ return 1;
5080
+ }
5081
+ let browser;
5082
+ try {
5083
+ browser = login.browser ?? await openLoginBrowser(process.env.YOUI_CHROMIUM);
5084
+ } catch (error) {
5085
+ process.stderr.write(`${style.error("\u2716")} youiyoux screens login opens a browser window to sign in with, and could not: ${(error instanceof Error ? error.message : String(error)).split("\n")[0]}. It needs Google Chrome installed, or YOUI_CHROMIUM set to a Chromium.
5086
+ `);
5087
+ return 1;
5088
+ }
5089
+ try {
5090
+ print(`Sign in to ${base} in the window that opened, then press Enter here (or type q and Enter to give up).`);
5091
+ const signedIn = login.signedIn ?? (async (page) => {
5092
+ const { createInterface } = await import("node:readline/promises");
5093
+ const prompt = createInterface({ input: process.stdin, output: process.stdout });
5094
+ const closed = new Promise((done) => page.once("close", () => done("closed")));
5095
+ try {
5096
+ return (await Promise.race([prompt.question(""), closed])).trim().toLowerCase() !== "q";
5097
+ } finally {
5098
+ prompt.close();
5099
+ }
5100
+ });
5101
+ const result = await recordSession({ base, file, browser, signedIn });
5102
+ if (!result.file) {
5103
+ print("Nothing was saved: the window was closed, or you gave up. Run youiyoux screens login again to sign in.");
5104
+ return 1;
5105
+ }
5106
+ print(`Saved the session (${result.cookies} cookies, storage for ${result.origins} sites) to ${result.file}, readable only by you: it holds your sign-in, so it is kept outside the product.`);
5107
+ print("Next: youiyoux screens capture --base <url> uses it from now on; delete the file to capture signed out.");
5108
+ return 0;
5109
+ } finally {
5110
+ if (!login.browser) await browser.close();
5111
+ }
5112
+ }
4906
5113
  async function cmdReport(values, positionals, io) {
4907
5114
  if (positionals.length > 1) throw new UsageError("youiyoux report [path] [--out dir] [--render [--yes]] [--include-code] [--json] (one repo)");
4908
5115
  const result = await runReport({ ...positionals[0] ? { path: positionals[0] } : {}, ...values.out ? { out: values.out } : {}, render: values.render, yes: values.yes, includeCode: values["include-code"], json: values.json }, io.report ?? {});
4909
5116
  return result.code;
4910
5117
  }
4911
5118
  async function cmdMcp(values) {
4912
- const { startStdioServer } = await import("./src-WQWYJ6IQ.js");
5119
+ const { startStdioServer } = await import("./src-BPETAFLW.js");
4913
5120
  await startStdioServer({ projectRoot: values.project ?? process.cwd(), ...loadOptions(values), ...values.cache ? { discoverCache: values.cache } : {} });
4914
5121
  return -1;
4915
5122
  }
@@ -4941,6 +5148,11 @@ async function cmdDiscover(values, positionals, io) {
4941
5148
  ${style.dim(`store \u2192 ${storeLocation({ path })} (${plural(loaded.chosen, "listing")})`)}`);
4942
5149
  return 0;
4943
5150
  }
5151
+ if (action === "previews") {
5152
+ if (rest.length > 0) throw new UsageError("youiyoux discover previews [--port n] [--registry @shadcn|all] [--role r] [--limit n] [--force]");
5153
+ positiveInt(values.limit, "--limit");
5154
+ return runPreviews(TOOL_ROOT, { ...values.port ? { port: values.port } : {}, ...values.registry ? { registry: values.registry } : {}, ...values.role ? { role: values.role } : {}, ...values.limit ? { limit: values.limit } : {}, ...values.cache ? { cache: values.cache } : {}, force: values.force });
5155
+ }
4944
5156
  if (action === "show") {
4945
5157
  if (!rest[0]) throw new UsageError("youiyoux discover show <id|ref>");
4946
5158
  if (values.source) {
@@ -4987,18 +5199,20 @@ var COMMANDS = {
4987
5199
  index: cmdIndex,
4988
5200
  report: cmdReport,
4989
5201
  render: cmdRender,
5202
+ screens: cmdScreens,
5203
+ requests: cmdRequests,
4990
5204
  detect: cmdDetect,
4991
5205
  build: cmdBuild,
4992
5206
  parity: cmdParity,
4993
5207
  schema: cmdSchema
4994
5208
  };
4995
- var COMMAND_NAMES = [...Object.keys(COMMANDS), "inventory", "help"];
5209
+ var COMMAND_NAMES = [...Object.keys(COMMANDS), "inventory", "gaps", "help"];
4996
5210
  async function main(argv, io = {}) {
4997
5211
  let parsed;
4998
5212
  try {
4999
- if (argv[0] === "inventory") {
5213
+ if (argv[0] === "inventory" || argv[0] === "gaps") {
5000
5214
  const inventory = parseArgs({ args: argv, options: INVENTORY_OPTIONS, allowPositionals: true });
5001
- if (!inventory.values.help) return await guarded(() => cmdInventory(inventory.values));
5215
+ if (!inventory.values.help) return await guarded(() => argv[0] === "gaps" ? cmdGaps(inventory.values) : cmdInventory(inventory.values));
5002
5216
  }
5003
5217
  parsed = parseArgs({ args: diffArgs(argv), options: OPTIONS, allowPositionals: true });
5004
5218
  } catch (error) {