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