youiyoux 0.1.3 → 0.1.5

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 (211) 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-74KBRI2H.js → chunk-4BBYLTZX.js} +10 -10
  4. package/dist/chunks/{chunk-RTOTNSOV.js → chunk-5QRDHWK5.js} +600 -506
  5. package/dist/chunks/{chunk-OIHNEGWH.js → chunk-BSJLO2LJ.js} +96 -142
  6. package/dist/chunks/chunk-FVVL5JCY.js +24 -0
  7. package/dist/chunks/{chunk-UEB6MJDX.js → chunk-M5XZWCPS.js} +176 -61
  8. package/dist/chunks/{chunk-C3TSTYP7.js → chunk-NYDJYMID.js} +212 -26
  9. package/dist/chunks/chunk-O6QTQCTC.js +129 -0
  10. package/dist/chunks/chunk-VG3ZCSST.js +8720 -0
  11. package/dist/chunks/chunk-VM6BGPRI.js +188 -0
  12. package/dist/chunks/{chunk-E4HNK7VF.js → chunk-WNTS7ZO4.js} +1 -1
  13. package/dist/chunks/chunk-YJ7WMBTT.js +13761 -0
  14. package/dist/chunks/{src-U5NTDPUX.js → src-34R7QMJC.js} +11 -35
  15. package/dist/chunks/src-5IP4NGKT.js +115 -0
  16. package/dist/chunks/{src-YGEZCUAO.js → src-E25GHWWT.js} +134 -77
  17. package/dist/chunks/src-RHNZELGE.js +306 -0
  18. package/dist/chunks/{src-YUC5PIDK.js → src-SFKFERZ2.js} +14 -8
  19. package/dist/chunks/{src-ZS76UD7E.js → src-SZHEV4YC.js} +27 -16
  20. package/dist/chunks/{src-DS43LCRA.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/tailwind-worker.js +3 -3
  208. package/dist/youi.js +10 -8
  209. package/dist/youiyoux.js +10 -8
  210. package/package.json +4 -4
  211. package/dist/chunks/chunk-L5BV4PI7.js +0 -22159
@@ -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-74KBRI2H.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-UEB6MJDX.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,15 +9,13 @@ import {
40
9
  adoptionRequest,
41
10
  buildInventory,
42
11
  buildTheme,
43
- buildTokens,
44
12
  captureScreens,
45
13
  captureText,
46
14
  checkCapabilities,
47
- compareParity,
15
+ chosenSession,
48
16
  compareToBaseline,
49
17
  componentCard,
50
18
  createBaseline,
51
- cssValue,
52
19
  describeIdentity,
53
20
  evaluateRule,
54
21
  failedChecks,
@@ -62,26 +29,68 @@ import {
62
29
  loadProject,
63
30
  nearDuplicates,
64
31
  offLibraryImports,
65
- parseCssColor,
32
+ openLoginBrowser,
66
33
  parseModule,
67
34
  planChange,
68
35
  ratchetBaseline,
69
36
  readStylesheet,
37
+ recordSession,
70
38
  renderShots,
71
39
  ruleCapability,
72
40
  runMigration,
73
41
  scanProject,
74
42
  screenBom,
75
43
  searchLibrary,
44
+ sessionFile,
76
45
  shotsNext,
77
46
  shotsText,
78
47
  substituteVars,
79
48
  vendoredPath,
80
49
  whereUsed
81
- } from "./chunk-L5BV4PI7.js";
50
+ } from "./chunk-VG3ZCSST.js";
82
51
  import {
83
52
  within
84
- } 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";
85
94
  import {
86
95
  Baseline,
87
96
  DefinitionError,
@@ -106,48 +115,27 @@ import {
106
115
  relPath,
107
116
  renderJsonSchemas,
108
117
  require_dist,
118
+ ruleSlug,
109
119
  safeId,
110
120
  sha256,
111
121
  shortHash,
122
+ toPosix,
112
123
  writeFileAtomic
113
- } from "./chunk-C3TSTYP7.js";
124
+ } from "./chunk-NYDJYMID.js";
114
125
  import {
115
126
  __toESM
116
- } from "./chunk-E4HNK7VF.js";
117
-
118
- // src/node20.js
119
- if (!Map.groupBy) {
120
- Map.groupBy = (items, key2) => {
121
- const groups = /* @__PURE__ */ new Map();
122
- let i = 0;
123
- for (const item of items) {
124
- const k = key2(item, i++);
125
- const list = groups.get(k);
126
- if (list) list.push(item);
127
- else groups.set(k, [item]);
128
- }
129
- return groups;
130
- };
131
- }
132
- if (!Object.groupBy) Object.groupBy = (items, key2) => Object.fromEntries(Map.groupBy(items, key2));
133
- if (!Promise.withResolvers) {
134
- Promise.withResolvers = () => {
135
- let resolve13, reject;
136
- const promise = new Promise((yes, no) => (resolve13 = yes, reject = no));
137
- return { promise, resolve: resolve13, reject };
138
- };
139
- }
127
+ } from "./chunk-WNTS7ZO4.js";
140
128
 
141
129
  // ../cli/src/main.ts
142
130
  var import_yaml3 = __toESM(require_dist(), 1);
143
- import { execFile as execFile6 } from "node:child_process";
144
- import { existsSync as existsSync4 } from "node:fs";
131
+ import { execFile as execFile5 } from "node:child_process";
132
+ import { existsSync as existsSync5 } from "node:fs";
145
133
  import { appendFile as appendFile2, readFile as readFile8 } from "node:fs/promises";
146
- import { basename as basename3, join as join22, relative as relative9, resolve as resolve12 } from "node:path";
147
- 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";
148
136
 
149
137
  // ../cli/src/check.ts
150
- 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";
151
139
 
152
140
  // ../cli/src/check-diff.ts
153
141
  import { execFile } from "node:child_process";
@@ -161,7 +149,7 @@ import { join as join2, relative, resolve } from "node:path";
161
149
 
162
150
  // ../cli/src/command.ts
163
151
  var COMMAND = "youiyoux";
164
- 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", "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"];
165
153
 
166
154
  // ../cli/src/agents.ts
167
155
  var import_yaml = __toESM(require_dist(), 1);
@@ -191,10 +179,9 @@ function viaNpx(text, root) {
191
179
  return text.replace(BARE, `${portableYoui(root).run} `);
192
180
  }
193
181
  function diffInstruction(root) {
194
- const { run: run8, setup } = portableYoui(root);
195
- 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.`;
196
184
  }
197
- var DIFF_INSTRUCTION = diffInstruction();
198
185
  function repoAgentsBlock(root) {
199
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);
200
187
  }
@@ -234,7 +221,7 @@ function agentsBlock(project2) {
234
221
  `- Never edit files marked \`${GENERATED_MARK}\` (\`${config.outputs.dir}/*\`, ${config.agents.designMd}, the youiyoux skill). Change the design language and run \`youiyoux sync\`.`,
235
222
  `- Owned component files in \`${config.componentsDir}\` may be edited; \`youiyoux sync\` 3-way merges upstream changes into them and reports drift.`,
236
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.",
237
- "- 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 (`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.",
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.",
238
225
  "- Before you finish, run `youiyoux check`; it must report 0 errors.",
239
226
  diffInstruction(project2.root),
240
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.",
@@ -302,7 +289,7 @@ function frontmatter(project2, build) {
302
289
  colors,
303
290
  typography,
304
291
  rounded: dims("radius"),
305
- spacing: Object.fromEntries(Object.entries(dims("space"))),
292
+ spacing: dims("space"),
306
293
  components
307
294
  };
308
295
  }
@@ -346,7 +333,7 @@ function skillMd(project2, uncontracted = []) {
346
333
  const from = importHint(config);
347
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.`;
348
335
  const components = system.components.map(({ contract }) => `- **${contract.name}**: ${contract.summary}${enumSummary(contract) ? ` (${enumSummary(contract)})` : ""}`);
349
- 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} |`);
350
337
  return viaNpx([
351
338
  "---",
352
339
  "name: youiyoux",
@@ -412,22 +399,15 @@ function astGrepRules(project2) {
412
399
  # Portable subset: youiyoux check also resolves imports; these run with plain \`ast-grep scan -r <file>\`.
413
400
  `;
414
401
  for (const rule2 of enforced(project2.system.rules)) {
415
- if (rule2.check.engine === "ast-grep") {
416
- 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 }) });
417
- }
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 }));
418
404
  if (rule2.check.engine === "youi" && rule2.check.id === "native-element") {
419
405
  const tags = Object.keys(rule2.check.options.elements ?? {});
420
406
  const regex = `^(${tags.join("|")})$`;
421
- out.push({
422
- file: `${rule2.id.split("/rule/")[1]}.yml`,
423
- 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 } }] } })
424
- });
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 } }] } }));
425
408
  }
426
409
  if (rule2.check.engine === "youi" && rule2.check.id === "raw-color") {
427
- out.push({
428
- file: `${rule2.id.split("/rule/")[1]}.yml`,
429
- 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" } } })
430
- });
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" } } }));
431
411
  }
432
412
  }
433
413
  return out;
@@ -478,8 +458,7 @@ async function initProject(options) {
478
458
  }
479
459
  }
480
460
  notes.push(`owned components: ${componentsDir}`);
481
- const tsconfig = await exists(join2(root, "tsconfig.json")) ? "tsconfig.json" : "tsconfig.json";
482
- const alias = await aliasRoot(root, tsconfig);
461
+ const alias = await aliasRoot(root);
483
462
  const importFrom = alias !== void 0 && componentsDir.startsWith(alias) ? `@/${componentsDir.slice(alias.length)}` : void 0;
484
463
  if (importFrom) notes.push(`imports use the @/ alias: ${importFrom}`);
485
464
  const include = (await Promise.all(
@@ -627,7 +606,6 @@ async function stagedChange(project2) {
627
606
  for (const file of added.keys()) sources[file] = await git(root, ["show", `:${prefix}${file}`]);
628
607
  return { base: "HEAD (staged changes)", commit: head, added, sources };
629
608
  }
630
- var slugOf = (rule2) => rule2.slice(rule2.indexOf("/rule/") + 6);
631
609
  var DRIFT_SEVERITY = { "near-duplicate": "warn", "hardcoded-value": "error", "off-library": "error" };
632
610
  async function importOf(project2) {
633
611
  const alias = await aliasRoot(project2.root, project2.config.tsconfig);
@@ -658,9 +636,9 @@ async function chosenLibraries(project2, added, notes) {
658
636
  notes.push(`imports were compared only with the System's components (${error instanceof Error ? error.message : String(error)})`);
659
637
  return chosen;
660
638
  }
661
- const outside = hits.filter((hit) => (hit.where ?? []).some((where) => {
662
- const at = where.lastIndexOf(":");
663
- 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)));
664
642
  }));
665
643
  const byType = /* @__PURE__ */ new Map();
666
644
  for (const hit of outside) {
@@ -730,11 +708,11 @@ async function analyseChange(project2, scan, change, options) {
730
708
  const merged = findings2.findIndex((f, i) => f.kind !== "rule" && samePlace(f, violation) && sameText(drift[i]?.text ?? "", violation.text));
731
709
  if (merged >= 0) {
732
710
  const into = findings2[merged];
733
- 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 };
734
712
  continue;
735
713
  }
736
- const fix = violation.suggestion ?? rules.get(violation.rule)?.agent ?? `see \`youiyoux rules\` for ${slugOf(violation.rule)}`;
737
- 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) });
738
716
  }
739
717
  findings2.sort((a, b) => a.file.localeCompare(b.file) || a.line - b.line || a.column - b.column);
740
718
  return { base: change.base, commit: change.commit, files, findings: findings2, notes, incomplete };
@@ -751,7 +729,6 @@ import { promisify as promisify2 } from "node:util";
751
729
  import { dirname, join as join4 } from "node:path";
752
730
  import { createRequire } from "node:module";
753
731
  var TOOL_ROOT = join4(dirname(createRequire(import.meta.url).resolve("youiyoux/package.json")), "dist");
754
- var CLI_BIN = join4(TOOL_ROOT, "packages/cli/bin/youi.js");
755
732
 
