@agent-scope/cli 1.18.0 → 1.19.0

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/dist/index.cjs CHANGED
@@ -118,6 +118,21 @@ import { createElement } from "react";
118
118
  // Suppress "React must be in scope" warnings from old JSX (we use automatic)
119
119
  banner: {
120
120
  js: "/* @agent-scope/cli component harness */"
121
+ },
122
+ // CSS imports (e.g. `import './styles.css'`) are handled at the page level via
123
+ // globalCSS injection. Tell esbuild to treat CSS files as empty modules so
124
+ // components that import CSS directly (e.g. App.tsx) don't error during bundling.
125
+ loader: {
126
+ ".css": "empty",
127
+ ".svg": "dataurl",
128
+ ".png": "dataurl",
129
+ ".jpg": "dataurl",
130
+ ".jpeg": "dataurl",
131
+ ".gif": "dataurl",
132
+ ".webp": "dataurl",
133
+ ".ttf": "dataurl",
134
+ ".woff": "dataurl",
135
+ ".woff2": "dataurl"
121
136
  }
122
137
  });
123
138
  if (result.errors.length > 0) {
@@ -525,6 +540,57 @@ async function getCompiledCssForClasses(cwd, classes) {
525
540
  if (deduped.length === 0) return null;
526
541
  return build3(deduped);
527
542
  }
543
+ async function compileGlobalCssFile(cssFilePath, cwd) {
544
+ const { existsSync: existsSync16, readFileSync: readFileSync14 } = await import('fs');
545
+ const { createRequire: createRequire3 } = await import('module');
546
+ if (!existsSync16(cssFilePath)) return null;
547
+ const raw = readFileSync14(cssFilePath, "utf-8");
548
+ const needsCompile = /@tailwind|@import\s+['"]tailwindcss/.test(raw);
549
+ if (!needsCompile) {
550
+ return raw;
551
+ }
552
+ try {
553
+ const require2 = createRequire3(path.resolve(cwd, "package.json"));
554
+ let postcss;
555
+ let twPlugin;
556
+ try {
557
+ postcss = require2("postcss");
558
+ twPlugin = require2("tailwindcss");
559
+ } catch {
560
+ return raw;
561
+ }
562
+ let autoprefixerPlugin;
563
+ try {
564
+ autoprefixerPlugin = require2("autoprefixer");
565
+ } catch {
566
+ autoprefixerPlugin = null;
567
+ }
568
+ const plugins = autoprefixerPlugin ? [twPlugin, autoprefixerPlugin] : [twPlugin];
569
+ const result = await postcss(plugins).process(raw, {
570
+ from: cssFilePath,
571
+ to: cssFilePath
572
+ });
573
+ return result.css;
574
+ } catch (err) {
575
+ process.stderr.write(
576
+ `[scope/render] Warning: CSS compilation failed for ${cssFilePath}: ${err instanceof Error ? err.message : String(err)}
577
+ `
578
+ );
579
+ return raw;
580
+ }
581
+ }
582
+ async function loadGlobalCss(globalCssFiles, cwd) {
583
+ if (globalCssFiles.length === 0) return null;
584
+ const parts = [];
585
+ for (const relPath of globalCssFiles) {
586
+ const absPath = path.resolve(cwd, relPath);
587
+ const css = await compileGlobalCssFile(absPath, cwd);
588
+ if (css !== null && css.trim().length > 0) {
589
+ parts.push(css);
590
+ }
591
+ }
592
+ return parts.length > 0 ? parts.join("\n") : null;
593
+ }
528
594
 
529
595
  // src/ci/commands.ts
530
596
  var CI_EXIT = {
@@ -947,6 +1013,157 @@ function createCiCommand() {
947
1013
  }
948
1014
  );
949
1015
  }
1016
+ function collectSourceFiles(dir) {
1017
+ if (!fs.existsSync(dir)) return [];
1018
+ const results = [];
1019
+ for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
1020
+ const full = path.join(dir, entry.name);
1021
+ if (entry.isDirectory() && entry.name !== "node_modules" && entry.name !== ".reactscope") {
1022
+ results.push(...collectSourceFiles(full));
1023
+ } else if (entry.isFile() && /\.(tsx?|jsx?)$/.test(entry.name)) {
1024
+ results.push(full);
1025
+ }
1026
+ }
1027
+ return results;
1028
+ }
1029
+ function checkConfig(cwd) {
1030
+ const configPath = path.resolve(cwd, "reactscope.config.json");
1031
+ if (!fs.existsSync(configPath)) {
1032
+ return {
1033
+ name: "config",
1034
+ status: "error",
1035
+ message: "reactscope.config.json not found \u2014 run `scope init`"
1036
+ };
1037
+ }
1038
+ try {
1039
+ JSON.parse(fs.readFileSync(configPath, "utf-8"));
1040
+ return { name: "config", status: "ok", message: "reactscope.config.json valid" };
1041
+ } catch {
1042
+ return { name: "config", status: "error", message: "reactscope.config.json is not valid JSON" };
1043
+ }
1044
+ }
1045
+ function checkTokens(cwd) {
1046
+ const configPath = path.resolve(cwd, "reactscope.config.json");
1047
+ let tokensPath = path.resolve(cwd, "reactscope.tokens.json");
1048
+ if (fs.existsSync(configPath)) {
1049
+ try {
1050
+ const cfg = JSON.parse(fs.readFileSync(configPath, "utf-8"));
1051
+ if (cfg.tokens?.file) tokensPath = path.resolve(cwd, cfg.tokens.file);
1052
+ } catch {
1053
+ }
1054
+ }
1055
+ if (!fs.existsSync(tokensPath)) {
1056
+ return {
1057
+ name: "tokens",
1058
+ status: "warn",
1059
+ message: `Token file not found at ${tokensPath} \u2014 run \`scope init\``
1060
+ };
1061
+ }
1062
+ try {
1063
+ const raw = JSON.parse(fs.readFileSync(tokensPath, "utf-8"));
1064
+ if (!raw.version) {
1065
+ return { name: "tokens", status: "warn", message: "Token file is missing a `version` field" };
1066
+ }
1067
+ return { name: "tokens", status: "ok", message: "Token file valid" };
1068
+ } catch {
1069
+ return { name: "tokens", status: "error", message: "Token file is not valid JSON" };
1070
+ }
1071
+ }
1072
+ function checkGlobalCss(cwd) {
1073
+ const configPath = path.resolve(cwd, "reactscope.config.json");
1074
+ let globalCss = [];
1075
+ if (fs.existsSync(configPath)) {
1076
+ try {
1077
+ const cfg = JSON.parse(fs.readFileSync(configPath, "utf-8"));
1078
+ globalCss = cfg.components?.wrappers?.globalCSS ?? [];
1079
+ } catch {
1080
+ }
1081
+ }
1082
+ if (globalCss.length === 0) {
1083
+ return {
1084
+ name: "globalCSS",
1085
+ status: "warn",
1086
+ message: "No globalCSS configured \u2014 Tailwind styles won't apply to renders. Add `components.wrappers.globalCSS` to reactscope.config.json"
1087
+ };
1088
+ }
1089
+ const missing = globalCss.filter((f) => !fs.existsSync(path.resolve(cwd, f)));
1090
+ if (missing.length > 0) {
1091
+ return {
1092
+ name: "globalCSS",
1093
+ status: "error",
1094
+ message: `globalCSS file(s) not found: ${missing.join(", ")}`
1095
+ };
1096
+ }
1097
+ return {
1098
+ name: "globalCSS",
1099
+ status: "ok",
1100
+ message: `${globalCss.length} globalCSS file(s) present`
1101
+ };
1102
+ }
1103
+ function checkManifest(cwd) {
1104
+ const manifestPath = path.resolve(cwd, ".reactscope", "manifest.json");
1105
+ if (!fs.existsSync(manifestPath)) {
1106
+ return {
1107
+ name: "manifest",
1108
+ status: "warn",
1109
+ message: "Manifest not found \u2014 run `scope manifest generate`"
1110
+ };
1111
+ }
1112
+ const manifestMtime = fs.statSync(manifestPath).mtimeMs;
1113
+ const sourceDir = path.resolve(cwd, "src");
1114
+ const sourceFiles = collectSourceFiles(sourceDir);
1115
+ const stale = sourceFiles.filter((f) => fs.statSync(f).mtimeMs > manifestMtime);
1116
+ if (stale.length > 0) {
1117
+ return {
1118
+ name: "manifest",
1119
+ status: "warn",
1120
+ message: `Manifest may be stale \u2014 ${stale.length} source file(s) modified since last generate. Run \`scope manifest generate\``
1121
+ };
1122
+ }
1123
+ return { name: "manifest", status: "ok", message: "Manifest present and up to date" };
1124
+ }
1125
+ var ICONS = { ok: "\u2713", warn: "!", error: "\u2717" };
1126
+ function formatCheck(check) {
1127
+ return ` [${ICONS[check.status]}] ${check.name.padEnd(12)} ${check.message}`;
1128
+ }
1129
+ function createDoctorCommand() {
1130
+ return new commander.Command("doctor").description("Check the health of your Scope setup (config, tokens, CSS, manifest)").option("--json", "Emit structured JSON output", false).action((opts) => {
1131
+ const cwd = process.cwd();
1132
+ const checks = [
1133
+ checkConfig(cwd),
1134
+ checkTokens(cwd),
1135
+ checkGlobalCss(cwd),
1136
+ checkManifest(cwd)
1137
+ ];
1138
+ const errors = checks.filter((c) => c.status === "error").length;
1139
+ const warnings = checks.filter((c) => c.status === "warn").length;
1140
+ if (opts.json) {
1141
+ process.stdout.write(
1142
+ `${JSON.stringify({ passed: checks.length - errors - warnings, warnings, errors, checks }, null, 2)}
1143
+ `
1144
+ );
1145
+ if (errors > 0) process.exit(1);
1146
+ return;
1147
+ }
1148
+ process.stdout.write("\nScope Doctor\n");
1149
+ process.stdout.write("\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\n");
1150
+ for (const check of checks) process.stdout.write(`${formatCheck(check)}
1151
+ `);
1152
+ process.stdout.write("\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\n");
1153
+ if (errors > 0) {
1154
+ process.stdout.write(` ${errors} error(s), ${warnings} warning(s)
1155
+
1156
+ `);
1157
+ process.exit(1);
1158
+ } else if (warnings > 0) {
1159
+ process.stdout.write(` ${warnings} warning(s) \u2014 everything works but could be better
1160
+
1161
+ `);
1162
+ } else {
1163
+ process.stdout.write(" All checks passed!\n\n");
1164
+ }
1165
+ });
1166
+ }
950
1167
  function hasConfigFile(dir, stem) {
951
1168
  if (!fs.existsSync(dir)) return false;
952
1169
  try {
@@ -1028,6 +1245,20 @@ function detectComponentPatterns(rootDir, typescript) {
1028
1245
  }
1029
1246
  return unique;
1030
1247
  }
1248
+ var GLOBAL_CSS_CANDIDATES = [
1249
+ "src/styles.css",
1250
+ "src/index.css",
1251
+ "src/global.css",
1252
+ "src/globals.css",
1253
+ "src/app.css",
1254
+ "src/main.css",
1255
+ "styles/globals.css",
1256
+ "styles/global.css",
1257
+ "styles/index.css"
1258
+ ];
1259
+ function detectGlobalCSSFiles(rootDir) {
1260
+ return GLOBAL_CSS_CANDIDATES.filter((rel) => fs.existsSync(path.join(rootDir, rel)));
1261
+ }
1031
1262
  var TAILWIND_STEMS = ["tailwind.config"];
1032
1263
  var CSS_EXTS = [".css", ".scss", ".sass", ".less"];
1033
1264
  var THEME_SUFFIXES = [".theme.ts", ".theme.js", ".theme.tsx"];
@@ -1095,13 +1326,15 @@ function detectProject(rootDir) {
1095
1326
  const packageManager = detectPackageManager(rootDir);
1096
1327
  const componentPatterns = detectComponentPatterns(rootDir, typescript);
1097
1328
  const tokenSources = detectTokenSources(rootDir);
1329
+ const globalCSSFiles = detectGlobalCSSFiles(rootDir);
1098
1330
  return {
1099
1331
  framework,
1100
1332
  typescript,
1101
1333
  tsconfigPath,
1102
1334
  componentPatterns,
1103
1335
  tokenSources,
1104
- packageManager
1336
+ packageManager,
1337
+ globalCSSFiles
1105
1338
  };
1106
1339
  }
1107
1340
  function buildDefaultConfig(detected, tokenFile, outputDir) {
@@ -1110,7 +1343,7 @@ function buildDefaultConfig(detected, tokenFile, outputDir) {
1110
1343
  components: {
1111
1344
  include,
1112
1345
  exclude: ["**/*.test.tsx", "**/*.stories.tsx"],
1113
- wrappers: { providers: [], globalCSS: [] }
1346
+ wrappers: { providers: [], globalCSS: detected.globalCSSFiles ?? [] }
1114
1347
  },
1115
1348
  render: {
1116
1349
  viewport: { default: { width: 1280, height: 800 } },
@@ -1141,9 +1374,9 @@ function createRL() {
1141
1374
  });
1142
1375
  }
1143
1376
  async function ask(rl, question) {
1144
- return new Promise((resolve18) => {
1377
+ return new Promise((resolve19) => {
1145
1378
  rl.question(question, (answer) => {
1146
- resolve18(answer.trim());
1379
+ resolve19(answer.trim());
1147
1380
  });
1148
1381
  });
1149
1382
  }
@@ -1168,18 +1401,118 @@ function ensureGitignoreEntry(rootDir, entry) {
1168
1401
  `);
1169
1402
  }
1170
1403
  }
1404
+ function extractTailwindTokens(tokenSources) {
1405
+ const tailwindSource = tokenSources.find((s) => s.kind === "tailwind-config");
1406
+ if (!tailwindSource) return null;
1407
+ try {
1408
+ let parseBlock2 = function(block) {
1409
+ const result = {};
1410
+ const lineRe = /['"]?(\w[\w.-]*|\d+)['"]?\s*:\s*['"]?(#[0-9a-fA-F]{3,8}|\d+(?:px|rem|em|%)|[\w-]+(?:\/[\w]+)?)['"]?/g;
1411
+ for (const m of block.matchAll(lineRe)) {
1412
+ if (m[1] !== void 0 && m[2] !== void 0) {
1413
+ result[m[1]] = m[2];
1414
+ }
1415
+ }
1416
+ return result;
1417
+ };
1418
+ var parseBlock = parseBlock2;
1419
+ const raw = fs.readFileSync(tailwindSource.path, "utf-8");
1420
+ const tokens = {};
1421
+ const colorsKeyIdx = raw.indexOf("colors:");
1422
+ if (colorsKeyIdx !== -1) {
1423
+ const colorsBraceStart = raw.indexOf("{", colorsKeyIdx);
1424
+ if (colorsBraceStart !== -1) {
1425
+ let colorDepth = 0;
1426
+ let colorsBraceEnd = -1;
1427
+ for (let ci = colorsBraceStart; ci < raw.length; ci++) {
1428
+ if (raw[ci] === "{") colorDepth++;
1429
+ else if (raw[ci] === "}") {
1430
+ colorDepth--;
1431
+ if (colorDepth === 0) {
1432
+ colorsBraceEnd = ci;
1433
+ break;
1434
+ }
1435
+ }
1436
+ }
1437
+ if (colorsBraceEnd > colorsBraceStart) {
1438
+ const colorSection = raw.slice(colorsBraceStart + 1, colorsBraceEnd);
1439
+ const scaleRe = /(\w+)\s*:\s*\{([^}]+)\}/g;
1440
+ const colorTokens = {};
1441
+ for (const sm of colorSection.matchAll(scaleRe)) {
1442
+ if (sm[1] === void 0 || sm[2] === void 0) continue;
1443
+ const scaleName = sm[1];
1444
+ const scaleValues = parseBlock2(sm[2]);
1445
+ if (Object.keys(scaleValues).length > 0) {
1446
+ const scaleTokens = {};
1447
+ for (const [step, hex] of Object.entries(scaleValues)) {
1448
+ scaleTokens[step] = { value: hex, type: "color" };
1449
+ }
1450
+ colorTokens[scaleName] = scaleTokens;
1451
+ }
1452
+ }
1453
+ if (Object.keys(colorTokens).length > 0) {
1454
+ tokens.color = colorTokens;
1455
+ }
1456
+ }
1457
+ }
1458
+ }
1459
+ const spacingMatch = raw.match(/spacing\s*:\s*\{([\s\S]*?)\n\s*\}/);
1460
+ if (spacingMatch?.[1] !== void 0) {
1461
+ const spacingValues = parseBlock2(spacingMatch[1]);
1462
+ if (Object.keys(spacingValues).length > 0) {
1463
+ const spacingTokens = {};
1464
+ for (const [key, val] of Object.entries(spacingValues)) {
1465
+ spacingTokens[key] = { value: val, type: "dimension" };
1466
+ }
1467
+ tokens.spacing = spacingTokens;
1468
+ }
1469
+ }
1470
+ const fontFamilyMatch = raw.match(/fontFamily\s*:\s*\{([\s\S]*?)\n\s*\}/);
1471
+ if (fontFamilyMatch?.[1] !== void 0) {
1472
+ const fontFamilyRe = /(\w+)\s*:\s*\[\s*['"]([^'"]+)['"]/g;
1473
+ const fontTokens = {};
1474
+ for (const fm of fontFamilyMatch[1].matchAll(fontFamilyRe)) {
1475
+ if (fm[1] !== void 0 && fm[2] !== void 0) {
1476
+ fontTokens[fm[1]] = { value: fm[2], type: "fontFamily" };
1477
+ }
1478
+ }
1479
+ if (Object.keys(fontTokens).length > 0) {
1480
+ tokens.font = fontTokens;
1481
+ }
1482
+ }
1483
+ const borderRadiusMatch = raw.match(/borderRadius\s*:\s*\{([\s\S]*?)\n\s*\}/);
1484
+ if (borderRadiusMatch?.[1] !== void 0) {
1485
+ const radiusValues = parseBlock2(borderRadiusMatch[1]);
1486
+ if (Object.keys(radiusValues).length > 0) {
1487
+ const radiusTokens = {};
1488
+ for (const [key, val] of Object.entries(radiusValues)) {
1489
+ radiusTokens[key] = { value: val, type: "dimension" };
1490
+ }
1491
+ tokens.radius = radiusTokens;
1492
+ }
1493
+ }
1494
+ return Object.keys(tokens).length > 0 ? tokens : null;
1495
+ } catch {
1496
+ return null;
1497
+ }
1498
+ }
1171
1499
  function scaffoldConfig(rootDir, config) {
1172
1500
  const path$1 = path.join(rootDir, "reactscope.config.json");
1173
1501
  fs.writeFileSync(path$1, `${JSON.stringify(config, null, 2)}
1174
1502
  `);
1175
1503
  return path$1;
1176
1504
  }
1177
- function scaffoldTokenFile(rootDir, tokenFile) {
1505
+ function scaffoldTokenFile(rootDir, tokenFile, extractedTokens) {
1178
1506
  const path$1 = path.join(rootDir, tokenFile);
1179
1507
  if (!fs.existsSync(path$1)) {
1180
1508
  const stub = {
1181
1509
  $schema: "https://raw.githubusercontent.com/FlatFilers/Scope/main/packages/tokens/schema.json",
1182
- tokens: {}
1510
+ version: "1.0.0",
1511
+ meta: {
1512
+ name: "Design Tokens",
1513
+ lastUpdated: (/* @__PURE__ */ new Date()).toISOString().split("T")[0]
1514
+ },
1515
+ tokens: extractedTokens ?? {}
1183
1516
  };
1184
1517
  fs.writeFileSync(path$1, `${JSON.stringify(stub, null, 2)}
1185
1518
  `);
@@ -1257,7 +1590,13 @@ async function runInit(options) {
1257
1590
  }
1258
1591
  const cfgPath = scaffoldConfig(rootDir, config);
1259
1592
  created.push(cfgPath);
1260
- const tokPath = scaffoldTokenFile(rootDir, config.tokens.file);
1593
+ const extractedTokens = extractTailwindTokens(detected.tokenSources);
1594
+ if (extractedTokens !== null) {
1595
+ const tokenGroupCount = Object.keys(extractedTokens).length;
1596
+ process.stdout.write(` Extracted ${tokenGroupCount} token group(s) from Tailwind config
1597
+ `);
1598
+ }
1599
+ const tokPath = scaffoldTokenFile(rootDir, config.tokens.file, extractedTokens ?? void 0);
1261
1600
  created.push(tokPath);
1262
1601
  const outDirPath = scaffoldOutputDir(rootDir, config.output.dir);
1263
1602
  created.push(outDirPath);
@@ -1268,7 +1607,28 @@ async function runInit(options) {
1268
1607
  process.stdout.write(` ${p}
1269
1608
  `);
1270
1609
  }
1271
- process.stdout.write("\n Next steps: run `scope manifest` to scan your components.\n\n");
1610
+ process.stdout.write("\n Scanning components...\n");
1611
+ try {
1612
+ const manifestConfig = {
1613
+ include: config.components.include,
1614
+ rootDir
1615
+ };
1616
+ const manifest$1 = await manifest.generateManifest(manifestConfig);
1617
+ const manifestCount = Object.keys(manifest$1.components).length;
1618
+ const manifestOutPath = path.join(rootDir, config.output.dir, "manifest.json");
1619
+ fs.mkdirSync(path.join(rootDir, config.output.dir), { recursive: true });
1620
+ fs.writeFileSync(manifestOutPath, `${JSON.stringify(manifest$1, null, 2)}
1621
+ `);
1622
+ process.stdout.write(
1623
+ ` Found ${manifestCount} component(s) \u2014 manifest written to ${manifestOutPath}
1624
+ `
1625
+ );
1626
+ } catch {
1627
+ process.stdout.write(
1628
+ " (manifest generate skipped \u2014 run `scope manifest generate` manually)\n"
1629
+ );
1630
+ }
1631
+ process.stdout.write("\n");
1272
1632
  return {
1273
1633
  success: true,
1274
1634
  message: "Project initialised successfully.",
@@ -1357,7 +1717,10 @@ Available: ${available}${hint}`
1357
1717
  });
1358
1718
  }
1359
1719
  function registerQuery(manifestCmd) {
1360
- manifestCmd.command("query").description("Query components by attributes").option("--context <name>", "Find components consuming a context").option("--hook <name>", "Find components using a specific hook").option("--complexity <class>", "Filter by complexity class: simple or complex").option("--side-effects", "Find components with any side effects", false).option("--has-fetch", "Find components with fetch calls", false).option("--format <fmt>", "Output format: json or table (default: auto-detect)").option("--manifest <path>", "Path to manifest.json", MANIFEST_PATH).action(
1720
+ manifestCmd.command("query").description("Query components by attributes").option("--context <name>", "Find components consuming a context").option("--hook <name>", "Find components using a specific hook").option("--complexity <class>", "Filter by complexity class: simple or complex").option("--side-effects", "Find components with any side effects", false).option("--has-fetch", "Find components with fetch calls", false).option(
1721
+ "--has-prop <spec>",
1722
+ "Find components with a prop matching name or name:type (e.g. 'loading' or 'variant:union')"
1723
+ ).option("--composed-by <name>", "Find components that compose the named component").option("--format <fmt>", "Output format: json or table (default: auto-detect)").option("--manifest <path>", "Path to manifest.json", MANIFEST_PATH).action(
1361
1724
  (opts) => {
1362
1725
  try {
1363
1726
  const manifest = loadManifest(opts.manifest);
@@ -1368,9 +1731,11 @@ function registerQuery(manifestCmd) {
1368
1731
  if (opts.complexity !== void 0) queryParts.push(`complexity=${opts.complexity}`);
1369
1732
  if (opts.sideEffects) queryParts.push("side-effects");
1370
1733
  if (opts.hasFetch) queryParts.push("has-fetch");
1734
+ if (opts.hasProp !== void 0) queryParts.push(`has-prop=${opts.hasProp}`);
1735
+ if (opts.composedBy !== void 0) queryParts.push(`composed-by=${opts.composedBy}`);
1371
1736
  if (queryParts.length === 0) {
1372
1737
  process.stderr.write(
1373
- "No query flags specified. Use --context, --hook, --complexity, --side-effects, or --has-fetch.\n"
1738
+ "No query flags specified. Use --context, --hook, --complexity, --side-effects, --has-fetch, --has-prop, or --composed-by.\n"
1374
1739
  );
1375
1740
  process.exit(1);
1376
1741
  }
@@ -1397,6 +1762,27 @@ function registerQuery(manifestCmd) {
1397
1762
  if (opts.hasFetch) {
1398
1763
  entries = entries.filter(([, d]) => d.sideEffects.fetches.length > 0);
1399
1764
  }
1765
+ if (opts.hasProp !== void 0) {
1766
+ const spec = opts.hasProp;
1767
+ const colonIdx = spec.indexOf(":");
1768
+ const propName = colonIdx >= 0 ? spec.slice(0, colonIdx) : spec;
1769
+ const propType = colonIdx >= 0 ? spec.slice(colonIdx + 1) : void 0;
1770
+ entries = entries.filter(([, d]) => {
1771
+ const props = d.props;
1772
+ if (!props || !(propName in props)) return false;
1773
+ if (propType !== void 0) {
1774
+ return props[propName]?.type === propType;
1775
+ }
1776
+ return true;
1777
+ });
1778
+ }
1779
+ if (opts.composedBy !== void 0) {
1780
+ const targetName = opts.composedBy;
1781
+ entries = entries.filter(([, d]) => {
1782
+ const composedBy = d.composedBy;
1783
+ return composedBy !== void 0 && composedBy.includes(targetName);
1784
+ });
1785
+ }
1400
1786
  const rows = entries.map(([name, d]) => ({
1401
1787
  name,
1402
1788
  file: d.filePath,
@@ -3081,6 +3467,17 @@ ${msg}`);
3081
3467
  }
3082
3468
 
3083
3469
  // src/render-commands.ts
3470
+ function loadGlobalCssFilesFromConfig(cwd) {
3471
+ const configPath = path.resolve(cwd, "reactscope.config.json");
3472
+ if (!fs.existsSync(configPath)) return [];
3473
+ try {
3474
+ const raw = fs.readFileSync(configPath, "utf-8");
3475
+ const cfg = JSON.parse(raw);
3476
+ return cfg.components?.wrappers?.globalCSS ?? [];
3477
+ } catch {
3478
+ return [];
3479
+ }
3480
+ }
3084
3481
  var MANIFEST_PATH6 = ".reactscope/manifest.json";
3085
3482
  var DEFAULT_OUTPUT_DIR = ".reactscope/renders";
3086
3483
  var _pool3 = null;
@@ -3101,7 +3498,7 @@ async function shutdownPool3() {
3101
3498
  _pool3 = null;
3102
3499
  }
3103
3500
  }
3104
- function buildRenderer(filePath, componentName, viewportWidth, viewportHeight, wrapperScript) {
3501
+ function buildRenderer(filePath, componentName, viewportWidth, viewportHeight, globalCssFiles = [], projectCwd = process.cwd(), wrapperScript) {
3105
3502
  const satori = new render.SatoriRenderer({
3106
3503
  defaultViewport: { width: viewportWidth, height: viewportHeight }
3107
3504
  });
@@ -3110,13 +3507,13 @@ function buildRenderer(filePath, componentName, viewportWidth, viewportHeight, w
3110
3507
  async renderCell(props, _complexityClass) {
3111
3508
  const startMs = performance.now();
3112
3509
  const pool = await getPool3(viewportWidth, viewportHeight);
3510
+ const projectCss = await loadGlobalCss(globalCssFiles, projectCwd);
3113
3511
  const htmlHarness = await buildComponentHarness(
3114
3512
  filePath,
3115
3513
  componentName,
3116
3514
  props,
3117
3515
  viewportWidth,
3118
- void 0,
3119
- // projectCss (handled separately)
3516
+ projectCss ?? void 0,
3120
3517
  wrapperScript
3121
3518
  );
3122
3519
  const slot = await pool.acquire();
@@ -3145,10 +3542,10 @@ function buildRenderer(filePath, componentName, viewportWidth, viewportHeight, w
3145
3542
  }
3146
3543
  });
3147
3544
  return [...set];
3148
- });
3149
- const projectCss = await getCompiledCssForClasses(rootDir, classes);
3150
- if (projectCss != null && projectCss.length > 0) {
3151
- await page.addStyleTag({ content: projectCss });
3545
+ }) ?? [];
3546
+ const projectCss2 = await getCompiledCssForClasses(rootDir, classes);
3547
+ if (projectCss2 != null && projectCss2.length > 0) {
3548
+ await page.addStyleTag({ content: projectCss2 });
3152
3549
  }
3153
3550
  const renderTimeMs = performance.now() - startMs;
3154
3551
  const rootLocator = page.locator("[data-reactscope-root]");
@@ -3158,49 +3555,147 @@ function buildRenderer(filePath, componentName, viewportWidth, viewportHeight, w
3158
3555
  `Component "${componentName}" rendered with zero bounding box \u2014 it may be invisible or not mounted`
3159
3556
  );
3160
3557
  }
3161
- const PAD = 24;
3162
- const MIN_W = 320;
3163
- const MIN_H = 200;
3558
+ const PAD = 8;
3164
3559
  const clipX = Math.max(0, boundingBox.x - PAD);
3165
3560
  const clipY = Math.max(0, boundingBox.y - PAD);
3166
3561
  const rawW = boundingBox.width + PAD * 2;
3167
3562
  const rawH = boundingBox.height + PAD * 2;
3168
- const clipW = Math.max(rawW, MIN_W);
3169
- const clipH = Math.max(rawH, MIN_H);
3170
- const safeW = Math.min(clipW, viewportWidth - clipX);
3171
- const safeH = Math.min(clipH, viewportHeight - clipY);
3563
+ const safeW = Math.min(rawW, viewportWidth - clipX);
3564
+ const safeH = Math.min(rawH, viewportHeight - clipY);
3172
3565
  const screenshot = await page.screenshot({
3173
3566
  clip: { x: clipX, y: clipY, width: safeW, height: safeH },
3174
3567
  type: "png"
3175
3568
  });
3569
+ const STYLE_PROPS = [
3570
+ "display",
3571
+ "width",
3572
+ "height",
3573
+ "color",
3574
+ "backgroundColor",
3575
+ "fontSize",
3576
+ "fontFamily",
3577
+ "fontWeight",
3578
+ "lineHeight",
3579
+ "padding",
3580
+ "paddingTop",
3581
+ "paddingRight",
3582
+ "paddingBottom",
3583
+ "paddingLeft",
3584
+ "margin",
3585
+ "marginTop",
3586
+ "marginRight",
3587
+ "marginBottom",
3588
+ "marginLeft",
3589
+ "gap",
3590
+ "borderRadius",
3591
+ "borderWidth",
3592
+ "borderColor",
3593
+ "borderStyle",
3594
+ "boxShadow",
3595
+ "opacity",
3596
+ "position",
3597
+ "flexDirection",
3598
+ "alignItems",
3599
+ "justifyContent",
3600
+ "overflow"
3601
+ ];
3602
+ const _domResult = await page.evaluate(
3603
+ (args) => {
3604
+ let count = 0;
3605
+ const styles = {};
3606
+ function captureStyles(el, id, propList) {
3607
+ const computed = window.getComputedStyle(el);
3608
+ const out = {};
3609
+ for (const prop of propList) {
3610
+ const val = computed[prop] ?? "";
3611
+ if (val && val !== "none" && val !== "normal" && val !== "auto") out[prop] = val;
3612
+ }
3613
+ styles[id] = out;
3614
+ }
3615
+ function walk(node) {
3616
+ if (node.nodeType === Node.TEXT_NODE) {
3617
+ return {
3618
+ tag: "#text",
3619
+ attrs: {},
3620
+ text: node.textContent?.trim() ?? "",
3621
+ children: []
3622
+ };
3623
+ }
3624
+ const el = node;
3625
+ const id = count++;
3626
+ captureStyles(el, id, args.props);
3627
+ const attrs = {};
3628
+ for (const attr of Array.from(el.attributes)) {
3629
+ attrs[attr.name] = attr.value;
3630
+ }
3631
+ const children = Array.from(el.childNodes).filter(
3632
+ (n) => n.nodeType === Node.ELEMENT_NODE || n.nodeType === Node.TEXT_NODE && (n.textContent?.trim() ?? "").length > 0
3633
+ ).map(walk);
3634
+ return { tag: el.tagName.toLowerCase(), attrs, nodeId: id, children };
3635
+ }
3636
+ const root = document.querySelector(args.sel);
3637
+ if (!root)
3638
+ return {
3639
+ tree: { tag: "div", attrs: {}, children: [] },
3640
+ elementCount: 0,
3641
+ nodeStyles: {}
3642
+ };
3643
+ return { tree: walk(root), elementCount: count, nodeStyles: styles };
3644
+ },
3645
+ { sel: "[data-reactscope-root] > *", props: STYLE_PROPS }
3646
+ );
3647
+ const domTree = _domResult?.tree ?? { tag: "div", attrs: {}, children: [] };
3648
+ const elementCount = _domResult?.elementCount ?? 0;
3649
+ const nodeStyles = _domResult?.nodeStyles ?? {};
3176
3650
  const computedStyles = {};
3177
- const styles = await page.evaluate((sel) => {
3178
- const el = document.querySelector(sel);
3179
- if (el === null) return {};
3180
- const computed = window.getComputedStyle(el);
3181
- const out = {};
3182
- for (const prop of [
3183
- "display",
3184
- "width",
3185
- "height",
3186
- "color",
3187
- "backgroundColor",
3188
- "fontSize",
3189
- "fontFamily",
3190
- "padding",
3191
- "margin"
3192
- ]) {
3193
- out[prop] = computed.getPropertyValue(prop);
3651
+ if (nodeStyles[0]) computedStyles["[data-reactscope-root] > *"] = nodeStyles[0];
3652
+ for (const [nodeId, styles] of Object.entries(nodeStyles)) {
3653
+ computedStyles[`#node-${nodeId}`] = styles;
3654
+ }
3655
+ const dom = {
3656
+ tree: domTree,
3657
+ elementCount,
3658
+ boundingBox: {
3659
+ x: boundingBox.x,
3660
+ y: boundingBox.y,
3661
+ width: boundingBox.width,
3662
+ height: boundingBox.height
3194
3663
  }
3195
- return out;
3196
- }, "[data-reactscope-root] > *");
3197
- computedStyles["[data-reactscope-root] > *"] = styles;
3664
+ };
3665
+ const a11yInfo = await page.evaluate((sel) => {
3666
+ const wrapper = document.querySelector(sel);
3667
+ const el = wrapper?.firstElementChild ?? wrapper;
3668
+ if (!el) return { role: "generic", name: "" };
3669
+ return {
3670
+ role: el.getAttribute("role") ?? el.tagName.toLowerCase() ?? "generic",
3671
+ name: el.getAttribute("aria-label") ?? el.getAttribute("aria-labelledby") ?? el.textContent?.trim().slice(0, 100) ?? ""
3672
+ };
3673
+ }, "[data-reactscope-root]") ?? {
3674
+ role: "generic",
3675
+ name: ""
3676
+ };
3677
+ const imgViolations = await page.evaluate((sel) => {
3678
+ const container = document.querySelector(sel);
3679
+ if (!container) return [];
3680
+ const issues = [];
3681
+ container.querySelectorAll("img").forEach((img) => {
3682
+ if (!img.alt) issues.push("Image missing accessible name");
3683
+ });
3684
+ return issues;
3685
+ }, "[data-reactscope-root]") ?? [];
3686
+ const accessibility = {
3687
+ role: a11yInfo.role,
3688
+ name: a11yInfo.name,
3689
+ violations: imgViolations
3690
+ };
3198
3691
  return {
3199
3692
  screenshot,
3200
3693
  width: Math.round(safeW),
3201
3694
  height: Math.round(safeH),
3202
3695
  renderTimeMs,
3203
- computedStyles
3696
+ computedStyles,
3697
+ dom,
3698
+ accessibility
3204
3699
  };
3205
3700
  } finally {
3206
3701
  pool.release(slot);
@@ -3250,26 +3745,64 @@ function registerRenderSingle(renderCmd) {
3250
3745
  Available: ${available}`
3251
3746
  );
3252
3747
  }
3748
+ let props = {};
3749
+ if (opts.props !== void 0) {
3750
+ try {
3751
+ props = JSON.parse(opts.props);
3752
+ } catch {
3753
+ throw new Error(`Invalid props JSON: ${opts.props}`);
3754
+ }
3755
+ }
3756
+ if (descriptor.props !== void 0) {
3757
+ const propDefs = descriptor.props;
3758
+ for (const [propName, propDef] of Object.entries(propDefs)) {
3759
+ if (propName in props) continue;
3760
+ if (!propDef.required && propDef.default !== void 0) continue;
3761
+ if (propDef.type === "node" || propDef.type === "string") {
3762
+ props[propName] = propName === "children" ? componentName : propName;
3763
+ } else if (propDef.type === "union" && propDef.values && propDef.values.length > 0) {
3764
+ props[propName] = propDef.values[0];
3765
+ } else if (propDef.type === "boolean") {
3766
+ props[propName] = false;
3767
+ } else if (propDef.type === "number") {
3768
+ props[propName] = 0;
3769
+ }
3770
+ }
3771
+ }
3253
3772
  const { width, height } = parseViewport(opts.viewport);
3254
3773
  const rootDir = process.cwd();
3255
3774
  const filePath = path.resolve(rootDir, descriptor.filePath);
3256
3775
  const scopeData = await loadScopeFileForComponent(filePath);
3257
3776
  const wrapperScript = scopeData?.hasWrapper === true ? await buildWrapperScript(scopeData.filePath) : void 0;
3258
3777
  const scenarios = buildScenarioMap(opts, scopeData);
3259
- const renderer = buildRenderer(filePath, componentName, width, height, wrapperScript);
3778
+ const globalCssFiles = loadGlobalCssFilesFromConfig(rootDir);
3779
+ if (globalCssFiles.length === 0) {
3780
+ process.stderr.write(
3781
+ "warning: No globalCSS files configured. Tailwind/CSS styles will not be applied to renders.\n Add `components.wrappers.globalCSS` to reactscope.config.json\n"
3782
+ );
3783
+ }
3784
+ const renderer = buildRenderer(
3785
+ filePath,
3786
+ componentName,
3787
+ width,
3788
+ height,
3789
+ globalCssFiles,
3790
+ rootDir,
3791
+ wrapperScript
3792
+ );
3260
3793
  process.stderr.write(
3261
3794
  `Rendering ${componentName} [${descriptor.complexityClass}] at ${width}\xD7${height}\u2026
3262
3795
  `
3263
3796
  );
3264
3797
  const fmt2 = resolveSingleFormat(opts.format);
3265
3798
  let anyFailed = false;
3266
- for (const [scenarioName, props] of Object.entries(scenarios)) {
3799
+ for (const [scenarioName, props2] of Object.entries(scenarios)) {
3267
3800
  const isNamed = scenarioName !== "__default__";
3268
3801
  const label = isNamed ? `${componentName}:${scenarioName}` : componentName;
3269
3802
  const outcome = await render.safeRender(
3270
- () => renderer.renderCell(props, descriptor.complexityClass),
3803
+ () => renderer.renderCell(props2, descriptor.complexityClass),
3271
3804
  {
3272
- props,
3805
+ props: props2,
3273
3806
  sourceLocation: {
3274
3807
  file: descriptor.filePath,
3275
3808
  line: descriptor.loc.start,
@@ -3298,7 +3831,7 @@ Available: ${available}`
3298
3831
  `
3299
3832
  );
3300
3833
  } else if (fmt2 === "json") {
3301
- const json = formatRenderJson(label, props, result);
3834
+ const json = formatRenderJson(label, props2, result);
3302
3835
  process.stdout.write(`${JSON.stringify(json, null, 2)}
3303
3836
  `);
3304
3837
  } else {
@@ -3325,7 +3858,10 @@ Available: ${available}`
3325
3858
  );
3326
3859
  }
3327
3860
  function registerRenderMatrix(renderCmd) {
3328
- renderCmd.command("matrix <component>").description("Render a component across a matrix of prop axes").option("--axes <spec>", "Axis definitions e.g. 'variant:primary,secondary size:sm,md,lg'").option(
3861
+ renderCmd.command("matrix <component>").description("Render a component across a matrix of prop axes").option(
3862
+ "--axes <spec>",
3863
+ `Axis definitions: key:v1,v2 space-separated OR JSON object e.g. 'variant:primary,ghost size:sm,lg' or '{"variant":["primary","ghost"],"size":["sm","lg"]}'`
3864
+ ).option(
3329
3865
  "--contexts <ids>",
3330
3866
  "Composition context IDs, comma-separated (e.g. centered,rtl,sidebar)"
3331
3867
  ).option("--stress <ids>", "Stress preset IDs, comma-separated (e.g. text.long,text.unicode)").option("--sprite <path>", "Write sprite sheet PNG to file").option("--format <fmt>", "Output format: json|png|html|csv (default: auto)").option("--concurrency <n>", "Max parallel renders", "8").option("--manifest <path>", "Path to manifest.json", MANIFEST_PATH6).action(
@@ -3344,21 +3880,47 @@ Available: ${available}`
3344
3880
  const { width, height } = { width: 375, height: 812 };
3345
3881
  const rootDir = process.cwd();
3346
3882
  const filePath = path.resolve(rootDir, descriptor.filePath);
3347
- const renderer = buildRenderer(filePath, componentName, width, height);
3883
+ const matrixCssFiles = loadGlobalCssFilesFromConfig(rootDir);
3884
+ const renderer = buildRenderer(
3885
+ filePath,
3886
+ componentName,
3887
+ width,
3888
+ height,
3889
+ matrixCssFiles,
3890
+ rootDir
3891
+ );
3348
3892
  const axes = [];
3349
3893
  if (opts.axes !== void 0) {
3350
- const axisSpecs = opts.axes.trim().split(/\s+/);
3351
- for (const spec of axisSpecs) {
3352
- const colonIdx = spec.indexOf(":");
3353
- if (colonIdx < 0) {
3354
- throw new Error(`Invalid axis spec "${spec}". Expected format: name:val1,val2,...`);
3894
+ const axesRaw = opts.axes.trim();
3895
+ if (axesRaw.startsWith("{")) {
3896
+ let parsed;
3897
+ try {
3898
+ parsed = JSON.parse(axesRaw);
3899
+ } catch {
3900
+ throw new Error(`Invalid JSON in --axes: ${axesRaw}`);
3901
+ }
3902
+ for (const [name, vals] of Object.entries(parsed)) {
3903
+ if (!Array.isArray(vals)) {
3904
+ throw new Error(`Axis "${name}" must be an array of values in JSON format`);
3905
+ }
3906
+ axes.push({ name, values: vals.map(String) });
3355
3907
  }
3356
- const name = spec.slice(0, colonIdx);
3357
- const values = spec.slice(colonIdx + 1).split(",").map((v) => v.trim());
3358
- if (name.length === 0 || values.length === 0) {
3359
- throw new Error(`Invalid axis spec "${spec}"`);
3908
+ } else {
3909
+ const axisSpecs = axesRaw.split(/\s+/);
3910
+ for (const spec of axisSpecs) {
3911
+ const colonIdx = spec.indexOf(":");
3912
+ if (colonIdx < 0) {
3913
+ throw new Error(
3914
+ `Invalid axis spec "${spec}". Expected format: name:val1,val2,...`
3915
+ );
3916
+ }
3917
+ const name = spec.slice(0, colonIdx);
3918
+ const values = spec.slice(colonIdx + 1).split(",").map((v) => v.trim());
3919
+ if (name.length === 0 || values.length === 0) {
3920
+ throw new Error(`Invalid axis spec "${spec}"`);
3921
+ }
3922
+ axes.push({ name, values });
3360
3923
  }
3361
- axes.push({ name, values });
3362
3924
  }
3363
3925
  }
3364
3926
  if (opts.contexts !== void 0) {
@@ -3472,16 +4034,31 @@ function registerRenderAll(renderCmd) {
3472
4034
  process.stderr.write(`Rendering ${total} components (concurrency: ${concurrency})\u2026
3473
4035
  `);
3474
4036
  const results = [];
4037
+ const complianceStylesMap = {};
3475
4038
  let completed = 0;
3476
4039
  const renderOne = async (name) => {
3477
4040
  const descriptor = manifest.components[name];
3478
4041
  if (descriptor === void 0) return;
3479
4042
  const filePath = path.resolve(rootDir, descriptor.filePath);
3480
- const renderer = buildRenderer(filePath, name, 375, 812);
4043
+ const allCssFiles = loadGlobalCssFilesFromConfig(process.cwd());
4044
+ const scopeData = await loadScopeFileForComponent(filePath);
4045
+ const scenarioEntries = scopeData !== null ? Object.entries(scopeData.scenarios) : [];
4046
+ const defaultEntry = scenarioEntries.find(([k]) => k === "default") ?? scenarioEntries[0];
4047
+ const renderProps = defaultEntry !== void 0 ? defaultEntry[1] : {};
4048
+ const wrapperScript = scopeData?.hasWrapper === true ? await buildWrapperScript(scopeData.filePath) : void 0;
4049
+ const renderer = buildRenderer(
4050
+ filePath,
4051
+ name,
4052
+ 375,
4053
+ 812,
4054
+ allCssFiles,
4055
+ process.cwd(),
4056
+ wrapperScript
4057
+ );
3481
4058
  const outcome = await render.safeRender(
3482
- () => renderer.renderCell({}, descriptor.complexityClass),
4059
+ () => renderer.renderCell(renderProps, descriptor.complexityClass),
3483
4060
  {
3484
- props: {},
4061
+ props: renderProps,
3485
4062
  sourceLocation: {
3486
4063
  file: descriptor.filePath,
3487
4064
  line: descriptor.loc.start,
@@ -3521,6 +4098,77 @@ function registerRenderAll(renderCmd) {
3521
4098
  fs.writeFileSync(pngPath, result.screenshot);
3522
4099
  const jsonPath = path.resolve(outputDir, `${name}.json`);
3523
4100
  fs.writeFileSync(jsonPath, JSON.stringify(formatRenderJson(name, {}, result), null, 2));
4101
+ const rawStyles = result.computedStyles["[data-reactscope-root] > *"] ?? {};
4102
+ const compStyles = {
4103
+ colors: {},
4104
+ spacing: {},
4105
+ typography: {},
4106
+ borders: {},
4107
+ shadows: {}
4108
+ };
4109
+ for (const [prop, val] of Object.entries(rawStyles)) {
4110
+ if (!val || val === "none" || val === "") continue;
4111
+ const lower = prop.toLowerCase();
4112
+ if (lower.includes("color") || lower.includes("background")) {
4113
+ compStyles.colors[prop] = val;
4114
+ } else if (lower.includes("padding") || lower.includes("margin") || lower.includes("gap") || lower.includes("width") || lower.includes("height")) {
4115
+ compStyles.spacing[prop] = val;
4116
+ } else if (lower.includes("font") || lower.includes("lineheight") || lower.includes("letterspacing") || lower.includes("texttransform")) {
4117
+ compStyles.typography[prop] = val;
4118
+ } else if (lower.includes("border") || lower.includes("radius") || lower.includes("outline")) {
4119
+ compStyles.borders[prop] = val;
4120
+ } else if (lower.includes("shadow")) {
4121
+ compStyles.shadows[prop] = val;
4122
+ }
4123
+ }
4124
+ complianceStylesMap[name] = compStyles;
4125
+ if (scopeData !== null && Object.keys(scopeData.scenarios).length >= 2) {
4126
+ try {
4127
+ const scenarioEntries2 = Object.entries(scopeData.scenarios);
4128
+ const scenarioAxis = {
4129
+ name: "scenario",
4130
+ values: scenarioEntries2.map(([k]) => k)
4131
+ };
4132
+ const scenarioPropsMap = Object.fromEntries(scenarioEntries2);
4133
+ const matrixRenderer = buildRenderer(
4134
+ filePath,
4135
+ name,
4136
+ 375,
4137
+ 812,
4138
+ allCssFiles,
4139
+ process.cwd(),
4140
+ wrapperScript
4141
+ );
4142
+ const wrappedRenderer = {
4143
+ _satori: matrixRenderer._satori,
4144
+ async renderCell(props, cc) {
4145
+ const scenarioName = props.scenario;
4146
+ const realProps = scenarioName !== void 0 ? scenarioPropsMap[scenarioName] ?? props : props;
4147
+ return matrixRenderer.renderCell(realProps, cc ?? "simple");
4148
+ }
4149
+ };
4150
+ const matrix = new render.RenderMatrix(wrappedRenderer, [scenarioAxis], {
4151
+ concurrency: 2
4152
+ });
4153
+ const matrixResult = await matrix.render();
4154
+ const matrixCells = matrixResult.cells.map((cell) => ({
4155
+ axisValues: [scenarioEntries2[cell.axisIndices[0] ?? 0]?.[0] ?? ""],
4156
+ screenshot: cell.result.screenshot.toString("base64"),
4157
+ width: cell.result.width,
4158
+ height: cell.result.height,
4159
+ renderTimeMs: cell.result.renderTimeMs
4160
+ }));
4161
+ const existingJson = JSON.parse(fs.readFileSync(jsonPath, "utf-8"));
4162
+ existingJson.cells = matrixCells;
4163
+ existingJson.axisLabels = [scenarioAxis.values];
4164
+ fs.writeFileSync(jsonPath, JSON.stringify(existingJson, null, 2));
4165
+ } catch (matrixErr) {
4166
+ process.stderr.write(
4167
+ ` [warn] Matrix render for ${name} failed: ${matrixErr instanceof Error ? matrixErr.message : String(matrixErr)}
4168
+ `
4169
+ );
4170
+ }
4171
+ }
3524
4172
  if (isTTY()) {
3525
4173
  process.stdout.write(
3526
4174
  `\u2713 ${name} \u2192 ${opts.outputDir}/${name}.png (${result.width}\xD7${result.height}, ${result.renderTimeMs.toFixed(0)}ms)
@@ -3544,6 +4192,14 @@ function registerRenderAll(renderCmd) {
3544
4192
  }
3545
4193
  await Promise.all(workers);
3546
4194
  await shutdownPool3();
4195
+ const compStylesPath = path.resolve(
4196
+ path.resolve(process.cwd(), opts.outputDir),
4197
+ "..",
4198
+ "compliance-styles.json"
4199
+ );
4200
+ fs.writeFileSync(compStylesPath, JSON.stringify(complianceStylesMap, null, 2));
4201
+ process.stderr.write(`[scope/render] \u2713 Wrote compliance-styles.json
4202
+ `);
3547
4203
  process.stderr.write("\n");
3548
4204
  const summary = formatSummaryText(results, outputDir);
3549
4205
  process.stderr.write(`${summary}
@@ -3743,12 +4399,12 @@ async function runBaseline(options = {}) {
3743
4399
  fs.mkdirSync(rendersDir, { recursive: true });
3744
4400
  let manifest$1;
3745
4401
  if (manifestPath !== void 0) {
3746
- const { readFileSync: readFileSync12 } = await import('fs');
4402
+ const { readFileSync: readFileSync14 } = await import('fs');
3747
4403
  const absPath = path.resolve(rootDir, manifestPath);
3748
4404
  if (!fs.existsSync(absPath)) {
3749
4405
  throw new Error(`Manifest not found at ${absPath}.`);
3750
4406
  }
3751
- manifest$1 = JSON.parse(readFileSync12(absPath, "utf-8"));
4407
+ manifest$1 = JSON.parse(readFileSync14(absPath, "utf-8"));
3752
4408
  process.stderr.write(`Loaded manifest from ${manifestPath}
3753
4409
  `);
3754
4410
  } else {
@@ -5109,10 +5765,20 @@ function createTokensExportCommand() {
5109
5765
  ).action(
5110
5766
  (opts) => {
5111
5767
  if (!SUPPORTED_FORMATS.includes(opts.format)) {
5768
+ const FORMAT_ALIASES = {
5769
+ json: "flat-json",
5770
+ "json-flat": "flat-json",
5771
+ javascript: "ts",
5772
+ js: "ts",
5773
+ sass: "scss",
5774
+ tw: "tailwind"
5775
+ };
5776
+ const hint = FORMAT_ALIASES[opts.format.toLowerCase()];
5112
5777
  process.stderr.write(
5113
5778
  `Error: unsupported format "${opts.format}".
5114
5779
  Supported formats: ${SUPPORTED_FORMATS.join(", ")}
5115
- `
5780
+ ` + (hint ? `Did you mean "${hint}"?
5781
+ ` : "")
5116
5782
  );
5117
5783
  process.exit(1);
5118
5784
  }
@@ -5926,6 +6592,7 @@ function createProgram(options = {}) {
5926
6592
  program.addCommand(createInstrumentCommand());
5927
6593
  program.addCommand(createInitCommand());
5928
6594
  program.addCommand(createCiCommand());
6595
+ program.addCommand(createDoctorCommand());
5929
6596
  const existingReportCmd = program.commands.find((c) => c.name() === "report");
5930
6597
  if (existingReportCmd !== void 0) {
5931
6598
  registerBaselineSubCommand(existingReportCmd);
@@ -5938,6 +6605,7 @@ function createProgram(options = {}) {
5938
6605
 
5939
6606
  exports.CI_EXIT = CI_EXIT;
5940
6607
  exports.createCiCommand = createCiCommand;
6608
+ exports.createDoctorCommand = createDoctorCommand;
5941
6609
  exports.createInitCommand = createInitCommand;
5942
6610
  exports.createInstrumentCommand = createInstrumentCommand;
5943
6611
  exports.createManifestCommand = createManifestCommand;