@agent-scope/cli 1.18.1 → 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
@@ -541,10 +541,10 @@ async function getCompiledCssForClasses(cwd, classes) {
541
541
  return build3(deduped);
542
542
  }
543
543
  async function compileGlobalCssFile(cssFilePath, cwd) {
544
- const { existsSync: existsSync15, readFileSync: readFileSync13 } = await import('fs');
544
+ const { existsSync: existsSync16, readFileSync: readFileSync14 } = await import('fs');
545
545
  const { createRequire: createRequire3 } = await import('module');
546
- if (!existsSync15(cssFilePath)) return null;
547
- const raw = readFileSync13(cssFilePath, "utf-8");
546
+ if (!existsSync16(cssFilePath)) return null;
547
+ const raw = readFileSync14(cssFilePath, "utf-8");
548
548
  const needsCompile = /@tailwind|@import\s+['"]tailwindcss/.test(raw);
549
549
  if (!needsCompile) {
550
550
  return raw;
@@ -1013,6 +1013,157 @@ function createCiCommand() {
1013
1013
  }
1014
1014
  );
1015
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
+ }
1016
1167
  function hasConfigFile(dir, stem) {
1017
1168
  if (!fs.existsSync(dir)) return false;
1018
1169
  try {
@@ -1223,9 +1374,9 @@ function createRL() {
1223
1374
  });
1224
1375
  }
1225
1376
  async function ask(rl, question) {
1226
- return new Promise((resolve18) => {
1377
+ return new Promise((resolve19) => {
1227
1378
  rl.question(question, (answer) => {
1228
- resolve18(answer.trim());
1379
+ resolve19(answer.trim());
1229
1380
  });
1230
1381
  });
1231
1382
  }
@@ -1300,7 +1451,7 @@ function extractTailwindTokens(tokenSources) {
1300
1451
  }
1301
1452
  }
1302
1453
  if (Object.keys(colorTokens).length > 0) {
1303
- tokens["color"] = colorTokens;
1454
+ tokens.color = colorTokens;
1304
1455
  }
1305
1456
  }
1306
1457
  }
@@ -1313,7 +1464,7 @@ function extractTailwindTokens(tokenSources) {
1313
1464
  for (const [key, val] of Object.entries(spacingValues)) {
1314
1465
  spacingTokens[key] = { value: val, type: "dimension" };
1315
1466
  }
1316
- tokens["spacing"] = spacingTokens;
1467
+ tokens.spacing = spacingTokens;
1317
1468
  }
1318
1469
  }
1319
1470
  const fontFamilyMatch = raw.match(/fontFamily\s*:\s*\{([\s\S]*?)\n\s*\}/);
@@ -1326,7 +1477,7 @@ function extractTailwindTokens(tokenSources) {
1326
1477
  }
1327
1478
  }
1328
1479
  if (Object.keys(fontTokens).length > 0) {
1329
- tokens["font"] = fontTokens;
1480
+ tokens.font = fontTokens;
1330
1481
  }
1331
1482
  }
1332
1483
  const borderRadiusMatch = raw.match(/borderRadius\s*:\s*\{([\s\S]*?)\n\s*\}/);
@@ -1337,7 +1488,7 @@ function extractTailwindTokens(tokenSources) {
1337
1488
  for (const [key, val] of Object.entries(radiusValues)) {
1338
1489
  radiusTokens[key] = { value: val, type: "dimension" };
1339
1490
  }
1340
- tokens["radius"] = radiusTokens;
1491
+ tokens.radius = radiusTokens;
1341
1492
  }
1342
1493
  }
1343
1494
  return Object.keys(tokens).length > 0 ? tokens : null;
@@ -1456,7 +1607,28 @@ async function runInit(options) {
1456
1607
  process.stdout.write(` ${p}
1457
1608
  `);
1458
1609
  }
1459
- 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");
1460
1632
  return {
1461
1633
  success: true,
1462
1634
  message: "Project initialised successfully.",
@@ -3370,7 +3542,7 @@ function buildRenderer(filePath, componentName, viewportWidth, viewportHeight, g
3370
3542
  }
3371
3543
  });
3372
3544
  return [...set];
3373
- });
3545
+ }) ?? [];
3374
3546
  const projectCss2 = await getCompiledCssForClasses(rootDir, classes);
3375
3547
  if (projectCss2 != null && projectCss2.length > 0) {
3376
3548
  await page.addStyleTag({ content: projectCss2 });
@@ -3383,49 +3555,147 @@ function buildRenderer(filePath, componentName, viewportWidth, viewportHeight, g
3383
3555
  `Component "${componentName}" rendered with zero bounding box \u2014 it may be invisible or not mounted`
3384
3556
  );
3385
3557
  }
3386
- const PAD = 24;
3387
- const MIN_W = 320;
3388
- const MIN_H = 200;
3558
+ const PAD = 8;
3389
3559
  const clipX = Math.max(0, boundingBox.x - PAD);
3390
3560
  const clipY = Math.max(0, boundingBox.y - PAD);
3391
3561
  const rawW = boundingBox.width + PAD * 2;
3392
3562
  const rawH = boundingBox.height + PAD * 2;