756
733
  // ../cli/src/tool-command.ts
757
734
  import { join as join5, relative as relative2, isAbsolute as isAbsolute2 } from "node:path";
@@ -1039,7 +1016,7 @@ async function verifyWiring(project2) {
1039
1016
  }
1040
1017
 
1041
1018
  // ../cli/src/check.ts
1042
- var toProjectPath = (project2, file) => relative4(project2.root, resolve2(file)).split(sep2).join("/");
1019
+ var toProjectPath = (project2, file) => relPath(project2.root, resolve2(file));
1043
1020
  function baselinePath(project2, configPath) {
1044
1021
  return join7(dirname3(resolve2(configPath ?? project2.configFile ?? join7(project2.root, PROJECT_FILE))), BASELINE_FILE);
1045
1022
  }
@@ -1059,7 +1036,7 @@ async function runCheck(project2, options = {}) {
1059
1036
  const wantedRule = (id) => !options.rule || id.endsWith(`/rule/${options.rule}`);
1060
1037
  const violations = report.violations.filter((v) => wantedRule(v.rule) && (!options.quiet || v.severity === "error"));
1061
1038
  const notRun = failedChecks(report.checks).filter((c) => wantedRule(c.rule));
1062
- const lock = options.lock === false || diff && options.lock !== true ? null : await lockIssues(project2);
1039
+ const lock = options.lock === false || diff ? null : await lockIssues(project2);
1063
1040
  const recorded = options.baseline ? await readBaseline(options.baseline) : null;
1064
1041
  if (options.baseline && !recorded) throw new Error(`no baseline at ${options.baseline}; record one with \`youiyoux baseline\``);
1065
1042
  const baseline = recorded && options.baseline ? { file: options.baseline, recorded: recorded.identity, comparison: compareToBaseline(recorded, violations, scope ?? void 0) } : null;
@@ -1077,7 +1054,7 @@ var diffExitCodeOf = (result, strict) => strict && ((result.diff?.findings.lengt
1077
1054
  async function writeBaseline(project2, file, options = {}) {
1078
1055
  const scan = await scanProject(project2, options.cache ? { cache: options.cache } : {});
1079
1056
  if (scan.report.status !== "complete" && !options.force) {
1080
- 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)}`)];
1081
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`);
1082
1059
  }
1083
1060
  const existing = await readBaseline(file);
@@ -1091,23 +1068,208 @@ async function writeBaseline(project2, file, options = {}) {
1091
1068
  }
1092
1069
 
1093
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
1094
1075
  import { execFile as execFile3 } from "node:child_process";
1095
- import { access as access2, readFile as readFile3 } from "node:fs/promises";
1096
- 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";
1097
1078
  import { promisify as promisify3 } from "node:util";
1098
1079
  var run3 = promisify3(execFile3);
1099
- 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
1100
1262
  async function loadCheckProject(root, options = {}, inferTop = false) {
1101
- let dir = resolve3(root);
1263
+ let dir = resolve4(root);
1102
1264
  if (options.configPath) return loadProject(dir, options);
1103
1265
  if (inferTop) {
1104
- for (let ancestor = dir; ; ancestor = dirname4(ancestor)) {
1105
- if (await exists2(join8(ancestor, PROJECT_FILE))) return loadProject(ancestor, options);
1106
- 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;
1107
1269
  }
1108
- dir = await run3("git", ["rev-parse", "--show-toplevel"], { cwd: dir }).then((r) => r.stdout.trim(), () => dir);
1270
+ dir = await repoRootOf(dir);
1109
1271
  }
1110
- if (await exists2(join8(dir, PROJECT_FILE))) return loadProject(dir, options);
1272
+ if (await exists(join9(dir, PROJECT_FILE))) return loadProject(dir, options);
1111
1273
  return inferProject(options.productRoot ?? dir);
1112
1274
  }
1113
1275
  async function inferProject(dir, loaded) {
@@ -1143,7 +1305,7 @@ async function runRepoCheck(project2, change) {
1143
1305
  const changedUi = (file) => change.added.has(file) && (/\.(tsx|jsx|astro|css)$/.test(file) || modules.some((m) => m.file === file && m.jsx.length > 0));
1144
1306
  for (const file of /* @__PURE__ */ new Set([...target.files, ...change.added.keys()])) {
1145
1307
  try {
1146
- 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"));
1147
1309
  if (module) {
1148
1310
  modules.push(module);
1149
1311
  if (changedUi(file) && module.parseErrors > 0) incomplete.push(`${file}: fix the syntax errors, then run youiyoux check --diff again`);
@@ -1168,7 +1330,7 @@ async function runRepoCheck(project2, change) {
1168
1330
  } catch (error) {
1169
1331
  incomplete.push(`Components could not be compared (${String(error)}); run youiyoux inventory --project ${project2.root} to diagnose`);
1170
1332
  }
1171
- 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] }));
1172
1334
  findings2.sort((a, b) => a.file.localeCompare(b.file) || a.line - b.line || a.column - b.column);
1173
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 } };
1174
1336
  }
@@ -1247,25 +1409,26 @@ function renderScan(report, outFile) {
1247
1409
  coverageLine(report),
1248
1410
  ...report.coverage.failed.map((f) => style.warn(` ! ${f.file}: ${f.reason}`)),
1249
1411
  ...ruleProblemLines(report),
1250
- ...outFile ? [style.dim(`usage map \u2192 ${outFile}`)] : []
1412
+ style.dim(`usage map \u2192 ${outFile}`)
1251
1413
  ];
1252
1414
  return lines.join("\n");
1253
1415
  }
1254
- function renderWhere(where, report, all) {
1255
- const max = Math.max(...where.byRoute.map((r) => r.sites), 1);
1256
- const routeWidth = Math.max(...where.byRoute.map((r) => r.route.length), 6);
1257
- const unknown = where.unknown.length > 0 ? style.warn(plural(where.unknown.length, "unknown")) : style.dim("0 unknown");
1258
- 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);
1259
1422
  const lines = [
1260
- `${style.accent(where.name)} ${style.dim(`${where.element} \xB7 v${where.version} \xB7 ${where.status}`)}`,
1261
- 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),
1262
1425
  "",
1263
- `${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")}`,
1264
1427
  rule(),
1265
- ...where.byRoute.map((r) => `${padEnd(r.route, routeWidth)} ${padStart(String(r.sites), 4)} ${bar(r.sites, max)}${r.route === "*" ? ` ${SHELL_NOTE}` : ""}`),
1266
- ...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")}`] : [],
1267
1430
  "",
1268
- ...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 "))}`),
1269
1432
  ""
1270
1433
  ];
1271
1434
  const byRoute = Map.groupBy(all, (s) => s.routes[0] ?? "(unrouted)");
@@ -1273,16 +1436,14 @@ function renderWhere(where, report, all) {
1273
1436
  lines.push(style.bold(route));
1274
1437
  for (const site of sites) lines.push(` ${padEnd(style.dim(site.at), 46)} ${padEnd(site.use, 14)} ${site.props}`);
1275
1438
  }
1276
- if (where.nextOffset !== null) lines.push(style.dim(` \u2026 ${where.total - all.length} more (--all, or --route <route>)`));
1277
- 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"));
1278
- 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})`));
1279
1442
  lines.push("", coverageLine(report));
1280
1443
  return lines.join("\n");
1281
1444
  }
1282
- function ruleSlug(id) {
1283
- return id.slice(id.indexOf("/rule/") + 6);
1284
- }
1285
- function renderCheck(violations, lockIssues2, report, suppressed) {
1445
+ function renderCheck(violations, lockIssues2, report) {
1446
+ const { suppressed } = report;
1286
1447
  const lines = [];
1287
1448
  for (const [file, list] of Map.groupBy(violations, (v) => v.file)) {
1288
1449
  lines.push(style.bold(file));
@@ -1302,7 +1463,7 @@ function renderCheck(violations, lockIssues2, report, suppressed) {
1302
1463
  lines.push(
1303
1464
  "",
1304
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"),
1305
- ...[...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}`)),
1306
1467
  ...suppressed > 0 ? [style.dim(` ${padStart(String(suppressed), 4)} suppressed by youi-ignore`)] : [],
1307
1468
  ...lockIssues2 === null ? [style.dim(" no youi.lock.json yet: generated-file and drift checks start after `youiyoux sync`")] : [],
1308
1469
  coverageLine(report),
@@ -1429,14 +1590,13 @@ function renderShots2(report) {
1429
1590
  }
1430
1591
 
1431
1592
  // ../cli/src/check-render.ts
1432
- var slug = (rule2) => rule2.slice(rule2.indexOf("/rule/") + 6);
1433
1593
  function actionable(result) {
1434
1594
  if (!result.baseline) return result.violations;
1435
1595
  const { comparison } = result.baseline;
1436
1596
  return [...comparison.new, ...comparison.regressed].sort((a, b) => a.file.localeCompare(b.file) || a.line - b.line);
1437
1597
  }
1438
1598
  function renderCheckText(result) {
1439
- 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)];
1440
1600
  if (result.baseline) {
1441
1601
  const { comparison, file, recorded } = result.baseline;
1442
1602
  const gone = comparison.fixed.reduce((n, f) => n + f.gone, 0);
@@ -1464,13 +1624,13 @@ function githubAnnotations(result, prefix) {
1464
1624
  file: `${prefix}${v.file}`,
1465
1625
  line: v.line,
1466
1626
  col: v.column,
1467
- title: `youi ${slug(v.rule)}`
1627
+ title: `youi ${ruleSlug(v.rule)}`
1468
1628
  })
1469
1629
  );
1470
1630
  for (const issue of result.lock ?? []) lines.push(command(issue.severity === "error" ? "error" : "warning", issue.message, { file: `${prefix}${issue.file}`, title: "youi setup" }));
1471
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" }));
1472
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" }));
1473
- 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" }));
1474
1634
  return lines;
1475
1635
  }
1476
1636
  var revision = (state) => state.commit ? `\`${state.commit}\`${state.dirty ? " (dirty)" : ""}` : "not a git checkout";
@@ -1492,7 +1652,7 @@ function diffGaps(result) {
1492
1652
  return [
1493
1653
  ...result.unmatched.map((f) => `${f} was not checked: it is not in this project's include globs`),
1494
1654
  ...result.failed.map((f) => `${f.file} could not be fully analysed (${f.reason})`),
1495
- ...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})`),
1496
1656
  ...result.diff?.incomplete ?? []
1497
1657
  ];
1498
1658
  }
@@ -1546,33 +1706,27 @@ function githubDiffSummary(result) {
1546
1706
 
1547
1707
  // ../cli/src/hooks.ts
1548
1708
  import { execFile as execFile4 } from "node:child_process";
1549
- import { access as access3, realpath as realpath2 } from "node:fs/promises";
1550
- 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";
1551
1711
  import { promisify as promisify4 } from "node:util";
1552
1712
  var run4 = promisify4(execFile4);
1553
1713
  var MAX_LISTED = 20;
1554
1714
  var COMPONENT_FILE = /\.(?:tsx|jsx)$/;
1555
1715
  var quoted = (path) => /^[\w./-]+$/.test(path) ? path : `'${path.replace(/'/g, "'\\''")}'`;
1556
1716
  var SILENT = { stdout: "", exitCode: 0 };
1557
- async function exists3(file) {
1558
- return access3(file).then(
1559
- () => true,
1560
- () => false
1561
- );
1562
- }
1563
1717
  async function load(target, near) {
1564
1718
  return loadCheckProject(target.project ?? near, { ...target.config ? { configPath: target.config } : {}, ...target.design ? { designRoot: target.design } : {}, ...target.root ? { productRoot: target.root } : {} }, !target.project);
1565
1719
  }
1566
1720
  async function baselineFor(project2, target) {
1567
1721
  const file = baselinePath(project2, target.config);
1568
- return await exists3(file) ? file : null;
1722
+ return await exists(file) ? file : null;
1569
1723
  }
1570
1724
  function findingLines(result, root) {
1571
1725
  const findings2 = result.diff?.findings ?? [];
1572
1726
  const lines = findings2.slice(0, MAX_LISTED).map((f) => `- ${diffFindingLine(f)}`);
1573
1727
  if (findings2.length > MAX_LISTED) {
1574
- const { run: run8, setup } = portableYoui(root);
1575
- 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\`.`);
1576
1730
  }
