@stylecrib/cli 0.3.1 → 0.3.3
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 +1606 -467
- package/package.json +12 -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/THEME.md +125 -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/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/BUILD_BRIEF.md +171 -0
- package/themes/bento-modular/THEME.md +80 -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/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/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/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/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/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/THEME.md +167 -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/THEME.md +144 -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/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/BUILD_BRIEF.md +156 -0
- package/themes/kinetic-headline/THEME.md +78 -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/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/BUILD_BRIEF.md +158 -0
- package/themes/nature-distilled/THEME.md +81 -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/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/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/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/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/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/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/THEME.md +167 -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/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.3" : "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) {
|
|
@@ -3349,12 +3846,35 @@ function runAllRegressionDetectors(file, oldBody, newBody) {
|
|
|
3349
3846
|
|
|
3350
3847
|
// src/commands/check.ts
|
|
3351
3848
|
init_slop_rules();
|
|
3849
|
+
|
|
3850
|
+
// src/lib/verdict.ts
|
|
3851
|
+
init_esm_shims();
|
|
3852
|
+
var VERDICT_LINE = /^[\s#>*_`-]*(verdict)[\s*_`]*:[\s*_`]*(pass|block)\b/i;
|
|
3853
|
+
function parseVerdict(text2) {
|
|
3854
|
+
const lines = text2.split(/\r?\n/);
|
|
3855
|
+
let lastAnyCase = null;
|
|
3856
|
+
for (let i = lines.length - 1; i >= 0; i--) {
|
|
3857
|
+
const m = VERDICT_LINE.exec(lines[i]);
|
|
3858
|
+
if (!m) continue;
|
|
3859
|
+
const hit = { verdict: m[2].toUpperCase(), line: lines[i].trim() };
|
|
3860
|
+
if (m[1] === "VERDICT") return { ...hit, found: true };
|
|
3861
|
+
lastAnyCase ??= hit;
|
|
3862
|
+
}
|
|
3863
|
+
if (lastAnyCase) return { ...lastAnyCase, found: true };
|
|
3864
|
+
return {
|
|
3865
|
+
verdict: "BLOCK",
|
|
3866
|
+
found: false,
|
|
3867
|
+
reason: 'no final "VERDICT: PASS" or "VERDICT: BLOCK" line in the reply'
|
|
3868
|
+
};
|
|
3869
|
+
}
|
|
3870
|
+
|
|
3871
|
+
// src/commands/check.ts
|
|
3352
3872
|
function existsInHead(target, rel) {
|
|
3353
3873
|
const r = spawnSync("git", ["cat-file", "-e", `HEAD:${rel}`], { cwd: target, stdio: "ignore" });
|
|
3354
3874
|
return r.status === 0;
|
|
3355
3875
|
}
|
|
3356
3876
|
async function check(opts = {}) {
|
|
3357
|
-
const target =
|
|
3877
|
+
const target = resolve6(opts.target ?? process.cwd());
|
|
3358
3878
|
const apiKey = process.env.ANTHROPIC_API_KEY;
|
|
3359
3879
|
const changed = detectChangedFiles(target, opts);
|
|
3360
3880
|
const changedApp = detectChangedAppFiles(target, opts);
|
|
@@ -3367,7 +3887,7 @@ async function check(opts = {}) {
|
|
|
3367
3887
|
const precedentMisses = [];
|
|
3368
3888
|
for (const rel of changed) {
|
|
3369
3889
|
if (!isComponentFile(rel)) continue;
|
|
3370
|
-
const body = readSafe(
|
|
3890
|
+
const body = readSafe(join10(target, rel));
|
|
3371
3891
|
if (!body) continue;
|
|
3372
3892
|
const hasDeclaration = /\/\/\s*Composition study:/i.test(body);
|
|
3373
3893
|
if (!hasDeclaration) {
|
|
@@ -3413,7 +3933,7 @@ async function check(opts = {}) {
|
|
|
3413
3933
|
const fullScan = process.env.STYLECRIB_LINT_FULL === "1";
|
|
3414
3934
|
for (const rel of changed) {
|
|
3415
3935
|
if (!isComponentFile(rel)) continue;
|
|
3416
|
-
const body = readSafe(
|
|
3936
|
+
const body = readSafe(join10(target, rel));
|
|
3417
3937
|
if (!body) continue;
|
|
3418
3938
|
const newHits = runMonoTrackedCapsCheck(rel, body);
|
|
3419
3939
|
if (fullScan) {
|
|
@@ -3444,22 +3964,23 @@ async function check(opts = {}) {
|
|
|
3444
3964
|
);
|
|
3445
3965
|
process.exit(1);
|
|
3446
3966
|
}
|
|
3967
|
+
const theme = themeContextForTarget(target);
|
|
3447
3968
|
const allHits = [];
|
|
3448
3969
|
for (const rel of changed) {
|
|
3449
3970
|
if (!looksLikeUiFile(rel)) continue;
|
|
3450
|
-
const body = readSafe(
|
|
3971
|
+
const body = readSafe(join10(target, rel));
|
|
3451
3972
|
if (!body) continue;
|
|
3452
|
-
const newHits = runAllTextDetectors(rel, body);
|
|
3973
|
+
const newHits = runAllTextDetectors(rel, body, theme);
|
|
3453
3974
|
if (fullScan) {
|
|
3454
3975
|
allHits.push(...newHits);
|
|
3455
3976
|
continue;
|
|
3456
3977
|
}
|
|
3457
3978
|
const oldBody = readHeadBody(target, rel);
|
|
3458
|
-
const oldHits = oldBody ? runAllTextDetectors(rel, oldBody) : [];
|
|
3979
|
+
const oldHits = oldBody ? runAllTextDetectors(rel, oldBody, theme) : [];
|
|
3459
3980
|
allHits.push(...subtractHitsBySample(newHits, oldHits));
|
|
3460
3981
|
}
|
|
3461
3982
|
for (const rel of changedApp) {
|
|
3462
|
-
const body = readSafe(
|
|
3983
|
+
const body = readSafe(join10(target, rel));
|
|
3463
3984
|
if (!body) continue;
|
|
3464
3985
|
const oldBody = readHeadBody(target, rel);
|
|
3465
3986
|
if (oldBody === null) continue;
|
|
@@ -3538,7 +4059,7 @@ async function check(opts = {}) {
|
|
|
3538
4059
|
console.error(import_picocolors4.default.dim(' STYLECRIB_SKIP_QA="<one-line reason>" git commit'));
|
|
3539
4060
|
process.exit(1);
|
|
3540
4061
|
}
|
|
3541
|
-
const conductor = readSafe(
|
|
4062
|
+
const conductor = readSafe(join10(target, ".claude/agents/qa-conductor.md"));
|
|
3542
4063
|
if (!conductor) {
|
|
3543
4064
|
console.error(import_picocolors4.default.red("\u2717 ") + "QA Conductor agent not installed.");
|
|
3544
4065
|
console.error(
|
|
@@ -3548,8 +4069,8 @@ async function check(opts = {}) {
|
|
|
3548
4069
|
);
|
|
3549
4070
|
process.exit(1);
|
|
3550
4071
|
}
|
|
3551
|
-
const direction = readSafe(
|
|
3552
|
-
const feedback = readSafe(
|
|
4072
|
+
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.)";
|
|
4073
|
+
const feedback = readSafe(join10(target, ".stylecrib/feedback.md"));
|
|
3553
4074
|
if (!opts.silent) {
|
|
3554
4075
|
const route = apiKey ? "Anthropic API" : "Claude Code CLI";
|
|
3555
4076
|
console.log(
|
|
@@ -3560,7 +4081,7 @@ async function check(opts = {}) {
|
|
|
3560
4081
|
for (const f of changed) console.log(import_picocolors4.default.dim(" \xB7 " + f));
|
|
3561
4082
|
}
|
|
3562
4083
|
const fileContents = changed.map((rel) => {
|
|
3563
|
-
const path2 =
|
|
4084
|
+
const path2 = join10(target, rel);
|
|
3564
4085
|
const body = readSafe(path2);
|
|
3565
4086
|
return body ? `## ${rel}
|
|
3566
4087
|
|
|
@@ -3628,14 +4149,17 @@ ${userText}`;
|
|
|
3628
4149
|
process.exit(2);
|
|
3629
4150
|
}
|
|
3630
4151
|
const elapsed = ((Date.now() - t0) / 1e3).toFixed(1);
|
|
3631
|
-
const
|
|
3632
|
-
const verdict =
|
|
4152
|
+
const parsed = parseVerdict(verdictText);
|
|
4153
|
+
const verdict = parsed.verdict;
|
|
3633
4154
|
if (!opts.silent) {
|
|
3634
4155
|
console.log("");
|
|
3635
4156
|
console.log(verdictText);
|
|
3636
4157
|
console.log("");
|
|
3637
4158
|
console.log(import_picocolors4.default.dim(`\u25C7 Loop completed in ${elapsed}s`));
|
|
3638
4159
|
}
|
|
4160
|
+
if (!parsed.found) {
|
|
4161
|
+
console.error(import_picocolors4.default.red("\u2717 ") + `Conductor reply had ${parsed.reason} \u2014 treating as BLOCK.`);
|
|
4162
|
+
}
|
|
3639
4163
|
if (verdict === "PASS") {
|
|
3640
4164
|
if (!opts.silent) console.log(import_picocolors4.default.green("\u2713 PASS \u2014 cleared to commit."));
|
|
3641
4165
|
process.exit(0);
|
|
@@ -3690,7 +4214,7 @@ function looksLikeAppFile(path2) {
|
|
|
3690
4214
|
}
|
|
3691
4215
|
function logQaBypass(target, files, kind, reason) {
|
|
3692
4216
|
try {
|
|
3693
|
-
const dir =
|
|
4217
|
+
const dir = join10(target, "reports");
|
|
3694
4218
|
mkdirSync3(dir, { recursive: true });
|
|
3695
4219
|
ensureReportsReadme(dir);
|
|
3696
4220
|
const record = {
|
|
@@ -3706,13 +4230,13 @@ function logQaBypass(target, files, kind, reason) {
|
|
|
3706
4230
|
}
|
|
3707
4231
|
})()
|
|
3708
4232
|
};
|
|
3709
|
-
appendFileSync(
|
|
4233
|
+
appendFileSync(join10(dir, "qa-bypasses.jsonl"), JSON.stringify(record) + "\n");
|
|
3710
4234
|
} catch {
|
|
3711
4235
|
}
|
|
3712
4236
|
}
|
|
3713
4237
|
function ensureReportsReadme(reportsDir) {
|
|
3714
|
-
const path2 =
|
|
3715
|
-
if (
|
|
4238
|
+
const path2 = join10(reportsDir, "README.md");
|
|
4239
|
+
if (existsSync5(path2)) return;
|
|
3716
4240
|
const body = `# reports/
|
|
3717
4241
|
|
|
3718
4242
|
This directory is written by the StyleCrib harness. Files here are not
|
|
@@ -3817,8 +4341,8 @@ function isComponentFile(path2) {
|
|
|
3817
4341
|
}
|
|
3818
4342
|
function readSafe(path2) {
|
|
3819
4343
|
try {
|
|
3820
|
-
if (!
|
|
3821
|
-
return
|
|
4344
|
+
if (!existsSync5(path2)) return null;
|
|
4345
|
+
return readFileSync4(path2, "utf8");
|
|
3822
4346
|
} catch {
|
|
3823
4347
|
return null;
|
|
3824
4348
|
}
|
|
@@ -3839,7 +4363,7 @@ function findClaudeCli() {
|
|
|
3839
4363
|
"/opt/homebrew/bin/claude"
|
|
3840
4364
|
];
|
|
3841
4365
|
for (const p of candidates) {
|
|
3842
|
-
if (
|
|
4366
|
+
if (existsSync5(p)) return p;
|
|
3843
4367
|
}
|
|
3844
4368
|
return null;
|
|
3845
4369
|
}
|
|
@@ -3847,21 +4371,21 @@ function findClaudeCli() {
|
|
|
3847
4371
|
// src/commands/review-site.ts
|
|
3848
4372
|
init_esm_shims();
|
|
3849
4373
|
var import_picocolors5 = __toESM(require_picocolors(), 1);
|
|
3850
|
-
import { readFileSync as
|
|
3851
|
-
import { resolve as
|
|
4374
|
+
import { readFileSync as readFileSync5, readdirSync as readdirSync2, statSync, mkdirSync as mkdirSync4, writeFileSync as writeFileSync4 } from "fs";
|
|
4375
|
+
import { resolve as resolve7, join as join11, relative as relative4 } from "path";
|
|
3852
4376
|
init_slop_rules();
|
|
3853
4377
|
function walkUiFiles(siteRoot) {
|
|
3854
4378
|
const out = [];
|
|
3855
4379
|
const visit = (dir) => {
|
|
3856
4380
|
let entries;
|
|
3857
4381
|
try {
|
|
3858
|
-
entries =
|
|
4382
|
+
entries = readdirSync2(dir);
|
|
3859
4383
|
} catch {
|
|
3860
4384
|
return;
|
|
3861
4385
|
}
|
|
3862
4386
|
for (const name of entries) {
|
|
3863
4387
|
if (name.startsWith(".") || name === "node_modules" || name === "dist" || name === "build") continue;
|
|
3864
|
-
const p =
|
|
4388
|
+
const p = join11(dir, name);
|
|
3865
4389
|
let s;
|
|
3866
4390
|
try {
|
|
3867
4391
|
s = statSync(p);
|
|
@@ -3882,22 +4406,23 @@ function walkUiFiles(siteRoot) {
|
|
|
3882
4406
|
return out;
|
|
3883
4407
|
}
|
|
3884
4408
|
async function reviewSite(opts = {}) {
|
|
3885
|
-
const target =
|
|
3886
|
-
const siteRoot =
|
|
3887
|
-
const outDir =
|
|
4409
|
+
const target = resolve7(opts.target ?? process.cwd());
|
|
4410
|
+
const siteRoot = resolve7(opts.siteRoot ?? join11(target, "site/src"));
|
|
4411
|
+
const outDir = resolve7(opts.outDir ?? join11(target, "reports"));
|
|
3888
4412
|
console.log(import_picocolors5.default.dim(`Walking ${siteRoot} for UI files\u2026`));
|
|
3889
4413
|
const files = walkUiFiles(siteRoot);
|
|
3890
4414
|
console.log(import_picocolors5.default.dim(` ${files.length} files found. Running structural slop detectors\u2026`));
|
|
4415
|
+
const sweepTheme = themeContextForTarget(resolve7(target));
|
|
3891
4416
|
const allHits = [];
|
|
3892
4417
|
for (const abs of files) {
|
|
3893
4418
|
let body;
|
|
3894
4419
|
try {
|
|
3895
|
-
body =
|
|
4420
|
+
body = readFileSync5(abs, "utf8");
|
|
3896
4421
|
} catch {
|
|
3897
4422
|
continue;
|
|
3898
4423
|
}
|
|
3899
|
-
const rel = relative4(
|
|
3900
|
-
allHits.push(...runAllTextDetectors(rel, body));
|
|
4424
|
+
const rel = relative4(resolve7(target), abs);
|
|
4425
|
+
allHits.push(...runAllTextDetectors(rel, body, sweepTheme));
|
|
3901
4426
|
}
|
|
3902
4427
|
const byRule = /* @__PURE__ */ new Map();
|
|
3903
4428
|
const byFile = /* @__PURE__ */ new Map();
|
|
@@ -3919,8 +4444,14 @@ async function reviewSite(opts = {}) {
|
|
|
3919
4444
|
lines.push(`Block-severity hits: **${blocks}**`);
|
|
3920
4445
|
lines.push(`Warn-severity hits: **${warns}**`);
|
|
3921
4446
|
lines.push("");
|
|
3922
|
-
if (
|
|
3923
|
-
lines.push(
|
|
4447
|
+
if (files.length === 0) {
|
|
4448
|
+
lines.push(
|
|
4449
|
+
"**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`."
|
|
4450
|
+
);
|
|
4451
|
+
} else if (blocks === 0 && warns === 0) {
|
|
4452
|
+
lines.push(
|
|
4453
|
+
`**No slop detected.** All ${files.length} UI file(s) under the scan root pass the structural gate.`
|
|
4454
|
+
);
|
|
3924
4455
|
} else {
|
|
3925
4456
|
lines.push("## Hits by rule");
|
|
3926
4457
|
lines.push("");
|
|
@@ -3947,9 +4478,9 @@ async function reviewSite(opts = {}) {
|
|
|
3947
4478
|
}
|
|
3948
4479
|
}
|
|
3949
4480
|
mkdirSync4(outDir, { recursive: true });
|
|
3950
|
-
const outPath =
|
|
4481
|
+
const outPath = join11(outDir, `nightly-slop-${today}.md`);
|
|
3951
4482
|
writeFileSync4(outPath, lines.join("\n") + "\n");
|
|
3952
|
-
writeFileSync4(
|
|
4483
|
+
writeFileSync4(join11(outDir, "nightly-slop-latest.md"), lines.join("\n") + "\n");
|
|
3953
4484
|
console.log("");
|
|
3954
4485
|
console.log(import_picocolors5.default.dim(`Wrote ${outPath}`));
|
|
3955
4486
|
console.log(import_picocolors5.default.dim(` ${blocks} block-severity, ${warns} warn-severity`));
|
|
@@ -3958,8 +4489,8 @@ async function reviewSite(opts = {}) {
|
|
|
3958
4489
|
// src/commands/rule.ts
|
|
3959
4490
|
init_esm_shims();
|
|
3960
4491
|
var import_picocolors6 = __toESM(require_picocolors(), 1);
|
|
3961
|
-
import { readFileSync as
|
|
3962
|
-
import { resolve as
|
|
4492
|
+
import { readFileSync as readFileSync6, writeFileSync as writeFileSync5, existsSync as existsSync6, mkdirSync as mkdirSync5 } from "fs";
|
|
4493
|
+
import { resolve as resolve8, join as join12 } from "path";
|
|
3963
4494
|
import { input as input2, select as select2 } from "@inquirer/prompts";
|
|
3964
4495
|
var CATEGORIES = [
|
|
3965
4496
|
"chrome",
|
|
@@ -3973,7 +4504,8 @@ var CATEGORIES = [
|
|
|
3973
4504
|
"provenance"
|
|
3974
4505
|
];
|
|
3975
4506
|
async function ruleAdd(opts = {}) {
|
|
3976
|
-
const target =
|
|
4507
|
+
const target = resolve8(opts.target ?? process.cwd());
|
|
4508
|
+
assertInteractive("`stylecrib rule add`");
|
|
3977
4509
|
console.log(import_picocolors6.default.bold("Add a slop rule to this repo's custom rulebook."));
|
|
3978
4510
|
console.log(import_picocolors6.default.dim(" Custom rules live at .stylecrib/custom-rules.ts and load automatically."));
|
|
3979
4511
|
console.log("");
|
|
@@ -4022,10 +4554,10 @@ async function ruleAdd(opts = {}) {
|
|
|
4022
4554
|
message: "Sample fix \u2014 what to write instead:",
|
|
4023
4555
|
validate: (v) => v.length > 0 || "Write the corrected version so authors know the right pattern."
|
|
4024
4556
|
});
|
|
4025
|
-
const dir =
|
|
4557
|
+
const dir = join12(target, ".stylecrib");
|
|
4026
4558
|
mkdirSync5(dir, { recursive: true });
|
|
4027
|
-
const path2 =
|
|
4028
|
-
const existing =
|
|
4559
|
+
const path2 = join12(dir, "custom-rules.ts");
|
|
4560
|
+
const existing = existsSync6(path2) ? readFileSync6(path2, "utf8") : SEED_CUSTOM_RULES;
|
|
4029
4561
|
const insertion = `
|
|
4030
4562
|
// \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
4563
|
// ${description}
|
|
@@ -4059,13 +4591,13 @@ async function ruleAdd(opts = {}) {
|
|
|
4059
4591
|
console.log(import_picocolors6.default.dim(` To share with the community: stylecrib rule share ${id}`));
|
|
4060
4592
|
}
|
|
4061
4593
|
async function ruleShare(id, opts = {}) {
|
|
4062
|
-
const target =
|
|
4063
|
-
const path2 =
|
|
4064
|
-
if (!
|
|
4594
|
+
const target = resolve8(opts.target ?? process.cwd());
|
|
4595
|
+
const path2 = join12(target, ".stylecrib", "custom-rules.ts");
|
|
4596
|
+
if (!existsSync6(path2)) {
|
|
4065
4597
|
console.error(import_picocolors6.default.red("\u2717 ") + ".stylecrib/custom-rules.ts not found. Run `stylecrib rule add` first.");
|
|
4066
4598
|
process.exit(1);
|
|
4067
4599
|
}
|
|
4068
|
-
const body =
|
|
4600
|
+
const body = readFileSync6(path2, "utf8");
|
|
4069
4601
|
const start = body.indexOf(`id: "${id}"`);
|
|
4070
4602
|
if (start < 0) {
|
|
4071
4603
|
console.error(import_picocolors6.default.red("\u2717 ") + `Rule ${id} not found in .stylecrib/custom-rules.ts.`);
|
|
@@ -4163,11 +4695,11 @@ function extractFilePaths(event) {
|
|
|
4163
4695
|
// src/commands/audit.ts
|
|
4164
4696
|
init_esm_shims();
|
|
4165
4697
|
var import_picocolors7 = __toESM(require_picocolors(), 1);
|
|
4166
|
-
import { existsSync as
|
|
4167
|
-
import { resolve as
|
|
4698
|
+
import { existsSync as existsSync7, readdirSync as readdirSync3, readFileSync as readFileSync7, statSync as statSync2 } from "fs";
|
|
4699
|
+
import { resolve as resolve9, join as join13, basename as basename2 } from "path";
|
|
4168
4700
|
import Anthropic3 from "@anthropic-ai/sdk";
|
|
4169
4701
|
async function audit(opts = {}) {
|
|
4170
|
-
const target =
|
|
4702
|
+
const target = resolve9(opts.target ?? process.cwd());
|
|
4171
4703
|
const apiKey = process.env.ANTHROPIC_API_KEY;
|
|
4172
4704
|
if (!apiKey) {
|
|
4173
4705
|
console.error(import_picocolors7.default.red("\u2717 ") + "ANTHROPIC_API_KEY not set.");
|
|
@@ -4187,8 +4719,8 @@ async function audit(opts = {}) {
|
|
|
4187
4719
|
);
|
|
4188
4720
|
process.exit(1);
|
|
4189
4721
|
}
|
|
4190
|
-
const usingBundled = !
|
|
4191
|
-
|
|
4722
|
+
const usingBundled = !existsSync7(
|
|
4723
|
+
join13(target, ".claude/agents/homogeneity-detector.md")
|
|
4192
4724
|
);
|
|
4193
4725
|
if (usingBundled && !opts.silent) {
|
|
4194
4726
|
console.log(
|
|
@@ -4197,7 +4729,7 @@ async function audit(opts = {}) {
|
|
|
4197
4729
|
)
|
|
4198
4730
|
);
|
|
4199
4731
|
}
|
|
4200
|
-
const directionDoctrine = readSafe2(
|
|
4732
|
+
const directionDoctrine = readSafe2(join13(target, "design-system/THEME.md")) ?? "";
|
|
4201
4733
|
if (!opts.silent) {
|
|
4202
4734
|
console.log(import_picocolors7.default.bold("\u25C7 StyleCrib audit \u2014 whole-project drift report"));
|
|
4203
4735
|
console.log("");
|
|
@@ -4225,7 +4757,7 @@ async function audit(opts = {}) {
|
|
|
4225
4757
|
);
|
|
4226
4758
|
}
|
|
4227
4759
|
const filesBody = fam.files.map((rel) => {
|
|
4228
|
-
const body = readSafe2(
|
|
4760
|
+
const body = readSafe2(join13(target, rel));
|
|
4229
4761
|
return body ? `## ${rel}
|
|
4230
4762
|
|
|
4231
4763
|
\`\`\`tsx
|
|
@@ -4263,8 +4795,7 @@ Apply your Homogeneity Detector doctrine \u2014 build the 5-axis matrix, score e
|
|
|
4263
4795
|
]
|
|
4264
4796
|
});
|
|
4265
4797
|
const report = resp.content.filter((b) => b.type === "text").map((b) => b.text).join("\n");
|
|
4266
|
-
const
|
|
4267
|
-
const verdict = verdictLine ? verdictLine[1].toUpperCase() : "BLOCK";
|
|
4798
|
+
const verdict = parseVerdict(report).verdict;
|
|
4268
4799
|
verdicts.push({ family: fam.name, verdict, report });
|
|
4269
4800
|
if (verdict === "BLOCK") anyBlock = true;
|
|
4270
4801
|
if (!opts.silent) {
|
|
@@ -4315,18 +4846,18 @@ function discoverFamilies(target, only) {
|
|
|
4315
4846
|
const out = [];
|
|
4316
4847
|
for (const c2 of candidates) {
|
|
4317
4848
|
if (only && c2.name !== only) continue;
|
|
4318
|
-
const abs =
|
|
4319
|
-
if (!
|
|
4849
|
+
const abs = join13(target, c2.dir);
|
|
4850
|
+
if (!existsSync7(abs)) continue;
|
|
4320
4851
|
if (!statSync2(abs).isDirectory()) continue;
|
|
4321
|
-
const files =
|
|
4852
|
+
const files = readdirSync3(abs).filter((f) => f.endsWith(".tsx")).map((f) => join13(c2.dir, f));
|
|
4322
4853
|
if (files.length > 0) out.push({ name: c2.name, files });
|
|
4323
4854
|
}
|
|
4324
4855
|
return out;
|
|
4325
4856
|
}
|
|
4326
4857
|
function readSafe2(path2) {
|
|
4327
4858
|
try {
|
|
4328
|
-
if (!
|
|
4329
|
-
return
|
|
4859
|
+
if (!existsSync7(path2)) return null;
|
|
4860
|
+
return readFileSync7(path2, "utf8");
|
|
4330
4861
|
} catch {
|
|
4331
4862
|
return null;
|
|
4332
4863
|
}
|
|
@@ -4335,11 +4866,11 @@ function readSafe2(path2) {
|
|
|
4335
4866
|
// src/commands/reject.ts
|
|
4336
4867
|
init_esm_shims();
|
|
4337
4868
|
var import_picocolors8 = __toESM(require_picocolors(), 1);
|
|
4338
|
-
import { existsSync as
|
|
4339
|
-
import { resolve as
|
|
4869
|
+
import { existsSync as existsSync8, mkdirSync as mkdirSync6, readFileSync as readFileSync8, writeFileSync as writeFileSync6 } from "fs";
|
|
4870
|
+
import { resolve as resolve10, join as join14, dirname as dirname6 } from "path";
|
|
4340
4871
|
async function reject(opts = {}) {
|
|
4341
|
-
const target =
|
|
4342
|
-
const path2 =
|
|
4872
|
+
const target = resolve10(opts.target ?? process.cwd());
|
|
4873
|
+
const path2 = join14(target, ".stylecrib", "feedback.md");
|
|
4343
4874
|
const pattern = opts.pattern?.trim();
|
|
4344
4875
|
const replacement = opts.replacement?.trim();
|
|
4345
4876
|
if (!pattern) {
|
|
@@ -4354,7 +4885,7 @@ async function reject(opts = {}) {
|
|
|
4354
4885
|
);
|
|
4355
4886
|
process.exit(1);
|
|
4356
4887
|
}
|
|
4357
|
-
const now = process.env.
|
|
4888
|
+
const now = process.env.STYLECRIB_NOW ?? (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
|
|
4358
4889
|
const entry = `
|
|
4359
4890
|
## Rejection \u2014 ${now}
|
|
4360
4891
|
|
|
@@ -4364,9 +4895,9 @@ async function reject(opts = {}) {
|
|
|
4364
4895
|
` : `
|
|
4365
4896
|
**Replacement:** _(not specified \u2014 propose alternatives that don't repeat this pattern)_
|
|
4366
4897
|
`);
|
|
4367
|
-
const existing =
|
|
4898
|
+
const existing = existsSync8(path2) ? readFileSync8(path2, "utf8") : HEADER_TEMPLATE;
|
|
4368
4899
|
const body = existing.trimEnd() + "\n" + entry;
|
|
4369
|
-
mkdirSync6(
|
|
4900
|
+
mkdirSync6(dirname6(path2), { recursive: true });
|
|
4370
4901
|
writeFileSync6(path2, body);
|
|
4371
4902
|
console.log(import_picocolors8.default.green("\u2713 ") + "Rejection recorded in " + import_picocolors8.default.cyan(".stylecrib/feedback.md"));
|
|
4372
4903
|
console.log("");
|
|
@@ -4397,19 +4928,19 @@ that gap. It's plain markdown \u2014 readable by humans, loaded by agents.
|
|
|
4397
4928
|
// src/commands/status.ts
|
|
4398
4929
|
init_esm_shims();
|
|
4399
4930
|
var import_picocolors9 = __toESM(require_picocolors(), 1);
|
|
4400
|
-
import { existsSync as
|
|
4401
|
-
import { resolve as
|
|
4931
|
+
import { existsSync as existsSync9, readFileSync as readFileSync9, statSync as statSync3 } from "fs";
|
|
4932
|
+
import { resolve as resolve11, join as join15 } from "path";
|
|
4402
4933
|
async function status(opts = {}) {
|
|
4403
|
-
const target =
|
|
4934
|
+
const target = resolve11(opts.target ?? process.cwd());
|
|
4404
4935
|
console.log(import_picocolors9.default.bold("\u25C7 StyleCrib enforcement status"));
|
|
4405
4936
|
console.log(import_picocolors9.default.dim(" " + target));
|
|
4406
4937
|
console.log("");
|
|
4407
4938
|
let anyFail = false;
|
|
4408
|
-
const huskyPath =
|
|
4409
|
-
const gitHookPath =
|
|
4410
|
-
const hookPath =
|
|
4411
|
-
const hookExists =
|
|
4412
|
-
const hookCallsCheck = hookExists ?
|
|
4939
|
+
const huskyPath = join15(target, ".husky", "pre-commit");
|
|
4940
|
+
const gitHookPath = join15(target, ".git", "hooks", "pre-commit");
|
|
4941
|
+
const hookPath = existsSync9(huskyPath) ? huskyPath : gitHookPath;
|
|
4942
|
+
const hookExists = existsSync9(hookPath);
|
|
4943
|
+
const hookCallsCheck = hookExists ? readFileSync9(hookPath, "utf8").includes("stylecrib check") : false;
|
|
4413
4944
|
const hookExec = hookExists ? (statSync3(hookPath).mode & 73) !== 0 : false;
|
|
4414
4945
|
const hookOk = hookExists && hookCallsCheck && hookExec;
|
|
4415
4946
|
printLine(
|
|
@@ -4418,7 +4949,7 @@ async function status(opts = {}) {
|
|
|
4418
4949
|
hookOk ? "pass" : "fail"
|
|
4419
4950
|
);
|
|
4420
4951
|
if (!hookOk) anyFail = true;
|
|
4421
|
-
const agentsDir =
|
|
4952
|
+
const agentsDir = join15(target, ".claude", "agents");
|
|
4422
4953
|
const requiredAgents = [
|
|
4423
4954
|
"design-director.md",
|
|
4424
4955
|
"design-systems-engineer.md",
|
|
@@ -4429,7 +4960,7 @@ async function status(opts = {}) {
|
|
|
4429
4960
|
"brand-precedent.md"
|
|
4430
4961
|
];
|
|
4431
4962
|
const presentAgents = requiredAgents.filter(
|
|
4432
|
-
(f) =>
|
|
4963
|
+
(f) => existsSync9(join15(agentsDir, f))
|
|
4433
4964
|
);
|
|
4434
4965
|
const agentsOk = presentAgents.length === requiredAgents.length;
|
|
4435
4966
|
printLine(
|
|
@@ -4437,24 +4968,24 @@ async function status(opts = {}) {
|
|
|
4437
4968
|
agentsOk ? `PASS (${presentAgents.length}/${requiredAgents.length})` : `WARN (${presentAgents.length}/${requiredAgents.length}) \u2014 missing: ` + requiredAgents.filter((f) => !presentAgents.includes(f)).join(", "),
|
|
4438
4969
|
agentsOk ? "pass" : "warn"
|
|
4439
4970
|
);
|
|
4440
|
-
const cursorrulesOk =
|
|
4971
|
+
const cursorrulesOk = existsSync9(join15(target, ".cursorrules"));
|
|
4441
4972
|
printLine(
|
|
4442
4973
|
".cursorrules",
|
|
4443
4974
|
cursorrulesOk ? "PASS (Cursor users routed through agents)" : "WARN: missing \u2014 Cursor users won't route through agents on this repo",
|
|
4444
4975
|
cursorrulesOk ? "pass" : "warn"
|
|
4445
4976
|
);
|
|
4446
|
-
const actionPath =
|
|
4447
|
-
const actionOk =
|
|
4977
|
+
const actionPath = join15(target, ".github", "workflows", "stylecrib-review.yml");
|
|
4978
|
+
const actionOk = existsSync9(actionPath);
|
|
4448
4979
|
printLine(
|
|
4449
4980
|
"GitHub Action (PR gating)",
|
|
4450
4981
|
actionOk ? "PASS" : "WARN: not installed \u2014 PRs won't be gated by the QA loop in CI",
|
|
4451
4982
|
actionOk ? "pass" : "warn"
|
|
4452
4983
|
);
|
|
4453
|
-
const feedbackPath =
|
|
4454
|
-
const feedbackOk =
|
|
4984
|
+
const feedbackPath = join15(target, ".stylecrib", "feedback.md");
|
|
4985
|
+
const feedbackOk = existsSync9(feedbackPath);
|
|
4455
4986
|
let rejectionCount = 0;
|
|
4456
4987
|
if (feedbackOk) {
|
|
4457
|
-
const body =
|
|
4988
|
+
const body = readFileSync9(feedbackPath, "utf8");
|
|
4458
4989
|
rejectionCount = (body.match(/^## Rejection — /gm) || []).length;
|
|
4459
4990
|
}
|
|
4460
4991
|
printLine(
|
|
@@ -4462,7 +4993,7 @@ async function status(opts = {}) {
|
|
|
4462
4993
|
feedbackOk ? `PASS (${rejectionCount} rejection${rejectionCount === 1 ? "" : "s"} recorded)` : 'INFO: no rejections recorded yet \u2014 add one with `stylecrib reject "<pattern>"`',
|
|
4463
4994
|
feedbackOk ? "pass" : "info"
|
|
4464
4995
|
);
|
|
4465
|
-
const directionOk =
|
|
4996
|
+
const directionOk = existsSync9(join15(target, "design-system", "THEME.md"));
|
|
4466
4997
|
printLine(
|
|
4467
4998
|
"design-system/THEME.md",
|
|
4468
4999
|
directionOk ? "PASS" : "FAIL: missing \u2014 agents have no project direction to pivot reviews on. Run `stylecrib init`",
|
|
@@ -4475,9 +5006,9 @@ async function status(opts = {}) {
|
|
|
4475
5006
|
apiKey ? "PASS" : "WARN: not set \u2014 `stylecrib check` will exit early; hook will warn & no-op",
|
|
4476
5007
|
apiKey ? "pass" : "warn"
|
|
4477
5008
|
);
|
|
4478
|
-
const bypassPath =
|
|
4479
|
-
if (
|
|
4480
|
-
const lines =
|
|
5009
|
+
const bypassPath = join15(target, "reports", "qa-bypasses.jsonl");
|
|
5010
|
+
if (existsSync9(bypassPath)) {
|
|
5011
|
+
const lines = readFileSync9(bypassPath, "utf8").split("\n").filter((l) => l.trim());
|
|
4481
5012
|
printLine(
|
|
4482
5013
|
"qa-bypass log",
|
|
4483
5014
|
`INFO: ${lines.length} bypass${lines.length === 1 ? "" : "es"} recorded \u2192 reports/qa-bypasses.jsonl`,
|
|
@@ -4504,20 +5035,574 @@ function printLine(label, state, level) {
|
|
|
4504
5035
|
console.log(" " + tag + import_picocolors9.default.bold(label.padEnd(28)) + import_picocolors9.default.dim(" " + state));
|
|
4505
5036
|
}
|
|
4506
5037
|
|
|
4507
|
-
// src/commands/
|
|
5038
|
+
// src/commands/verify.ts
|
|
4508
5039
|
init_esm_shims();
|
|
4509
5040
|
var import_picocolors10 = __toESM(require_picocolors(), 1);
|
|
4510
|
-
import { existsSync as
|
|
4511
|
-
import { join as
|
|
5041
|
+
import { existsSync as existsSync10, readFileSync as readFileSync10, mkdirSync as mkdirSync7 } from "fs";
|
|
5042
|
+
import { resolve as resolve12, join as join16, dirname as dirname7 } from "path";
|
|
5043
|
+
import { spawn, execSync as execSync4, execFileSync } from "child_process";
|
|
5044
|
+
async function verify(opts = {}) {
|
|
5045
|
+
const target = resolve12(opts.target ?? process.cwd());
|
|
5046
|
+
const bootTimeoutMs = Math.max(5e3, parseInt(opts.timeout ?? "60000", 10) || 6e4);
|
|
5047
|
+
console.log(import_picocolors10.default.bold("\u25C7 StyleCrib verify") + import_picocolors10.default.dim(" \u2014 runtime smoke gate"));
|
|
5048
|
+
console.log(import_picocolors10.default.dim(" " + target));
|
|
5049
|
+
console.log("");
|
|
5050
|
+
const results = [];
|
|
5051
|
+
if (opts.typecheck === false) {
|
|
5052
|
+
results.push({ name: "typecheck", status: "skip", detail: "--no-typecheck" });
|
|
5053
|
+
} else {
|
|
5054
|
+
results.push(runTypecheck(target));
|
|
5055
|
+
}
|
|
5056
|
+
if (opts.boot === false) {
|
|
5057
|
+
results.push({ name: "dev-server boot", status: "skip", detail: "--no-boot" });
|
|
5058
|
+
} else {
|
|
5059
|
+
results.push(await runBoot(target, opts.route ?? "/", opts.screenshot, bootTimeoutMs));
|
|
5060
|
+
}
|
|
5061
|
+
console.log("");
|
|
5062
|
+
for (const r of results) {
|
|
5063
|
+
const tag = r.status === "pass" ? import_picocolors10.default.green("PASS") : r.status === "fail" ? import_picocolors10.default.red("FAIL") : import_picocolors10.default.dim("SKIP");
|
|
5064
|
+
console.log(` ${tag} ${r.name}${r.detail ? import_picocolors10.default.dim(" \u2014 " + r.detail) : ""}`);
|
|
5065
|
+
}
|
|
5066
|
+
const failed = results.filter((r) => r.status === "fail");
|
|
5067
|
+
if (failed.length > 0) {
|
|
5068
|
+
console.log("");
|
|
5069
|
+
for (const f of failed) {
|
|
5070
|
+
if (!f.output) continue;
|
|
5071
|
+
console.log(import_picocolors10.default.red(`\u2500\u2500 ${f.name} output \u2500\u2500`));
|
|
5072
|
+
console.log(indent2(trimOutput(f.output), 2));
|
|
5073
|
+
console.log("");
|
|
5074
|
+
}
|
|
5075
|
+
console.log(import_picocolors10.default.red(`\u2717 verify failed \u2014 ${failed.length} of ${results.length} step(s).`));
|
|
5076
|
+
console.log(import_picocolors10.default.dim(" A sweep that passes `stylecrib check` can still break the app."));
|
|
5077
|
+
console.log(import_picocolors10.default.dim(" Fix the above before pushing."));
|
|
5078
|
+
process.exitCode = 1;
|
|
5079
|
+
return;
|
|
5080
|
+
}
|
|
5081
|
+
const ran = results.filter((r) => r.status === "pass").length;
|
|
5082
|
+
console.log("");
|
|
5083
|
+
if (ran === 0) {
|
|
5084
|
+
console.log(import_picocolors10.default.yellow("\u26A0 verify ran no checks \u2014 nothing was verified."));
|
|
5085
|
+
for (const r of results) {
|
|
5086
|
+
console.log(import_picocolors10.default.dim(` ${r.name}: ${r.detail}`));
|
|
5087
|
+
}
|
|
5088
|
+
process.exitCode = 1;
|
|
5089
|
+
return;
|
|
5090
|
+
}
|
|
5091
|
+
console.log(import_picocolors10.default.green(`\u2713 verify passed \u2014 ${ran} step(s), 0 failures.`));
|
|
5092
|
+
}
|
|
5093
|
+
function runTypecheck(target) {
|
|
5094
|
+
const pkgPath = join16(target, "package.json");
|
|
5095
|
+
if (!existsSync10(pkgPath)) {
|
|
5096
|
+
return { name: "typecheck", status: "skip", detail: "no package.json" };
|
|
5097
|
+
}
|
|
5098
|
+
let scripts = {};
|
|
5099
|
+
try {
|
|
5100
|
+
scripts = JSON.parse(readFileSync10(pkgPath, "utf8")).scripts ?? {};
|
|
5101
|
+
} catch {
|
|
5102
|
+
return { name: "typecheck", status: "skip", detail: "unreadable package.json" };
|
|
5103
|
+
}
|
|
5104
|
+
const hasTsconfig = existsSync10(join16(target, "tsconfig.json"));
|
|
5105
|
+
let cmd;
|
|
5106
|
+
if (scripts.typecheck) {
|
|
5107
|
+
cmd = "npm run typecheck --silent";
|
|
5108
|
+
} else if (hasTsconfig) {
|
|
5109
|
+
cmd = "npx --no-install tsc --noEmit";
|
|
5110
|
+
} else {
|
|
5111
|
+
return { name: "typecheck", status: "skip", detail: "no typecheck script and no tsconfig.json" };
|
|
5112
|
+
}
|
|
5113
|
+
process.stdout.write(import_picocolors10.default.dim(` \xB7 typecheck (${cmd}) \u2026 `));
|
|
5114
|
+
try {
|
|
5115
|
+
execSync4(cmd, {
|
|
5116
|
+
cwd: target,
|
|
5117
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
5118
|
+
timeout: 3e5,
|
|
5119
|
+
encoding: "utf8"
|
|
5120
|
+
});
|
|
5121
|
+
console.log(import_picocolors10.default.green("ok"));
|
|
5122
|
+
return { name: "typecheck", status: "pass", detail: cmd };
|
|
5123
|
+
} catch (err) {
|
|
5124
|
+
console.log(import_picocolors10.default.red("failed"));
|
|
5125
|
+
const e = err;
|
|
5126
|
+
return {
|
|
5127
|
+
name: "typecheck",
|
|
5128
|
+
status: "fail",
|
|
5129
|
+
detail: cmd,
|
|
5130
|
+
output: [e.stdout, e.stderr].filter(Boolean).join("\n") || e.message || "(no output)"
|
|
5131
|
+
};
|
|
5132
|
+
}
|
|
5133
|
+
}
|
|
5134
|
+
async function runBoot(target, route, screenshotPath, timeoutMs) {
|
|
5135
|
+
const pkgPath = join16(target, "package.json");
|
|
5136
|
+
if (!existsSync10(pkgPath)) {
|
|
5137
|
+
return { name: "dev-server boot", status: "skip", detail: "no package.json" };
|
|
5138
|
+
}
|
|
5139
|
+
let scripts = {};
|
|
5140
|
+
try {
|
|
5141
|
+
scripts = JSON.parse(readFileSync10(pkgPath, "utf8")).scripts ?? {};
|
|
5142
|
+
} catch {
|
|
5143
|
+
return { name: "dev-server boot", status: "skip", detail: "unreadable package.json" };
|
|
5144
|
+
}
|
|
5145
|
+
if (!scripts.dev) {
|
|
5146
|
+
return { name: "dev-server boot", status: "skip", detail: "no `dev` script" };
|
|
5147
|
+
}
|
|
5148
|
+
if (!existsSync10(join16(target, "node_modules"))) {
|
|
5149
|
+
return { name: "dev-server boot", status: "skip", detail: "node_modules missing \u2014 run npm install" };
|
|
5150
|
+
}
|
|
5151
|
+
process.stdout.write(import_picocolors10.default.dim(" \xB7 booting dev server \u2026 "));
|
|
5152
|
+
const child = spawn("npm", ["run", "dev"], {
|
|
5153
|
+
cwd: target,
|
|
5154
|
+
detached: true,
|
|
5155
|
+
stdio: ["ignore", "pipe", "pipe"]
|
|
5156
|
+
});
|
|
5157
|
+
let log = "";
|
|
5158
|
+
const onData = (b) => {
|
|
5159
|
+
log += b.toString();
|
|
5160
|
+
};
|
|
5161
|
+
child.stdout?.on("data", onData);
|
|
5162
|
+
child.stderr?.on("data", onData);
|
|
5163
|
+
const killGroup = () => {
|
|
5164
|
+
if (child.pid === void 0) return;
|
|
5165
|
+
try {
|
|
5166
|
+
process.kill(-child.pid, "SIGTERM");
|
|
5167
|
+
} catch {
|
|
5168
|
+
}
|
|
5169
|
+
try {
|
|
5170
|
+
process.kill(-child.pid, "SIGKILL");
|
|
5171
|
+
} catch {
|
|
5172
|
+
}
|
|
5173
|
+
try {
|
|
5174
|
+
child.kill("SIGKILL");
|
|
5175
|
+
} catch {
|
|
5176
|
+
}
|
|
5177
|
+
};
|
|
5178
|
+
const onSignal = (sig) => {
|
|
5179
|
+
killGroup();
|
|
5180
|
+
process.exitCode = 130;
|
|
5181
|
+
process.removeListener("SIGINT", onSignal);
|
|
5182
|
+
process.removeListener("SIGTERM", onSignal);
|
|
5183
|
+
process.kill(process.pid, sig);
|
|
5184
|
+
};
|
|
5185
|
+
process.once("SIGINT", onSignal);
|
|
5186
|
+
process.once("SIGTERM", onSignal);
|
|
5187
|
+
try {
|
|
5188
|
+
const url = await waitForUrl(child, () => log, timeoutMs);
|
|
5189
|
+
if (!url) {
|
|
5190
|
+
console.log(import_picocolors10.default.red("failed"));
|
|
5191
|
+
return {
|
|
5192
|
+
name: "dev-server boot",
|
|
5193
|
+
status: "fail",
|
|
5194
|
+
detail: `no server URL within ${Math.round(timeoutMs / 1e3)}s`,
|
|
5195
|
+
output: log
|
|
5196
|
+
};
|
|
5197
|
+
}
|
|
5198
|
+
const pageUrl = url.replace(/\/$/, "") + (route.startsWith("/") ? route : "/" + route);
|
|
5199
|
+
const res = await fetchWithRetry(pageUrl, 10, 500);
|
|
5200
|
+
if (!res) {
|
|
5201
|
+
console.log(import_picocolors10.default.red("failed"));
|
|
5202
|
+
return { name: "dev-server boot", status: "fail", detail: `no response from ${pageUrl}`, output: log };
|
|
5203
|
+
}
|
|
5204
|
+
if (!res.ok) {
|
|
5205
|
+
console.log(import_picocolors10.default.red("failed"));
|
|
5206
|
+
return {
|
|
5207
|
+
name: "dev-server boot",
|
|
5208
|
+
status: "fail",
|
|
5209
|
+
detail: `${pageUrl} returned HTTP ${res.status}`,
|
|
5210
|
+
output: res.body.slice(0, 2e3) + "\n---\n" + log
|
|
5211
|
+
};
|
|
5212
|
+
}
|
|
5213
|
+
const dom = renderDom(pageUrl);
|
|
5214
|
+
if (!dom) {
|
|
5215
|
+
console.log(import_picocolors10.default.yellow("skipped"));
|
|
5216
|
+
return {
|
|
5217
|
+
name: "dev-server boot",
|
|
5218
|
+
status: "skip",
|
|
5219
|
+
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`
|
|
5220
|
+
};
|
|
5221
|
+
}
|
|
5222
|
+
const inspected = dom;
|
|
5223
|
+
const method = "rendered DOM";
|
|
5224
|
+
const bodyProblem = describeBodyProblem(inspected, true);
|
|
5225
|
+
if (bodyProblem) {
|
|
5226
|
+
console.log(import_picocolors10.default.red("failed"));
|
|
5227
|
+
return {
|
|
5228
|
+
name: "dev-server boot",
|
|
5229
|
+
status: "fail",
|
|
5230
|
+
detail: `${bodyProblem} (checked ${dom ? "rendered DOM" : "HTTP body"})`,
|
|
5231
|
+
output: inspected.slice(0, 3e3)
|
|
5232
|
+
};
|
|
5233
|
+
}
|
|
5234
|
+
let shotNote = "";
|
|
5235
|
+
if (screenshotPath) {
|
|
5236
|
+
const abs = resolve12(screenshotPath);
|
|
5237
|
+
mkdirSync7(dirname7(abs), { recursive: true });
|
|
5238
|
+
try {
|
|
5239
|
+
captureScreenshot(pageUrl, abs);
|
|
5240
|
+
shotNote = ` \xB7 screenshot \u2192 ${screenshotPath}`;
|
|
5241
|
+
} catch (e) {
|
|
5242
|
+
shotNote = ` \xB7 screenshot skipped (${e.message})`;
|
|
5243
|
+
}
|
|
5244
|
+
}
|
|
5245
|
+
console.log(import_picocolors10.default.green("ok"));
|
|
5246
|
+
return {
|
|
5247
|
+
name: "dev-server boot",
|
|
5248
|
+
status: "pass",
|
|
5249
|
+
detail: `${pageUrl} \xB7 ${method}, ${inspected.length} bytes${shotNote}`
|
|
5250
|
+
};
|
|
5251
|
+
} finally {
|
|
5252
|
+
process.removeListener("SIGINT", onSignal);
|
|
5253
|
+
process.removeListener("SIGTERM", onSignal);
|
|
5254
|
+
killGroup();
|
|
5255
|
+
}
|
|
5256
|
+
}
|
|
5257
|
+
function waitForUrl(child, getLog, timeoutMs) {
|
|
5258
|
+
return new Promise((res) => {
|
|
5259
|
+
const started = Date.now();
|
|
5260
|
+
let settled = false;
|
|
5261
|
+
const done = (v) => {
|
|
5262
|
+
if (!settled) {
|
|
5263
|
+
settled = true;
|
|
5264
|
+
clearInterval(t);
|
|
5265
|
+
res(v);
|
|
5266
|
+
}
|
|
5267
|
+
};
|
|
5268
|
+
child.once("exit", () => done(null));
|
|
5269
|
+
child.once("error", () => done(null));
|
|
5270
|
+
const t = setInterval(() => {
|
|
5271
|
+
const m = getLog().match(/https?:\/\/(?:localhost|127\.0\.0\.1)(?::\d+)?\/?/);
|
|
5272
|
+
if (m) return done(m[0]);
|
|
5273
|
+
if (Date.now() - started > timeoutMs) done(null);
|
|
5274
|
+
}, 250);
|
|
5275
|
+
});
|
|
5276
|
+
}
|
|
5277
|
+
async function fetchWithRetry(url, attempts, delayMs) {
|
|
5278
|
+
for (let i = 0; i < attempts; i++) {
|
|
5279
|
+
try {
|
|
5280
|
+
const r = await fetch(url, { redirect: "follow" });
|
|
5281
|
+
return { ok: r.ok, status: r.status, body: await r.text() };
|
|
5282
|
+
} catch {
|
|
5283
|
+
await sleep(delayMs);
|
|
5284
|
+
}
|
|
5285
|
+
}
|
|
5286
|
+
return null;
|
|
5287
|
+
}
|
|
5288
|
+
function renderDom(url) {
|
|
5289
|
+
for (const c2 of chromeCandidates()) {
|
|
5290
|
+
try {
|
|
5291
|
+
return execFileSync(c2, [
|
|
5292
|
+
"--headless",
|
|
5293
|
+
"--disable-gpu",
|
|
5294
|
+
"--hide-scrollbars",
|
|
5295
|
+
"--virtual-time-budget=5000",
|
|
5296
|
+
"--dump-dom",
|
|
5297
|
+
url
|
|
5298
|
+
], { stdio: ["ignore", "pipe", "ignore"], timeout: 3e4, encoding: "utf8" });
|
|
5299
|
+
} catch {
|
|
5300
|
+
continue;
|
|
5301
|
+
}
|
|
5302
|
+
}
|
|
5303
|
+
return null;
|
|
5304
|
+
}
|
|
5305
|
+
function describeBodyProblem(body, isDom) {
|
|
5306
|
+
if (/vite-error-overlay/i.test(body)) {
|
|
5307
|
+
return "page served a Vite error overlay";
|
|
5308
|
+
}
|
|
5309
|
+
if (/Internal Server Error|Cannot GET |Failed to resolve import/i.test(body)) {
|
|
5310
|
+
return "page contains a server error";
|
|
5311
|
+
}
|
|
5312
|
+
if (isDom) {
|
|
5313
|
+
const mount = body.match(/<div[^>]+id=["']root["'][^>]*>([\s\S]*?)<\/div>/i);
|
|
5314
|
+
if (mount && mount[1].trim().length === 0) {
|
|
5315
|
+
return "mount node #root is empty after JS ran \u2014 the app threw before rendering";
|
|
5316
|
+
}
|
|
5317
|
+
if (!mount && !/<div[^>]+id=["']app["']/i.test(body)) {
|
|
5318
|
+
return "no #root / #app mount node in the rendered DOM";
|
|
5319
|
+
}
|
|
5320
|
+
} else if (body.trim().length < 80) {
|
|
5321
|
+
return `page body is only ${body.trim().length} bytes \u2014 likely not a real render`;
|
|
5322
|
+
}
|
|
5323
|
+
return null;
|
|
5324
|
+
}
|
|
5325
|
+
function chromeCandidates() {
|
|
5326
|
+
return [
|
|
5327
|
+
"/Applications/Google Chrome.app/Contents/MacOS/Google Chrome",
|
|
5328
|
+
"/Applications/Chromium.app/Contents/MacOS/Chromium",
|
|
5329
|
+
"google-chrome",
|
|
5330
|
+
"chromium"
|
|
5331
|
+
];
|
|
5332
|
+
}
|
|
5333
|
+
function captureScreenshot(url, outPath) {
|
|
5334
|
+
for (const c2 of chromeCandidates()) {
|
|
5335
|
+
try {
|
|
5336
|
+
execFileSync(c2, [
|
|
5337
|
+
"--headless",
|
|
5338
|
+
"--disable-gpu",
|
|
5339
|
+
"--hide-scrollbars",
|
|
5340
|
+
`--screenshot=${outPath}`,
|
|
5341
|
+
"--window-size=1440,1600",
|
|
5342
|
+
url
|
|
5343
|
+
], { stdio: ["ignore", "ignore", "ignore"], timeout: 3e4 });
|
|
5344
|
+
return;
|
|
5345
|
+
} catch {
|
|
5346
|
+
continue;
|
|
5347
|
+
}
|
|
5348
|
+
}
|
|
5349
|
+
throw new Error("no headless Chrome found");
|
|
5350
|
+
}
|
|
5351
|
+
function sleep(ms) {
|
|
5352
|
+
return new Promise((r) => setTimeout(r, ms));
|
|
5353
|
+
}
|
|
5354
|
+
function trimOutput(s, maxLines = 40) {
|
|
5355
|
+
const lines = s.split("\n").filter((l) => l.trim() !== "");
|
|
5356
|
+
if (lines.length <= maxLines) return lines.join("\n");
|
|
5357
|
+
return [...lines.slice(0, maxLines), `\u2026 ${lines.length - maxLines} more line(s)`].join("\n");
|
|
5358
|
+
}
|
|
5359
|
+
function indent2(s, n) {
|
|
5360
|
+
const pad = " ".repeat(n);
|
|
5361
|
+
return s.split("\n").map((l) => pad + l).join("\n");
|
|
5362
|
+
}
|
|
5363
|
+
|
|
5364
|
+
// src/commands/mcp.ts
|
|
5365
|
+
init_esm_shims();
|
|
5366
|
+
import { readFileSync as readFileSync11, existsSync as existsSync11 } from "fs";
|
|
5367
|
+
import { join as join17 } from "path";
|
|
5368
|
+
import { McpServer, ResourceTemplate } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
5369
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
5370
|
+
import { z } from "zod";
|
|
5371
|
+
init_slop_rules();
|
|
5372
|
+
async function mcp(_opts = {}) {
|
|
5373
|
+
const toStderr = (...args) => console.error(...args);
|
|
5374
|
+
console.log = toStderr;
|
|
5375
|
+
console.info = toStderr;
|
|
5376
|
+
console.warn = toStderr;
|
|
5377
|
+
const server = new McpServer({ name: "stylecrib", version: VERSION });
|
|
5378
|
+
server.registerTool(
|
|
5379
|
+
"list_themes",
|
|
5380
|
+
{
|
|
5381
|
+
title: "List StyleCrib themes",
|
|
5382
|
+
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.",
|
|
5383
|
+
inputSchema: {}
|
|
5384
|
+
},
|
|
5385
|
+
async () => {
|
|
5386
|
+
const themes = DIRECTION_CHOICES.filter((d) => d.value !== "custom").map((d) => ({
|
|
5387
|
+
slug: d.value,
|
|
5388
|
+
name: d.name,
|
|
5389
|
+
description: d.description,
|
|
5390
|
+
emittable: isEmittable(d.value)
|
|
5391
|
+
}));
|
|
5392
|
+
return text(JSON.stringify({ count: themes.length, themes }, null, 2));
|
|
5393
|
+
}
|
|
5394
|
+
);
|
|
5395
|
+
server.registerTool(
|
|
5396
|
+
"get_theme",
|
|
5397
|
+
{
|
|
5398
|
+
title: "Get a StyleCrib theme",
|
|
5399
|
+
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.",
|
|
5400
|
+
inputSchema: {
|
|
5401
|
+
slug: z.string().describe("Theme slug from list_themes, e.g. 'signal-dark'")
|
|
5402
|
+
}
|
|
5403
|
+
},
|
|
5404
|
+
async ({ slug }) => {
|
|
5405
|
+
if (!VALID_DIRECTIONS.includes(slug) || slug === "custom") {
|
|
5406
|
+
return fail(`Unknown theme "${slug}". Call list_themes for valid slugs.`);
|
|
5407
|
+
}
|
|
5408
|
+
const themesDir = resolveAssetDir("themes");
|
|
5409
|
+
if (!themesDir) return fail(missingAssetMessage("themes"));
|
|
5410
|
+
const mdPath = join17(themesDir, slug, "THEME.md");
|
|
5411
|
+
if (!existsSync11(mdPath)) return fail(`No THEME.md for "${slug}" at ${mdPath}.`);
|
|
5412
|
+
const header2 = `# ${slug}
|
|
5413
|
+
|
|
5414
|
+
emittable: ${isEmittable(slug)}
|
|
5415
|
+
tokens: themes/${slug}/tokens.ts
|
|
5416
|
+
|
|
5417
|
+
---
|
|
5418
|
+
|
|
5419
|
+
`;
|
|
5420
|
+
return text(header2 + readFileSync11(mdPath, "utf8"));
|
|
5421
|
+
}
|
|
5422
|
+
);
|
|
5423
|
+
server.registerTool(
|
|
5424
|
+
"emit_theme_tokens",
|
|
5425
|
+
{
|
|
5426
|
+
title: "Generate Tailwind tokens for a theme",
|
|
5427
|
+
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.",
|
|
5428
|
+
inputSchema: {
|
|
5429
|
+
slug: z.string().describe("Theme slug from list_themes"),
|
|
5430
|
+
typescript: z.boolean().optional().describe("Emit tailwind.config.ts (default true) or tailwind.config.js")
|
|
5431
|
+
}
|
|
5432
|
+
},
|
|
5433
|
+
async ({ slug, typescript }) => {
|
|
5434
|
+
if (!VALID_DIRECTIONS.includes(slug) || slug === "custom") {
|
|
5435
|
+
return fail(`Unknown theme "${slug}". Call list_themes for valid slugs.`);
|
|
5436
|
+
}
|
|
5437
|
+
try {
|
|
5438
|
+
const direction = slug;
|
|
5439
|
+
await assertDirectionEmittable(direction);
|
|
5440
|
+
const emitted = await emitTokens({ direction, stack: syntheticStack(typescript ?? true) });
|
|
5441
|
+
return text(
|
|
5442
|
+
`// ===== ${emitted.tailwindConfig.path} =====
|
|
5443
|
+
` + emitted.tailwindConfig.content + `
|
|
5444
|
+
|
|
5445
|
+
/* ===== ${emitted.indexCss.path} ===== */
|
|
5446
|
+
` + emitted.indexCss.content
|
|
5447
|
+
);
|
|
5448
|
+
} catch (err) {
|
|
5449
|
+
return fail(err instanceof Error ? err.message : String(err));
|
|
5450
|
+
}
|
|
5451
|
+
}
|
|
5452
|
+
);
|
|
5453
|
+
server.registerTool(
|
|
5454
|
+
"list_rules",
|
|
5455
|
+
{
|
|
5456
|
+
title: "List slop rules",
|
|
5457
|
+
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.",
|
|
5458
|
+
inputSchema: {
|
|
5459
|
+
category: z.string().optional().describe("e.g. typography, palette, layout, content, motion"),
|
|
5460
|
+
severity: z.enum(["block", "warn"]).optional(),
|
|
5461
|
+
detectorAt: z.string().optional().describe("check.ts | slop-blocklist.ts | qa-conductor")
|
|
5462
|
+
}
|
|
5463
|
+
},
|
|
5464
|
+
async ({ category, severity, detectorAt }) => {
|
|
5465
|
+
const rules = SLOP_RULES.filter(
|
|
5466
|
+
(r) => (!category || r.category === category) && (!severity || r.severity === severity) && (!detectorAt || r.detectorAt === detectorAt)
|
|
5467
|
+
);
|
|
5468
|
+
return text(JSON.stringify({ count: rules.length, rules }, null, 2));
|
|
5469
|
+
}
|
|
5470
|
+
);
|
|
5471
|
+
server.registerTool(
|
|
5472
|
+
"check_slop",
|
|
5473
|
+
{
|
|
5474
|
+
title: "Check a file for AI-default slop",
|
|
5475
|
+
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.",
|
|
5476
|
+
inputSchema: {
|
|
5477
|
+
path: z.string().describe("File path, used for rule scoping (JSX brace-stripping applies to .tsx/.jsx)"),
|
|
5478
|
+
content: z.string().describe("The file's full contents"),
|
|
5479
|
+
theme: z.string().optional().describe("Active theme slug, enables theme-aware rules (e.g. 'signal-dark')")
|
|
5480
|
+
}
|
|
5481
|
+
},
|
|
5482
|
+
async ({ path: path2, content, theme }) => {
|
|
5483
|
+
let themeCtx = null;
|
|
5484
|
+
if (theme) {
|
|
5485
|
+
if (!VALID_DIRECTIONS.includes(theme)) {
|
|
5486
|
+
return fail(`Unknown theme "${theme}". Call list_themes for valid slugs.`);
|
|
5487
|
+
}
|
|
5488
|
+
themeCtx = resolveThemeContext(theme);
|
|
5489
|
+
}
|
|
5490
|
+
const hits = runAllTextDetectors(path2, content, themeCtx);
|
|
5491
|
+
return text(JSON.stringify(summarise(path2, hits), null, 2));
|
|
5492
|
+
}
|
|
5493
|
+
);
|
|
5494
|
+
server.registerResource(
|
|
5495
|
+
"rules",
|
|
5496
|
+
"stylecrib://rules",
|
|
5497
|
+
{
|
|
5498
|
+
title: "StyleCrib slop-rule catalog",
|
|
5499
|
+
description: "All 39 rules as JSON. Same data as list_rules with no filter.",
|
|
5500
|
+
mimeType: "application/json"
|
|
5501
|
+
},
|
|
5502
|
+
async (uri) => ({
|
|
5503
|
+
contents: [{ uri: uri.href, mimeType: "application/json", text: JSON.stringify(SLOP_RULES, null, 2) }]
|
|
5504
|
+
})
|
|
5505
|
+
);
|
|
5506
|
+
server.registerResource(
|
|
5507
|
+
"agents",
|
|
5508
|
+
new ResourceTemplate("stylecrib://agents/{name}", {
|
|
5509
|
+
list: async () => ({
|
|
5510
|
+
resources: Object.keys(BUNDLED_AGENTS).map((name) => ({
|
|
5511
|
+
uri: `stylecrib://agents/${name}`,
|
|
5512
|
+
name,
|
|
5513
|
+
mimeType: "text/markdown"
|
|
5514
|
+
}))
|
|
5515
|
+
})
|
|
5516
|
+
}),
|
|
5517
|
+
{
|
|
5518
|
+
title: "StyleCrib agent doctrines",
|
|
5519
|
+
description: "The 19 subagent prompts that ship with the pack, by name.",
|
|
5520
|
+
mimeType: "text/markdown"
|
|
5521
|
+
},
|
|
5522
|
+
async (uri, { name }) => {
|
|
5523
|
+
const key = String(name);
|
|
5524
|
+
const doc = BUNDLED_AGENTS[key];
|
|
5525
|
+
if (!doc) throw new CliError(`No agent named "${key}". Known: ${Object.keys(BUNDLED_AGENTS).join(", ")}`);
|
|
5526
|
+
return { contents: [{ uri: uri.href, mimeType: "text/markdown", text: doc }] };
|
|
5527
|
+
}
|
|
5528
|
+
);
|
|
5529
|
+
const transport = new StdioServerTransport();
|
|
5530
|
+
await server.connect(transport);
|
|
5531
|
+
console.error(`stylecrib mcp ${VERSION} \u2014 serving over stdio`);
|
|
5532
|
+
}
|
|
5533
|
+
function text(body) {
|
|
5534
|
+
return { content: [{ type: "text", text: body }] };
|
|
5535
|
+
}
|
|
5536
|
+
function fail(message) {
|
|
5537
|
+
return { content: [{ type: "text", text: message }], isError: true };
|
|
5538
|
+
}
|
|
5539
|
+
var emittableCache = /* @__PURE__ */ new Map();
|
|
5540
|
+
function isEmittable(slug) {
|
|
5541
|
+
const cached = emittableCache.get(slug);
|
|
5542
|
+
if (cached !== void 0) return cached;
|
|
5543
|
+
let ok = true;
|
|
5544
|
+
try {
|
|
5545
|
+
const themesDir = resolveAssetDir("themes");
|
|
5546
|
+
const src = themesDir ? readFileSync11(join17(themesDir, slug, "tokens.ts"), "utf8") : "";
|
|
5547
|
+
ok = /^\s*colors:\s*\{/m.test(src);
|
|
5548
|
+
} catch {
|
|
5549
|
+
ok = false;
|
|
5550
|
+
}
|
|
5551
|
+
emittableCache.set(slug, ok);
|
|
5552
|
+
return ok;
|
|
5553
|
+
}
|
|
5554
|
+
function syntheticStack(ts) {
|
|
5555
|
+
return {
|
|
5556
|
+
rootPath: "",
|
|
5557
|
+
packageJsonPath: "",
|
|
5558
|
+
ts,
|
|
5559
|
+
react: { version: "n/a", major: 0 },
|
|
5560
|
+
vite: { version: "n/a", major: 0 },
|
|
5561
|
+
tailwind: { version: "n/a", major: 0 },
|
|
5562
|
+
hasTailwindConfig: false,
|
|
5563
|
+
tailwindConfigPath: null,
|
|
5564
|
+
hasSrcDir: true,
|
|
5565
|
+
srcPath: "src",
|
|
5566
|
+
notes: []
|
|
5567
|
+
};
|
|
5568
|
+
}
|
|
5569
|
+
function summarise(path2, hits) {
|
|
5570
|
+
const enriched = hits.map((h) => {
|
|
5571
|
+
const rule2 = ruleById(h.ruleId);
|
|
5572
|
+
return {
|
|
5573
|
+
rule: h.ruleId,
|
|
5574
|
+
severity: rule2?.severity ?? "block",
|
|
5575
|
+
category: rule2?.category ?? "unknown",
|
|
5576
|
+
line: h.line,
|
|
5577
|
+
sample: h.sample,
|
|
5578
|
+
description: rule2?.description ?? ""
|
|
5579
|
+
};
|
|
5580
|
+
});
|
|
5581
|
+
const blocks = enriched.filter((h) => h.severity === "block").length;
|
|
5582
|
+
return {
|
|
5583
|
+
path: path2,
|
|
5584
|
+
verdict: blocks > 0 ? "BLOCK" : enriched.length > 0 ? "WARN" : "PASS",
|
|
5585
|
+
blockHits: blocks,
|
|
5586
|
+
warnHits: enriched.length - blocks,
|
|
5587
|
+
hits: enriched,
|
|
5588
|
+
override: enriched.length > 0 ? "Silence a legitimate hit with `// slop-override: R-XXX \u2014 <reason>` within 3 lines above or 8 below." : void 0
|
|
5589
|
+
};
|
|
5590
|
+
}
|
|
5591
|
+
|
|
5592
|
+
// src/commands/harness.ts
|
|
5593
|
+
init_esm_shims();
|
|
5594
|
+
var import_picocolors11 = __toESM(require_picocolors(), 1);
|
|
5595
|
+
import { existsSync as existsSync19 } from "fs";
|
|
5596
|
+
import { join as join26, resolve as resolve13 } from "path";
|
|
4512
5597
|
|
|
4513
5598
|
// src/theme-harness/materialize.ts
|
|
4514
5599
|
init_esm_shims();
|
|
4515
5600
|
import { writeFileSync as writeFileSync7 } from "fs";
|
|
4516
|
-
import { join as
|
|
5601
|
+
import { join as join18 } from "path";
|
|
4517
5602
|
|
|
4518
5603
|
// src/theme-harness/lib/theme-yml.ts
|
|
4519
5604
|
init_esm_shims();
|
|
4520
|
-
import { readFileSync as
|
|
5605
|
+
import { readFileSync as readFileSync12 } from "fs";
|
|
4521
5606
|
import { parse as parseYaml } from "yaml";
|
|
4522
5607
|
var REQUIRED_TOP = [
|
|
4523
5608
|
"name",
|
|
@@ -4543,7 +5628,7 @@ var HEX_RE = /^#[0-9A-Fa-f]{6}$/;
|
|
|
4543
5628
|
function loadTheme(path2) {
|
|
4544
5629
|
let raw;
|
|
4545
5630
|
try {
|
|
4546
|
-
raw = parseYaml(
|
|
5631
|
+
raw = parseYaml(readFileSync12(path2, "utf8"));
|
|
4547
5632
|
} catch (e) {
|
|
4548
5633
|
return { ok: false, errors: [`YAML parse error: ${e.message}`] };
|
|
4549
5634
|
}
|
|
@@ -4629,7 +5714,7 @@ function validateTheme(raw) {
|
|
|
4629
5714
|
|
|
4630
5715
|
// src/theme-harness/materialize.ts
|
|
4631
5716
|
function materializeTheme(slug, themeDir) {
|
|
4632
|
-
const ymlPath =
|
|
5717
|
+
const ymlPath = join18(themeDir, "theme.yml");
|
|
4633
5718
|
const parsed = loadTheme(ymlPath);
|
|
4634
5719
|
if (!parsed.ok) return { ok: false, errors: parsed.errors };
|
|
4635
5720
|
const theme = parsed.theme;
|
|
@@ -4637,11 +5722,11 @@ function materializeTheme(slug, themeDir) {
|
|
|
4637
5722
|
const tokensTs = renderTokensTs(slug, theme);
|
|
4638
5723
|
const tailwindSnippet = renderTailwindSnippet(slug, theme);
|
|
4639
5724
|
const wrote = [];
|
|
4640
|
-
writeFileSync7(
|
|
5725
|
+
writeFileSync7(join18(themeDir, "THEME.md"), themeMd);
|
|
4641
5726
|
wrote.push("THEME.md");
|
|
4642
|
-
writeFileSync7(
|
|
5727
|
+
writeFileSync7(join18(themeDir, "tokens.ts"), tokensTs);
|
|
4643
5728
|
wrote.push("tokens.ts");
|
|
4644
|
-
writeFileSync7(
|
|
5729
|
+
writeFileSync7(join18(themeDir, "tailwind.snippet.ts"), tailwindSnippet);
|
|
4645
5730
|
wrote.push("tailwind.snippet.ts");
|
|
4646
5731
|
return { ok: true, wrote };
|
|
4647
5732
|
}
|
|
@@ -4726,9 +5811,27 @@ function renderTokensTs(slug, t) {
|
|
|
4726
5811
|
].join("\n");
|
|
4727
5812
|
}
|
|
4728
5813
|
function renderTailwindSnippet(slug, t) {
|
|
5814
|
+
const display = extractFontStack(t.typography.display);
|
|
5815
|
+
const body = extractFontStack(t.typography.body);
|
|
5816
|
+
const mono = extractFontStack(t.typography.mono);
|
|
5817
|
+
const fontLines = [];
|
|
5818
|
+
if (display) fontLines.push(` display: ${JSON.stringify(display)},`);
|
|
5819
|
+
if (body) fontLines.push(` body: ${JSON.stringify(body)},`);
|
|
5820
|
+
if (mono) fontLines.push(` mono: ${JSON.stringify(mono)},`);
|
|
5821
|
+
const fontBlock = fontLines.length ? [` fontFamily: {`, ...fontLines, ` },`] : [
|
|
5822
|
+
` // fontFamily omitted \u2014 theme.yml's typography brief doesn't state a`,
|
|
5823
|
+
` // machine-readable fallback stack. Read the Typography section of`,
|
|
5824
|
+
` // THEME.md and set the stack by hand.`
|
|
5825
|
+
];
|
|
5826
|
+
const unresolved = [
|
|
5827
|
+
!display ? "display" : "",
|
|
5828
|
+
!body ? "body" : "",
|
|
5829
|
+
!mono ? "mono" : ""
|
|
5830
|
+
].filter(Boolean);
|
|
4729
5831
|
return [
|
|
4730
5832
|
`// Generated from theme.yml. Merge into your tailwind.config.ts under theme.extend.`,
|
|
4731
5833
|
`// slug: ${slug}`,
|
|
5834
|
+
unresolved.length ? `// Unresolved font stack(s): ${unresolved.join(", ")} \u2014 see THEME.md \xA7 Typography.` : "",
|
|
4732
5835
|
``,
|
|
4733
5836
|
`export const ${camelCase(slug)}TailwindExtend = {`,
|
|
4734
5837
|
` colors: {`,
|
|
@@ -4739,15 +5842,21 @@ function renderTailwindSnippet(slug, t) {
|
|
|
4739
5842
|
t.palette.secondary ? ` secondary: ${JSON.stringify(t.palette.secondary)},` : "",
|
|
4740
5843
|
t.palette.signal ? ` signal: ${JSON.stringify(t.palette.signal)},` : "",
|
|
4741
5844
|
` },`,
|
|
4742
|
-
|
|
4743
|
-
` display: ${JSON.stringify([t.typography.display])},`,
|
|
4744
|
-
` body: ${JSON.stringify([t.typography.body])},`,
|
|
4745
|
-
` mono: ${JSON.stringify([t.typography.mono])},`,
|
|
4746
|
-
` },`,
|
|
5845
|
+
...fontBlock,
|
|
4747
5846
|
`};`,
|
|
4748
5847
|
``
|
|
4749
5848
|
].filter((l) => l !== "").join("\n");
|
|
4750
5849
|
}
|
|
5850
|
+
function extractFontStack(brief) {
|
|
5851
|
+
if (!brief) return null;
|
|
5852
|
+
const text2 = String(brief).trim();
|
|
5853
|
+
if (/^none\b/i.test(text2)) return null;
|
|
5854
|
+
const m = text2.match(/fallback stack\s*[—–-]\s*([^.]+)\./i);
|
|
5855
|
+
if (!m) return null;
|
|
5856
|
+
const families = m[1].split(",").map((f) => f.trim().replace(/^["'“”]|["'“”]$/g, "").trim()).filter((f) => f.length > 0 && f.length < 40 && !/\s{2,}/.test(f));
|
|
5857
|
+
if (families.length < 2) return null;
|
|
5858
|
+
return families;
|
|
5859
|
+
}
|
|
4751
5860
|
function camelCase(slug) {
|
|
4752
5861
|
return slug.split("-").map((p, i) => i === 0 ? p : p[0].toUpperCase() + p.slice(1)).join("");
|
|
4753
5862
|
}
|
|
@@ -4763,8 +5872,8 @@ function inferFamily(t) {
|
|
|
4763
5872
|
|
|
4764
5873
|
// src/theme-harness/grade.ts
|
|
4765
5874
|
init_esm_shims();
|
|
4766
|
-
import { appendFileSync as appendFileSync3, readFileSync as
|
|
4767
|
-
import { join as
|
|
5875
|
+
import { appendFileSync as appendFileSync3, readFileSync as readFileSync17, existsSync as existsSync16 } from "fs";
|
|
5876
|
+
import { join as join23 } from "path";
|
|
4768
5877
|
|
|
4769
5878
|
// src/theme-harness/gates/slop-blocklist.ts
|
|
4770
5879
|
init_esm_shims();
|
|
@@ -4884,14 +5993,14 @@ function runSourceRules(src, theme, failures) {
|
|
|
4884
5993
|
const capsCssRe2 = /letterSpacing:\s*["']?(0?\.\d+)em[\s\S]{0,200}?textTransform:\s*["']uppercase["']/g;
|
|
4885
5994
|
const fontSizeRe = /fontSize:\s*(\d+)/;
|
|
4886
5995
|
const capsHits = [];
|
|
4887
|
-
const scanCaps = (re,
|
|
5996
|
+
const scanCaps = (re, text2) => {
|
|
4888
5997
|
let m;
|
|
4889
|
-
while ((m = re.exec(
|
|
5998
|
+
while ((m = re.exec(text2)) !== null) {
|
|
4890
5999
|
const spacing = parseFloat(m[1]);
|
|
4891
6000
|
if (spacing < 0.18) continue;
|
|
4892
6001
|
const start = Math.max(0, m.index - 240);
|
|
4893
|
-
const end = Math.min(
|
|
4894
|
-
const window =
|
|
6002
|
+
const end = Math.min(text2.length, m.index + m[0].length + 240);
|
|
6003
|
+
const window = text2.slice(start, end);
|
|
4895
6004
|
const fs = window.match(fontSizeRe);
|
|
4896
6005
|
const fontSize = fs ? parseInt(fs[1], 10) : 12;
|
|
4897
6006
|
if (fontSize < 20) capsHits.push(`${m[1]}em @ ~${fontSize}px`);
|
|
@@ -5026,8 +6135,8 @@ function normalizeFont(f) {
|
|
|
5026
6135
|
|
|
5027
6136
|
// src/theme-harness/gates/avoid-rubric.ts
|
|
5028
6137
|
init_esm_shims();
|
|
5029
|
-
import { readFileSync as
|
|
5030
|
-
import { join as
|
|
6138
|
+
import { readFileSync as readFileSync13, existsSync as existsSync12 } from "fs";
|
|
6139
|
+
import { join as join19 } from "path";
|
|
5031
6140
|
import Anthropic4 from "@anthropic-ai/sdk";
|
|
5032
6141
|
var RUBRIC_TOOL = {
|
|
5033
6142
|
name: "report_verdict",
|
|
@@ -5141,25 +6250,25 @@ async function runAvoidRubric(theme, themeDir) {
|
|
|
5141
6250
|
return { passed: failures.length === 0, failures };
|
|
5142
6251
|
}
|
|
5143
6252
|
function collectEvidence(themeDir) {
|
|
5144
|
-
const readOr = (p) =>
|
|
6253
|
+
const readOr = (p) => existsSync12(p) ? readFileSync13(p, "utf8") : "<file not present>";
|
|
5145
6254
|
return {
|
|
5146
|
-
themeMd: readOr(
|
|
5147
|
-
tokensTs: readOr(
|
|
5148
|
-
tailwindSnippet: readOr(
|
|
6255
|
+
themeMd: readOr(join19(themeDir, "THEME.md")),
|
|
6256
|
+
tokensTs: readOr(join19(themeDir, "tokens.ts")),
|
|
6257
|
+
tailwindSnippet: readOr(join19(themeDir, "tailwind.snippet.ts"))
|
|
5149
6258
|
};
|
|
5150
6259
|
}
|
|
5151
6260
|
|
|
5152
6261
|
// src/theme-harness/gates/regression-scan.ts
|
|
5153
6262
|
init_esm_shims();
|
|
5154
|
-
import { readFileSync as
|
|
5155
|
-
import { join as
|
|
6263
|
+
import { readFileSync as readFileSync14, existsSync as existsSync13 } from "fs";
|
|
6264
|
+
import { join as join20 } from "path";
|
|
5156
6265
|
function resolveRegressionSet(themeDir) {
|
|
5157
|
-
return
|
|
6266
|
+
return join20(themeDir, "..", "..", "site", "design-system", "regression-set.jsonl");
|
|
5158
6267
|
}
|
|
5159
6268
|
function loadRegressionRules(themeDir) {
|
|
5160
6269
|
const p = resolveRegressionSet(themeDir);
|
|
5161
|
-
if (!
|
|
5162
|
-
const raw =
|
|
6270
|
+
if (!existsSync13(p)) return [];
|
|
6271
|
+
const raw = readFileSync14(p, "utf8").trim();
|
|
5163
6272
|
if (!raw) return [];
|
|
5164
6273
|
const rules = [];
|
|
5165
6274
|
for (const line of raw.split("\n")) {
|
|
@@ -5175,9 +6284,9 @@ function loadRegressionRules(themeDir) {
|
|
|
5175
6284
|
function runRegressionScan(theme, themeDir) {
|
|
5176
6285
|
const rules = loadRegressionRules(themeDir);
|
|
5177
6286
|
if (rules.length === 0) return { passed: true, failures: [] };
|
|
5178
|
-
const tokensSrc = safeRead(
|
|
5179
|
-
const twSrc = safeRead(
|
|
5180
|
-
const themeMdSrc = safeRead(
|
|
6287
|
+
const tokensSrc = safeRead(join20(themeDir, "tokens.ts"));
|
|
6288
|
+
const twSrc = safeRead(join20(themeDir, "tailwind.snippet.ts"));
|
|
6289
|
+
const themeMdSrc = safeRead(join20(themeDir, "THEME.md"));
|
|
5181
6290
|
const combined = tokensSrc + "\n" + twSrc;
|
|
5182
6291
|
const failures = [];
|
|
5183
6292
|
for (const rule2 of rules) {
|
|
@@ -5233,7 +6342,7 @@ function checkRule(rule2, theme, materializedCode, themeMdSrc) {
|
|
|
5233
6342
|
return null;
|
|
5234
6343
|
}
|
|
5235
6344
|
function safeRead(p) {
|
|
5236
|
-
return
|
|
6345
|
+
return existsSync13(p) ? readFileSync14(p, "utf8") : "";
|
|
5237
6346
|
}
|
|
5238
6347
|
function hexToRgb2(hex) {
|
|
5239
6348
|
const m = hex.match(/^#([0-9a-f]{2})([0-9a-f]{2})([0-9a-f]{2})$/i);
|
|
@@ -5249,9 +6358,9 @@ function rgbDistance2(a, b) {
|
|
|
5249
6358
|
|
|
5250
6359
|
// src/theme-harness/gates/anchor-compare.ts
|
|
5251
6360
|
init_esm_shims();
|
|
5252
|
-
import { existsSync as
|
|
5253
|
-
import { execSync as
|
|
5254
|
-
import { join as
|
|
6361
|
+
import { existsSync as existsSync14, readFileSync as readFileSync15 } from "fs";
|
|
6362
|
+
import { execSync as execSync5 } from "child_process";
|
|
6363
|
+
import { join as join21 } from "path";
|
|
5255
6364
|
import Anthropic5 from "@anthropic-ai/sdk";
|
|
5256
6365
|
var RUBRIC_TOOL2 = {
|
|
5257
6366
|
name: "report_anchor_verdict",
|
|
@@ -5318,16 +6427,16 @@ async function runAnchorCompare(theme, themeDir, repoRoot) {
|
|
|
5318
6427
|
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
6428
|
};
|
|
5320
6429
|
}
|
|
5321
|
-
const familyDir =
|
|
5322
|
-
const rubricPath =
|
|
5323
|
-
if (!
|
|
6430
|
+
const familyDir = join21(repoRoot, "site", "design-system", "golden-set", family);
|
|
6431
|
+
const rubricPath = join21(familyDir, "rubric.md");
|
|
6432
|
+
if (!existsSync14(rubricPath)) {
|
|
5324
6433
|
return {
|
|
5325
6434
|
passed: true,
|
|
5326
6435
|
failures: [],
|
|
5327
6436
|
skipped: `Family ${family} rubric.md missing at ${rubricPath} \u2014 anchor-compare skipped.`
|
|
5328
6437
|
};
|
|
5329
6438
|
}
|
|
5330
|
-
const rubric =
|
|
6439
|
+
const rubric = readFileSync15(rubricPath, "utf8");
|
|
5331
6440
|
const anchorPngs = findAnchorPngs(familyDir);
|
|
5332
6441
|
if (anchorPngs.length === 0) {
|
|
5333
6442
|
return {
|
|
@@ -5343,7 +6452,7 @@ async function runAnchorCompare(theme, themeDir, repoRoot) {
|
|
|
5343
6452
|
skipped: `theme.yml has no render_url \u2014 anchor-compare skipped. Add render_url to enable.`
|
|
5344
6453
|
};
|
|
5345
6454
|
}
|
|
5346
|
-
const renderPath =
|
|
6455
|
+
const renderPath = join21(themeDir, ".anchor-render.png");
|
|
5347
6456
|
try {
|
|
5348
6457
|
captureRender(theme.render_url, renderPath);
|
|
5349
6458
|
} catch (e) {
|
|
@@ -5353,7 +6462,7 @@ async function runAnchorCompare(theme, themeDir, repoRoot) {
|
|
|
5353
6462
|
skipped: `Render capture failed: ${e.message}. Verify dev server is running at ${theme.render_url}. Gate skipped.`
|
|
5354
6463
|
};
|
|
5355
6464
|
}
|
|
5356
|
-
if (!
|
|
6465
|
+
if (!existsSync14(renderPath)) {
|
|
5357
6466
|
return {
|
|
5358
6467
|
passed: true,
|
|
5359
6468
|
failures: [],
|
|
@@ -5374,7 +6483,7 @@ async function runAnchorCompare(theme, themeDir, repoRoot) {
|
|
|
5374
6483
|
source: {
|
|
5375
6484
|
type: "base64",
|
|
5376
6485
|
media_type: "image/png",
|
|
5377
|
-
data:
|
|
6486
|
+
data: readFileSync15(p).toString("base64")
|
|
5378
6487
|
}
|
|
5379
6488
|
}));
|
|
5380
6489
|
const renderBlock = {
|
|
@@ -5382,7 +6491,7 @@ async function runAnchorCompare(theme, themeDir, repoRoot) {
|
|
|
5382
6491
|
source: {
|
|
5383
6492
|
type: "base64",
|
|
5384
6493
|
media_type: "image/png",
|
|
5385
|
-
data:
|
|
6494
|
+
data: readFileSync15(renderPath).toString("base64")
|
|
5386
6495
|
}
|
|
5387
6496
|
};
|
|
5388
6497
|
const userContent = [
|
|
@@ -5466,20 +6575,20 @@ Grade the render against each rubric axis. Return via report_anchor_verdict.`
|
|
|
5466
6575
|
return { passed: failures.length === 0, failures };
|
|
5467
6576
|
}
|
|
5468
6577
|
function findAnchorPngs(familyDir) {
|
|
5469
|
-
if (!
|
|
5470
|
-
const dirents =
|
|
5471
|
-
return dirents.filter((n) => /^[0-9]+-[a-z0-9-]+\.png$/.test(n)).map((n) =>
|
|
6578
|
+
if (!existsSync14(familyDir)) return [];
|
|
6579
|
+
const dirents = execSync5(`ls -1 "${familyDir}" 2>/dev/null`).toString().split("\n").filter(Boolean);
|
|
6580
|
+
return dirents.filter((n) => /^[0-9]+-[a-z0-9-]+\.png$/.test(n)).map((n) => join21(familyDir, n));
|
|
5472
6581
|
}
|
|
5473
6582
|
function captureRender(url, outPath) {
|
|
5474
|
-
const
|
|
6583
|
+
const chromeCandidates2 = [
|
|
5475
6584
|
"/Applications/Google Chrome.app/Contents/MacOS/Google Chrome",
|
|
5476
6585
|
"/Applications/Chromium.app/Contents/MacOS/Chromium",
|
|
5477
6586
|
"google-chrome",
|
|
5478
6587
|
"chromium"
|
|
5479
6588
|
];
|
|
5480
|
-
for (const c2 of
|
|
6589
|
+
for (const c2 of chromeCandidates2) {
|
|
5481
6590
|
try {
|
|
5482
|
-
|
|
6591
|
+
execSync5(`"${c2}" --headless --disable-gpu --hide-scrollbars --screenshot="${outPath}" --window-size=1440,1600 "${url}"`, {
|
|
5483
6592
|
stdio: ["ignore", "ignore", "ignore"],
|
|
5484
6593
|
timeout: 3e4
|
|
5485
6594
|
});
|
|
@@ -5488,13 +6597,13 @@ function captureRender(url, outPath) {
|
|
|
5488
6597
|
continue;
|
|
5489
6598
|
}
|
|
5490
6599
|
}
|
|
5491
|
-
throw new Error(`No headless Chrome found. Tried: ${
|
|
6600
|
+
throw new Error(`No headless Chrome found. Tried: ${chromeCandidates2.join(", ")}`);
|
|
5492
6601
|
}
|
|
5493
6602
|
|
|
5494
6603
|
// src/theme-harness/dismiss.ts
|
|
5495
6604
|
init_esm_shims();
|
|
5496
|
-
import { existsSync as
|
|
5497
|
-
import { join as
|
|
6605
|
+
import { existsSync as existsSync15, readFileSync as readFileSync16, writeFileSync as writeFileSync8, appendFileSync as appendFileSync2 } from "fs";
|
|
6606
|
+
import { join as join22 } from "path";
|
|
5498
6607
|
var DISMISSALS_FILE = "dismissals.jsonl";
|
|
5499
6608
|
function writeDismissal(themeDir, finding_id, reason, scope, now) {
|
|
5500
6609
|
const d = {
|
|
@@ -5504,18 +6613,18 @@ function writeDismissal(themeDir, finding_id, reason, scope, now) {
|
|
|
5504
6613
|
dismissed_at: now,
|
|
5505
6614
|
applied: false
|
|
5506
6615
|
};
|
|
5507
|
-
appendFileSync2(
|
|
6616
|
+
appendFileSync2(join22(themeDir, DISMISSALS_FILE), JSON.stringify(d) + "\n");
|
|
5508
6617
|
return d;
|
|
5509
6618
|
}
|
|
5510
6619
|
function loadDismissals(themeDir) {
|
|
5511
|
-
const p =
|
|
5512
|
-
if (!
|
|
5513
|
-
const raw =
|
|
6620
|
+
const p = join22(themeDir, DISMISSALS_FILE);
|
|
6621
|
+
if (!existsSync15(p)) return [];
|
|
6622
|
+
const raw = readFileSync16(p, "utf8").trim();
|
|
5514
6623
|
if (!raw) return [];
|
|
5515
6624
|
return raw.split("\n").filter(Boolean).map((line) => JSON.parse(line));
|
|
5516
6625
|
}
|
|
5517
6626
|
function persistDismissals(themeDir, ds) {
|
|
5518
|
-
const p =
|
|
6627
|
+
const p = join22(themeDir, DISMISSALS_FILE);
|
|
5519
6628
|
const body = ds.map((d) => JSON.stringify(d)).join("\n") + (ds.length ? "\n" : "");
|
|
5520
6629
|
writeFileSync8(p, body);
|
|
5521
6630
|
}
|
|
@@ -5537,7 +6646,7 @@ function claimDismissal(ds, finding_id, runTimestamp) {
|
|
|
5537
6646
|
|
|
5538
6647
|
// src/theme-harness/grade.ts
|
|
5539
6648
|
async function gradeTheme(slug, themeDir, now, repoRoot = process.cwd()) {
|
|
5540
|
-
const ymlPath =
|
|
6649
|
+
const ymlPath = join23(themeDir, "theme.yml");
|
|
5541
6650
|
const parsed = loadTheme(ymlPath);
|
|
5542
6651
|
if (!parsed.ok) return { error: parsed.errors };
|
|
5543
6652
|
const theme = parsed.theme;
|
|
@@ -5595,18 +6704,18 @@ async function gradeTheme(slug, themeDir, now, repoRoot = process.cwd()) {
|
|
|
5595
6704
|
}
|
|
5596
6705
|
const overall = gates.some((g) => !g.verdict.passed) ? "BLOCK" : "PASS";
|
|
5597
6706
|
const report = { slug, gates, overall, ran_at: now };
|
|
5598
|
-
const logPath =
|
|
6707
|
+
const logPath = join23(themeDir, "audit.log.jsonl");
|
|
5599
6708
|
appendFileSync3(logPath, JSON.stringify(report) + "\n");
|
|
5600
6709
|
if (dismissalsMutated) persistDismissals(themeDir, dismissals);
|
|
5601
6710
|
return report;
|
|
5602
6711
|
}
|
|
5603
6712
|
function readSectionSource(slug, repoRoot) {
|
|
5604
6713
|
const candidates = [
|
|
5605
|
-
|
|
5606
|
-
|
|
6714
|
+
join23(repoRoot, "site", "src", "components", "sections", `${slug}.tsx`),
|
|
6715
|
+
join23(repoRoot, "src", "components", "sections", `${slug}.tsx`)
|
|
5607
6716
|
];
|
|
5608
6717
|
for (const p of candidates) {
|
|
5609
|
-
if (
|
|
6718
|
+
if (existsSync16(p)) return readFileSync17(p, "utf8");
|
|
5610
6719
|
}
|
|
5611
6720
|
return void 0;
|
|
5612
6721
|
}
|
|
@@ -5680,15 +6789,15 @@ function formatReport(r) {
|
|
|
5680
6789
|
|
|
5681
6790
|
// src/theme-harness/promote.ts
|
|
5682
6791
|
init_esm_shims();
|
|
5683
|
-
import { readFileSync as
|
|
5684
|
-
import { join as
|
|
6792
|
+
import { readFileSync as readFileSync18, writeFileSync as writeFileSync9, existsSync as existsSync17, appendFileSync as appendFileSync4 } from "fs";
|
|
6793
|
+
import { join as join24 } from "path";
|
|
5685
6794
|
function regressionSetPath(themeDir) {
|
|
5686
|
-
return
|
|
6795
|
+
return join24(themeDir, "..", "..", "site", "design-system", "regression-set.jsonl");
|
|
5687
6796
|
}
|
|
5688
6797
|
function nextRegressionId(themeDir) {
|
|
5689
6798
|
const p = regressionSetPath(themeDir);
|
|
5690
|
-
if (!
|
|
5691
|
-
const raw =
|
|
6799
|
+
if (!existsSync17(p)) return "REG-0001";
|
|
6800
|
+
const raw = readFileSync18(p, "utf8");
|
|
5692
6801
|
let max = 0;
|
|
5693
6802
|
for (const line of raw.split("\n")) {
|
|
5694
6803
|
const m = line.match(/"id":\s*"REG-(\d+)"/);
|
|
@@ -5710,7 +6819,7 @@ function promoteToRegression(themeDir, input3) {
|
|
|
5710
6819
|
scope: "global"
|
|
5711
6820
|
};
|
|
5712
6821
|
const p = regressionSetPath(themeDir);
|
|
5713
|
-
if (!
|
|
6822
|
+
if (!existsSync17(p)) {
|
|
5714
6823
|
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
6824
|
}
|
|
5716
6825
|
appendFileSync4(p, JSON.stringify(rule2) + "\n");
|
|
@@ -5719,8 +6828,8 @@ function promoteToRegression(themeDir, input3) {
|
|
|
5719
6828
|
|
|
5720
6829
|
// src/theme-harness/waivers.ts
|
|
5721
6830
|
init_esm_shims();
|
|
5722
|
-
import { existsSync as
|
|
5723
|
-
import { join as
|
|
6831
|
+
import { existsSync as existsSync18, mkdirSync as mkdirSync8, readFileSync as readFileSync19, readdirSync as readdirSync4, statSync as statSync4, writeFileSync as writeFileSync10 } from "fs";
|
|
6832
|
+
import { join as join25, relative as relative5 } from "path";
|
|
5724
6833
|
var SKIP_DIRS = /* @__PURE__ */ new Set([
|
|
5725
6834
|
"node_modules",
|
|
5726
6835
|
".git",
|
|
@@ -5735,9 +6844,9 @@ var SKIP_DIRS = /* @__PURE__ */ new Set([
|
|
|
5735
6844
|
]);
|
|
5736
6845
|
var CODE_SUFFIXES = /\.(?:tsx?|jsx?|mjs|cjs|css|scss|md|ya?ml)$/;
|
|
5737
6846
|
function walk2(root, acc) {
|
|
5738
|
-
for (const entry of
|
|
6847
|
+
for (const entry of readdirSync4(root)) {
|
|
5739
6848
|
if (SKIP_DIRS.has(entry) || entry.startsWith(".")) continue;
|
|
5740
|
-
const full =
|
|
6849
|
+
const full = join25(root, entry);
|
|
5741
6850
|
let st;
|
|
5742
6851
|
try {
|
|
5743
6852
|
st = statSync4(full);
|
|
@@ -5775,7 +6884,7 @@ function collectWaivers(target) {
|
|
|
5775
6884
|
for (const abs of files) {
|
|
5776
6885
|
let body;
|
|
5777
6886
|
try {
|
|
5778
|
-
body =
|
|
6887
|
+
body = readFileSync19(abs, "utf8");
|
|
5779
6888
|
} catch {
|
|
5780
6889
|
continue;
|
|
5781
6890
|
}
|
|
@@ -5816,15 +6925,15 @@ function formatWaiverDigest(waivers, today) {
|
|
|
5816
6925
|
return lines.join("\n") + "\n";
|
|
5817
6926
|
}
|
|
5818
6927
|
function writeDigest(target, waivers, today) {
|
|
5819
|
-
const dir =
|
|
5820
|
-
if (!
|
|
6928
|
+
const dir = join25(target, "reports");
|
|
6929
|
+
if (!existsSync18(dir)) {
|
|
5821
6930
|
if (waivers.length === 0) return null;
|
|
5822
|
-
|
|
6931
|
+
mkdirSync8(dir, { recursive: true });
|
|
5823
6932
|
}
|
|
5824
6933
|
const digest = formatWaiverDigest(waivers, today);
|
|
5825
|
-
const outPath =
|
|
6934
|
+
const outPath = join25(dir, `qa-waivers-${today}.md`);
|
|
5826
6935
|
writeFileSync10(outPath, digest);
|
|
5827
|
-
writeFileSync10(
|
|
6936
|
+
writeFileSync10(join25(dir, "qa-waivers-latest.md"), digest);
|
|
5828
6937
|
return outPath;
|
|
5829
6938
|
}
|
|
5830
6939
|
|
|
@@ -5841,30 +6950,30 @@ function registerHarnessCommand(program2) {
|
|
|
5841
6950
|
const harness = program2.command("harness").description("v2 theme-authoring harness \u2014 materialize + grade + (later) dismiss.");
|
|
5842
6951
|
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
6952
|
const dir = resolveThemeDir(slug, opts);
|
|
5844
|
-
if (!
|
|
5845
|
-
console.error(
|
|
6953
|
+
if (!existsSync19(join26(dir, "theme.yml"))) {
|
|
6954
|
+
console.error(import_picocolors11.default.red(`\u2717 No theme.yml at ${join26(dir, "theme.yml")}`));
|
|
5846
6955
|
process.exit(1);
|
|
5847
6956
|
}
|
|
5848
6957
|
const result = materializeTheme(slug, dir);
|
|
5849
6958
|
if (!result.ok) {
|
|
5850
|
-
console.error(
|
|
5851
|
-
for (const e of result.errors) console.error(
|
|
6959
|
+
console.error(import_picocolors11.default.red(`\u2717 theme.yml validation failed:`));
|
|
6960
|
+
for (const e of result.errors) console.error(import_picocolors11.default.red(` \xB7 ${e}`));
|
|
5852
6961
|
process.exit(1);
|
|
5853
6962
|
}
|
|
5854
|
-
console.log(
|
|
6963
|
+
console.log(import_picocolors11.default.green(`\u2713 Materialized ${slug}:`));
|
|
5855
6964
|
for (const f of result.wrote) console.log(` \xB7 ${f}`);
|
|
5856
6965
|
});
|
|
5857
6966
|
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
6967
|
const dir = resolveThemeDir(slug, opts);
|
|
5859
|
-
if (!
|
|
5860
|
-
console.error(
|
|
6968
|
+
if (!existsSync19(join26(dir, "theme.yml"))) {
|
|
6969
|
+
console.error(import_picocolors11.default.red(`\u2717 No theme.yml at ${join26(dir, "theme.yml")}`));
|
|
5861
6970
|
process.exit(1);
|
|
5862
6971
|
}
|
|
5863
6972
|
const repoRoot = resolve13(opts.themesDir ?? DEFAULT_THEMES_DIR, "..");
|
|
5864
6973
|
const report = await gradeTheme(slug, dir, isoNow(), repoRoot);
|
|
5865
6974
|
if ("error" in report) {
|
|
5866
|
-
console.error(
|
|
5867
|
-
for (const e of report.error) console.error(
|
|
6975
|
+
console.error(import_picocolors11.default.red(`\u2717 theme.yml validation failed:`));
|
|
6976
|
+
for (const e of report.error) console.error(import_picocolors11.default.red(` \xB7 ${e}`));
|
|
5868
6977
|
process.exit(1);
|
|
5869
6978
|
}
|
|
5870
6979
|
console.log(formatReport(report));
|
|
@@ -5874,14 +6983,14 @@ function registerHarnessCommand(program2) {
|
|
|
5874
6983
|
"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
6984
|
).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
6985
|
const dir = resolveThemeDir(slug, opts);
|
|
5877
|
-
if (!
|
|
5878
|
-
console.error(
|
|
6986
|
+
if (!existsSync19(join26(dir, "theme.yml"))) {
|
|
6987
|
+
console.error(import_picocolors11.default.red(`\u2717 No theme.yml at ${join26(dir, "theme.yml")}`));
|
|
5879
6988
|
process.exit(1);
|
|
5880
6989
|
}
|
|
5881
6990
|
let matcher;
|
|
5882
6991
|
if (opts.matcherType === "hex_distance") {
|
|
5883
6992
|
if (!/^#[0-9A-Fa-f]{6}$/.test(opts.target)) {
|
|
5884
|
-
console.error(
|
|
6993
|
+
console.error(import_picocolors11.default.red(`\u2717 --target must be a 6-digit hex for hex_distance matcher, got: ${opts.target}`));
|
|
5885
6994
|
process.exit(1);
|
|
5886
6995
|
}
|
|
5887
6996
|
matcher = { type: "hex_distance", target: opts.target, threshold: parseInt(opts.threshold, 10) };
|
|
@@ -5890,7 +6999,7 @@ function registerHarnessCommand(program2) {
|
|
|
5890
6999
|
} else if (opts.matcherType === "inline_style") {
|
|
5891
7000
|
matcher = { type: "inline_style", pattern: opts.target };
|
|
5892
7001
|
} else {
|
|
5893
|
-
console.error(
|
|
7002
|
+
console.error(import_picocolors11.default.red(`\u2717 --matcher-type must be hex_distance | class_pattern | inline_style, got: ${opts.matcherType}`));
|
|
5894
7003
|
process.exit(1);
|
|
5895
7004
|
}
|
|
5896
7005
|
const rule2 = promoteToRegression(dir, {
|
|
@@ -5900,30 +7009,30 @@ function registerHarnessCommand(program2) {
|
|
|
5900
7009
|
matcher,
|
|
5901
7010
|
now: isoNow()
|
|
5902
7011
|
});
|
|
5903
|
-
console.log(
|
|
7012
|
+
console.log(import_picocolors11.default.green(`\u2713 Promoted to regression-set as ${rule2.id}`));
|
|
5904
7013
|
console.log(` matcher: ${JSON.stringify(matcher)}`);
|
|
5905
7014
|
console.log(` reason: ${rule2.reason}`);
|
|
5906
|
-
console.log(
|
|
7015
|
+
console.log(import_picocolors11.default.dim(` Rule now runs against every future grade. Delete from regression-set.jsonl to reverse.`));
|
|
5907
7016
|
});
|
|
5908
7017
|
harness.command("dismiss <slug> <finding-id>").description(
|
|
5909
7018
|
"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
7019
|
).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
7020
|
const dir = resolveThemeDir(slug, opts);
|
|
5912
|
-
if (!
|
|
5913
|
-
console.error(
|
|
7021
|
+
if (!existsSync19(join26(dir, "theme.yml"))) {
|
|
7022
|
+
console.error(import_picocolors11.default.red(`\u2717 No theme.yml at ${join26(dir, "theme.yml")}`));
|
|
5914
7023
|
process.exit(1);
|
|
5915
7024
|
}
|
|
5916
7025
|
if (opts.scope !== "this_run" && opts.scope !== "this_theme") {
|
|
5917
|
-
console.error(
|
|
7026
|
+
console.error(import_picocolors11.default.red(`\u2717 --scope must be "this_run" or "this_theme", got: ${opts.scope}`));
|
|
5918
7027
|
process.exit(1);
|
|
5919
7028
|
}
|
|
5920
7029
|
const d = writeDismissal(dir, findingId, opts.reason, opts.scope, isoNow());
|
|
5921
|
-
console.log(
|
|
7030
|
+
console.log(import_picocolors11.default.green(`\u2713 Dismissed ${findingId} for ${slug} (scope: ${d.scope})`));
|
|
5922
7031
|
console.log(` reason: ${d.reason}`);
|
|
5923
7032
|
if (d.scope === "this_run") {
|
|
5924
|
-
console.log(
|
|
7033
|
+
console.log(import_picocolors11.default.dim(` Applies to the next grade run only. If ${findingId} fires again after that, it will BLOCK.`));
|
|
5925
7034
|
} else {
|
|
5926
|
-
console.log(
|
|
7035
|
+
console.log(import_picocolors11.default.dim(` Applies to every future grade run for this theme. Delete the entry from dismissals.jsonl to reverse.`));
|
|
5927
7036
|
}
|
|
5928
7037
|
});
|
|
5929
7038
|
harness.command("waivers").description(
|
|
@@ -5933,10 +7042,10 @@ function registerHarnessCommand(program2) {
|
|
|
5933
7042
|
const waivers = collectWaivers(target);
|
|
5934
7043
|
const today = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
|
|
5935
7044
|
if (waivers.length === 0) {
|
|
5936
|
-
console.log(
|
|
7045
|
+
console.log(import_picocolors11.default.green(`\u2713 No slop-override waivers found in ${target}.`));
|
|
5937
7046
|
return;
|
|
5938
7047
|
}
|
|
5939
|
-
console.log(
|
|
7048
|
+
console.log(import_picocolors11.default.bold(`\u25C7 Waivers \u2014 ${waivers.length} static override${waivers.length === 1 ? "" : "s"} across the tree`));
|
|
5940
7049
|
console.log("");
|
|
5941
7050
|
const byRule = /* @__PURE__ */ new Map();
|
|
5942
7051
|
for (const w of waivers) {
|
|
@@ -5944,37 +7053,43 @@ function registerHarnessCommand(program2) {
|
|
|
5944
7053
|
byRule.get(w.ruleId).push(w);
|
|
5945
7054
|
}
|
|
5946
7055
|
for (const [ruleId, ws] of byRule) {
|
|
5947
|
-
console.log(
|
|
7056
|
+
console.log(import_picocolors11.default.yellow(` ${ruleId}`) + import_picocolors11.default.dim(` \u2014 ${ws.length} waiver${ws.length === 1 ? "" : "s"}`));
|
|
5948
7057
|
for (const w of ws.slice(0, 8)) {
|
|
5949
|
-
const scope = w.scope === "file" ?
|
|
5950
|
-
const reason = w.reason ||
|
|
5951
|
-
console.log(` ${scope} ${
|
|
7058
|
+
const scope = w.scope === "file" ? import_picocolors11.default.magenta("[file]") : import_picocolors11.default.cyan("[local]");
|
|
7059
|
+
const reason = w.reason || import_picocolors11.default.dim("(no reason given)");
|
|
7060
|
+
console.log(` ${scope} ${import_picocolors11.default.cyan(w.file + ":" + w.line)}`);
|
|
5952
7061
|
console.log(` ${reason}`);
|
|
5953
7062
|
}
|
|
5954
|
-
if (ws.length > 8) console.log(
|
|
7063
|
+
if (ws.length > 8) console.log(import_picocolors11.default.dim(` \u2026 +${ws.length - 8} more`));
|
|
5955
7064
|
}
|
|
5956
7065
|
console.log("");
|
|
5957
7066
|
if (opts.write) {
|
|
5958
7067
|
const path2 = writeDigest(target, waivers, today);
|
|
5959
|
-
if (path2) console.log(
|
|
7068
|
+
if (path2) console.log(import_picocolors11.default.dim(` Digest written to ${path2.replace(target + "/", "")}`));
|
|
5960
7069
|
}
|
|
5961
7070
|
});
|
|
5962
7071
|
}
|
|
5963
7072
|
|
|
5964
7073
|
// src/index.ts
|
|
5965
|
-
var
|
|
7074
|
+
var import_picocolors12 = __toESM(require_picocolors(), 1);
|
|
5966
7075
|
var program = new Command();
|
|
5967
7076
|
program.name("stylecrib").description(
|
|
5968
7077
|
"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(
|
|
7078
|
+
).version(VERSION);
|
|
5970
7079
|
program.command("init").description(
|
|
5971
7080
|
"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
7081
|
).option(
|
|
5973
7082
|
"-s, --style <direction>",
|
|
5974
|
-
"Skip the
|
|
7083
|
+
"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
7084
|
).option(
|
|
5976
7085
|
"-y, --yes",
|
|
5977
|
-
"
|
|
7086
|
+
"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."
|
|
7087
|
+
).option(
|
|
7088
|
+
"--apply",
|
|
7089
|
+
"Apply staged files without asking. Overwrites tailwind.config.* and src/index.css on a new git branch."
|
|
7090
|
+
).option(
|
|
7091
|
+
"--no-apply",
|
|
7092
|
+
"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
7093
|
).option(
|
|
5979
7094
|
"--target <path>",
|
|
5980
7095
|
"Target project root (defaults to current working directory)"
|
|
@@ -5989,7 +7104,7 @@ program.command("init").description(
|
|
|
5989
7104
|
"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
7105
|
).option(
|
|
5991
7106
|
"--force-runtime",
|
|
5992
|
-
"Overwrite src/lib/stylecrib-wire.tsx if it exists. Existing content is backed up to `src/lib/stylecrib-wire.tsx.pre-
|
|
7107
|
+
"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
7108
|
).action(init);
|
|
5994
7109
|
program.command("apply").description(
|
|
5995
7110
|
"Apply staged changes from .design-upgrade/ to your project (on a new git branch)."
|
|
@@ -6027,6 +7142,27 @@ program.command("install-hooks").description(
|
|
|
6027
7142
|
program.command("hook-check", { hidden: true }).description(
|
|
6028
7143
|
"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
7144
|
).action(hookCheck);
|
|
7145
|
+
program.command("verify").description(
|
|
7146
|
+
"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."
|
|
7147
|
+
).option(
|
|
7148
|
+
"--target <path>",
|
|
7149
|
+
"Project root (defaults to current working directory)"
|
|
7150
|
+
).option(
|
|
7151
|
+
"--no-typecheck",
|
|
7152
|
+
"Skip the typecheck step."
|
|
7153
|
+
).option(
|
|
7154
|
+
"--no-boot",
|
|
7155
|
+
"Skip the dev-server boot. Leaves only the typecheck \u2014 useful in CI without a browser."
|
|
7156
|
+
).option(
|
|
7157
|
+
"--route <path>",
|
|
7158
|
+
"Route to fetch once the server is up (default: /)"
|
|
7159
|
+
).option(
|
|
7160
|
+
"--screenshot <file>",
|
|
7161
|
+
"Also capture a headless-Chrome screenshot of the route to this path."
|
|
7162
|
+
).option(
|
|
7163
|
+
"--timeout <ms>",
|
|
7164
|
+
"How long to wait for the dev server to announce a URL (default: 60000)"
|
|
7165
|
+
).action(verify);
|
|
6030
7166
|
program.command("status").description(
|
|
6031
7167
|
"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
7168
|
).option(
|
|
@@ -6057,12 +7193,15 @@ program.command("reject").description(
|
|
|
6057
7193
|
target: opts.target
|
|
6058
7194
|
});
|
|
6059
7195
|
});
|
|
7196
|
+
program.command("mcp").description(
|
|
7197
|
+
"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`."
|
|
7198
|
+
).action(mcp);
|
|
6060
7199
|
registerHarnessCommand(program);
|
|
6061
7200
|
program.parseAsync(process.argv).catch((err) => {
|
|
6062
7201
|
console.error("");
|
|
6063
|
-
console.error(
|
|
7202
|
+
console.error(import_picocolors12.default.red("\u2717 ") + (err instanceof Error ? err.message : String(err)));
|
|
6064
7203
|
if (err instanceof Error && err.cause) {
|
|
6065
|
-
console.error(
|
|
7204
|
+
console.error(import_picocolors12.default.dim(" caused by: " + String(err.cause)));
|
|
6066
7205
|
}
|
|
6067
7206
|
process.exit(1);
|
|
6068
7207
|
});
|