3393
- const clipW = Math.max(rawW, MIN_W);
3394
- const clipH = Math.max(rawH, MIN_H);
3395
- const safeW = Math.min(clipW, viewportWidth - clipX);
3396
- const safeH = Math.min(clipH, viewportHeight - clipY);
3563
+ const safeW = Math.min(rawW, viewportWidth - clipX);
3564
+ const safeH = Math.min(rawH, viewportHeight - clipY);
3397
3565
  const screenshot = await page.screenshot({
3398
3566
  clip: { x: clipX, y: clipY, width: safeW, height: safeH },
3399
3567
  type: "png"
3400
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 ?? {};
3401
3650
  const computedStyles = {};
3402
- const styles = await page.evaluate((sel) => {
3403
- const el = document.querySelector(sel);
3404
- if (el === null) return {};
3405
- const computed = window.getComputedStyle(el);
3406
- const out = {};
3407
- for (const prop of [
3408
- "display",
3409
- "width",
3410
- "height",
3411
- "color",
3412
- "backgroundColor",
3413
- "fontSize",
3414
- "fontFamily",
3415
- "padding",
3416
- "margin"
3417
- ]) {
3418
- 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
3419
3663
  }
3420
- return out;
3421
- }, "[data-reactscope-root] > *");
3422
- 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
+ };
3423
3691
  return {
3424
3692
  screenshot,
3425
3693
  width: Math.round(safeW),
3426
3694
  height: Math.round(safeH),
3427
3695
  renderTimeMs,
3428
- computedStyles
3696
+ computedStyles,
3697
+ dom,
3698
+ accessibility
3429
3699
  };
3430
3700
  } finally {
3431
3701
  pool.release(slot);
@@ -3506,6 +3776,11 @@ Available: ${available}`
3506
3776
  const wrapperScript = scopeData?.hasWrapper === true ? await buildWrapperScript(scopeData.filePath) : void 0;
3507
3777
  const scenarios = buildScenarioMap(opts, scopeData);
3508
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
+ }
3509
3784
  const renderer = buildRenderer(
3510
3785
  filePath,
3511
3786
  componentName,
@@ -3759,17 +4034,31 @@ function registerRenderAll(renderCmd) {
3759
4034
  process.stderr.write(`Rendering ${total} components (concurrency: ${concurrency})\u2026
3760
4035
  `);
3761
4036
  const results = [];
4037
+ const complianceStylesMap = {};
3762
4038
  let completed = 0;
3763
4039
  const renderOne = async (name) => {
3764
4040
  const descriptor = manifest.components[name];
3765
4041
  if (descriptor === void 0) return;
3766
4042
  const filePath = path.resolve(rootDir, descriptor.filePath);
3767
4043
  const allCssFiles = loadGlobalCssFilesFromConfig(process.cwd());
3768
- const renderer = buildRenderer(filePath, name, 375, 812, allCssFiles, 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
+ );
3769
4058
  const outcome = await render.safeRender(
3770
- () => renderer.renderCell({}, descriptor.complexityClass),
4059
+ () => renderer.renderCell(renderProps, descriptor.complexityClass),
3771
4060
  {
3772
- props: {},
4061
+ props: renderProps,
3773
4062
  sourceLocation: {
3774
4063
  file: descriptor.filePath,
3775
4064
  line: descriptor.loc.start,
@@ -3809,6 +4098,77 @@ function registerRenderAll(renderCmd) {
3809
4098
  fs.writeFileSync(pngPath, result.screenshot);
3810
4099
  const jsonPath = path.resolve(outputDir, `${name}.json`);
3811
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
+ }
3812
4172
  if (isTTY()) {
3813
4173
  process.stdout.write(
3814
4174
  `\u2713 ${name} \u2192 ${opts.outputDir}/${name}.png (${result.width}\xD7${result.height}, ${result.renderTimeMs.toFixed(0)}ms)
@@ -3832,6 +4192,14 @@ function registerRenderAll(renderCmd) {
3832
4192
  }
3833
4193
  await Promise.all(workers);
3834
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
+ `);
3835
4203
  process.stderr.write("\n");
3836
4204
  const summary = formatSummaryText(results, outputDir);
3837
4205
  process.stderr.write(`${summary}
@@ -4031,12 +4399,12 @@ async function runBaseline(options = {}) {
4031
4399
  fs.mkdirSync(rendersDir, { recursive: true });
4032
4400
  let manifest$1;
4033
4401
  if (manifestPath !== void 0) {
4034
- const { readFileSync: readFileSync13 } = await import('fs');
4402
+ const { readFileSync: readFileSync14 } = await import('fs');
4035
4403
  const absPath = path.resolve(rootDir, manifestPath);
4036
4404
  if (!fs.existsSync(absPath)) {
4037
4405
  throw new Error(`Manifest not found at ${absPath}.`);
4038
4406
  }
4039
- manifest$1 = JSON.parse(readFileSync13(absPath, "utf-8"));
4407
+ manifest$1 = JSON.parse(readFileSync14(absPath, "utf-8"));
4040
4408
  process.stderr.write(`Loaded manifest from ${manifestPath}
4041
4409
  `);
4042
4410
  } else {
@@ -6224,6 +6592,7 @@ function createProgram(options = {}) {
6224
6592
  program.addCommand(createInstrumentCommand());
6225
6593
  program.addCommand(createInitCommand());
6226
6594
  program.addCommand(createCiCommand());
6595
+ program.addCommand(createDoctorCommand());
6227
6596
  const existingReportCmd = program.commands.find((c) => c.name() === "report");
6228
6597
  if (existingReportCmd !== void 0) {
6229
6598
  registerBaselineSubCommand(existingReportCmd);
@@ -6236,6 +6605,7 @@ function createProgram(options = {}) {
6236
6605
 
6237
6606
  exports.CI_EXIT = CI_EXIT;
6238
6607
  exports.createCiCommand = createCiCommand;
6608
+ exports.createDoctorCommand = createDoctorCommand;
6239
6609
  exports.createInitCommand = createInitCommand;
6240
6610
  exports.createInstrumentCommand = createInstrumentCommand;
6241
6611
  exports.createManifestCommand = createManifestCommand;