1577
1731
  for (const gap of diffGaps(result)) lines.push(`- incomplete: ${gap}; 0 findings there is not a pass`);
1578
1732
  return lines;
@@ -1597,21 +1751,22 @@ async function runClaudeHook(input, target = {}) {
1597
1751
  }
1598
1752
  const raw = payload.tool_input?.file_path;
1599
1753
  if (typeof raw !== "string" || raw.length === 0) return SILENT;
1600
- const file = resolve4(payload.cwd ?? process.cwd(), raw);
1754
+ const file = resolve5(payload.cwd ?? process.cwd(), raw);
1601
1755
  try {
1602
- const project2 = await load(target, dirname5(file));
1603
- const real = (path) => realpath2(path).catch(() => path);
1604
- const relative10 = relPath(await real(project2.root), await real(file));
1605
- if (!isChecked(project2, relative10)) return SILENT;
1606
- const { change, problem } = await changeFor(() => workingTreeChange(project2, void 0, [relative10]));
1607
- if (problem) return claudeContext(problem, project2.root);
1608
- 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);
1609
1764
  const lines = findingLines(result, project2.root);
1610
- 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;
1611
- if (lines.length === 0) return see ? claudeContext(`youiyoux: ${relative10} passes the design-system check. ${see}`, project2.root) : SILENT;
1612
- 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;
1613
1768
  const known = older > 0 ? ` (${older} older findings on unchanged lines omitted)` : "";
1614
- return claudeContext(`youiyoux design-system check (advisory) on ${relative10}${known}:
1769
+ return claudeContext(`youiyoux design-system check (advisory) on ${relative9}${known}:
1615
1770
  ${lines.join("\n")}${!configured(project2) ? `
1616
1771
  ${result.diff?.notes.join("\n")}` : ""}
1617
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 ? `
@@ -1639,10 +1794,11 @@ async function runPreCommitHook(target = {}, options = {}) {
1639
1794
  const staged = await stagedSources(project2);
1640
1795
  const files = Object.keys(staged);
1641
1796
  if (files.length === 0) return SILENT;
1642
- const { change, problem } = await changeFor(() => stagedChange(project2));
1643
- 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)}
1644
1799
  `, exitCode: options.strict ? 1 : 0 };
1645
- 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);
1646
1802
  const lines = findingLines(result, project2.root);
1647
1803
  const failing = result.incomplete || (result.diff?.findings.length ?? 0) > 0;
1648
1804
  if (lines.length === 0) return { stdout: `${label}: ${files.length} staged file(s) clean, no drift
@@ -1657,195 +1813,9 @@ ${viaNpx(lines.join("\n"), project2.root)}
1657
1813
  }
1658
1814
  }
1659
1815
 
1660
- // ../cli/src/hooks-install.ts
1661
- import { execFile as execFile5 } from "node:child_process";
1662
- import { chmod, mkdir as mkdir2, realpath as realpath3 } from "node:fs/promises";
1663
- import { dirname as dirname6, join as join10, relative as relative5, resolve as resolve5, sep as sep3 } from "node:path";
1664
- import { promisify as promisify5 } from "node:util";
1665
- var run5 = promisify5(execFile5);
1666
- var CLAUDE_MARKER = "hooks run claude";
1667
- var BLOCK_START = "# >>> youi pre-commit >>>";
1668
- var BLOCK_END2 = "# <<< youi pre-commit <<<";
1669
- var WORKFLOW = ".github/workflows/youi-check.yml";
1670
- var WORKFLOW_HEADER = "# Generated by `youiyoux hooks install --ci`.";
1671
- var LEGACY_WORKFLOW_HEADER = WORKFLOW_HEADER.replace("youiyoux", "youi");
1672
- var managedWorkflow = (text) => text.startsWith(WORKFLOW_HEADER) || text.startsWith(LEGACY_WORKFLOW_HEADER);
1673
- var DIFF_STEP = "name: youiyoux check --diff";
1674
- var CLAUDE_MATCHER = "Edit|Write|MultiEdit";
1675
- var CLAUDE_TIMEOUT_S = 60;
1676
- var shellQuote = (arg) => /^[\w@%+=:,./-]+$/.test(arg) ? arg : `'${arg.replace(/'/g, `'\\''`)}'`;
1677
- async function gitOut(cwd, args) {
1678
- return (await run5("git", [...args], { cwd })).stdout.trim();
1679
- }
1680
- async function repoRootOf(dir) {
1681
- return gitOut(dir, ["rev-parse", "--show-toplevel"]).catch(() => dir);
1682
- }
1683
- function hookCommand(toolRoot, kind, target, extra = []) {
1684
- const server = toolCommand(toolRoot);
1685
- const args = [server.command, ...server.args, "hooks", "run", kind];
1686
- for (const key2 of ["project", "config", "design", "root"]) {
1687
- const value = target[key2];
1688
- if (value) args.push(`--${key2}`, resolve5(value));
1689
- }
1690
- return [...args, ...extra].map(shellQuote).join(" ");
1691
- }
1692
- async function write(file, content, before, guarantee, mode) {
1693
- if (before === content) return { file, action: "unchanged", guarantee };
1694
- await mkdir2(dirname6(file), { recursive: true });
1695
- await writeFileAtomic(file, content);
1696
- if (mode !== void 0) await chmod(file, mode);
1697
- return { file, action: before === null ? "created" : "updated", guarantee };
1698
- }
1699
- var isYouiEntry = (entry) => (entry.hooks ?? []).some((h) => typeof h.command === "string" && h.command.includes(CLAUDE_MARKER));
1700
- async function installClaudeHook(repoRoot, command2) {
1701
- const file = join10(repoRoot, ".claude/settings.local.json");
1702
- const before = await readTextOrNull(file);
1703
- let settings;
1704
- try {
1705
- settings = before === null ? {} : JSON.parse(before);
1706
- } catch {
1707
- throw new Error(`${file} is not valid JSON; fix it first (youi will not overwrite it)`);
1708
- }
1709
- const hooks = settings.hooks ?? {};
1710
- const existing = hooks.PostToolUse ?? [];
1711
- const entry = { matcher: CLAUDE_MATCHER, hooks: [{ type: "command", command: command2, timeout: CLAUDE_TIMEOUT_S }] };
1712
- const postToolUse = existing.some(isYouiEntry) ? existing.map((e) => isYouiEntry(e) ? entry : e) : [...existing, entry];
1713
- const next2 = { ...settings, hooks: { ...hooks, PostToolUse: postToolUse } };
1714
- return write(file, `${JSON.stringify(next2, null, 2)}
1715
- `, 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");
1716
- }
1717
- function withBlock(script, block) {
1718
- if (script === null) return `#!/bin/sh
1719
- ${block}
1720
- `;
1721
- const start = script.indexOf(BLOCK_START);
1722
- const end = script.indexOf(BLOCK_END2);
1723
- if (start >= 0 && end > start) return `${script.slice(0, start)}${block}${script.slice(end + BLOCK_END2.length)}`;
1724
- return `${script.endsWith("\n") ? script : `${script}
1725
- `}
1726
- ${block}
1727
- `;
1728
- }
1729
- async function installPreCommit(projectRoot, command2, strict) {
1730
- const hooksDir = resolve5(projectRoot, await gitOut(projectRoot, ["rev-parse", "--git-path", "hooks"]));
1731
- const file = join10(hooksDir, "pre-commit");
1732
- const before = await readTextOrNull(file);
1733
- const shebang = before?.split("\n", 1)[0] ?? "";
1734
- if (before !== null && shebang.startsWith("#!") && !/\b(?:sh|bash|zsh|dash)\b/.test(shebang)) {
1735
- throw new Error(`${file} is not a shell script (${shebang}); add this line to it yourself: ${command2}`);
1736
- }
1737
- if (before !== null && /^\s*exec\s/m.test(before) && !before.includes(BLOCK_START)) {
1738
- throw new Error(`${file} hands off with exec, so an appended check would never run; add this line before the exec yourself: ${command2}`);
1739
- }
1740
- const what = strict ? "Blocks commits with new design-system errors" : "Advisory, never blocks";
1741
- const block = `${BLOCK_START}
1742
- # ${what}: checks the staged content of staged files only; CI is authoritative.
1743
- ${command2} || exit $?
1744
- ${BLOCK_END2}`;
1745
- 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)";
1746
- return write(file, withBlock(before, block), before, guarantee, 493);
1747
- }
1748
- async function installRepoAgentInstructions(root) {
1749
- const file = join10(root, "AGENTS.md");
1750
- const before = await readTextOrNull(file);
1751
- const guarantee = "agents run youiyoux check --diff after UI edits; pre-commit also checks staged edits";
1752
- const ruled = before !== null && before.indexOf(RULE_END) > before.indexOf(RULE_BEGIN) && before.includes(RULE_BEGIN);
1753
- return write(file, ruled ? before : upsertBlock(before, repoAgentsBlock(root)), before, guarantee);
1754
- }
1755
- function codexFallback(projectRoot, agentsFile) {
1756
- return {
1757
- file: join10(projectRoot, agentsFile),
1758
- action: "skipped",
1759
- 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"
1760
- };
1761
- }
1762
- function githubSlug(url) {
1763
- const match = /github\.com[:/]([^/\s]+)\/([^/\s]+?)(?:\.git)?\/?$/.exec(url.trim());
1764
- return match ? `${match[1]}/${match[2]}` : null;
1765
- }
1766
- async function repoAt(root) {
1767
- const slug2 = githubSlug(await gitOut(root, ["remote", "get-url", "origin"]).catch(() => ""));
1768
- if (!slug2) throw new Error(`${root} has no GitHub origin remote; the workflow needs one to check it out`);
1769
- return { slug: slug2, commit: await gitOut(root, ["rev-parse", "HEAD"]) };
1770
- }
1771
- var posix2 = (path) => path.split(sep3).join("/") || ".";
1772
- var inside = (root, path) => !relative5(root, path).startsWith("..");
1773
- function workflowText(input) {
1774
- const dir = input.projectDir || ".";
1775
- const target = ["--project", dir, "--design", ".youi-ci/design", ...input.configPath ? ["--config", input.configPath, "--root", dir] : []];
1776
- const checkArgs = ["--baseline", "--format", "github", ...target].map(shellQuote).join(" ");
1777
- const diffArgs2 = ["--strict", "--format", "github", ...target].map(shellQuote).join(" ");
1778
- const diffStep = input.diff ? ` - ${DIFF_STEP} (drift this pull request adds)
1779
- if: \${{ !cancelled() && github.event_name == 'pull_request' }}
1780
- env:
1781
- YOUI_BASE: \${{ github.base_ref }}
1782
- run: node .youi-ci/tool/packages/cli/bin/youi.js check --diff "origin/$YOUI_BASE" ${diffArgs2}
1783
- ` : "";
1784
- return `${WORKFLOW_HEADER}
1785
- # The authoritative design-system check: it fails on violations the baseline does not
1786
- # accept, annotates them on the PR's lines, and states the rules version (design commit
1787
- # and definitions digest) in the job summary. Raise the rules version by changing the
1788
- # design ref below; YOUI_REPO_TOKEN must be able to read both repositories.
1789
- name: youiyoux check
1790
- on:
1791
- pull_request:
1792
- push:
1793
- branches: [main]
1794
- permissions:
1795
- contents: read
1796
- jobs:
1797
- youi-check:
1798
- runs-on: ubuntu-latest
1799
- timeout-minutes: 10
1800
- steps:
1801
- - uses: actions/checkout@v4${input.diff ? "\n with:\n fetch-depth: 0" : ""}
1802
- - name: Check out youi
1803
- uses: actions/checkout@v4
1804
- with:
1805
- repository: ${input.tool.slug}
1806
- ref: ${input.tool.commit}
1807
- path: .youi-ci/tool
1808
- token: \${{ secrets.YOUI_REPO_TOKEN }}
1809
- - name: Check out the design rules (the rules version)
1810
- uses: actions/checkout@v4
1811
- with:
1812
- repository: ${input.design.slug}
1813
- ref: ${input.design.commit}
1814
- path: .youi-ci/design
1815
- token: \${{ secrets.YOUI_REPO_TOKEN }}
1816
- - uses: pnpm/action-setup@v4
1817
- with:
1818
- version: 10.13.1
1819
- - uses: actions/setup-node@v4
1820
- with:
1821
- node-version: 24
1822
- - run: pnpm install --frozen-lockfile --ignore-scripts
1823
- working-directory: .youi-ci/tool
1824
- - name: youiyoux check
1825
- run: node .youi-ci/tool/packages/cli/bin/youi.js check ${checkArgs}
1826
- ${diffStep}`;
1827
- }
1828
- async function installWorkflow(project2, toolRoot, target, force, options = {}) {
1829
- const top = await gitOut(project2.root, ["rev-parse", "--show-toplevel"]);
1830
- const [tool, design] = await Promise.all([ephemeralTool(toolRoot) ? Promise.resolve({ slug: "npm/youiyoux", commit: "published" }) : repoAt(toolRoot), repoAt(project2.repo.root)]);
1831
- const configFile = resolve5(target.config ?? project2.configFile ?? join10(project2.root, PROJECT_FILE));
1832
- 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;
1833
- if (target.config && !configPath) throw new Error(`${configFile} is in neither this repo nor the design repo, so CI cannot read it`);
1834
- const file = join10(top, WORKFLOW);
1835
- const before = await readTextOrNull(file);
1836
- if (before !== null && !managedWorkflow(before) && !force) throw new Error(`${file} exists and was not written by youi; pass --force to replace it`);
1837
- const diff = options.diff === true || before !== null && managedWorkflow(before) && (before.includes(DIFF_STEP) || before.includes(DIFF_STEP.replace("youiyoux", "youi")));
1838
- const projectDir = posix2(relative5(top, await realpath3(project2.root).catch(() => project2.root)));
1839
- let text = workflowText({ tool, design, projectDir, configPath, diff });
1840
- if (ephemeralTool(toolRoot)) {
1841
- 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");
1842
- }
1843
- 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)" : ""}`);
1844
- }
1845
-
1846
1816
  // ../cli/src/agents-install.ts
