@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/cli.js CHANGED
@@ -1,9 +1,9 @@
1
- #!/usr/bin/env node
1
+ #!/usr/bin/env bun
2
2
 
3
3
  // src/program.ts
4
- import { readFileSync as readFileSync11 } from "fs";
4
+ import { readFileSync as readFileSync13 } from "fs";
5
5
  import { generateTest, loadTrace } from "@agent-scope/playwright";
6
- import { Command as Command10 } from "commander";
6
+ import { Command as Command11 } from "commander";
7
7
 
8
8
  // src/browser.ts
9
9
  import { writeFileSync } from "fs";
@@ -141,6 +141,21 @@ import { createElement } from "react";
141
141
  // Suppress "React must be in scope" warnings from old JSX (we use automatic)
142
142
  banner: {
143
143
  js: "/* @agent-scope/cli component harness */"
144
+ },
145
+ // CSS imports (e.g. `import './styles.css'`) are handled at the page level via
146
+ // globalCSS injection. Tell esbuild to treat CSS files as empty modules so
147
+ // components that import CSS directly (e.g. App.tsx) don't error during bundling.
148
+ loader: {
149
+ ".css": "empty",
150
+ ".svg": "dataurl",
151
+ ".png": "dataurl",
152
+ ".jpg": "dataurl",
153
+ ".jpeg": "dataurl",
154
+ ".gif": "dataurl",
155
+ ".webp": "dataurl",
156
+ ".ttf": "dataurl",
157
+ ".woff": "dataurl",
158
+ ".woff2": "dataurl"
144
159
  }
145
160
  });
