@stylecrib/cli 0.3.1 → 0.3.2
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 +94 -9
- package/agents/MANIFEST.yml +136 -0
- package/agents/README.md +75 -0
- package/agents/art-director.md +168 -0
- package/agents/audacity-director.md +109 -0
- package/agents/brand-identity.md +179 -0
- package/agents/brand-precedent.md +119 -0
- package/agents/brief-enforcer.md +136 -0
- package/agents/component-qa.md +167 -0
- package/agents/cto.md +49 -0
- package/agents/design-director.md +184 -0
- package/agents/design-systems-engineer.md +162 -0
- package/agents/direction-curator.md +133 -0
- package/agents/homogeneity-detector.md +104 -0
- package/agents/logo-designer.md +240 -0
- package/agents/marketing-designer.md +156 -0
- package/agents/media-curator.md +165 -0
- package/agents/motion-engineer.md +270 -0
- package/agents/qa-conductor.md +143 -0
- package/agents/render-qa.md +133 -0
- package/agents/upgrade-simulator.md +123 -0
- package/agents/vp-architecture.md +49 -0
- package/agents/vp-engineering.md +49 -0
- package/dist/index.js +1577 -463
- package/package.json +11 -4
- package/themes/README.md +70 -0
- package/themes/acrylic-soft/THEME.md +142 -0
- package/themes/acrylic-soft/tokens.ts +180 -0
- package/themes/agency-creative/THEME.md +91 -0
- package/themes/agency-creative/tokens.ts +202 -0
- package/themes/agency-portfolio/THEME.md +109 -0
- package/themes/agency-portfolio/tokens.ts +159 -0
- package/themes/ai-startup/THEME.md +95 -0
- package/themes/ai-startup/tokens.ts +162 -0
- package/themes/airline-modern/THEME.md +104 -0
- package/themes/airline-modern/tokens.ts +179 -0
- package/themes/arc-stack/.anchor-render.png +0 -0
- package/themes/arc-stack/THEME.md +125 -0
- package/themes/arc-stack/audit.log.jsonl +6 -0
- package/themes/arc-stack/dismissals.jsonl +3 -0
- package/themes/arc-stack/tailwind.snippet.ts +17 -0
- package/themes/arc-stack/theme.yml +153 -0
- package/themes/arc-stack/tokens.ts +29 -0
- package/themes/architectural/THEME.md +102 -0
- package/themes/architectural/tokens.ts +159 -0
- package/themes/atelier-radiance/THEME.md +93 -0
- package/themes/atelier-radiance/audit.log.jsonl +1 -0
- package/themes/atelier-radiance/tailwind.snippet.ts +15 -0
- package/themes/atelier-radiance/theme.yml +130 -0
- package/themes/atelier-radiance/tokens.ts +29 -0
- package/themes/automotive-ev/THEME.md +87 -0
- package/themes/automotive-ev/tokens.ts +183 -0
- package/themes/automotive-luxury/THEME.md +88 -0
- package/themes/automotive-luxury/tokens.ts +175 -0
- package/themes/baby-modern/THEME.md +79 -0
- package/themes/baby-modern/tokens.ts +184 -0
- package/themes/banking-gravity/THEME.md +171 -0
- package/themes/banking-gravity/tokens.ts +169 -0
- package/themes/beauty-clean/THEME.md +92 -0
- package/themes/beauty-clean/tokens.ts +176 -0
- package/themes/beauty-luxury/THEME.md +96 -0
- package/themes/beauty-luxury/tokens.ts +178 -0
- package/themes/bento-modular/.anchor-render.png +0 -0
- package/themes/bento-modular/BUILD_BRIEF.md +171 -0
- package/themes/bento-modular/THEME.md +80 -0
- package/themes/bento-modular/audit.log.jsonl +3 -0
- package/themes/bento-modular/dismissals.jsonl +1 -0
- package/themes/bento-modular/tailwind.snippet.ts +17 -0
- package/themes/bento-modular/theme.yml +83 -0
- package/themes/bento-modular/tokens.ts +27 -0
- package/themes/bookstore-indie/THEME.md +78 -0
- package/themes/bookstore-indie/tokens.ts +147 -0
- package/themes/brutalist/THEME.md +96 -0
- package/themes/brutalist/tokens.ts +151 -0
- package/themes/business-tiles/THEME.md +141 -0
- package/themes/business-tiles/tokens.ts +178 -0
- package/themes/cabana-poster/THEME.md +158 -0
- package/themes/cabana-poster/audit.log.jsonl +1 -0
- package/themes/cabana-poster/tailwind.snippet.ts +16 -0
- package/themes/cabana-poster/theme.yml +221 -0
- package/themes/cabana-poster/tokens.ts +34 -0
- package/themes/case-panel/THEME.md +126 -0
- package/themes/case-panel/audit.log.jsonl +2 -0
- package/themes/case-panel/tailwind.snippet.ts +15 -0
- package/themes/case-panel/theme.yml +153 -0
- package/themes/case-panel/tokens.ts +28 -0
- package/themes/cinematic-dark/THEME.md +97 -0
- package/themes/cinematic-dark/tokens.ts +195 -0
- package/themes/classic-car/THEME.md +86 -0
- package/themes/classic-car/tokens.ts +173 -0
- package/themes/cloud-friendly/THEME.md +152 -0
- package/themes/cloud-friendly/tokens.ts +181 -0
- package/themes/community-niche/THEME.md +102 -0
- package/themes/community-niche/tokens.ts +177 -0
- package/themes/corporate-grid/THEME.md +154 -0
- package/themes/corporate-grid/tokens.ts +182 -0
- package/themes/couture-luxury/THEME.md +85 -0
- package/themes/couture-luxury/tokens.ts +146 -0
- package/themes/creator-social/THEME.md +131 -0
- package/themes/creator-social/tokens.ts +249 -0
- package/themes/crypto-onchain/THEME.md +100 -0
- package/themes/crypto-onchain/tokens.ts +207 -0
- package/themes/cycling-brand/THEME.md +78 -0
- package/themes/cycling-brand/tokens.ts +185 -0
- package/themes/dating-friendly/THEME.md +102 -0
- package/themes/dating-friendly/tokens.ts +178 -0
- package/themes/dense-utility/THEME.md +85 -0
- package/themes/dense-utility/tokens.ts +155 -0
- package/themes/devtool-cli/THEME.md +100 -0
- package/themes/devtool-cli/tokens.ts +195 -0
- package/themes/devtool-docs/THEME.md +105 -0
- package/themes/devtool-docs/tokens.ts +177 -0
- package/themes/editorial/THEME.md +129 -0
- package/themes/editorial/audit.log.jsonl +2 -0
- package/themes/editorial/tailwind.snippet.ts +15 -0
- package/themes/editorial/theme.yml +173 -0
- package/themes/editorial/tokens.ts +29 -0
- package/themes/education-formal/THEME.md +154 -0
- package/themes/education-formal/audit.log.jsonl +1 -0
- package/themes/education-formal/tailwind.snippet.ts +15 -0
- package/themes/education-formal/theme.yml +186 -0
- package/themes/education-formal/tokens.ts +29 -0
- package/themes/education-warm/THEME.md +91 -0
- package/themes/education-warm/tokens.ts +185 -0
- package/themes/ember-poster/README.md +79 -0
- package/themes/ember-poster/THEME.md +101 -0
- package/themes/ember-poster/audit.log.jsonl +17 -0
- package/themes/ember-poster/package.json +34 -0
- package/themes/ember-poster/tailwind.snippet.ts +17 -0
- package/themes/ember-poster/theme.yml +109 -0
- package/themes/ember-poster/tokens.ts +27 -0
- package/themes/enterprise-cloud/THEME.md +139 -0
- package/themes/enterprise-cloud/tokens.ts +179 -0
- package/themes/enterprise-tech/.anchor-render.png +0 -0
- package/themes/enterprise-tech/THEME.md +167 -0
- package/themes/enterprise-tech/audit.log.jsonl +1 -0
- package/themes/enterprise-tech/tailwind.snippet.ts +15 -0
- package/themes/enterprise-tech/theme.yml +202 -0
- package/themes/enterprise-tech/tokens.ts +29 -0
- package/themes/ev-charging/THEME.md +94 -0
- package/themes/ev-charging/tokens.ts +185 -0
- package/themes/farm-direct/THEME.md +92 -0
- package/themes/farm-direct/tokens.ts +180 -0
- package/themes/fintech-card/THEME.md +96 -0
- package/themes/fintech-card/tokens.ts +191 -0
- package/themes/fintech-trust/THEME.md +94 -0
- package/themes/fintech-trust/tokens.ts +192 -0
- package/themes/food-blog/THEME.md +98 -0
- package/themes/food-blog/tokens.ts +183 -0
- package/themes/forum-classic/THEME.md +100 -0
- package/themes/forum-classic/tokens.ts +168 -0
- package/themes/fresh-plate/.anchor-render.png +0 -0
- package/themes/fresh-plate/THEME.md +144 -0
- package/themes/fresh-plate/audit.log.jsonl +2 -0
- package/themes/fresh-plate/dismissals.jsonl +3 -0
- package/themes/fresh-plate/tailwind.snippet.ts +16 -0
- package/themes/fresh-plate/theme.yml +189 -0
- package/themes/fresh-plate/tokens.ts +30 -0
- package/themes/furniture-modern/THEME.md +94 -0
- package/themes/furniture-modern/tokens.ts +176 -0
- package/themes/gaming-esports/THEME.md +146 -0
- package/themes/gaming-esports/tokens.ts +199 -0
- package/themes/gaming-streamer/THEME.md +139 -0
- package/themes/gaming-streamer/tokens.ts +201 -0
- package/themes/glassmorphic-modern/THEME.md +119 -0
- package/themes/glassmorphic-modern/tokens.ts +174 -0
- package/themes/government-civic/THEME.md +138 -0
- package/themes/government-civic/tokens.ts +164 -0
- package/themes/gym-chain/THEME.md +79 -0
- package/themes/gym-chain/tokens.ts +196 -0
- package/themes/healthcare-bright/THEME.md +95 -0
- package/themes/healthcare-bright/tokens.ts +184 -0
- package/themes/home-decor/THEME.md +94 -0
- package/themes/home-decor/tokens.ts +177 -0
- package/themes/hotel-boutique/THEME.md +110 -0
- package/themes/hotel-boutique/tokens.ts +179 -0
- package/themes/hyperminimal-mono/THEME.md +91 -0
- package/themes/hyperminimal-mono/tokens.ts +150 -0
- package/themes/indie-art-game/THEME.md +107 -0
- package/themes/indie-art-game/tokens.ts +216 -0
- package/themes/insurance-legacy/THEME.md +101 -0
- package/themes/insurance-legacy/tokens.ts +182 -0
- package/themes/insurance-modern/THEME.md +103 -0
- package/themes/insurance-modern/tokens.ts +192 -0
- package/themes/kaiju-reveal/THEME.md +97 -0
- package/themes/kaiju-reveal/tokens.ts +185 -0
- package/themes/kinetic-data/THEME.md +172 -0
- package/themes/kinetic-data/audit.log.jsonl +1 -0
- package/themes/kinetic-data/tailwind.snippet.ts +16 -0
- package/themes/kinetic-data/theme.yml +207 -0
- package/themes/kinetic-data/tokens.ts +30 -0
- package/themes/kinetic-headline/.anchor-render.png +0 -0
- package/themes/kinetic-headline/BUILD_BRIEF.md +156 -0
- package/themes/kinetic-headline/THEME.md +78 -0
- package/themes/kinetic-headline/audit.log.jsonl +4 -0
- package/themes/kinetic-headline/dismissals.jsonl +1 -0
- package/themes/kinetic-headline/tailwind.snippet.ts +16 -0
- package/themes/kinetic-headline/theme.yml +81 -0
- package/themes/kinetic-headline/tokens.ts +26 -0
- package/themes/law-firm/THEME.md +108 -0
- package/themes/law-firm/tokens.ts +158 -0
- package/themes/lawtech-modern/THEME.md +107 -0
- package/themes/lawtech-modern/tokens.ts +192 -0
- package/themes/linkedin-pro/THEME.md +102 -0
- package/themes/linkedin-pro/tokens.ts +174 -0
- package/themes/logistics-modern/THEME.md +94 -0
- package/themes/logistics-modern/tokens.ts +187 -0
- package/themes/magazine-glossy/THEME.md +74 -0
- package/themes/magazine-glossy/tokens.ts +158 -0
- package/themes/marker-mag/THEME.md +128 -0
- package/themes/marker-mag/audit.log.jsonl +1 -0
- package/themes/marker-mag/tailwind.snippet.ts +15 -0
- package/themes/marker-mag/theme.yml +159 -0
- package/themes/marker-mag/tokens.ts +27 -0
- package/themes/marketplace-modern/THEME.md +135 -0
- package/themes/marketplace-modern/tokens.ts +186 -0
- package/themes/marketplace-vintage/THEME.md +110 -0
- package/themes/marketplace-vintage/tokens.ts +185 -0
- package/themes/maximalist/THEME.md +96 -0
- package/themes/maximalist/tokens.ts +168 -0
- package/themes/mid-century/THEME.md +109 -0
- package/themes/mid-century/tokens.ts +162 -0
- package/themes/motorcycle-brand/THEME.md +86 -0
- package/themes/motorcycle-brand/tokens.ts +174 -0
- package/themes/museum/THEME.md +83 -0
- package/themes/museum/tokens.ts +147 -0
- package/themes/music-streaming/THEME.md +101 -0
- package/themes/music-streaming/tokens.ts +187 -0
- package/themes/music-vinyl/THEME.md +98 -0
- package/themes/music-vinyl/tokens.ts +167 -0
- package/themes/nature-distilled/.anchor-render.png +0 -0
- package/themes/nature-distilled/BUILD_BRIEF.md +158 -0
- package/themes/nature-distilled/THEME.md +81 -0
- package/themes/nature-distilled/audit.log.jsonl +2 -0
- package/themes/nature-distilled/tailwind.snippet.ts +16 -0
- package/themes/nature-distilled/theme.yml +91 -0
- package/themes/nature-distilled/tokens.ts +26 -0
- package/themes/news-tabloid/THEME.md +72 -0
- package/themes/news-tabloid/tokens.ts +158 -0
- package/themes/news-tech/THEME.md +75 -0
- package/themes/news-tech/tokens.ts +149 -0
- package/themes/newsletter/THEME.md +82 -0
- package/themes/newsletter/tokens.ts +138 -0
- package/themes/newspaper-classic/THEME.md +74 -0
- package/themes/newspaper-classic/tokens.ts +159 -0
- package/themes/nonprofit-arts/THEME.md +78 -0
- package/themes/nonprofit-arts/tokens.ts +157 -0
- package/themes/nonprofit-warm/THEME.md +105 -0
- package/themes/nonprofit-warm/tokens.ts +188 -0
- package/themes/outdoor-adventure/THEME.md +113 -0
- package/themes/outdoor-adventure/tokens.ts +181 -0
- package/themes/overland-broadside/THEME.md +119 -0
- package/themes/overland-broadside/audit.log.jsonl +1 -0
- package/themes/overland-broadside/tailwind.snippet.ts +17 -0
- package/themes/overland-broadside/theme.yml +138 -0
- package/themes/overland-broadside/tokens.ts +27 -0
- package/themes/performance-engineered/THEME.md +162 -0
- package/themes/performance-engineered/audit.log.jsonl +1 -0
- package/themes/performance-engineered/tailwind.snippet.ts +15 -0
- package/themes/performance-engineered/theme.yml +193 -0
- package/themes/performance-engineered/tokens.ts +29 -0
- package/themes/pet-food/THEME.md +78 -0
- package/themes/pet-food/tokens.ts +185 -0
- package/themes/pharma-clinical/THEME.md +164 -0
- package/themes/pharma-clinical/audit.log.jsonl +1 -0
- package/themes/pharma-clinical/tailwind.snippet.ts +15 -0
- package/themes/pharma-clinical/theme.yml +201 -0
- package/themes/pharma-clinical/tokens.ts +29 -0
- package/themes/pill-editorial/THEME.md +125 -0
- package/themes/pill-editorial/audit.log.jsonl +1 -0
- package/themes/pill-editorial/tailwind.snippet.ts +15 -0
- package/themes/pill-editorial/theme.yml +168 -0
- package/themes/pill-editorial/tokens.ts +31 -0
- package/themes/podcast-warm/THEME.md +103 -0
- package/themes/podcast-warm/tokens.ts +192 -0
- package/themes/publisher-indie/THEME.md +78 -0
- package/themes/publisher-indie/tokens.ts +148 -0
- package/themes/raw-concrete/BUILD_BRIEF.md +157 -0
- package/themes/raw-concrete/THEME.md +76 -0
- package/themes/raw-concrete/audit.log.jsonl +10 -0
- package/themes/raw-concrete/tailwind.snippet.ts +16 -0
- package/themes/raw-concrete/theme.yml +88 -0
- package/themes/raw-concrete/tokens.ts +27 -0
- package/themes/realestate-luxury/THEME.md +94 -0
- package/themes/realestate-luxury/tokens.ts +169 -0
- package/themes/realestate-marketplace/THEME.md +106 -0
- package/themes/realestate-marketplace/tokens.ts +183 -0
- package/themes/restaurant-modern/THEME.md +109 -0
- package/themes/restaurant-modern/tokens.ts +172 -0
- package/themes/runway-fashion/THEME.md +99 -0
- package/themes/runway-fashion/tokens.ts +167 -0
- package/themes/saturday-morning/THEME.md +117 -0
- package/themes/saturday-morning/tokens.ts +281 -0
- package/themes/schema.ts +235 -0
- package/themes/signal-dark/BUILD_BRIEF.md +168 -0
- package/themes/signal-dark/THEME.md +107 -0
- package/themes/signal-dark/tokens.ts +181 -0
- package/themes/soft-modern/THEME.md +91 -0
- package/themes/soft-modern/tokens.ts +166 -0
- package/themes/solar-residential/THEME.md +92 -0
- package/themes/solar-residential/tokens.ts +176 -0
- package/themes/sports-league/THEME.md +180 -0
- package/themes/sports-league/audit.log.jsonl +1 -0
- package/themes/sports-league/tailwind.snippet.ts +15 -0
- package/themes/sports-league/theme.yml +213 -0
- package/themes/sports-league/tokens.ts +32 -0
- package/themes/stadium-sport/THEME.md +114 -0
- package/themes/stadium-sport/tokens.ts +187 -0
- package/themes/streetwear-hype/THEME.md +100 -0
- package/themes/streetwear-hype/tokens.ts +174 -0
- package/themes/studio-architect/THEME.md +108 -0
- package/themes/studio-architect/tokens.ts +162 -0
- package/themes/studio-photographer/THEME.md +88 -0
- package/themes/studio-photographer/tokens.ts +164 -0
- package/themes/team-kanban/.anchor-render.png +0 -0
- package/themes/team-kanban/THEME.md +167 -0
- package/themes/team-kanban/audit.log.jsonl +2 -0
- package/themes/team-kanban/dismissals.jsonl +1 -0
- package/themes/team-kanban/tailwind.snippet.ts +15 -0
- package/themes/team-kanban/theme.yml +209 -0
- package/themes/team-kanban/tokens.ts +35 -0
- package/themes/telehealth-modern/THEME.md +95 -0
- package/themes/telehealth-modern/tokens.ts +197 -0
- package/themes/tourism-board/THEME.md +111 -0
- package/themes/tourism-board/tokens.ts +173 -0
- package/themes/toys-modern/THEME.md +79 -0
- package/themes/toys-modern/tokens.ts +200 -0
- package/themes/travel-luxury/THEME.md +95 -0
- package/themes/travel-luxury/tokens.ts +162 -0
- package/themes/vacation-rental/THEME.md +105 -0
- package/themes/vacation-rental/tokens.ts +174 -0
- package/themes/vault-reel/THEME.md +134 -0
- package/themes/vault-reel/audit.log.jsonl +3 -0
- package/themes/vault-reel/tailwind.snippet.ts +16 -0
- package/themes/vault-reel/theme.yml +167 -0
- package/themes/vault-reel/tokens.ts +30 -0
- package/themes/vet-modern/THEME.md +79 -0
- package/themes/vet-modern/tokens.ts +194 -0
- package/themes/web3-restraint/THEME.md +87 -0
- package/themes/web3-restraint/tokens.ts +154 -0
- package/themes/wellness-active/THEME.md +102 -0
- package/themes/wellness-active/tokens.ts +203 -0
- package/themes/wellness-spa/THEME.md +93 -0
- package/themes/wellness-spa/tokens.ts +173 -0
- package/themes/y2k-chrome/THEME.md +118 -0
- package/themes/y2k-chrome/tokens.ts +249 -0
- package/themes/yoga-studio/THEME.md +76 -0
- package/themes/yoga-studio/tokens.ts +163 -0
package/dist/index.js
CHANGED
|
@@ -281,10 +281,10 @@ var init_slop_rules = __esm({
|
|
|
281
281
|
},
|
|
282
282
|
{
|
|
283
283
|
id: "R-SECOND-ACCENT",
|
|
284
|
-
description: "Component introduces a second saturated accent hue on a theme whose
|
|
284
|
+
description: "Component introduces a second saturated accent hue on a theme whose policy permits exactly one (signal-dark, enterprise-tech, pharma-clinical, kinetic-data). Alarms/status must use ink + hairline + mono glyph, not a second hue. Theme-aware: resolves the active theme from .stylecrib/config.json and its permitted accent from the bundled themes/, then compares hue distance. No-ops when the theme isn't resolvable or has no single-accent policy.",
|
|
285
285
|
severity: "block",
|
|
286
286
|
category: "palette",
|
|
287
|
-
specSource: "
|
|
287
|
+
specSource: "slop-blocklist.md",
|
|
288
288
|
detectorAt: "check.ts"
|
|
289
289
|
},
|
|
290
290
|
{
|
|
@@ -371,19 +371,19 @@ var init_slop_rules = __esm({
|
|
|
371
371
|
},
|
|
372
372
|
{
|
|
373
373
|
id: "R-MONO-CAPS-SECTION-EYEBROW",
|
|
374
|
-
description: "
|
|
374
|
+
description: "Renders `SECTION \xB7 X` / `CHAPTER \xB7 X` / `PART <N>` / `WHAT WE X` mono-caps section eyebrow above content \u2014 universal slop. Detected at both the component level (check.ts, anchored to quoted/JSX text) and at theme materialization (slop-blocklist.ts).",
|
|
375
375
|
severity: "block",
|
|
376
376
|
category: "typography",
|
|
377
|
-
specSource: "
|
|
378
|
-
detectorAt: "
|
|
377
|
+
specSource: "slop-blocklist.md",
|
|
378
|
+
detectorAt: "check.ts"
|
|
379
379
|
},
|
|
380
380
|
{
|
|
381
381
|
id: "R-CORNER-TAG-ABBREVIATION",
|
|
382
|
-
description: "Small mono-caps corner tags with period abbreviations (`NO. 03`, `EST. 2019`, `VOL. 2`, `CH. 4`, `ESSAY NO. 04`) \u2014 editorial-ornament defaults, always slop.",
|
|
382
|
+
description: "Small mono-caps corner tags with period abbreviations (`NO. 03`, `EST. 2019`, `VOL. 2`, `CH. 4`, `ESSAY NO. 04`) \u2014 editorial-ornament defaults, always slop. Detected at both the component level (check.ts, digit-suffixed only) and at theme materialization (slop-blocklist.ts, which also matches interpolated values).",
|
|
383
383
|
severity: "block",
|
|
384
384
|
category: "content",
|
|
385
|
-
specSource: "
|
|
386
|
-
detectorAt: "
|
|
385
|
+
specSource: "slop-blocklist.md",
|
|
386
|
+
detectorAt: "check.ts"
|
|
387
387
|
},
|
|
388
388
|
{
|
|
389
389
|
id: "R-READ-TIME-META",
|
|
@@ -403,19 +403,19 @@ var init_slop_rules = __esm({
|
|
|
403
403
|
},
|
|
404
404
|
{
|
|
405
405
|
id: "R-BYLINE-MONO-CAPS",
|
|
406
|
-
description: "Bylines rendered in mono-caps (`BY R. IWASAKI`, `WORDS BY X`, `PHOTOGRAPHY BY Y`) \u2014 real editorial bylines are set in body type; mono-caps bylines are editorial cosplay.",
|
|
406
|
+
description: "Bylines rendered in mono-caps (`BY R. IWASAKI`, `WORDS BY X`, `PHOTOGRAPHY BY Y`) \u2014 real editorial bylines are set in body type; mono-caps bylines are editorial cosplay. Detected at both the component level (check.ts) and at theme materialization (slop-blocklist.ts).",
|
|
407
407
|
severity: "block",
|
|
408
408
|
category: "typography",
|
|
409
|
-
specSource: "
|
|
410
|
-
detectorAt: "
|
|
409
|
+
specSource: "slop-blocklist.md",
|
|
410
|
+
detectorAt: "check.ts"
|
|
411
411
|
},
|
|
412
412
|
{
|
|
413
413
|
id: "R-TAG-ROW-ABOVE-BODY",
|
|
414
|
-
description: "`\u25B8 TAG` / `\u25C6 TAG` / `\xBB TAG` / `\u25CF TAG` mono-caps prefix labels used as category tags next to body copy \u2014 the eyebrow relocated, still slop.",
|
|
414
|
+
description: "`\u25B8 TAG` / `\u25C6 TAG` / `\xBB TAG` / `\u25CF TAG` mono-caps prefix labels used as category tags next to body copy \u2014 the eyebrow relocated, still slop. Detected at both the component level (check.ts) and at theme materialization (slop-blocklist.ts).",
|
|
415
415
|
severity: "block",
|
|
416
416
|
category: "typography",
|
|
417
|
-
specSource: "
|
|
418
|
-
detectorAt: "
|
|
417
|
+
specSource: "slop-blocklist.md",
|
|
418
|
+
detectorAt: "check.ts"
|
|
419
419
|
},
|
|
420
420
|
{
|
|
421
421
|
id: "R-TRUSTED-BY-EMPTY-ROW",
|
|
@@ -477,9 +477,8 @@ import { Command } from "commander";
|
|
|
477
477
|
|
|
478
478
|
// src/commands/init.ts
|
|
479
479
|
init_esm_shims();
|
|
480
|
-
import { resolve as
|
|
480
|
+
import { resolve as resolve5, join as join8, basename } from "path";
|
|
481
481
|
import { readFile as readFile6 } from "fs/promises";
|
|
482
|
-
import { fileURLToPath as fileURLToPath4 } from "url";
|
|
483
482
|
import { confirm as confirm2 } from "@inquirer/prompts";
|
|
484
483
|
|
|
485
484
|
// src/lib/colors.ts
|
|
@@ -508,6 +507,74 @@ function step(n, total, label) {
|
|
|
508
507
|
// src/wizard.ts
|
|
509
508
|
init_esm_shims();
|
|
510
509
|
import { select, input, confirm, editor } from "@inquirer/prompts";
|
|
510
|
+
|
|
511
|
+
// src/lib/errors.ts
|
|
512
|
+
init_esm_shims();
|
|
513
|
+
var CliError = class extends Error {
|
|
514
|
+
constructor(message, options) {
|
|
515
|
+
super(message, options);
|
|
516
|
+
this.name = "CliError";
|
|
517
|
+
}
|
|
518
|
+
};
|
|
519
|
+
var MissingApiKeyError = class extends CliError {
|
|
520
|
+
constructor() {
|
|
521
|
+
super(
|
|
522
|
+
"ANTHROPIC_API_KEY not set.\n\nThe key is needed for two things: synthesizing a direction from your own\nreferences, and the deep review the pre-commit hook runs. Installing a\ncanonical theme as-is needs no key and makes no API call:\n\n npx stylecrib init --style soft-modern --yes\n\nTo use references, get a key at https://console.anthropic.com/ and run:\n\n export ANTHROPIC_API_KEY=sk-ant-...\n\nOr pass it inline for one run:\n\n ANTHROPIC_API_KEY=sk-ant-... npx stylecrib init"
|
|
523
|
+
);
|
|
524
|
+
this.name = "MissingApiKeyError";
|
|
525
|
+
}
|
|
526
|
+
};
|
|
527
|
+
var NotAGitRepoError = class extends CliError {
|
|
528
|
+
constructor(target) {
|
|
529
|
+
super(
|
|
530
|
+
`Not a git repository: ${target}
|
|
531
|
+
|
|
532
|
+
The enforcement layer is a git pre-commit hook, so there has to be a repo
|
|
533
|
+
to hook into. Run \`git init\` there first, then \`stylecrib install-hooks\`.`
|
|
534
|
+
);
|
|
535
|
+
this.name = "NotAGitRepoError";
|
|
536
|
+
}
|
|
537
|
+
};
|
|
538
|
+
var HookCollisionError = class extends CliError {
|
|
539
|
+
paths;
|
|
540
|
+
wrote;
|
|
541
|
+
/**
|
|
542
|
+
* @param paths files skipped because they exist with different content
|
|
543
|
+
* @param wrote how many files DID land before this was thrown
|
|
544
|
+
*
|
|
545
|
+
* The count matters. installHooks writes every non-colliding file and only
|
|
546
|
+
* then throws, so the install is partial — saying "nothing was written"
|
|
547
|
+
* (as this did) sends the customer looking for a clean slate that isn't
|
|
548
|
+
* there.
|
|
549
|
+
*/
|
|
550
|
+
constructor(paths, wrote = 0) {
|
|
551
|
+
super(
|
|
552
|
+
`${paths.length} file(s) already exist with different content and were left alone.
|
|
553
|
+
|
|
554
|
+
` + paths.map((p) => ` \xB7 ${p}`).join("\n") + (wrote > 0 ? `
|
|
555
|
+
|
|
556
|
+
${wrote} other file(s) WERE written, so this install is partial \u2014 the enforcement layer is not fully wired yet.` : "") + "\n\nRe-run with --force to overwrite the files above (each is backed up to\n<name>.pre-stylecrib.bak first), or merge them by hand and re-run."
|
|
557
|
+
);
|
|
558
|
+
this.name = "HookCollisionError";
|
|
559
|
+
this.paths = paths;
|
|
560
|
+
this.wrote = wrote;
|
|
561
|
+
}
|
|
562
|
+
};
|
|
563
|
+
var StackMismatchError = class extends CliError {
|
|
564
|
+
constructor(detail) {
|
|
565
|
+
super(
|
|
566
|
+
`Project stack not supported in v1.
|
|
567
|
+
|
|
568
|
+
${detail}
|
|
569
|
+
|
|
570
|
+
v1 targets React + Tailwind + Vite + TS. Vue, Svelte, Next, styled-components, CSS modules are v2.
|
|
571
|
+
If you're sure the project matches and detection misfired, pass --target with the correct path.`
|
|
572
|
+
);
|
|
573
|
+
this.name = "StackMismatchError";
|
|
574
|
+
}
|
|
575
|
+
};
|
|
576
|
+
|
|
577
|
+
// src/wizard.ts
|
|
511
578
|
var DIRECTION_CHOICES = [
|
|
512
579
|
// reading-first
|
|
513
580
|
{
|
|
@@ -650,11 +717,29 @@ var DIRECTION_CHOICES = [
|
|
|
650
717
|
}
|
|
651
718
|
];
|
|
652
719
|
async function runWizard(opts) {
|
|
720
|
+
assertValidDirection(opts.skippedStyle);
|
|
721
|
+
if (opts.nonInteractive) {
|
|
722
|
+
const out = {
|
|
723
|
+
baseDirection: opts.skippedStyle ?? "soft-modern",
|
|
724
|
+
projectName: opts.defaultProjectName,
|
|
725
|
+
references: [],
|
|
726
|
+
antiReferences: [],
|
|
727
|
+
vibe: "",
|
|
728
|
+
urls: [],
|
|
729
|
+
imagePaths: []
|
|
730
|
+
};
|
|
731
|
+
console.log(header("StyleCrib \u2014 non-interactive install"));
|
|
732
|
+
printSummary(out);
|
|
733
|
+
console.log(
|
|
734
|
+
c.dim(" Refine any of this later with `npx stylecrib init` from a terminal.\n")
|
|
735
|
+
);
|
|
736
|
+
return out;
|
|
737
|
+
}
|
|
653
738
|
console.log(header("StyleCrib \u2014 design upgrade wizard"));
|
|
654
739
|
console.log(
|
|
655
740
|
"Answer a few questions and the Director will synthesize a design direction\nfor your project. Reference brands beat hex codes; vivid words beat specs.\n"
|
|
656
741
|
);
|
|
657
|
-
const baseDirection = opts.skippedStyle
|
|
742
|
+
const baseDirection = opts.skippedStyle ? opts.skippedStyle : await select({
|
|
658
743
|
message: "Pick a starting direction (you can refine it with references next):",
|
|
659
744
|
choices: DIRECTION_CHOICES,
|
|
660
745
|
default: "soft-modern"
|
|
@@ -717,14 +802,14 @@ async function runWizard(opts) {
|
|
|
717
802
|
}
|
|
718
803
|
}
|
|
719
804
|
console.log(header("Summary"));
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
805
|
+
printSummary({ baseDirection, projectName, references, antiReferences, vibe, urls, imagePaths });
|
|
806
|
+
const willCallDirector = baseDirection === "custom" || references.length > 0 || antiReferences.length > 0 || !!vibe || urls.length > 0 || imagePaths.length > 0;
|
|
807
|
+
if (willCallDirector && !process.env.ANTHROPIC_API_KEY) {
|
|
808
|
+
console.log(
|
|
809
|
+
c.warn(" \u26A0 ANTHROPIC_API_KEY is not set, and these answers need the Director.\n") + c.dim(" Set the key and re-run, or clear the references above to install the\n") + c.dim(` canonical "${baseDirection}" theme verbatim with no API call.
|
|
810
|
+
`)
|
|
811
|
+
);
|
|
812
|
+
}
|
|
728
813
|
const proceed = await confirm({
|
|
729
814
|
message: "Send this to the Director and write outputs?",
|
|
730
815
|
default: true
|
|
@@ -746,73 +831,36 @@ async function runWizard(opts) {
|
|
|
746
831
|
function parseList(raw) {
|
|
747
832
|
return raw.split(",").map((s) => s.trim()).filter(Boolean);
|
|
748
833
|
}
|
|
834
|
+
function printSummary(w) {
|
|
835
|
+
console.log(" " + c.dim("Base direction: ") + c.bold(w.baseDirection));
|
|
836
|
+
console.log(" " + c.dim("Project: ") + w.projectName);
|
|
837
|
+
console.log(" " + c.dim("References: ") + (w.references.length ? w.references.join(", ") : c.dim("(none)")));
|
|
838
|
+
console.log(" " + c.dim("Avoid: ") + (w.antiReferences.length ? w.antiReferences.join(", ") : c.dim("(none)")));
|
|
839
|
+
console.log(" " + c.dim("Vibe: ") + (w.vibe || c.dim("(none)")));
|
|
840
|
+
console.log(" " + c.dim("URLs: ") + (w.urls.length ? w.urls.join(", ") : c.dim("(none)")));
|
|
841
|
+
console.log(" " + c.dim("Images: ") + (w.imagePaths.length ? w.imagePaths.join(", ") : c.dim("(none)")));
|
|
842
|
+
console.log("");
|
|
843
|
+
}
|
|
844
|
+
var VALID_DIRECTIONS = DIRECTION_CHOICES.map((d) => d.value);
|
|
749
845
|
function isValidDirection(s) {
|
|
750
|
-
return
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
"
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
"maximalist",
|
|
763
|
-
"y2k-chrome",
|
|
764
|
-
"couture-luxury",
|
|
765
|
-
"stadium-sport",
|
|
766
|
-
"performance-engineered",
|
|
767
|
-
"saturday-morning",
|
|
768
|
-
"enterprise-tech",
|
|
769
|
-
"creator-social",
|
|
770
|
-
"kaiju-reveal",
|
|
771
|
-
"runway-fashion",
|
|
772
|
-
"streetwear-hype",
|
|
773
|
-
"architectural",
|
|
774
|
-
"kinetic-data",
|
|
775
|
-
"indie-art-game",
|
|
776
|
-
"custom"
|
|
777
|
-
].includes(s);
|
|
846
|
+
return VALID_DIRECTIONS.includes(s);
|
|
847
|
+
}
|
|
848
|
+
function assertValidDirection(slug) {
|
|
849
|
+
if (slug === void 0 || isValidDirection(slug)) return;
|
|
850
|
+
throw new CliError(
|
|
851
|
+
`Unknown --style "${slug}".
|
|
852
|
+
|
|
853
|
+
Valid directions:
|
|
854
|
+
${VALID_DIRECTIONS.map((v) => ` \xB7 ${v}`).join("\n")}
|
|
855
|
+
|
|
856
|
+
Browse them at https://github.com/frischermf/stylecrib/tree/main/themes`
|
|
857
|
+
);
|
|
778
858
|
}
|
|
779
859
|
|
|
780
860
|
// src/ingest.ts
|
|
781
861
|
init_esm_shims();
|
|
782
862
|
import { readFile } from "fs/promises";
|
|
783
863
|
import { extname } from "path";
|
|
784
|
-
|
|
785
|
-
// src/lib/errors.ts
|
|
786
|
-
init_esm_shims();
|
|
787
|
-
var CliError = class extends Error {
|
|
788
|
-
constructor(message, options) {
|
|
789
|
-
super(message, options);
|
|
790
|
-
this.name = "CliError";
|
|
791
|
-
}
|
|
792
|
-
};
|
|
793
|
-
var MissingApiKeyError = class extends CliError {
|
|
794
|
-
constructor() {
|
|
795
|
-
super(
|
|
796
|
-
"ANTHROPIC_API_KEY not set.\n\nStyleCrib calls the Anthropic API to synthesize your design direction.\nGet a key at https://console.anthropic.com/, then run:\n\n export ANTHROPIC_API_KEY=sk-ant-...\n\nOr pass it inline for one run:\n\n ANTHROPIC_API_KEY=sk-ant-... npx stylecrib init"
|
|
797
|
-
);
|
|
798
|
-
this.name = "MissingApiKeyError";
|
|
799
|
-
}
|
|
800
|
-
};
|
|
801
|
-
var StackMismatchError = class extends CliError {
|
|
802
|
-
constructor(detail) {
|
|
803
|
-
super(
|
|
804
|
-
`Project stack not supported in v1.
|
|
805
|
-
|
|
806
|
-
${detail}
|
|
807
|
-
|
|
808
|
-
v1 targets React + Tailwind + Vite + TS. Vue, Svelte, Next, styled-components, CSS modules are v2.
|
|
809
|
-
If you're sure the project matches and detection misfired, pass --target with the correct path.`
|
|
810
|
-
);
|
|
811
|
-
this.name = "StackMismatchError";
|
|
812
|
-
}
|
|
813
|
-
};
|
|
814
|
-
|
|
815
|
-
// src/ingest.ts
|
|
816
864
|
async function ingestUrl(url) {
|
|
817
865
|
const res = await fetch(url, {
|
|
818
866
|
headers: {
|
|
@@ -835,16 +883,16 @@ async function ingestUrl(url) {
|
|
|
835
883
|
);
|
|
836
884
|
}
|
|
837
885
|
const html = await res.text();
|
|
838
|
-
const
|
|
886
|
+
const text2 = htmlToReadableText(html);
|
|
839
887
|
console.log(
|
|
840
888
|
c.dim(
|
|
841
|
-
` \xB7 fetched ${url} (${(html.length / 1024).toFixed(1)} KB HTML \u2192 ${(
|
|
889
|
+
` \xB7 fetched ${url} (${(html.length / 1024).toFixed(1)} KB HTML \u2192 ${(text2.length / 1024).toFixed(1)} KB text)`
|
|
842
890
|
)
|
|
843
891
|
);
|
|
844
892
|
return {
|
|
845
893
|
type: "text",
|
|
846
894
|
text: `=== URL reference: ${url} ===
|
|
847
|
-
` +
|
|
895
|
+
` + text2.slice(0, 12e3) + (text2.length > 12e3 ? "\n[...truncated]" : "")
|
|
848
896
|
};
|
|
849
897
|
}
|
|
850
898
|
async function ingestImage(path2) {
|
|
@@ -1026,32 +1074,62 @@ Now produce the JSON object exactly as your system prompt specifies. Synthesize,
|
|
|
1026
1074
|
// src/writer.ts
|
|
1027
1075
|
init_esm_shims();
|
|
1028
1076
|
import { mkdir, writeFile, readFile as readFile2, access, cp, readdir, stat } from "fs/promises";
|
|
1029
|
-
import { join, dirname
|
|
1077
|
+
import { join as join2, dirname as dirname2, relative } from "path";
|
|
1078
|
+
|
|
1079
|
+
// src/lib/assets.ts
|
|
1080
|
+
init_esm_shims();
|
|
1081
|
+
import { existsSync, readdirSync } from "fs";
|
|
1082
|
+
import { dirname, join, resolve } from "path";
|
|
1030
1083
|
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
1031
1084
|
var __dirname2 = dirname(fileURLToPath2(import.meta.url));
|
|
1032
|
-
|
|
1085
|
+
var cache = /* @__PURE__ */ new Map();
|
|
1086
|
+
function resolveAssetDir(kind) {
|
|
1087
|
+
const hit = cache.get(kind);
|
|
1088
|
+
if (hit !== void 0) return hit;
|
|
1033
1089
|
const candidates = [
|
|
1034
|
-
resolve(__dirname2, "..",
|
|
1035
|
-
|
|
1090
|
+
resolve(__dirname2, "..", kind),
|
|
1091
|
+
// npm install: <pkg>/dist/../<kind>
|
|
1092
|
+
resolve(__dirname2, "..", "..", kind),
|
|
1093
|
+
// dev monorepo: cli/dist/../../<kind>
|
|
1094
|
+
resolve(__dirname2, "..", "..", "..", kind)
|
|
1036
1095
|
];
|
|
1037
|
-
|
|
1096
|
+
const found = candidates.find((p) => existsSync(p) && looksLikeOurs(p, kind)) ?? null;
|
|
1097
|
+
cache.set(kind, found);
|
|
1098
|
+
return found;
|
|
1099
|
+
}
|
|
1100
|
+
function looksLikeOurs(dir, kind) {
|
|
1101
|
+
try {
|
|
1102
|
+
if (kind === "agents") {
|
|
1103
|
+
return existsSync(join(dir, "MANIFEST.yml"));
|
|
1104
|
+
}
|
|
1105
|
+
const entries = readdirSync(dir, { withFileTypes: true }).filter((e) => e.isDirectory()).slice(0, 40);
|
|
1106
|
+
return entries.some(
|
|
1107
|
+
(e) => existsSync(join(dir, e.name, "THEME.md")) || existsSync(join(dir, e.name, "tokens.ts"))
|
|
1108
|
+
);
|
|
1109
|
+
} catch {
|
|
1110
|
+
return false;
|
|
1111
|
+
}
|
|
1112
|
+
}
|
|
1113
|
+
function missingAssetMessage(kind) {
|
|
1114
|
+
return `The CLI install is missing its bundled ${kind}/ directory. Reinstall with \`npm install -D @stylecrib/cli\`, or report this at https://github.com/frischermf/stylecrib/issues.`;
|
|
1038
1115
|
}
|
|
1116
|
+
|
|
1117
|
+
// src/writer.ts
|
|
1039
1118
|
async function stageOutputs(opts) {
|
|
1040
1119
|
const { targetRoot, director, emitted } = opts;
|
|
1041
|
-
const stagingRoot =
|
|
1120
|
+
const stagingRoot = join2(targetRoot, ".design-upgrade");
|
|
1042
1121
|
const filesStaged = [];
|
|
1043
1122
|
const filesSkipped = [];
|
|
1044
1123
|
await mkdir(stagingRoot, { recursive: true });
|
|
1045
1124
|
await stage(
|
|
1046
|
-
|
|
1125
|
+
join2(targetRoot, "design-system", "THEME.md"),
|
|
1047
1126
|
director.directionMarkdown,
|
|
1048
1127
|
stagingRoot,
|
|
1049
1128
|
targetRoot,
|
|
1050
1129
|
filesStaged,
|
|
1051
1130
|
filesSkipped
|
|
1052
1131
|
);
|
|
1053
|
-
const
|
|
1054
|
-
for (const name of [
|
|
1132
|
+
const agentNames = [
|
|
1055
1133
|
"design-director.md",
|
|
1056
1134
|
"design-systems-engineer.md",
|
|
1057
1135
|
"component-qa.md",
|
|
@@ -1060,22 +1138,31 @@ async function stageOutputs(opts) {
|
|
|
1060
1138
|
"logo-designer.md",
|
|
1061
1139
|
"brand-identity.md",
|
|
1062
1140
|
"marketing-designer.md"
|
|
1063
|
-
]
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1141
|
+
];
|
|
1142
|
+
const agentsDir = resolveAssetDir("agents");
|
|
1143
|
+
if (!agentsDir) {
|
|
1144
|
+
filesSkipped.push({
|
|
1145
|
+
targetPath: join2(targetRoot, ".claude", "agents"),
|
|
1146
|
+
reason: missingAssetMessage("agents")
|
|
1147
|
+
});
|
|
1148
|
+
} else {
|
|
1149
|
+
for (const name of agentNames) {
|
|
1150
|
+
try {
|
|
1151
|
+
const content = await readFile2(join2(agentsDir, name), "utf8");
|
|
1152
|
+
await stage(
|
|
1153
|
+
join2(targetRoot, ".claude", "agents", name),
|
|
1154
|
+
content,
|
|
1155
|
+
stagingRoot,
|
|
1156
|
+
targetRoot,
|
|
1157
|
+
filesStaged,
|
|
1158
|
+
filesSkipped
|
|
1159
|
+
);
|
|
1160
|
+
} catch (e) {
|
|
1161
|
+
filesSkipped.push({
|
|
1162
|
+
targetPath: join2(targetRoot, ".claude", "agents", name),
|
|
1163
|
+
reason: `source missing \u2014 ${e.message}`
|
|
1164
|
+
});
|
|
1165
|
+
}
|
|
1079
1166
|
}
|
|
1080
1167
|
}
|
|
1081
1168
|
await stage(
|
|
@@ -1099,11 +1186,11 @@ async function stageOutputs(opts) {
|
|
|
1099
1186
|
}
|
|
1100
1187
|
async function stage(targetPath, content, stagingRoot, targetRoot, filesStaged, _filesSkipped) {
|
|
1101
1188
|
const rel = relative(targetRoot, targetPath);
|
|
1102
|
-
const stagedPath =
|
|
1103
|
-
await mkdir(
|
|
1189
|
+
const stagedPath = join2(stagingRoot, rel);
|
|
1190
|
+
await mkdir(dirname2(stagedPath), { recursive: true });
|
|
1104
1191
|
await writeFile(stagedPath, content, "utf8");
|
|
1105
1192
|
filesStaged.push({ stagedPath, targetPath });
|
|
1106
|
-
console.log(c.ok(" + ") + c.dim(relPath(stagedPath)));
|
|
1193
|
+
console.log(c.ok(" + ") + c.dim(relPath(stagedPath, targetRoot)));
|
|
1107
1194
|
}
|
|
1108
1195
|
var ROUTING_MARK_START = "<!-- stylecrib:routing:start -->";
|
|
1109
1196
|
var ROUTING_MARK_END = "<!-- stylecrib:routing:end -->";
|
|
@@ -1129,7 +1216,7 @@ Source of truth for design: \`design-system/THEME.md\`.
|
|
|
1129
1216
|
${ROUTING_MARK_END}
|
|
1130
1217
|
`;
|
|
1131
1218
|
async function writePatchInstruction(targetRoot, stagingRoot, filesStaged) {
|
|
1132
|
-
const claudeMdPath =
|
|
1219
|
+
const claudeMdPath = join2(targetRoot, "CLAUDE.md");
|
|
1133
1220
|
const exists = await fileExists(claudeMdPath);
|
|
1134
1221
|
let stagedContent;
|
|
1135
1222
|
if (!exists) {
|
|
@@ -1147,23 +1234,23 @@ See \`design-system/THEME.md\` and \`.claude/agents/\` for the design team.
|
|
|
1147
1234
|
}
|
|
1148
1235
|
stagedContent = current.replace(/\s*$/, "") + "\n\n" + ROUTING_BLOCK;
|
|
1149
1236
|
}
|
|
1150
|
-
const stagedPath =
|
|
1151
|
-
await mkdir(
|
|
1237
|
+
const stagedPath = join2(stagingRoot, "CLAUDE.md");
|
|
1238
|
+
await mkdir(dirname2(stagedPath), { recursive: true });
|
|
1152
1239
|
await writeFile(stagedPath, stagedContent, "utf8");
|
|
1153
1240
|
filesStaged.push({ stagedPath, targetPath: claudeMdPath });
|
|
1154
|
-
console.log(c.ok(" + ") + c.dim(relPath(stagedPath)));
|
|
1241
|
+
console.log(c.ok(" + ") + c.dim(relPath(stagedPath, targetRoot)));
|
|
1155
1242
|
}
|
|
1156
1243
|
async function* walk(dir) {
|
|
1157
1244
|
const entries = await readdir(dir);
|
|
1158
1245
|
for (const e of entries) {
|
|
1159
|
-
const p =
|
|
1246
|
+
const p = join2(dir, e);
|
|
1160
1247
|
const s = await stat(p);
|
|
1161
1248
|
if (s.isDirectory()) yield* walk(p);
|
|
1162
1249
|
else yield p;
|
|
1163
1250
|
}
|
|
1164
1251
|
}
|
|
1165
1252
|
async function copyFile(src, dest) {
|
|
1166
|
-
await mkdir(
|
|
1253
|
+
await mkdir(dirname2(dest), { recursive: true });
|
|
1167
1254
|
await cp(src, dest);
|
|
1168
1255
|
}
|
|
1169
1256
|
async function fileExists(p) {
|
|
@@ -1174,22 +1261,21 @@ async function fileExists(p) {
|
|
|
1174
1261
|
return false;
|
|
1175
1262
|
}
|
|
1176
1263
|
}
|
|
1177
|
-
function relPath(p) {
|
|
1178
|
-
|
|
1179
|
-
return p.startsWith(cwd) ? p.slice(cwd.length + 1) : p;
|
|
1264
|
+
function relPath(p, base) {
|
|
1265
|
+
return relative(base, p) || p;
|
|
1180
1266
|
}
|
|
1181
1267
|
|
|
1182
1268
|
// src/codemod/detect.ts
|
|
1183
1269
|
init_esm_shims();
|
|
1184
1270
|
import { readFile as readFile3, access as access2 } from "fs/promises";
|
|
1185
|
-
import { join as
|
|
1271
|
+
import { join as join3 } from "path";
|
|
1186
1272
|
var REQUIRED_DEPS = {
|
|
1187
1273
|
react: { name: "react", minMajor: 18 },
|
|
1188
1274
|
vite: { name: "vite", minMajor: 5 },
|
|
1189
|
-
tailwind: { name: "tailwindcss", minMajor: 3 }
|
|
1275
|
+
tailwind: { name: "tailwindcss", minMajor: 3, maxMajor: 3 }
|
|
1190
1276
|
};
|
|
1191
1277
|
async function detectStack(rootPath) {
|
|
1192
|
-
const pkgPath =
|
|
1278
|
+
const pkgPath = join3(rootPath, "package.json");
|
|
1193
1279
|
let pkg;
|
|
1194
1280
|
try {
|
|
1195
1281
|
pkg = JSON.parse(await readFile3(pkgPath, "utf8"));
|
|
@@ -1202,6 +1288,7 @@ async function detectStack(rootPath) {
|
|
|
1202
1288
|
const versions = {};
|
|
1203
1289
|
const missing = [];
|
|
1204
1290
|
const wrongVersion = [];
|
|
1291
|
+
const unparseable = [];
|
|
1205
1292
|
for (const [key, req] of Object.entries(REQUIRED_DEPS)) {
|
|
1206
1293
|
const raw = allDeps[req.name];
|
|
1207
1294
|
if (!raw) {
|
|
@@ -1210,11 +1297,30 @@ async function detectStack(rootPath) {
|
|
|
1210
1297
|
}
|
|
1211
1298
|
const major = parseMajor(raw);
|
|
1212
1299
|
versions[key] = { raw, major };
|
|
1300
|
+
if (major === 0) {
|
|
1301
|
+
unparseable.push(
|
|
1302
|
+
`${req.name}@"${raw}" \u2014 can't read a version out of that. Pin an explicit range (e.g. "^${req.minMajor}.0.0") in package.json, or pass --target at the workspace member that has one.`
|
|
1303
|
+
);
|
|
1304
|
+
continue;
|
|
1305
|
+
}
|
|
1213
1306
|
if (major < req.minMajor) {
|
|
1214
1307
|
wrongVersion.push(
|
|
1215
1308
|
`${req.name}@${raw} \u2014 v1 requires ${req.name} >= ${req.minMajor}.x`
|
|
1216
1309
|
);
|
|
1217
1310
|
}
|
|
1311
|
+
if (req.maxMajor !== void 0 && major > req.maxMajor) {
|
|
1312
|
+
wrongVersion.push(
|
|
1313
|
+
`${req.name}@${raw} \u2014 v1 requires ${req.name} <= ${req.maxMajor}.x.
|
|
1314
|
+
Taste emits a Tailwind 3 config (tailwind.config.ts + @tailwind directives
|
|
1315
|
+
in src/index.css). Tailwind 4 ignores the JS config and uses CSS-first
|
|
1316
|
+
@theme instead, so the generated tokens would silently do nothing.
|
|
1317
|
+
Downgrade with \`npm install -D tailwindcss@^3.4.17\`, or run with
|
|
1318
|
+
--no-apply and port the emitted tokens to @theme by hand.`
|
|
1319
|
+
);
|
|
1320
|
+
}
|
|
1321
|
+
}
|
|
1322
|
+
if (unparseable.length) {
|
|
1323
|
+
throw new StackMismatchError(unparseable.join("\n"));
|
|
1218
1324
|
}
|
|
1219
1325
|
if (missing.length) {
|
|
1220
1326
|
throw new StackMismatchError(
|
|
@@ -1232,13 +1338,13 @@ async function detectStack(rootPath) {
|
|
|
1232
1338
|
];
|
|
1233
1339
|
let tailwindConfigPath = null;
|
|
1234
1340
|
for (const c2 of tailwindConfigCandidates) {
|
|
1235
|
-
if (await fileExists2(
|
|
1236
|
-
tailwindConfigPath =
|
|
1341
|
+
if (await fileExists2(join3(rootPath, c2))) {
|
|
1342
|
+
tailwindConfigPath = join3(rootPath, c2);
|
|
1237
1343
|
break;
|
|
1238
1344
|
}
|
|
1239
1345
|
}
|
|
1240
|
-
const ts = await fileExists2(
|
|
1241
|
-
const srcPath =
|
|
1346
|
+
const ts = await fileExists2(join3(rootPath, "tsconfig.json"));
|
|
1347
|
+
const srcPath = join3(rootPath, "src");
|
|
1242
1348
|
const hasSrcDir = await fileExists2(srcPath);
|
|
1243
1349
|
if (!hasSrcDir) {
|
|
1244
1350
|
throw new StackMismatchError(
|
|
@@ -1285,19 +1391,20 @@ async function fileExists2(p) {
|
|
|
1285
1391
|
|
|
1286
1392
|
// src/codemod/emit-tokens.ts
|
|
1287
1393
|
init_esm_shims();
|
|
1288
|
-
import { join as
|
|
1394
|
+
import { join as join4 } from "path";
|
|
1289
1395
|
import { readFile as readFile4 } from "fs/promises";
|
|
1290
|
-
import { fileURLToPath as fileURLToPath3 } from "url";
|
|
1291
|
-
var __dirname3 = dirname2(fileURLToPath3(import.meta.url));
|
|
1292
1396
|
async function emitTokens(opts) {
|
|
1293
1397
|
const { direction, stack } = opts;
|
|
1294
1398
|
const tokensTs = await loadDirectionTokensSource(direction);
|
|
1295
1399
|
const directionMd = await loadDirectionMarkdown(direction);
|
|
1296
1400
|
const parsed = parseDirectionTokens(tokensTs, direction);
|
|
1401
|
+
if (Object.keys(parsed.colors).length === 0) {
|
|
1402
|
+
throw unemittableError(direction, tokensTs);
|
|
1403
|
+
}
|
|
1297
1404
|
const tailwindConfig = renderTailwindConfig(parsed, stack);
|
|
1298
1405
|
const indexCss = renderIndexCss(parsed, directionMd);
|
|
1299
|
-
const tailwindPath = stack.tailwindConfigPath ? stack.tailwindConfigPath :
|
|
1300
|
-
const indexCssPath =
|
|
1406
|
+
const tailwindPath = stack.tailwindConfigPath ? stack.tailwindConfigPath : join4(stack.rootPath, stack.ts ? "tailwind.config.ts" : "tailwind.config.js");
|
|
1407
|
+
const indexCssPath = join4(stack.srcPath, "index.css");
|
|
1301
1408
|
const notes = [];
|
|
1302
1409
|
if (stack.hasTailwindConfig) {
|
|
1303
1410
|
notes.push(
|
|
@@ -1310,33 +1417,55 @@ async function emitTokens(opts) {
|
|
|
1310
1417
|
notes
|
|
1311
1418
|
};
|
|
1312
1419
|
}
|
|
1420
|
+
async function assertDirectionEmittable(slug) {
|
|
1421
|
+
const src = await loadDirectionTokensSource(slug);
|
|
1422
|
+
const parsed = parseDirectionTokens(src, slug);
|
|
1423
|
+
if (Object.keys(parsed.colors).length === 0) {
|
|
1424
|
+
throw unemittableError(slug, src);
|
|
1425
|
+
}
|
|
1426
|
+
}
|
|
1427
|
+
function unemittableError(slug, src) {
|
|
1428
|
+
const shape = /^\s*palette:\s*\{/m.test(src) ? `"${slug}" ships the newer theme.yml palette format, which the token
|
|
1429
|
+
emitter can't consume yet.` : `"${slug}" has no readable colors block in its tokens.ts.`;
|
|
1430
|
+
return new CliError(
|
|
1431
|
+
`Can't generate tokens for direction "${slug}".
|
|
1432
|
+
|
|
1433
|
+
${shape}
|
|
1434
|
+
|
|
1435
|
+
Directions that emit cleanly today include:
|
|
1436
|
+
soft-modern \xB7 signal-dark \xB7 brutalist \xB7 newsletter \xB7 cinematic-dark
|
|
1437
|
+
dense-utility \xB7 mid-century \xB7 maximalist \xB7 couture-luxury \xB7 architectural
|
|
1438
|
+
|
|
1439
|
+
Nothing has been written \u2014 pick another --style and re-run.`
|
|
1440
|
+
);
|
|
1441
|
+
}
|
|
1313
1442
|
async function loadDirectionTokensSource(slug) {
|
|
1314
|
-
const
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
|
|
1323
|
-
|
|
1443
|
+
const themesDir = resolveAssetDir("themes");
|
|
1444
|
+
if (!themesDir) {
|
|
1445
|
+
throw new CliError(
|
|
1446
|
+
`Canonical tokens.ts for direction "${slug}" not found.
|
|
1447
|
+
|
|
1448
|
+
${missingAssetMessage("themes")}`
|
|
1449
|
+
);
|
|
1450
|
+
}
|
|
1451
|
+
try {
|
|
1452
|
+
return await readFile4(join4(themesDir, slug, "tokens.ts"), "utf8");
|
|
1453
|
+
} catch {
|
|
1454
|
+
throw new CliError(
|
|
1455
|
+
`Canonical tokens.ts for direction "${slug}" not found at ${join4(themesDir, slug, "tokens.ts")}.
|
|
1456
|
+
|
|
1457
|
+
Pass a different --style, or report this at https://github.com/frischermf/taste/issues.`
|
|
1458
|
+
);
|
|
1324
1459
|
}
|
|
1325
|
-
throw new CliError(`Canonical tokens.ts for direction "${slug}" not found.`);
|
|
1326
1460
|
}
|
|
1327
1461
|
async function loadDirectionMarkdown(slug) {
|
|
1328
|
-
const
|
|
1329
|
-
|
|
1330
|
-
|
|
1331
|
-
|
|
1332
|
-
|
|
1333
|
-
|
|
1334
|
-
return await readFile4(p, "utf8");
|
|
1335
|
-
} catch {
|
|
1336
|
-
continue;
|
|
1337
|
-
}
|
|
1462
|
+
const themesDir = resolveAssetDir("themes");
|
|
1463
|
+
if (!themesDir) return "";
|
|
1464
|
+
try {
|
|
1465
|
+
return await readFile4(join4(themesDir, slug, "THEME.md"), "utf8");
|
|
1466
|
+
} catch {
|
|
1467
|
+
return "";
|
|
1338
1468
|
}
|
|
1339
|
-
return "";
|
|
1340
1469
|
}
|
|
1341
1470
|
function parseDirectionTokens(src, slug) {
|
|
1342
1471
|
const ethos = matchString(src, /ethos:\s*"([^"]+)"/) ?? `(no ethos for ${slug})`;
|
|
@@ -1524,6 +1653,11 @@ ${indent(k.css, 4)}
|
|
|
1524
1653
|
box-shadow: ${value};
|
|
1525
1654
|
}`
|
|
1526
1655
|
).join("\n\n");
|
|
1656
|
+
const borderReset = d.colors.border ? ` * {
|
|
1657
|
+
@apply border-border;
|
|
1658
|
+
}
|
|
1659
|
+
|
|
1660
|
+
` : "";
|
|
1527
1661
|
return `${headerComment}${fontImport}@tailwind base;
|
|
1528
1662
|
@tailwind components;
|
|
1529
1663
|
@tailwind utilities;
|
|
@@ -1535,11 +1669,7 @@ ${tokenLines}
|
|
|
1535
1669
|
}
|
|
1536
1670
|
|
|
1537
1671
|
@layer base {
|
|
1538
|
-
|
|
1539
|
-
@apply border-border;
|
|
1540
|
-
}
|
|
1541
|
-
|
|
1542
|
-
html {
|
|
1672
|
+
${borderReset} html {
|
|
1543
1673
|
scroll-behavior: smooth;
|
|
1544
1674
|
}
|
|
1545
1675
|
|
|
@@ -1623,12 +1753,12 @@ function indent(s, n) {
|
|
|
1623
1753
|
|
|
1624
1754
|
// src/commands/apply.ts
|
|
1625
1755
|
init_esm_shims();
|
|
1626
|
-
import { join as
|
|
1756
|
+
import { join as join5, relative as relative2, resolve as resolve2 } from "path";
|
|
1627
1757
|
import { access as access3, mkdir as mkdir2, readFile as readFile5, writeFile as writeFile2 } from "fs/promises";
|
|
1628
1758
|
import { execSync } from "child_process";
|
|
1629
1759
|
var ROUTING_MARK_START2 = "<!-- stylecrib:routing:start -->";
|
|
1630
1760
|
async function apply(opts = {}) {
|
|
1631
|
-
const targetRoot =
|
|
1761
|
+
const targetRoot = resolve2(opts.target ?? process.cwd());
|
|
1632
1762
|
await applyFromStaging({
|
|
1633
1763
|
targetRoot,
|
|
1634
1764
|
branchName: opts.branch,
|
|
@@ -1637,13 +1767,13 @@ async function apply(opts = {}) {
|
|
|
1637
1767
|
}
|
|
1638
1768
|
async function applyFromStaging(opts) {
|
|
1639
1769
|
const { targetRoot, noBranch } = opts;
|
|
1640
|
-
const stagingRoot =
|
|
1770
|
+
const stagingRoot = join5(targetRoot, ".design-upgrade");
|
|
1641
1771
|
if (!await dirExists(stagingRoot)) {
|
|
1642
1772
|
throw new CliError(
|
|
1643
1773
|
`No staged changes at ${stagingRoot}. Run \`stylecrib init\` first.`
|
|
1644
1774
|
);
|
|
1645
1775
|
}
|
|
1646
|
-
const isGit = await dirExists(
|
|
1776
|
+
const isGit = await dirExists(join5(targetRoot, ".git"));
|
|
1647
1777
|
if (isGit && !noBranch) {
|
|
1648
1778
|
const branchName = opts.branchName ?? defaultBranchName();
|
|
1649
1779
|
try {
|
|
@@ -1672,7 +1802,7 @@ Pass --no-branch to skip branch creation.`,
|
|
|
1672
1802
|
const filesCopied = [];
|
|
1673
1803
|
for await (const stagedPath of walk(stagingRoot)) {
|
|
1674
1804
|
const rel = relative2(stagingRoot, stagedPath);
|
|
1675
|
-
const targetPath =
|
|
1805
|
+
const targetPath = join5(targetRoot, rel);
|
|
1676
1806
|
if (rel === "CLAUDE.md" && await fileExists3(targetPath)) {
|
|
1677
1807
|
const existing = await readFile5(targetPath, "utf8");
|
|
1678
1808
|
if (existing.includes(ROUTING_MARK_START2)) {
|
|
@@ -1734,8 +1864,8 @@ function dirname3(p) {
|
|
|
1734
1864
|
// src/commands/install-hooks.ts
|
|
1735
1865
|
init_esm_shims();
|
|
1736
1866
|
var import_picocolors2 = __toESM(require_picocolors(), 1);
|
|
1737
|
-
import { existsSync, mkdirSync, writeFileSync, chmodSync, readFileSync } from "fs";
|
|
1738
|
-
import { resolve as
|
|
1867
|
+
import { existsSync as existsSync2, mkdirSync, writeFileSync, chmodSync, readFileSync } from "fs";
|
|
1868
|
+
import { resolve as resolve3, join as join6, dirname as dirname4 } from "path";
|
|
1739
1869
|
|
|
1740
1870
|
// src/lib/bundled-agents.ts
|
|
1741
1871
|
init_esm_shims();
|
|
@@ -1833,11 +1963,11 @@ var BUNDLED_AGENTS = {
|
|
|
1833
1963
|
};
|
|
1834
1964
|
function loadAgentDoctrine(target, agentName) {
|
|
1835
1965
|
try {
|
|
1836
|
-
const { existsSync:
|
|
1837
|
-
const { join:
|
|
1838
|
-
const localPath =
|
|
1839
|
-
if (
|
|
1840
|
-
return
|
|
1966
|
+
const { existsSync: existsSync20, readFileSync: readFileSync20 } = __require("fs");
|
|
1967
|
+
const { join: join27 } = __require("path");
|
|
1968
|
+
const localPath = join27(target, ".claude", "agents", `${agentName}.md`);
|
|
1969
|
+
if (existsSync20(localPath)) {
|
|
1970
|
+
return readFileSync20(localPath, "utf8");
|
|
1841
1971
|
}
|
|
1842
1972
|
} catch {
|
|
1843
1973
|
}
|
|
@@ -1846,73 +1976,77 @@ function loadAgentDoctrine(target, agentName) {
|
|
|
1846
1976
|
|
|
1847
1977
|
// src/commands/install-hooks.ts
|
|
1848
1978
|
async function installHooks(opts = {}) {
|
|
1849
|
-
const target =
|
|
1850
|
-
if (!
|
|
1851
|
-
|
|
1852
|
-
console.error(import_picocolors2.default.dim(" Run `git init` first, then `stylecrib install-hooks`."));
|
|
1853
|
-
process.exit(1);
|
|
1979
|
+
const target = resolve3(opts.target ?? process.cwd());
|
|
1980
|
+
if (!existsSync2(join6(target, ".git"))) {
|
|
1981
|
+
throw new NotAGitRepoError(target);
|
|
1854
1982
|
}
|
|
1855
1983
|
const writes = [];
|
|
1856
|
-
const huskyDir =
|
|
1857
|
-
const useHusky =
|
|
1858
|
-
const hookPath = useHusky ?
|
|
1984
|
+
const huskyDir = join6(target, ".husky");
|
|
1985
|
+
const useHusky = existsSync2(huskyDir);
|
|
1986
|
+
const hookPath = useHusky ? join6(huskyDir, "pre-commit") : join6(target, ".git/hooks/pre-commit");
|
|
1859
1987
|
const hookBody = PRE_COMMIT_HOOK;
|
|
1860
1988
|
writes.push({ path: hookPath, body: hookBody, chmod: 493 });
|
|
1861
1989
|
if (opts.cursorrules !== false) {
|
|
1862
1990
|
writes.push({
|
|
1863
|
-
path:
|
|
1991
|
+
path: join6(target, ".cursorrules"),
|
|
1864
1992
|
body: CURSORRULES
|
|
1865
1993
|
});
|
|
1866
1994
|
}
|
|
1867
1995
|
writes.push({
|
|
1868
|
-
path:
|
|
1996
|
+
path: join6(target, ".github/workflows/stylecrib-review.yml"),
|
|
1869
1997
|
body: GITHUB_ACTION
|
|
1870
1998
|
});
|
|
1871
1999
|
writes.push({
|
|
1872
|
-
path:
|
|
2000
|
+
path: join6(target, ".claude/settings.json"),
|
|
1873
2001
|
body: CLAUDE_SETTINGS
|
|
1874
2002
|
});
|
|
1875
2003
|
writes.push({
|
|
1876
|
-
path:
|
|
1877
|
-
body:
|
|
2004
|
+
path: join6(target, ".stylecrib/config.json"),
|
|
2005
|
+
body: mergeTasteConfig(join6(target, ".stylecrib/config.json"), opts.theme),
|
|
2006
|
+
merge: true
|
|
1878
2007
|
});
|
|
1879
2008
|
writes.push({
|
|
1880
|
-
path:
|
|
2009
|
+
path: join6(target, ".stylecrib/RULEBOOK.md"),
|
|
1881
2010
|
body: renderRulebook()
|
|
1882
2011
|
});
|
|
1883
2012
|
for (const [name, doctrine] of Object.entries(BUNDLED_AGENTS)) {
|
|
1884
2013
|
writes.push({
|
|
1885
|
-
path:
|
|
2014
|
+
path: join6(target, ".claude/agents", `${name}.md`),
|
|
1886
2015
|
body: doctrine
|
|
1887
2016
|
});
|
|
1888
2017
|
}
|
|
1889
2018
|
console.log(import_picocolors2.default.bold("\u25C7 StyleCrib enforcement layer\n"));
|
|
1890
2019
|
const collisions = [];
|
|
1891
2020
|
let wroteAny = false;
|
|
2021
|
+
let wroteCount = 0;
|
|
1892
2022
|
for (const w of writes) {
|
|
1893
|
-
const exists =
|
|
2023
|
+
const exists = existsSync2(w.path);
|
|
1894
2024
|
if (exists) {
|
|
1895
2025
|
const existing = readFileSync(w.path, "utf8");
|
|
1896
2026
|
if (existing === w.body) {
|
|
1897
2027
|
console.log(import_picocolors2.default.dim(" \xB7 " + relPath2(w.path, target) + " (unchanged)"));
|
|
1898
2028
|
continue;
|
|
1899
2029
|
}
|
|
1900
|
-
if (
|
|
2030
|
+
if (w.merge) {
|
|
2031
|
+
console.log(import_picocolors2.default.dim(" \xB7 " + relPath2(w.path, target) + " (merged)"));
|
|
2032
|
+
} else if (!opts.force) {
|
|
1901
2033
|
console.log(
|
|
1902
2034
|
import_picocolors2.default.yellow(" \u26A0 ") + relPath2(w.path, target) + import_picocolors2.default.dim(" exists with different content \u2014 see summary below")
|
|
1903
2035
|
);
|
|
1904
2036
|
collisions.push({ path: w.path, existing });
|
|
1905
2037
|
continue;
|
|
2038
|
+
} else {
|
|
2039
|
+
const backup = w.path + ".pre-stylecrib.bak";
|
|
2040
|
+
writeFileSync(backup, existing);
|
|
2041
|
+
console.log(import_picocolors2.default.dim(" \u21AA backed up existing content to " + relPath2(backup, target)));
|
|
1906
2042
|
}
|
|
1907
|
-
const backup = w.path + ".pre-taste.bak";
|
|
1908
|
-
writeFileSync(backup, existing);
|
|
1909
|
-
console.log(import_picocolors2.default.dim(" \u21AA backed up existing content to " + relPath2(backup, target)));
|
|
1910
2043
|
}
|
|
1911
2044
|
mkdirSync(dirname4(w.path), { recursive: true });
|
|
1912
2045
|
writeFileSync(w.path, w.body);
|
|
1913
2046
|
if (w.chmod) chmodSync(w.path, w.chmod);
|
|
1914
2047
|
console.log(import_picocolors2.default.green(" \u2713 ") + relPath2(w.path, target));
|
|
1915
2048
|
wroteAny = true;
|
|
2049
|
+
wroteCount++;
|
|
1916
2050
|
}
|
|
1917
2051
|
if (collisions.length > 0) {
|
|
1918
2052
|
console.error("");
|
|
@@ -1921,7 +2055,7 @@ async function installHooks(opts = {}) {
|
|
|
1921
2055
|
console.error(import_picocolors2.default.bold(" What to do:"));
|
|
1922
2056
|
console.error("");
|
|
1923
2057
|
console.error(" Option 1 \u2014 pass --force to overwrite. The existing content of each file");
|
|
1924
|
-
console.error(" will be backed up to `<path>.pre-
|
|
2058
|
+
console.error(" will be backed up to `<path>.pre-stylecrib.bak` before the new content lands.");
|
|
1925
2059
|
console.error("");
|
|
1926
2060
|
console.error(" Option 2 \u2014 inspect each collision, merge by hand, and re-run.");
|
|
1927
2061
|
console.error("");
|
|
@@ -1932,7 +2066,7 @@ async function installHooks(opts = {}) {
|
|
|
1932
2066
|
console.error("");
|
|
1933
2067
|
console.error(import_picocolors2.default.dim(" Half-installed enforcement is worse than none \u2014 the customer would"));
|
|
1934
2068
|
console.error(import_picocolors2.default.dim(" assume the gate is active. Refusing to print the success banner."));
|
|
1935
|
-
|
|
2069
|
+
throw new HookCollisionError(collisions.map((x) => relPath2(x.path, target)), wroteCount);
|
|
1936
2070
|
}
|
|
1937
2071
|
if (!wroteAny) {
|
|
1938
2072
|
console.log("");
|
|
@@ -1957,7 +2091,7 @@ async function installHooks(opts = {}) {
|
|
|
1957
2091
|
console.log(" AI agents should read this at session start; humans should read it in code review.");
|
|
1958
2092
|
console.log("");
|
|
1959
2093
|
console.log(import_picocolors2.default.bold("The agents:"));
|
|
1960
|
-
console.log(" \xB7 " + import_picocolors2.default.cyan(".claude/agents/") + " \u2014 qa-conductor +
|
|
2094
|
+
console.log(" \xB7 " + import_picocolors2.default.cyan(".claude/agents/") + " \u2014 qa-conductor + 18 specialist agents, ready for immediate use.");
|
|
1961
2095
|
console.log("");
|
|
1962
2096
|
console.log(import_picocolors2.default.dim("Config: .stylecrib/config.json (enforce: 'hard' by default; downgrade to 'warn' for gradual migration)."));
|
|
1963
2097
|
console.log(import_picocolors2.default.dim('Bypass: `git commit --no-verify` or `STYLECRIB_SKIP_QA="reason" git commit` (logged for review).'));
|
|
@@ -1994,15 +2128,46 @@ var CLAUDE_SETTINGS = `{
|
|
|
1994
2128
|
}
|
|
1995
2129
|
}
|
|
1996
2130
|
`;
|
|
1997
|
-
|
|
1998
|
-
|
|
1999
|
-
|
|
2131
|
+
function mergeTasteConfig(path2, theme) {
|
|
2132
|
+
let existing = null;
|
|
2133
|
+
if (existsSync2(path2)) {
|
|
2134
|
+
try {
|
|
2135
|
+
const parsed = JSON.parse(readFileSync(path2, "utf8"));
|
|
2136
|
+
if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
|
|
2137
|
+
existing = parsed;
|
|
2138
|
+
}
|
|
2139
|
+
} catch {
|
|
2140
|
+
try {
|
|
2141
|
+
writeFileSync(path2 + ".pre-stylecrib.bak", readFileSync(path2, "utf8"));
|
|
2142
|
+
} catch {
|
|
2143
|
+
}
|
|
2144
|
+
}
|
|
2145
|
+
}
|
|
2146
|
+
if (!existing) return tasteConfig(theme);
|
|
2147
|
+
const merged = { ...existing };
|
|
2148
|
+
if (typeof merged.enforce !== "string") merged.enforce = "hard";
|
|
2149
|
+
if (theme && typeof merged.theme !== "string") merged.theme = theme;
|
|
2150
|
+
merged.notes = JSON.parse(tasteConfig(typeof merged.theme === "string" ? merged.theme : theme)).notes;
|
|
2151
|
+
return JSON.stringify(merged, null, 2) + "\n";
|
|
2152
|
+
}
|
|
2153
|
+
function tasteConfig(theme) {
|
|
2154
|
+
const notes = [
|
|
2000
2155
|
"enforce: 'hard' (default) \u2014 pre-commit BLOCKs on block-severity slop hits AND requires qa-conductor auth on every UI-touching commit.",
|
|
2001
|
-
"enforce: 'warn' \u2014 downgrades every gate to advisory. Reserved for gradual-migration mode when the pack was installed on top of a large existing codebase."
|
|
2156
|
+
"enforce: 'warn' \u2014 downgrades every gate to advisory. Reserved for gradual-migration mode when the pack was installed on top of a large existing codebase."
|
|
2157
|
+
];
|
|
2158
|
+
if (theme) {
|
|
2159
|
+
notes.push(
|
|
2160
|
+
"theme: the active StyleCrib theme slug. Read by 'stylecrib check' to enforce per-theme rules such as R-SECOND-ACCENT. Change it if you switch themes."
|
|
2161
|
+
);
|
|
2162
|
+
}
|
|
2163
|
+
notes.push(
|
|
2002
2164
|
"See https://github.com/frischermf/stylecrib/blob/main/README.md for the full rule catalog and each rule's escape hatch."
|
|
2003
|
-
|
|
2165
|
+
);
|
|
2166
|
+
const config = { enforce: "hard" };
|
|
2167
|
+
if (theme) config.theme = theme;
|
|
2168
|
+
config.notes = notes;
|
|
2169
|
+
return JSON.stringify(config, null, 2) + "\n";
|
|
2004
2170
|
}
|
|
2005
|
-
`;
|
|
2006
2171
|
function renderRulebook() {
|
|
2007
2172
|
const { SLOP_RULES: SLOP_RULES2 } = (init_slop_rules(), __toCommonJS(slop_rules_exports));
|
|
2008
2173
|
const lines = [];
|
|
@@ -2122,10 +2287,10 @@ jobs:
|
|
|
2122
2287
|
with:
|
|
2123
2288
|
node-version: "20"
|
|
2124
2289
|
cache: npm
|
|
2125
|
-
# Install the
|
|
2126
|
-
#
|
|
2127
|
-
#
|
|
2128
|
-
#
|
|
2290
|
+
# Install the repo's own pinned dependencies first. Without this, a bare
|
|
2291
|
+
# \`npx stylecrib\` on a fresh runner would fetch whatever package owns
|
|
2292
|
+
# that name on the registry and run it with ANTHROPIC_API_KEY in the
|
|
2293
|
+
# environment.
|
|
2129
2294
|
- run: npm ci
|
|
2130
2295
|
- name: Diff against base
|
|
2131
2296
|
run: |
|
|
@@ -2142,11 +2307,13 @@ jobs:
|
|
|
2142
2307
|
init_esm_shims();
|
|
2143
2308
|
var import_picocolors3 = __toESM(require_picocolors(), 1);
|
|
2144
2309
|
import { execSync as execSync2 } from "child_process";
|
|
2145
|
-
import { existsSync as
|
|
2146
|
-
import { dirname as dirname5, join as
|
|
2310
|
+
import { existsSync as existsSync3, mkdirSync as mkdirSync2, readFileSync as readFileSync2, writeFileSync as writeFileSync2 } from "fs";
|
|
2311
|
+
import { dirname as dirname5, join as join7, relative as relative3, resolve as resolve4 } from "path";
|
|
2312
|
+
var RUNTIME_PKG = "@stylecrib/react";
|
|
2313
|
+
var RUNTIME_PKG_RANGE = ">=0.2.2 <0.3.0";
|
|
2147
2314
|
function readDeps(target) {
|
|
2148
|
-
const path2 =
|
|
2149
|
-
if (!
|
|
2315
|
+
const path2 = join7(target, "package.json");
|
|
2316
|
+
if (!existsSync3(path2)) return null;
|
|
2150
2317
|
try {
|
|
2151
2318
|
const pkg = JSON.parse(readFileSync2(path2, "utf8"));
|
|
2152
2319
|
return { ...pkg.dependencies ?? {}, ...pkg.devDependencies ?? {} };
|
|
@@ -2161,14 +2328,55 @@ function hasReactRouter(deps) {
|
|
|
2161
2328
|
return "react-router-dom" in deps;
|
|
2162
2329
|
}
|
|
2163
2330
|
function resolveWireFilePath(target) {
|
|
2164
|
-
const srcLib =
|
|
2165
|
-
const src =
|
|
2166
|
-
if (
|
|
2167
|
-
if (
|
|
2168
|
-
return
|
|
2331
|
+
const srcLib = join7(target, "src", "lib");
|
|
2332
|
+
const src = join7(target, "src");
|
|
2333
|
+
if (existsSync3(srcLib)) return join7(srcLib, "stylecrib-wire.tsx");
|
|
2334
|
+
if (existsSync3(src)) return join7(src, "lib", "stylecrib-wire.tsx");
|
|
2335
|
+
return join7(target, "src", "lib", "stylecrib-wire.tsx");
|
|
2336
|
+
}
|
|
2337
|
+
function runtimeAtLeast(target, major, minor, patch = 0) {
|
|
2338
|
+
try {
|
|
2339
|
+
const pkg = JSON.parse(
|
|
2340
|
+
readFileSync2(join7(target, "node_modules", RUNTIME_PKG, "package.json"), "utf8")
|
|
2341
|
+
);
|
|
2342
|
+
const m = String(pkg.version ?? "").match(/^(\d+)\.(\d+)\.(\d+)/);
|
|
2343
|
+
if (!m) return false;
|
|
2344
|
+
const [maj, min, pat] = [m[1], m[2], m[3]].map((n) => parseInt(n, 10));
|
|
2345
|
+
if (maj !== major) return maj > major;
|
|
2346
|
+
if (min !== minor) return min > minor;
|
|
2347
|
+
return pat >= patch;
|
|
2348
|
+
} catch {
|
|
2349
|
+
return false;
|
|
2350
|
+
}
|
|
2169
2351
|
}
|
|
2170
|
-
function
|
|
2352
|
+
function resolveThemeFonts(themeSlug) {
|
|
2353
|
+
const themesDir = resolveAssetDir("themes");
|
|
2354
|
+
if (!themesDir) return null;
|
|
2355
|
+
const tokensPath = join7(themesDir, themeSlug, "tokens.ts");
|
|
2356
|
+
if (!existsSync3(tokensPath)) return null;
|
|
2357
|
+
let src;
|
|
2358
|
+
try {
|
|
2359
|
+
src = readFileSync2(tokensPath, "utf8");
|
|
2360
|
+
} catch {
|
|
2361
|
+
return null;
|
|
2362
|
+
}
|
|
2363
|
+
const pick = (role) => {
|
|
2364
|
+
const block = src.match(new RegExp(`${role}:\\s*\\{([\\s\\S]{0,600}?)\\}`));
|
|
2365
|
+
if (!block) return [];
|
|
2366
|
+
const fam = block[1].match(/family:\s*"([^"]+)"/);
|
|
2367
|
+
return fam ? [fam[1]] : [];
|
|
2368
|
+
};
|
|
2369
|
+
const fonts = { display: pick("display"), body: pick("body"), mono: pick("mono") };
|
|
2370
|
+
if (!fonts.display.length && !fonts.body.length && !fonts.mono.length) return null;
|
|
2371
|
+
return fonts;
|
|
2372
|
+
}
|
|
2373
|
+
function buildTasteWireTemplate(opts) {
|
|
2171
2374
|
const { themeSlug, themeName, themeSwatch, scaffoldedAt } = opts;
|
|
2375
|
+
const fonts = opts.emitFonts ? resolveThemeFonts(themeSlug) : null;
|
|
2376
|
+
const themeFontsBlock = fonts ? `
|
|
2377
|
+
fonts: {
|
|
2378
|
+
` + ["display", "body", "mono"].filter((role) => fonts[role].length > 0).map((role) => ` ${role}: ${JSON.stringify(fonts[role])},`).join("\n") + `
|
|
2379
|
+
},` : "";
|
|
2172
2380
|
return `// StyleCrib \u2014 customer runtime wire-up
|
|
2173
2381
|
// Scaffolded by \`stylecrib init\` on ${scaffoldedAt}.
|
|
2174
2382
|
//
|
|
@@ -2208,7 +2416,7 @@ const INSTALLED_THEMES: RegisteredTheme[] = [
|
|
|
2208
2416
|
name: "${themeName}",
|
|
2209
2417
|
swatch: "${themeSwatch}",
|
|
2210
2418
|
installed: true,
|
|
2211
|
-
briefUrl: "https://github.com/frischermf/stylecrib/tree/main/themes/${themeSlug}"
|
|
2419
|
+
briefUrl: "https://github.com/frischermf/stylecrib/tree/main/themes/${themeSlug}",${themeFontsBlock}
|
|
2212
2420
|
},
|
|
2213
2421
|
];
|
|
2214
2422
|
|
|
@@ -2242,9 +2450,10 @@ export function StyleCribWire({ children }: { children: ReactNode }) {
|
|
|
2242
2450
|
}
|
|
2243
2451
|
function runNpmInstall(target, silent) {
|
|
2244
2452
|
try {
|
|
2245
|
-
const hasYarnLock =
|
|
2246
|
-
const hasPnpmLock =
|
|
2247
|
-
const
|
|
2453
|
+
const hasYarnLock = existsSync3(join7(target, "yarn.lock"));
|
|
2454
|
+
const hasPnpmLock = existsSync3(join7(target, "pnpm-lock.yaml"));
|
|
2455
|
+
const spec = `"${RUNTIME_PKG}@${RUNTIME_PKG_RANGE}"`;
|
|
2456
|
+
const cmd = hasPnpmLock ? `pnpm add ${spec}` : hasYarnLock ? `yarn add ${spec}` : `npm install ${spec}`;
|
|
2248
2457
|
if (!silent) console.log(import_picocolors3.default.dim(" running: " + cmd));
|
|
2249
2458
|
execSync2(cmd, { cwd: target, stdio: silent ? "ignore" : "inherit" });
|
|
2250
2459
|
return true;
|
|
@@ -2253,7 +2462,7 @@ function runNpmInstall(target, silent) {
|
|
|
2253
2462
|
}
|
|
2254
2463
|
}
|
|
2255
2464
|
async function installRuntimeScaffold(opts) {
|
|
2256
|
-
const target =
|
|
2465
|
+
const target = resolve4(opts.target);
|
|
2257
2466
|
const themeSwatch = opts.themeSwatch ?? "#FFDF29";
|
|
2258
2467
|
const silent = opts.silent ?? false;
|
|
2259
2468
|
if (!silent) console.log(import_picocolors3.default.bold("\u25C7 Runtime pack\n"));
|
|
@@ -2290,29 +2499,30 @@ async function installRuntimeScaffold(opts) {
|
|
|
2290
2499
|
}
|
|
2291
2500
|
const scaffoldPath = resolveWireFilePath(target);
|
|
2292
2501
|
const scaffoldRel = relative3(target, scaffoldPath);
|
|
2293
|
-
if (
|
|
2502
|
+
if (existsSync3(scaffoldPath) && !opts.force) {
|
|
2294
2503
|
if (!silent) {
|
|
2295
2504
|
console.log("");
|
|
2296
2505
|
console.log(import_picocolors3.default.red(" \u2717 ") + scaffoldRel + " already exists.");
|
|
2297
2506
|
console.log(import_picocolors3.default.dim(" Refusing to overwrite. Options:"));
|
|
2298
2507
|
console.log(import_picocolors3.default.dim(" \xB7 Rename / archive the existing file and re-run `stylecrib init`."));
|
|
2299
|
-
console.log(import_picocolors3.default.dim(" \xB7 Pass `--force-runtime` to overwrite (backs up existing to `.pre-
|
|
2508
|
+
console.log(import_picocolors3.default.dim(" \xB7 Pass `--force-runtime` to overwrite (backs up existing to `.pre-stylecrib.bak` first)."));
|
|
2300
2509
|
console.log(import_picocolors3.default.dim(" \xB7 Skip this scaffold with `--no-runtime` \u2014 copy the wire-up manually per docs/STYLECRIB_ADMIN.md."));
|
|
2301
2510
|
}
|
|
2302
2511
|
return { kind: "collision", scaffoldPath };
|
|
2303
2512
|
}
|
|
2304
|
-
if (
|
|
2305
|
-
const backup = scaffoldPath + ".pre-
|
|
2513
|
+
if (existsSync3(scaffoldPath) && opts.force) {
|
|
2514
|
+
const backup = scaffoldPath + ".pre-stylecrib.bak";
|
|
2306
2515
|
writeFileSync2(backup, readFileSync2(scaffoldPath, "utf8"));
|
|
2307
2516
|
if (!silent) console.log(import_picocolors3.default.dim(" \u21AA backed up existing " + scaffoldRel + " to " + relative3(target, backup)));
|
|
2308
2517
|
}
|
|
2309
2518
|
mkdirSync2(dirname5(scaffoldPath), { recursive: true });
|
|
2310
2519
|
writeFileSync2(
|
|
2311
2520
|
scaffoldPath,
|
|
2312
|
-
|
|
2521
|
+
buildTasteWireTemplate({
|
|
2313
2522
|
themeSlug: opts.themeSlug,
|
|
2314
2523
|
themeName: opts.themeName,
|
|
2315
2524
|
themeSwatch,
|
|
2525
|
+
emitFonts: runtimeAtLeast(target, 0, 2, 2),
|
|
2316
2526
|
scaffoldedAt: (/* @__PURE__ */ new Date()).toISOString().slice(0, 10)
|
|
2317
2527
|
})
|
|
2318
2528
|
);
|
|
@@ -2337,12 +2547,36 @@ async function installRuntimeScaffold(opts) {
|
|
|
2337
2547
|
return { kind: "installed", scaffoldPath, ranNpmInstall };
|
|
2338
2548
|
}
|
|
2339
2549
|
|
|
2550
|
+
// src/lib/tty.ts
|
|
2551
|
+
init_esm_shims();
|
|
2552
|
+
function isInteractive() {
|
|
2553
|
+
return process.stdin.isTTY === true && process.stdout.isTTY === true;
|
|
2554
|
+
}
|
|
2555
|
+
function assertInteractive(what) {
|
|
2556
|
+
if (isInteractive()) return;
|
|
2557
|
+
throw new CliError(
|
|
2558
|
+
`${what} needs a terminal, and none is attached.
|
|
2559
|
+
|
|
2560
|
+
Re-run one of these ways:
|
|
2561
|
+
\u2022 --yes accept canonical defaults, no prompts
|
|
2562
|
+
\u2022 --style <slug> pick the theme directly, no prompts
|
|
2563
|
+
\u2022 from an interactive shell (not piped, not CI)`
|
|
2564
|
+
);
|
|
2565
|
+
}
|
|
2566
|
+
|
|
2567
|
+
// src/lib/version.ts
|
|
2568
|
+
init_esm_shims();
|
|
2569
|
+
var VERSION = true ? "0.3.2" : "0.0.0-dev";
|
|
2570
|
+
|
|
2340
2571
|
// src/commands/init.ts
|
|
2341
|
-
var __dirname4 = dirname6(fileURLToPath4(import.meta.url));
|
|
2342
2572
|
async function init(opts) {
|
|
2343
|
-
const target =
|
|
2344
|
-
|
|
2345
|
-
|
|
2573
|
+
const target = resolve5(opts.target ?? process.cwd());
|
|
2574
|
+
assertValidDirection(opts.style);
|
|
2575
|
+
if (opts.style && opts.style !== "custom") {
|
|
2576
|
+
await assertDirectionEmittable(opts.style);
|
|
2577
|
+
}
|
|
2578
|
+
const nonInteractive = opts.yes === true || !isInteractive();
|
|
2579
|
+
console.log(c.brand("StyleCrib") + c.dim(` \u2014 design upgrade CLI \xB7 v${VERSION}`));
|
|
2346
2580
|
console.log(c.dim("target: ") + target);
|
|
2347
2581
|
if (opts.dryRun) console.log(c.warn("DRY RUN \u2014 no files will be written\n"));
|
|
2348
2582
|
console.log(step(1, 5, "Detect stack"));
|
|
@@ -2352,7 +2586,8 @@ async function init(opts) {
|
|
|
2352
2586
|
console.log(header(step(2, 5, "Collect direction")));
|
|
2353
2587
|
const wizard = await runWizard({
|
|
2354
2588
|
defaultProjectName: basename(target),
|
|
2355
|
-
skippedStyle: opts.style
|
|
2589
|
+
skippedStyle: opts.style,
|
|
2590
|
+
nonInteractive
|
|
2356
2591
|
});
|
|
2357
2592
|
console.log(header(step(3, 5, "Ingest references")));
|
|
2358
2593
|
const contextBlocks = [];
|
|
@@ -2403,6 +2638,7 @@ async function init(opts) {
|
|
|
2403
2638
|
rationale: `Pure-canonical "${wizard.baseDirection}" install \u2014 Director call skipped to save tokens.`
|
|
2404
2639
|
};
|
|
2405
2640
|
} else {
|
|
2641
|
+
if (!process.env.ANTHROPIC_API_KEY) throw new MissingApiKeyError();
|
|
2406
2642
|
console.log(c.dim(" calling Anthropic API (this can take 20-60s)..."));
|
|
2407
2643
|
const baseMd = wizard.baseDirection === "custom" ? "" : await loadCanonicalDirectionMarkdown(wizard.baseDirection);
|
|
2408
2644
|
directorOutput = await callDirector({
|
|
@@ -2450,10 +2686,17 @@ async function init(opts) {
|
|
|
2450
2686
|
console.log(
|
|
2451
2687
|
c.dim(` ${result.filesStaged.length} file(s) staged at ${result.stagingRoot}`)
|
|
2452
2688
|
);
|
|
2453
|
-
|
|
2454
|
-
|
|
2455
|
-
|
|
2456
|
-
})
|
|
2689
|
+
let apply2;
|
|
2690
|
+
if (opts.apply !== void 0) {
|
|
2691
|
+
apply2 = opts.apply;
|
|
2692
|
+
} else if (nonInteractive) {
|
|
2693
|
+
apply2 = false;
|
|
2694
|
+
} else {
|
|
2695
|
+
apply2 = await confirm2({
|
|
2696
|
+
message: "Apply now? This creates a new git branch and copies staged files into your project.",
|
|
2697
|
+
default: true
|
|
2698
|
+
});
|
|
2699
|
+
}
|
|
2457
2700
|
if (apply2) {
|
|
2458
2701
|
await applyFromStaging({ targetRoot: target });
|
|
2459
2702
|
} else {
|
|
@@ -2461,6 +2704,11 @@ async function init(opts) {
|
|
|
2461
2704
|
console.log(c.dim("Next:"));
|
|
2462
2705
|
console.log(c.dim(" \u2022 Review .design-upgrade/ in your editor."));
|
|
2463
2706
|
console.log(c.dim(" \u2022 Run `stylecrib apply` when ready to commit the changes to a new branch."));
|
|
2707
|
+
if (nonInteractive && opts.apply === void 0) {
|
|
2708
|
+
console.log(
|
|
2709
|
+
c.dim(" (staged rather than applied because there was no terminal to confirm with \u2014\n") + c.dim(" pass --apply to skip this step next time.)")
|
|
2710
|
+
);
|
|
2711
|
+
}
|
|
2464
2712
|
}
|
|
2465
2713
|
if (opts.runtime !== false && !opts.dryRun) {
|
|
2466
2714
|
console.log("");
|
|
@@ -2478,6 +2726,16 @@ async function init(opts) {
|
|
|
2478
2726
|
" \u26A0 Runtime scaffold skipped \u2014 src/lib/stylecrib-wire.tsx already exists. Re-run with --force-runtime to overwrite (backup taken), or --no-runtime to suppress this step."
|
|
2479
2727
|
)
|
|
2480
2728
|
);
|
|
2729
|
+
} else if (result2.kind === "skipped-no-react-router") {
|
|
2730
|
+
console.log(
|
|
2731
|
+
c.warn(
|
|
2732
|
+
" \u26A0 Runtime pack skipped \u2014 react-router-dom is not a dependency.\n The <StyleCribAdmin> drawer uses useLocation for its route-scoped section\n registry, so it can't mount without a router. Install it and re-run:\n npm install react-router-dom && npx stylecrib init --no-hooks"
|
|
2733
|
+
)
|
|
2734
|
+
);
|
|
2735
|
+
} else if (result2.kind === "skipped-no-package-json") {
|
|
2736
|
+
console.log(
|
|
2737
|
+
c.warn(" \u26A0 Runtime pack skipped \u2014 no package.json at the target.")
|
|
2738
|
+
);
|
|
2481
2739
|
}
|
|
2482
2740
|
} catch (err) {
|
|
2483
2741
|
console.log(
|
|
@@ -2496,7 +2754,10 @@ async function init(opts) {
|
|
|
2496
2754
|
)
|
|
2497
2755
|
);
|
|
2498
2756
|
try {
|
|
2499
|
-
await installHooks({
|
|
2757
|
+
await installHooks({
|
|
2758
|
+
target,
|
|
2759
|
+
theme: wizard.baseDirection === "custom" ? void 0 : wizard.baseDirection
|
|
2760
|
+
});
|
|
2500
2761
|
} catch (err) {
|
|
2501
2762
|
console.log(
|
|
2502
2763
|
c.warn(
|
|
@@ -2569,23 +2830,24 @@ async function runSelfTest(target) {
|
|
|
2569
2830
|
}
|
|
2570
2831
|
}
|
|
2571
2832
|
async function loadCanonicalDirectionMarkdown(slug) {
|
|
2572
|
-
const
|
|
2573
|
-
|
|
2574
|
-
resolve6(__dirname4, "..", "..", "themes", slug, "THEME.md")
|
|
2575
|
-
];
|
|
2576
|
-
for (const p of candidates) {
|
|
2833
|
+
const themesDir = resolveAssetDir("themes");
|
|
2834
|
+
if (themesDir) {
|
|
2577
2835
|
try {
|
|
2578
|
-
return await readFile6(
|
|
2836
|
+
return await readFile6(join8(themesDir, slug, "THEME.md"), "utf8");
|
|
2579
2837
|
} catch {
|
|
2580
|
-
|
|
2838
|
+
console.log(
|
|
2839
|
+
c.warn(` ! No THEME.md for ${slug} at ${join8(themesDir, slug)} \u2014 falling back to a stub.`)
|
|
2840
|
+
);
|
|
2841
|
+
return `# Design Direction \u2014 ${slug}
|
|
2842
|
+
|
|
2843
|
+
(Canonical THEME.md not bundled \u2014 re-install stylecrib.)
|
|
2844
|
+
`;
|
|
2581
2845
|
}
|
|
2582
2846
|
}
|
|
2583
|
-
console.log(
|
|
2584
|
-
c.warn(` ! Couldn't locate canonical THEME.md for ${slug} \u2014 falling back to a stub.`)
|
|
2585
|
-
);
|
|
2847
|
+
console.log(c.warn(` ! ${missingAssetMessage("themes")}`));
|
|
2586
2848
|
return `# Design Direction \u2014 ${slug}
|
|
2587
2849
|
|
|
2588
|
-
(Canonical THEME.md not bundled \u2014 re-install
|
|
2850
|
+
(Canonical THEME.md not bundled \u2014 re-install stylecrib.)
|
|
2589
2851
|
`;
|
|
2590
2852
|
}
|
|
2591
2853
|
function extractEthos(md) {
|
|
@@ -2597,13 +2859,61 @@ function extractEthos(md) {
|
|
|
2597
2859
|
init_esm_shims();
|
|
2598
2860
|
var import_picocolors4 = __toESM(require_picocolors(), 1);
|
|
2599
2861
|
import { execSync as execSync3, spawnSync } from "child_process";
|
|
2600
|
-
import { readFileSync as
|
|
2601
|
-
import { resolve as
|
|
2862
|
+
import { readFileSync as readFileSync4, existsSync as existsSync5, mkdirSync as mkdirSync3, appendFileSync, writeFileSync as writeFileSync3 } from "fs";
|
|
2863
|
+
import { resolve as resolve6, join as join10 } from "path";
|
|
2602
2864
|
import { homedir } from "os";
|
|
2603
2865
|
import Anthropic2 from "@anthropic-ai/sdk";
|
|
2604
2866
|
|
|
2605
2867
|
// src/lib/slop-detectors.ts
|
|
2606
2868
|
init_esm_shims();
|
|
2869
|
+
|
|
2870
|
+
// src/lib/color.ts
|
|
2871
|
+
init_esm_shims();
|
|
2872
|
+
function hexToHsl(hex) {
|
|
2873
|
+
let m = hex.match(/^#([0-9a-f]{3})$/i);
|
|
2874
|
+
let r, g, b;
|
|
2875
|
+
if (m) {
|
|
2876
|
+
const [a, c2, d] = m[1].split("");
|
|
2877
|
+
r = parseInt(a + a, 16);
|
|
2878
|
+
g = parseInt(c2 + c2, 16);
|
|
2879
|
+
b = parseInt(d + d, 16);
|
|
2880
|
+
} else {
|
|
2881
|
+
m = hex.match(/^#([0-9a-f]{2})([0-9a-f]{2})([0-9a-f]{2})$/i);
|
|
2882
|
+
if (!m) return null;
|
|
2883
|
+
r = parseInt(m[1], 16);
|
|
2884
|
+
g = parseInt(m[2], 16);
|
|
2885
|
+
b = parseInt(m[3], 16);
|
|
2886
|
+
}
|
|
2887
|
+
return rgbToHsl(r, g, b);
|
|
2888
|
+
}
|
|
2889
|
+
function rgbToHsl(r8, g8, b8) {
|
|
2890
|
+
const r = r8 / 255, g = g8 / 255, b = b8 / 255;
|
|
2891
|
+
const max = Math.max(r, g, b), min = Math.min(r, g, b);
|
|
2892
|
+
const l = (max + min) / 2;
|
|
2893
|
+
const d = max - min;
|
|
2894
|
+
if (d === 0) return { h: 0, s: 0, l: l * 100 };
|
|
2895
|
+
const s = d / (1 - Math.abs(2 * l - 1));
|
|
2896
|
+
let h;
|
|
2897
|
+
if (max === r) h = 60 * ((g - b) / d % 6);
|
|
2898
|
+
else if (max === g) h = 60 * ((b - r) / d + 2);
|
|
2899
|
+
else h = 60 * ((r - g) / d + 4);
|
|
2900
|
+
if (h < 0) h += 360;
|
|
2901
|
+
return { h, s: s * 100, l: l * 100 };
|
|
2902
|
+
}
|
|
2903
|
+
function parseHslTriplet(raw) {
|
|
2904
|
+
const m = raw.trim().match(/^(\d+(?:\.\d+)?)\s+(\d+(?:\.\d+)?)%\s+(\d+(?:\.\d+)?)%$/);
|
|
2905
|
+
if (!m) return null;
|
|
2906
|
+
return { h: parseFloat(m[1]), s: parseFloat(m[2]), l: parseFloat(m[3]) };
|
|
2907
|
+
}
|
|
2908
|
+
function hueDistance(a, b) {
|
|
2909
|
+
const d = Math.abs((a % 360 + 360) % 360 - (b % 360 + 360) % 360);
|
|
2910
|
+
return d > 180 ? 360 - d : d;
|
|
2911
|
+
}
|
|
2912
|
+
function isAccentLike(c2) {
|
|
2913
|
+
return c2.s >= 45 && c2.l >= 20 && c2.l <= 80;
|
|
2914
|
+
}
|
|
2915
|
+
|
|
2916
|
+
// src/lib/slop-detectors.ts
|
|
2607
2917
|
function extractOverrides(body) {
|
|
2608
2918
|
const local = /* @__PURE__ */ new Map();
|
|
2609
2919
|
const fileScope = /* @__PURE__ */ new Set();
|
|
@@ -2664,7 +2974,7 @@ function detectMonoDecorativeEyebrow(file, body) {
|
|
|
2664
2974
|
if (/<kbd\b|<code\b|<pre\b/i.test(window)) continue;
|
|
2665
2975
|
if (/⌘|⌃|⌥|⇧|⏎|⇥|←|→|↑|↓/.test(window)) continue;
|
|
2666
2976
|
if (/\$\s|\bLEAD-\d|\bAC-\d|\bID-\d|\bDEA-\d|\bCDN-\d|\bPT-\d/.test(window)) continue;
|
|
2667
|
-
if (/[a-z]+\/[a-z\-]+\/?[a-z\-]*/.test(window) && /
|
|
2977
|
+
if (/[a-z]+\/[a-z\-]+\/?[a-z\-]*/.test(window) && /stylecrib\/|routes?\.|route:/i.test(window)) continue;
|
|
2668
2978
|
if (/terminal|CLI|specimen|\$\s*[a-z\-]+ /i.test(window)) continue;
|
|
2669
2979
|
if (/tabular-nums/i.test(line)) continue;
|
|
2670
2980
|
hits.push({ ruleId: "R-MONO-DECORATIVE-EYEBROW", file, line: i + 1, sample: line.trim().slice(0, 120) });
|
|
@@ -2722,11 +3032,13 @@ function detectMonoCapsCTA(file, body) {
|
|
|
2722
3032
|
function detectDotMetadataRow(file, body) {
|
|
2723
3033
|
const hits = [];
|
|
2724
3034
|
const lines = body.split("\n");
|
|
2725
|
-
const dotToken = String.raw`[A-
|
|
3035
|
+
const dotToken = String.raw`[A-Z0-9][A-Z0-9./\-]{0,20}(?:\s[A-Z0-9][A-Z0-9./\-]{0,20}){0,2}`;
|
|
2726
3036
|
const dotTokens = new RegExp(`(?:${dotToken}\\s\xB7\\s){2,}${dotToken}`);
|
|
2727
3037
|
const smallMarker = /fontSize\s*:\s*(?:['"]?)(?:[1-9]|1[0-4])(?:px)?['"]?|text-xs|text-\[0\.\d+rem\]|font-mono/;
|
|
2728
3038
|
for (let i = 0; i < lines.length; i++) {
|
|
2729
|
-
|
|
3039
|
+
const dotHit = lines[i].match(dotTokens);
|
|
3040
|
+
if (!dotHit) continue;
|
|
3041
|
+
if (!/[A-Z]/.test(dotHit[0])) continue;
|
|
2730
3042
|
const window = lines.slice(Math.max(0, i - 2), Math.min(lines.length, i + 4)).join("\n");
|
|
2731
3043
|
if (!smallMarker.test(window)) continue;
|
|
2732
3044
|
if (/<p\b[^>]*(?:lede|prose|body)/i.test(window)) continue;
|
|
@@ -3248,14 +3560,113 @@ function detectTechBroGradient(file, body) {
|
|
|
3248
3560
|
}
|
|
3249
3561
|
return hits.filter((h) => !isOverridden(lines, h.line - 1, h.ruleId));
|
|
3250
3562
|
}
|
|
3251
|
-
function
|
|
3252
|
-
const
|
|
3253
|
-
const
|
|
3254
|
-
const
|
|
3255
|
-
|
|
3256
|
-
|
|
3257
|
-
|
|
3258
|
-
|
|
3563
|
+
function detectMonoCapsSectionEyebrow(file, body) {
|
|
3564
|
+
const hits = [];
|
|
3565
|
+
const lines = body.split("\n");
|
|
3566
|
+
const eyebrowRe = /["'`>]\s*(SECTION|CHAPTER|PART|WHAT WE)\s*[·•]?\s*[A-Z][A-Z']/;
|
|
3567
|
+
for (let i = 0; i < lines.length; i++) {
|
|
3568
|
+
if (isCommentOrRuleDoc(lines[i], "R-MONO-CAPS-SECTION-EYEBROW")) continue;
|
|
3569
|
+
if (!eyebrowRe.test(lines[i])) continue;
|
|
3570
|
+
hits.push({
|
|
3571
|
+
ruleId: "R-MONO-CAPS-SECTION-EYEBROW",
|
|
3572
|
+
file,
|
|
3573
|
+
line: i + 1,
|
|
3574
|
+
sample: lines[i].trim().slice(0, 120)
|
|
3575
|
+
});
|
|
3576
|
+
}
|
|
3577
|
+
return hits.filter((h) => !isOverridden(lines, h.line - 1, h.ruleId));
|
|
3578
|
+
}
|
|
3579
|
+
function detectCornerTagAbbreviation(file, body) {
|
|
3580
|
+
const hits = [];
|
|
3581
|
+
const lines = body.split("\n");
|
|
3582
|
+
const cornerRe = /\b(NO|EST|VOL|CH|PT|SEC|EP|PG)\.\s*\d/;
|
|
3583
|
+
for (let i = 0; i < lines.length; i++) {
|
|
3584
|
+
if (isCommentOrRuleDoc(lines[i], "R-CORNER-TAG-ABBREVIATION")) continue;
|
|
3585
|
+
const m = lines[i].match(cornerRe);
|
|
3586
|
+
if (!m) continue;
|
|
3587
|
+
hits.push({
|
|
3588
|
+
ruleId: "R-CORNER-TAG-ABBREVIATION",
|
|
3589
|
+
file,
|
|
3590
|
+
line: i + 1,
|
|
3591
|
+
sample: lines[i].trim().slice(0, 120)
|
|
3592
|
+
});
|
|
3593
|
+
}
|
|
3594
|
+
return hits.filter((h) => !isOverridden(lines, h.line - 1, h.ruleId));
|
|
3595
|
+
}
|
|
3596
|
+
function detectBylineMonoCaps(file, body) {
|
|
3597
|
+
const hits = [];
|
|
3598
|
+
const lines = body.split("\n");
|
|
3599
|
+
const bylineRe = /["'`>]\s*(BY|WORDS BY|WRITTEN BY|PHOTOGRAPHY BY|PHOTOS BY|EDITED BY)\s+[A-Z]/;
|
|
3600
|
+
for (let i = 0; i < lines.length; i++) {
|
|
3601
|
+
if (isCommentOrRuleDoc(lines[i], "R-BYLINE-MONO-CAPS")) continue;
|
|
3602
|
+
if (!bylineRe.test(lines[i])) continue;
|
|
3603
|
+
hits.push({
|
|
3604
|
+
ruleId: "R-BYLINE-MONO-CAPS",
|
|
3605
|
+
file,
|
|
3606
|
+
line: i + 1,
|
|
3607
|
+
sample: lines[i].trim().slice(0, 120)
|
|
3608
|
+
});
|
|
3609
|
+
}
|
|
3610
|
+
return hits.filter((h) => !isOverridden(lines, h.line - 1, h.ruleId));
|
|
3611
|
+
}
|
|
3612
|
+
function detectTagRowAboveBody(file, body) {
|
|
3613
|
+
const hits = [];
|
|
3614
|
+
const lines = body.split("\n");
|
|
3615
|
+
const tagRe = /[▸◆»●]\s+[A-Z][A-Z]{2,}/;
|
|
3616
|
+
for (let i = 0; i < lines.length; i++) {
|
|
3617
|
+
if (isCommentOrRuleDoc(lines[i], "R-TAG-ROW-ABOVE-BODY")) continue;
|
|
3618
|
+
const m = lines[i].match(tagRe);
|
|
3619
|
+
if (!m) continue;
|
|
3620
|
+
hits.push({
|
|
3621
|
+
ruleId: "R-TAG-ROW-ABOVE-BODY",
|
|
3622
|
+
file,
|
|
3623
|
+
line: i + 1,
|
|
3624
|
+
sample: lines[i].trim().slice(0, 120)
|
|
3625
|
+
});
|
|
3626
|
+
}
|
|
3627
|
+
return hits.filter((h) => !isOverridden(lines, h.line - 1, h.ruleId));
|
|
3628
|
+
}
|
|
3629
|
+
function detectSecondAccent(file, body, theme) {
|
|
3630
|
+
if (!theme || !theme.singleAccentOnly || !theme.accent) return [];
|
|
3631
|
+
const hits = [];
|
|
3632
|
+
const lines = body.split("\n");
|
|
3633
|
+
const HUE_TOLERANCE = 40;
|
|
3634
|
+
for (let i = 0; i < lines.length; i++) {
|
|
3635
|
+
const line = lines[i];
|
|
3636
|
+
if (isCommentOrRuleDoc(line, "R-SECOND-ACCENT")) continue;
|
|
3637
|
+
const candidates = [];
|
|
3638
|
+
for (const m of line.matchAll(/#[0-9a-fA-F]{6}\b|#[0-9a-fA-F]{3}\b/g)) {
|
|
3639
|
+
candidates.push({ raw: m[0], hsl: hexToHsl(m[0]) });
|
|
3640
|
+
}
|
|
3641
|
+
for (const m of line.matchAll(/hsl\(\s*(\d+(?:\.\d+)?)\s*,?\s*(\d+(?:\.\d+)?)%\s*,?\s*(\d+(?:\.\d+)?)%/g)) {
|
|
3642
|
+
candidates.push({
|
|
3643
|
+
raw: m[0],
|
|
3644
|
+
hsl: { h: parseFloat(m[1]), s: parseFloat(m[2]), l: parseFloat(m[3]) }
|
|
3645
|
+
});
|
|
3646
|
+
}
|
|
3647
|
+
for (const c2 of candidates) {
|
|
3648
|
+
if (!c2.hsl || !isAccentLike(c2.hsl)) continue;
|
|
3649
|
+
const dist = hueDistance(c2.hsl.h, theme.accent.h);
|
|
3650
|
+
if (dist <= HUE_TOLERANCE) continue;
|
|
3651
|
+
hits.push({
|
|
3652
|
+
ruleId: "R-SECOND-ACCENT",
|
|
3653
|
+
file,
|
|
3654
|
+
line: i + 1,
|
|
3655
|
+
sample: `${c2.raw} (hue ${c2.hsl.h.toFixed(0)}\xB0 vs ${theme.slug} accent ${theme.accent.h.toFixed(0)}\xB0) ${line.trim().slice(0, 70)}`
|
|
3656
|
+
});
|
|
3657
|
+
break;
|
|
3658
|
+
}
|
|
3659
|
+
}
|
|
3660
|
+
return hits.filter((h) => !isOverridden(lines, h.line - 1, h.ruleId));
|
|
3661
|
+
}
|
|
3662
|
+
function runAllTextDetectors(file, body, theme = null) {
|
|
3663
|
+
const overrides = extractOverrides(body);
|
|
3664
|
+
const cleaned = stripJsxExpressionBraces(file, stripBlockComments(body));
|
|
3665
|
+
const rawHits = [
|
|
3666
|
+
...detectMonoDecorativeEyebrow(file, cleaned),
|
|
3667
|
+
...detectMonoInCssApply(file, cleaned),
|
|
3668
|
+
...detectArrowEyebrowPrefix(file, cleaned),
|
|
3669
|
+
...detectMonoCapsCTA(file, cleaned),
|
|
3259
3670
|
...detectDotMetadataRow(file, cleaned),
|
|
3260
3671
|
...detectStaticGlowBloom(file, cleaned),
|
|
3261
3672
|
...detectTrustedByRow(file, cleaned),
|
|
@@ -3272,11 +3683,97 @@ function runAllTextDetectors(file, body) {
|
|
|
3272
3683
|
...detectMotionNoReducedMotion(file, cleaned),
|
|
3273
3684
|
...detectClaudeCream(file, cleaned),
|
|
3274
3685
|
...detectVoidAcid(file, cleaned),
|
|
3275
|
-
...detectTechBroGradient(file, cleaned)
|
|
3686
|
+
...detectTechBroGradient(file, cleaned),
|
|
3687
|
+
...detectMonoCapsSectionEyebrow(file, cleaned),
|
|
3688
|
+
...detectCornerTagAbbreviation(file, cleaned),
|
|
3689
|
+
...detectBylineMonoCaps(file, cleaned),
|
|
3690
|
+
...detectTagRowAboveBody(file, cleaned),
|
|
3691
|
+
...detectSecondAccent(file, cleaned, theme)
|
|
3276
3692
|
];
|
|
3277
3693
|
return rawHits.filter((h) => !isOverriddenByMap(h.line - 1, h.ruleId, overrides));
|
|
3278
3694
|
}
|
|
3279
3695
|
|
|
3696
|
+
// src/lib/theme-policy.ts
|
|
3697
|
+
init_esm_shims();
|
|
3698
|
+
import { existsSync as existsSync4, readFileSync as readFileSync3 } from "fs";
|
|
3699
|
+
import { join as join9 } from "path";
|
|
3700
|
+
var SINGLE_ACCENT_THEMES = [
|
|
3701
|
+
"signal-dark",
|
|
3702
|
+
"enterprise-tech",
|
|
3703
|
+
"pharma-clinical",
|
|
3704
|
+
"kinetic-data"
|
|
3705
|
+
];
|
|
3706
|
+
function resolveActiveThemeSlug(target) {
|
|
3707
|
+
const cfgPath = join9(target, ".stylecrib", "config.json");
|
|
3708
|
+
if (existsSync4(cfgPath)) {
|
|
3709
|
+
try {
|
|
3710
|
+
const cfg = JSON.parse(readFileSync3(cfgPath, "utf8"));
|
|
3711
|
+
if (typeof cfg.theme === "string" && cfg.theme.trim()) return cfg.theme.trim();
|
|
3712
|
+
} catch {
|
|
3713
|
+
}
|
|
3714
|
+
}
|
|
3715
|
+
const wirePath = join9(target, "src", "lib", "stylecrib-wire.tsx");
|
|
3716
|
+
if (existsSync4(wirePath)) {
|
|
3717
|
+
try {
|
|
3718
|
+
const m = readFileSync3(wirePath, "utf8").match(/defaultTheme=["']([a-z0-9-]+)["']/);
|
|
3719
|
+
if (m) return m[1];
|
|
3720
|
+
} catch {
|
|
3721
|
+
}
|
|
3722
|
+
}
|
|
3723
|
+
return null;
|
|
3724
|
+
}
|
|
3725
|
+
function themePermitsSingleAccentOnly(slug, themeYmlSrc) {
|
|
3726
|
+
if (SINGLE_ACCENT_THEMES.includes(slug)) return true;
|
|
3727
|
+
if (themeYmlSrc && /^\s*single_accent:\s*true\s*$/m.test(themeYmlSrc)) return true;
|
|
3728
|
+
return false;
|
|
3729
|
+
}
|
|
3730
|
+
function resolveThemeContext(slug) {
|
|
3731
|
+
const themesDir = resolveAssetDir("themes");
|
|
3732
|
+
if (!themesDir) return null;
|
|
3733
|
+
const dir = join9(themesDir, slug);
|
|
3734
|
+
if (!existsSync4(dir)) return null;
|
|
3735
|
+
let accent = null;
|
|
3736
|
+
let accentSource = "unresolved";
|
|
3737
|
+
let ymlSrc;
|
|
3738
|
+
const ymlPath = join9(dir, "theme.yml");
|
|
3739
|
+
if (existsSync4(ymlPath)) {
|
|
3740
|
+
try {
|
|
3741
|
+
ymlSrc = readFileSync3(ymlPath, "utf8");
|
|
3742
|
+
const m = ymlSrc.match(/^\s*accent:\s*["']?(#[0-9a-fA-F]{3,6})["']?/m);
|
|
3743
|
+
if (m) {
|
|
3744
|
+
accent = hexToHsl(m[1]);
|
|
3745
|
+
accentSource = `theme.yml palette.accent ${m[1]}`;
|
|
3746
|
+
}
|
|
3747
|
+
} catch {
|
|
3748
|
+
}
|
|
3749
|
+
}
|
|
3750
|
+
if (!accent) {
|
|
3751
|
+
const tokPath = join9(dir, "tokens.ts");
|
|
3752
|
+
if (existsSync4(tokPath)) {
|
|
3753
|
+
try {
|
|
3754
|
+
const src = readFileSync3(tokPath, "utf8");
|
|
3755
|
+
const m = src.match(/\bprimary:\s*\{\s*hsl:\s*["']([^"']+)["']/);
|
|
3756
|
+
if (m) {
|
|
3757
|
+
accent = parseHslTriplet(m[1]);
|
|
3758
|
+
accentSource = `tokens.ts colors.primary "${m[1]}"`;
|
|
3759
|
+
}
|
|
3760
|
+
} catch {
|
|
3761
|
+
}
|
|
3762
|
+
}
|
|
3763
|
+
}
|
|
3764
|
+
return {
|
|
3765
|
+
slug,
|
|
3766
|
+
accent,
|
|
3767
|
+
accentSource,
|
|
3768
|
+
singleAccentOnly: themePermitsSingleAccentOnly(slug, ymlSrc)
|
|
3769
|
+
};
|
|
3770
|
+
}
|
|
3771
|
+
function themeContextForTarget(target) {
|
|
3772
|
+
const slug = resolveActiveThemeSlug(target);
|
|
3773
|
+
if (!slug) return null;
|
|
3774
|
+
return resolveThemeContext(slug);
|
|
3775
|
+
}
|
|
3776
|
+
|
|
3280
3777
|
// src/lib/regression-detectors.ts
|
|
3281
3778
|
init_esm_shims();
|
|
3282
3779
|
function firstLineWith(body, needle) {
|
|
@@ -3354,7 +3851,7 @@ function existsInHead(target, rel) {
|
|
|
3354
3851
|
return r.status === 0;
|
|
3355
3852
|
}
|
|
3356
3853
|
async function check(opts = {}) {
|
|
3357
|
-
const target =
|
|
3854
|
+
const target = resolve6(opts.target ?? process.cwd());
|
|
3358
3855
|
const apiKey = process.env.ANTHROPIC_API_KEY;
|
|
3359
3856
|
const changed = detectChangedFiles(target, opts);
|
|
3360
3857
|
const changedApp = detectChangedAppFiles(target, opts);
|
|
@@ -3367,7 +3864,7 @@ async function check(opts = {}) {
|
|
|
3367
3864
|
const precedentMisses = [];
|
|
3368
3865
|
for (const rel of changed) {
|
|
3369
3866
|
if (!isComponentFile(rel)) continue;
|
|
3370
|
-
const body = readSafe(
|
|
3867
|
+
const body = readSafe(join10(target, rel));
|
|
3371
3868
|
if (!body) continue;
|
|
3372
3869
|
const hasDeclaration = /\/\/\s*Composition study:/i.test(body);
|
|
3373
3870
|
if (!hasDeclaration) {
|
|
@@ -3413,7 +3910,7 @@ async function check(opts = {}) {
|
|
|
3413
3910
|
const fullScan = process.env.STYLECRIB_LINT_FULL === "1";
|
|
3414
3911
|
for (const rel of changed) {
|
|
3415
3912
|
if (!isComponentFile(rel)) continue;
|
|
3416
|
-
const body = readSafe(
|
|
3913
|
+
const body = readSafe(join10(target, rel));
|
|
3417
3914
|
if (!body) continue;
|
|
3418
3915
|
const newHits = runMonoTrackedCapsCheck(rel, body);
|
|
3419
3916
|
if (fullScan) {
|
|
@@ -3444,22 +3941,23 @@ async function check(opts = {}) {
|
|
|
3444
3941
|
);
|
|
3445
3942
|
process.exit(1);
|
|
3446
3943
|
}
|
|
3944
|
+
const theme = themeContextForTarget(target);
|
|
3447
3945
|
const allHits = [];
|
|
3448
3946
|
for (const rel of changed) {
|
|
3449
3947
|
if (!looksLikeUiFile(rel)) continue;
|
|
3450
|
-
const body = readSafe(
|
|
3948
|
+
const body = readSafe(join10(target, rel));
|
|
3451
3949
|
if (!body) continue;
|
|
3452
|
-
const newHits = runAllTextDetectors(rel, body);
|
|
3950
|
+
const newHits = runAllTextDetectors(rel, body, theme);
|
|
3453
3951
|
if (fullScan) {
|
|
3454
3952
|
allHits.push(...newHits);
|
|
3455
3953
|
continue;
|
|
3456
3954
|
}
|
|
3457
3955
|
const oldBody = readHeadBody(target, rel);
|
|
3458
|
-
const oldHits = oldBody ? runAllTextDetectors(rel, oldBody) : [];
|
|
3956
|
+
const oldHits = oldBody ? runAllTextDetectors(rel, oldBody, theme) : [];
|
|
3459
3957
|
allHits.push(...subtractHitsBySample(newHits, oldHits));
|
|
3460
3958
|
}
|
|
3461
3959
|
for (const rel of changedApp) {
|
|
3462
|
-
const body = readSafe(
|
|
3960
|
+
const body = readSafe(join10(target, rel));
|
|
3463
3961
|
if (!body) continue;
|
|
3464
3962
|
const oldBody = readHeadBody(target, rel);
|
|
3465
3963
|
if (oldBody === null) continue;
|
|
@@ -3538,7 +4036,7 @@ async function check(opts = {}) {
|
|
|
3538
4036
|
console.error(import_picocolors4.default.dim(' STYLECRIB_SKIP_QA="<one-line reason>" git commit'));
|
|
3539
4037
|
process.exit(1);
|
|
3540
4038
|
}
|
|
3541
|
-
const conductor = readSafe(
|
|
4039
|
+
const conductor = readSafe(join10(target, ".claude/agents/qa-conductor.md"));
|
|
3542
4040
|
if (!conductor) {
|
|
3543
4041
|
console.error(import_picocolors4.default.red("\u2717 ") + "QA Conductor agent not installed.");
|
|
3544
4042
|
console.error(
|
|
@@ -3548,8 +4046,8 @@ async function check(opts = {}) {
|
|
|
3548
4046
|
);
|
|
3549
4047
|
process.exit(1);
|
|
3550
4048
|
}
|
|
3551
|
-
const direction = readSafe(
|
|
3552
|
-
const feedback = readSafe(
|
|
4049
|
+
const direction = readSafe(join10(target, "design-system/DIRECTION.md")) || readSafe(join10(target, "design-system/THEME.md")) || "(No design-system/DIRECTION.md or THEME.md found \u2014 review will rely on the QA Conductor's general principles.)";
|
|
4050
|
+
const feedback = readSafe(join10(target, ".stylecrib/feedback.md"));
|
|
3553
4051
|
if (!opts.silent) {
|
|
3554
4052
|
const route = apiKey ? "Anthropic API" : "Claude Code CLI";
|
|
3555
4053
|
console.log(
|
|
@@ -3560,7 +4058,7 @@ async function check(opts = {}) {
|
|
|
3560
4058
|
for (const f of changed) console.log(import_picocolors4.default.dim(" \xB7 " + f));
|
|
3561
4059
|
}
|
|
3562
4060
|
const fileContents = changed.map((rel) => {
|
|
3563
|
-
const path2 =
|
|
4061
|
+
const path2 = join10(target, rel);
|
|
3564
4062
|
const body = readSafe(path2);
|
|
3565
4063
|
return body ? `## ${rel}
|
|
3566
4064
|
|
|
@@ -3690,7 +4188,7 @@ function looksLikeAppFile(path2) {
|
|
|
3690
4188
|
}
|
|
3691
4189
|
function logQaBypass(target, files, kind, reason) {
|
|
3692
4190
|
try {
|
|
3693
|
-
const dir =
|
|
4191
|
+
const dir = join10(target, "reports");
|
|
3694
4192
|
mkdirSync3(dir, { recursive: true });
|
|
3695
4193
|
ensureReportsReadme(dir);
|
|
3696
4194
|
const record = {
|
|
@@ -3706,13 +4204,13 @@ function logQaBypass(target, files, kind, reason) {
|
|
|
3706
4204
|
}
|
|
3707
4205
|
})()
|
|
3708
4206
|
};
|
|
3709
|
-
appendFileSync(
|
|
4207
|
+
appendFileSync(join10(dir, "qa-bypasses.jsonl"), JSON.stringify(record) + "\n");
|
|
3710
4208
|
} catch {
|
|
3711
4209
|
}
|
|
3712
4210
|
}
|
|
3713
4211
|
function ensureReportsReadme(reportsDir) {
|
|
3714
|
-
const path2 =
|
|
3715
|
-
if (
|
|
4212
|
+
const path2 = join10(reportsDir, "README.md");
|
|
4213
|
+
if (existsSync5(path2)) return;
|
|
3716
4214
|
const body = `# reports/
|
|
3717
4215
|
|
|
3718
4216
|
This directory is written by the StyleCrib harness. Files here are not
|
|
@@ -3817,8 +4315,8 @@ function isComponentFile(path2) {
|
|
|
3817
4315
|
}
|
|
3818
4316
|
function readSafe(path2) {
|
|
3819
4317
|
try {
|
|
3820
|
-
if (!
|
|
3821
|
-
return
|
|
4318
|
+
if (!existsSync5(path2)) return null;
|
|
4319
|
+
return readFileSync4(path2, "utf8");
|
|
3822
4320
|
} catch {
|
|
3823
4321
|
return null;
|
|
3824
4322
|
}
|
|
@@ -3839,7 +4337,7 @@ function findClaudeCli() {
|
|
|
3839
4337
|
"/opt/homebrew/bin/claude"
|
|
3840
4338
|
];
|
|
3841
4339
|
for (const p of candidates) {
|
|
3842
|
-
if (
|
|
4340
|
+
if (existsSync5(p)) return p;
|
|
3843
4341
|
}
|
|
3844
4342
|
return null;
|
|
3845
4343
|
}
|
|
@@ -3847,21 +4345,21 @@ function findClaudeCli() {
|
|
|
3847
4345
|
// src/commands/review-site.ts
|
|
3848
4346
|
init_esm_shims();
|
|
3849
4347
|
var import_picocolors5 = __toESM(require_picocolors(), 1);
|
|
3850
|
-
import { readFileSync as
|
|
3851
|
-
import { resolve as
|
|
4348
|
+
import { readFileSync as readFileSync5, readdirSync as readdirSync2, statSync, mkdirSync as mkdirSync4, writeFileSync as writeFileSync4 } from "fs";
|
|
4349
|
+
import { resolve as resolve7, join as join11, relative as relative4 } from "path";
|
|
3852
4350
|
init_slop_rules();
|
|
3853
4351
|
function walkUiFiles(siteRoot) {
|
|
3854
4352
|
const out = [];
|
|
3855
4353
|
const visit = (dir) => {
|
|
3856
4354
|
let entries;
|
|
3857
4355
|
try {
|
|
3858
|
-
entries =
|
|
4356
|
+
entries = readdirSync2(dir);
|
|
3859
4357
|
} catch {
|
|
3860
4358
|
return;
|
|
3861
4359
|
}
|
|
3862
4360
|
for (const name of entries) {
|
|
3863
4361
|
if (name.startsWith(".") || name === "node_modules" || name === "dist" || name === "build") continue;
|
|
3864
|
-
const p =
|
|
4362
|
+
const p = join11(dir, name);
|
|
3865
4363
|
let s;
|
|
3866
4364
|
try {
|
|
3867
4365
|
s = statSync(p);
|
|
@@ -3882,22 +4380,23 @@ function walkUiFiles(siteRoot) {
|
|
|
3882
4380
|
return out;
|
|
3883
4381
|
}
|
|
3884
4382
|
async function reviewSite(opts = {}) {
|
|
3885
|
-
const target =
|
|
3886
|
-
const siteRoot =
|
|
3887
|
-
const outDir =
|
|
4383
|
+
const target = resolve7(opts.target ?? process.cwd());
|
|
4384
|
+
const siteRoot = resolve7(opts.siteRoot ?? join11(target, "site/src"));
|
|
4385
|
+
const outDir = resolve7(opts.outDir ?? join11(target, "reports"));
|
|
3888
4386
|
console.log(import_picocolors5.default.dim(`Walking ${siteRoot} for UI files\u2026`));
|
|
3889
4387
|
const files = walkUiFiles(siteRoot);
|
|
3890
4388
|
console.log(import_picocolors5.default.dim(` ${files.length} files found. Running structural slop detectors\u2026`));
|
|
4389
|
+
const sweepTheme = themeContextForTarget(resolve7(target));
|
|
3891
4390
|
const allHits = [];
|
|
3892
4391
|
for (const abs of files) {
|
|
3893
4392
|
let body;
|
|
3894
4393
|
try {
|
|
3895
|
-
body =
|
|
4394
|
+
body = readFileSync5(abs, "utf8");
|
|
3896
4395
|
} catch {
|
|
3897
4396
|
continue;
|
|
3898
4397
|
}
|
|
3899
|
-
const rel = relative4(
|
|
3900
|
-
allHits.push(...runAllTextDetectors(rel, body));
|
|
4398
|
+
const rel = relative4(resolve7(target), abs);
|
|
4399
|
+
allHits.push(...runAllTextDetectors(rel, body, sweepTheme));
|
|
3901
4400
|
}
|
|
3902
4401
|
const byRule = /* @__PURE__ */ new Map();
|
|
3903
4402
|
const byFile = /* @__PURE__ */ new Map();
|
|
@@ -3919,8 +4418,14 @@ async function reviewSite(opts = {}) {
|
|
|
3919
4418
|
lines.push(`Block-severity hits: **${blocks}**`);
|
|
3920
4419
|
lines.push(`Warn-severity hits: **${warns}**`);
|
|
3921
4420
|
lines.push("");
|
|
3922
|
-
if (
|
|
3923
|
-
lines.push(
|
|
4421
|
+
if (files.length === 0) {
|
|
4422
|
+
lines.push(
|
|
4423
|
+
"**Scanned nothing \u2014 this sweep proves nothing.** No UI files were found under the scan root, so the result below is vacuous rather than clean. Check that the sweep is pointed at a directory that actually contains `src/components` / `src/pages`."
|
|
4424
|
+
);
|
|
4425
|
+
} else if (blocks === 0 && warns === 0) {
|
|
4426
|
+
lines.push(
|
|
4427
|
+
`**No slop detected.** All ${files.length} UI file(s) under the scan root pass the structural gate.`
|
|
4428
|
+
);
|
|
3924
4429
|
} else {
|
|
3925
4430
|
lines.push("## Hits by rule");
|
|
3926
4431
|
lines.push("");
|
|
@@ -3947,9 +4452,9 @@ async function reviewSite(opts = {}) {
|
|
|
3947
4452
|
}
|
|
3948
4453
|
}
|
|
3949
4454
|
mkdirSync4(outDir, { recursive: true });
|
|
3950
|
-
const outPath =
|
|
4455
|
+
const outPath = join11(outDir, `nightly-slop-${today}.md`);
|
|
3951
4456
|
writeFileSync4(outPath, lines.join("\n") + "\n");
|
|
3952
|
-
writeFileSync4(
|
|
4457
|
+
writeFileSync4(join11(outDir, "nightly-slop-latest.md"), lines.join("\n") + "\n");
|
|
3953
4458
|
console.log("");
|
|
3954
4459
|
console.log(import_picocolors5.default.dim(`Wrote ${outPath}`));
|
|
3955
4460
|
console.log(import_picocolors5.default.dim(` ${blocks} block-severity, ${warns} warn-severity`));
|
|
@@ -3958,8 +4463,8 @@ async function reviewSite(opts = {}) {
|
|
|
3958
4463
|
// src/commands/rule.ts
|
|
3959
4464
|
init_esm_shims();
|
|
3960
4465
|
var import_picocolors6 = __toESM(require_picocolors(), 1);
|
|
3961
|
-
import { readFileSync as
|
|
3962
|
-
import { resolve as
|
|
4466
|
+
import { readFileSync as readFileSync6, writeFileSync as writeFileSync5, existsSync as existsSync6, mkdirSync as mkdirSync5 } from "fs";
|
|
4467
|
+
import { resolve as resolve8, join as join12 } from "path";
|
|
3963
4468
|
import { input as input2, select as select2 } from "@inquirer/prompts";
|
|
3964
4469
|
var CATEGORIES = [
|
|
3965
4470
|
"chrome",
|
|
@@ -3973,7 +4478,8 @@ var CATEGORIES = [
|
|
|
3973
4478
|
"provenance"
|
|
3974
4479
|
];
|
|
3975
4480
|
async function ruleAdd(opts = {}) {
|
|
3976
|
-
const target =
|
|
4481
|
+
const target = resolve8(opts.target ?? process.cwd());
|
|
4482
|
+
assertInteractive("`stylecrib rule add`");
|
|
3977
4483
|
console.log(import_picocolors6.default.bold("Add a slop rule to this repo's custom rulebook."));
|
|
3978
4484
|
console.log(import_picocolors6.default.dim(" Custom rules live at .stylecrib/custom-rules.ts and load automatically."));
|
|
3979
4485
|
console.log("");
|
|
@@ -4022,10 +4528,10 @@ async function ruleAdd(opts = {}) {
|
|
|
4022
4528
|
message: "Sample fix \u2014 what to write instead:",
|
|
4023
4529
|
validate: (v) => v.length > 0 || "Write the corrected version so authors know the right pattern."
|
|
4024
4530
|
});
|
|
4025
|
-
const dir =
|
|
4531
|
+
const dir = join12(target, ".stylecrib");
|
|
4026
4532
|
mkdirSync5(dir, { recursive: true });
|
|
4027
|
-
const path2 =
|
|
4028
|
-
const existing =
|
|
4533
|
+
const path2 = join12(dir, "custom-rules.ts");
|
|
4534
|
+
const existing = existsSync6(path2) ? readFileSync6(path2, "utf8") : SEED_CUSTOM_RULES;
|
|
4029
4535
|
const insertion = `
|
|
4030
4536
|
// \u2500\u2500\u2500 ${id} \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
4031
4537
|
// ${description}
|
|
@@ -4059,13 +4565,13 @@ async function ruleAdd(opts = {}) {
|
|
|
4059
4565
|
console.log(import_picocolors6.default.dim(` To share with the community: stylecrib rule share ${id}`));
|
|
4060
4566
|
}
|
|
4061
4567
|
async function ruleShare(id, opts = {}) {
|
|
4062
|
-
const target =
|
|
4063
|
-
const path2 =
|
|
4064
|
-
if (!
|
|
4568
|
+
const target = resolve8(opts.target ?? process.cwd());
|
|
4569
|
+
const path2 = join12(target, ".stylecrib", "custom-rules.ts");
|
|
4570
|
+
if (!existsSync6(path2)) {
|
|
4065
4571
|
console.error(import_picocolors6.default.red("\u2717 ") + ".stylecrib/custom-rules.ts not found. Run `stylecrib rule add` first.");
|
|
4066
4572
|
process.exit(1);
|
|
4067
4573
|
}
|
|
4068
|
-
const body =
|
|
4574
|
+
const body = readFileSync6(path2, "utf8");
|
|
4069
4575
|
const start = body.indexOf(`id: "${id}"`);
|
|
4070
4576
|
if (start < 0) {
|
|
4071
4577
|
console.error(import_picocolors6.default.red("\u2717 ") + `Rule ${id} not found in .stylecrib/custom-rules.ts.`);
|
|
@@ -4163,11 +4669,11 @@ function extractFilePaths(event) {
|
|
|
4163
4669
|
// src/commands/audit.ts
|
|
4164
4670
|
init_esm_shims();
|
|
4165
4671
|
var import_picocolors7 = __toESM(require_picocolors(), 1);
|
|
4166
|
-
import { existsSync as
|
|
4167
|
-
import { resolve as
|
|
4672
|
+
import { existsSync as existsSync7, readdirSync as readdirSync3, readFileSync as readFileSync7, statSync as statSync2 } from "fs";
|
|
4673
|
+
import { resolve as resolve9, join as join13, basename as basename2 } from "path";
|
|
4168
4674
|
import Anthropic3 from "@anthropic-ai/sdk";
|
|
4169
4675
|
async function audit(opts = {}) {
|
|
4170
|
-
const target =
|
|
4676
|
+
const target = resolve9(opts.target ?? process.cwd());
|
|
4171
4677
|
const apiKey = process.env.ANTHROPIC_API_KEY;
|
|
4172
4678
|
if (!apiKey) {
|
|
4173
4679
|
console.error(import_picocolors7.default.red("\u2717 ") + "ANTHROPIC_API_KEY not set.");
|
|
@@ -4187,8 +4693,8 @@ async function audit(opts = {}) {
|
|
|
4187
4693
|
);
|
|
4188
4694
|
process.exit(1);
|
|
4189
4695
|
}
|
|
4190
|
-
const usingBundled = !
|
|
4191
|
-
|
|
4696
|
+
const usingBundled = !existsSync7(
|
|
4697
|
+
join13(target, ".claude/agents/homogeneity-detector.md")
|
|
4192
4698
|
);
|
|
4193
4699
|
if (usingBundled && !opts.silent) {
|
|
4194
4700
|
console.log(
|
|
@@ -4197,7 +4703,7 @@ async function audit(opts = {}) {
|
|
|
4197
4703
|
)
|
|
4198
4704
|
);
|
|
4199
4705
|
}
|
|
4200
|
-
const directionDoctrine = readSafe2(
|
|
4706
|
+
const directionDoctrine = readSafe2(join13(target, "design-system/THEME.md")) ?? "";
|
|
4201
4707
|
if (!opts.silent) {
|
|
4202
4708
|
console.log(import_picocolors7.default.bold("\u25C7 StyleCrib audit \u2014 whole-project drift report"));
|
|
4203
4709
|
console.log("");
|
|
@@ -4225,7 +4731,7 @@ async function audit(opts = {}) {
|
|
|
4225
4731
|
);
|
|
4226
4732
|
}
|
|
4227
4733
|
const filesBody = fam.files.map((rel) => {
|
|
4228
|
-
const body = readSafe2(
|
|
4734
|
+
const body = readSafe2(join13(target, rel));
|
|
4229
4735
|
return body ? `## ${rel}
|
|
4230
4736
|
|
|
4231
4737
|
\`\`\`tsx
|
|
@@ -4315,18 +4821,18 @@ function discoverFamilies(target, only) {
|
|
|
4315
4821
|
const out = [];
|
|
4316
4822
|
for (const c2 of candidates) {
|
|
4317
4823
|
if (only && c2.name !== only) continue;
|
|
4318
|
-
const abs =
|
|
4319
|
-
if (!
|
|
4824
|
+
const abs = join13(target, c2.dir);
|
|
4825
|
+
if (!existsSync7(abs)) continue;
|
|
4320
4826
|
if (!statSync2(abs).isDirectory()) continue;
|
|
4321
|
-
const files =
|
|
4827
|
+
const files = readdirSync3(abs).filter((f) => f.endsWith(".tsx")).map((f) => join13(c2.dir, f));
|
|
4322
4828
|
if (files.length > 0) out.push({ name: c2.name, files });
|
|
4323
4829
|
}
|
|
4324
4830
|
return out;
|
|
4325
4831
|
}
|
|
4326
4832
|
function readSafe2(path2) {
|
|
4327
4833
|
try {
|
|
4328
|
-
if (!
|
|
4329
|
-
return
|
|
4834
|
+
if (!existsSync7(path2)) return null;
|
|
4835
|
+
return readFileSync7(path2, "utf8");
|
|
4330
4836
|
} catch {
|
|
4331
4837
|
return null;
|
|
4332
4838
|
}
|
|
@@ -4335,11 +4841,11 @@ function readSafe2(path2) {
|
|
|
4335
4841
|
// src/commands/reject.ts
|
|
4336
4842
|
init_esm_shims();
|
|
4337
4843
|
var import_picocolors8 = __toESM(require_picocolors(), 1);
|
|
4338
|
-
import { existsSync as
|
|
4339
|
-
import { resolve as
|
|
4844
|
+
import { existsSync as existsSync8, mkdirSync as mkdirSync6, readFileSync as readFileSync8, writeFileSync as writeFileSync6 } from "fs";
|
|
4845
|
+
import { resolve as resolve10, join as join14, dirname as dirname6 } from "path";
|
|
4340
4846
|
async function reject(opts = {}) {
|
|
4341
|
-
const target =
|
|
4342
|
-
const path2 =
|
|
4847
|
+
const target = resolve10(opts.target ?? process.cwd());
|
|
4848
|
+
const path2 = join14(target, ".stylecrib", "feedback.md");
|
|
4343
4849
|
const pattern = opts.pattern?.trim();
|
|
4344
4850
|
const replacement = opts.replacement?.trim();
|
|
4345
4851
|
if (!pattern) {
|
|
@@ -4354,7 +4860,7 @@ async function reject(opts = {}) {
|
|
|
4354
4860
|
);
|
|
4355
4861
|
process.exit(1);
|
|
4356
4862
|
}
|
|
4357
|
-
const now = process.env.
|
|
4863
|
+
const now = process.env.STYLECRIB_NOW ?? (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
|
|
4358
4864
|
const entry = `
|
|
4359
4865
|
## Rejection \u2014 ${now}
|
|
4360
4866
|
|
|
@@ -4364,9 +4870,9 @@ async function reject(opts = {}) {
|
|
|
4364
4870
|
` : `
|
|
4365
4871
|
**Replacement:** _(not specified \u2014 propose alternatives that don't repeat this pattern)_
|
|
4366
4872
|
`);
|
|
4367
|
-
const existing =
|
|
4873
|
+
const existing = existsSync8(path2) ? readFileSync8(path2, "utf8") : HEADER_TEMPLATE;
|
|
4368
4874
|
const body = existing.trimEnd() + "\n" + entry;
|
|
4369
|
-
mkdirSync6(
|
|
4875
|
+
mkdirSync6(dirname6(path2), { recursive: true });
|
|
4370
4876
|
writeFileSync6(path2, body);
|
|
4371
4877
|
console.log(import_picocolors8.default.green("\u2713 ") + "Rejection recorded in " + import_picocolors8.default.cyan(".stylecrib/feedback.md"));
|
|
4372
4878
|
console.log("");
|
|
@@ -4397,19 +4903,19 @@ that gap. It's plain markdown \u2014 readable by humans, loaded by agents.
|
|
|
4397
4903
|
// src/commands/status.ts
|
|
4398
4904
|
init_esm_shims();
|
|
4399
4905
|
var import_picocolors9 = __toESM(require_picocolors(), 1);
|
|
4400
|
-
import { existsSync as
|
|
4401
|
-
import { resolve as
|
|
4906
|
+
import { existsSync as existsSync9, readFileSync as readFileSync9, statSync as statSync3 } from "fs";
|
|
4907
|
+
import { resolve as resolve11, join as join15 } from "path";
|
|
4402
4908
|
async function status(opts = {}) {
|
|
4403
|
-
const target =
|
|
4909
|
+
const target = resolve11(opts.target ?? process.cwd());
|
|
4404
4910
|
console.log(import_picocolors9.default.bold("\u25C7 StyleCrib enforcement status"));
|
|
4405
4911
|
console.log(import_picocolors9.default.dim(" " + target));
|
|
4406
4912
|
console.log("");
|
|
4407
4913
|
let anyFail = false;
|
|
4408
|
-
const huskyPath =
|
|
4409
|
-
const gitHookPath =
|
|
4410
|
-
const hookPath =
|
|
4411
|
-
const hookExists =
|
|
4412
|
-
const hookCallsCheck = hookExists ?
|
|
4914
|
+
const huskyPath = join15(target, ".husky", "pre-commit");
|
|
4915
|
+
const gitHookPath = join15(target, ".git", "hooks", "pre-commit");
|
|
4916
|
+
const hookPath = existsSync9(huskyPath) ? huskyPath : gitHookPath;
|
|
4917
|
+
const hookExists = existsSync9(hookPath);
|
|
4918
|
+
const hookCallsCheck = hookExists ? readFileSync9(hookPath, "utf8").includes("stylecrib check") : false;
|
|
4413
4919
|
const hookExec = hookExists ? (statSync3(hookPath).mode & 73) !== 0 : false;
|
|
4414
4920
|
const hookOk = hookExists && hookCallsCheck && hookExec;
|
|
4415
4921
|
printLine(
|
|
@@ -4418,7 +4924,7 @@ async function status(opts = {}) {
|
|
|
4418
4924
|
hookOk ? "pass" : "fail"
|
|
4419
4925
|
);
|
|
4420
4926
|
if (!hookOk) anyFail = true;
|
|
4421
|
-
const agentsDir =
|
|
4927
|
+
const agentsDir = join15(target, ".claude", "agents");
|
|
4422
4928
|
const requiredAgents = [
|
|
4423
4929
|
"design-director.md",
|
|
4424
4930
|
"design-systems-engineer.md",
|
|
@@ -4429,7 +4935,7 @@ async function status(opts = {}) {
|
|
|
4429
4935
|
"brand-precedent.md"
|
|
4430
4936
|
];
|
|
4431
4937
|
const presentAgents = requiredAgents.filter(
|
|
4432
|
-
(f) =>
|
|
4938
|
+
(f) => existsSync9(join15(agentsDir, f))
|
|
4433
4939
|
);
|
|
4434
4940
|
const agentsOk = presentAgents.length === requiredAgents.length;
|
|
4435
4941
|
printLine(
|
|
@@ -4437,24 +4943,24 @@ async function status(opts = {}) {
|
|
|
4437
4943
|
agentsOk ? `PASS (${presentAgents.length}/${requiredAgents.length})` : `WARN (${presentAgents.length}/${requiredAgents.length}) \u2014 missing: ` + requiredAgents.filter((f) => !presentAgents.includes(f)).join(", "),
|
|
4438
4944
|
agentsOk ? "pass" : "warn"
|
|
4439
4945
|
);
|
|
4440
|
-
const cursorrulesOk =
|
|
4946
|
+
const cursorrulesOk = existsSync9(join15(target, ".cursorrules"));
|
|
4441
4947
|
printLine(
|
|
4442
4948
|
".cursorrules",
|
|
4443
4949
|
cursorrulesOk ? "PASS (Cursor users routed through agents)" : "WARN: missing \u2014 Cursor users won't route through agents on this repo",
|
|
4444
4950
|
cursorrulesOk ? "pass" : "warn"
|
|
4445
4951
|
);
|
|
4446
|
-
const actionPath =
|
|
4447
|
-
const actionOk =
|
|
4952
|
+
const actionPath = join15(target, ".github", "workflows", "stylecrib-review.yml");
|
|
4953
|
+
const actionOk = existsSync9(actionPath);
|
|
4448
4954
|
printLine(
|
|
4449
4955
|
"GitHub Action (PR gating)",
|
|
4450
4956
|
actionOk ? "PASS" : "WARN: not installed \u2014 PRs won't be gated by the QA loop in CI",
|
|
4451
4957
|
actionOk ? "pass" : "warn"
|
|
4452
4958
|
);
|
|
4453
|
-
const feedbackPath =
|
|
4454
|
-
const feedbackOk =
|
|
4959
|
+
const feedbackPath = join15(target, ".stylecrib", "feedback.md");
|
|
4960
|
+
const feedbackOk = existsSync9(feedbackPath);
|
|
4455
4961
|
let rejectionCount = 0;
|
|
4456
4962
|
if (feedbackOk) {
|
|
4457
|
-
const body =
|
|
4963
|
+
const body = readFileSync9(feedbackPath, "utf8");
|
|
4458
4964
|
rejectionCount = (body.match(/^## Rejection — /gm) || []).length;
|
|
4459
4965
|
}
|
|
4460
4966
|
printLine(
|
|
@@ -4462,7 +4968,7 @@ async function status(opts = {}) {
|
|
|
4462
4968
|
feedbackOk ? `PASS (${rejectionCount} rejection${rejectionCount === 1 ? "" : "s"} recorded)` : 'INFO: no rejections recorded yet \u2014 add one with `stylecrib reject "<pattern>"`',
|
|
4463
4969
|
feedbackOk ? "pass" : "info"
|
|
4464
4970
|
);
|
|
4465
|
-
const directionOk =
|
|
4971
|
+
const directionOk = existsSync9(join15(target, "design-system", "THEME.md"));
|
|
4466
4972
|
printLine(
|
|
4467
4973
|
"design-system/THEME.md",
|
|
4468
4974
|
directionOk ? "PASS" : "FAIL: missing \u2014 agents have no project direction to pivot reviews on. Run `stylecrib init`",
|
|
@@ -4475,9 +4981,9 @@ async function status(opts = {}) {
|
|
|
4475
4981
|
apiKey ? "PASS" : "WARN: not set \u2014 `stylecrib check` will exit early; hook will warn & no-op",
|
|
4476
4982
|
apiKey ? "pass" : "warn"
|
|
4477
4983
|
);
|
|
4478
|
-
const bypassPath =
|
|
4479
|
-
if (
|
|
4480
|
-
const lines =
|
|
4984
|
+
const bypassPath = join15(target, "reports", "qa-bypasses.jsonl");
|
|
4985
|
+
if (existsSync9(bypassPath)) {
|
|
4986
|
+
const lines = readFileSync9(bypassPath, "utf8").split("\n").filter((l) => l.trim());
|
|
4481
4987
|
printLine(
|
|
4482
4988
|
"qa-bypass log",
|
|
4483
4989
|
`INFO: ${lines.length} bypass${lines.length === 1 ? "" : "es"} recorded \u2192 reports/qa-bypasses.jsonl`,
|
|
@@ -4504,20 +5010,574 @@ function printLine(label, state, level) {
|
|
|
4504
5010
|
console.log(" " + tag + import_picocolors9.default.bold(label.padEnd(28)) + import_picocolors9.default.dim(" " + state));
|
|
4505
5011
|
}
|
|
4506
5012
|
|
|
4507
|
-
// src/commands/
|
|
5013
|
+
// src/commands/verify.ts
|
|
4508
5014
|
init_esm_shims();
|
|
4509
5015
|
var import_picocolors10 = __toESM(require_picocolors(), 1);
|
|
4510
|
-
import { existsSync as
|
|
4511
|
-
import { join as
|
|
5016
|
+
import { existsSync as existsSync10, readFileSync as readFileSync10, mkdirSync as mkdirSync7 } from "fs";
|
|
5017
|
+
import { resolve as resolve12, join as join16, dirname as dirname7 } from "path";
|
|
5018
|
+
import { spawn, execSync as execSync4, execFileSync } from "child_process";
|
|
5019
|
+
async function verify(opts = {}) {
|
|
5020
|
+
const target = resolve12(opts.target ?? process.cwd());
|
|
5021
|
+
const bootTimeoutMs = Math.max(5e3, parseInt(opts.timeout ?? "60000", 10) || 6e4);
|
|
5022
|
+
console.log(import_picocolors10.default.bold("\u25C7 StyleCrib verify") + import_picocolors10.default.dim(" \u2014 runtime smoke gate"));
|
|
5023
|
+
console.log(import_picocolors10.default.dim(" " + target));
|
|
5024
|
+
console.log("");
|
|
5025
|
+
const results = [];
|
|
5026
|
+
if (opts.typecheck === false) {
|
|
5027
|
+
results.push({ name: "typecheck", status: "skip", detail: "--no-typecheck" });
|
|
5028
|
+
} else {
|
|
5029
|
+
results.push(runTypecheck(target));
|
|
5030
|
+
}
|
|
5031
|
+
if (opts.boot === false) {
|
|
5032
|
+
results.push({ name: "dev-server boot", status: "skip", detail: "--no-boot" });
|
|
5033
|
+
} else {
|
|
5034
|
+
results.push(await runBoot(target, opts.route ?? "/", opts.screenshot, bootTimeoutMs));
|
|
5035
|
+
}
|
|
5036
|
+
console.log("");
|
|
5037
|
+
for (const r of results) {
|
|
5038
|
+
const tag = r.status === "pass" ? import_picocolors10.default.green("PASS") : r.status === "fail" ? import_picocolors10.default.red("FAIL") : import_picocolors10.default.dim("SKIP");
|
|
5039
|
+
console.log(` ${tag} ${r.name}${r.detail ? import_picocolors10.default.dim(" \u2014 " + r.detail) : ""}`);
|
|
5040
|
+
}
|
|
5041
|
+
const failed = results.filter((r) => r.status === "fail");
|
|
5042
|
+
if (failed.length > 0) {
|
|
5043
|
+
console.log("");
|
|
5044
|
+
for (const f of failed) {
|
|
5045
|
+
if (!f.output) continue;
|
|
5046
|
+
console.log(import_picocolors10.default.red(`\u2500\u2500 ${f.name} output \u2500\u2500`));
|
|
5047
|
+
console.log(indent2(trimOutput(f.output), 2));
|
|
5048
|
+
console.log("");
|
|
5049
|
+
}
|
|
5050
|
+
console.log(import_picocolors10.default.red(`\u2717 verify failed \u2014 ${failed.length} of ${results.length} step(s).`));
|
|
5051
|
+
console.log(import_picocolors10.default.dim(" A sweep that passes `stylecrib check` can still break the app."));
|
|
5052
|
+
console.log(import_picocolors10.default.dim(" Fix the above before pushing."));
|
|
5053
|
+
process.exitCode = 1;
|
|
5054
|
+
return;
|
|
5055
|
+
}
|
|
5056
|
+
const ran = results.filter((r) => r.status === "pass").length;
|
|
5057
|
+
console.log("");
|
|
5058
|
+
if (ran === 0) {
|
|
5059
|
+
console.log(import_picocolors10.default.yellow("\u26A0 verify ran no checks \u2014 nothing was verified."));
|
|
5060
|
+
for (const r of results) {
|
|
5061
|
+
console.log(import_picocolors10.default.dim(` ${r.name}: ${r.detail}`));
|
|
5062
|
+
}
|
|
5063
|
+
process.exitCode = 1;
|
|
5064
|
+
return;
|
|
5065
|
+
}
|
|
5066
|
+
console.log(import_picocolors10.default.green(`\u2713 verify passed \u2014 ${ran} step(s), 0 failures.`));
|
|
5067
|
+
}
|
|
5068
|
+
function runTypecheck(target) {
|
|
5069
|
+
const pkgPath = join16(target, "package.json");
|
|
5070
|
+
if (!existsSync10(pkgPath)) {
|
|
5071
|
+
return { name: "typecheck", status: "skip", detail: "no package.json" };
|
|
5072
|
+
}
|
|
5073
|
+
let scripts = {};
|
|
5074
|
+
try {
|
|
5075
|
+
scripts = JSON.parse(readFileSync10(pkgPath, "utf8")).scripts ?? {};
|
|
5076
|
+
} catch {
|
|
5077
|
+
return { name: "typecheck", status: "skip", detail: "unreadable package.json" };
|
|
5078
|
+
}
|
|
5079
|
+
const hasTsconfig = existsSync10(join16(target, "tsconfig.json"));
|
|
5080
|
+
let cmd;
|
|
5081
|
+
if (scripts.typecheck) {
|
|
5082
|
+
cmd = "npm run typecheck --silent";
|
|
5083
|
+
} else if (hasTsconfig) {
|
|
5084
|
+
cmd = "npx --no-install tsc --noEmit";
|
|
5085
|
+
} else {
|
|
5086
|
+
return { name: "typecheck", status: "skip", detail: "no typecheck script and no tsconfig.json" };
|
|
5087
|
+
}
|
|
5088
|
+
process.stdout.write(import_picocolors10.default.dim(` \xB7 typecheck (${cmd}) \u2026 `));
|
|
5089
|
+
try {
|
|
5090
|
+
execSync4(cmd, {
|
|
5091
|
+
cwd: target,
|
|
5092
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
5093
|
+
timeout: 3e5,
|
|
5094
|
+
encoding: "utf8"
|
|
5095
|
+
});
|
|
5096
|
+
console.log(import_picocolors10.default.green("ok"));
|
|
5097
|
+
return { name: "typecheck", status: "pass", detail: cmd };
|
|
5098
|
+
} catch (err) {
|
|
5099
|
+
console.log(import_picocolors10.default.red("failed"));
|
|
5100
|
+
const e = err;
|
|
5101
|
+
return {
|
|
5102
|
+
name: "typecheck",
|
|
5103
|
+
status: "fail",
|
|
5104
|
+
detail: cmd,
|
|
5105
|
+
output: [e.stdout, e.stderr].filter(Boolean).join("\n") || e.message || "(no output)"
|
|
5106
|
+
};
|
|
5107
|
+
}
|
|
5108
|
+
}
|
|
5109
|
+
async function runBoot(target, route, screenshotPath, timeoutMs) {
|
|
5110
|
+
const pkgPath = join16(target, "package.json");
|
|
5111
|
+
if (!existsSync10(pkgPath)) {
|
|
5112
|
+
return { name: "dev-server boot", status: "skip", detail: "no package.json" };
|
|
5113
|
+
}
|
|
5114
|
+
let scripts = {};
|
|
5115
|
+
try {
|
|
5116
|
+
scripts = JSON.parse(readFileSync10(pkgPath, "utf8")).scripts ?? {};
|
|
5117
|
+
} catch {
|
|
5118
|
+
return { name: "dev-server boot", status: "skip", detail: "unreadable package.json" };
|
|
5119
|
+
}
|
|
5120
|
+
if (!scripts.dev) {
|
|
5121
|
+
return { name: "dev-server boot", status: "skip", detail: "no `dev` script" };
|
|
5122
|
+
}
|
|
5123
|
+
if (!existsSync10(join16(target, "node_modules"))) {
|
|
5124
|
+
return { name: "dev-server boot", status: "skip", detail: "node_modules missing \u2014 run npm install" };
|
|
5125
|
+
}
|
|
5126
|
+
process.stdout.write(import_picocolors10.default.dim(" \xB7 booting dev server \u2026 "));
|
|
5127
|
+
const child = spawn("npm", ["run", "dev"], {
|
|
5128
|
+
cwd: target,
|
|
5129
|
+
detached: true,
|
|
5130
|
+
stdio: ["ignore", "pipe", "pipe"]
|
|
5131
|
+
});
|
|
5132
|
+
let log = "";
|
|
5133
|
+
const onData = (b) => {
|
|
5134
|
+
log += b.toString();
|
|
5135
|
+
};
|
|
5136
|
+
child.stdout?.on("data", onData);
|
|
5137
|
+
child.stderr?.on("data", onData);
|
|
5138
|
+
const killGroup = () => {
|
|
5139
|
+
if (child.pid === void 0) return;
|
|
5140
|
+
try {
|
|
5141
|
+
process.kill(-child.pid, "SIGTERM");
|
|
5142
|
+
} catch {
|
|
5143
|
+
}
|
|
5144
|
+
try {
|
|
5145
|
+
process.kill(-child.pid, "SIGKILL");
|
|
5146
|
+
} catch {
|
|
5147
|
+
}
|
|
5148
|
+
try {
|
|
5149
|
+
child.kill("SIGKILL");
|
|
5150
|
+
} catch {
|
|
5151
|
+
}
|
|
5152
|
+
};
|
|
5153
|
+
const onSignal = (sig) => {
|
|
5154
|
+
killGroup();
|
|
5155
|
+
process.exitCode = 130;
|
|
5156
|
+
process.removeListener("SIGINT", onSignal);
|
|
5157
|
+
process.removeListener("SIGTERM", onSignal);
|
|
5158
|
+
process.kill(process.pid, sig);
|
|
5159
|
+
};
|
|
5160
|
+
process.once("SIGINT", onSignal);
|
|
5161
|
+
process.once("SIGTERM", onSignal);
|
|
5162
|
+
try {
|
|
5163
|
+
const url = await waitForUrl(child, () => log, timeoutMs);
|
|
5164
|
+
if (!url) {
|
|
5165
|
+
console.log(import_picocolors10.default.red("failed"));
|
|
5166
|
+
return {
|
|
5167
|
+
name: "dev-server boot",
|
|
5168
|
+
status: "fail",
|
|
5169
|
+
detail: `no server URL within ${Math.round(timeoutMs / 1e3)}s`,
|
|
5170
|
+
output: log
|
|
5171
|
+
};
|
|
5172
|
+
}
|
|
5173
|
+
const pageUrl = url.replace(/\/$/, "") + (route.startsWith("/") ? route : "/" + route);
|
|
5174
|
+
const res = await fetchWithRetry(pageUrl, 10, 500);
|
|
5175
|
+
if (!res) {
|
|
5176
|
+
console.log(import_picocolors10.default.red("failed"));
|
|
5177
|
+
return { name: "dev-server boot", status: "fail", detail: `no response from ${pageUrl}`, output: log };
|
|
5178
|
+
}
|
|
5179
|
+
if (!res.ok) {
|
|
5180
|
+
console.log(import_picocolors10.default.red("failed"));
|
|
5181
|
+
return {
|
|
5182
|
+
name: "dev-server boot",
|
|
5183
|
+
status: "fail",
|
|
5184
|
+
detail: `${pageUrl} returned HTTP ${res.status}`,
|
|
5185
|
+
output: res.body.slice(0, 2e3) + "\n---\n" + log
|
|
5186
|
+
};
|
|
5187
|
+
}
|
|
5188
|
+
const dom = renderDom(pageUrl);
|
|
5189
|
+
if (!dom) {
|
|
5190
|
+
console.log(import_picocolors10.default.yellow("skipped"));
|
|
5191
|
+
return {
|
|
5192
|
+
name: "dev-server boot",
|
|
5193
|
+
status: "skip",
|
|
5194
|
+
detail: `server came up at ${pageUrl}, but no headless Chrome was usable, so the render could not be verified \u2014 install Chrome, or re-run where it is available`
|
|
5195
|
+
};
|
|
5196
|
+
}
|
|
5197
|
+
const inspected = dom;
|
|
5198
|
+
const method = "rendered DOM";
|
|
5199
|
+
const bodyProblem = describeBodyProblem(inspected, true);
|
|
5200
|
+
if (bodyProblem) {
|
|
5201
|
+
console.log(import_picocolors10.default.red("failed"));
|
|
5202
|
+
return {
|
|
5203
|
+
name: "dev-server boot",
|
|
5204
|
+
status: "fail",
|
|
5205
|
+
detail: `${bodyProblem} (checked ${dom ? "rendered DOM" : "HTTP body"})`,
|
|
5206
|
+
output: inspected.slice(0, 3e3)
|
|
5207
|
+
};
|
|
5208
|
+
}
|
|
5209
|
+
let shotNote = "";
|
|
5210
|
+
if (screenshotPath) {
|
|
5211
|
+
const abs = resolve12(screenshotPath);
|
|
5212
|
+
mkdirSync7(dirname7(abs), { recursive: true });
|
|
5213
|
+
try {
|
|
5214
|
+
captureScreenshot(pageUrl, abs);
|
|
5215
|
+
shotNote = ` \xB7 screenshot \u2192 ${screenshotPath}`;
|
|
5216
|
+
} catch (e) {
|
|
5217
|
+
shotNote = ` \xB7 screenshot skipped (${e.message})`;
|
|
5218
|
+
}
|
|
5219
|
+
}
|
|
5220
|
+
console.log(import_picocolors10.default.green("ok"));
|
|
5221
|
+
return {
|
|
5222
|
+
name: "dev-server boot",
|
|
5223
|
+
status: "pass",
|
|
5224
|
+
detail: `${pageUrl} \xB7 ${method}, ${inspected.length} bytes${shotNote}`
|
|
5225
|
+
};
|
|
5226
|
+
} finally {
|
|
5227
|
+
process.removeListener("SIGINT", onSignal);
|
|
5228
|
+
process.removeListener("SIGTERM", onSignal);
|
|
5229
|
+
killGroup();
|
|
5230
|
+
}
|
|
5231
|
+
}
|
|
5232
|
+
function waitForUrl(child, getLog, timeoutMs) {
|
|
5233
|
+
return new Promise((res) => {
|
|
5234
|
+
const started = Date.now();
|
|
5235
|
+
let settled = false;
|
|
5236
|
+
const done = (v) => {
|
|
5237
|
+
if (!settled) {
|
|
5238
|
+
settled = true;
|
|
5239
|
+
clearInterval(t);
|
|
5240
|
+
res(v);
|
|
5241
|
+
}
|
|
5242
|
+
};
|
|
5243
|
+
child.once("exit", () => done(null));
|
|
5244
|
+
child.once("error", () => done(null));
|
|
5245
|
+
const t = setInterval(() => {
|
|
5246
|
+
const m = getLog().match(/https?:\/\/(?:localhost|127\.0\.0\.1)(?::\d+)?\/?/);
|
|
5247
|
+
if (m) return done(m[0]);
|
|
5248
|
+
if (Date.now() - started > timeoutMs) done(null);
|
|
5249
|
+
}, 250);
|
|
5250
|
+
});
|
|
5251
|
+
}
|
|
5252
|
+
async function fetchWithRetry(url, attempts, delayMs) {
|
|
5253
|
+
for (let i = 0; i < attempts; i++) {
|
|
5254
|
+
try {
|
|
5255
|
+
const r = await fetch(url, { redirect: "follow" });
|
|
5256
|
+
return { ok: r.ok, status: r.status, body: await r.text() };
|
|
5257
|
+
} catch {
|
|
5258
|
+
await sleep(delayMs);
|
|
5259
|
+
}
|
|
5260
|
+
}
|
|
5261
|
+
return null;
|
|
5262
|
+
}
|
|
5263
|
+
function renderDom(url) {
|
|
5264
|
+
for (const c2 of chromeCandidates()) {
|
|
5265
|
+
try {
|
|
5266
|
+
return execFileSync(c2, [
|
|
5267
|
+
"--headless",
|
|
5268
|
+
"--disable-gpu",
|
|
5269
|
+
"--hide-scrollbars",
|
|
5270
|
+
"--virtual-time-budget=5000",
|
|
5271
|
+
"--dump-dom",
|
|
5272
|
+
url
|
|
5273
|
+
], { stdio: ["ignore", "pipe", "ignore"], timeout: 3e4, encoding: "utf8" });
|
|
5274
|
+
} catch {
|
|
5275
|
+
continue;
|
|
5276
|
+
}
|
|
5277
|
+
}
|
|
5278
|
+
return null;
|
|
5279
|
+
}
|
|
5280
|
+
function describeBodyProblem(body, isDom) {
|
|
5281
|
+
if (/vite-error-overlay/i.test(body)) {
|
|
5282
|
+
return "page served a Vite error overlay";
|
|
5283
|
+
}
|
|
5284
|
+
if (/Internal Server Error|Cannot GET |Failed to resolve import/i.test(body)) {
|
|
5285
|
+
return "page contains a server error";
|
|
5286
|
+
}
|
|
5287
|
+
if (isDom) {
|
|
5288
|
+
const mount = body.match(/<div[^>]+id=["']root["'][^>]*>([\s\S]*?)<\/div>/i);
|
|
5289
|
+
if (mount && mount[1].trim().length === 0) {
|
|
5290
|
+
return "mount node #root is empty after JS ran \u2014 the app threw before rendering";
|
|
5291
|
+
}
|
|
5292
|
+
if (!mount && !/<div[^>]+id=["']app["']/i.test(body)) {
|
|
5293
|
+
return "no #root / #app mount node in the rendered DOM";
|
|
5294
|
+
}
|
|
5295
|
+
} else if (body.trim().length < 80) {
|
|
5296
|
+
return `page body is only ${body.trim().length} bytes \u2014 likely not a real render`;
|
|
5297
|
+
}
|
|
5298
|
+
return null;
|
|
5299
|
+
}
|
|
5300
|
+
function chromeCandidates() {
|
|
5301
|
+
return [
|
|
5302
|
+
"/Applications/Google Chrome.app/Contents/MacOS/Google Chrome",
|
|
5303
|
+
"/Applications/Chromium.app/Contents/MacOS/Chromium",
|
|
5304
|
+
"google-chrome",
|
|
5305
|
+
"chromium"
|
|
5306
|
+
];
|
|
5307
|
+
}
|
|
5308
|
+
function captureScreenshot(url, outPath) {
|
|
5309
|
+
for (const c2 of chromeCandidates()) {
|
|
5310
|
+
try {
|
|
5311
|
+
execFileSync(c2, [
|
|
5312
|
+
"--headless",
|
|
5313
|
+
"--disable-gpu",
|
|
5314
|
+
"--hide-scrollbars",
|
|
5315
|
+
`--screenshot=${outPath}`,
|
|
5316
|
+
"--window-size=1440,1600",
|
|
5317
|
+
url
|
|
5318
|
+
], { stdio: ["ignore", "ignore", "ignore"], timeout: 3e4 });
|
|
5319
|
+
return;
|
|
5320
|
+
} catch {
|
|
5321
|
+
continue;
|
|
5322
|
+
}
|
|
5323
|
+
}
|
|
5324
|
+
throw new Error("no headless Chrome found");
|
|
5325
|
+
}
|
|
5326
|
+
function sleep(ms) {
|
|
5327
|
+
return new Promise((r) => setTimeout(r, ms));
|
|
5328
|
+
}
|
|
5329
|
+
function trimOutput(s, maxLines = 40) {
|
|
5330
|
+
const lines = s.split("\n").filter((l) => l.trim() !== "");
|
|
5331
|
+
if (lines.length <= maxLines) return lines.join("\n");
|
|
5332
|
+
return [...lines.slice(0, maxLines), `\u2026 ${lines.length - maxLines} more line(s)`].join("\n");
|
|
5333
|
+
}
|
|
5334
|
+
function indent2(s, n) {
|
|
5335
|
+
const pad = " ".repeat(n);
|
|
5336
|
+
return s.split("\n").map((l) => pad + l).join("\n");
|
|
5337
|
+
}
|
|
5338
|
+
|
|
5339
|
+
// src/commands/mcp.ts
|
|
5340
|
+
init_esm_shims();
|
|
5341
|
+
import { readFileSync as readFileSync11, existsSync as existsSync11 } from "fs";
|
|
5342
|
+
import { join as join17 } from "path";
|
|
5343
|
+
import { McpServer, ResourceTemplate } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
5344
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
5345
|
+
import { z } from "zod";
|
|
5346
|
+
init_slop_rules();
|
|
5347
|
+
async function mcp(_opts = {}) {
|
|
5348
|
+
const toStderr = (...args) => console.error(...args);
|
|
5349
|
+
console.log = toStderr;
|
|
5350
|
+
console.info = toStderr;
|
|
5351
|
+
console.warn = toStderr;
|
|
5352
|
+
const server = new McpServer({ name: "stylecrib", version: VERSION });
|
|
5353
|
+
server.registerTool(
|
|
5354
|
+
"list_themes",
|
|
5355
|
+
{
|
|
5356
|
+
title: "List StyleCrib themes",
|
|
5357
|
+
description: "The 25 canonical StyleCrib directions: slug, display name, one-line description, and whether `emit_theme_tokens` can currently generate a Tailwind config for it. Four themes use a newer token format the emitter can't read yet and are marked emittable: false \u2014 pick another or use get_theme for the brief alone.",
|
|
5358
|
+
inputSchema: {}
|
|
5359
|
+
},
|
|
5360
|
+
async () => {
|
|
5361
|
+
const themes = DIRECTION_CHOICES.filter((d) => d.value !== "custom").map((d) => ({
|
|
5362
|
+
slug: d.value,
|
|
5363
|
+
name: d.name,
|
|
5364
|
+
description: d.description,
|
|
5365
|
+
emittable: isEmittable(d.value)
|
|
5366
|
+
}));
|
|
5367
|
+
return text(JSON.stringify({ count: themes.length, themes }, null, 2));
|
|
5368
|
+
}
|
|
5369
|
+
);
|
|
5370
|
+
server.registerTool(
|
|
5371
|
+
"get_theme",
|
|
5372
|
+
{
|
|
5373
|
+
title: "Get a StyleCrib theme",
|
|
5374
|
+
description: "The full THEME.md design brief for one theme \u2014 ethos, palette, typography, motion, and the per-theme refusals. Use this to brief an agent before it writes UI for a project on that theme.",
|
|
5375
|
+
inputSchema: {
|
|
5376
|
+
slug: z.string().describe("Theme slug from list_themes, e.g. 'signal-dark'")
|
|
5377
|
+
}
|
|
5378
|
+
},
|
|
5379
|
+
async ({ slug }) => {
|
|
5380
|
+
if (!VALID_DIRECTIONS.includes(slug) || slug === "custom") {
|
|
5381
|
+
return fail(`Unknown theme "${slug}". Call list_themes for valid slugs.`);
|
|
5382
|
+
}
|
|
5383
|
+
const themesDir = resolveAssetDir("themes");
|
|
5384
|
+
if (!themesDir) return fail(missingAssetMessage("themes"));
|
|
5385
|
+
const mdPath = join17(themesDir, slug, "THEME.md");
|
|
5386
|
+
if (!existsSync11(mdPath)) return fail(`No THEME.md for "${slug}" at ${mdPath}.`);
|
|
5387
|
+
const header2 = `# ${slug}
|
|
5388
|
+
|
|
5389
|
+
emittable: ${isEmittable(slug)}
|
|
5390
|
+
tokens: themes/${slug}/tokens.ts
|
|
5391
|
+
|
|
5392
|
+
---
|
|
5393
|
+
|
|
5394
|
+
`;
|
|
5395
|
+
return text(header2 + readFileSync11(mdPath, "utf8"));
|
|
5396
|
+
}
|
|
5397
|
+
);
|
|
5398
|
+
server.registerTool(
|
|
5399
|
+
"emit_theme_tokens",
|
|
5400
|
+
{
|
|
5401
|
+
title: "Generate Tailwind tokens for a theme",
|
|
5402
|
+
description: "Generate tailwind.config.ts (or .js) and src/index.css for a theme \u2014 the same files `stylecrib init` writes. Returns both as text. The config references `tailwindcss-animate`, so that package must be installed alongside Tailwind 3. Fails cleanly for the four themes whose token format isn't readable yet; list_themes marks those.",
|
|
5403
|
+
inputSchema: {
|
|
5404
|
+
slug: z.string().describe("Theme slug from list_themes"),
|
|
5405
|
+
typescript: z.boolean().optional().describe("Emit tailwind.config.ts (default true) or tailwind.config.js")
|
|
5406
|
+
}
|
|
5407
|
+
},
|
|
5408
|
+
async ({ slug, typescript }) => {
|
|
5409
|
+
if (!VALID_DIRECTIONS.includes(slug) || slug === "custom") {
|
|
5410
|
+
return fail(`Unknown theme "${slug}". Call list_themes for valid slugs.`);
|
|
5411
|
+
}
|
|
5412
|
+
try {
|
|
5413
|
+
const direction = slug;
|
|
5414
|
+
await assertDirectionEmittable(direction);
|
|
5415
|
+
const emitted = await emitTokens({ direction, stack: syntheticStack(typescript ?? true) });
|
|
5416
|
+
return text(
|
|
5417
|
+
`// ===== ${emitted.tailwindConfig.path} =====
|
|
5418
|
+
` + emitted.tailwindConfig.content + `
|
|
5419
|
+
|
|
5420
|
+
/* ===== ${emitted.indexCss.path} ===== */
|
|
5421
|
+
` + emitted.indexCss.content
|
|
5422
|
+
);
|
|
5423
|
+
} catch (err) {
|
|
5424
|
+
return fail(err instanceof Error ? err.message : String(err));
|
|
5425
|
+
}
|
|
5426
|
+
}
|
|
5427
|
+
);
|
|
5428
|
+
server.registerTool(
|
|
5429
|
+
"list_rules",
|
|
5430
|
+
{
|
|
5431
|
+
title: "List slop rules",
|
|
5432
|
+
description: "The 39-rule catalog StyleCrib enforces \u2014 what each refuses, severity, category, and where it's detected (commit time vs theme materialization). Filter by any field.",
|
|
5433
|
+
inputSchema: {
|
|
5434
|
+
category: z.string().optional().describe("e.g. typography, palette, layout, content, motion"),
|
|
5435
|
+
severity: z.enum(["block", "warn"]).optional(),
|
|
5436
|
+
detectorAt: z.string().optional().describe("check.ts | slop-blocklist.ts | qa-conductor")
|
|
5437
|
+
}
|
|
5438
|
+
},
|
|
5439
|
+
async ({ category, severity, detectorAt }) => {
|
|
5440
|
+
const rules = SLOP_RULES.filter(
|
|
5441
|
+
(r) => (!category || r.category === category) && (!severity || r.severity === severity) && (!detectorAt || r.detectorAt === detectorAt)
|
|
5442
|
+
);
|
|
5443
|
+
return text(JSON.stringify({ count: rules.length, rules }, null, 2));
|
|
5444
|
+
}
|
|
5445
|
+
);
|
|
5446
|
+
server.registerTool(
|
|
5447
|
+
"check_slop",
|
|
5448
|
+
{
|
|
5449
|
+
title: "Check a file for AI-default slop",
|
|
5450
|
+
description: "Run StyleCrib's text-pattern slop detectors against a file's contents and return every hit with its rule, line, and the offending sample. Covers 26 of the 32 commit-time rules \u2014 everything in the pre-commit hook except the two composition-study provenance checks, R-MONO-CAPS-TRACKED, the three diff-signature regression detectors (which need a diff, not a file), and the LLM review. Whole-file, not diff-scoped: it reports every hit present, not only ones a change introduced. Pass `theme` to also enforce per-theme rules such as R-SECOND-ACCENT.",
|
|
5451
|
+
inputSchema: {
|
|
5452
|
+
path: z.string().describe("File path, used for rule scoping (JSX brace-stripping applies to .tsx/.jsx)"),
|
|
5453
|
+
content: z.string().describe("The file's full contents"),
|
|
5454
|
+
theme: z.string().optional().describe("Active theme slug, enables theme-aware rules (e.g. 'signal-dark')")
|
|
5455
|
+
}
|
|
5456
|
+
},
|
|
5457
|
+
async ({ path: path2, content, theme }) => {
|
|
5458
|
+
let themeCtx = null;
|
|
5459
|
+
if (theme) {
|
|
5460
|
+
if (!VALID_DIRECTIONS.includes(theme)) {
|
|
5461
|
+
return fail(`Unknown theme "${theme}". Call list_themes for valid slugs.`);
|
|
5462
|
+
}
|
|
5463
|
+
themeCtx = resolveThemeContext(theme);
|
|
5464
|
+
}
|
|
5465
|
+
const hits = runAllTextDetectors(path2, content, themeCtx);
|
|
5466
|
+
return text(JSON.stringify(summarise(path2, hits), null, 2));
|
|
5467
|
+
}
|
|
5468
|
+
);
|
|
5469
|
+
server.registerResource(
|
|
5470
|
+
"rules",
|
|
5471
|
+
"stylecrib://rules",
|
|
5472
|
+
{
|
|
5473
|
+
title: "StyleCrib slop-rule catalog",
|
|
5474
|
+
description: "All 39 rules as JSON. Same data as list_rules with no filter.",
|
|
5475
|
+
mimeType: "application/json"
|
|
5476
|
+
},
|
|
5477
|
+
async (uri) => ({
|
|
5478
|
+
contents: [{ uri: uri.href, mimeType: "application/json", text: JSON.stringify(SLOP_RULES, null, 2) }]
|
|
5479
|
+
})
|
|
5480
|
+
);
|
|
5481
|
+
server.registerResource(
|
|
5482
|
+
"agents",
|
|
5483
|
+
new ResourceTemplate("stylecrib://agents/{name}", {
|
|
5484
|
+
list: async () => ({
|
|
5485
|
+
resources: Object.keys(BUNDLED_AGENTS).map((name) => ({
|
|
5486
|
+
uri: `stylecrib://agents/${name}`,
|
|
5487
|
+
name,
|
|
5488
|
+
mimeType: "text/markdown"
|
|
5489
|
+
}))
|
|
5490
|
+
})
|
|
5491
|
+
}),
|
|
5492
|
+
{
|
|
5493
|
+
title: "StyleCrib agent doctrines",
|
|
5494
|
+
description: "The 19 subagent prompts that ship with the pack, by name.",
|
|
5495
|
+
mimeType: "text/markdown"
|
|
5496
|
+
},
|
|
5497
|
+
async (uri, { name }) => {
|
|
5498
|
+
const key = String(name);
|
|
5499
|
+
const doc = BUNDLED_AGENTS[key];
|
|
5500
|
+
if (!doc) throw new CliError(`No agent named "${key}". Known: ${Object.keys(BUNDLED_AGENTS).join(", ")}`);
|
|
5501
|
+
return { contents: [{ uri: uri.href, mimeType: "text/markdown", text: doc }] };
|
|
5502
|
+
}
|
|
5503
|
+
);
|
|
5504
|
+
const transport = new StdioServerTransport();
|
|
5505
|
+
await server.connect(transport);
|
|
5506
|
+
console.error(`stylecrib mcp ${VERSION} \u2014 serving over stdio`);
|
|
5507
|
+
}
|
|
5508
|
+
function text(body) {
|
|
5509
|
+
return { content: [{ type: "text", text: body }] };
|
|
5510
|
+
}
|
|
5511
|
+
function fail(message) {
|
|
5512
|
+
return { content: [{ type: "text", text: message }], isError: true };
|
|
5513
|
+
}
|
|
5514
|
+
var emittableCache = /* @__PURE__ */ new Map();
|
|
5515
|
+
function isEmittable(slug) {
|
|
5516
|
+
const cached = emittableCache.get(slug);
|
|
5517
|
+
if (cached !== void 0) return cached;
|
|
5518
|
+
let ok = true;
|
|
5519
|
+
try {
|
|
5520
|
+
const themesDir = resolveAssetDir("themes");
|
|
5521
|
+
const src = themesDir ? readFileSync11(join17(themesDir, slug, "tokens.ts"), "utf8") : "";
|
|
5522
|
+
ok = /^\s*colors:\s*\{/m.test(src);
|
|
5523
|
+
} catch {
|
|
5524
|
+
ok = false;
|
|
5525
|
+
}
|
|
5526
|
+
emittableCache.set(slug, ok);
|
|
5527
|
+
return ok;
|
|
5528
|
+
}
|
|
5529
|
+
function syntheticStack(ts) {
|
|
5530
|
+
return {
|
|
5531
|
+
rootPath: "",
|
|
5532
|
+
packageJsonPath: "",
|
|
5533
|
+
ts,
|
|
5534
|
+
react: { version: "n/a", major: 0 },
|
|
5535
|
+
vite: { version: "n/a", major: 0 },
|
|
5536
|
+
tailwind: { version: "n/a", major: 0 },
|
|
5537
|
+
hasTailwindConfig: false,
|
|
5538
|
+
tailwindConfigPath: null,
|
|
5539
|
+
hasSrcDir: true,
|
|
5540
|
+
srcPath: "src",
|
|
5541
|
+
notes: []
|
|
5542
|
+
};
|
|
5543
|
+
}
|
|
5544
|
+
function summarise(path2, hits) {
|
|
5545
|
+
const enriched = hits.map((h) => {
|
|
5546
|
+
const rule2 = ruleById(h.ruleId);
|
|
5547
|
+
return {
|
|
5548
|
+
rule: h.ruleId,
|
|
5549
|
+
severity: rule2?.severity ?? "block",
|
|
5550
|
+
category: rule2?.category ?? "unknown",
|
|
5551
|
+
line: h.line,
|
|
5552
|
+
sample: h.sample,
|
|
5553
|
+
description: rule2?.description ?? ""
|
|
5554
|
+
};
|
|
5555
|
+
});
|
|
5556
|
+
const blocks = enriched.filter((h) => h.severity === "block").length;
|
|
5557
|
+
return {
|
|
5558
|
+
path: path2,
|
|
5559
|
+
verdict: blocks > 0 ? "BLOCK" : enriched.length > 0 ? "WARN" : "PASS",
|
|
5560
|
+
blockHits: blocks,
|
|
5561
|
+
warnHits: enriched.length - blocks,
|
|
5562
|
+
hits: enriched,
|
|
5563
|
+
override: enriched.length > 0 ? "Silence a legitimate hit with `// slop-override: R-XXX \u2014 <reason>` within 3 lines above or 8 below." : void 0
|
|
5564
|
+
};
|
|
5565
|
+
}
|
|
5566
|
+
|
|
5567
|
+
// src/commands/harness.ts
|
|
5568
|
+
init_esm_shims();
|
|
5569
|
+
var import_picocolors11 = __toESM(require_picocolors(), 1);
|
|
5570
|
+
import { existsSync as existsSync19 } from "fs";
|
|
5571
|
+
import { join as join26, resolve as resolve13 } from "path";
|
|
4512
5572
|
|
|
4513
5573
|
// src/theme-harness/materialize.ts
|
|
4514
5574
|
init_esm_shims();
|
|
4515
5575
|
import { writeFileSync as writeFileSync7 } from "fs";
|
|
4516
|
-
import { join as
|
|
5576
|
+
import { join as join18 } from "path";
|
|
4517
5577
|
|
|
4518
5578
|
// src/theme-harness/lib/theme-yml.ts
|
|
4519
5579
|
init_esm_shims();
|
|
4520
|
-
import { readFileSync as
|
|
5580
|
+
import { readFileSync as readFileSync12 } from "fs";
|
|
4521
5581
|
import { parse as parseYaml } from "yaml";
|
|
4522
5582
|
var REQUIRED_TOP = [
|
|
4523
5583
|
"name",
|
|
@@ -4543,7 +5603,7 @@ var HEX_RE = /^#[0-9A-Fa-f]{6}$/;
|
|
|
4543
5603
|
function loadTheme(path2) {
|
|
4544
5604
|
let raw;
|
|
4545
5605
|
try {
|
|
4546
|
-
raw = parseYaml(
|
|
5606
|
+
raw = parseYaml(readFileSync12(path2, "utf8"));
|
|
4547
5607
|
} catch (e) {
|
|
4548
5608
|
return { ok: false, errors: [`YAML parse error: ${e.message}`] };
|
|
4549
5609
|
}
|
|
@@ -4629,7 +5689,7 @@ function validateTheme(raw) {
|
|
|
4629
5689
|
|
|
4630
5690
|
// src/theme-harness/materialize.ts
|
|
4631
5691
|
function materializeTheme(slug, themeDir) {
|
|
4632
|
-
const ymlPath =
|
|
5692
|
+
const ymlPath = join18(themeDir, "theme.yml");
|
|
4633
5693
|
const parsed = loadTheme(ymlPath);
|
|
4634
5694
|
if (!parsed.ok) return { ok: false, errors: parsed.errors };
|
|
4635
5695
|
const theme = parsed.theme;
|
|
@@ -4637,11 +5697,11 @@ function materializeTheme(slug, themeDir) {
|
|
|
4637
5697
|
const tokensTs = renderTokensTs(slug, theme);
|
|
4638
5698
|
const tailwindSnippet = renderTailwindSnippet(slug, theme);
|
|
4639
5699
|
const wrote = [];
|
|
4640
|
-
writeFileSync7(
|
|
5700
|
+
writeFileSync7(join18(themeDir, "THEME.md"), themeMd);
|
|
4641
5701
|
wrote.push("THEME.md");
|
|
4642
|
-
writeFileSync7(
|
|
5702
|
+
writeFileSync7(join18(themeDir, "tokens.ts"), tokensTs);
|
|
4643
5703
|
wrote.push("tokens.ts");
|
|
4644
|
-
writeFileSync7(
|
|
5704
|
+
writeFileSync7(join18(themeDir, "tailwind.snippet.ts"), tailwindSnippet);
|
|
4645
5705
|
wrote.push("tailwind.snippet.ts");
|
|
4646
5706
|
return { ok: true, wrote };
|
|
4647
5707
|
}
|
|
@@ -4726,9 +5786,27 @@ function renderTokensTs(slug, t) {
|
|
|
4726
5786
|
].join("\n");
|
|
4727
5787
|
}
|
|
4728
5788
|
function renderTailwindSnippet(slug, t) {
|
|
5789
|
+
const display = extractFontStack(t.typography.display);
|
|
5790
|
+
const body = extractFontStack(t.typography.body);
|
|
5791
|
+
const mono = extractFontStack(t.typography.mono);
|
|
5792
|
+
const fontLines = [];
|
|
5793
|
+
if (display) fontLines.push(` display: ${JSON.stringify(display)},`);
|
|
5794
|
+
if (body) fontLines.push(` body: ${JSON.stringify(body)},`);
|
|
5795
|
+
if (mono) fontLines.push(` mono: ${JSON.stringify(mono)},`);
|
|
5796
|
+
const fontBlock = fontLines.length ? [` fontFamily: {`, ...fontLines, ` },`] : [
|
|
5797
|
+
` // fontFamily omitted \u2014 theme.yml's typography brief doesn't state a`,
|
|
5798
|
+
` // machine-readable fallback stack. Read the Typography section of`,
|
|
5799
|
+
` // THEME.md and set the stack by hand.`
|
|
5800
|
+
];
|
|
5801
|
+
const unresolved = [
|
|
5802
|
+
!display ? "display" : "",
|
|
5803
|
+
!body ? "body" : "",
|
|
5804
|
+
!mono ? "mono" : ""
|
|
5805
|
+
].filter(Boolean);
|
|
4729
5806
|
return [
|
|
4730
5807
|
`// Generated from theme.yml. Merge into your tailwind.config.ts under theme.extend.`,
|
|
4731
5808
|
`// slug: ${slug}`,
|
|
5809
|
+
unresolved.length ? `// Unresolved font stack(s): ${unresolved.join(", ")} \u2014 see THEME.md \xA7 Typography.` : "",
|
|
4732
5810
|
``,
|
|
4733
5811
|
`export const ${camelCase(slug)}TailwindExtend = {`,
|
|
4734
5812
|
` colors: {`,
|
|
@@ -4739,15 +5817,21 @@ function renderTailwindSnippet(slug, t) {
|
|
|
4739
5817
|
t.palette.secondary ? ` secondary: ${JSON.stringify(t.palette.secondary)},` : "",
|
|
4740
5818
|
t.palette.signal ? ` signal: ${JSON.stringify(t.palette.signal)},` : "",
|
|
4741
5819
|
` },`,
|
|
4742
|
-
|
|
4743
|
-
` display: ${JSON.stringify([t.typography.display])},`,
|
|
4744
|
-
` body: ${JSON.stringify([t.typography.body])},`,
|
|
4745
|
-
` mono: ${JSON.stringify([t.typography.mono])},`,
|
|
4746
|
-
` },`,
|
|
5820
|
+
...fontBlock,
|
|
4747
5821
|
`};`,
|
|
4748
5822
|
``
|
|
4749
5823
|
].filter((l) => l !== "").join("\n");
|
|
4750
5824
|
}
|
|
5825
|
+
function extractFontStack(brief) {
|
|
5826
|
+
if (!brief) return null;
|
|
5827
|
+
const text2 = String(brief).trim();
|
|
5828
|
+
if (/^none\b/i.test(text2)) return null;
|
|
5829
|
+
const m = text2.match(/fallback stack\s*[—–-]\s*([^.]+)\./i);
|
|
5830
|
+
if (!m) return null;
|
|
5831
|
+
const families = m[1].split(",").map((f) => f.trim().replace(/^["'“”]|["'“”]$/g, "").trim()).filter((f) => f.length > 0 && f.length < 40 && !/\s{2,}/.test(f));
|
|
5832
|
+
if (families.length < 2) return null;
|
|
5833
|
+
return families;
|
|
5834
|
+
}
|
|
4751
5835
|
function camelCase(slug) {
|
|
4752
5836
|
return slug.split("-").map((p, i) => i === 0 ? p : p[0].toUpperCase() + p.slice(1)).join("");
|
|
4753
5837
|
}
|
|
@@ -4763,8 +5847,8 @@ function inferFamily(t) {
|
|
|
4763
5847
|
|
|
4764
5848
|
// src/theme-harness/grade.ts
|
|
4765
5849
|
init_esm_shims();
|
|
4766
|
-
import { appendFileSync as appendFileSync3, readFileSync as
|
|
4767
|
-
import { join as
|
|
5850
|
+
import { appendFileSync as appendFileSync3, readFileSync as readFileSync17, existsSync as existsSync16 } from "fs";
|
|
5851
|
+
import { join as join23 } from "path";
|
|
4768
5852
|
|
|
4769
5853
|
// src/theme-harness/gates/slop-blocklist.ts
|
|
4770
5854
|
init_esm_shims();
|
|
@@ -4884,14 +5968,14 @@ function runSourceRules(src, theme, failures) {
|
|
|
4884
5968
|
const capsCssRe2 = /letterSpacing:\s*["']?(0?\.\d+)em[\s\S]{0,200}?textTransform:\s*["']uppercase["']/g;
|
|
4885
5969
|
const fontSizeRe = /fontSize:\s*(\d+)/;
|
|
4886
5970
|
const capsHits = [];
|
|
4887
|
-
const scanCaps = (re,
|
|
5971
|
+
const scanCaps = (re, text2) => {
|
|
4888
5972
|
let m;
|
|
4889
|
-
while ((m = re.exec(
|
|
5973
|
+
while ((m = re.exec(text2)) !== null) {
|
|
4890
5974
|
const spacing = parseFloat(m[1]);
|
|
4891
5975
|
if (spacing < 0.18) continue;
|
|
4892
5976
|
const start = Math.max(0, m.index - 240);
|
|
4893
|
-
const end = Math.min(
|
|
4894
|
-
const window =
|
|
5977
|
+
const end = Math.min(text2.length, m.index + m[0].length + 240);
|
|
5978
|
+
const window = text2.slice(start, end);
|
|
4895
5979
|
const fs = window.match(fontSizeRe);
|
|
4896
5980
|
const fontSize = fs ? parseInt(fs[1], 10) : 12;
|
|
4897
5981
|
if (fontSize < 20) capsHits.push(`${m[1]}em @ ~${fontSize}px`);
|
|
@@ -5026,8 +6110,8 @@ function normalizeFont(f) {
|
|
|
5026
6110
|
|
|
5027
6111
|
// src/theme-harness/gates/avoid-rubric.ts
|
|
5028
6112
|
init_esm_shims();
|
|
5029
|
-
import { readFileSync as
|
|
5030
|
-
import { join as
|
|
6113
|
+
import { readFileSync as readFileSync13, existsSync as existsSync12 } from "fs";
|
|
6114
|
+
import { join as join19 } from "path";
|
|
5031
6115
|
import Anthropic4 from "@anthropic-ai/sdk";
|
|
5032
6116
|
var RUBRIC_TOOL = {
|
|
5033
6117
|
name: "report_verdict",
|
|
@@ -5141,25 +6225,25 @@ async function runAvoidRubric(theme, themeDir) {
|
|
|
5141
6225
|
return { passed: failures.length === 0, failures };
|
|
5142
6226
|
}
|
|
5143
6227
|
function collectEvidence(themeDir) {
|
|
5144
|
-
const readOr = (p) =>
|
|
6228
|
+
const readOr = (p) => existsSync12(p) ? readFileSync13(p, "utf8") : "<file not present>";
|
|
5145
6229
|
return {
|
|
5146
|
-
themeMd: readOr(
|
|
5147
|
-
tokensTs: readOr(
|
|
5148
|
-
tailwindSnippet: readOr(
|
|
6230
|
+
themeMd: readOr(join19(themeDir, "THEME.md")),
|
|
6231
|
+
tokensTs: readOr(join19(themeDir, "tokens.ts")),
|
|
6232
|
+
tailwindSnippet: readOr(join19(themeDir, "tailwind.snippet.ts"))
|
|
5149
6233
|
};
|
|
5150
6234
|
}
|
|
5151
6235
|
|
|
5152
6236
|
// src/theme-harness/gates/regression-scan.ts
|
|
5153
6237
|
init_esm_shims();
|
|
5154
|
-
import { readFileSync as
|
|
5155
|
-
import { join as
|
|
6238
|
+
import { readFileSync as readFileSync14, existsSync as existsSync13 } from "fs";
|
|
6239
|
+
import { join as join20 } from "path";
|
|
5156
6240
|
function resolveRegressionSet(themeDir) {
|
|
5157
|
-
return
|
|
6241
|
+
return join20(themeDir, "..", "..", "site", "design-system", "regression-set.jsonl");
|
|
5158
6242
|
}
|
|
5159
6243
|
function loadRegressionRules(themeDir) {
|
|
5160
6244
|
const p = resolveRegressionSet(themeDir);
|
|
5161
|
-
if (!
|
|
5162
|
-
const raw =
|
|
6245
|
+
if (!existsSync13(p)) return [];
|
|
6246
|
+
const raw = readFileSync14(p, "utf8").trim();
|
|
5163
6247
|
if (!raw) return [];
|
|
5164
6248
|
const rules = [];
|
|
5165
6249
|
for (const line of raw.split("\n")) {
|
|
@@ -5175,9 +6259,9 @@ function loadRegressionRules(themeDir) {
|
|
|
5175
6259
|
function runRegressionScan(theme, themeDir) {
|
|
5176
6260
|
const rules = loadRegressionRules(themeDir);
|
|
5177
6261
|
if (rules.length === 0) return { passed: true, failures: [] };
|
|
5178
|
-
const tokensSrc = safeRead(
|
|
5179
|
-
const twSrc = safeRead(
|
|
5180
|
-
const themeMdSrc = safeRead(
|
|
6262
|
+
const tokensSrc = safeRead(join20(themeDir, "tokens.ts"));
|
|
6263
|
+
const twSrc = safeRead(join20(themeDir, "tailwind.snippet.ts"));
|
|
6264
|
+
const themeMdSrc = safeRead(join20(themeDir, "THEME.md"));
|
|
5181
6265
|
const combined = tokensSrc + "\n" + twSrc;
|
|
5182
6266
|
const failures = [];
|
|
5183
6267
|
for (const rule2 of rules) {
|
|
@@ -5233,7 +6317,7 @@ function checkRule(rule2, theme, materializedCode, themeMdSrc) {
|
|
|
5233
6317
|
return null;
|
|
5234
6318
|
}
|
|
5235
6319
|
function safeRead(p) {
|
|
5236
|
-
return
|
|
6320
|
+
return existsSync13(p) ? readFileSync14(p, "utf8") : "";
|
|
5237
6321
|
}
|
|
5238
6322
|
function hexToRgb2(hex) {
|
|
5239
6323
|
const m = hex.match(/^#([0-9a-f]{2})([0-9a-f]{2})([0-9a-f]{2})$/i);
|
|
@@ -5249,9 +6333,9 @@ function rgbDistance2(a, b) {
|
|
|
5249
6333
|
|
|
5250
6334
|
// src/theme-harness/gates/anchor-compare.ts
|
|
5251
6335
|
init_esm_shims();
|
|
5252
|
-
import { existsSync as
|
|
5253
|
-
import { execSync as
|
|
5254
|
-
import { join as
|
|
6336
|
+
import { existsSync as existsSync14, readFileSync as readFileSync15 } from "fs";
|
|
6337
|
+
import { execSync as execSync5 } from "child_process";
|
|
6338
|
+
import { join as join21 } from "path";
|
|
5255
6339
|
import Anthropic5 from "@anthropic-ai/sdk";
|
|
5256
6340
|
var RUBRIC_TOOL2 = {
|
|
5257
6341
|
name: "report_anchor_verdict",
|
|
@@ -5318,16 +6402,16 @@ async function runAnchorCompare(theme, themeDir, repoRoot) {
|
|
|
5318
6402
|
skipped: `Family ${family} has no golden-set anchor yet \u2014 anchor-compare skipped (per Matt's Path B decision, families A+G bootstrap from their own first-authored theme).`
|
|
5319
6403
|
};
|
|
5320
6404
|
}
|
|
5321
|
-
const familyDir =
|
|
5322
|
-
const rubricPath =
|
|
5323
|
-
if (!
|
|
6405
|
+
const familyDir = join21(repoRoot, "site", "design-system", "golden-set", family);
|
|
6406
|
+
const rubricPath = join21(familyDir, "rubric.md");
|
|
6407
|
+
if (!existsSync14(rubricPath)) {
|
|
5324
6408
|
return {
|
|
5325
6409
|
passed: true,
|
|
5326
6410
|
failures: [],
|
|
5327
6411
|
skipped: `Family ${family} rubric.md missing at ${rubricPath} \u2014 anchor-compare skipped.`
|
|
5328
6412
|
};
|
|
5329
6413
|
}
|
|
5330
|
-
const rubric =
|
|
6414
|
+
const rubric = readFileSync15(rubricPath, "utf8");
|
|
5331
6415
|
const anchorPngs = findAnchorPngs(familyDir);
|
|
5332
6416
|
if (anchorPngs.length === 0) {
|
|
5333
6417
|
return {
|
|
@@ -5343,7 +6427,7 @@ async function runAnchorCompare(theme, themeDir, repoRoot) {
|
|
|
5343
6427
|
skipped: `theme.yml has no render_url \u2014 anchor-compare skipped. Add render_url to enable.`
|
|
5344
6428
|
};
|
|
5345
6429
|
}
|
|
5346
|
-
const renderPath =
|
|
6430
|
+
const renderPath = join21(themeDir, ".anchor-render.png");
|
|
5347
6431
|
try {
|
|
5348
6432
|
captureRender(theme.render_url, renderPath);
|
|
5349
6433
|
} catch (e) {
|
|
@@ -5353,7 +6437,7 @@ async function runAnchorCompare(theme, themeDir, repoRoot) {
|
|
|
5353
6437
|
skipped: `Render capture failed: ${e.message}. Verify dev server is running at ${theme.render_url}. Gate skipped.`
|
|
5354
6438
|
};
|
|
5355
6439
|
}
|
|
5356
|
-
if (!
|
|
6440
|
+
if (!existsSync14(renderPath)) {
|
|
5357
6441
|
return {
|
|
5358
6442
|
passed: true,
|
|
5359
6443
|
failures: [],
|
|
@@ -5374,7 +6458,7 @@ async function runAnchorCompare(theme, themeDir, repoRoot) {
|
|
|
5374
6458
|
source: {
|
|
5375
6459
|
type: "base64",
|
|
5376
6460
|
media_type: "image/png",
|
|
5377
|
-
data:
|
|
6461
|
+
data: readFileSync15(p).toString("base64")
|
|
5378
6462
|
}
|
|
5379
6463
|
}));
|
|
5380
6464
|
const renderBlock = {
|
|
@@ -5382,7 +6466,7 @@ async function runAnchorCompare(theme, themeDir, repoRoot) {
|
|
|
5382
6466
|
source: {
|
|
5383
6467
|
type: "base64",
|
|
5384
6468
|
media_type: "image/png",
|
|
5385
|
-
data:
|
|
6469
|
+
data: readFileSync15(renderPath).toString("base64")
|
|
5386
6470
|
}
|
|
5387
6471
|
};
|
|
5388
6472
|
const userContent = [
|
|
@@ -5466,20 +6550,20 @@ Grade the render against each rubric axis. Return via report_anchor_verdict.`
|
|
|
5466
6550
|
return { passed: failures.length === 0, failures };
|
|
5467
6551
|
}
|
|
5468
6552
|
function findAnchorPngs(familyDir) {
|
|
5469
|
-
if (!
|
|
5470
|
-
const dirents =
|
|
5471
|
-
return dirents.filter((n) => /^[0-9]+-[a-z0-9-]+\.png$/.test(n)).map((n) =>
|
|
6553
|
+
if (!existsSync14(familyDir)) return [];
|
|
6554
|
+
const dirents = execSync5(`ls -1 "${familyDir}" 2>/dev/null`).toString().split("\n").filter(Boolean);
|
|
6555
|
+
return dirents.filter((n) => /^[0-9]+-[a-z0-9-]+\.png$/.test(n)).map((n) => join21(familyDir, n));
|
|
5472
6556
|
}
|
|
5473
6557
|
function captureRender(url, outPath) {
|
|
5474
|
-
const
|
|
6558
|
+
const chromeCandidates2 = [
|
|
5475
6559
|
"/Applications/Google Chrome.app/Contents/MacOS/Google Chrome",
|
|
5476
6560
|
"/Applications/Chromium.app/Contents/MacOS/Chromium",
|
|
5477
6561
|
"google-chrome",
|
|
5478
6562
|
"chromium"
|
|
5479
6563
|
];
|
|
5480
|
-
for (const c2 of
|
|
6564
|
+
for (const c2 of chromeCandidates2) {
|
|
5481
6565
|
try {
|
|
5482
|
-
|
|
6566
|
+
execSync5(`"${c2}" --headless --disable-gpu --hide-scrollbars --screenshot="${outPath}" --window-size=1440,1600 "${url}"`, {
|
|
5483
6567
|
stdio: ["ignore", "ignore", "ignore"],
|
|
5484
6568
|
timeout: 3e4
|
|
5485
6569
|
});
|
|
@@ -5488,13 +6572,13 @@ function captureRender(url, outPath) {
|
|
|
5488
6572
|
continue;
|
|
5489
6573
|
}
|
|
5490
6574
|
}
|
|
5491
|
-
throw new Error(`No headless Chrome found. Tried: ${
|
|
6575
|
+
throw new Error(`No headless Chrome found. Tried: ${chromeCandidates2.join(", ")}`);
|
|
5492
6576
|
}
|
|
5493
6577
|
|
|
5494
6578
|
// src/theme-harness/dismiss.ts
|
|
5495
6579
|
init_esm_shims();
|
|
5496
|
-
import { existsSync as
|
|
5497
|
-
import { join as
|
|
6580
|
+
import { existsSync as existsSync15, readFileSync as readFileSync16, writeFileSync as writeFileSync8, appendFileSync as appendFileSync2 } from "fs";
|
|
6581
|
+
import { join as join22 } from "path";
|
|
5498
6582
|
var DISMISSALS_FILE = "dismissals.jsonl";
|
|
5499
6583
|
function writeDismissal(themeDir, finding_id, reason, scope, now) {
|
|
5500
6584
|
const d = {
|
|
@@ -5504,18 +6588,18 @@ function writeDismissal(themeDir, finding_id, reason, scope, now) {
|
|
|
5504
6588
|
dismissed_at: now,
|
|
5505
6589
|
applied: false
|
|
5506
6590
|
};
|
|
5507
|
-
appendFileSync2(
|
|
6591
|
+
appendFileSync2(join22(themeDir, DISMISSALS_FILE), JSON.stringify(d) + "\n");
|
|
5508
6592
|
return d;
|
|
5509
6593
|
}
|
|
5510
6594
|
function loadDismissals(themeDir) {
|
|
5511
|
-
const p =
|
|
5512
|
-
if (!
|
|
5513
|
-
const raw =
|
|
6595
|
+
const p = join22(themeDir, DISMISSALS_FILE);
|
|
6596
|
+
if (!existsSync15(p)) return [];
|
|
6597
|
+
const raw = readFileSync16(p, "utf8").trim();
|
|
5514
6598
|
if (!raw) return [];
|
|
5515
6599
|
return raw.split("\n").filter(Boolean).map((line) => JSON.parse(line));
|
|
5516
6600
|
}
|
|
5517
6601
|
function persistDismissals(themeDir, ds) {
|
|
5518
|
-
const p =
|
|
6602
|
+
const p = join22(themeDir, DISMISSALS_FILE);
|
|
5519
6603
|
const body = ds.map((d) => JSON.stringify(d)).join("\n") + (ds.length ? "\n" : "");
|
|
5520
6604
|
writeFileSync8(p, body);
|
|
5521
6605
|
}
|
|
@@ -5537,7 +6621,7 @@ function claimDismissal(ds, finding_id, runTimestamp) {
|
|
|
5537
6621
|
|
|
5538
6622
|
// src/theme-harness/grade.ts
|
|
5539
6623
|
async function gradeTheme(slug, themeDir, now, repoRoot = process.cwd()) {
|
|
5540
|
-
const ymlPath =
|
|
6624
|
+
const ymlPath = join23(themeDir, "theme.yml");
|
|
5541
6625
|
const parsed = loadTheme(ymlPath);
|
|
5542
6626
|
if (!parsed.ok) return { error: parsed.errors };
|
|
5543
6627
|
const theme = parsed.theme;
|
|
@@ -5595,18 +6679,18 @@ async function gradeTheme(slug, themeDir, now, repoRoot = process.cwd()) {
|
|
|
5595
6679
|
}
|
|
5596
6680
|
const overall = gates.some((g) => !g.verdict.passed) ? "BLOCK" : "PASS";
|
|
5597
6681
|
const report = { slug, gates, overall, ran_at: now };
|
|
5598
|
-
const logPath =
|
|
6682
|
+
const logPath = join23(themeDir, "audit.log.jsonl");
|
|
5599
6683
|
appendFileSync3(logPath, JSON.stringify(report) + "\n");
|
|
5600
6684
|
if (dismissalsMutated) persistDismissals(themeDir, dismissals);
|
|
5601
6685
|
return report;
|
|
5602
6686
|
}
|
|
5603
6687
|
function readSectionSource(slug, repoRoot) {
|
|
5604
6688
|
const candidates = [
|
|
5605
|
-
|
|
5606
|
-
|
|
6689
|
+
join23(repoRoot, "site", "src", "components", "sections", `${slug}.tsx`),
|
|
6690
|
+
join23(repoRoot, "src", "components", "sections", `${slug}.tsx`)
|
|
5607
6691
|
];
|
|
5608
6692
|
for (const p of candidates) {
|
|
5609
|
-
if (
|
|
6693
|
+
if (existsSync16(p)) return readFileSync17(p, "utf8");
|
|
5610
6694
|
}
|
|
5611
6695
|
return void 0;
|
|
5612
6696
|
}
|
|
@@ -5680,15 +6764,15 @@ function formatReport(r) {
|
|
|
5680
6764
|
|
|
5681
6765
|
// src/theme-harness/promote.ts
|
|
5682
6766
|
init_esm_shims();
|
|
5683
|
-
import { readFileSync as
|
|
5684
|
-
import { join as
|
|
6767
|
+
import { readFileSync as readFileSync18, writeFileSync as writeFileSync9, existsSync as existsSync17, appendFileSync as appendFileSync4 } from "fs";
|
|
6768
|
+
import { join as join24 } from "path";
|
|
5685
6769
|
function regressionSetPath(themeDir) {
|
|
5686
|
-
return
|
|
6770
|
+
return join24(themeDir, "..", "..", "site", "design-system", "regression-set.jsonl");
|
|
5687
6771
|
}
|
|
5688
6772
|
function nextRegressionId(themeDir) {
|
|
5689
6773
|
const p = regressionSetPath(themeDir);
|
|
5690
|
-
if (!
|
|
5691
|
-
const raw =
|
|
6774
|
+
if (!existsSync17(p)) return "REG-0001";
|
|
6775
|
+
const raw = readFileSync18(p, "utf8");
|
|
5692
6776
|
let max = 0;
|
|
5693
6777
|
for (const line of raw.split("\n")) {
|
|
5694
6778
|
const m = line.match(/"id":\s*"REG-(\d+)"/);
|
|
@@ -5710,7 +6794,7 @@ function promoteToRegression(themeDir, input3) {
|
|
|
5710
6794
|
scope: "global"
|
|
5711
6795
|
};
|
|
5712
6796
|
const p = regressionSetPath(themeDir);
|
|
5713
|
-
if (!
|
|
6797
|
+
if (!existsSync17(p)) {
|
|
5714
6798
|
writeFileSync9(p, "// Regression matchers \u2014 permanent lessons from caught defects.\n// One JSON object per line. See site/design-system/HARNESS_DESIGN.md.\n");
|
|
5715
6799
|
}
|
|
5716
6800
|
appendFileSync4(p, JSON.stringify(rule2) + "\n");
|
|
@@ -5719,8 +6803,8 @@ function promoteToRegression(themeDir, input3) {
|
|
|
5719
6803
|
|
|
5720
6804
|
// src/theme-harness/waivers.ts
|
|
5721
6805
|
init_esm_shims();
|
|
5722
|
-
import { existsSync as
|
|
5723
|
-
import { join as
|
|
6806
|
+
import { existsSync as existsSync18, mkdirSync as mkdirSync8, readFileSync as readFileSync19, readdirSync as readdirSync4, statSync as statSync4, writeFileSync as writeFileSync10 } from "fs";
|
|
6807
|
+
import { join as join25, relative as relative5 } from "path";
|
|
5724
6808
|
var SKIP_DIRS = /* @__PURE__ */ new Set([
|
|
5725
6809
|
"node_modules",
|
|
5726
6810
|
".git",
|
|
@@ -5735,9 +6819,9 @@ var SKIP_DIRS = /* @__PURE__ */ new Set([
|
|
|
5735
6819
|
]);
|
|
5736
6820
|
var CODE_SUFFIXES = /\.(?:tsx?|jsx?|mjs|cjs|css|scss|md|ya?ml)$/;
|
|
5737
6821
|
function walk2(root, acc) {
|
|
5738
|
-
for (const entry of
|
|
6822
|
+
for (const entry of readdirSync4(root)) {
|
|
5739
6823
|
if (SKIP_DIRS.has(entry) || entry.startsWith(".")) continue;
|
|
5740
|
-
const full =
|
|
6824
|
+
const full = join25(root, entry);
|
|
5741
6825
|
let st;
|
|
5742
6826
|
try {
|
|
5743
6827
|
st = statSync4(full);
|
|
@@ -5775,7 +6859,7 @@ function collectWaivers(target) {
|
|
|
5775
6859
|
for (const abs of files) {
|
|
5776
6860
|
let body;
|
|
5777
6861
|
try {
|
|
5778
|
-
body =
|
|
6862
|
+
body = readFileSync19(abs, "utf8");
|
|
5779
6863
|
} catch {
|
|
5780
6864
|
continue;
|
|
5781
6865
|
}
|
|
@@ -5816,15 +6900,15 @@ function formatWaiverDigest(waivers, today) {
|
|
|
5816
6900
|
return lines.join("\n") + "\n";
|
|
5817
6901
|
}
|
|
5818
6902
|
function writeDigest(target, waivers, today) {
|
|
5819
|
-
const dir =
|
|
5820
|
-
if (!
|
|
6903
|
+
const dir = join25(target, "reports");
|
|
6904
|
+
if (!existsSync18(dir)) {
|
|
5821
6905
|
if (waivers.length === 0) return null;
|
|
5822
|
-
|
|
6906
|
+
mkdirSync8(dir, { recursive: true });
|
|
5823
6907
|
}
|
|
5824
6908
|
const digest = formatWaiverDigest(waivers, today);
|
|
5825
|
-
const outPath =
|
|
6909
|
+
const outPath = join25(dir, `qa-waivers-${today}.md`);
|
|
5826
6910
|
writeFileSync10(outPath, digest);
|
|
5827
|
-
writeFileSync10(
|
|
6911
|
+
writeFileSync10(join25(dir, "qa-waivers-latest.md"), digest);
|
|
5828
6912
|
return outPath;
|
|
5829
6913
|
}
|
|
5830
6914
|
|
|
@@ -5841,30 +6925,30 @@ function registerHarnessCommand(program2) {
|
|
|
5841
6925
|
const harness = program2.command("harness").description("v2 theme-authoring harness \u2014 materialize + grade + (later) dismiss.");
|
|
5842
6926
|
harness.command("materialize <slug>").description("Read themes/<slug>/theme.yml \u2192 write THEME.md + tokens.ts + tailwind.snippet.ts.").option("--themes-dir <path>", "Root directory containing per-theme folders", DEFAULT_THEMES_DIR).action((slug, opts) => {
|
|
5843
6927
|
const dir = resolveThemeDir(slug, opts);
|
|
5844
|
-
if (!
|
|
5845
|
-
console.error(
|
|
6928
|
+
if (!existsSync19(join26(dir, "theme.yml"))) {
|
|
6929
|
+
console.error(import_picocolors11.default.red(`\u2717 No theme.yml at ${join26(dir, "theme.yml")}`));
|
|
5846
6930
|
process.exit(1);
|
|
5847
6931
|
}
|
|
5848
6932
|
const result = materializeTheme(slug, dir);
|
|
5849
6933
|
if (!result.ok) {
|
|
5850
|
-
console.error(
|
|
5851
|
-
for (const e of result.errors) console.error(
|
|
6934
|
+
console.error(import_picocolors11.default.red(`\u2717 theme.yml validation failed:`));
|
|
6935
|
+
for (const e of result.errors) console.error(import_picocolors11.default.red(` \xB7 ${e}`));
|
|
5852
6936
|
process.exit(1);
|
|
5853
6937
|
}
|
|
5854
|
-
console.log(
|
|
6938
|
+
console.log(import_picocolors11.default.green(`\u2713 Materialized ${slug}:`));
|
|
5855
6939
|
for (const f of result.wrote) console.log(` \xB7 ${f}`);
|
|
5856
6940
|
});
|
|
5857
6941
|
harness.command("grade <slug>").description("Run harness gates against themes/<slug>. Appends audit.log.jsonl and exits 1 on BLOCK.").option("--themes-dir <path>", "Root directory containing per-theme folders", DEFAULT_THEMES_DIR).action(async (slug, opts) => {
|
|
5858
6942
|
const dir = resolveThemeDir(slug, opts);
|
|
5859
|
-
if (!
|
|
5860
|
-
console.error(
|
|
6943
|
+
if (!existsSync19(join26(dir, "theme.yml"))) {
|
|
6944
|
+
console.error(import_picocolors11.default.red(`\u2717 No theme.yml at ${join26(dir, "theme.yml")}`));
|
|
5861
6945
|
process.exit(1);
|
|
5862
6946
|
}
|
|
5863
6947
|
const repoRoot = resolve13(opts.themesDir ?? DEFAULT_THEMES_DIR, "..");
|
|
5864
6948
|
const report = await gradeTheme(slug, dir, isoNow(), repoRoot);
|
|
5865
6949
|
if ("error" in report) {
|
|
5866
|
-
console.error(
|
|
5867
|
-
for (const e of report.error) console.error(
|
|
6950
|
+
console.error(import_picocolors11.default.red(`\u2717 theme.yml validation failed:`));
|
|
6951
|
+
for (const e of report.error) console.error(import_picocolors11.default.red(` \xB7 ${e}`));
|
|
5868
6952
|
process.exit(1);
|
|
5869
6953
|
}
|
|
5870
6954
|
console.log(formatReport(report));
|
|
@@ -5874,14 +6958,14 @@ function registerHarnessCommand(program2) {
|
|
|
5874
6958
|
"Turn a caught defect into a permanent regression matcher \u2014 appends to site/design-system/regression-set.jsonl. Rules are tracked in git so the whole team inherits the lesson."
|
|
5875
6959
|
).requiredOption("--matcher-type <type>", "hex_distance | class_pattern | inline_style").requiredOption("--target <value>", "For hex_distance: a hex color like #D97757. For class_pattern / inline_style: a regex.").option("--threshold <n>", "For hex_distance: RGB distance threshold (default: 12).", "12").requiredOption("-r, --reason <text>", "Human-readable explanation of the defect and why it's a permanent rule.").option("--themes-dir <path>", "Root directory containing per-theme folders", DEFAULT_THEMES_DIR).action((slug, opts) => {
|
|
5876
6960
|
const dir = resolveThemeDir(slug, opts);
|
|
5877
|
-
if (!
|
|
5878
|
-
console.error(
|
|
6961
|
+
if (!existsSync19(join26(dir, "theme.yml"))) {
|
|
6962
|
+
console.error(import_picocolors11.default.red(`\u2717 No theme.yml at ${join26(dir, "theme.yml")}`));
|
|
5879
6963
|
process.exit(1);
|
|
5880
6964
|
}
|
|
5881
6965
|
let matcher;
|
|
5882
6966
|
if (opts.matcherType === "hex_distance") {
|
|
5883
6967
|
if (!/^#[0-9A-Fa-f]{6}$/.test(opts.target)) {
|
|
5884
|
-
console.error(
|
|
6968
|
+
console.error(import_picocolors11.default.red(`\u2717 --target must be a 6-digit hex for hex_distance matcher, got: ${opts.target}`));
|
|
5885
6969
|
process.exit(1);
|
|
5886
6970
|
}
|
|
5887
6971
|
matcher = { type: "hex_distance", target: opts.target, threshold: parseInt(opts.threshold, 10) };
|
|
@@ -5890,7 +6974,7 @@ function registerHarnessCommand(program2) {
|
|
|
5890
6974
|
} else if (opts.matcherType === "inline_style") {
|
|
5891
6975
|
matcher = { type: "inline_style", pattern: opts.target };
|
|
5892
6976
|
} else {
|
|
5893
|
-
console.error(
|
|
6977
|
+
console.error(import_picocolors11.default.red(`\u2717 --matcher-type must be hex_distance | class_pattern | inline_style, got: ${opts.matcherType}`));
|
|
5894
6978
|
process.exit(1);
|
|
5895
6979
|
}
|
|
5896
6980
|
const rule2 = promoteToRegression(dir, {
|
|
@@ -5900,30 +6984,30 @@ function registerHarnessCommand(program2) {
|
|
|
5900
6984
|
matcher,
|
|
5901
6985
|
now: isoNow()
|
|
5902
6986
|
});
|
|
5903
|
-
console.log(
|
|
6987
|
+
console.log(import_picocolors11.default.green(`\u2713 Promoted to regression-set as ${rule2.id}`));
|
|
5904
6988
|
console.log(` matcher: ${JSON.stringify(matcher)}`);
|
|
5905
6989
|
console.log(` reason: ${rule2.reason}`);
|
|
5906
|
-
console.log(
|
|
6990
|
+
console.log(import_picocolors11.default.dim(` Rule now runs against every future grade. Delete from regression-set.jsonl to reverse.`));
|
|
5907
6991
|
});
|
|
5908
6992
|
harness.command("dismiss <slug> <finding-id>").description(
|
|
5909
6993
|
"Dismiss a finding from the theme's next grade run (this_run) or permanently for this theme (this_theme). Default scope: this_run \u2014 dismissal applies once, then is consumed. Nothing regresses silently."
|
|
5910
6994
|
).requiredOption("-r, --reason <text>", "Why this finding is not a real defect. Persisted to dismissals.jsonl and shown in every future run's report.").option("-s, --scope <scope>", "this_run | this_theme (default: this_run)", "this_run").option("--themes-dir <path>", "Root directory containing per-theme folders", DEFAULT_THEMES_DIR).action((slug, findingId, opts) => {
|
|
5911
6995
|
const dir = resolveThemeDir(slug, opts);
|
|
5912
|
-
if (!
|
|
5913
|
-
console.error(
|
|
6996
|
+
if (!existsSync19(join26(dir, "theme.yml"))) {
|
|
6997
|
+
console.error(import_picocolors11.default.red(`\u2717 No theme.yml at ${join26(dir, "theme.yml")}`));
|
|
5914
6998
|
process.exit(1);
|
|
5915
6999
|
}
|
|
5916
7000
|
if (opts.scope !== "this_run" && opts.scope !== "this_theme") {
|
|
5917
|
-
console.error(
|
|
7001
|
+
console.error(import_picocolors11.default.red(`\u2717 --scope must be "this_run" or "this_theme", got: ${opts.scope}`));
|
|
5918
7002
|
process.exit(1);
|
|
5919
7003
|
}
|
|
5920
7004
|
const d = writeDismissal(dir, findingId, opts.reason, opts.scope, isoNow());
|
|
5921
|
-
console.log(
|
|
7005
|
+
console.log(import_picocolors11.default.green(`\u2713 Dismissed ${findingId} for ${slug} (scope: ${d.scope})`));
|
|
5922
7006
|
console.log(` reason: ${d.reason}`);
|
|
5923
7007
|
if (d.scope === "this_run") {
|
|
5924
|
-
console.log(
|
|
7008
|
+
console.log(import_picocolors11.default.dim(` Applies to the next grade run only. If ${findingId} fires again after that, it will BLOCK.`));
|
|
5925
7009
|
} else {
|
|
5926
|
-
console.log(
|
|
7010
|
+
console.log(import_picocolors11.default.dim(` Applies to every future grade run for this theme. Delete the entry from dismissals.jsonl to reverse.`));
|
|
5927
7011
|
}
|
|
5928
7012
|
});
|
|
5929
7013
|
harness.command("waivers").description(
|
|
@@ -5933,10 +7017,10 @@ function registerHarnessCommand(program2) {
|
|
|
5933
7017
|
const waivers = collectWaivers(target);
|
|
5934
7018
|
const today = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
|
|
5935
7019
|
if (waivers.length === 0) {
|
|
5936
|
-
console.log(
|
|
7020
|
+
console.log(import_picocolors11.default.green(`\u2713 No slop-override waivers found in ${target}.`));
|
|
5937
7021
|
return;
|
|
5938
7022
|
}
|
|
5939
|
-
console.log(
|
|
7023
|
+
console.log(import_picocolors11.default.bold(`\u25C7 Waivers \u2014 ${waivers.length} static override${waivers.length === 1 ? "" : "s"} across the tree`));
|
|
5940
7024
|
console.log("");
|
|
5941
7025
|
const byRule = /* @__PURE__ */ new Map();
|
|
5942
7026
|
for (const w of waivers) {
|
|
@@ -5944,37 +7028,43 @@ function registerHarnessCommand(program2) {
|
|
|
5944
7028
|
byRule.get(w.ruleId).push(w);
|
|
5945
7029
|
}
|
|
5946
7030
|
for (const [ruleId, ws] of byRule) {
|
|
5947
|
-
console.log(
|
|
7031
|
+
console.log(import_picocolors11.default.yellow(` ${ruleId}`) + import_picocolors11.default.dim(` \u2014 ${ws.length} waiver${ws.length === 1 ? "" : "s"}`));
|
|
5948
7032
|
for (const w of ws.slice(0, 8)) {
|
|
5949
|
-
const scope = w.scope === "file" ?
|
|
5950
|
-
const reason = w.reason ||
|
|
5951
|
-
console.log(` ${scope} ${
|
|
7033
|
+
const scope = w.scope === "file" ? import_picocolors11.default.magenta("[file]") : import_picocolors11.default.cyan("[local]");
|
|
7034
|
+
const reason = w.reason || import_picocolors11.default.dim("(no reason given)");
|
|
7035
|
+
console.log(` ${scope} ${import_picocolors11.default.cyan(w.file + ":" + w.line)}`);
|
|
5952
7036
|
console.log(` ${reason}`);
|
|
5953
7037
|
}
|
|
5954
|
-
if (ws.length > 8) console.log(
|
|
7038
|
+
if (ws.length > 8) console.log(import_picocolors11.default.dim(` \u2026 +${ws.length - 8} more`));
|
|
5955
7039
|
}
|
|
5956
7040
|
console.log("");
|
|
5957
7041
|
if (opts.write) {
|
|
5958
7042
|
const path2 = writeDigest(target, waivers, today);
|
|
5959
|
-
if (path2) console.log(
|
|
7043
|
+
if (path2) console.log(import_picocolors11.default.dim(` Digest written to ${path2.replace(target + "/", "")}`));
|
|
5960
7044
|
}
|
|
5961
7045
|
});
|
|
5962
7046
|
}
|
|
5963
7047
|
|
|
5964
7048
|
// src/index.ts
|
|
5965
|
-
var
|
|
7049
|
+
var import_picocolors12 = __toESM(require_picocolors(), 1);
|
|
5966
7050
|
var program = new Command();
|
|
5967
7051
|
program.name("stylecrib").description(
|
|
5968
7052
|
"Design upgrade for vibe-coded React apps. Installs 19 Claude Code subagents (craft reviewers, enforcer, exec auditors) + a chosen theme from a 118-theme library into your repo. See https://github.com/frischermf/stylecrib for the mandate."
|
|
5969
|
-
).version(
|
|
7053
|
+
).version(VERSION);
|
|
5970
7054
|
program.command("init").description(
|
|
5971
7055
|
"Interactive wizard: pick a direction, describe references, drop the agents into your repo, and install the enforcement layer (pre-commit hook + .cursorrules + GitHub Action). The hook is what makes the agents actually fire \u2014 installed by default. Pass --no-hooks to skip."
|
|
5972
7056
|
).option(
|
|
5973
7057
|
"-s, --style <direction>",
|
|
5974
|
-
"Skip the
|
|
7058
|
+
"Skip the direction picker and use a canonical theme (soft-modern, brutalist, signal-dark, newsletter, \u2026 \u2014 an unknown slug is an error, not a fallback). Run `stylecrib init --help` or browse themes/ for the full list."
|
|
5975
7059
|
).option(
|
|
5976
7060
|
"-y, --yes",
|
|
5977
|
-
"
|
|
7061
|
+
"Non-interactive: no prompts at all. Takes --style (or soft-modern), the directory name as the project name, and no references \u2014 which keeps the run on the canonical path, so it makes no Anthropic call and needs no API key. Implied when there's no terminal attached."
|
|
7062
|
+
).option(
|
|
7063
|
+
"--apply",
|
|
7064
|
+
"Apply staged files without asking. Overwrites tailwind.config.* and src/index.css on a new git branch."
|
|
7065
|
+
).option(
|
|
7066
|
+
"--no-apply",
|
|
7067
|
+
"Stage into .design-upgrade/ and stop. Review, then run `stylecrib apply` yourself. This is the default when running non-interactively against a project that already exists."
|
|
5978
7068
|
).option(
|
|
5979
7069
|
"--target <path>",
|
|
5980
7070
|
"Target project root (defaults to current working directory)"
|
|
@@ -5989,7 +7079,7 @@ program.command("init").description(
|
|
|
5989
7079
|
"Skip installing @stylecrib/react + scaffolding src/lib/stylecrib-wire.tsx. Default-on: init installs the runtime pack and writes a wire-up file the customer imports into their App.tsx. Opt out for repos that have the runtime wired already."
|
|
5990
7080
|
).option(
|
|
5991
7081
|
"--force-runtime",
|
|
5992
|
-
"Overwrite src/lib/stylecrib-wire.tsx if it exists. Existing content is backed up to `src/lib/stylecrib-wire.tsx.pre-
|
|
7082
|
+
"Overwrite src/lib/stylecrib-wire.tsx if it exists. Existing content is backed up to `src/lib/stylecrib-wire.tsx.pre-stylecrib.bak` first."
|
|
5993
7083
|
).action(init);
|
|
5994
7084
|
program.command("apply").description(
|
|
5995
7085
|
"Apply staged changes from .design-upgrade/ to your project (on a new git branch)."
|
|
@@ -6027,6 +7117,27 @@ program.command("install-hooks").description(
|
|
|
6027
7117
|
program.command("hook-check", { hidden: true }).description(
|
|
6028
7118
|
"Internal \u2014 reads Claude Code PostToolUse hook JSON from stdin, forwards to `stylecrib check` on the touched file. Wired by install-hooks. Not intended for direct invocation."
|
|
6029
7119
|
).action(hookCheck);
|
|
7120
|
+
program.command("verify").description(
|
|
7121
|
+
"Runtime smoke gate \u2014 typecheck the project, then boot its dev server and fetch a route to prove the app still renders. Catches the class of regression a diff-scoped detector cannot see: a sweep that leaves the source looking plausible but the app broken (a type error, a bad import, an empty render). Run it after an AI sweep and before a push \u2014 NOT on every commit, it takes tens of seconds."
|
|
7122
|
+
).option(
|
|
7123
|
+
"--target <path>",
|
|
7124
|
+
"Project root (defaults to current working directory)"
|
|
7125
|
+
).option(
|
|
7126
|
+
"--no-typecheck",
|
|
7127
|
+
"Skip the typecheck step."
|
|
7128
|
+
).option(
|
|
7129
|
+
"--no-boot",
|
|
7130
|
+
"Skip the dev-server boot. Leaves only the typecheck \u2014 useful in CI without a browser."
|
|
7131
|
+
).option(
|
|
7132
|
+
"--route <path>",
|
|
7133
|
+
"Route to fetch once the server is up (default: /)"
|
|
7134
|
+
).option(
|
|
7135
|
+
"--screenshot <file>",
|
|
7136
|
+
"Also capture a headless-Chrome screenshot of the route to this path."
|
|
7137
|
+
).option(
|
|
7138
|
+
"--timeout <ms>",
|
|
7139
|
+
"How long to wait for the dev server to announce a URL (default: 60000)"
|
|
7140
|
+
).action(verify);
|
|
6030
7141
|
program.command("status").description(
|
|
6031
7142
|
"Print which enforcement layers are active in this repo. Verifies the hook is installed + executable + wired, agents are present, .cursorrules exists, GH Action wired, feedback memory initialized, THEME.md present, and ANTHROPIC_API_KEY set. Use any time to confirm the loop will actually fire."
|
|
6032
7143
|
).option(
|
|
@@ -6057,12 +7168,15 @@ program.command("reject").description(
|
|
|
6057
7168
|
target: opts.target
|
|
6058
7169
|
});
|
|
6059
7170
|
});
|
|
7171
|
+
program.command("mcp").description(
|
|
7172
|
+
"Run StyleCrib as an MCP server over stdio, so Cursor, Windsurf, Claude Desktop, Zed, or any other MCP client can list themes, fetch a theme's brief, generate its Tailwind tokens, read the rule catalog, and run the slop detectors on a file \u2014 without Claude Code or the pre-commit hook. Point your client's MCP config at `npx -y @stylecrib/cli mcp`."
|
|
7173
|
+
).action(mcp);
|
|
6060
7174
|
registerHarnessCommand(program);
|
|
6061
7175
|
program.parseAsync(process.argv).catch((err) => {
|
|
6062
7176
|
console.error("");
|
|
6063
|
-
console.error(
|
|
7177
|
+
console.error(import_picocolors12.default.red("\u2717 ") + (err instanceof Error ? err.message : String(err)));
|
|
6064
7178
|
if (err instanceof Error && err.cause) {
|
|
6065
|
-
console.error(
|
|
7179
|
+
console.error(import_picocolors12.default.dim(" caused by: " + String(err.cause)));
|
|
6066
7180
|
}
|
|
6067
7181
|
process.exit(1);
|
|
6068
7182
|
});
|