1847
1817
  import { readdir as readdir2, rm as rm2, rmdir } from "node:fs/promises";
1848
- 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";
1849
1819
  var TOML_BEGIN = "# >>> youi mcp >>>";
1850
1820
  var TOML_END = "# <<< youi mcp <<<";
1851
1821
  var MCP_NAME = "youiyoux";
@@ -1862,7 +1832,7 @@ function mcpCommand(toolRoot, target) {
1862
1832
  return { command: server.command, args };
1863
1833
  }
1864
1834
  function relativeTo(repoRoot, server) {
1865
- 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) };
1866
1836
  }
1867
1837
  var shellLine = ({ command: command2, args }) => [command2, ...args].map(shellQuote).join(" ");
1868
1838
  function withBlock2(text, block, begin, end) {
@@ -2091,7 +2061,7 @@ import { homedir, userInfo } from "node:os";
2091
2061
  import { join as join12, resolve as resolve6 } from "node:path";
2092
2062
  async function swapImpact(candidate, family) {
2093
2063
  try {
2094
- const { discoverIndexPath: discoverIndexPath2, sharedStore: sharedStore2 } = await import("./src-YUC5PIDK.js");
2064
+ const { discoverIndexPath: discoverIndexPath2, sharedStore: sharedStore2 } = await import("./src-SFKFERZ2.js");
2095
2065
  const path = discoverIndexPath2();
2096
2066
  if (!process.env.DATABASE_URL && !existsSync(path)) return null;
2097
2067
  const impact = await (await sharedStore2(path, process.env, { readOnly: true })).swapImpact(candidate, family);
@@ -2102,7 +2072,7 @@ async function swapImpact(candidate, family) {
2102
2072
  }
2103
2073
  async function libraryLicence(id) {
2104
2074
  try {
2105
- const { discoverCacheDir: discoverCacheDir2, findListing } = await import("./src-YUC5PIDK.js");
2075
+ const { discoverCacheDir: discoverCacheDir2, findListing } = await import("./src-SFKFERZ2.js");
2106
2076
  const { entry } = await findListing(discoverCacheDir2(void 0, process.env), id);
2107
2077
  return entry.id === id ? entry.license : null;
2108
2078
  } catch {
@@ -2110,7 +2080,7 @@ async function libraryLicence(id) {
2110
2080
  }
2111
2081
  }
2112
2082
  async function startRuleEditor(options) {
2113
- const { FileStore, RuleWorkflow, serveWorkflow } = await import("./src-U5NTDPUX.js");
2083
+ const { FileStore, RuleWorkflow, serveWorkflow } = await import("./src-34R7QMJC.js");
2114
2084
  const projectRoot = resolve6(options.project);
2115
2085
  const load2 = { ...options.config ? { configPath: resolve6(options.config) } : {}, ...options.design ? { designRoot: resolve6(options.design) } : {}, ...options.root ? { productRoot: resolve6(options.root) } : {} };
2116
2086
  const project2 = await loadProject(projectRoot, load2);
@@ -2125,41 +2095,79 @@ async function startRuleEditor(options) {
2125
2095
 
2126
2096
  // ../cli/src/hub.ts
2127
2097
  import { spawn } from "node:child_process";
2128
- 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";
2129
2101
  import { join as join13, resolve as resolve7 } from "node:path";
2130
2102
  var HUB_SNAPSHOT_DIR = ".hub";
2131
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]);
2132
2106
  function hubPlan(toolRoot, options) {
2133
2107
  const web = join13(toolRoot, "apps/web");
2134
2108
  const vite = [join13(web, "node_modules/vite/bin/vite.js"), ...options.port ? ["--port", options.port, "--strictPort"] : []];
2135
- const named = ["design", "project", "config", "root", "inventory", "detect"].flatMap((key2) => options[key2] ? [`--${key2}`, resolve7(options[key2])] : []);
2136
- const watchesDemo = options.watch && !["design", "project", "config", "root"].some((key2) => options[key2]);
2137
- 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);
2138
2111
  const api = options.api ? { YOUI_API: options.api } : {};
2139
2112
  if (flags.length === 0) return { web, snapshot: null, env: api, vite, watch: null };
2140
2113
  const out = join13(web, HUB_SNAPSHOT_DIR);
2141
2114
  const snapshot = [join13(web, "scripts/snapshot.ts"), "--out", out, ...flags];
2142
2115
  return { web, snapshot, env: { YOUI_HUB_SNAPSHOT: out, ...api }, vite, watch: options.watch ? [...snapshot, "--watch"] : null };
2143
2116
  }
2144
- 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) {
2145
2128
  return new Promise((done, fail) => {
2146
2129
  const child = spawn(process.execPath, args, { cwd, env: { ...process.env, ...env }, stdio: "inherit" });
2130
+ started?.(child);
2147
2131
  child.on("error", fail);
2148
2132
  child.on("exit", (code, signal) => done(code ?? (signal === "SIGINT" || signal === "SIGTERM" ? 0 : 1)));
2149
2133
  });
2150
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
+ }
2151
2158
  async function runHub(toolRoot, options) {
2159
+ if (existsSync2(join13(toolRoot, BUILT_HUB, "index.html"))) return runBuiltHub(toolRoot, options);
2152
2160
  const plan = hubPlan(toolRoot, options);
2153
- await access4(plan.vite[0]).catch(() => {
2161
+ await access2(plan.vite[0]).catch(() => {
2154
2162
  throw new Error(`the hub's dependencies are not installed: run \`pnpm install\` in ${toolRoot}`);
2155
2163
  });
2156
2164
  if (plan.snapshot) {
2157
- const code = await run6(plan.snapshot, process.cwd(), {});
2165
+ const code = await run5(plan.snapshot, process.cwd(), {});
2158
2166
  if (code !== 0) return code;
2159
2167
  }
2160
2168
  const watcher = plan.watch ? spawn(process.execPath, plan.watch, { cwd: process.cwd(), stdio: "inherit" }) : null;
2161
2169
  try {
2162
- return await run6(plan.vite, plan.web, plan.env);
2170
+ return await run5(plan.vite, plan.web, plan.env);
2163
2171
  } finally {
2164
2172
  watcher?.kill("SIGTERM");
2165
2173
  }
@@ -2176,10 +2184,11 @@ function previewsPlan(toolRoot, options) {
2176
2184
  }
2177
2185
  async function runPreviews(toolRoot, options) {
2178
2186
  const plan = previewsPlan(toolRoot, options);
2179
- await access4(join13(plan.web, "node_modules/playwright-core")).catch(() => {
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(() => {
2180
2189
  throw new Error(`the hub's dependencies are not installed: run \`pnpm install\` in ${toolRoot}`);
2181
2190
  });
2182
- return run6(plan.args, plan.web, {});
2191
+ return run5(plan.args, plan.web, {});
2183
2192
  }
2184
2193
 
2185
2194
  // ../cli/src/requests.ts
@@ -2204,7 +2213,7 @@ function renderRequests(briefs, status, unreadable) {
2204
2213
  }
2205
2214
 
2206
2215
  // ../cli/src/index-store.ts
2207
- 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";
2208
2217
  import { dirname as dirname8, join as join14, resolve as resolve8 } from "node:path";
2209
2218
  var SOURCES_FILE = "index-sources.json";
2210
2219
  var SCANS_LOG = "scans.jsonl";
@@ -2228,26 +2237,19 @@ async function noteSource(home, change) {
2228
2237
  `);
2229
2238
  }
2230
2239
  async function recordInventory(store, home, report, file) {
2231
- const run8 = inventoryRun(report, resolve8(file));
2232
- 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 };
2233
2242
  await mkdir3(home, { recursive: true });
2234
2243
  await appendFile(join14(home, SCANS_LOG), `${JSON.stringify({ schema: "youi.scan-log/v1", ...logged })}
2235
2244
  `);
2236
- await store.recordInventory(run8);
2237
- await noteSource(home, { inventory: run8.source });
2245
+ await store.recordInventory(run7);
2246
+ await noteSource(home, { inventory: run7.source });
2238
2247
  await linkCandidates(store);
2239
- return run8;
2248
+ return run7;
2240
2249
  }
2241
2250
  async function recordProject(store, project2, usage) {
2242
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 }));
2243
2252
  }
2244
- async function readJsonFile(file) {
2245
- try {
2246
- return JSON.parse(await readFile4(file, "utf8"));
2247
- } catch {
2248
- return null;
2249
- }
2250
- }
2251
2253
  async function readLog(home) {
2252
2254
  const text = await readFile4(join14(home, SCANS_LOG), "utf8").catch(() => "");
2253
2255
  return text.split("\n").flatMap((line) => {
@@ -2265,7 +2267,7 @@ async function rebuildIndex(path, options = {}) {
2265
2267
  const env = options.env ?? process.env;
2266
2268
  const home = indexHome(path, env);
2267
2269
  await mkdir3(dirname8(path), { recursive: true });
2268
- const scratch = !env.DATABASE_URL ? await mkdtemp2(`${path}.rebuild-`) : null;
2270
+ const scratch = !env.DATABASE_URL ? await mkdtemp3(`${path}.rebuild-`) : null;
2269
2271
  const target = scratch ? join14(scratch, "index.db") : path;
2270
2272
  const store = scratch ? await openSqlite(target) : await sharedStore(path, env);
2271
2273
  try {
@@ -2283,7 +2285,7 @@ async function rebuildIndex(path, options = {}) {
2283
2285
  const missing = [];
2284
2286
  let replayed = 0;
2285
2287
  for (const file of sources.inventories) {
2286
- const parsed = InventoryReport.safeParse(await readJsonFile(file));
2288
+ const parsed = InventoryReport.safeParse(await readJson(file));
2287
2289
  if (!parsed.success) {
2288
2290
  missing.push(file);
2289
2291
  continue;
@@ -2302,7 +2304,7 @@ async function rebuildIndex(path, options = {}) {
2302
2304
  }
2303
2305
  try {
2304
2306
  const project2 = await loadProject(source.root, source.options);
2305
- 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")));
2306
2308
  await recordProject(store, project2, usage.success ? usage.data : null);
2307
2309
  projects++;
2308
2310
  } catch (error) {
@@ -2337,7 +2339,7 @@ async function rebuildIndex(path, options = {}) {
2337
2339
  } finally {
2338
2340
  if (scratch) {
2339
2341
  await store.close();
2340
- await rm3(scratch, { recursive: true, force: true });
2342
+ await rm4(scratch, { recursive: true, force: true });
2341
2343
  }
2342
2344
  }
2343
2345
  }
@@ -2369,6 +2371,40 @@ function renderHistory(scans) {
2369
2371
  return lines.join("\n");
2370
2372
  }
2371
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
+
2372
2408
  // ../cli/src/inventory.ts
2373
2409
  var FAMILIES_PER_ROLE = 6;
2374
2410
  function shapeLabel(shape) {
@@ -2405,7 +2441,7 @@ function storyLines(report) {
2405
2441
  function storyNext(report, outFile) {
2406
2442
  const stories = report.projects.flatMap((p) => p.stories ? [p.stories] : []);
2407
2443
  if (stories.length === 0) return null;
2408
- const open = `youiyoux ui --inventory ${outFile ?? ".youi/inventory.json"}`;
2444
+ const open = `youiyoux ui --inventory ${outFile}`;
2409
2445
  const skipped = stories.reduce((n, s) => n + s.skipped.length, 0);
2410
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}`;
2411
2447
  return `Next: see the stories beside their real uses with ${open}`;
@@ -2453,7 +2489,7 @@ function renderInventory(report, outFile) {
2453
2489
  }
2454
2490
  const stories = storyLines(report);
2455
2491
  if (stories.length > 0) lines.push("", ...stories);
2456
- if (outFile) lines.push(style.dim(`inventory \u2192 ${outFile}`));
2492
+ lines.push(style.dim(`inventory \u2192 ${outFile}`));
2457
2493
  const next2 = storyNext(report, outFile);
2458
2494
  if (next2) lines.push(next2);
2459
2495
  return lines.join("\n");
@@ -2487,7 +2523,7 @@ function renderAdoptionRequest(request) {
2487
2523
  // ../cli/src/report/run.ts
2488
2524
  import { randomBytes as randomBytes2 } from "node:crypto";
2489
2525
  import { mkdir as mkdir4, rename as rename2, stat as stat2, writeFile as writeFile2 } from "node:fs/promises";
2490
- 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";
2491
2527
 
2492
2528
  // ../cli/src/report/card.ts
2493
2529
  import { deflateSync } from "node:zlib";
@@ -2801,7 +2837,7 @@ function prefixOf(tokens) {
2801
2837
  const [best, n] = [...counts].sort((a, b) => b[1] - a[1])[0] ?? [null, 0];
2802
2838
  return best && n >= tokens.length / 4 ? best : null;
2803
2839
  }
2804
- function scaleTokens(index, file, usage = { count: /* @__PURE__ */ new Map(), byFile: /* @__PURE__ */ new Map() }) {
2840
+ function scaleTokens(index, file, usage) {
2805
2841
  const value = (t) => `${t.id.split(".")[0]}\0${JSON.stringify(t.light)}`;
2806
2842
  const prefix = prefixOf(index.tokens);
2807
2843
  const rank = new Map(index.tokens.map((t) => [t, genericity(t, prefix)]));
@@ -2815,7 +2851,7 @@ function scaleTokens(index, file, usage = { count: /* @__PURE__ */ new Map(), by
2815
2851
  const component = componentOf(t);
2816
2852
  return component !== null && !general.has(value(t)) && new RegExp(`(?:^|[/._-])${component}s?(?:[/._-]|$)`).test(path);
2817
2853
  });
2818
- 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];
2819
2855
  const higher = (a, b) => {
2820
2856
  for (let i = 0; i < a.length; i++) if (a[i] !== b[i]) return b[i] - a[i];
2821
2857
  return 0;
@@ -2826,21 +2862,20 @@ function scaleTokens(index, file, usage = { count: /* @__PURE__ */ new Map(), by
2826
2862
  var escape = (text) => text.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
2827
2863
  async function reportTokens(module, tokens, usage) {
2828
2864
  if (isShowcase(module.file) || module.lang !== "css" && module.jsx.length === 0) return [];
2829
- const { parseModule: parseModule2 } = await import("./src-ZS76UD7E.js");
2830
2865
  let source = withoutVariables(module.source);
2831
2866
  if (module.lang !== "css") {
2832
2867
  source = maskFunctions(source, /(?:repeating-)?(?:linear|radial|conic)-gradient\s*\(/gi);
2833
2868
  source = source.replace(/(?<=[[,]\s*)(["'`])(?:#[0-9a-fA-F]{3,8}|(?:rgba?|hsla?)\([^)\n]*\))\1(?=\s*[,\]])/g, (literal) => " ".repeat(literal.length));
2834
2869
  }
2835
- const clean = source === module.source ? module : await parseModule2(module.file, source);
2870
+ const clean = source === module.source ? module : await parseModule(module.file, source);
2836
2871
  if (!clean) return [];
2837
2872
  const index = scaleTokens(tokens, module.file, usage);
2838
2873
  return hardcodedValues(clean, index, { isTokenSource: module.lang === "css", added: () => true }).filter((f) => {
2839
2874
  if (/\bvar\(/.test(f.text)) return false;
2840
2875
  const kind = /^Hardcoded (colour|radius|spacing|type)\b/.exec(f.message)?.[1];
2841
2876
  const groups = kind === "colour" ? /^(color|colour)\./ : kind === "radius" ? /^(radius|radii|rounded)\./ : kind === "spacing" ? /^(space|spacing)\./ : /^(type|typography|fontSize)\./;
2842
- const named = index.tokens.filter((t) => new RegExp(`[\\s(]${escape(t.id)}(?=[\\s,)]|$)`).test(f.fix));
2843
- 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));
2844
2879
  });
2845
2880
  }
2846
2881
  function publicLook(caption) {
@@ -2850,12 +2885,12 @@ function publicLook(caption) {
2850
2885
  }
2851
2886
 
2852
2887
  // ../cli/src/report/groups.ts
2853
- import { existsSync as existsSync2 } from "node:fs";
2888
+ import { existsSync as existsSync3 } from "node:fs";
2854
2889
  import { join as join16 } from "node:path";
2855
2890
 
2856
2891
  // ../cli/src/report/imports.ts
2857
2892
  import { realpathSync } from "node:fs";
2858
- 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";
2859
2894
  import ts from "typescript";
2860
2895
  var resolvers = /* @__PURE__ */ new Map();
2861
2896
  function resolverOf(root) {
@@ -2874,7 +2909,7 @@ function clearImportCache() {
2874
2909
  }
2875
2910
  async function verifiedImport(root, keeper, from, suggested) {
2876
2911
  const { options, cache } = resolverOf(root);
2877
- 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)$/, "");
2878
2913
  if (!local.startsWith(".")) local = `./${local}`;
2879
2914
  for (const specifier of /* @__PURE__ */ new Set([suggested, local])) {
2880
2915
  const found = ts.resolveModuleName(specifier, resolve9(root, from), options, ts.sys, cache).resolvedModule?.resolvedFileName;
@@ -2992,7 +3027,6 @@ var SVG_TAGS = /* @__PURE__ */ new Set(["svg", "path", "g", "circle", "rect", "l
2992
3027
  var NEAR_IDENTICAL = 0.5;
2993
3028
  var MIN_STRUCTURE = 6;
2994
3029
  var GENERIC_IDENTICAL = 0.85;
2995
- var plural2 = (count, word, many = `${word}s`) => `${count} ${count === 1 ? word : many}`;
2996
3030
  var repoPath = (dir, file) => dir === "." ? file : `${dir}/${file}`;
2997
3031
  var kebab = (name) => name.replace(/[a-z0-9](?=[A-Z])/g, "$&-").toLowerCase();
2998
3032
  function shapeWords(shape) {
@@ -3158,8 +3192,8 @@ async function nearDuplicateGroups(input) {
3158
3192
  actions.push(`replace the hand-styled ${tag}${at.length > 1 ? "s" : ""} at line${at.length > 1 ? "s" : ""} ${at.join(", ")} with ${keeperName}`);
3159
3193
  }
3160
3194
  for (const v of list.filter((c) => c.kind === "component")) {
3161
- const named = v.name === "default" ? "its default export" : v.name;
3162
- 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`);
3163
3197
  }
3164
3198
  if (actions.length > 0) steps.push(`In ${file}, ${actions.join("; ")} (${use})`);
3165
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`);
@@ -3168,13 +3202,13 @@ async function nearDuplicateGroups(input) {
3168
3202
  const copyIds = new Set(copies.map((v) => v.id));
3169
3203
  const counted = members.filter((m) => copyIds.has(m.version.id));
3170
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;
3171
- const where = keeperFacts.project.name && importedBy(keeperFacts) > 0 ? `${keeperFacts.project.name}, ${keeperFile}` : keeperFile;
3172
- 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`;
3173
3207
  const [one, many] = KIND_WORDS[kind];
3174
3208
  const components = new Set([kept, ...copies].flatMap((v) => v?.kind === "component" ? [v.id] : [])).size;
3175
3209
  findings2.push(finding(
3176
3210
  home.project.dir,
3177
- `${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`,
3178
3212
  counted.length,
3179
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 })),
3180
3214
  lead,
@@ -3187,7 +3221,7 @@ async function nearDuplicateGroups(input) {
3187
3221
  }
3188
3222
  function finding(project2, title, count, sites, lead, steps, exported) {
3189
3223
  const shown2 = steps.slice(0, STEPS_SHOWN);
3190
- 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)` : "";
3191
3225
  return { kind: "near-duplicate", project: project2, title, detail: null, count, sites, lead, steps, next: `${lead}. ${shown2.join(". ")}${more}`, exported };
3192
3226
  }
3193
3227
  function commonDir(files) {
@@ -3289,7 +3323,7 @@ function factsOf(project2, id, modules, eligible, workspaceNames, dependencies,
3289
3323
  if (dirs.has(file.slice(0, at))) break;
3290
3324
  dirs.add(file.slice(0, at));
3291
3325
  }
3292
- 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);
3293
3327
  const nested = (file) => roots.find((dir) => file.startsWith(`${dir}/`)) ?? "";
3294
3328
  return { project: project2, id, dependencies, packageImports, jsxCounts, eligible, twins, dirs, importer, packageOf: nested };
3295
3329
  }
@@ -3299,13 +3333,6 @@ var import_yaml2 = __toESM(require_dist(), 1);
3299
3333
  import { readFile as readFile5 } from "node:fs/promises";
3300
3334
  import { basename as basename2, join as join17, resolve as resolve10 } from "node:path";
3301
3335
  var UI_FILES = ["**/*.{tsx,jsx,astro,vue,svelte}"];
3302
- async function readJson3(file) {
3303
- try {
3304
- return JSON.parse(await readFile5(file, "utf8"));
3305
- } catch {
3306
- return null;
3307
- }
3308
- }
3309
3336
  var strings = (value) => Array.isArray(value) ? value.filter((v) => typeof v === "string") : [];
3310
3337
  async function workspaceGlobs(root, pkg) {
3311
3338
  const pnpm = await readFile5(join17(root, "pnpm-workspace.yaml"), "utf8").catch(() => null);
@@ -3323,7 +3350,7 @@ async function workspaceGlobs(root, pkg) {
3323
3350
  var hasUi = async (dir) => (await findFiles(dir, UI_FILES, DEFAULT_EXCLUDE)).length > 0;
3324
3351
  async function findProjects(dir) {
3325
3352
  const root = resolve10(dir);
3326
- const pkg = await readJson3(join17(root, "package.json"));
3353
+ const pkg = await readJson(join17(root, "package.json"));
3327
3354
  const name = typeof pkg?.name === "string" && pkg.name.trim() ? pkg.name.trim() : basename2(root);
3328
3355
  const single = { dir: ".", root, name: typeof pkg?.name === "string" ? pkg.name : null };
3329
3356
  const workspace = await workspaceGlobs(root, pkg);
@@ -3339,7 +3366,7 @@ async function findProjects(dir) {
3339
3366
  skipped.push(member);
3340
3367
  continue;
3341
3368
  }
3342
- const memberPkg = await readJson3(join17(memberRoot, "package.json"));
3369
+ const memberPkg = await readJson(join17(memberRoot, "package.json"));
3343
3370
  projects.push({ dir: member, root: memberRoot, name: typeof memberPkg?.name === "string" ? memberPkg.name : null });
3344
3371
  }
3345
3372
  if (projects.length === 0) return { root, name, layout: `one project (${workspace.source} names no package with UI files)`, projects: [single], skipped };
@@ -3349,13 +3376,10 @@ async function findProjects(dir) {
3349
3376
  // ../cli/src/report/model.ts
3350
3377
  var REPORT_SCHEMA = "youiyoux.report/1";
3351
3378
  var NPX = `npx ${COMMAND}`;
3352
- var SITES_PER_FINDING2 = 6;
3353
3379
  var TOP_COMPONENTS = 10;
3354
3380
  var TOP_FIXES = 5;
3355
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.`;
3356
- var plural3 = (count, word, many = `${word}s`) => `${count} ${count === 1 ? word : many}`;
3357
3382
  var sentence = (text) => text.charAt(0).toUpperCase() + text.slice(1);
3358
- var repoPath2 = (dir, file) => dir === "." ? file : `${dir}/${file}`;
3359
3383
  var errorText = (error) => error instanceof Error ? error.message : String(error);
3360
3384
  var Lines = class {
3361
3385
  files = /* @__PURE__ */ new Map();
@@ -3392,7 +3416,7 @@ async function modulesOf(project2, target, failed) {
3392
3416
  const module = await parseModule(file, await readFile6(join18(project2.root, file), "utf8"));
3393
3417
  if (module) modules.push(module);
3394
3418
  } catch (error) {
3395
- failed.push(`${repoPath2(project2.dir, file)}: ${errorText(error)}`);
3419
+ failed.push(`${repoPath(project2.dir, file)}: ${errorText(error)}`);
3396
3420
  }
3397
3421
  }
3398
3422
  return modules;
@@ -3406,8 +3430,9 @@ async function grouped(project2, found, lines, describe) {
3406
3430
  const findings2 = [];
3407
3431
  for (const list of groups.values()) {
3408
3432
  const first = list[0];
3409
- 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)));
3410
- 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 });
3411
3436
  }
3412
3437
  return findings2.sort((a, b) => b.count - a.count || a.title.localeCompare(b.title));
3413
3438
  }
@@ -3437,8 +3462,8 @@ async function analyseProject(project2, index, inventory, inventoryError, worksp
3437
3462
  if (isShowcase(at)) showcaseFiles.add(at.replace(/:\d+(?::\d+)?$/, ""));
3438
3463
  }
3439
3464
  const live = (items) => items.flatMap((item) => {
3440
- const where = item.where?.filter((at) => !isShowcase(at));
3441
- 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) } : {} }];
3442
3467
  });
3443
3468
  const signals = live(scan.signals);
3444
3469
  const used = new Set(signals.filter((s) => s.kind !== "package" && s.kind !== "lockfile").map((s) => s.library));
@@ -3456,14 +3481,14 @@ async function analyseProject(project2, index, inventory, inventoryError, worksp
3456
3481
  for (const look of looks) {
3457
3482
  if (look.recipe?.kind !== "component") continue;
3458
3483
  const key2 = `${look.recipe.file}#${look.recipe.export}`;
3459
- 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: [] };
3460
3485
  entry.roles.push(look.role);
3461
3486
  byComponent.set(key2, entry);
3462
3487
  }
3463
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);
3464
3489
  const inventoryProject = inventory?.projects[index];
3465
3490
  if (inventoryError) incomplete.push(`The component inventory could not be built (${inventoryError}); run ${NPX} inventory --project ${project2.dir} to see why`);
3466
- 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}`);
3467
3492
  let hardcoded = [];
3468
3493
  let offLibrary = [];
3469
3494
  const everyLine = () => true;
@@ -3475,7 +3500,7 @@ async function analyseProject(project2, index, inventory, inventoryError, worksp
3475
3500
  const found = (await Promise.all(modules.map((module) => reportTokens(module, tokens, usage)))).flat();
3476
3501
  hardcoded = await grouped(project2, found, lines, (first, count) => {
3477
3502
  const { word, value, tokens: names } = hardcodedValue(first);
3478
- 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` };
3479
3504
  });
3480
3505
  } catch (error) {
3481
3506
  incomplete.push(`Hardcoded values were not checked (${errorText(error)}); run ${NPX} check --diff to see why`);
@@ -3487,7 +3512,7 @@ async function analyseProject(project2, index, inventory, inventoryError, worksp
3487
3512
  const kitDirs = [changeProject.config.componentsDir, ...inventory?.kits.filter((k) => k.project === projectId && k.kind === "cva-folder").map((k) => k.path) ?? []];
3488
3513
  const exempt = (file) => kitDirs.some((dir) => file === dir || file.startsWith(`${dir}/`));
3489
3514
  const found = modules.flatMap((module) => offLibraryImports(module, { added: everyLine, classify, chosen, exempt }));
3490
- 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) }));
3491
3516
  } catch (error) {
3492
3517
  incomplete.push(`Library choices were not compared (${errorText(error)}); run ${NPX} detect ${project2.dir} to see why`);
3493
3518
  }
@@ -3504,7 +3529,7 @@ async function analyseProject(project2, index, inventory, inventoryError, worksp
3504
3529
  libraries,
3505
3530
  unused,
3506
3531
  definitions: inventory ? inventory.definitions.filter((d) => d.project === projectId && !isShowcase(d.file)).length : 0,
3507
- 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 })) : [],
3508
3533
  roles,
3509
3534
  uses: looks.length,
3510
3535
  components,
@@ -3517,7 +3542,7 @@ async function analyseProject(project2, index, inventory, inventoryError, worksp
3517
3542
  return { section, facts };
3518
3543
  }
3519
3544
  function topNext(f) {
3520
- 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;
3521
3546
  return `${text.charAt(0).toLowerCase()}${text.slice(1).replace(/\.$/, "")}`;
3522
3547
  }
3523
3548
  var DUPLICATE_RULE = "Duplicated components: groups of 2+ exported components that look alike or share a name.";
@@ -3527,10 +3552,10 @@ function summaryLines(model) {
3527
3552
  const top = model.projects.flatMap((p) => p.components).sort((a, b) => b.uses - a.uses)[0];
3528
3553
  const first = model.topFixes[0];
3529
3554
  return [
3530
- `${model.repo}: ${plural3(t.projects, "project")}, ${plural3(t.files, "file")} scanned; UI libraries: ${t.libraries.length > 0 ? t.libraries.join(", ") : "none found"}.`,
3531
- `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")})` : ""}.`,
3532
- `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.`,
3533
- `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}.`,
3534
3559
  first ? `Top fix: ${first.title.replace(/\.$/, "")}; ${topNext(first)}.` : `Nothing to fix first; next: ${NPX} agents install, so your agent keeps it that way.`
3535
3560
  ];
3536
3561
  }
@@ -3565,7 +3590,7 @@ async function buildReportModel(dir, options = {}) {
3565
3590
  offLibrary: projects.reduce((n, p) => n + p.offLibrary.reduce((m, f) => m + f.count, 0), 0)
3566
3591
  };
3567
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);
3568
- 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;
3569
3594
  const model = {
3570
3595
  schema: REPORT_SCHEMA,
3571
3596
  repo: repo.name,
@@ -3587,7 +3612,6 @@ async function buildReportModel(dir, options = {}) {
3587
3612
 
3588
3613
  // ../cli/src/report/html.ts
3589
3614
  var escape2 = (text) => String(text).replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#39;");
3590
- var plural4 = (count, word, many = `${word}s`) => `${count} ${count === 1 ? word : many}`;
3591
3615
  var anchor = (dir) => `p-${dir === "." ? "root" : dir.replace(/[^a-zA-Z0-9]+/g, "-").replace(/^-|-$/g, "")}`;
3592
3616
  var CSS = `
3593
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)}
@@ -3765,7 +3789,6 @@ var rich = (text) => escape2(text).replace(FACT, (fact) => `<span class="mono">$
3765
3789
  function next(text) {
3766
3790
  return `<p class="next"><strong>Next:</strong> ${rich(text.replace(/\.$/, ""))}.</p>`;
3767
3791
  }
3768
- var STEPS_SHOWN2 = 5;
3769
3792
  var KIND = { "near-duplicate": "Duplicate", "hardcoded-value": "Hardcoded", "off-library": "Off-library" };
3770
3793
  function splitMeasures(title) {
3771
3794
  const m = /\s*\(([^()]*\d[^()]*)\)/.exec(title);
@@ -3789,18 +3812,18 @@ function shots(f, looks) {
3789
3812
  }
3790
3813
  function finding2(f, showProject, looks) {
3791
3814
  const { text, measures } = f.kind === "near-duplicate" ? splitMeasures(f.title) : { text: f.title, measures: [] };
3792
- const where = showProject && f.project !== "." ? `<p class="muted">in <span class="mono">${escape2(f.project)}</span></p>` : "";
3793
- 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>`;
3794
3817
  const specs = measures.length > 0 ? `<ul class="specs">${measures.map((m) => `<li>${escape2(m)}</li>`).join("")}</ul>` : "";
3795
3818
  if (f.lead && f.steps && f.steps.length > 0) {
3796
3819
  const item = (step) => `<li>${rich(step.replace(/\.$/, ""))}</li>`;
3797
- const rest = f.steps.slice(STEPS_SHOWN2);
3798
- 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>` : "";
3799
- 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>`;
3800
3823
  }
3801
3824
  const code = f.sites.some((s) => s.code);
3802
3825
  const sites = f.sites.map((s) => `<li><code>${escape2(s.at)}</code>${s.code ? `<pre>${escape2(s.code)}</pre>` : ""}</li>`).join("");
3803
- 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>` : "";
3804
3827
  return `<li class="finding" data-kind="${f.kind}">${head}${specs}<ul class="sites${code ? " code" : ""}">${sites}${more}</ul>${next(f.next)}</li>`;
3805
3828
  }
3806
3829
  function findings(list, none, after, looks) {
@@ -3812,29 +3835,29 @@ function panel(name, id, title, aside, body, level = "h3") {
3812
3835
  }
3813
3836
  function projectSection(p, many, looks) {
3814
3837
  const id = anchor(p.dir);
3815
- const where = p.dir === "." ? "" : ` --project ${p.dir}`;
3838
+ const where2 = p.dir === "." ? "" : ` --project ${p.dir}`;
3816
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>`;
3817
- 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>` : "";
3818
- 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>` : "";
3819
- 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>`;
3820
- 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>` : "";
3821
- 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")}`;
3822
3845
  return `<section class="project" data-section="project" data-project="${escape2(p.dir)}" id="${id}">
3823
3846
  <h2>${escape2(many || p.dir !== "." ? p.dir : "This repo")}${p.name && p.name !== p.dir ? ` <small>${escape2(p.name)}</small>` : ""}</h2>
3824
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>` : ""}
3825
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)}`)}
3826
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)}`)}
3827
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)}`)}
3828
- ${panel("inventory", `${id}-inventory`, "Component inventory", `${plural4(p.definitions, "component")} \xB7 ${plural4(p.uses, "use")}`, inventory)}
3829
- ${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)}
3830
3853
  ${p.notes.map((n) => `<p class="muted">${escape2(n.replace(/\.?$/, "."))}</p>`).join("")}
3831
3854
  </section>`;
3832
3855
  }
3833
3856
  function looksSection(model) {
3834
3857
  const drawn = model.projects.flatMap((p) => p.rendered.map((r) => ({ ...r, project: p.dir })));
3835
- 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>` : "";
3836
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`);
3837
- 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>`;
3838
3861
  }
3839
3862
  function placesByKind(model) {
3840
3863
  const places = (pick) => model.projects.reduce((n, p) => n + pick(p).reduce((m, f) => m + f.count, 0), 0);
@@ -3846,25 +3869,21 @@ function placesByKind(model) {
3846
3869
  }
3847
3870
  function headline(model) {
3848
3871
  const t = model.totals;
3849
- const [near, hard, off] = placesByKind(model);
3850
- const parts = [
3851
- { label: "Near-duplicates", places: near.places, href: model.projects.length > 1 ? "top-fixes" : `${anchor(model.projects[0]?.dir ?? ".")}-near` },
3852
- { label: "Hardcoded values", places: hard.places, href: model.projects.length > 1 ? "top-fixes" : `${anchor(model.projects[0]?.dir ?? ".")}-hardcoded` },
3853
- { label: "Off-library imports", places: off.places, href: model.projects.length > 1 ? "top-fixes" : `${anchor(model.projects[0]?.dir ?? ".")}-off` }
3854
- ];
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]}` }));
3855
3874
  const total = parts.reduce((n2, part) => n2 + part.places, 0);