146
161
  if (result.errors.length > 0) {
@@ -553,6 +568,57 @@ async function getCompiledCssForClasses(cwd, classes) {
553
568
  if (deduped.length === 0) return null;
554
569
  return build3(deduped);
555
570
  }
571
+ async function compileGlobalCssFile(cssFilePath, cwd) {
572
+ const { existsSync: existsSync16, readFileSync: readFileSync14 } = await import("fs");
573
+ const { createRequire: createRequire3 } = await import("module");
574
+ if (!existsSync16(cssFilePath)) return null;
575
+ const raw = readFileSync14(cssFilePath, "utf-8");
576
+ const needsCompile = /@tailwind|@import\s+['"]tailwindcss/.test(raw);
577
+ if (!needsCompile) {
578
+ return raw;
579
+ }
580
+ try {
581
+ const require2 = createRequire3(resolve(cwd, "package.json"));
582
+ let postcss;
583
+ let twPlugin;
584
+ try {
585
+ postcss = require2("postcss");
586
+ twPlugin = require2("tailwindcss");
587
+ } catch {
588
+ return raw;
589
+ }
590
+ let autoprefixerPlugin;
591
+ try {
592
+ autoprefixerPlugin = require2("autoprefixer");
593
+ } catch {
594
+ autoprefixerPlugin = null;
595
+ }
596
+ const plugins = autoprefixerPlugin ? [twPlugin, autoprefixerPlugin] : [twPlugin];
597
+ const result = await postcss(plugins).process(raw, {
598
+ from: cssFilePath,
599
+ to: cssFilePath
600
+ });
601
+ return result.css;
602
+ } catch (err) {
603
+ process.stderr.write(
604
+ `[scope/render] Warning: CSS compilation failed for ${cssFilePath}: ${err instanceof Error ? err.message : String(err)}
605
+ `
606
+ );
607
+ return raw;
608
+ }
609
+ }
610
+ async function loadGlobalCss(globalCssFiles, cwd) {
611
+ if (globalCssFiles.length === 0) return null;
612
+ const parts = [];
613
+ for (const relPath of globalCssFiles) {
614
+ const absPath = resolve(cwd, relPath);
615
+ const css = await compileGlobalCssFile(absPath, cwd);
616
+ if (css !== null && css.trim().length > 0) {
617
+ parts.push(css);
618
+ }
619
+ }
620
+ return parts.length > 0 ? parts.join("\n") : null;
621
+ }
556
622
 
557
623
  // src/ci/commands.ts
558
624
  var CI_EXIT = {
@@ -976,18 +1042,174 @@ function createCiCommand() {
976
1042
  );
977
1043
  }
978
1044
 
1045
+ // src/doctor-commands.ts
1046
+ import { existsSync as existsSync3, readdirSync, readFileSync as readFileSync3, statSync } from "fs";
1047
+ import { join, resolve as resolve3 } from "path";
1048
+ import { Command as Command2 } from "commander";
1049
+ function collectSourceFiles(dir) {
1050
+ if (!existsSync3(dir)) return [];
1051
+ const results = [];
1052
+ for (const entry of readdirSync(dir, { withFileTypes: true })) {
1053
+ const full = join(dir, entry.name);
1054
+ if (entry.isDirectory() && entry.name !== "node_modules" && entry.name !== ".reactscope") {
1055
+ results.push(...collectSourceFiles(full));
1056
+ } else if (entry.isFile() && /\.(tsx?|jsx?)$/.test(entry.name)) {
1057
+ results.push(full);
1058
+ }
1059
+ }
1060
+ return results;
1061
+ }
1062
+ function checkConfig(cwd) {
1063
+ const configPath = resolve3(cwd, "reactscope.config.json");
1064
+ if (!existsSync3(configPath)) {
1065
+ return {
1066
+ name: "config",
1067
+ status: "error",
1068
+ message: "reactscope.config.json not found \u2014 run `scope init`"
1069
+ };
1070
+ }
1071
+ try {
1072
+ JSON.parse(readFileSync3(configPath, "utf-8"));
1073
+ return { name: "config", status: "ok", message: "reactscope.config.json valid" };
1074
+ } catch {
1075
+ return { name: "config", status: "error", message: "reactscope.config.json is not valid JSON" };
1076
+ }
1077
+ }
1078
+ function checkTokens(cwd) {
1079
+ const configPath = resolve3(cwd, "reactscope.config.json");
1080
+ let tokensPath = resolve3(cwd, "reactscope.tokens.json");
1081
+ if (existsSync3(configPath)) {
1082
+ try {
1083
+ const cfg = JSON.parse(readFileSync3(configPath, "utf-8"));
1084
+ if (cfg.tokens?.file) tokensPath = resolve3(cwd, cfg.tokens.file);
1085
+ } catch {
1086
+ }
1087
+ }
1088
+ if (!existsSync3(tokensPath)) {
1089
+ return {
1090
+ name: "tokens",
1091
+ status: "warn",
1092
+ message: `Token file not found at ${tokensPath} \u2014 run \`scope init\``
1093
+ };
1094
+ }
1095
+ try {
1096
+ const raw = JSON.parse(readFileSync3(tokensPath, "utf-8"));
1097
+ if (!raw.version) {
1098
+ return { name: "tokens", status: "warn", message: "Token file is missing a `version` field" };
1099
+ }
1100
+ return { name: "tokens", status: "ok", message: "Token file valid" };
1101
+ } catch {
1102
+ return { name: "tokens", status: "error", message: "Token file is not valid JSON" };
1103
+ }
1104
+ }
1105
+ function checkGlobalCss(cwd) {
1106
+ const configPath = resolve3(cwd, "reactscope.config.json");
1107
+ let globalCss = [];
1108
+ if (existsSync3(configPath)) {
1109
+ try {
1110
+ const cfg = JSON.parse(readFileSync3(configPath, "utf-8"));
1111
+ globalCss = cfg.components?.wrappers?.globalCSS ?? [];
1112
+ } catch {
1113
+ }
1114
+ }
1115
+ if (globalCss.length === 0) {
1116
+ return {
1117
+ name: "globalCSS",
1118
+ status: "warn",
1119
+ message: "No globalCSS configured \u2014 Tailwind styles won't apply to renders. Add `components.wrappers.globalCSS` to reactscope.config.json"
1120
+ };
1121
+ }
1122
+ const missing = globalCss.filter((f) => !existsSync3(resolve3(cwd, f)));
1123
+ if (missing.length > 0) {
1124
+ return {
1125
+ name: "globalCSS",
1126
+ status: "error",
1127
+ message: `globalCSS file(s) not found: ${missing.join(", ")}`
1128
+ };
1129
+ }
1130
+ return {
1131
+ name: "globalCSS",
1132
+ status: "ok",
1133
+ message: `${globalCss.length} globalCSS file(s) present`
1134
+ };
1135
+ }
1136
+ function checkManifest(cwd) {
1137
+ const manifestPath = resolve3(cwd, ".reactscope", "manifest.json");
1138
+ if (!existsSync3(manifestPath)) {
1139
+ return {
1140
+ name: "manifest",
1141
+ status: "warn",
1142
+ message: "Manifest not found \u2014 run `scope manifest generate`"
1143
+ };
1144
+ }
1145
+ const manifestMtime = statSync(manifestPath).mtimeMs;
1146
+ const sourceDir = resolve3(cwd, "src");
1147
+ const sourceFiles = collectSourceFiles(sourceDir);
1148
+ const stale = sourceFiles.filter((f) => statSync(f).mtimeMs > manifestMtime);
1149
+ if (stale.length > 0) {
1150
+ return {
1151
+ name: "manifest",
1152
+ status: "warn",
1153
+ message: `Manifest may be stale \u2014 ${stale.length} source file(s) modified since last generate. Run \`scope manifest generate\``
1154
+ };
1155
+ }
1156
+ return { name: "manifest", status: "ok", message: "Manifest present and up to date" };
1157
+ }
1158
+ var ICONS = { ok: "\u2713", warn: "!", error: "\u2717" };
1159
+ function formatCheck(check) {
1160
+ return ` [${ICONS[check.status]}] ${check.name.padEnd(12)} ${check.message}`;
1161
+ }
1162
+ function createDoctorCommand() {
1163
+ return new Command2("doctor").description("Check the health of your Scope setup (config, tokens, CSS, manifest)").option("--json", "Emit structured JSON output", false).action((opts) => {
1164
+ const cwd = process.cwd();
1165
+ const checks = [
1166
+ checkConfig(cwd),
1167
+ checkTokens(cwd),
1168
+ checkGlobalCss(cwd),
1169
+ checkManifest(cwd)
1170
+ ];
1171
+ const errors = checks.filter((c) => c.status === "error").length;
1172
+ const warnings = checks.filter((c) => c.status === "warn").length;
1173
+ if (opts.json) {
1174
+ process.stdout.write(
1175
+ `${JSON.stringify({ passed: checks.length - errors - warnings, warnings, errors, checks }, null, 2)}
1176
+ `
1177
+ );
1178
+ if (errors > 0) process.exit(1);
1179
+ return;
1180
+ }
1181
+ process.stdout.write("\nScope Doctor\n");
1182
+ 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");
1183
+ for (const check of checks) process.stdout.write(`${formatCheck(check)}
1184
+ `);
1185
+ 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");
1186
+ if (errors > 0) {
1187
+ process.stdout.write(` ${errors} error(s), ${warnings} warning(s)
1188
+
1189
+ `);
1190
+ process.exit(1);
1191
+ } else if (warnings > 0) {
1192
+ process.stdout.write(` ${warnings} warning(s) \u2014 everything works but could be better
1193
+
1194
+ `);
1195
+ } else {
1196
+ process.stdout.write(" All checks passed!\n\n");
1197
+ }
1198
+ });
1199
+ }
1200
+
979
1201
  // src/init/index.ts
980
- import { appendFileSync, existsSync as existsSync4, mkdirSync, readFileSync as readFileSync4, writeFileSync as writeFileSync3 } from "fs";
981
- import { join as join2 } from "path";
1202
+ import { appendFileSync, existsSync as existsSync5, mkdirSync, readFileSync as readFileSync5, writeFileSync as writeFileSync3 } from "fs";
1203
+ import { join as join3 } from "path";
982
1204
  import * as readline from "readline";
983
1205
 
984
1206
  // src/init/detect.ts
985
- import { existsSync as existsSync3, readdirSync, readFileSync as readFileSync3 } from "fs";
986
- import { join } from "path";
1207
+ import { existsSync as existsSync4, readdirSync as readdirSync2, readFileSync as readFileSync4 } from "fs";
1208
+ import { join as join2 } from "path";
987
1209
  function hasConfigFile(dir, stem) {
988
- if (!existsSync3(dir)) return false;
1210
+ if (!existsSync4(dir)) return false;
989
1211
  try {
990
- const entries = readdirSync(dir);
1212
+ const entries = readdirSync2(dir);
991
1213
  return entries.some((f) => f === stem || f.startsWith(`${stem}.`));
992
1214
  } catch {
993
1215
  return false;
@@ -995,7 +1217,7 @@ function hasConfigFile(dir, stem) {
995
1217
  }
996
1218
  function readSafe(path) {
997
1219
  try {
998
- return readFileSync3(path, "utf-8");
1220
+ return readFileSync4(path, "utf-8");
999
1221
  } catch {
1000
1222
  return null;
1001
1223
  }
@@ -1008,15 +1230,15 @@ function detectFramework(rootDir, packageDeps) {
1008
1230
  return "unknown";
1009
1231
  }
1010
1232
  function detectPackageManager(rootDir) {
1011
- if (existsSync3(join(rootDir, "bun.lock"))) return "bun";
1012
- if (existsSync3(join(rootDir, "yarn.lock"))) return "yarn";
1013
- if (existsSync3(join(rootDir, "pnpm-lock.yaml"))) return "pnpm";
1014
- if (existsSync3(join(rootDir, "package-lock.json"))) return "npm";
1233
+ if (existsSync4(join2(rootDir, "bun.lock"))) return "bun";
1234
+ if (existsSync4(join2(rootDir, "yarn.lock"))) return "yarn";
1235
+ if (existsSync4(join2(rootDir, "pnpm-lock.yaml"))) return "pnpm";
1236
+ if (existsSync4(join2(rootDir, "package-lock.json"))) return "npm";
1015
1237
  return "npm";
1016
1238
  }
1017
1239
  function detectTypeScript(rootDir) {
1018
- const candidate = join(rootDir, "tsconfig.json");
1019
- if (existsSync3(candidate)) {
1240
+ const candidate = join2(rootDir, "tsconfig.json");
1241
+ if (existsSync4(candidate)) {
1020
1242
  return { typescript: true, tsconfigPath: candidate };
1021
1243
  }
1022
1244
  return { typescript: false, tsconfigPath: null };
@@ -1028,11 +1250,11 @@ function detectComponentPatterns(rootDir, typescript) {
1028
1250
  const ext = typescript ? "tsx" : "jsx";
1029
1251
  const altExt = typescript ? "jsx" : "jsx";
1030
1252
  for (const dir of COMPONENT_DIRS) {
1031
- const absDir = join(rootDir, dir);
1032
- if (!existsSync3(absDir)) continue;
1253
+ const absDir = join2(rootDir, dir);
1254
+ if (!existsSync4(absDir)) continue;
1033
1255
  let hasComponents = false;
1034
1256
  try {
1035
- const entries = readdirSync(absDir, { withFileTypes: true });
1257
+ const entries = readdirSync2(absDir, { withFileTypes: true });
1036
1258
  hasComponents = entries.some(
1037
1259
  (e) => e.isFile() && COMPONENT_EXTS.some((x) => e.name.endsWith(x))
1038
1260
  );
@@ -1040,7 +1262,7 @@ function detectComponentPatterns(rootDir, typescript) {
1040
1262
  hasComponents = entries.some(
1041
1263
  (e) => e.isDirectory() && (() => {
1042
1264
  try {
1043
- return readdirSync(join(absDir, e.name)).some(
1265
+ return readdirSync2(join2(absDir, e.name)).some(
1044
1266
  (f) => COMPONENT_EXTS.some((x) => f.endsWith(x))
1045
1267
  );
1046
1268
  } catch {
@@ -1065,6 +1287,20 @@ function detectComponentPatterns(rootDir, typescript) {
1065
1287
  }
1066
1288
  return unique;
1067
1289
  }
1290
+ var GLOBAL_CSS_CANDIDATES = [
1291
+ "src/styles.css",
1292
+ "src/index.css",
1293
+ "src/global.css",
1294
+ "src/globals.css",
1295
+ "src/app.css",
1296
+ "src/main.css",
1297
+ "styles/globals.css",
1298
+ "styles/global.css",
1299
+ "styles/index.css"
1300
+ ];
1301
+ function detectGlobalCSSFiles(rootDir) {
1302
+ return GLOBAL_CSS_CANDIDATES.filter((rel) => existsSync4(join2(rootDir, rel)));
1303
+ }
1068
1304
  var TAILWIND_STEMS = ["tailwind.config"];
1069
1305
  var CSS_EXTS = [".css", ".scss", ".sass", ".less"];
1070
1306
  var THEME_SUFFIXES = [".theme.ts", ".theme.js", ".theme.tsx"];
@@ -1074,23 +1310,23 @@ function detectTokenSources(rootDir) {
1074
1310
  for (const stem of TAILWIND_STEMS) {
1075
1311
  if (hasConfigFile(rootDir, stem)) {
1076
1312
  try {
1077
- const entries = readdirSync(rootDir);
1313
+ const entries = readdirSync2(rootDir);
1078
1314
  const match = entries.find((f) => f === stem || f.startsWith(`${stem}.`));
1079
1315
  if (match) {
1080
- sources.push({ kind: "tailwind-config", path: join(rootDir, match) });
1316
+ sources.push({ kind: "tailwind-config", path: join2(rootDir, match) });
1081
1317
  }
1082
1318
  } catch {
1083
1319
  }
1084
1320
  }
1085
1321
  }
1086
- const srcDir = join(rootDir, "src");
1087
- const dirsToScan = existsSync3(srcDir) ? [srcDir] : [];
1322
+ const srcDir = join2(rootDir, "src");
1323
+ const dirsToScan = existsSync4(srcDir) ? [srcDir] : [];
1088
1324
  for (const scanDir of dirsToScan) {
1089
1325
  try {
1090
- const entries = readdirSync(scanDir, { withFileTypes: true });
1326
+ const entries = readdirSync2(scanDir, { withFileTypes: true });
1091
1327
  for (const entry of entries) {
1092
1328
  if (entry.isFile() && CSS_EXTS.some((x) => entry.name.endsWith(x))) {
1093
- const filePath = join(scanDir, entry.name);
1329
+ const filePath = join2(scanDir, entry.name);
1094
1330
  const content = readSafe(filePath);
1095
1331
  if (content !== null && CSS_CUSTOM_PROPS_RE.test(content)) {
1096
1332
  sources.push({ kind: "css-custom-properties", path: filePath });
@@ -1100,12 +1336,12 @@ function detectTokenSources(rootDir) {
1100
1336
  } catch {
1101
1337
  }
1102
1338
  }
1103
- if (existsSync3(srcDir)) {
1339
+ if (existsSync4(srcDir)) {
1104
1340
  try {
1105
- const entries = readdirSync(srcDir);
1341
+ const entries = readdirSync2(srcDir);
1106
1342
  for (const entry of entries) {
1107
1343
  if (THEME_SUFFIXES.some((s) => entry.endsWith(s))) {
1108
- sources.push({ kind: "theme-file", path: join(srcDir, entry) });
1344
+ sources.push({ kind: "theme-file", path: join2(srcDir, entry) });
1109
1345
  }
1110
1346
  }
1111
1347
  } catch {
@@ -1114,7 +1350,7 @@ function detectTokenSources(rootDir) {
1114
1350
  return sources;
1115
1351
  }
1116
1352
  function detectProject(rootDir) {
1117
- const pkgPath = join(rootDir, "package.json");
1353
+ const pkgPath = join2(rootDir, "package.json");
1118
1354
  let packageDeps = {};
1119
1355
  const pkgContent = readSafe(pkgPath);
1120
1356
  if (pkgContent !== null) {
@@ -1132,25 +1368,28 @@ function detectProject(rootDir) {
1132
1368
  const packageManager = detectPackageManager(rootDir);
1133
1369
  const componentPatterns = detectComponentPatterns(rootDir, typescript);
1134
1370
  const tokenSources = detectTokenSources(rootDir);
1371
+ const globalCSSFiles = detectGlobalCSSFiles(rootDir);
1135
1372
  return {
1136
1373
  framework,
1137
1374
  typescript,
1138
1375
  tsconfigPath,
1139
1376
  componentPatterns,
1140
1377
  tokenSources,
1141
- packageManager
1378
+ packageManager,
1379
+ globalCSSFiles
1142
1380
  };
1143
1381
  }
1144
1382
 
1145
1383
  // src/init/index.ts
1146
- import { Command as Command2 } from "commander";
1384
+ import { generateManifest as generateManifest2 } from "@agent-scope/manifest";
1385
+ import { Command as Command3 } from "commander";
1147
1386
  function buildDefaultConfig(detected, tokenFile, outputDir) {
1148
1387
  const include = detected.componentPatterns.length > 0 ? detected.componentPatterns : ["src/**/*.tsx"];
1149
1388
  return {
1150
1389
  components: {
1151
1390
  include,
1152
1391
  exclude: ["**/*.test.tsx", "**/*.stories.tsx"],
1153
- wrappers: { providers: [], globalCSS: [] }
1392
+ wrappers: { providers: [], globalCSS: detected.globalCSSFiles ?? [] }
1154
1393
  },
1155
1394
  render: {
1156
1395
  viewport: { default: { width: 1280, height: 800 } },
@@ -1181,9 +1420,9 @@ function createRL() {
1181
1420
  });
1182
1421
  }
1183
1422
  async function ask(rl, question) {
1184
- return new Promise((resolve18) => {
1423
+ return new Promise((resolve19) => {
1185
1424
  rl.question(question, (answer) => {
1186
- resolve18(answer.trim());
1425
+ resolve19(answer.trim());
1187
1426
  });
1188
1427
  });
1189
1428
  }
@@ -1192,9 +1431,9 @@ async function askWithDefault(rl, label, defaultValue) {
1192
1431
  return answer.length > 0 ? answer : defaultValue;
1193
1432
  }
1194
1433
  function ensureGitignoreEntry(rootDir, entry) {
1195
- const gitignorePath = join2(rootDir, ".gitignore");
1196
- if (existsSync4(gitignorePath)) {
1197
- const content = readFileSync4(gitignorePath, "utf-8");
1434
+ const gitignorePath = join3(rootDir, ".gitignore");
1435
+ if (existsSync5(gitignorePath)) {
1436
+ const content = readFileSync5(gitignorePath, "utf-8");
1198
1437
  const normalised = entry.replace(/\/$/, "");
1199
1438
  const lines = content.split("\n").map((l) => l.trim());
1200
1439
  if (lines.includes(entry) || lines.includes(normalised)) {
@@ -1208,18 +1447,118 @@ function ensureGitignoreEntry(rootDir, entry) {
1208
1447
  `);
1209
1448
  }
1210
1449
  }
1450
+ function extractTailwindTokens(tokenSources) {
1451
+ const tailwindSource = tokenSources.find((s) => s.kind === "tailwind-config");
1452
+ if (!tailwindSource) return null;
1453
+ try {
1454
+ let parseBlock2 = function(block) {
1455
+ const result = {};
1456
+ const lineRe = /['"]?(\w[\w.-]*|\d+)['"]?\s*:\s*['"]?(#[0-9a-fA-F]{3,8}|\d+(?:px|rem|em|%)|[\w-]+(?:\/[\w]+)?)['"]?/g;
1457
+ for (const m of block.matchAll(lineRe)) {
1458
+ if (m[1] !== void 0 && m[2] !== void 0) {
1459
+ result[m[1]] = m[2];
1460
+ }
1461
+ }
1462
+ return result;
1463
+ };
1464
+ var parseBlock = parseBlock2;
1465
+ const raw = readFileSync5(tailwindSource.path, "utf-8");
1466
+ const tokens = {};
1467
+ const colorsKeyIdx = raw.indexOf("colors:");
1468
+ if (colorsKeyIdx !== -1) {
1469
+ const colorsBraceStart = raw.indexOf("{", colorsKeyIdx);
1470
+ if (colorsBraceStart !== -1) {
1471
+ let colorDepth = 0;
1472
+ let colorsBraceEnd = -1;
1473
+ for (let ci = colorsBraceStart; ci < raw.length; ci++) {
1474
+ if (raw[ci] === "{") colorDepth++;
1475
+ else if (raw[ci] === "}") {
1476
+ colorDepth--;
1477
+ if (colorDepth === 0) {
1478
+ colorsBraceEnd = ci;
1479
+ break;
1480
+ }
1481
+ }
1482
+ }
1483
+ if (colorsBraceEnd > colorsBraceStart) {
1484
+ const colorSection = raw.slice(colorsBraceStart + 1, colorsBraceEnd);
1485
+ const scaleRe = /(\w+)\s*:\s*\{([^}]+)\}/g;
1486
+ const colorTokens = {};
1487
+ for (const sm of colorSection.matchAll(scaleRe)) {
1488
+ if (sm[1] === void 0 || sm[2] === void 0) continue;
1489
+ const scaleName = sm[1];
1490
+ const scaleValues = parseBlock2(sm[2]);
1491
+ if (Object.keys(scaleValues).length > 0) {
1492
+ const scaleTokens = {};
1493
+ for (const [step, hex] of Object.entries(scaleValues)) {
1494
+ scaleTokens[step] = { value: hex, type: "color" };
1495
+ }
1496
+ colorTokens[scaleName] = scaleTokens;
1497
+ }
1498
+ }
1499
+ if (Object.keys(colorTokens).length > 0) {
1500
+ tokens.color = colorTokens;
1501
+ }
1502
+ }
1503
+ }
1504
+ }
1505
+ const spacingMatch = raw.match(/spacing\s*:\s*\{([\s\S]*?)\n\s*\}/);
1506
+ if (spacingMatch?.[1] !== void 0) {
1507
+ const spacingValues = parseBlock2(spacingMatch[1]);
1508
+ if (Object.keys(spacingValues).length > 0) {
1509
+ const spacingTokens = {};
1510
+ for (const [key, val] of Object.entries(spacingValues)) {
1511
+ spacingTokens[key] = { value: val, type: "dimension" };
1512
+ }
1513
+ tokens.spacing = spacingTokens;
1514
+ }
1515
+ }
1516
+ const fontFamilyMatch = raw.match(/fontFamily\s*:\s*\{([\s\S]*?)\n\s*\}/);
1517
+ if (fontFamilyMatch?.[1] !== void 0) {
1518
+ const fontFamilyRe = /(\w+)\s*:\s*\[\s*['"]([^'"]+)['"]/g;
1519
+ const fontTokens = {};
1520
+ for (const fm of fontFamilyMatch[1].matchAll(fontFamilyRe)) {
1521
+ if (fm[1] !== void 0 && fm[2] !== void 0) {
1522
+ fontTokens[fm[1]] = { value: fm[2], type: "fontFamily" };
1523
+ }
1524
+ }
1525
+ if (Object.keys(fontTokens).length > 0) {
1526
+ tokens.font = fontTokens;
1527
+ }
1528
+ }
1529
+ const borderRadiusMatch = raw.match(/borderRadius\s*:\s*\{([\s\S]*?)\n\s*\}/);
1530
+ if (borderRadiusMatch?.[1] !== void 0) {
1531
+ const radiusValues = parseBlock2(borderRadiusMatch[1]);
1532
+ if (Object.keys(radiusValues).length > 0) {
1533
+ const radiusTokens = {};
1534
+ for (const [key, val] of Object.entries(radiusValues)) {
1535
+ radiusTokens[key] = { value: val, type: "dimension" };
1536
+ }
1537
+ tokens.radius = radiusTokens;
1538
+ }
1539
+ }
1540
+ return Object.keys(tokens).length > 0 ? tokens : null;
1541
+ } catch {
1542
+ return null;
1543
+ }
1544
+ }
1211
1545
  function scaffoldConfig(rootDir, config) {
1212
- const path = join2(rootDir, "reactscope.config.json");
1546
+ const path = join3(rootDir, "reactscope.config.json");
1213
1547
  writeFileSync3(path, `${JSON.stringify(config, null, 2)}
1214
1548
  `);
1215
1549
  return path;
1216
1550
  }
1217
- function scaffoldTokenFile(rootDir, tokenFile) {
1218
- const path = join2(rootDir, tokenFile);
1219
- if (!existsSync4(path)) {
1551
+ function scaffoldTokenFile(rootDir, tokenFile, extractedTokens) {
1552
+ const path = join3(rootDir, tokenFile);
1553
+ if (!existsSync5(path)) {
1220
1554
  const stub = {
1221
1555
  $schema: "https://raw.githubusercontent.com/FlatFilers/Scope/main/packages/tokens/schema.json",
1222
- tokens: {}
1556
+ version: "1.0.0",
1557
+ meta: {
1558
+ name: "Design Tokens",
1559
+ lastUpdated: (/* @__PURE__ */ new Date()).toISOString().split("T")[0]
1560
+ },
1561
+ tokens: extractedTokens ?? {}
1223
1562
  };
1224
1563
  writeFileSync3(path, `${JSON.stringify(stub, null, 2)}
1225
1564
  `);
@@ -1227,19 +1566,19 @@ function scaffoldTokenFile(rootDir, tokenFile) {
1227
1566
  return path;
1228
1567
  }
1229
1568
  function scaffoldOutputDir(rootDir, outputDir) {
1230
- const dirPath = join2(rootDir, outputDir);
1569
+ const dirPath = join3(rootDir, outputDir);
1231
1570
  mkdirSync(dirPath, { recursive: true });
1232
- const keepPath = join2(dirPath, ".gitkeep");
1233
- if (!existsSync4(keepPath)) {
1571
+ const keepPath = join3(dirPath, ".gitkeep");
1572
+ if (!existsSync5(keepPath)) {
1234
1573
  writeFileSync3(keepPath, "");
1235
1574
  }
1236
1575
  return dirPath;
1237
1576
  }
1238
1577
  async function runInit(options) {
1239
1578
  const rootDir = options.cwd ?? process.cwd();
1240
- const configPath = join2(rootDir, "reactscope.config.json");
1579
+ const configPath = join3(rootDir, "reactscope.config.json");
1241
1580
  const created = [];
1242
- if (existsSync4(configPath) && !options.force) {
1581
+ if (existsSync5(configPath) && !options.force) {
1243
1582
  const msg = "reactscope.config.json already exists. Run with --force to overwrite.";
1244
1583
  process.stderr.write(`\u26A0\uFE0F ${msg}
1245
1584
  `);
@@ -1297,7 +1636,13 @@ async function runInit(options) {
1297
1636
  }
1298
1637
  const cfgPath = scaffoldConfig(rootDir, config);
1299
1638
  created.push(cfgPath);
1300
- const tokPath = scaffoldTokenFile(rootDir, config.tokens.file);
1639
+ const extractedTokens = extractTailwindTokens(detected.tokenSources);
1640
+ if (extractedTokens !== null) {
1641
+ const tokenGroupCount = Object.keys(extractedTokens).length;
1642
+ process.stdout.write(` Extracted ${tokenGroupCount} token group(s) from Tailwind config
1643
+ `);
1644
+ }
1645
+ const tokPath = scaffoldTokenFile(rootDir, config.tokens.file, extractedTokens ?? void 0);
1301
1646
  created.push(tokPath);
1302
1647
  const outDirPath = scaffoldOutputDir(rootDir, config.output.dir);
1303
1648
  created.push(outDirPath);
@@ -1308,7 +1653,28 @@ async function runInit(options) {
1308
1653
  process.stdout.write(` ${p}
1309
1654
  `);
1310
1655
  }
1311
- process.stdout.write("\n Next steps: run `scope manifest` to scan your components.\n\n");
1656
+ process.stdout.write("\n Scanning components...\n");
1657
+ try {
1658
+ const manifestConfig = {
1659
+ include: config.components.include,
1660
+ rootDir
1661
+ };
1662
+ const manifest = await generateManifest2(manifestConfig);
1663
+ const manifestCount = Object.keys(manifest.components).length;
1664
+ const manifestOutPath = join3(rootDir, config.output.dir, "manifest.json");
1665
+ mkdirSync(join3(rootDir, config.output.dir), { recursive: true });
1666
+ writeFileSync3(manifestOutPath, `${JSON.stringify(manifest, null, 2)}
1667
+ `);
1668
+ process.stdout.write(
1669
+ ` Found ${manifestCount} component(s) \u2014 manifest written to ${manifestOutPath}
1670
+ `
1671
+ );
1672
+ } catch {
1673
+ process.stdout.write(
1674
+ " (manifest generate skipped \u2014 run `scope manifest generate` manually)\n"
1675
+ );
1676
+ }
1677
+ process.stdout.write("\n");
1312
1678
  return {
1313
1679
  success: true,
1314
1680
  message: "Project initialised successfully.",
@@ -1317,7 +1683,7 @@ async function runInit(options) {
1317
1683
  };
1318
1684
  }
1319
1685
  function createInitCommand() {
1320
- return new Command2("init").description("Initialise a Scope project \u2014 scaffold reactscope.config.json and friends").option("-y, --yes", "Accept all detected defaults without prompting", false).option("--force", "Overwrite existing reactscope.config.json if present", false).action(async (opts) => {
1686
+ return new Command3("init").description("Initialise a Scope project \u2014 scaffold reactscope.config.json and friends").option("-y, --yes", "Accept all detected defaults without prompting", false).option("--force", "Overwrite existing reactscope.config.json if present", false).action(async (opts) => {
1321
1687
  try {
1322
1688
  const result = await runInit({ yes: opts.yes, force: opts.force });
1323
1689
  if (!result.success && !result.skipped) {
@@ -1332,24 +1698,24 @@ function createInitCommand() {
1332
1698
  }
1333
1699
 
1334
1700
  // src/instrument/renders.ts
1335
- import { resolve as resolve7 } from "path";
1701
+ import { resolve as resolve8 } from "path";
1336
1702
  import { getBrowserEntryScript as getBrowserEntryScript5 } from "@agent-scope/playwright";
1337
1703
  import { BrowserPool as BrowserPool2 } from "@agent-scope/render";
1338
- import { Command as Command5 } from "commander";
1704
+ import { Command as Command6 } from "commander";
1339
1705
 
1340
1706
  // src/manifest-commands.ts
1341
- import { existsSync as existsSync5, mkdirSync as mkdirSync2, readFileSync as readFileSync5, writeFileSync as writeFileSync4 } from "fs";
1342
- import { resolve as resolve3 } from "path";
1343
- import { generateManifest as generateManifest2 } from "@agent-scope/manifest";
1344
- import { Command as Command3 } from "commander";
1707
+ import { existsSync as existsSync6, mkdirSync as mkdirSync2, readFileSync as readFileSync6, writeFileSync as writeFileSync4 } from "fs";
1708
+ import { resolve as resolve4 } from "path";
1709
+ import { generateManifest as generateManifest3 } from "@agent-scope/manifest";
1710
+ import { Command as Command4 } from "commander";
1345
1711
  var MANIFEST_PATH = ".reactscope/manifest.json";
1346
1712
  function loadManifest(manifestPath = MANIFEST_PATH) {
1347
- const absPath = resolve3(process.cwd(), manifestPath);
1348
- if (!existsSync5(absPath)) {
1713
+ const absPath = resolve4(process.cwd(), manifestPath);
1714
+ if (!existsSync6(absPath)) {
1349
1715
  throw new Error(`Manifest not found at ${absPath}.
1350
1716
  Run \`scope manifest generate\` first.`);
1351
1717
  }
1352
- const raw = readFileSync5(absPath, "utf-8");
1718
+ const raw = readFileSync6(absPath, "utf-8");
1353
1719
  return JSON.parse(raw);
1354
1720
  }
1355
1721
  function resolveFormat(formatFlag) {
@@ -1409,7 +1775,10 @@ Available: ${available}${hint}`
1409
1775
  });
1410
1776
  }
1411
1777
  function registerQuery(manifestCmd) {
1412
- 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(
1778
+ 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(
1779
+ "--has-prop <spec>",
1780
+ "Find components with a prop matching name or name:type (e.g. 'loading' or 'variant:union')"
1781
+ ).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(
1413
1782
  (opts) => {
1414
1783
  try {
1415
1784
  const manifest = loadManifest(opts.manifest);
@@ -1420,9 +1789,11 @@ function registerQuery(manifestCmd) {
1420
1789
  if (opts.complexity !== void 0) queryParts.push(`complexity=${opts.complexity}`);
1421
1790
  if (opts.sideEffects) queryParts.push("side-effects");
1422
1791
  if (opts.hasFetch) queryParts.push("has-fetch");
1792
+ if (opts.hasProp !== void 0) queryParts.push(`has-prop=${opts.hasProp}`);
1793
+ if (opts.composedBy !== void 0) queryParts.push(`composed-by=${opts.composedBy}`);
1423
1794
  if (queryParts.length === 0) {
1424
1795
  process.stderr.write(
1425
- "No query flags specified. Use --context, --hook, --complexity, --side-effects, or --has-fetch.\n"
1796
+ "No query flags specified. Use --context, --hook, --complexity, --side-effects, --has-fetch, --has-prop, or --composed-by.\n"
1426
1797
  );
1427
1798
  process.exit(1);
1428
1799
  }
@@ -1449,6 +1820,27 @@ function registerQuery(manifestCmd) {
1449
1820
  if (opts.hasFetch) {
1450
1821
  entries = entries.filter(([, d]) => d.sideEffects.fetches.length > 0);
1451
1822
  }
1823
+ if (opts.hasProp !== void 0) {
1824
+ const spec = opts.hasProp;
1825
+ const colonIdx = spec.indexOf(":");
1826
+ const propName = colonIdx >= 0 ? spec.slice(0, colonIdx) : spec;
1827
+ const propType = colonIdx >= 0 ? spec.slice(colonIdx + 1) : void 0;
1828
+ entries = entries.filter(([, d]) => {
1829
+ const props = d.props;
1830
+ if (!props || !(propName in props)) return false;
1831
+ if (propType !== void 0) {
1832
+ return props[propName]?.type === propType;
1833
+ }
1834
+ return true;
1835
+ });
1836
+ }
1837
+ if (opts.composedBy !== void 0) {
1838
+ const targetName = opts.composedBy;
1839
+ entries = entries.filter(([, d]) => {
1840
+ const composedBy = d.composedBy;
1841
+ return composedBy !== void 0 && composedBy.includes(targetName);
1842
+ });
1843
+ }
1452
1844
  const rows = entries.map(([name, d]) => ({
1453
1845
  name,
1454
1846
  file: d.filePath,
@@ -1472,13 +1864,13 @@ function registerGenerate(manifestCmd) {
1472
1864
  "Generate the component manifest from source and write to .reactscope/manifest.json"
1473
1865
  ).option("--root <path>", "Project root directory (default: cwd)").option("--output <path>", "Output path for manifest.json", MANIFEST_PATH).option("--include <globs>", "Comma-separated glob patterns to include").option("--exclude <globs>", "Comma-separated glob patterns to exclude").action(async (opts) => {
1474
1866
  try {
1475
- const rootDir = resolve3(process.cwd(), opts.root ?? ".");
1476
- const outputPath = resolve3(process.cwd(), opts.output);
1867
+ const rootDir = resolve4(process.cwd(), opts.root ?? ".");
1868
+ const outputPath = resolve4(process.cwd(), opts.output);
1477
1869
  const include = opts.include?.split(",").map((s) => s.trim());
1478
1870
  const exclude = opts.exclude?.split(",").map((s) => s.trim());
1479
1871
  process.stderr.write(`Scanning ${rootDir} for React components...
1480
1872
  `);
1481
- const manifest = await generateManifest2({
1873
+ const manifest = await generateManifest3({
1482
1874
  rootDir,
1483
1875
  ...include !== void 0 && { include },
1484
1876
  ...exclude !== void 0 && { exclude }
@@ -1487,7 +1879,7 @@ function registerGenerate(manifestCmd) {
1487
1879
  process.stderr.write(`Found ${componentCount} components.
1488
1880
  `);
1489
1881
  const outputDir = outputPath.replace(/\/[^/]+$/, "");
1490
- if (!existsSync5(outputDir)) {
1882
+ if (!existsSync6(outputDir)) {
1491
1883
  mkdirSync2(outputDir, { recursive: true });
1492
1884
  }
1493
1885
  writeFileSync4(outputPath, JSON.stringify(manifest, null, 2), "utf-8");
@@ -1503,7 +1895,7 @@ function registerGenerate(manifestCmd) {
1503
1895
  });
1504
1896
  }
1505
1897
  function createManifestCommand() {
1506
- const manifestCmd = new Command3("manifest").description(
1898
+ const manifestCmd = new Command4("manifest").description(
1507
1899
  "Query and explore the component manifest"
1508
1900
  );
1509
1901
  registerList(manifestCmd);
@@ -1514,7 +1906,7 @@ function createManifestCommand() {
1514
1906
  }
1515
1907
 
1516
1908
  // src/instrument/hooks.ts
1517
- import { resolve as resolve4 } from "path";
1909
+ import { resolve as resolve5 } from "path";
1518
1910
  import { getBrowserEntryScript as getBrowserEntryScript2 } from "@agent-scope/playwright";
1519
1911
  import { Command as Cmd } from "commander";
1520
1912
  import { chromium as chromium2 } from "playwright";
@@ -1846,7 +2238,7 @@ Available: ${available}`
1846
2238
  throw new Error(`Invalid props JSON: ${opts.props}`);
1847
2239
  }
1848
2240
  const rootDir = process.cwd();
1849
- const filePath = resolve4(rootDir, descriptor.filePath);
2241
+ const filePath = resolve5(rootDir, descriptor.filePath);
1850
2242
  process.stderr.write(`Instrumenting hooks for ${componentName}\u2026
1851
2243
  `);
1852
2244
  const result = await runHooksProfiling(componentName, filePath, props);
@@ -1874,7 +2266,7 @@ Available: ${available}`
1874
2266
  }
1875
2267
 
1876
2268
  // src/instrument/profile.ts
1877
- import { resolve as resolve5 } from "path";
2269
+ import { resolve as resolve6 } from "path";
1878
2270
  import { getBrowserEntryScript as getBrowserEntryScript3 } from "@agent-scope/playwright";
1879
2271
  import { Command as Cmd2 } from "commander";
1880
2272
  import { chromium as chromium3 } from "playwright";
@@ -2149,7 +2541,7 @@ Available: ${available}`
2149
2541
  throw new Error(`Invalid interaction JSON: ${opts.interaction}`);
2150
2542
  }
2151
2543
  const rootDir = process.cwd();
2152
- const filePath = resolve5(rootDir, descriptor.filePath);
2544
+ const filePath = resolve6(rootDir, descriptor.filePath);
2153
2545
  process.stderr.write(`Profiling interaction for ${componentName}\u2026
2154
2546
  `);
2155
2547
  const result = await runInteractionProfile(componentName, filePath, props, interaction);
@@ -2177,9 +2569,9 @@ Available: ${available}`
2177
2569
  }
2178
2570
 
2179
2571
  // src/instrument/tree.ts
2180
- import { resolve as resolve6 } from "path";
2572
+ import { resolve as resolve7 } from "path";
2181
2573
  import { getBrowserEntryScript as getBrowserEntryScript4 } from "@agent-scope/playwright";
2182
- import { Command as Command4 } from "commander";
2574
+ import { Command as Command5 } from "commander";
2183
2575
  import { chromium as chromium4 } from "playwright";
2184
2576
  var MANIFEST_PATH4 = ".reactscope/manifest.json";
2185
2577
  var DEFAULT_VIEWPORT_WIDTH = 375;
@@ -2460,7 +2852,7 @@ async function runInstrumentTree(options) {
2460
2852
  }
2461
2853
  }
2462
2854
  function createInstrumentTreeCommand() {
2463
- return new Command4("tree").description("Render a component via BrowserPool and output a structured instrumentation tree").argument("<component>", "Component name to instrument (must exist in the manifest)").option("--sort-by <field>", "Sort nodes by field: renderCount | depth").option("--limit <n>", "Limit output to the first N nodes (depth-first)").option("--uses-context <name>", "Filter to components that use a specific context").option("--provider-depth", "Annotate each node with its context-provider nesting depth", false).option(
2855
+ return new Command5("tree").description("Render a component via BrowserPool and output a structured instrumentation tree").argument("<component>", "Component name to instrument (must exist in the manifest)").option("--sort-by <field>", "Sort nodes by field: renderCount | depth").option("--limit <n>", "Limit output to the first N nodes (depth-first)").option("--uses-context <name>", "Filter to components that use a specific context").option("--provider-depth", "Annotate each node with its context-provider nesting depth", false).option(
2464
2856
  "--wasted-renders",
2465
2857
  "Filter to components with wasted renders (no prop/state/context changes, not memoized)",
2466
2858
  false
@@ -2484,7 +2876,7 @@ Available: ${available}`
2484
2876
  }
2485
2877
  }
2486
2878
  const rootDir = process.cwd();
2487
- const filePath = resolve6(rootDir, descriptor.filePath);
2879
+ const filePath = resolve7(rootDir, descriptor.filePath);
2488
2880
  process.stderr.write(`Instrumenting ${componentName}\u2026
2489
2881
  `);
2490
2882
  const instrumentRoot = await runInstrumentTree({
@@ -2855,7 +3247,7 @@ Available: ${available}`
2855
3247
  );
2856
3248
  }
2857
3249
  const rootDir = process.cwd();
2858
- const filePath = resolve7(rootDir, descriptor.filePath);
3250
+ const filePath = resolve8(rootDir, descriptor.filePath);
2859
3251
  const preScript = getBrowserEntryScript5() + "\n" + buildInstrumentationScript();
2860
3252
  const htmlHarness = await buildComponentHarness(
2861
3253
  filePath,
@@ -2945,7 +3337,7 @@ function formatRendersTable(result) {
2945
3337
  return lines.join("\n");
2946
3338
  }
2947
3339
  function createInstrumentRendersCommand() {
2948
- return new Command5("renders").description("Trace re-render causality chains for a component during an interaction sequence").argument("<component>", "Component name to instrument (must be in manifest)").option(
3340
+ return new Command6("renders").description("Trace re-render causality chains for a component during an interaction sequence").argument("<component>", "Component name to instrument (must be in manifest)").option(
2949
3341
  "--interaction <json>",
2950
3342
  `Interaction sequence JSON, e.g. '[{"action":"click","target":"button"}]'`,
2951
3343
  "[]"
@@ -2990,7 +3382,7 @@ function createInstrumentRendersCommand() {
2990
3382
  );
2991
3383
  }
2992
3384
  function createInstrumentCommand() {
2993
- const instrumentCmd = new Command5("instrument").description(
3385
+ const instrumentCmd = new Command6("instrument").description(
2994
3386
  "Structured instrumentation commands for React component analysis"
2995
3387
  );
2996
3388
  instrumentCmd.addCommand(createInstrumentRendersCommand());
@@ -3001,8 +3393,8 @@ function createInstrumentCommand() {
3001
3393
  }
3002
3394
 
3003
3395
  // src/render-commands.ts
3004
- import { mkdirSync as mkdirSync4, writeFileSync as writeFileSync5 } from "fs";
3005
- import { resolve as resolve9 } from "path";
3396
+ import { existsSync as existsSync8, mkdirSync as mkdirSync4, readFileSync as readFileSync7, writeFileSync as writeFileSync5 } from "fs";
3397
+ import { resolve as resolve10 } from "path";
3006
3398
  import {
3007
3399
  ALL_CONTEXT_IDS,
3008
3400
  ALL_STRESS_IDS,
@@ -3013,13 +3405,13 @@ import {
3013
3405
  safeRender as safeRender2,
3014
3406
  stressAxis
3015
3407
  } from "@agent-scope/render";
3016
- import { Command as Command6 } from "commander";
3408
+ import { Command as Command7 } from "commander";
3017
3409
 
3018
3410
  // src/scope-file.ts
3019
- import { existsSync as existsSync6, mkdirSync as mkdirSync3, rmSync } from "fs";
3411
+ import { existsSync as existsSync7, mkdirSync as mkdirSync3, rmSync } from "fs";
3020
3412
  import { createRequire as createRequire2 } from "module";
3021
3413
  import { tmpdir } from "os";
3022
- import { dirname as dirname2, join as join3, resolve as resolve8 } from "path";
3414
+ import { dirname as dirname2, join as join4, resolve as resolve9 } from "path";
3023
3415
  import * as esbuild2 from "esbuild";
3024
3416
  var SCOPE_EXTENSIONS = [".scope.tsx", ".scope.ts", ".scope.jsx", ".scope.js"];
3025
3417
  function findScopeFile(componentFilePath) {
@@ -3027,15 +3419,15 @@ function findScopeFile(componentFilePath) {
3027
3419
  const stem = componentFilePath.replace(/\.(tsx?|jsx?)$/, "");
3028
3420
  const baseName = stem.slice(dir.length + 1);
3029
3421
  for (const ext of SCOPE_EXTENSIONS) {
3030
- const candidate = join3(dir, `${baseName}${ext}`);
3031
- if (existsSync6(candidate)) return candidate;
3422
+ const candidate = join4(dir, `${baseName}${ext}`);
3423
+ if (existsSync7(candidate)) return candidate;
3032
3424
  }
3033
3425
  return null;
3034
3426
  }
3035
3427
  async function loadScopeFile(scopeFilePath) {
3036
- const tmpDir = join3(tmpdir(), `scope-file-${Date.now()}-${Math.random().toString(36).slice(2)}`);
3428
+ const tmpDir = join4(tmpdir(), `scope-file-${Date.now()}-${Math.random().toString(36).slice(2)}`);
3037
3429
  mkdirSync3(tmpDir, { recursive: true });
3038
- const outFile = join3(tmpDir, "scope-file.cjs");
3430
+ const outFile = join4(tmpDir, "scope-file.cjs");
3039
3431
  try {
3040
3432
  const result = await esbuild2.build({
3041
3433
  entryPoints: [scopeFilePath],
@@ -3060,7 +3452,7 @@ async function loadScopeFile(scopeFilePath) {
3060
3452
  ${msg}`);
3061
3453
  }
3062
3454
  const req = createRequire2(import.meta.url);
3063
- delete req.cache[resolve8(outFile)];
3455
+ delete req.cache[resolve9(outFile)];
3064
3456
  const mod = req(outFile);
3065
3457
  const scenarios = extractScenarios(mod, scopeFilePath);
3066
3458
  const hasWrapper = typeof mod.wrapper === "function" || typeof mod.default?.wrapper === "function";
@@ -3137,6 +3529,17 @@ ${msg}`);
3137
3529
  }
3138
3530
 
3139
3531
  // src/render-commands.ts
3532
+ function loadGlobalCssFilesFromConfig(cwd) {
3533
+ const configPath = resolve10(cwd, "reactscope.config.json");
3534
+ if (!existsSync8(configPath)) return [];
3535
+ try {
3536
+ const raw = readFileSync7(configPath, "utf-8");
3537
+ const cfg = JSON.parse(raw);
3538
+ return cfg.components?.wrappers?.globalCSS ?? [];
3539
+ } catch {
3540
+ return [];
3541
+ }
3542
+ }
3140
3543
  var MANIFEST_PATH6 = ".reactscope/manifest.json";
3141
3544
  var DEFAULT_OUTPUT_DIR = ".reactscope/renders";
3142
3545
  var _pool3 = null;
@@ -3157,7 +3560,7 @@ async function shutdownPool3() {
3157
3560
  _pool3 = null;
3158
3561
  }
3159
3562
  }
3160
- function buildRenderer(filePath, componentName, viewportWidth, viewportHeight, wrapperScript) {
3563
+ function buildRenderer(filePath, componentName, viewportWidth, viewportHeight, globalCssFiles = [], projectCwd = process.cwd(), wrapperScript) {
3161
3564
  const satori = new SatoriRenderer({
3162
3565
  defaultViewport: { width: viewportWidth, height: viewportHeight }
3163
3566
  });
@@ -3166,13 +3569,13 @@ function buildRenderer(filePath, componentName, viewportWidth, viewportHeight, w
3166
3569
  async renderCell(props, _complexityClass) {
3167
3570
  const startMs = performance.now();
3168
3571
  const pool = await getPool3(viewportWidth, viewportHeight);
3572
+ const projectCss = await loadGlobalCss(globalCssFiles, projectCwd);
3169
3573
  const htmlHarness = await buildComponentHarness(
3170
3574
  filePath,
3171
3575
  componentName,
3172
3576
  props,
3173
3577
  viewportWidth,
3174
- void 0,
3175
- // projectCss (handled separately)
3578
+ projectCss ?? void 0,
3176
3579
  wrapperScript
3177
3580
  );
3178
3581
  const slot = await pool.acquire();
@@ -3201,10 +3604,10 @@ function buildRenderer(filePath, componentName, viewportWidth, viewportHeight, w
3201
3604
  }
3202
3605
  });
3203
3606
  return [...set];
3204
- });
3205
- const projectCss = await getCompiledCssForClasses(rootDir, classes);
3206
- if (projectCss != null && projectCss.length > 0) {
3207
- await page.addStyleTag({ content: projectCss });
3607
+ }) ?? [];
3608
+ const projectCss2 = await getCompiledCssForClasses(rootDir, classes);
3609
+ if (projectCss2 != null && projectCss2.length > 0) {
3610
+ await page.addStyleTag({ content: projectCss2 });
3208
3611
  }
3209
3612
  const renderTimeMs = performance.now() - startMs;
3210
3613
  const rootLocator = page.locator("[data-reactscope-root]");
@@ -3214,49 +3617,147 @@ function buildRenderer(filePath, componentName, viewportWidth, viewportHeight, w
3214
3617
  `Component "${componentName}" rendered with zero bounding box \u2014 it may be invisible or not mounted`
3215
3618
  );
3216
3619
  }
3217
- const PAD = 24;
3218
- const MIN_W = 320;
3219
- const MIN_H = 200;
3620
+ const PAD = 8;
3220
3621
  const clipX = Math.max(0, boundingBox.x - PAD);
3221
3622
  const clipY = Math.max(0, boundingBox.y - PAD);
3222
3623
  const rawW = boundingBox.width + PAD * 2;
3223
3624
  const rawH = boundingBox.height + PAD * 2;
3224
- const clipW = Math.max(rawW, MIN_W);
3225
- const clipH = Math.max(rawH, MIN_H);
3226
- const safeW = Math.min(clipW, viewportWidth - clipX);
3227
- const safeH = Math.min(clipH, viewportHeight - clipY);
3625
+ const safeW = Math.min(rawW, viewportWidth - clipX);
3626
+ const safeH = Math.min(rawH, viewportHeight - clipY);
3228
3627
  const screenshot = await page.screenshot({
3229
3628
  clip: { x: clipX, y: clipY, width: safeW, height: safeH },
3230
3629
  type: "png"
3231
3630
  });
3631
+ const STYLE_PROPS = [
3632
+ "display",
3633
+ "width",
3634
+ "height",
3635
+ "color",
3636
+ "backgroundColor",
3637
+ "fontSize",
3638
+ "fontFamily",
3639
+ "fontWeight",
3640
+ "lineHeight",
3641
+ "padding",
3642
+ "paddingTop",
3643
+ "paddingRight",
3644
+ "paddingBottom",
3645
+ "paddingLeft",
3646
+ "margin",
3647
+ "marginTop",
3648
+ "marginRight",
3649
+ "marginBottom",
3650
+ "marginLeft",
3651
+ "gap",
3652
+ "borderRadius",
3653
+ "borderWidth",
3654
+ "borderColor",
3655
+ "borderStyle",
3656
+ "boxShadow",
3657
+ "opacity",
3658
+ "position",
3659
+ "flexDirection",
3660
+ "alignItems",
3661
+ "justifyContent",
3662
+ "overflow"
3663
+ ];
3664
+ const _domResult = await page.evaluate(
3665
+ (args) => {
3666
+ let count = 0;
3667
+ const styles = {};
3668
+ function captureStyles(el, id, propList) {
3669
+ const computed = window.getComputedStyle(el);
3670
+ const out = {};
3671
+ for (const prop of propList) {
3672
+ const val = computed[prop] ?? "";
3673
+ if (val && val !== "none" && val !== "normal" && val !== "auto") out[prop] = val;
3674
+ }
3675
+ styles[id] = out;
3676
+ }
3677
+ function walk(node) {
3678
+ if (node.nodeType === Node.TEXT_NODE) {
3679
+ return {
3680
+ tag: "#text",
3681
+ attrs: {},
3682
+ text: node.textContent?.trim() ?? "",
3683
+ children: []
3684
+ };
3685
+ }
3686
+ const el = node;
3687
+ const id = count++;
3688
+ captureStyles(el, id, args.props);
3689
+ const attrs = {};
3690
+ for (const attr of Array.from(el.attributes)) {
3691
+ attrs[attr.name] = attr.value;
3692
+ }
3693
+ const children = Array.from(el.childNodes).filter(
3694
+ (n) => n.nodeType === Node.ELEMENT_NODE || n.nodeType === Node.TEXT_NODE && (n.textContent?.trim() ?? "").length > 0
3695
+ ).map(walk);
3696
+ return { tag: el.tagName.toLowerCase(), attrs, nodeId: id, children };
3697
+ }
3698
+ const root = document.querySelector(args.sel);
3699
+ if (!root)
3700
+ return {
3701
+ tree: { tag: "div", attrs: {}, children: [] },
3702
+ elementCount: 0,
3703
+ nodeStyles: {}
3704
+ };
3705
+ return { tree: walk(root), elementCount: count, nodeStyles: styles };
3706
+ },
3707
+ { sel: "[data-reactscope-root] > *", props: STYLE_PROPS }
3708
+ );
3709
+ const domTree = _domResult?.tree ?? { tag: "div", attrs: {}, children: [] };
3710
+ const elementCount = _domResult?.elementCount ?? 0;
3711
+ const nodeStyles = _domResult?.nodeStyles ?? {};
3232
3712
  const computedStyles = {};
3233
- const styles = await page.evaluate((sel) => {
3234
- const el = document.querySelector(sel);
3235
- if (el === null) return {};
3236
- const computed = window.getComputedStyle(el);
3237
- const out = {};
3238
- for (const prop of [
3239
- "display",
3240
- "width",
3241
- "height",
3242
- "color",
3243
- "backgroundColor",
3244
- "fontSize",
3245
- "fontFamily",
3246
- "padding",
3247
- "margin"
3248
- ]) {
3249
- out[prop] = computed.getPropertyValue(prop);
3713
+ if (nodeStyles[0]) computedStyles["[data-reactscope-root] > *"] = nodeStyles[0];
3714
+ for (const [nodeId, styles] of Object.entries(nodeStyles)) {
3715
+ computedStyles[`#node-${nodeId}`] = styles;
3716
+ }
3717
+ const dom = {
3718
+ tree: domTree,
3719
+ elementCount,
3720
+ boundingBox: {
3721
+ x: boundingBox.x,
3722
+ y: boundingBox.y,
3723
+ width: boundingBox.width,
3724
+ height: boundingBox.height
3250
3725
  }
3251
- return out;
3252
- }, "[data-reactscope-root] > *");
3253
- computedStyles["[data-reactscope-root] > *"] = styles;
3726
+ };
3727
+ const a11yInfo = await page.evaluate((sel) => {
3728
+ const wrapper = document.querySelector(sel);
3729
+ const el = wrapper?.firstElementChild ?? wrapper;
3730
+ if (!el) return { role: "generic", name: "" };
3731
+ return {
3732
+ role: el.getAttribute("role") ?? el.tagName.toLowerCase() ?? "generic",
3733
+ name: el.getAttribute("aria-label") ?? el.getAttribute("aria-labelledby") ?? el.textContent?.trim().slice(0, 100) ?? ""
3734
+ };
3735
+ }, "[data-reactscope-root]") ?? {
3736
+ role: "generic",
3737
+ name: ""
3738
+ };
3739
+ const imgViolations = await page.evaluate((sel) => {
3740
+ const container = document.querySelector(sel);
3741
+ if (!container) return [];
3742
+ const issues = [];
3743
+ container.querySelectorAll("img").forEach((img) => {
3744
+ if (!img.alt) issues.push("Image missing accessible name");
3745
+ });
3746
+ return issues;
3747
+ }, "[data-reactscope-root]") ?? [];
3748
+ const accessibility = {
3749
+ role: a11yInfo.role,
3750
+ name: a11yInfo.name,
3751
+ violations: imgViolations
3752
+ };
3254
3753
  return {
3255
3754
  screenshot,
3256
3755
  width: Math.round(safeW),
3257
3756
  height: Math.round(safeH),
3258
3757
  renderTimeMs,
3259
- computedStyles
3758
+ computedStyles,
3759
+ dom,
3760
+ accessibility
3260
3761
  };
3261
3762
  } finally {
3262
3763
  pool.release(slot);
@@ -3306,26 +3807,64 @@ function registerRenderSingle(renderCmd) {
3306
3807
  Available: ${available}`
3307
3808
  );
3308
3809
  }
3810
+ let props = {};
3811
+ if (opts.props !== void 0) {
3812
+ try {
3813
+ props = JSON.parse(opts.props);
3814
+ } catch {
3815
+ throw new Error(`Invalid props JSON: ${opts.props}`);
3816
+ }
3817
+ }
3818
+ if (descriptor.props !== void 0) {
3819
+ const propDefs = descriptor.props;
3820
+ for (const [propName, propDef] of Object.entries(propDefs)) {
3821
+ if (propName in props) continue;
3822
+ if (!propDef.required && propDef.default !== void 0) continue;
3823
+ if (propDef.type === "node" || propDef.type === "string") {
3824
+ props[propName] = propName === "children" ? componentName : propName;
3825
+ } else if (propDef.type === "union" && propDef.values && propDef.values.length > 0) {
3826
+ props[propName] = propDef.values[0];
3827
+ } else if (propDef.type === "boolean") {
3828
+ props[propName] = false;
3829
+ } else if (propDef.type === "number") {
3830
+ props[propName] = 0;
3831
+ }
3832
+ }
3833
+ }
3309
3834
  const { width, height } = parseViewport(opts.viewport);
3310
3835
  const rootDir = process.cwd();
3311
- const filePath = resolve9(rootDir, descriptor.filePath);
3836
+ const filePath = resolve10(rootDir, descriptor.filePath);
3312
3837
  const scopeData = await loadScopeFileForComponent(filePath);
3313
3838
  const wrapperScript = scopeData?.hasWrapper === true ? await buildWrapperScript(scopeData.filePath) : void 0;
3314
3839
  const scenarios = buildScenarioMap(opts, scopeData);
3315
- const renderer = buildRenderer(filePath, componentName, width, height, wrapperScript);
3840
+ const globalCssFiles = loadGlobalCssFilesFromConfig(rootDir);
3841
+ if (globalCssFiles.length === 0) {
3842
+ process.stderr.write(
3843
+ "warning: No globalCSS files configured. Tailwind/CSS styles will not be applied to renders.\n Add `components.wrappers.globalCSS` to reactscope.config.json\n"
3844
+ );
3845
+ }
3846
+ const renderer = buildRenderer(
3847
+ filePath,
3848
+ componentName,
3849
+ width,
3850
+ height,
3851
+ globalCssFiles,
3852
+ rootDir,
3853
+ wrapperScript
3854
+ );
3316
3855
  process.stderr.write(
3317
3856
  `Rendering ${componentName} [${descriptor.complexityClass}] at ${width}\xD7${height}\u2026
3318
3857
  `
3319
3858
  );
3320
3859
  const fmt2 = resolveSingleFormat(opts.format);
3321
3860
  let anyFailed = false;
3322
- for (const [scenarioName, props] of Object.entries(scenarios)) {
3861
+ for (const [scenarioName, props2] of Object.entries(scenarios)) {
3323
3862
  const isNamed = scenarioName !== "__default__";
3324
3863
  const label = isNamed ? `${componentName}:${scenarioName}` : componentName;
3325
3864
  const outcome = await safeRender2(
3326
- () => renderer.renderCell(props, descriptor.complexityClass),
3865
+ () => renderer.renderCell(props2, descriptor.complexityClass),
3327
3866
  {
3328
- props,
3867
+ props: props2,
3329
3868
  sourceLocation: {
3330
3869
  file: descriptor.filePath,
3331
3870
  line: descriptor.loc.start,
@@ -3347,20 +3886,20 @@ Available: ${available}`
3347
3886
  const result = outcome.result;
3348
3887
  const outFileName = isNamed ? `${componentName}-${scenarioName}.png` : `${componentName}.png`;
3349
3888
  if (opts.output !== void 0 && !isNamed) {
3350
- const outPath = resolve9(process.cwd(), opts.output);
3889
+ const outPath = resolve10(process.cwd(), opts.output);
3351
3890
  writeFileSync5(outPath, result.screenshot);
3352
3891
  process.stdout.write(
3353
3892
  `\u2713 ${label} \u2192 ${opts.output} (${result.width}\xD7${result.height}, ${result.renderTimeMs.toFixed(0)}ms)
3354
3893
  `
3355
3894
  );
3356
3895
  } else if (fmt2 === "json") {
3357
- const json = formatRenderJson(label, props, result);
3896
+ const json = formatRenderJson(label, props2, result);
3358
3897
  process.stdout.write(`${JSON.stringify(json, null, 2)}
3359
3898
  `);
3360
3899
  } else {
3361
- const dir = resolve9(process.cwd(), DEFAULT_OUTPUT_DIR);
3900
+ const dir = resolve10(process.cwd(), DEFAULT_OUTPUT_DIR);
3362
3901
  mkdirSync4(dir, { recursive: true });
3363
- const outPath = resolve9(dir, outFileName);
3902
+ const outPath = resolve10(dir, outFileName);
3364
3903
  writeFileSync5(outPath, result.screenshot);
3365
3904
  const relPath = `${DEFAULT_OUTPUT_DIR}/${outFileName}`;
3366
3905
  process.stdout.write(
@@ -3381,7 +3920,10 @@ Available: ${available}`
3381
3920
  );
3382
3921
  }
3383
3922
  function registerRenderMatrix(renderCmd) {
3384
- 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(
3923
+ renderCmd.command("matrix <component>").description("Render a component across a matrix of prop axes").option(
3924
+ "--axes <spec>",
3925
+ `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"]}'`
3926
+ ).option(
3385
3927
  "--contexts <ids>",
3386
3928
  "Composition context IDs, comma-separated (e.g. centered,rtl,sidebar)"
3387
3929
  ).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(
@@ -3399,22 +3941,48 @@ Available: ${available}`
3399
3941
  const concurrency = Math.max(1, parseInt(opts.concurrency, 10) || 8);
3400
3942
  const { width, height } = { width: 375, height: 812 };
3401
3943
  const rootDir = process.cwd();
3402
- const filePath = resolve9(rootDir, descriptor.filePath);
3403
- const renderer = buildRenderer(filePath, componentName, width, height);
3944
+ const filePath = resolve10(rootDir, descriptor.filePath);
3945
+ const matrixCssFiles = loadGlobalCssFilesFromConfig(rootDir);
3946
+ const renderer = buildRenderer(
3947
+ filePath,
3948
+ componentName,
3949
+ width,
3950
+ height,
3951
+ matrixCssFiles,
3952
+ rootDir
3953
+ );
3404
3954
  const axes = [];
3405
3955
  if (opts.axes !== void 0) {
3406
- const axisSpecs = opts.axes.trim().split(/\s+/);
3407
- for (const spec of axisSpecs) {
3408
- const colonIdx = spec.indexOf(":");
3409
- if (colonIdx < 0) {
3410
- throw new Error(`Invalid axis spec "${spec}". Expected format: name:val1,val2,...`);
3956
+ const axesRaw = opts.axes.trim();
3957
+ if (axesRaw.startsWith("{")) {
3958
+ let parsed;
3959
+ try {
3960
+ parsed = JSON.parse(axesRaw);
3961
+ } catch {
3962
+ throw new Error(`Invalid JSON in --axes: ${axesRaw}`);
3411
3963
  }
3412
- const name = spec.slice(0, colonIdx);
3413
- const values = spec.slice(colonIdx + 1).split(",").map((v) => v.trim());
3414
- if (name.length === 0 || values.length === 0) {
3415
- throw new Error(`Invalid axis spec "${spec}"`);
3964
+ for (const [name, vals] of Object.entries(parsed)) {
3965
+ if (!Array.isArray(vals)) {
3966
+ throw new Error(`Axis "${name}" must be an array of values in JSON format`);
3967
+ }
3968
+ axes.push({ name, values: vals.map(String) });
3969
+ }
3970
+ } else {
3971
+ const axisSpecs = axesRaw.split(/\s+/);
3972
+ for (const spec of axisSpecs) {
3973
+ const colonIdx = spec.indexOf(":");
3974
+ if (colonIdx < 0) {
3975
+ throw new Error(
3976
+ `Invalid axis spec "${spec}". Expected format: name:val1,val2,...`
3977
+ );
3978
+ }
3979
+ const name = spec.slice(0, colonIdx);
3980
+ const values = spec.slice(colonIdx + 1).split(",").map((v) => v.trim());
3981
+ if (name.length === 0 || values.length === 0) {
3982
+ throw new Error(`Invalid axis spec "${spec}"`);
3983
+ }
3984
+ axes.push({ name, values });
3416
3985
  }
3417
- axes.push({ name, values });
3418
3986
  }
3419
3987
  }
3420
3988
  if (opts.contexts !== void 0) {
@@ -3466,7 +4034,7 @@ Available: ${available}`
3466
4034
  const { SpriteSheetGenerator: SpriteSheetGenerator2 } = await import("@agent-scope/render");
3467
4035
  const gen = new SpriteSheetGenerator2();
3468
4036
  const sheet = await gen.generate(result);
3469
- const spritePath = resolve9(process.cwd(), opts.sprite);
4037
+ const spritePath = resolve10(process.cwd(), opts.sprite);
3470
4038
  writeFileSync5(spritePath, sheet.png);
3471
4039
  process.stderr.write(`Sprite sheet saved to ${spritePath}
3472
4040
  `);
@@ -3476,9 +4044,9 @@ Available: ${available}`
3476
4044
  const { SpriteSheetGenerator: SpriteSheetGenerator2 } = await import("@agent-scope/render");
3477
4045
  const gen = new SpriteSheetGenerator2();
3478
4046
  const sheet = await gen.generate(result);
3479
- const dir = resolve9(process.cwd(), DEFAULT_OUTPUT_DIR);
4047
+ const dir = resolve10(process.cwd(), DEFAULT_OUTPUT_DIR);
3480
4048
  mkdirSync4(dir, { recursive: true });
3481
- const outPath = resolve9(dir, `${componentName}-matrix.png`);
4049
+ const outPath = resolve10(dir, `${componentName}-matrix.png`);
3482
4050
  writeFileSync5(outPath, sheet.png);
3483
4051
  const relPath = `${DEFAULT_OUTPUT_DIR}/${componentName}-matrix.png`;
3484
4052
  process.stdout.write(
@@ -3522,22 +4090,37 @@ function registerRenderAll(renderCmd) {
3522
4090
  return;
3523
4091
  }
3524
4092
  const concurrency = Math.max(1, parseInt(opts.concurrency, 10) || 4);
3525
- const outputDir = resolve9(process.cwd(), opts.outputDir);
4093
+ const outputDir = resolve10(process.cwd(), opts.outputDir);
3526
4094
  mkdirSync4(outputDir, { recursive: true });
3527
4095
  const rootDir = process.cwd();
3528
4096
  process.stderr.write(`Rendering ${total} components (concurrency: ${concurrency})\u2026
3529
4097
  `);
3530
4098
  const results = [];
4099
+ const complianceStylesMap = {};
3531
4100
  let completed = 0;
3532
4101
  const renderOne = async (name) => {
3533
4102
  const descriptor = manifest.components[name];
3534
4103
  if (descriptor === void 0) return;
3535
- const filePath = resolve9(rootDir, descriptor.filePath);
3536
- const renderer = buildRenderer(filePath, name, 375, 812);
4104
+ const filePath = resolve10(rootDir, descriptor.filePath);
4105
+ const allCssFiles = loadGlobalCssFilesFromConfig(process.cwd());
4106
+ const scopeData = await loadScopeFileForComponent(filePath);
4107
+ const scenarioEntries = scopeData !== null ? Object.entries(scopeData.scenarios) : [];
4108
+ const defaultEntry = scenarioEntries.find(([k]) => k === "default") ?? scenarioEntries[0];
4109
+ const renderProps = defaultEntry !== void 0 ? defaultEntry[1] : {};
4110
+ const wrapperScript = scopeData?.hasWrapper === true ? await buildWrapperScript(scopeData.filePath) : void 0;
4111
+ const renderer = buildRenderer(
4112
+ filePath,
4113
+ name,
4114
+ 375,
4115
+ 812,
4116
+ allCssFiles,
4117
+ process.cwd(),
4118
+ wrapperScript
4119
+ );
3537
4120
  const outcome = await safeRender2(
3538
- () => renderer.renderCell({}, descriptor.complexityClass),
4121
+ () => renderer.renderCell(renderProps, descriptor.complexityClass),
3539
4122
  {
3540
- props: {},
4123
+ props: renderProps,
3541
4124
  sourceLocation: {
3542
4125
  file: descriptor.filePath,
3543
4126
  line: descriptor.loc.start,
@@ -3555,7 +4138,7 @@ function registerRenderAll(renderCmd) {
3555
4138
  success: false,
3556
4139
  errorMessage: outcome.error.message
3557
4140
  });
3558
- const errPath = resolve9(outputDir, `${name}.error.json`);
4141
+ const errPath = resolve10(outputDir, `${name}.error.json`);
3559
4142
  writeFileSync5(
3560
4143
  errPath,
3561
4144
  JSON.stringify(
@@ -3573,10 +4156,81 @@ function registerRenderAll(renderCmd) {
3573
4156
  }
3574
4157
  const result = outcome.result;
3575
4158
  results.push({ name, renderTimeMs: result.renderTimeMs, success: true });
3576
- const pngPath = resolve9(outputDir, `${name}.png`);
4159
+ const pngPath = resolve10(outputDir, `${name}.png`);
3577
4160
  writeFileSync5(pngPath, result.screenshot);
3578
- const jsonPath = resolve9(outputDir, `${name}.json`);
4161
+ const jsonPath = resolve10(outputDir, `${name}.json`);
3579
4162
  writeFileSync5(jsonPath, JSON.stringify(formatRenderJson(name, {}, result), null, 2));
4163
+ const rawStyles = result.computedStyles["[data-reactscope-root] > *"] ?? {};
4164
+ const compStyles = {
4165
+ colors: {},
4166
+ spacing: {},
4167
+ typography: {},
4168
+ borders: {},
4169
+ shadows: {}
4170
+ };
4171
+ for (const [prop, val] of Object.entries(rawStyles)) {
4172
+ if (!val || val === "none" || val === "") continue;
4173
+ const lower = prop.toLowerCase();
4174
+ if (lower.includes("color") || lower.includes("background")) {
4175
+ compStyles.colors[prop] = val;
4176
+ } else if (lower.includes("padding") || lower.includes("margin") || lower.includes("gap") || lower.includes("width") || lower.includes("height")) {
4177
+ compStyles.spacing[prop] = val;
4178
+ } else if (lower.includes("font") || lower.includes("lineheight") || lower.includes("letterspacing") || lower.includes("texttransform")) {
4179
+ compStyles.typography[prop] = val;
4180
+ } else if (lower.includes("border") || lower.includes("radius") || lower.includes("outline")) {
4181
+ compStyles.borders[prop] = val;
4182
+ } else if (lower.includes("shadow")) {
4183
+ compStyles.shadows[prop] = val;
4184
+ }
4185
+ }
4186
+ complianceStylesMap[name] = compStyles;
4187
+ if (scopeData !== null && Object.keys(scopeData.scenarios).length >= 2) {
4188
+ try {
4189
+ const scenarioEntries2 = Object.entries(scopeData.scenarios);
4190
+ const scenarioAxis = {
4191
+ name: "scenario",
4192
+ values: scenarioEntries2.map(([k]) => k)
4193
+ };
4194
+ const scenarioPropsMap = Object.fromEntries(scenarioEntries2);
4195
+ const matrixRenderer = buildRenderer(
4196
+ filePath,
4197
+ name,
4198
+ 375,
4199
+ 812,
4200
+ allCssFiles,
4201
+ process.cwd(),
4202
+ wrapperScript
4203
+ );
4204
+ const wrappedRenderer = {
4205
+ _satori: matrixRenderer._satori,
4206
+ async renderCell(props, cc) {
4207
+ const scenarioName = props.scenario;
4208
+ const realProps = scenarioName !== void 0 ? scenarioPropsMap[scenarioName] ?? props : props;
4209
+ return matrixRenderer.renderCell(realProps, cc ?? "simple");
4210
+ }
4211
+ };
4212
+ const matrix = new RenderMatrix(wrappedRenderer, [scenarioAxis], {
4213
+ concurrency: 2
4214
+ });
4215
+ const matrixResult = await matrix.render();
4216
+ const matrixCells = matrixResult.cells.map((cell) => ({
4217
+ axisValues: [scenarioEntries2[cell.axisIndices[0] ?? 0]?.[0] ?? ""],
4218
+ screenshot: cell.result.screenshot.toString("base64"),
4219
+ width: cell.result.width,
4220
+ height: cell.result.height,
4221
+ renderTimeMs: cell.result.renderTimeMs
4222
+ }));
4223
+ const existingJson = JSON.parse(readFileSync7(jsonPath, "utf-8"));
4224
+ existingJson.cells = matrixCells;
4225
+ existingJson.axisLabels = [scenarioAxis.values];
4226
+ writeFileSync5(jsonPath, JSON.stringify(existingJson, null, 2));
4227
+ } catch (matrixErr) {
4228
+ process.stderr.write(
4229
+ ` [warn] Matrix render for ${name} failed: ${matrixErr instanceof Error ? matrixErr.message : String(matrixErr)}
4230
+ `
4231
+ );
4232
+ }
4233
+ }
3580
4234
  if (isTTY()) {
3581
4235
  process.stdout.write(
3582
4236
  `\u2713 ${name} \u2192 ${opts.outputDir}/${name}.png (${result.width}\xD7${result.height}, ${result.renderTimeMs.toFixed(0)}ms)
@@ -3600,6 +4254,14 @@ function registerRenderAll(renderCmd) {
3600
4254
  }
3601
4255
  await Promise.all(workers);
3602
4256
  await shutdownPool3();
4257
+ const compStylesPath = resolve10(
4258
+ resolve10(process.cwd(), opts.outputDir),
4259
+ "..",
4260
+ "compliance-styles.json"
4261
+ );
4262
+ writeFileSync5(compStylesPath, JSON.stringify(complianceStylesMap, null, 2));
4263
+ process.stderr.write(`[scope/render] \u2713 Wrote compliance-styles.json
4264
+ `);
3603
4265
  process.stderr.write("\n");
3604
4266
  const summary = formatSummaryText(results, outputDir);
3605
4267
  process.stderr.write(`${summary}
@@ -3638,7 +4300,7 @@ function resolveMatrixFormat(formatFlag, spriteAlreadyWritten) {
3638
4300
  return "json";
3639
4301
  }
3640
4302
  function createRenderCommand() {
3641
- const renderCmd = new Command6("render").description(
4303
+ const renderCmd = new Command7("render").description(
3642
4304
  "Render components to PNG or JSON via esbuild + BrowserPool"
3643
4305
  );
3644
4306
  registerRenderSingle(renderCmd);
@@ -3648,9 +4310,9 @@ function createRenderCommand() {
3648
4310
  }
3649
4311
 
3650
4312
  // src/report/baseline.ts
3651
- import { existsSync as existsSync7, mkdirSync as mkdirSync5, rmSync as rmSync2, writeFileSync as writeFileSync6 } from "fs";
3652
- import { resolve as resolve10 } from "path";
3653
- import { generateManifest as generateManifest3 } from "@agent-scope/manifest";
4313
+ import { existsSync as existsSync9, mkdirSync as mkdirSync5, rmSync as rmSync2, writeFileSync as writeFileSync6 } from "fs";
4314
+ import { resolve as resolve11 } from "path";
4315
+ import { generateManifest as generateManifest4 } from "@agent-scope/manifest";
3654
4316
  import { BrowserPool as BrowserPool4, safeRender as safeRender3 } from "@agent-scope/render";
3655
4317
  import { ComplianceEngine as ComplianceEngine2, TokenResolver as TokenResolver2 } from "@agent-scope/tokens";
3656
4318
  var DEFAULT_BASELINE_DIR = ".reactscope/baseline";
@@ -3798,30 +4460,30 @@ async function runBaseline(options = {}) {
3798
4460
  } = options;
3799
4461
  const startTime = performance.now();
3800
4462
  const rootDir = process.cwd();
3801
- const baselineDir = resolve10(rootDir, outputDir);
3802
- const rendersDir = resolve10(baselineDir, "renders");
3803
- if (existsSync7(baselineDir)) {
4463
+ const baselineDir = resolve11(rootDir, outputDir);
4464
+ const rendersDir = resolve11(baselineDir, "renders");
4465
+ if (existsSync9(baselineDir)) {
3804
4466
  rmSync2(baselineDir, { recursive: true, force: true });
3805
4467
  }
3806
4468
  mkdirSync5(rendersDir, { recursive: true });
3807
4469
  let manifest;
3808
4470
  if (manifestPath !== void 0) {
3809
- const { readFileSync: readFileSync12 } = await import("fs");
3810
- const absPath = resolve10(rootDir, manifestPath);
3811
- if (!existsSync7(absPath)) {
4471
+ const { readFileSync: readFileSync14 } = await import("fs");
4472
+ const absPath = resolve11(rootDir, manifestPath);
4473
+ if (!existsSync9(absPath)) {
3812
4474
  throw new Error(`Manifest not found at ${absPath}.`);
3813
4475
  }
3814
- manifest = JSON.parse(readFileSync12(absPath, "utf-8"));
4476
+ manifest = JSON.parse(readFileSync14(absPath, "utf-8"));
3815
4477
  process.stderr.write(`Loaded manifest from ${manifestPath}
3816
4478
  `);
3817
4479
  } else {
3818
4480
  process.stderr.write("Scanning for React components\u2026\n");
3819
- manifest = await generateManifest3({ rootDir });
4481
+ manifest = await generateManifest4({ rootDir });
3820
4482
  const count = Object.keys(manifest.components).length;
3821
4483
  process.stderr.write(`Found ${count} components.
3822
4484
  `);
3823
4485
  }
3824
- writeFileSync6(resolve10(baselineDir, "manifest.json"), JSON.stringify(manifest, null, 2), "utf-8");
4486
+ writeFileSync6(resolve11(baselineDir, "manifest.json"), JSON.stringify(manifest, null, 2), "utf-8");
3825
4487
  let componentNames = Object.keys(manifest.components);
3826
4488
  if (componentsGlob !== void 0) {
3827
4489
  componentNames = componentNames.filter((name) => matchGlob(componentsGlob, name));
@@ -3842,7 +4504,7 @@ async function runBaseline(options = {}) {
3842
4504
  auditedAt: (/* @__PURE__ */ new Date()).toISOString()
3843
4505
  };
3844
4506
  writeFileSync6(
3845
- resolve10(baselineDir, "compliance.json"),
4507
+ resolve11(baselineDir, "compliance.json"),
3846
4508
  JSON.stringify(emptyReport, null, 2),
3847
4509
  "utf-8"
3848
4510
  );
@@ -3863,7 +4525,7 @@ async function runBaseline(options = {}) {
3863
4525
  const renderOne = async (name) => {
3864
4526
  const descriptor = manifest.components[name];
3865
4527
  if (descriptor === void 0) return;
3866
- const filePath = resolve10(rootDir, descriptor.filePath);
4528
+ const filePath = resolve11(rootDir, descriptor.filePath);
3867
4529
  const outcome = await safeRender3(
3868
4530
  () => renderComponent2(filePath, name, {}, viewportWidth, viewportHeight),
3869
4531
  {
@@ -3882,7 +4544,7 @@ async function runBaseline(options = {}) {
3882
4544
  }
3883
4545
  if (outcome.crashed) {
3884
4546
  failureCount++;
3885
- const errPath = resolve10(rendersDir, `${name}.error.json`);
4547
+ const errPath = resolve11(rendersDir, `${name}.error.json`);
3886
4548
  writeFileSync6(
3887
4549
  errPath,
3888
4550
  JSON.stringify(
@@ -3900,10 +4562,10 @@ async function runBaseline(options = {}) {
3900
4562
  return;
3901
4563
  }
3902
4564
  const result = outcome.result;
3903
- writeFileSync6(resolve10(rendersDir, `${name}.png`), result.screenshot);
4565
+ writeFileSync6(resolve11(rendersDir, `${name}.png`), result.screenshot);
3904
4566
  const jsonOutput = formatRenderJson(name, {}, result);
3905
4567
  writeFileSync6(
3906
- resolve10(rendersDir, `${name}.json`),
4568
+ resolve11(rendersDir, `${name}.json`),
3907
4569
  JSON.stringify(jsonOutput, null, 2),
3908
4570
  "utf-8"
3909
4571
  );
@@ -3931,7 +4593,7 @@ async function runBaseline(options = {}) {
3931
4593
  const engine = new ComplianceEngine2(resolver);
3932
4594
  const batchReport = engine.auditBatch(computedStylesMap);
3933
4595
  writeFileSync6(
3934
- resolve10(baselineDir, "compliance.json"),
4596
+ resolve11(baselineDir, "compliance.json"),
3935
4597
  JSON.stringify(batchReport, null, 2),
3936
4598
  "utf-8"
3937
4599
  );
@@ -3974,22 +4636,22 @@ function registerBaselineSubCommand(reportCmd) {
3974
4636
  }
3975
4637
 
3976
4638
  // src/report/diff.ts
3977
- import { existsSync as existsSync8, readFileSync as readFileSync6, writeFileSync as writeFileSync7 } from "fs";
3978
- import { resolve as resolve11 } from "path";
3979
- import { generateManifest as generateManifest4 } from "@agent-scope/manifest";
4639
+ import { existsSync as existsSync10, readFileSync as readFileSync8, writeFileSync as writeFileSync7 } from "fs";
4640
+ import { resolve as resolve12 } from "path";
4641
+ import { generateManifest as generateManifest5 } from "@agent-scope/manifest";
3980
4642
  import { BrowserPool as BrowserPool5, safeRender as safeRender4 } from "@agent-scope/render";
3981
4643
  import { ComplianceEngine as ComplianceEngine3, TokenResolver as TokenResolver3 } from "@agent-scope/tokens";
3982
4644
  var DEFAULT_BASELINE_DIR2 = ".reactscope/baseline";
3983
4645
  function loadBaselineCompliance(baselineDir) {
3984
- const compliancePath = resolve11(baselineDir, "compliance.json");
3985
- if (!existsSync8(compliancePath)) return null;
3986
- const raw = JSON.parse(readFileSync6(compliancePath, "utf-8"));
4646
+ const compliancePath = resolve12(baselineDir, "compliance.json");
4647
+ if (!existsSync10(compliancePath)) return null;
4648
+ const raw = JSON.parse(readFileSync8(compliancePath, "utf-8"));
3987
4649
  return raw;
3988
4650
  }
3989
4651
  function loadBaselineRenderJson2(baselineDir, componentName) {
3990
- const jsonPath = resolve11(baselineDir, "renders", `${componentName}.json`);
3991
- if (!existsSync8(jsonPath)) return null;
3992
- return JSON.parse(readFileSync6(jsonPath, "utf-8"));
4652
+ const jsonPath = resolve12(baselineDir, "renders", `${componentName}.json`);
4653
+ if (!existsSync10(jsonPath)) return null;
4654
+ return JSON.parse(readFileSync8(jsonPath, "utf-8"));
3993
4655
  }
3994
4656
  var _pool5 = null;
3995
4657
  async function getPool5(viewportWidth, viewportHeight) {
@@ -4156,19 +4818,19 @@ async function runDiff(options = {}) {
4156
4818
  } = options;
4157
4819
  const startTime = performance.now();
4158
4820
  const rootDir = process.cwd();
4159
- const baselineDir = resolve11(rootDir, baselineDirRaw);
4160
- if (!existsSync8(baselineDir)) {
4821
+ const baselineDir = resolve12(rootDir, baselineDirRaw);
4822
+ if (!existsSync10(baselineDir)) {
4161
4823
  throw new Error(
4162
4824
  `Baseline directory not found at "${baselineDir}". Run \`scope report baseline\` first to create a baseline snapshot.`
4163
4825
  );
4164
4826
  }
4165
- const baselineManifestPath = resolve11(baselineDir, "manifest.json");
4166
- if (!existsSync8(baselineManifestPath)) {
4827
+ const baselineManifestPath = resolve12(baselineDir, "manifest.json");
4828
+ if (!existsSync10(baselineManifestPath)) {
4167
4829
  throw new Error(
4168
4830
  `Baseline manifest.json not found at "${baselineManifestPath}". The baseline directory may be incomplete \u2014 re-run \`scope report baseline\`.`
4169
4831
  );
4170
4832
  }
4171
- const baselineManifest = JSON.parse(readFileSync6(baselineManifestPath, "utf-8"));
4833
+ const baselineManifest = JSON.parse(readFileSync8(baselineManifestPath, "utf-8"));
4172
4834
  const baselineCompliance = loadBaselineCompliance(baselineDir);
4173
4835
  const baselineComponentNames = new Set(Object.keys(baselineManifest.components));
4174
4836
  process.stderr.write(
@@ -4177,16 +4839,16 @@ async function runDiff(options = {}) {
4177
4839
  );
4178
4840
  let currentManifest;
4179
4841
  if (manifestPath !== void 0) {
4180
- const absPath = resolve11(rootDir, manifestPath);
4181
- if (!existsSync8(absPath)) {
4842
+ const absPath = resolve12(rootDir, manifestPath);
4843
+ if (!existsSync10(absPath)) {
4182
4844
  throw new Error(`Manifest not found at "${absPath}".`);
4183
4845
  }
4184
- currentManifest = JSON.parse(readFileSync6(absPath, "utf-8"));
4846
+ currentManifest = JSON.parse(readFileSync8(absPath, "utf-8"));
4185
4847
  process.stderr.write(`Loaded manifest from ${manifestPath}
4186
4848
  `);
4187
4849
  } else {
4188
4850
  process.stderr.write("Scanning for React components\u2026\n");
4189
- currentManifest = await generateManifest4({ rootDir });
4851
+ currentManifest = await generateManifest5({ rootDir });
4190
4852
  const count = Object.keys(currentManifest.components).length;
4191
4853
  process.stderr.write(`Found ${count} components.
4192
4854
  `);
@@ -4214,7 +4876,7 @@ async function runDiff(options = {}) {
4214
4876
  const renderOne = async (name) => {
4215
4877
  const descriptor = currentManifest.components[name];
4216
4878
  if (descriptor === void 0) return;
4217
- const filePath = resolve11(rootDir, descriptor.filePath);
4879
+ const filePath = resolve12(rootDir, descriptor.filePath);
4218
4880
  const outcome = await safeRender4(
4219
4881
  () => renderComponent3(filePath, name, {}, viewportWidth, viewportHeight),
4220
4882
  {
@@ -4454,8 +5116,8 @@ function registerDiffSubCommand(reportCmd) {
4454
5116
  }
4455
5117
 
4456
5118
  // src/report/pr-comment.ts
4457
- import { existsSync as existsSync9, readFileSync as readFileSync7, writeFileSync as writeFileSync8 } from "fs";
4458
- import { resolve as resolve12 } from "path";
5119
+ import { existsSync as existsSync11, readFileSync as readFileSync9, writeFileSync as writeFileSync8 } from "fs";
5120
+ import { resolve as resolve13 } from "path";
4459
5121
  var STATUS_BADGE = {
4460
5122
  added: "\u2705 added",
4461
5123
  removed: "\u{1F5D1}\uFE0F removed",
@@ -4538,13 +5200,13 @@ function formatPrComment(diff) {
4538
5200
  return lines.join("\n");
4539
5201
  }
4540
5202
  function loadDiffResult(filePath) {
4541
- const abs = resolve12(filePath);
4542
- if (!existsSync9(abs)) {
5203
+ const abs = resolve13(filePath);
5204
+ if (!existsSync11(abs)) {
4543
5205
  throw new Error(`DiffResult file not found: ${abs}`);
4544
5206
  }
4545
5207
  let raw;
4546
5208
  try {
4547
- raw = readFileSync7(abs, "utf-8");
5209
+ raw = readFileSync9(abs, "utf-8");
4548
5210
  } catch (err) {
4549
5211
  throw new Error(
4550
5212
  `Failed to read DiffResult file: ${err instanceof Error ? err.message : String(err)}`
@@ -4571,7 +5233,7 @@ function registerPrCommentSubCommand(reportCmd) {
4571
5233
  const diff = loadDiffResult(opts.input);
4572
5234
  const comment = formatPrComment(diff);
4573
5235
  if (opts.output !== void 0) {
4574
- writeFileSync8(resolve12(opts.output), comment, "utf-8");
5236
+ writeFileSync8(resolve13(opts.output), comment, "utf-8");
4575
5237
  process.stderr.write(`PR comment written to ${opts.output}
4576
5238
  `);
4577
5239
  } else {
@@ -4866,11 +5528,11 @@ function buildStructuredReport(report) {
4866
5528
  }
4867
5529
 
4868
5530
  // src/site-commands.ts
4869
- import { createReadStream, existsSync as existsSync10, statSync } from "fs";
5531
+ import { createReadStream, existsSync as existsSync12, statSync as statSync2 } from "fs";
4870
5532
  import { createServer } from "http";
4871
- import { extname, join as join4, resolve as resolve13 } from "path";
5533
+ import { extname, join as join5, resolve as resolve14 } from "path";
4872
5534
  import { buildSite } from "@agent-scope/site";
4873
- import { Command as Command7 } from "commander";
5535
+ import { Command as Command8 } from "commander";
4874
5536
  var MIME_TYPES = {
4875
5537
  ".html": "text/html; charset=utf-8",
4876
5538
  ".css": "text/css; charset=utf-8",
@@ -4886,16 +5548,16 @@ function registerBuild(siteCmd) {
4886
5548
  siteCmd.command("build").description("Build a static HTML gallery from .reactscope/ output").option("-i, --input <path>", "Path to .reactscope input directory", ".reactscope").option("-o, --output <path>", "Output directory for generated site", ".reactscope/site").option("--base-path <path>", "Base URL path prefix for subdirectory deployment", "/").option("--compliance <path>", "Path to compliance batch report JSON").option("--title <text>", "Site title", "Scope \u2014 Component Gallery").action(
4887
5549
  async (opts) => {
4888
5550
  try {
4889
- const inputDir = resolve13(process.cwd(), opts.input);
4890
- const outputDir = resolve13(process.cwd(), opts.output);
4891
- if (!existsSync10(inputDir)) {
5551
+ const inputDir = resolve14(process.cwd(), opts.input);
5552
+ const outputDir = resolve14(process.cwd(), opts.output);
5553
+ if (!existsSync12(inputDir)) {
4892
5554
  throw new Error(
4893
5555
  `Input directory not found: ${inputDir}
4894
5556
  Run \`scope manifest generate\` and \`scope render\` first.`
4895
5557
  );
4896
5558
  }
4897
- const manifestPath = join4(inputDir, "manifest.json");
4898
- if (!existsSync10(manifestPath)) {
5559
+ const manifestPath = join5(inputDir, "manifest.json");
5560
+ if (!existsSync12(manifestPath)) {
4899
5561
  throw new Error(
4900
5562
  `Manifest not found at ${manifestPath}
4901
5563
  Run \`scope manifest generate\` first.`
@@ -4908,7 +5570,7 @@ Run \`scope manifest generate\` first.`
4908
5570
  outputDir,
4909
5571
  basePath: opts.basePath,
4910
5572
  ...opts.compliance !== void 0 && {
4911
- compliancePath: resolve13(process.cwd(), opts.compliance)
5573
+ compliancePath: resolve14(process.cwd(), opts.compliance)
4912
5574
  },
4913
5575
  title: opts.title
4914
5576
  });
@@ -4931,8 +5593,8 @@ function registerServe(siteCmd) {
4931
5593
  if (Number.isNaN(port) || port < 1 || port > 65535) {
4932
5594
  throw new Error(`Invalid port: ${opts.port}`);
4933
5595
  }
4934
- const serveDir = resolve13(process.cwd(), opts.dir);
4935
- if (!existsSync10(serveDir)) {
5596
+ const serveDir = resolve14(process.cwd(), opts.dir);
5597
+ if (!existsSync12(serveDir)) {
4936
5598
  throw new Error(
4937
5599
  `Serve directory not found: ${serveDir}
4938
5600
  Run \`scope site build\` first.`
@@ -4941,13 +5603,13 @@ Run \`scope site build\` first.`
4941
5603
  const server = createServer((req, res) => {
4942
5604
  const rawUrl = req.url ?? "/";
4943
5605
  const urlPath = decodeURIComponent(rawUrl.split("?")[0] ?? "/");
4944
- const filePath = join4(serveDir, urlPath.endsWith("/") ? `${urlPath}index.html` : urlPath);
5606
+ const filePath = join5(serveDir, urlPath.endsWith("/") ? `${urlPath}index.html` : urlPath);
4945
5607
  if (!filePath.startsWith(serveDir)) {
4946
5608
  res.writeHead(403, { "Content-Type": "text/plain" });
4947
5609
  res.end("Forbidden");
4948
5610
  return;
4949
5611
  }
4950
- if (existsSync10(filePath) && statSync(filePath).isFile()) {
5612
+ if (existsSync12(filePath) && statSync2(filePath).isFile()) {
4951
5613
  const ext = extname(filePath).toLowerCase();
4952
5614
  const contentType = MIME_TYPES[ext] ?? "application/octet-stream";
4953
5615
  res.writeHead(200, { "Content-Type": contentType });
@@ -4955,7 +5617,7 @@ Run \`scope site build\` first.`
4955
5617
  return;
4956
5618
  }
4957
5619
  const htmlPath = `${filePath}.html`;
4958
- if (existsSync10(htmlPath) && statSync(htmlPath).isFile()) {
5620
+ if (existsSync12(htmlPath) && statSync2(htmlPath).isFile()) {
4959
5621
  res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
4960
5622
  createReadStream(htmlPath).pipe(res);
4961
5623
  return;
@@ -4988,7 +5650,7 @@ Run \`scope site build\` first.`
4988
5650
  });
4989
5651
  }
4990
5652
  function createSiteCommand() {
4991
- const siteCmd = new Command7("site").description(
5653
+ const siteCmd = new Command8("site").description(
4992
5654
  "Build and serve the static component gallery site"
4993
5655
  );
4994
5656
  registerBuild(siteCmd);
@@ -4997,8 +5659,8 @@ function createSiteCommand() {
4997
5659
  }
4998
5660
 
4999
5661
  // src/tokens/commands.ts
5000
- import { existsSync as existsSync13, readFileSync as readFileSync10 } from "fs";
5001
- import { resolve as resolve17 } from "path";
5662
+ import { existsSync as existsSync15, readFileSync as readFileSync12 } from "fs";
5663
+ import { resolve as resolve18 } from "path";
5002
5664
  import {
5003
5665
  parseTokenFileSync as parseTokenFileSync2,
5004
5666
  TokenParseError,
@@ -5006,26 +5668,26 @@ import {
5006
5668
  TokenValidationError,
5007
5669
  validateTokenFile
5008
5670
  } from "@agent-scope/tokens";
5009
- import { Command as Command9 } from "commander";
5671
+ import { Command as Command10 } from "commander";
5010
5672
 
5011
5673
  // src/tokens/compliance.ts
5012
- import { existsSync as existsSync11, readFileSync as readFileSync8 } from "fs";
5013
- import { resolve as resolve14 } from "path";
5674
+ import { existsSync as existsSync13, readFileSync as readFileSync10 } from "fs";
5675
+ import { resolve as resolve15 } from "path";
5014
5676
  import {
5015
5677
  ComplianceEngine as ComplianceEngine4,
5016
5678
  TokenResolver as TokenResolver4
5017
5679
  } from "@agent-scope/tokens";
5018
5680
  var DEFAULT_STYLES_PATH = ".reactscope/compliance-styles.json";
5019
5681
  function loadStylesFile(stylesPath) {
5020
- const absPath = resolve14(process.cwd(), stylesPath);
5021
- if (!existsSync11(absPath)) {
5682
+ const absPath = resolve15(process.cwd(), stylesPath);
5683
+ if (!existsSync13(absPath)) {
5022
5684
  throw new Error(
5023
5685
  `Compliance styles file not found at ${absPath}.
5024
5686
  Run \`scope render all\` first to generate component styles, or use --styles to specify a path.
5025
5687
  Expected format: { "ComponentName": { colors: {}, spacing: {}, typography: {}, borders: {}, shadows: {} } }`
5026
5688
  );
5027
5689
  }
5028
- const raw = readFileSync8(absPath, "utf-8");
5690
+ const raw = readFileSync10(absPath, "utf-8");
5029
5691
  let parsed;
5030
5692
  try {
5031
5693
  parsed = JSON.parse(raw);
@@ -5185,60 +5847,70 @@ function registerCompliance(tokensCmd) {
5185
5847
  }
5186
5848
 
5187
5849
  // src/tokens/export.ts
5188
- import { existsSync as existsSync12, readFileSync as readFileSync9, writeFileSync as writeFileSync9 } from "fs";
5189
- import { resolve as resolve15 } from "path";
5850
+ import { existsSync as existsSync14, readFileSync as readFileSync11, writeFileSync as writeFileSync9 } from "fs";
5851
+ import { resolve as resolve16 } from "path";
5190
5852
  import {
5191
5853
  exportTokens,
5192
5854
  parseTokenFileSync,
5193
5855
  ThemeResolver,
5194
5856
  TokenResolver as TokenResolver5
5195
5857
  } from "@agent-scope/tokens";
5196
- import { Command as Command8 } from "commander";
5858
+ import { Command as Command9 } from "commander";
5197
5859
  var DEFAULT_TOKEN_FILE = "reactscope.tokens.json";
5198
5860
  var CONFIG_FILE = "reactscope.config.json";
5199
5861
  var SUPPORTED_FORMATS = ["css", "ts", "scss", "tailwind", "flat-json", "figma"];
5200
5862
  function resolveTokenFilePath2(fileFlag) {
5201
5863
  if (fileFlag !== void 0) {
5202
- return resolve15(process.cwd(), fileFlag);
5864
+ return resolve16(process.cwd(), fileFlag);
5203
5865
  }
5204
- const configPath = resolve15(process.cwd(), CONFIG_FILE);
5205
- if (existsSync12(configPath)) {
5866
+ const configPath = resolve16(process.cwd(), CONFIG_FILE);
5867
+ if (existsSync14(configPath)) {
5206
5868
  try {
5207
- const raw = readFileSync9(configPath, "utf-8");
5869
+ const raw = readFileSync11(configPath, "utf-8");
5208
5870
  const config = JSON.parse(raw);
5209
5871
  if (typeof config === "object" && config !== null && "tokens" in config && typeof config.tokens === "object" && config.tokens !== null && typeof config.tokens?.file === "string") {
5210
5872
  const file = config.tokens.file;
5211
- return resolve15(process.cwd(), file);
5873
+ return resolve16(process.cwd(), file);
5212
5874
  }
5213
5875
  } catch {
5214
5876
  }
5215
5877
  }
5216
- return resolve15(process.cwd(), DEFAULT_TOKEN_FILE);
5878
+ return resolve16(process.cwd(), DEFAULT_TOKEN_FILE);
5217
5879
  }
5218
5880
  function createTokensExportCommand() {
5219
- return new Command8("export").description("Export design tokens to a downstream format").requiredOption("--format <fmt>", `Output format: ${SUPPORTED_FORMATS.join(", ")}`).option("--file <path>", "Path to token file (overrides config)").option("--out <path>", "Write output to file instead of stdout").option("--prefix <prefix>", "CSS/SCSS: prefix for variable names (e.g. 'scope')").option("--selector <selector>", "CSS: custom root selector (default: ':root')").option(
5881
+ return new Command9("export").description("Export design tokens to a downstream format").requiredOption("--format <fmt>", `Output format: ${SUPPORTED_FORMATS.join(", ")}`).option("--file <path>", "Path to token file (overrides config)").option("--out <path>", "Write output to file instead of stdout").option("--prefix <prefix>", "CSS/SCSS: prefix for variable names (e.g. 'scope')").option("--selector <selector>", "CSS: custom root selector (default: ':root')").option(
5220
5882
  "--theme <name>",
5221
5883
  "Include theme overrides for the named theme (applies to css, ts, scss, tailwind, figma)"
5222
5884
  ).action(
5223
5885
  (opts) => {
5224
5886
  if (!SUPPORTED_FORMATS.includes(opts.format)) {
5887
+ const FORMAT_ALIASES = {
5888
+ json: "flat-json",
5889
+ "json-flat": "flat-json",
5890
+ javascript: "ts",
5891
+ js: "ts",
5892
+ sass: "scss",
5893
+ tw: "tailwind"
5894
+ };
5895
+ const hint = FORMAT_ALIASES[opts.format.toLowerCase()];
5225
5896
  process.stderr.write(
5226
5897
  `Error: unsupported format "${opts.format}".
5227
5898
  Supported formats: ${SUPPORTED_FORMATS.join(", ")}
5228
- `
5899
+ ` + (hint ? `Did you mean "${hint}"?
5900
+ ` : "")
5229
5901
  );
5230
5902
  process.exit(1);
5231
5903
  }
5232
5904
  const format = opts.format;
5233
5905
  try {
5234
5906
  const filePath = resolveTokenFilePath2(opts.file);
5235
- if (!existsSync12(filePath)) {
5907
+ if (!existsSync14(filePath)) {
5236
5908
  throw new Error(
5237
5909
  `Token file not found at ${filePath}.
5238
5910
  Create a reactscope.tokens.json file or use --file to specify a path.`
5239
5911
  );
5240
5912
  }
5241
- const raw = readFileSync9(filePath, "utf-8");
5913
+ const raw = readFileSync11(filePath, "utf-8");
5242
5914
  const { tokens, rawFile } = parseTokenFileSync(raw);
5243
5915
  let themesMap;
5244
5916
  if (opts.theme !== void 0) {
@@ -5277,7 +5949,7 @@ Available themes: ${themeNames.join(", ")}`
5277
5949
  themes: themesMap
5278
5950
  });
5279
5951
  if (opts.out !== void 0) {
5280
- const outPath = resolve15(process.cwd(), opts.out);
5952
+ const outPath = resolve16(process.cwd(), opts.out);
5281
5953
  writeFileSync9(outPath, output, "utf-8");
5282
5954
  process.stderr.write(`Exported ${tokens.length} tokens to ${outPath}
5283
5955
  `);
@@ -5393,7 +6065,7 @@ ${formatImpactSummary(report)}
5393
6065
 
5394
6066
  // src/tokens/preview.ts
5395
6067
  import { mkdirSync as mkdirSync6, writeFileSync as writeFileSync10 } from "fs";
5396
- import { resolve as resolve16 } from "path";
6068
+ import { resolve as resolve17 } from "path";
5397
6069
  import { BrowserPool as BrowserPool6, SpriteSheetGenerator } from "@agent-scope/render";
5398
6070
  import { ComplianceEngine as ComplianceEngine6, ImpactAnalyzer as ImpactAnalyzer2, TokenResolver as TokenResolver7 } from "@agent-scope/tokens";
5399
6071
  var DEFAULT_STYLES_PATH3 = ".reactscope/compliance-styles.json";
@@ -5574,8 +6246,8 @@ function registerPreview(tokensCmd) {
5574
6246
  });
5575
6247
  const spriteResult = await generator.generate(matrixResult);
5576
6248
  const tokenLabel = tokenPath.replace(/\./g, "-");
5577
- const outputPath = opts.output ?? resolve16(process.cwd(), DEFAULT_OUTPUT_DIR2, `preview-${tokenLabel}.png`);
5578
- const outputDir = resolve16(outputPath, "..");
6249
+ const outputPath = opts.output ?? resolve17(process.cwd(), DEFAULT_OUTPUT_DIR2, `preview-${tokenLabel}.png`);
6250
+ const outputDir = resolve17(outputPath, "..");
5579
6251
  mkdirSync6(outputDir, { recursive: true });
5580
6252
  writeFileSync10(outputPath, spriteResult.png);
5581
6253
  const useJson = opts.format === "json" || opts.format !== "text" && !isTTY();
@@ -5636,30 +6308,30 @@ function buildTable2(headers, rows) {
5636
6308
  }
5637
6309
  function resolveTokenFilePath(fileFlag) {
5638
6310
  if (fileFlag !== void 0) {
5639
- return resolve17(process.cwd(), fileFlag);
6311
+ return resolve18(process.cwd(), fileFlag);
5640
6312
  }
5641
- const configPath = resolve17(process.cwd(), CONFIG_FILE2);
5642
- if (existsSync13(configPath)) {
6313
+ const configPath = resolve18(process.cwd(), CONFIG_FILE2);
6314
+ if (existsSync15(configPath)) {
5643
6315
  try {
5644
- const raw = readFileSync10(configPath, "utf-8");
6316
+ const raw = readFileSync12(configPath, "utf-8");
5645
6317
  const config = JSON.parse(raw);
5646
6318
  if (typeof config === "object" && config !== null && "tokens" in config && typeof config.tokens === "object" && config.tokens !== null && typeof config.tokens?.file === "string") {
5647
6319
  const file = config.tokens.file;
5648
- return resolve17(process.cwd(), file);
6320
+ return resolve18(process.cwd(), file);
5649
6321
  }
5650
6322
  } catch {
5651
6323
  }
5652
6324
  }
5653
- return resolve17(process.cwd(), DEFAULT_TOKEN_FILE2);
6325
+ return resolve18(process.cwd(), DEFAULT_TOKEN_FILE2);
5654
6326
  }
5655
6327
  function loadTokens(absPath) {
5656
- if (!existsSync13(absPath)) {
6328
+ if (!existsSync15(absPath)) {
5657
6329
  throw new Error(
5658
6330
  `Token file not found at ${absPath}.
5659
6331
  Create a reactscope.tokens.json file or use --file to specify a path.`
5660
6332
  );
5661
6333
  }
5662
- const raw = readFileSync10(absPath, "utf-8");
6334
+ const raw = readFileSync12(absPath, "utf-8");
5663
6335
  return parseTokenFileSync2(raw);
5664
6336
  }
5665
6337
  function getRawValue(node, segments) {
@@ -5873,13 +6545,13 @@ function registerValidate(tokensCmd) {
5873
6545
  ).option("--file <path>", "Path to token file (overrides config)").option("--format <fmt>", "Output format: json or text (default: auto-detect)").action((opts) => {
5874
6546
  try {
5875
6547
  const filePath = resolveTokenFilePath(opts.file);
5876
- if (!existsSync13(filePath)) {
6548
+ if (!existsSync15(filePath)) {
5877
6549
  throw new Error(
5878
6550
  `Token file not found at ${filePath}.
5879
6551
  Create a reactscope.tokens.json file or use --file to specify a path.`
5880
6552
  );
5881
6553
  }
5882
- const raw = readFileSync10(filePath, "utf-8");
6554
+ const raw = readFileSync12(filePath, "utf-8");
5883
6555
  const useJson = opts.format === "json" || opts.format !== "text" && !isTTY2();
5884
6556
  const errors = [];
5885
6557
  let parsed;
@@ -5947,7 +6619,7 @@ function outputValidationResult(filePath, errors, useJson) {
5947
6619
  }
5948
6620
  }
5949
6621
  function createTokensCommand() {
5950
- const tokensCmd = new Command9("tokens").description(
6622
+ const tokensCmd = new Command10("tokens").description(
5951
6623
  "Query and validate design tokens from a reactscope.tokens.json file"
5952
6624
  );
5953
6625
  registerGet2(tokensCmd);
@@ -5964,7 +6636,7 @@ function createTokensCommand() {
5964
6636
 
5965
6637
  // src/program.ts
5966
6638
  function createProgram(options = {}) {
5967
- const program2 = new Command10("scope").version(options.version ?? "0.1.0").description("Scope \u2014 React instrumentation toolkit");
6639
+ const program2 = new Command11("scope").version(options.version ?? "0.1.0").description("Scope \u2014 React instrumentation toolkit");
5968
6640
  program2.command("capture <url>").description("Capture a React component tree from a live URL and output as JSON").option("-o, --output <path>", "Write JSON to file instead of stdout").option("--pretty", "Pretty-print JSON output (default: minified)", false).option("--timeout <ms>", "Max wait time for React to mount (ms)", "10000").option("--wait <ms>", "Additional wait after page load before capture (ms)", "0").action(
5969
6641
  async (url, opts) => {
5970
6642
  try {
@@ -6037,7 +6709,7 @@ function createProgram(options = {}) {
6037
6709
  }
6038
6710
  );
6039
6711
  program2.command("generate").description("Generate a Playwright test from a Scope trace file").argument("<trace>", "Path to a serialized Scope trace (.json)").option("-o, --output <path>", "Output file path", "scope.spec.ts").option("-d, --description <text>", "Test description").action((tracePath, opts) => {
6040
- const raw = readFileSync11(tracePath, "utf-8");
6712
+ const raw = readFileSync13(tracePath, "utf-8");
6041
6713
  const trace = loadTrace(raw);
6042
6714
  const source = generateTest(trace, {
6043
6715
  description: opts.description,
@@ -6052,6 +6724,7 @@ function createProgram(options = {}) {
6052
6724
  program2.addCommand(createInstrumentCommand());
6053
6725
  program2.addCommand(createInitCommand());
6054
6726
  program2.addCommand(createCiCommand());
6727
+ program2.addCommand(createDoctorCommand());
6055
6728
  const existingReportCmd = program2.commands.find((c) => c.name() === "report");
6056
6729
  if (existingReportCmd !== void 0) {
6057
6730
  registerBaselineSubCommand(existingReportCmd);