3856
3875
  const n = (value) => `<b class="${value > 0 ? "warn" : "ok"}">${value}</b>`;
3857
3876
  const unknown = model.projects.some((p) => p.incomplete.length > 0 || p.failed.length > 0);
3858
3877
  const analyses = model.projects.reduce((n2, p) => n2 + p.incomplete.length, 0);
3859
3878
  const unread = model.projects.reduce((n2, p) => n2 + p.failed.length, 0);
3860
- 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 ");
3861
3880
  const said = [];
3862
3881
  if (t.nearDuplicates > 0) said.push(`${n(t.nearDuplicates)} ${t.nearDuplicates === 1 ? "look is" : "looks are"} built more than once`);
3863
3882
  if (t.hardcoded > 0) said.push(`${n(t.hardcoded)} ${t.hardcoded === 1 ? "value skips" : "values skip"} the theme`);
3864
3883
  if (t.offLibrary > 0) said.push(`${n(t.offLibrary)} ${t.offLibrary === 1 ? "import comes" : "imports come"} from a second library`);
3865
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.`;
3866
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>` : "";
3867
- 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>`;
3868
3887
  return `<section class="headline" data-section="headline"><p class="say">${sentence2}</p>${bar2}${legend}</section>`;
3869
3888
  }
3870
3889
  var SQUARES = 40;
@@ -3878,7 +3897,7 @@ function shapeMap(model) {
3878
3897
  const rows = [...roles].map(([role, { uses, families }]) => {
3879
3898
  const shown2 = Math.min(families, SQUARES);
3880
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>` : ""}`;
3881
- 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>`;
3882
3901
  }).join("");
3883
3902
  const many = model.projects.length > 1 ? ", summed over the projects" : "";
3884
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>`;
@@ -3925,7 +3944,7 @@ ${shapeMap(model)}
3925
3944
  ${panel("top-fixes", "top-fixes", "Top fixes", "Ranked by how many places each one touches", top, "h2")}
3926
3945
  <aside class="agent" data-section="agent"><p><strong>${escape2(model.agent)}</strong></p>${next(`${NPX} agents install, then restart your agent`)}</aside>
3927
3946
  ${looksSection(model)}
3928
- <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>
3929
3948
  ${model.projects.map((p) => projectSection(p, many, looks)).join("\n")}
3930
3949
  <footer>
3931
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>
@@ -3940,12 +3959,13 @@ ${model.projects.map((p) => projectSection(p, many, looks)).join("\n")}
3940
3959
 
3941
3960
  // ../cli/src/report/looks.ts
3942
3961
  import { spawn as spawn2 } from "node:child_process";
3943
- import { existsSync as existsSync3, readFileSync as readFileSync2 } from "node:fs";
3962
+ import { existsSync as existsSync4, readFileSync as readFileSync2 } from "node:fs";
3944
3963
  import { homedir as homedir2 } from "node:os";
3945
3964
  import { dirname as dirname10, join as join19 } from "node:path";
3946
3965
  var LOOKS_PER_PROJECT = 4;
3947
3966
  var DOWNLOAD_SIZE = "~100 MB";
3948
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;
3949
3969
  var playwrightDir = () => dirname10(playwrightCli());
3950
3970
  function browsersPath(env) {
3951
3971
  const set = env.PLAYWRIGHT_BROWSERS_PATH;
@@ -3956,12 +3976,12 @@ function browsersPath(env) {
3956
3976
  return join19(env.XDG_CACHE_HOME ?? join19(homedir2(), ".cache"), "ms-playwright");
3957
3977
  }
3958
3978
  function chromiumStatus(env = process.env) {
3959
- 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 };
3960
3980
  try {
3961
3981
  const browsers = JSON.parse(readFileSync2(join19(playwrightDir(), "browsers.json"), "utf8"));
3962
3982
  const revision2 = browsers.browsers.find((b) => b.name === "chromium-headless-shell")?.revision;
3963
3983
  const dir = join19(browsersPath(env), `chromium_headless_shell-${revision2}`);
3964
- 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 };
3965
3985
  } catch {
3966
3986
  return { available: false, where: "playwright-core is not installed" };
3967
3987
  }
@@ -3973,7 +3993,7 @@ function downloadChromium() {
3973
3993
  child.on("close", (code) => resolve13(code === 0));
3974
3994
  });
3975
3995
  }
3976
- 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;
3977
3997
  async function addLooks(model, repoRoot, options = {}) {
3978
3998
  const drawn = [];
3979
3999
  const problems = [];
@@ -3981,34 +4001,34 @@ async function addLooks(model, repoRoot, options = {}) {
3981
4001
  for (const section of model.projects) {
3982
4002
  const names = [...new Set(section.components.map((c) => c.name))].slice(0, LOOKS_PER_PROJECT);
3983
4003
  if (names.length === 0) continue;
3984
- const where = section.dir === "." ? "this repo" : section.dir;
4004
+ const where2 = section.dir === "." ? "this repo" : section.dir;
3985
4005
  try {
3986
4006
  const installed = (name) => {
3987
4007
  let dir = join19(repoRoot, section.dir);
3988
4008
  while (true) {
3989
- if (existsSync3(join19(dir, "node_modules", name, "package.json"))) return true;
4009
+ if (existsSync4(join19(dir, "node_modules", name, "package.json"))) return true;
3990
4010
  if (dir === repoRoot || dirname10(dir) === dir) return false;
3991
4011
  dir = dirname10(dir);
3992
4012
  }
3993
4013
  };
3994
4014
  if (!installed("react") || !installed("react-dom")) {
3995
- missing.push(where);
4015
+ missing.push(where2);
3996
4016
  continue;
3997
4017
  }
3998
4018
  const projectRoot = join19(repoRoot, section.dir);
3999
4019
  const target = await loadInventoryTarget(projectRoot);
4000
4020
  if (findStylesheets(projectRoot, target.files, { tsconfig: target.tsconfig, override: target.specimenCss, templates: target.templates }).found.length === 0) {
4001
- missing.push(where);
4021
+ missing.push(where2);
4002
4022
  continue;
4003
4023
  }
4004
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 } : {} });
4005
- 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));
4006
4026
  if (unavailable) {
4007
- missing.push(where);
4027
+ missing.push(where2);
4008
4028
  continue;
4009
4029
  }
4010
4030
  for (const component of report.components) {
4011
- 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))) {
4012
4032
  missing.push(component.component);
4013
4033
  continue;
4014
4034
  }
@@ -4024,7 +4044,7 @@ async function addLooks(model, repoRoot, options = {}) {
4024
4044
  drawn.push(component.component);
4025
4045
  }
4026
4046
  } catch (error) {
4027
- problems.push(`${where}: ${error instanceof Error ? error.message : String(error)}`);
4047
+ problems.push(`${where2}: ${error instanceof Error ? error.message : String(error)}`);
4028
4048
  if (error instanceof RenderUnavailableError) break;
4029
4049
  }
4030
4050
  }
@@ -4038,7 +4058,6 @@ function looksOff(model) {
4038
4058
  }
4039
4059
 
4040
4060
  // ../cli/src/report/markdown.ts
4041
- var STEPS_SHOWN3 = 5;
4042
4061
  var md = (text) => text.replace(/([\\`*_[\]<>|#])/g, "\\$1");
4043
4062
  function renderMarkdown(model, reportFile) {
4044
4063
  const t = model.totals;
@@ -4059,8 +4078,8 @@ function renderMarkdown(model, reportFile) {
4059
4078
  model.topFixes.forEach((fix, i) => {
4060
4079
  lines.push(`${i + 1}. ${md(fix.title)}${model.projects.length > 1 && fix.project !== "." ? ` (${md(fix.project)})` : ""} `);
4061
4080
  if (fix.lead && fix.steps && fix.steps.length > 0) {
4062
- lines.push(` Next: ${md(fix.lead)}.`, ...fix.steps.slice(0, STEPS_SHOWN3).map((step) => ` - ${md(step.replace(/\.$/, ""))}`));
4063
- 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`);
4064
4083
  } else lines.push(` Next: ${md(fix.next.replace(/\.$/, ""))}.`);
4065
4084
  });
4066
4085
  lines.push("");
@@ -4218,7 +4237,7 @@ async function writeBinaryAtomic(file, bytes) {
4218
4237
  await rename2(temp, file);
4219
4238
  }
4220
4239
  var shown = (file) => {
4221
- const rel = relative8(process.cwd(), file);
4240
+ const rel = relative7(process.cwd(), file);
4222
4241
  return rel && !rel.startsWith("..") ? rel : file;
4223
4242
  };
4224
4243
  async function runReport(options = {}, io = {}) {
@@ -4275,8 +4294,8 @@ async function runReport(options = {}, io = {}) {
4275
4294
  print2(JSON.stringify({ ...clean, projects: clean.projects.map((p) => ({ ...p, rendered: p.rendered.map(({ light: _l, dark: _d, ...r }) => r) })), files }, null, 2));
4276
4295
  return { code: 0, files, model: clean };
4277
4296
  }
4278
- const width = Math.max(...["Report", "Card", "Summary"].map((w) => w.length)) + 2;
4279
- 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` : "";
4280
4299
  print2([
4281
4300
  ...clean.summary,
4282
4301
  "",
@@ -4318,6 +4337,10 @@ ${style.bold("Anywhere")} ${style.dim("no design system needed")}
4318
4337
  <first project>/.youi/inventory.json unless --out, and records the scan
4319
4338
  (project, commit, time, counts, families) in the local index for trends
4320
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
4321
4344
  requests [--status open|addressed|closed|all] [--route r] [--json]
4322
4345
  comments and change requests people pinned to components on captured
4323
4346
  screens: what is asked, the file:line, the picture, and what to do next
@@ -4327,6 +4350,9 @@ ${style.bold("Anywhere")} ${style.dim("no design system needed")}
4327
4350
  photograph the routes in <project>/.youi/screens/screens.json, light and
4328
4351
  dark, from the app running in development at <url>, and place each
4329
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)
4330
4356
  render [file|component\u2026] [--base ref] [--json]
4331
4357
  see a component you changed: its looks drawn with the project's own code
4332
4358
  and CSS in headless Chromium, light and dark at 1280 and 390 px, axe's
@@ -4508,7 +4534,7 @@ var OPTIONS = {
4508
4534
  var INVENTORY_OPTIONS = { ...OPTIONS, project: { type: "string", multiple: true }, include: { type: "string", multiple: true }, exclude: { type: "string", multiple: true } };
4509
4535
  var UsageError = class extends Error {
4510
4536
  };
4511
- var run7 = promisify6(execFile6);
4537
+ var run6 = promisify5(execFile5);
4512
4538
  var print = (text) => void process.stdout.write(`${text}
4513
4539
  `);
4514
4540
  var json = (value) => print(JSON.stringify(value, null, 2));
@@ -4540,23 +4566,44 @@ async function recordUsage(values, p, report, usage) {
4540
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 } });
4541
4567
  return null;
4542
4568
  } catch (error) {
4543
- return style.warn(`! not recorded in the local index: ${error instanceof Error ? error.message : String(error)}`);
4569
+ return notRecorded(error);
4544
4570
  }
4545
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 } : {} });
4546
4575
  function indexOf(values) {
4547
4576
  const path = discoverIndexPath(values.cache);
4548
4577
  return { path, home: indexHome(path) };
4549
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
+ }
4550
4597
  async function cmdInventory(values) {
4551
4598
  if (values.history) return cmdHistory(values, []);
4552
- const dirs = values.project?.length ? values.project : [process.cwd()];
4553
- 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);
4554
4601
  const out = resolve12(values.out ?? join22(dirs[0], ".youi/inventory.json"));
4555
4602
  await writeFileAtomic(out, `${JSON.stringify(report, null, 2)}
4556
4603
  `);
4557
4604
  const recorded = await recordScan(values, report, out);
4558
4605
  if (values.json) json(report);
4559
- 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"));
4560
4607
  return 0;
4561
4608
  }
4562
4609
  async function recordScan(values, report, out) {
@@ -4567,7 +4614,7 @@ async function recordScan(values, report, out) {
4567
4614
  const scans = (await store.history({ limit: 500 })).filter((s) => report.projects.some((p) => p.id === s.project)).length;
4568
4615
  return style.dim(`history \u2192 ${store.location} (${plural(scans, "scan")} of these projects; youiyoux inventory --history)`);
4569
4616
  } catch (error) {
4570
- return style.warn(`! not recorded in the local index: ${error instanceof Error ? error.message : String(error)}`);
4617
+ return notRecorded(error);
4571
4618
  }
4572
4619
  }
4573
4620
  async function cmdHistory(values, positionals) {
@@ -4606,9 +4653,9 @@ async function cmdWhere(values, positionals) {
4606
4653
  const p = await project(values);
4607
4654
  const { report } = await scanProject(p);
4608
4655
  const limit = values.all ? Number.MAX_SAFE_INTEGER : Number(values.limit ?? (values.json ? 20 : 24));
4609
- const where = whereUsed(p, report, query, { ...values.route ? { route: values.route } : {}, limit });
4610
- if (values.json) json(where);
4611
- 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));
4612
4659
  return 0;
4613
4660
  }
4614
4661
  async function cmdShow(values, positionals) {
@@ -4635,9 +4682,10 @@ function baselineFile(values, p) {
4635
4682
  if (values["baseline-file"]) return resolve12(values["baseline-file"]);
4636
4683
  return values.baseline ? baselinePath(p, values.config) : null;
4637
4684
  }
4685
+ var checkScope = (values, positionals) => ({ ...positionals.length > 0 ? { files: positionals } : {}, ...values.rule ? { rule: values.rule } : {}, quiet: values.quiet });
4638
4686
  async function repoPrefix(root) {
4639
4687
  try {
4640
- 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();
4641
4689
  } catch {
4642
4690
  return "";
4643
4691
  }
@@ -4647,7 +4695,7 @@ async function cmdCheck(values, positionals) {
4647
4695
  if (!["text", "json", "github"].includes(format)) throw new UsageError("youiyoux check --format text|json|github");
4648
4696
  if (values.diff || values.base !== void 0) return cmdCheckDiff(values, positionals, await loadCheckProject(values.project ?? process.cwd(), loadOptions(values), !values.project), format);
4649
4697
  const p = await project(values);
4650
- 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) });
4651
4699
  if (format === "json") {
4652
4700
  const { report } = result.scan;
4653
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 });
@@ -4661,7 +4709,7 @@ async function cmdCheck(values, positionals) {
4661
4709
  async function cmdCheckDiff(values, positionals, p, format) {
4662
4710
  if (values.baseline) throw new UsageError("youiyoux check --diff looks only at added lines, so a baseline does not apply; drop --baseline");
4663
4711
  const change = await workingTreeChange(p, values.base);
4664
- 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);
4665
4713
  if (format === "json") json(diffJson(result));
4666
4714
  else if (format === "github") {
4667
4715
  for (const line of githubDiffAnnotations(result, await repoPrefix(p.root))) print(line);
@@ -4688,7 +4736,7 @@ async function cmdBaseline(values) {
4688
4736
  const { totals } = written.baseline;
4689
4737
  if (values.json) json({ file: written.file, action: written.action, totals, notAdded: written.notAdded, identity: written.baseline.identity });
4690
4738
  else {
4691
- 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`);
4692
4740
  print(style.dim(` rules ${describeIdentity(written.baseline.identity)}`));
4693
4741
  if (written.notAdded > 0) print(style.warn(` ${plural(written.notAdded, "current violation")} not added: fix them, or accept them deliberately with --force`));
4694
4742
  }
@@ -4852,7 +4900,7 @@ async function cmdAgents(values, positionals) {
4852
4900
  const repoRoot = await repoRootOf(projectRoot);
4853
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 || ".") } : {} };
4854
4902
  const server = mcpCommand(TOOL_ROOT, target);
4855
- 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;
4856
4904
  const claudeServer = system && system.root === repoRoot ? { command: "node", args: mcpArgs(system) } : relativeTo(repoRoot, server);
4857
4905
  const both = !values.claude && !values.codex;
4858
4906
  const results = [];
@@ -4910,10 +4958,10 @@ async function startEditor(values, port) {
4910
4958
  if (!values.project) return null;
4911
4959
  try {
4912
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 } : {} });
4913
- const { design, project: product } = editor.info;
4961
+ const { design, project: product2 } = editor.info;
4914
4962
  print(`${style.ok("rule editor")} http://localhost:${port}/#key=${editor.sessionKey}`);
4915
4963
  print(style.dim(` design ${design.root} (${design.branch} @ ${design.head.slice(0, 7)}${design.dirty ? ", working tree differs" : ""})`));
4916
- print(style.dim(` project ${product.id} at ${product.root}`));
4964
+ print(style.dim(` project ${product2.id} at ${product2.root}`));
4917
4965
  print(style.dim(` drafts ${editor.stateDir}`));
4918
4966
  print(style.dim(" open the link above: it carries this run's key, which every change needs (nothing else can approve)"));
4919
4967
  print(style.dim(" approved changes become a local review branch; nothing is pushed"));
@@ -4935,20 +4983,20 @@ async function cmdUi(values) {
4935
4983
  async function cmdDetect(values, positionals) {
4936
4984
  const target = detectTarget(positionals);
4937
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)");
4938
- const { repoReport, siteReport, writeReport } = await import("./src-DS43LCRA.js");
4986
+ const { repoReport, siteReport, writeReport } = await import("./src-VL2ZYO6X.js");
4939
4987
  const out = resolve12(values.out ?? ".youi/detect");
4940
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 } : {} });
4941
4989
  const file = await writeReport(report, out);
4942
4990
  if (values.json) json(report);
4943
4991
  else {
4944
- const shown2 = relative9(process.cwd(), file);
4992
+ const shown2 = relative8(process.cwd(), file);
4945
4993
  print(renderDetect(report, shown2 && !shown2.startsWith("..") ? shown2 : file));
4946
4994
  }
4947
4995
  return 0;
4948
4996
  }
4949
4997
  async function cmdRender(values, positionals, io) {
4950
4998
  const root = resolve12(values.root ?? values.project ?? process.cwd());
4951
- const targets = positionals.map((t) => existsSync4(resolve12(t)) ? resolve12(t) : t);
4999
+ const targets = positionals.map((t) => existsSync5(resolve12(t)) ? resolve12(t) : t);
4952
5000
  try {
4953
5001
  const report = await renderShots(root, targets, { ...values.base ? { base: values.base } : {}, ...process.env.YOUI_CHROMIUM ? { executablePath: process.env.YOUI_CHROMIUM } : {}, ...io.render });
4954
5002
  if (values.json) json({ ...report, next: shotsNext(report, (t) => `run youiyoux render${t ? ` ${t}` : ""} again`) });
@@ -4961,13 +5009,16 @@ async function cmdRender(values, positionals, io) {
4961
5009
  return 1;
4962
5010
  }
4963
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
+ }
4964
5016
  async function cmdRequests(values, positionals) {
4965
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>";
4966
5018
  const [action, id, ...rest] = positionals;
4967
5019
  if (rest.length > 0) throw new UsageError(usage);
4968
- const { RequestStore, requestBrief } = await import("./src-U5NTDPUX.js");
4969
- const project2 = await loadProject(resolve12(values.project ?? process.cwd()), loadOptions(values)).catch(() => null);
4970
- const root = project2?.root ?? resolve12(values.root ?? values.project ?? process.cwd());
5020
+ const { RequestStore, requestBrief } = await import("./src-34R7QMJC.js");
5021
+ const { project: project2, root } = await product(values);
4971
5022
  const store = new RequestStore(root, project2?.config.id ?? basename3(root));
4972
5023
  const agent = { kind: "agent", name: "youiyoux CLI" };
4973
5024
  const one = (request) => {
@@ -4994,20 +5045,24 @@ async function cmdRequests(values, positionals) {
4994
5045
  }
4995
5046
  throw new UsageError(usage);
4996
5047
  }
4997
- async function cmdScreens(values, positionals) {
4998
- if (positionals[0] !== "capture" || positionals.length > 1 || !values.base) throw new UsageError("youiyoux screens capture --base <url of the app in development> [--route <one route>] [--project dir] [--out <screens dir>] [--storage-state <signed-in session>] [--json]");
4999
- const project2 = await loadProject(resolve12(values.project ?? process.cwd()), loadOptions(values)).catch(() => null);
5000
- const root = project2?.root ?? resolve12(values.root ?? values.project ?? process.cwd());
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);
5001
5053
  const dir = resolve12(values.out ?? join22(root, ".youi/screens"));
5002
- if (!existsSync4(join22(dir, "screens.json"))) {
5054
+ if (!existsSync5(join22(dir, "screens.json"))) {
5003
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":"/"}]}
5004
5056
  `);
5005
5057
  return 1;
5006
5058
  }
5007
- const { launchBrowser } = await import("./src-DS43LCRA.js");
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");
5008
5063
  const browser = await launchBrowser(process.env.YOUI_CHROMIUM, { purpose: { what: "youiyoux screens capture", command: "youiyoux screens capture" } });
5009
5064
  try {
5010
- const report = await captureScreens(dir, { base: values.base, root, browser, ...values.route ? { routes: [values.route] } : {}, ...values["storage-state"] ? { storageState: resolve12(values["storage-state"]) } : {}, log: (line) => process.stderr.write(`${style.dim(line)}
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)}
5011
5066
  `) });
5012
5067
  if (values.json) json(report);
5013
5068
  else print(captureText(report));
@@ -5016,13 +5071,52 @@ async function cmdScreens(values, positionals) {
5016
5071
  await browser.close();
5017
5072
  }
5018
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
+ }
5019
5113
  async function cmdReport(values, positionals, io) {
5020
5114
  if (positionals.length > 1) throw new UsageError("youiyoux report [path] [--out dir] [--render [--yes]] [--include-code] [--json] (one repo)");
5021
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 ?? {});
5022
5116
  return result.code;
5023
5117
  }
5024
5118
  async function cmdMcp(values) {
5025
- const { startStdioServer } = await import("./src-YGEZCUAO.js");
5119
+ const { startStdioServer } = await import("./src-E25GHWWT.js");
5026
5120
  await startStdioServer({ projectRoot: values.project ?? process.cwd(), ...loadOptions(values), ...values.cache ? { discoverCache: values.cache } : {} });
5027
5121
  return -1;
5028
5122
  }
@@ -5112,13 +5206,13 @@ var COMMANDS = {
5112
5206
  parity: cmdParity,
5113
5207
  schema: cmdSchema
5114
5208
  };
5115
- var COMMAND_NAMES = [...Object.keys(COMMANDS), "inventory", "help"];
5209
+ var COMMAND_NAMES = [...Object.keys(COMMANDS), "inventory", "gaps", "help"];
5116
5210
  async function main(argv, io = {}) {
5117
5211
  let parsed;
5118
5212
  try {
5119
- if (argv[0] === "inventory") {
5213
+ if (argv[0] === "inventory" || argv[0] === "gaps") {
5120
5214
  const inventory = parseArgs({ args: argv, options: INVENTORY_OPTIONS, allowPositionals: true });
5121
- 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));
5122
5216
  }
5123
5217
  parsed = parseArgs({ args: diffArgs(argv), options: OPTIONS, allowPositionals: true });
5124
5218
  } catch (error) {