@agent-scope/cli 1.20.0 → 1.20.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.js CHANGED
@@ -1,7 +1,13 @@
1
1
  #!/usr/bin/env bun
2
+ var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require : typeof Proxy !== "undefined" ? new Proxy(x, {
3
+ get: (a, b) => (typeof require !== "undefined" ? require : a)[b]
4
+ }) : x)(function(x) {
5
+ if (typeof require !== "undefined") return require.apply(this, arguments);
6
+ throw Error('Dynamic require of "' + x + '" is not supported');
7
+ });
2
8
 
3
9
  // src/program.ts
4
- import { readFileSync as readFileSync13 } from "fs";
10
+ import { readFileSync as readFileSync17 } from "fs";
5
11
  import { generateTest, loadTrace } from "@agent-scope/playwright";
6
12
  import { Command as Command12 } from "commander";
7
13
 
@@ -61,9 +67,9 @@ import { Command } from "commander";
61
67
  // src/component-bundler.ts
62
68
  import { dirname } from "path";
63
69
  import * as esbuild from "esbuild";
64
- async function buildComponentHarness(filePath, componentName, props, viewportWidth, projectCss, wrapperScript) {
70
+ async function buildComponentHarness(filePath, componentName, props, viewportWidth, projectCss, wrapperScript, screenshotPadding = 0) {
65
71
  const bundledScript = await bundleComponentToIIFE(filePath, componentName, props);
66
- return wrapInHtml(bundledScript, viewportWidth, projectCss, wrapperScript);
72
+ return wrapInHtml(bundledScript, viewportWidth, projectCss, wrapperScript, screenshotPadding);
67
73
  }
68
74
  async function bundleComponentToIIFE(filePath, componentName, props) {
69
75
  const propsJson = JSON.stringify(props).replace(/<\/script>/gi, "<\\/script>");
@@ -169,7 +175,7 @@ ${msg}`);
169
175
  }
170
176
  return outputFile.text;
171
177
  }
172
- function wrapInHtml(bundledScript, viewportWidth, projectCss, wrapperScript) {
178
+ function wrapInHtml(bundledScript, viewportWidth, projectCss, wrapperScript, screenshotPadding = 0) {
173
179
  const projectStyleBlock = projectCss != null && projectCss.length > 0 ? `<style id="scope-project-css">
174
180
  ${projectCss.replace(/<\/style>/gi, "<\\/style>")}
175
181
  </style>` : "";
@@ -179,10 +185,17 @@ ${projectCss.replace(/<\/style>/gi, "<\\/style>")}
179
185
  <head>
180
186
  <meta charset="UTF-8" />
181
187
  <meta name="viewport" content="width=${viewportWidth}, initial-scale=1.0" />
188
+ <script>
189
+ // Reset globals that persist on window across page.setContent() calls
190
+ // (document.open/write/close clears the DOM but NOT the JS global scope)
191
+ window.__SCOPE_WRAPPER__ = null;
192
+ window.__SCOPE_RENDER_COMPLETE__ = false;
193
+ window.__SCOPE_RENDER_ERROR__ = null;
194
+ </script>
182
195
  <style>
183
196
  *, *::before, *::after { box-sizing: border-box; }
184
197
  html, body { margin: 0; padding: 0; background: #fff; font-family: system-ui, sans-serif; }
185
- #scope-root { display: inline-block; min-width: 1px; min-height: 1px; }
198
+ #scope-root { display: inline-block; min-width: 1px; min-height: 1px; margin: ${screenshotPadding}px; }
186
199
  </style>
187
200
  ${projectStyleBlock}
188
201
  </head>
@@ -479,6 +492,11 @@ var STYLE_ENTRY_CANDIDATES = [
479
492
  "index.css"
480
493
  ];
481
494
  var TAILWIND_IMPORT = /@import\s+["']tailwindcss["']\s*;?/;
495
+ function getElementClassNames(el) {
496
+ const className = el.className;
497
+ const raw = typeof className === "string" ? className : typeof className?.baseVal === "string" ? className.baseVal : el.getAttribute("class") ?? "";
498
+ return raw.split(/\s+/).filter(Boolean);
499
+ }
482
500
  var compilerCache = null;
483
501
  function getCachedBuild(cwd) {
484
502
  if (compilerCache !== null && resolve(compilerCache.cwd) === resolve(cwd)) {
@@ -569,22 +587,22 @@ async function getTailwindCompiler(cwd) {
569
587
  from: entryPath,
570
588
  loadStylesheet
571
589
  });
572
- const build3 = result.build.bind(result);
573
- compilerCache = { cwd, build: build3 };
574
- return build3;
590
+ const build4 = result.build.bind(result);
591
+ compilerCache = { cwd, build: build4 };
592
+ return build4;
575
593
  }
576
594
  async function getCompiledCssForClasses(cwd, classes) {
577
- const build3 = await getTailwindCompiler(cwd);
578
- if (build3 === null) return null;
595
+ const build4 = await getTailwindCompiler(cwd);
596
+ if (build4 === null) return null;
579
597
  const deduped = [...new Set(classes)].filter(Boolean);
580
598
  if (deduped.length === 0) return null;
581
- return build3(deduped);
599
+ return build4(deduped);
582
600
  }
583
601
  async function compileGlobalCssFile(cssFilePath, cwd) {
584
- const { existsSync: existsSync16, readFileSync: readFileSync14 } = await import("fs");
602
+ const { existsSync: existsSync18, readFileSync: readFileSync18 } = await import("fs");
585
603
  const { createRequire: createRequire3 } = await import("module");
586
- if (!existsSync16(cssFilePath)) return null;
587
- const raw = readFileSync14(cssFilePath, "utf-8");
604
+ if (!existsSync18(cssFilePath)) return null;
605
+ const raw = readFileSync18(cssFilePath, "utf-8");
588
606
  const needsCompile = /@tailwind|@import\s+['"]tailwindcss/.test(raw);
589
607
  if (!needsCompile) {
590
608
  return raw;
@@ -667,8 +685,17 @@ async function shutdownPool() {
667
685
  }
668
686
  }
669
687
  async function renderComponent(filePath, componentName, props, viewportWidth, viewportHeight) {
688
+ const PAD = 24;
670
689
  const pool = await getPool(viewportWidth, viewportHeight);
671
- const htmlHarness = await buildComponentHarness(filePath, componentName, props, viewportWidth);
690
+ const htmlHarness = await buildComponentHarness(
691
+ filePath,
692
+ componentName,
693
+ props,
694
+ viewportWidth,
695
+ void 0,
696
+ void 0,
697
+ PAD
698
+ );
672
699
  const slot = await pool.acquire();
673
700
  const { page } = slot;
674
701
  try {
@@ -690,8 +717,8 @@ async function renderComponent(filePath, componentName, props, viewportWidth, vi
690
717
  const classes = await page.evaluate(() => {
691
718
  const set = /* @__PURE__ */ new Set();
692
719
  document.querySelectorAll("[class]").forEach((el) => {
693
- for (const c of el.className.split(/\s+/)) {
694
- if (c) set.add(c);
720
+ for (const c of getElementClassNames(el)) {
721
+ set.add(c);
695
722
  }
696
723
  });
697
724
  return [...set];
@@ -708,7 +735,6 @@ async function renderComponent(filePath, componentName, props, viewportWidth, vi
708
735
  `Component "${componentName}" rendered with zero bounding box \u2014 it may be invisible or not mounted`
709
736
  );
710
737
  }
711
- const PAD = 24;
712
738
  const MIN_W = 320;
713
739
  const MIN_H = 200;
714
740
  const clipX = Math.max(0, boundingBox.x - PAD);
@@ -1055,14 +1081,137 @@ function createCiCommand() {
1055
1081
  }
1056
1082
 
1057
1083
  // src/doctor-commands.ts
1058
- import { existsSync as existsSync3, readdirSync, readFileSync as readFileSync3, statSync } from "fs";
1059
- import { join, resolve as resolve3 } from "path";
1084
+ import { existsSync as existsSync4, readdirSync, readFileSync as readFileSync4, statSync } from "fs";
1085
+ import { join as join2, resolve as resolve3 } from "path";
1060
1086
  import { Command as Command2 } from "commander";
1087
+
1088
+ // src/diagnostics.ts
1089
+ import { constants, existsSync as existsSync3, readFileSync as readFileSync3 } from "fs";
1090
+ import { access } from "fs/promises";
1091
+ import { dirname as dirname2, join } from "path";
1092
+ var PLAYWRIGHT_BROWSER_HINTS = [
1093
+ "executable doesn't exist",
1094
+ "browserType.launch",
1095
+ "looks like playwright was just installed or updated",
1096
+ "please run the following command to download new browsers",
1097
+ "could not find chromium"
1098
+ ];
1099
+ var MISSING_DEPENDENCY_HINTS = ["could not resolve", "cannot find module", "module not found"];
1100
+ var REQUIRED_HARNESS_DEPENDENCIES = ["react", "react-dom", "react/jsx-runtime"];
1101
+ function getEffectivePlaywrightBrowsersPath() {
1102
+ const value = process.env.PLAYWRIGHT_BROWSERS_PATH;
1103
+ return typeof value === "string" && value.length > 0 ? value : null;
1104
+ }
1105
+ function getPlaywrightBrowserRemediation(status) {
1106
+ const effectivePath = status?.effectiveBrowserPath ?? getEffectivePlaywrightBrowsersPath();
1107
+ if (effectivePath !== null) {
1108
+ const pathProblem = status?.browserPathExists === false ? "missing" : status?.browserPathWritable === false ? "unwritable" : "unavailable";
1109
+ return `PLAYWRIGHT_BROWSERS_PATH is set to ${effectivePath}, but that browser cache is ${pathProblem}. Unset PLAYWRIGHT_BROWSERS_PATH and run \`bunx playwright install chromium\`, or install and run with the same writable path: \`PLAYWRIGHT_BROWSERS_PATH=${effectivePath} bunx playwright install chromium\`.`;
1110
+ }
1111
+ return "Run `bunx playwright install chromium` in this sandbox, then retry the Scope command.";
1112
+ }
1113
+ function diagnoseScopeError(error, cwd = process.cwd()) {
1114
+ const message = error instanceof Error ? error.message : String(error);
1115
+ const normalized = message.toLowerCase();
1116
+ if (PLAYWRIGHT_BROWSER_HINTS.some((hint) => normalized.includes(hint))) {
1117
+ const browserPath = extractPlaywrightBrowserPath(message);
1118
+ const browserPathHint = browserPath === null ? "" : ` Scope tried to launch Chromium from ${browserPath}.`;
1119
+ return {
1120
+ code: "PLAYWRIGHT_BROWSERS_MISSING",
1121
+ message: "Playwright Chromium is unavailable for Scope browser rendering.",
1122
+ recovery: getPlaywrightBrowserRemediation() + browserPathHint + " Use `scope doctor --json` to verify the browser check passes before rerunning render/site/instrument."
1123
+ };
1124
+ }
1125
+ if (MISSING_DEPENDENCY_HINTS.some((hint) => normalized.includes(hint))) {
1126
+ const packageManager = detectPackageManager(cwd);
1127
+ return {
1128
+ code: "TARGET_PROJECT_DEPENDENCIES_MISSING",
1129
+ message: "The target project's dependencies appear to be missing or incomplete.",
1130
+ recovery: `Run \`${packageManager} install\` in ${cwd}, then rerun \`scope doctor\` and retry the Scope command.`
1131
+ };
1132
+ }
1133
+ return null;
1134
+ }
1135
+ function formatScopeDiagnostic(error, cwd = process.cwd()) {
1136
+ const message = error instanceof Error ? error.message : String(error);
1137
+ const diagnostic = diagnoseScopeError(error, cwd);
1138
+ if (diagnostic === null) return `Error: ${message}`;
1139
+ return `Error [${diagnostic.code}]: ${diagnostic.message}
1140
+ Recovery: ${diagnostic.recovery}
1141
+ Cause: ${message}`;
1142
+ }
1143
+ async function getPlaywrightBrowserStatus(cwd = process.cwd()) {
1144
+ const effectiveBrowserPath = getEffectivePlaywrightBrowsersPath();
1145
+ const executablePath = getPlaywrightChromiumExecutablePath(cwd);
1146
+ const available = executablePath !== null && existsSync3(executablePath);
1147
+ const browserPathExists = effectiveBrowserPath === null ? null : existsSync3(effectiveBrowserPath);
1148
+ const browserPathWritable = effectiveBrowserPath === null ? null : await isWritableBrowserPath(effectiveBrowserPath);
1149
+ return {
1150
+ effectiveBrowserPath,
1151
+ executablePath,
1152
+ available,
1153
+ browserPathExists,
1154
+ browserPathWritable,
1155
+ remediation: getPlaywrightBrowserRemediation({
1156
+ effectiveBrowserPath,
1157
+ browserPathExists,
1158
+ browserPathWritable
1159
+ })
1160
+ };
1161
+ }
1162
+ function getPlaywrightChromiumExecutablePath(cwd = process.cwd()) {
1163
+ try {
1164
+ const packageJsonPath = __require.resolve("playwright/package.json", { paths: [cwd] });
1165
+ const packageJson = JSON.parse(readFileSync3(packageJsonPath, "utf-8"));
1166
+ if (!packageJson.version) return null;
1167
+ const playwrightPath = __require.resolve("playwright", { paths: [cwd] });
1168
+ const { chromium: chromium5 } = __require(playwrightPath);
1169
+ const executablePath = chromium5?.executablePath?.();
1170
+ if (typeof executablePath !== "string" || executablePath.length === 0) return null;
1171
+ return executablePath;
1172
+ } catch {
1173
+ return null;
1174
+ }
1175
+ }
1176
+ async function isWritableBrowserPath(browserPath) {
1177
+ const candidate = existsSync3(browserPath) ? browserPath : dirname2(browserPath);
1178
+ try {
1179
+ await access(candidate, constants.W_OK);
1180
+ return true;
1181
+ } catch {
1182
+ return false;
1183
+ }
1184
+ }
1185
+ function detectPackageManager(cwd = process.cwd()) {
1186
+ if (existsSync3(join(cwd, "bun.lock")) || existsSync3(join(cwd, "bun.lockb"))) return "bun";
1187
+ if (existsSync3(join(cwd, "pnpm-lock.yaml"))) return "pnpm";
1188
+ if (existsSync3(join(cwd, "yarn.lock"))) return "yarn";
1189
+ return "npm";
1190
+ }
1191
+ function hasLikelyInstalledDependencies(cwd = process.cwd()) {
1192
+ return existsSync3(join(cwd, "node_modules"));
1193
+ }
1194
+ function getMissingHarnessDependencies(cwd = process.cwd()) {
1195
+ return REQUIRED_HARNESS_DEPENDENCIES.filter((dependencyName) => {
1196
+ try {
1197
+ __require.resolve(dependencyName, { paths: [cwd] });
1198
+ return false;
1199
+ } catch {
1200
+ return true;
1201
+ }
1202
+ });
1203
+ }
1204
+ function extractPlaywrightBrowserPath(message) {
1205
+ const match = message.match(/Executable doesn't exist at\s+([^\n]+)/i);
1206
+ return match?.[1]?.trim() ?? null;
1207
+ }
1208
+
1209
+ // src/doctor-commands.ts
1061
1210
  function collectSourceFiles(dir) {
1062
- if (!existsSync3(dir)) return [];
1211
+ if (!existsSync4(dir)) return [];
1063
1212
  const results = [];
1064
1213
  for (const entry of readdirSync(dir, { withFileTypes: true })) {
1065
- const full = join(dir, entry.name);
1214
+ const full = join2(dir, entry.name);
1066
1215
  if (entry.isDirectory() && entry.name !== "node_modules" && entry.name !== ".reactscope") {
1067
1216
  results.push(...collectSourceFiles(full));
1068
1217
  } else if (entry.isFile() && /\.(tsx?|jsx?)$/.test(entry.name)) {
@@ -1071,17 +1220,47 @@ function collectSourceFiles(dir) {
1071
1220
  }
1072
1221
  return results;
1073
1222
  }
1223
+ var TAILWIND_CONFIG_FILES = [
1224
+ "tailwind.config.js",
1225
+ "tailwind.config.cjs",
1226
+ "tailwind.config.mjs",
1227
+ "tailwind.config.ts",
1228
+ "postcss.config.js",
1229
+ "postcss.config.cjs",
1230
+ "postcss.config.mjs",
1231
+ "postcss.config.ts"
1232
+ ];
1233
+ function hasTailwindSetup(cwd) {
1234
+ if (TAILWIND_CONFIG_FILES.some((file) => existsSync4(resolve3(cwd, file)))) {
1235
+ return true;
1236
+ }
1237
+ const packageJsonPath = resolve3(cwd, "package.json");
1238
+ if (!existsSync4(packageJsonPath)) return false;
1239
+ try {
1240
+ const pkg = JSON.parse(readFileSync4(packageJsonPath, "utf-8"));
1241
+ return [pkg.dependencies, pkg.devDependencies].some(
1242
+ (deps) => deps && Object.keys(deps).some(
1243
+ (name) => name === "tailwindcss" || name.startsWith("@tailwindcss/")
1244
+ )
1245
+ );
1246
+ } catch {
1247
+ return false;
1248
+ }
1249
+ }
1250
+ function getPlaywrightInstallCommand(effectiveBrowserPath) {
1251
+ return effectiveBrowserPath === null ? "bunx playwright install chromium" : `PLAYWRIGHT_BROWSERS_PATH=${effectiveBrowserPath} bunx playwright install chromium`;
1252
+ }
1074
1253
  function checkConfig(cwd) {
1075
1254
  const configPath = resolve3(cwd, "reactscope.config.json");
1076
- if (!existsSync3(configPath)) {
1255
+ if (!existsSync4(configPath)) {
1077
1256
  return {
1078
1257
  name: "config",
1079
1258
  status: "error",
1080
- message: "reactscope.config.json not found \u2014 run `scope init`"
1259
+ message: "reactscope.config.json not found \u2014 run `scope init` in the target project root"
1081
1260
  };
1082
1261
  }
1083
1262
  try {
1084
- JSON.parse(readFileSync3(configPath, "utf-8"));
1263
+ JSON.parse(readFileSync4(configPath, "utf-8"));
1085
1264
  return { name: "config", status: "ok", message: "reactscope.config.json valid" };
1086
1265
  } catch {
1087
1266
  return { name: "config", status: "error", message: "reactscope.config.json is not valid JSON" };
@@ -1090,14 +1269,14 @@ function checkConfig(cwd) {
1090
1269
  function checkTokens(cwd) {
1091
1270
  const configPath = resolve3(cwd, "reactscope.config.json");
1092
1271
  let tokensPath = resolve3(cwd, "reactscope.tokens.json");
1093
- if (existsSync3(configPath)) {
1272
+ if (existsSync4(configPath)) {
1094
1273
  try {
1095
- const cfg = JSON.parse(readFileSync3(configPath, "utf-8"));
1274
+ const cfg = JSON.parse(readFileSync4(configPath, "utf-8"));
1096
1275
  if (cfg.tokens?.file) tokensPath = resolve3(cwd, cfg.tokens.file);
1097
1276
  } catch {
1098
1277
  }
1099
1278
  }
1100
- if (!existsSync3(tokensPath)) {
1279
+ if (!existsSync4(tokensPath)) {
1101
1280
  return {
1102
1281
  name: "tokens",
1103
1282
  status: "warn",
@@ -1105,7 +1284,7 @@ function checkTokens(cwd) {
1105
1284
  };
1106
1285
  }
1107
1286
  try {
1108
- const raw = JSON.parse(readFileSync3(tokensPath, "utf-8"));
1287
+ const raw = JSON.parse(readFileSync4(tokensPath, "utf-8"));
1109
1288
  if (!raw.version) {
1110
1289
  return { name: "tokens", status: "warn", message: "Token file is missing a `version` field" };
1111
1290
  }
@@ -1117,21 +1296,28 @@ function checkTokens(cwd) {
1117
1296
  function checkGlobalCss(cwd) {
1118
1297
  const configPath = resolve3(cwd, "reactscope.config.json");
1119
1298
  let globalCss = [];
1120
- if (existsSync3(configPath)) {
1299
+ if (existsSync4(configPath)) {
1121
1300
  try {
1122
- const cfg = JSON.parse(readFileSync3(configPath, "utf-8"));
1301
+ const cfg = JSON.parse(readFileSync4(configPath, "utf-8"));
1123
1302
  globalCss = cfg.components?.wrappers?.globalCSS ?? [];
1124
1303
  } catch {
1125
1304
  }
1126
1305
  }
1127
1306
  if (globalCss.length === 0) {
1307
+ if (!hasTailwindSetup(cwd)) {
1308
+ return {
1309
+ name: "globalCSS",
1310
+ status: "ok",
1311
+ message: "No globalCSS configured \u2014 skipping CSS injection for this non-Tailwind project"
1312
+ };
1313
+ }
1128
1314
  return {
1129
1315
  name: "globalCSS",
1130
1316
  status: "warn",
1131
1317
  message: "No globalCSS configured \u2014 Tailwind styles won't apply to renders. Add `components.wrappers.globalCSS` to reactscope.config.json"
1132
1318
  };
1133
1319
  }
1134
- const missing = globalCss.filter((f) => !existsSync3(resolve3(cwd, f)));
1320
+ const missing = globalCss.filter((f) => !existsSync4(resolve3(cwd, f)));
1135
1321
  if (missing.length > 0) {
1136
1322
  return {
1137
1323
  name: "globalCSS",
@@ -1147,11 +1333,11 @@ function checkGlobalCss(cwd) {
1147
1333
  }
1148
1334
  function checkManifest(cwd) {
1149
1335
  const manifestPath = resolve3(cwd, ".reactscope", "manifest.json");
1150
- if (!existsSync3(manifestPath)) {
1336
+ if (!existsSync4(manifestPath)) {
1151
1337
  return {
1152
1338
  name: "manifest",
1153
1339
  status: "warn",
1154
- message: "Manifest not found \u2014 run `scope manifest generate`"
1340
+ message: "Manifest not found \u2014 run `scope manifest generate` in the target project root"
1155
1341
  };
1156
1342
  }
1157
1343
  const manifestMtime = statSync(manifestPath).mtimeMs;
@@ -1168,6 +1354,54 @@ function checkManifest(cwd) {
1168
1354
  return { name: "manifest", status: "ok", message: "Manifest present and up to date" };
1169
1355
  }
1170
1356
  var ICONS = { ok: "\u2713", warn: "!", error: "\u2717" };
1357
+ function checkDependencies(cwd) {
1358
+ const packageManager = detectPackageManager(cwd);
1359
+ if (!hasLikelyInstalledDependencies(cwd)) {
1360
+ return {
1361
+ name: "dependencies",
1362
+ status: "error",
1363
+ remediationCode: "TARGET_PROJECT_DEPENDENCIES_MISSING",
1364
+ fixCommand: `${packageManager} install`,
1365
+ message: `node_modules not found \u2014 run \`${packageManager} install\` in ${cwd} before render/site/instrument`
1366
+ };
1367
+ }
1368
+ const missingHarnessDependencies = getMissingHarnessDependencies(cwd);
1369
+ if (missingHarnessDependencies.length > 0) {
1370
+ return {
1371
+ name: "dependencies",
1372
+ status: "error",
1373
+ remediationCode: "TARGET_PROJECT_HARNESS_DEPENDENCIES_MISSING",
1374
+ fixCommand: `${packageManager} install`,
1375
+ message: `Missing React harness dependencies: ${missingHarnessDependencies.join(", ")}. Run \`${packageManager} install\` in ${cwd}, then retry render/site/instrument.`
1376
+ };
1377
+ }
1378
+ return {
1379
+ name: "dependencies",
1380
+ status: "ok",
1381
+ message: "node_modules and React harness dependencies present"
1382
+ };
1383
+ }
1384
+ async function checkPlaywright(cwd) {
1385
+ const status = await getPlaywrightBrowserStatus(cwd);
1386
+ const pathDetails = status.effectiveBrowserPath === null ? "PLAYWRIGHT_BROWSERS_PATH is unset" : `PLAYWRIGHT_BROWSERS_PATH=${status.effectiveBrowserPath}; exists=${status.browserPathExists}; writable=${status.browserPathWritable}`;
1387
+ if (status.available) {
1388
+ return {
1389
+ name: "playwright",
1390
+ status: "ok",
1391
+ message: `Playwright package available (${pathDetails})`
1392
+ };
1393
+ }
1394
+ return {
1395
+ name: "playwright",
1396
+ status: "error",
1397
+ remediationCode: "PLAYWRIGHT_BROWSERS_MISSING",
1398
+ fixCommand: getPlaywrightInstallCommand(status.effectiveBrowserPath),
1399
+ message: `Playwright Chromium unavailable (${pathDetails}) \u2014 ${status.remediation}`
1400
+ };
1401
+ }
1402
+ function collectFixCommands(checks) {
1403
+ return checks.filter((check) => check.status === "error" && check.fixCommand !== void 0).map((check) => check.fixCommand).filter((command, index, commands) => commands.indexOf(command) === index);
1404
+ }
1171
1405
  function formatCheck(check) {
1172
1406
  return ` [${ICONS[check.status]}] ${check.name.padEnd(12)} ${check.message}`;
1173
1407
  }
@@ -1180,6 +1414,8 @@ CHECKS PERFORMED:
1180
1414
  tokens reactscope.tokens.json exists and passes validation
1181
1415
  css globalCSS files referenced in config actually exist
1182
1416
  manifest .reactscope/manifest.json exists and is not stale
1417
+ dependencies node_modules exists in the target project root
1418
+ playwright Playwright browser runtime is available
1183
1419
  (stale = source files modified after last generate)
1184
1420
 
1185
1421
  STATUS LEVELS: ok | warn | error
@@ -1189,20 +1425,34 @@ Run this first whenever renders fail or produce unexpected output.
1189
1425
  Examples:
1190
1426
  scope doctor
1191
1427
  scope doctor --json
1428
+ scope doctor --print-fix-commands
1192
1429
  scope doctor --json | jq '.checks[] | select(.status == "error")'`
1193
- ).option("--json", "Emit structured JSON output", false).action((opts) => {
1430
+ ).option("--json", "Emit structured JSON output", false).option(
1431
+ "--print-fix-commands",
1432
+ "Print deduplicated shell remediation commands for failing checks",
1433
+ false
1434
+ ).action(async (opts) => {
1194
1435
  const cwd = process.cwd();
1195
1436
  const checks = [
1196
1437
  checkConfig(cwd),
1197
1438
  checkTokens(cwd),
1198
1439
  checkGlobalCss(cwd),
1199
- checkManifest(cwd)
1440
+ checkManifest(cwd),
1441
+ checkDependencies(cwd),
1442
+ await checkPlaywright(cwd)
1200
1443
  ];
1201
1444
  const errors = checks.filter((c) => c.status === "error").length;
1202
1445
  const warnings = checks.filter((c) => c.status === "warn").length;
1446
+ const fixCommands = collectFixCommands(checks);
1447
+ if (opts.printFixCommands) {
1448
+ process.stdout.write(`${JSON.stringify({ cwd, fixCommands }, null, 2)}
1449
+ `);
1450
+ if (errors > 0) process.exit(1);
1451
+ return;
1452
+ }
1203
1453
  if (opts.json) {
1204
1454
  process.stdout.write(
1205
- `${JSON.stringify({ passed: checks.length - errors - warnings, warnings, errors, checks }, null, 2)}
1455
+ `${JSON.stringify({ passed: checks.length - errors - warnings, warnings, errors, fixCommands, checks }, null, 2)}
1206
1456
  `
1207
1457
  );
1208
1458
  if (errors > 0) process.exit(1);
@@ -1232,12 +1482,12 @@ Examples:
1232
1482
  import { Command as Command3 } from "commander";
1233
1483
 
1234
1484
  // src/skill-content.ts
1235
- var SKILL_CONTENT = '# Scope \u2014 Agent Skill\n\n## TLDR\nScope is a React codebase introspection toolkit. Use it to answer questions about component structure, props, context dependencies, side effects, and visual output \u2014 without running the app.\n\n**When to reach for it:** Any task requiring "which components use X", "what props does Y accept", "render Z for visual verification", "does this component depend on a provider", or "what design tokens are in use".\n\n**3-command workflow:**\n```\nscope init # scaffold config + auto-generate manifest\nscope manifest query --context ThemeContext # ask questions about the codebase\nscope render Button # produce a PNG of a component\n```\n\n---\n\n---\n\n## Mental Model\n\nUnderstanding how Scope\'s data flows is the key to using it effectively as an agent.\n\n```\nSource TypeScript files\n \u2193 (ts-morph AST parse)\n manifest.json \u2190 structural facts: props, hooks, contexts, complexity\n \u2193 (esbuild + Playwright)\n renders/*.json \u2190 visual facts: screenshot, computedStyles, dom, a11y\n \u2193 (token engine)\n compliance-styles.json \u2190 audit facts: which CSS values match tokens, which don\'t\n \u2193 (site generator)\n site/ \u2190 human-readable docs combining all of the above\n```\n\nEach layer depends on the previous. If you\'re getting unexpected results, check whether the earlier layers are stale (run `scope doctor` to diagnose).\n\n---\n\n## The Four Subsystems\n\n### 1. Manifest (`scope manifest *`)\nThe manifest is a static analysis snapshot of your TypeScript source. It tells you:\n- What components exist, where they live, and how they\'re exported\n- What props each component accepts (types, defaults, required/optional)\n- What React hooks they call (`detectedHooks`)\n- What contexts they consume (`requiredContexts`) \u2014 must be provided for a render to succeed\n- Whether they compose other components (`composes` / `composedBy`)\n- Their **complexity class** \u2014 `"simple"` or `"complex"` \u2014 which determines the render engine\n\nThe manifest never runs your code. It only reads TypeScript. This means it\'s fast and safe, but it can\'t know about runtime values.\n\n### 2. Render Engine (`scope render *`)\nThe render engine compiles components with esbuild and renders them in Chromium (Playwright). Two paths exist:\n\n| Path | When | Speed | Capability |\n|------|------|-------|------------|\n| **Satori** | `complexityClass: "simple"` | ~8ms | Flexbox only, no JS, no CSS-in-JS |\n| **BrowserPool** | `complexityClass: "complex"` | ~200\u2013800ms | Full DOM, CSS, Tailwind, animations |\n\nMost real-world components route through BrowserPool. Scope defaults to `"complex"` when uncertain (safe fallback).\n\nEach render produces:\n- `screenshot` \u2014 retina-quality PNG (2\xD7 `deviceScaleFactor`; display at CSS px dimensions)\n- `width` / `height` \u2014 CSS pixel dimensions of the component root\n- `computedStyles` \u2014 per-node computed CSS keyed by `#node-0`, `#node-1`, etc.\n- `dom` \u2014 full DOM tree with bounding boxes (BrowserPool only)\n- `accessibility` \u2014 role, aria-name, violation list (BrowserPool only)\n- `renderTimeMs` \u2014 wall-clock render duration\n\n### 3. Scope Files (`.scope.tsx`)\nScope files let you define **named rendering scenarios** for a component alongside it in the source tree. They are the primary way to ensure `render all` produces meaningful screenshots.\n\n```tsx\n// Button.scope.tsx\nimport type { ScopeFile } from \'@agent-scope/cli\';\nimport { Button } from \'./Button\';\n\nexport default {\n default: { variant: \'primary\', children: \'Click me\' },\n ghost: { variant: \'ghost\', children: \'Cancel\' },\n danger: { variant: \'danger\', children: \'Delete\' },\n disabled: { variant: \'primary\', children: \'Disabled\', disabled: true },\n} satisfies ScopeFile<typeof Button>;\n```\n\nKey rules:\n- The file must be named `<ComponentName>.scope.tsx` in the same directory\n- Export a default object where keys are scenario names and values are props\n- `render all` uses the `default` scenario (or first defined) as the primary screenshot\n- If 2+ scenarios exist, `render all` automatically runs a matrix and merges cells into the component JSON\n- Scenarios also feed the interactive Playground in the docs site\n\nWhen a component renders blank with `{}` props, **the fix is usually to create a `.scope.tsx` file** with real props.\n\n### 4. Token Compliance\nThe compliance pipeline:\n1. `scope render all` captures `computedStyles` for every element in every component\n2. These are written to `.reactscope/compliance-styles.json`\n3. The token engine compares each computed CSS value against your `reactscope.tokens.json`\n4. `scope tokens compliance` reports the aggregate on-system percentage\n5. `scope ci` fails if the percentage is below `complianceThreshold` (default 90%)\n\n**On-system** means the value exactly matches a resolved token value. Off-system means it\'s a hardcoded value with no token backing it.\n\n---\n\n## Complexity Classes \u2014 Practical Guide\n\nThe `complexityClass` field determines which render engine runs. Scope auto-detects it, but agents should understand it:\n\n**`"simple"` components:**\n- Pure presentational, flexbox layout only\n- No CSS grid, no absolute/fixed/sticky positioning\n- No CSS animations, transitions, or transforms\n- No `className` values Scope can\'t statically trace (e.g. dynamic Tailwind classes)\n- Renders in ~8ms via Satori (SVG-based, no browser needed)\n\n**`"complex"` components:**\n- Anything using Tailwind (CSS injection required)\n- CSS grid, positioned elements, overflow, z-index\n- Components that read from context at render time\n- Any component Scope isn\'t sure about (conservative default)\n- Renders in ~200\u2013800ms via Playwright BrowserPool\n\nWhen in doubt: complex is always safe. Simple is an optimization.\n\n---\n\n## Required Contexts \u2014 Why Renders Fail\n\nIf `requiredContexts` is non-empty, the component calls `useContext` on one or more contexts. Without a provider, it will either render broken or throw entirely.\n\nTwo ways to fix:\n1. **Provider presets in config** (recommended): add provider names to `reactscope.config.json \u2192 components.wrappers.providers`\n2. **Scope file with wrapper**: wrap the component in a provider in the scenario itself\n\nBuilt-in mocks (always provided): `ThemeContext \u2192 { theme: \'light\' }`, `LocaleContext \u2192 { locale: \'en-US\' }`.\n\n---\n\n## `scope doctor` \u2014 Always Run This First\n\nBefore debugging any render issue, run:\n```bash\nscope doctor\n```\n\nIt checks:\n- `reactscope.config.json` is valid JSON\n- Token file exists and has a `version` field\n- Every path in `globalCSS` resolves on disk\n- Manifest is present and up to date (not stale relative to source)\n\n**If `globalCSS` is empty or missing**: Tailwind styles won\'t apply to renders. Every component will look unstyled. This is the most common footgun. Fix: add your CSS entry file (the one with `@tailwind base; @tailwind components; @tailwind utilities;`) to `globalCSS` in config.\n\n---\n\n## Agent Decision Tree\n\n**"I want to know what props Component X accepts"**\n\u2192 `scope manifest get X --format json | jq \'.props\'`\n\n**"I want to know which components will break if I change a context"**\n\u2192 `scope manifest query --context MyContext --format json`\n\n**"I want to render a component to verify visual output"**\n\u2192 Create a `.scope.tsx` file with real props first, then `scope render X`\n\n**"I want to render all variants of a component"**\n\u2192 Define all variants in `.scope.tsx`, then `scope render all` (auto-matrix)\n\u2192 Or: `scope render matrix X --axes \'variant:primary,secondary,danger\'`\n\n**"I want to audit token compliance"**\n\u2192 `scope render all` first (populates computedStyles), then `scope tokens compliance`\n\n**"Renders look unstyled / blank"**\n\u2192 Run `scope doctor` \u2014 likely missing `globalCSS`\n\u2192 If props are the issue: create/update the `.scope.tsx` file\n\n**"I want to understand blast radius of a token change"**\n\u2192 `scope tokens impact color.primary.500 --new-value \'#0077dd\'`\n\u2192 `scope tokens preview color.primary.500 --new-value \'#0077dd\'` for visual diff\n\n**"I need to set up Scope in a new project"**\n\u2192 `scope init --yes` (auto-detects Tailwind + CSS, generates manifest automatically)\n\u2192 `scope doctor` to validate\n\u2192 Create `.scope.tsx` files for key components\n\u2192 `scope render all`\n\n**"I want to run Scope in CI"**\n\u2192 `scope ci --json --output ci-result.json`\n\u2192 Exit code 0 = pass, non-zero = specific failure type\n\n---\n\n\n## Installation\n\n```bash\nnpm install -g @agent-scope/cli # global\nnpm install --save-dev @agent-scope/cli # per-project\n```\n\nBinary: `scope`\n\n---\n\n## Core Workflow\n\n```\ninit \u2192 manifest generate \u2192 manifest query/get/list \u2192 render \u2192 (token audit) \u2192 ci\n```\n\n- **init**: Scaffold `reactscope.config.json` + token stub, auto-detect framework/globalCSS, **immediately runs `manifest generate`** so you see results right away.\n- **doctor**: Health-check command \u2014 validates config, token file, globalCSS presence, and manifest staleness.\n- **generate**: Parse TypeScript AST and emit `.reactscope/manifest.json`. Run once per codebase change (or automatically via `scope init`).\n- **query / get / list**: Ask structural questions. No network required. Works from manifest alone. Supports filtering by `--collection` and `--internal`.\n- **render**: Produce PNGs of components via esbuild + Playwright (BrowserPool). Requires manifest for file paths. Auto-injects required prop defaults and globalCSS.\n- **token audit**: Validate design tokens via `@scope/tokens` CLI commands (`tokens list`, `tokens compliance`, `tokens impact`, `tokens preview`, `tokens export`).\n- **ci**: Run compliance checks and exit with code 0/1 for CI pipelines. `report pr-comment` posts results to GitHub PRs.\n\n---\n\n## Full CLI Reference\n\n### `scope init`\nScaffold config, detect framework, extract Tailwind tokens, detect globalCSS files, and **automatically run `scope manifest generate`**.\n\n```bash\nscope init\nscope init --force # overwrite existing config\n```\n\nAfter init completes, the manifest is already written \u2014 no manual `scope manifest generate` step needed.\n\n**Tailwind token extraction**: reads `tailwind.config.js`, extracts colors (with nested scale support), spacing, fontFamily, borderRadius. Stored in `reactscope.tokens.json`.\n\n**globalCSS detection**: checks 9 common patterns (`src/styles.css`, `src/index.css`, `app/globals.css`, etc.). Stored in `components.wrappers.globalCSS` in config.\n\n---\n\n### `scope doctor`\nValidate the Scope setup. Exits non-zero on errors, zero on warnings-only.\n\n```bash\nscope doctor\nscope doctor --json\n```\n\nChecks:\n- `config` \u2014 `reactscope.config.json` is valid JSON with required fields\n- `tokens` \u2014 token file is present and has a valid `version` field\n- `globalCSS` \u2014 globalCSS files listed in config exist on disk\n- `manifest` \u2014 manifest exists and is not stale (compares source file mtimes)\n\n```\n$ scope doctor\nScope Doctor\n\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 [\u2713] config reactscope.config.json valid\n [\u2713] tokens Token file valid\n [\u2713] globalCSS 1 globalCSS file(s) present\n [!] manifest Manifest may be stale \u2014 5 source file(s) modified since last generate\n\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 1 warning(s) \u2014 everything works but could be better\n```\n\n---\n\n### `scope capture <url>`\nCapture a live React component tree from a running app URL.\n\n```bash\nscope capture http://localhost:3000\nscope capture http://localhost:3000 --output report.json --pretty\nscope capture http://localhost:3000 --timeout 15000 --wait 2000\n```\n\nFlags:\n| Flag | Type | Default | Description |\n|------|------|---------|-------------|\n| `-o, --output <path>` | string | stdout | Write JSON to file |\n| `--pretty` | bool | false | Pretty-print JSON |\n| `--timeout <ms>` | number | 10000 | Max wait for React to mount |\n| `--wait <ms>` | number | 0 | Additional wait after page load |\n\nOutput (stdout): serialized PageReport JSON (or path when `--output` is set)\n\n---\n\n### `scope tree <url>`\nPrint the React component tree from a live URL.\n\n```bash\nscope tree http://localhost:3000\nscope tree http://localhost:3000 --depth 3 --show-props --show-hooks\n```\n\nFlags:\n| Flag | Type | Default | Description |\n|------|------|---------|-------------|\n| `--depth <n>` | number | unlimited | Max depth to display |\n| `--show-props` | bool | false | Include prop names next to components |\n| `--show-hooks` | bool | false | Show hook counts per component |\n| `--timeout <ms>` | number | 10000 | Max wait for React to mount |\n| `--wait <ms>` | number | 0 | Additional wait after page load |\n\n---\n\n### `scope report <url>`\nCapture and print a human-readable summary of a React app.\n\n```bash\nscope report http://localhost:3000\nscope report http://localhost:3000 --json\n```\n\nFlags:\n| Flag | Type | Default | Description |\n|------|------|---------|-------------|\n| `--json` | bool | false | Emit structured JSON instead of text |\n| `--timeout <ms>` | number | 10000 | Max wait for React to mount |\n| `--wait <ms>` | number | 0 | Additional wait after page load |\n\n---\n\n### `scope report baseline`\nSave a baseline snapshot for future diff comparisons.\n\n```bash\nscope report baseline\nscope report baseline --output baselines/my-baseline.json\n```\n\n---\n\n### `scope report diff`\nDiff the current app state against a saved baseline.\n\n```bash\nscope report diff\nscope report diff --baseline baselines/my-baseline.json\nscope report diff --json\n```\n\n---\n\n### `scope report pr-comment`\nPost a Scope CI report as a GitHub PR comment. Used in CI pipelines via the reusable `scope-ci` workflow.\n\n```bash\nscope report pr-comment --report-path scope-ci-report.json\n```\n\nRequires `GITHUB_TOKEN`, `GITHUB_REPOSITORY`, and `GITHUB_PR_NUMBER` in environment.\n\n---\n\n### `scope manifest generate`\nScan source files and write `.reactscope/manifest.json`.\n\n```bash\nscope manifest generate\nscope manifest generate --root ./packages/ui\nscope manifest generate --include "src/**/*.tsx" --exclude "**/*.test.tsx"\nscope manifest generate --output custom/manifest.json\n```\n\nFlags:\n| Flag | Type | Default | Description |\n|------|------|---------|-------------|\n| `--root <path>` | string | cwd | Project root directory |\n| `--output <path>` | string | `.reactscope/manifest.json` | Output path |\n| `--include <globs>` | string | `src/**/*.tsx,src/**/*.ts` | Comma-separated include globs |\n| `--exclude <globs>` | string | `**/node_modules/**,...` | Comma-separated exclude globs |\n\n---\n\n### `scope manifest list`\nList all components in the manifest.\n\n```bash\nscope manifest list\nscope manifest list --filter "Button*"\nscope manifest list --format json\nscope manifest list --collection Forms # filter to named collection\nscope manifest list --internal # only internal components\nscope manifest list --no-internal # hide internal components\n```\n\nFlags:\n| Flag | Type | Default | Description |\n|------|------|---------|-------------|\n| `--format <fmt>` | `json\\|table` | auto (TTY\u2192table, pipe\u2192json) | Output format |\n| `--filter <glob>` | string | \u2014 | Filter component names by glob |\n| `--collection <name>` | string | \u2014 | Filter to named collection |\n| `--internal` | bool | false | Show only internal components |\n| `--no-internal` | bool | false | Hide internal components |\n| `--manifest <path>` | string | `.reactscope/manifest.json` | Manifest path |\n\nTTY table output (includes COLLECTION and INTERNAL columns):\n```\nNAME FILE COMPLEXITY HOOKS CONTEXTS COLLECTION INTERNAL\n------------ --------------------------- ---------- ----- -------- ---------- --------\nButton src/components/Button.tsx simple 1 0 \u2014 no\nThemeToggle src/components/Toggle.tsx complex 3 1 Forms no\n```\n\n---\n\n### `scope manifest get <name>`\nGet full details of a single component.\n\n```bash\nscope manifest get Button\nscope manifest get Button --format json\n```\n\nJSON output includes `collection` and `internal` fields:\n```json\n{\n "name": "Button",\n "filePath": "src/components/Button.tsx",\n "collection": "Primitives",\n "internal": false,\n ...\n}\n```\n\n---\n\n### `scope manifest query`\nQuery components by attributes.\n\n```bash\nscope manifest query --context ThemeContext\nscope manifest query --hook useEffect\nscope manifest query --complexity complex\nscope manifest query --side-effects\nscope manifest query --has-fetch\nscope manifest query --has-prop <propName>\nscope manifest query --composed-by <ComponentName>\nscope manifest query --internal\nscope manifest query --collection Forms\nscope manifest query --context ThemeContext --format json\n```\n\nFlags:\n| Flag | Type | Default | Description |\n|------|------|---------|-------------|\n| `--context <name>` | string | \u2014 | Find components consuming a context by name |\n| `--hook <name>` | string | \u2014 | Find components using a specific hook |\n| `--complexity <class>` | `simple\\|complex` | \u2014 | Filter by complexity class |\n| `--side-effects` | bool | false | Any side effects detected |\n| `--has-fetch` | bool | false | Components with fetch calls specifically |\n| `--has-prop <name>` | string | \u2014 | Components that accept a specific prop |\n| `--composed-by <name>` | string | \u2014 | Components rendered inside a specific parent |\n| `--internal` | bool | false | Only internal components |\n| `--collection <name>` | string | \u2014 | Filter to named collection |\n| `--format <fmt>` | `json\\|table` | auto | Output format |\n| `--manifest <path>` | string | `.reactscope/manifest.json` | Manifest path |\n\n---\n\n### `scope render <component>`\nRender a single component to PNG (TTY) or JSON (pipe).\n\n**Auto prop defaults**: if `--props` is omitted, Scope injects sensible defaults so required props don\'t produce blank renders: strings/nodes \u2192 component name, unions \u2192 first value, booleans \u2192 `false`, numbers \u2192 `0`.\n\n**globalCSS auto-injection**: reads `components.wrappers.globalCSS` from config and compiles/injects CSS (supports Tailwind v3 via PostCSS) into the render harness. A warning is printed to stderr if no globalCSS is configured (common cause of unstyled renders).\n\n```bash\nscope render Button\nscope render Button --props \'{"variant":"primary","children":"Click me"}\'\nscope render Button --viewport 375x812\nscope render Button --output button.png\nscope render Button --format json\n```\n\nFlags:\n| Flag | Type | Default | Description |\n|------|------|---------|-------------|\n| `--props <json>` | string | `{}` | Inline props as JSON string |\n| `--viewport <WxH>` | string | `375x812` | Viewport size |\n| `--theme <name>` | string | \u2014 | Theme name from token system |\n| `-o, --output <path>` | string | \u2014 | Write PNG to specific path |\n| `--format <fmt>` | `png\\|json` | auto (TTY\u2192file, pipe\u2192json) | Output format |\n| `--manifest <path>` | string | `.reactscope/manifest.json` | Manifest path |\n\n---\n\n### `scope render matrix <component>`\nRender across a Cartesian product of prop axes. Accepts both `key:v1,v2` and `{"key":["v1","v2"]}` JSON format for `--axes`.\n\n```bash\nscope render matrix Button --axes \'variant:primary,secondary,danger\'\nscope render matrix Button --axes \'{"variant":["primary","secondary"]}\'\nscope render matrix Button --axes \'variant:primary,secondary size:sm,md,lg\'\nscope render matrix Button --sprite button-matrix.png --format json\n```\n\n---\n\n### `scope render all`\nRender every component in the manifest.\n\n```bash\nscope render all\nscope render all --concurrency 4 --output-dir renders/\n```\n\nHandles imports of CSS files in components (maps to empty loader so styles are injected at page level). SVG and font imports are handled via dataurl loaders.\n\n---\n\n### `scope instrument tree`\nCapture the live React component tree with instrumentation metadata.\n\n```bash\nscope instrument tree http://localhost:3000\nscope instrument tree http://localhost:3000 --depth 5 --show-props\n```\n\n**Implementation note**: uses a fresh `chromium.launch()` + `newContext()` + `newPage()` per call (not BrowserPool), with `addInitScript` called before `setContent` to ensure the Scope runtime is injected at document-start before React loads.\n\n---\n\n### `scope instrument hooks`\nProfile hook execution in live components.\n\n```bash\nscope instrument hooks http://localhost:3000\nscope instrument hooks http://localhost:3000 --component Button\n```\n\n**Implementation note**: requires `addInitScript({ content: getBrowserEntryScript() })` before `setContent` so `__REACT_DEVTOOLS_GLOBAL_HOOK__` is present when React loads its renderer.\n\n---\n\n### `scope instrument profile`\nProfile render performance of live components.\n\n```bash\nscope instrument profile http://localhost:3000\n```\n\n---\n\n### `scope instrument renders`\nRe-render causality analysis \u2014 what triggered each render.\n\n```bash\nscope instrument renders http://localhost:3000\n```\n\n---\n\n### `scope tokens get <name>`\nGet details of a single design token.\n\n### `scope tokens list`\nList all tokens. Token file must have a `version` field (written by `scope init`).\n\n```bash\nscope tokens list\nscope tokens list --type color\nscope tokens list --format json\n```\n\n### `scope tokens search <query>`\nFull-text search across token names/values.\n\n### `scope tokens resolve <value>`\nResolve a CSS value or alias back to its token name.\n\n### `scope tokens validate`\nValidate token file schema.\n\n### `scope tokens compliance`\nCheck rendered components for design token compliance.\n\n```bash\nscope tokens compliance\nscope tokens compliance --threshold 95\n```\n\n### `scope tokens impact <token>`\nAnalyze impact of changing a token \u2014 which components use it.\n\n```bash\nscope tokens impact --token color.primary.500\n```\n\n### `scope tokens preview <token>`\nPreview a token value change visually before committing.\n\n### `scope tokens export`\nExport tokens in multiple formats.\n\n```bash\nscope tokens export --format flat-json\nscope tokens export --format css\nscope tokens export --format scss\nscope tokens export --format ts\nscope tokens export --format tailwind\nscope tokens export --format style-dictionary\n```\n\n**Format aliases** (auto-corrected with "Did you mean?" hint):\n- `json` \u2192 `flat-json`\n- `js` \u2192 `ts`\n- `sass` \u2192 `scss`\n- `tw` \u2192 `tailwind`\n\n---\n\n### `scope ci`\nRun all CI checks (compliance, accessibility, console errors) and exit 0/1.\n\n```bash\nscope ci\nscope ci --json\nscope ci --threshold 90 # compliance threshold (default: 90)\n```\n\n```\n$ scope ci --json\n\u2192 CI passed in 3.2s\n\u2192 Compliance 100.0% >= threshold 90.0% \u2705\n\u2192 Accessibility audit not yet implemented \u2014 skipped \u2705\n\u2192 No console errors detected \u2705\n\u2192 Exit code 0\n```\n\nThe `scope-ci` **reusable GitHub Actions workflow** is available at `.github/workflows/scope-ci.yml` and can be included in any repo\'s CI to run `scope ci` and post results as a PR comment via `scope report pr-comment`.\n\n---\n\n### `scope site build`\nGenerate a static HTML component gallery site from the manifest.\n\n```bash\nscope site build\nscope site build --output ./dist/site\n```\n\n**Collections support**: components are grouped under named collection sections in the sidebar and index grid. Internal components are hidden from the sidebar and card grid but appear in composition detail sections with an `internal` badge.\n\n**Collection display rules**:\n- Sidebar: one section divider per collection + an "Ungrouped" section; internal components excluded\n- Index page: named sections with heading + optional description; internal components excluded\n- Component detail page: Composes/Composed By lists ALL components including internal ones (with subtle badge)\n- Falls back to flat list when no collections configured (backwards-compatible)\n\n### `scope site serve`\nServe the generated site locally.\n\n```bash\nscope site serve\nscope site serve --port 4000\n```\n\n---\n\n## Collections & Internal Components\n\nComponents can be organized into named **collections** and flagged as **internal** (library implementation details not shown in the public gallery).\n\n### Defining collections\n\n**1. TSDoc tag** (highest precedence):\n```tsx\n/**\n * @collection Forms\n */\nexport function Input() { ... }\n```\n\n**2. `.scope.ts` co-located file**:\n```ts\n// Input.scope.ts\nexport const collection = "Forms"\n```\n\n**3. Config-level glob patterns**:\n```json\n// reactscope.config.json\n{\n "collections": [\n { "name": "Forms", "description": "Form inputs and controls", "patterns": ["src/forms/**"] },\n { "name": "Primitives", "patterns": ["src/primitives/**"] }\n ]\n}\n```\n\nResolution precedence: TSDoc `@collection` > `.scope.ts` export > config pattern.\n\n### Flagging internal components\n\n**TSDoc tag**:\n```tsx\n/**\n * @internal\n */\nexport function InternalHelperButton() { ... }\n```\n\n**Config glob patterns**:\n```json\n{\n "internalPatterns": ["src/internal/**", "src/**/*Internal*"]\n}\n```\n\n---\n\n## Manifest Output Schema\n\nFile: `.reactscope/manifest.json`\n\n```typescript\n{\n version: "0.1",\n generatedAt: string, // ISO 8601\n collections: CollectionConfig[], // echoes config.collections, [] when not set\n components: Record<string, ComponentDescriptor>,\n tree: Record<string, { children: string[], parents: string[] }>\n}\n```\n\n### `ComponentDescriptor` fields\n\n| Field | Type | Description |\n|-------|------|-------------|\n| `filePath` | `string` | Relative path from project root to source file |\n| `exportType` | `"named" \\| "default" \\| "none"` | How the component is exported |\n| `displayName` | `string` | `displayName` if set, else function/class name |\n| `collection` | `string?` | Resolved collection name (`undefined` = ungrouped) |\n| `internal` | `boolean` | `true` if flagged as internal (default: `false`) |\n| `props` | `Record<string, PropDescriptor>` | Extracted prop types keyed by prop name |\n| `composes` | `string[]` | Components this one renders in its JSX |\n| `composedBy` | `string[]` | Components that render this one in their JSX |\n| `complexityClass` | `"simple" \\| "complex"` | Render path: simple = Satori-safe, complex = requires BrowserPool |\n| `requiredContexts` | `string[]` | React context names consumed |\n| `detectedHooks` | `string[]` | All hooks called, sorted alphabetically |\n| `sideEffects` | `SideEffects` | Side effect categories detected |\n| `memoized` | `boolean` | Wrapped with `React.memo` |\n| `forwardedRef` | `boolean` | Wrapped with `React.forwardRef` |\n| `hocWrappers` | `string[]` | HOC wrapper names (excluding memo/forwardRef) |\n| `loc` | `{ start: number, end: number }` | Line numbers in source file |\n\n---\n\n## Common Agent Workflows\n\n### Structural queries\n\n```bash\n# Which components use ThemeContext?\nscope manifest query --context ThemeContext\n\n# What props does Button accept?\nscope manifest get Button --format json | jq \'.props\'\n\n# Which components are safe to render without a provider?\nscope manifest query --complexity simple # + check requiredContexts === []\n\n# Show all components with side effects\nscope manifest query --side-effects\n\n# Which components make fetch calls?\nscope manifest query --has-fetch\n\n# Which components use useEffect?\nscope manifest query --hook useEffect\n\n# Which components accept a disabled prop?\nscope manifest query --has-prop disabled\n\n# Which components are composed inside Modal?\nscope manifest query --composed-by Modal\n\n# All components in the Forms collection\nscope manifest list --collection Forms\n\n# All internal components (library implementation details)\nscope manifest list --internal\n\n# Public components only (hide internals)\nscope manifest list --no-internal\n```\n\n### Render workflows\n\n```bash\n# Render Button in all variants (auto-defaults props if not provided)\nscope render matrix Button --axes \'variant:primary,secondary,danger\'\n\n# Render with JSON axes format\nscope render matrix Button --axes \'{"variant":["primary","secondary"]}\'\n\n# Render with explicit props\nscope render Button --props \'{"variant":"primary","disabled":true}\'\n\n# Render all components (handles CSS/SVG/font imports automatically)\nscope render all --concurrency 8\n\n# Get render as JSON\nscope render Button --format json | jq \'.screenshot\' | base64 -d > button.png\n```\n\n### Token workflows\n\n```bash\n# List all tokens\nscope tokens list\n\n# Check compliance\nscope tokens compliance --threshold 95\n\n# See what a token change impacts\nscope tokens impact --token color.primary.500\n\n# Export for Tailwind\nscope tokens export --format tailwind\n```\n\n### CI workflow\n\n```bash\n# Full compliance check\nscope ci --json\n\n# In GitHub Actions \u2014 use the reusable workflow\n# .github/workflows/ci.yml:\n# uses: FlatFilers/Scope/.github/workflows/scope-ci.yml@main\n```\n\n---\n\n## Error Patterns\n\n| Error | Cause | Fix |\n|-------|-------|-----|\n| `"React root not found"` | App not running, wrong URL, or Vite HMR interfering | Use `scope capture --wait 2000` |\n| `"Component not in manifest"` | Manifest is stale | Run `scope manifest generate` first |\n| `"Manifest not found"` | Missing manifest | Run `scope init` or `scope manifest generate` |\n| `"requiredContexts missing"` | Component needs a provider | Add provider presets to `reactscope.config.json` |\n| Blank PNG / 16\xD76px renders | No globalCSS injected (common with Tailwind) | Set `components.wrappers.globalCSS` in config; run `scope doctor` to verify |\n| `"Invalid props JSON"` | Malformed JSON in `--props` | Use single outer quotes: `--props \'{"key":"val"}\'` |\n| `"SCOPE_CAPTURE_JSON not available"` | Scope runtime not injected before React loaded | Fixed in PR #83 \u2014 update CLI |\n| `"No React DevTools hook found"` | Hook instrumentation init order bug | Fixed in PR #83 \u2014 update CLI |\n| `"ERR_MODULE_NOT_FOUND"` after tokens commands | Old Node shebang in CLI binary | Fixed in PR #90 \u2014 CLI now uses `#!/usr/bin/env bun` |\n| `"version" field missing in tokens` | Token stub written by old `scope init` | Re-run `scope init --force` or add `"version": "1"` to token file |\n| `"unknown option --has-prop"` | Old CLI version | Fixed in PR #90 \u2014 update CLI |\n| Format alias error (`json`, `js`, `sass`, `tw`) | Wrong format name for `tokens export` | Use `flat-json`, `ts`, `scss`, `tailwind`; CLI shows "Did you mean?" hint |\n\n---\n\n## `reactscope.config.json`\n\n```json\n{\n "components": {\n "wrappers": {\n "globalCSS": ["src/styles.css"]\n }\n },\n "tokens": {\n "file": "reactscope.tokens.json"\n },\n "collections": [\n { "name": "Forms", "description": "Form inputs and controls", "patterns": ["src/forms/**"] },\n { "name": "Primitives", "patterns": ["src/primitives/**"] }\n ],\n "internalPatterns": ["src/internal/**"],\n "providers": {\n "theme": { "component": "ThemeProvider", "props": { "theme": "light" } },\n "router": { "component": "MemoryRouter", "props": { "initialEntries": ["/"] } }\n }\n}\n```\n\n**Built-in mock providers** (always available, no config needed):\n- `ThemeContext` \u2192 `{ theme: \'light\' }` (or `--theme <name>`)\n- `LocaleContext` \u2192 `{ locale: \'en-US\' }`\n\n---\n\n## What Scope Cannot Do\n\n- **Runtime state**: `useState` values after user interaction\n- **Network requests**: `fetch`, `XHR`, `WebSocket`\n- **User interactions**: click, type, hover, drag\n- **Auth/session-gated components**: components that redirect or throw without a session\n- **Server components (RSC)**: React Server Components\n- **Dynamic CSS**: CSS-in-JS styles computed at runtime from props Scope can\'t infer\n\n---\n\n## Version History\n\n| Version | Date | Summary |\n|---------|------|---------|\n| v1.0 | 2026-03-11 | Initial SKILL.md (PR #36) \u2014 manifest, render, capture, tree, report, tokens, ci commands |\n| v1.1 | 2026-03-11 | Updated through PR #82 \u2014 Phase 2 CLI commands complete |\n| v1.2 | 2026-03-13 | PRs #83\u2013#95: runtime injection fix, dogfooding fixes (12 bugs), `scope doctor`, `scope init` auto-manifest, globalCSS render warning, collections & internal components feature |\n';
1485
+ var SKILL_CONTENT = '# Scope \u2014 Agent Skill\n\n## TLDR\nScope is a React codebase introspection toolkit. Use it to answer questions about component structure, props, context dependencies, side effects, and visual output \u2014 without running the app.\n\n**When to reach for it:** Any task requiring "which components use X", "what props does Y accept", "render Z for visual verification", "does this component depend on a provider", or "what design tokens are in use".\n\n**Canonical agent workflow:**\n```\nscope get-skill > /tmp/scope-skill.md # bootstrap command semantics into agent context\nscope init --yes # scaffold config + auto-generate manifest\nscope doctor --json # validate config, tokens, globalCSS, manifest freshness, deps, Playwright\nscope manifest query --context ThemeContext --format json\nscope render all --format json --output-dir .reactscope/renders\nscope site build --output .reactscope/site\n```\n\n---\n\n---\n\n## Mental Model\n\nUnderstanding how Scope\'s data flows is the key to using it effectively as an agent.\n\n```\nSource TypeScript files\n \u2193 (ts-morph AST parse)\n manifest.json \u2190 structural facts: props, hooks, contexts, complexity\n \u2193 (esbuild + Playwright)\n renders/*.json \u2190 visual facts: screenshot, computedStyles, dom, a11y\n \u2193 (token engine)\n compliance-styles.json \u2190 audit facts: which CSS values match tokens, which don\'t\n \u2193 (site generator)\n site/ \u2190 human-readable docs combining all of the above\n```\n\nEach layer depends on the previous. If you\'re getting unexpected results, check whether the earlier layers are stale (run `scope doctor` to diagnose).\n\n---\n\n## The Four Subsystems\n\n### 1. Manifest (`scope manifest *`)\nThe manifest is a static analysis snapshot of your TypeScript source. It tells you:\n- What components exist, where they live, and how they\'re exported\n- What props each component accepts (types, defaults, required/optional)\n- What React hooks they call (`detectedHooks`)\n- What contexts they consume (`requiredContexts`) \u2014 must be provided for a render to succeed\n- Whether they compose other components (`composes` / `composedBy`)\n- Their **complexity class** \u2014 `"simple"` or `"complex"` \u2014 which determines the render engine\n\nThe manifest never runs your code. It only reads TypeScript. This means it\'s fast and safe, but it can\'t know about runtime values.\n\n### 2. Render Engine (`scope render *`)\nThe render engine compiles components with esbuild and renders them in Chromium (Playwright). Two paths exist:\n\n| Path | When | Speed | Capability |\n|------|------|-------|------------|\n| **Satori** | `complexityClass: "simple"` | ~8ms | Flexbox only, no JS, no CSS-in-JS |\n| **BrowserPool** | `complexityClass: "complex"` | ~200\u2013800ms | Full DOM, CSS, Tailwind, animations |\n\nMost real-world components route through BrowserPool. Scope defaults to `"complex"` when uncertain (safe fallback).\n\nEach render produces:\n- `screenshot` \u2014 retina-quality PNG (2\xD7 `deviceScaleFactor`; display at CSS px dimensions)\n- `width` / `height` \u2014 CSS pixel dimensions of the component root\n- `computedStyles` \u2014 per-node computed CSS keyed by `#node-0`, `#node-1`, etc.\n- `dom` \u2014 full DOM tree with bounding boxes (BrowserPool only)\n- `accessibility` \u2014 role, aria-name, violation list (BrowserPool only)\n- `renderTimeMs` \u2014 wall-clock render duration\n\n### 3. Scope Files (`.scope.tsx` / `.scope.ts`)\nScope files define **named rendering scenarios** and optional provider wrappers next to a component. They are the primary way to ensure `render all` produces meaningful screenshots.\n\nDiscovery is deterministic: for `Button.tsx`, Scope checks `Button.scope.tsx`, then `.scope.ts`, `.scope.jsx`, `.scope.js` in the same directory and uses the first file that exists.\n\n```tsx\n// Button.scope.tsx\nexport const scenarios = {\n default: { variant: \'primary\', children: \'Click me\' },\n ghost: { variant: \'ghost\', children: \'Cancel\' },\n danger: { variant: \'danger\', children: \'Delete\' },\n disabled: { variant: \'primary\', children: \'Disabled\', disabled: true },\n} satisfies Record<string, Record<string, unknown>>;\n```\n\nOptional wrapper:\n\n```tsx\nimport type { ReactNode } from \'react\';\nimport { ThemeProvider } from \'../providers/ThemeProvider\';\n\nexport function wrapper({ children }: { children: ReactNode }) {\n return <ThemeProvider theme="dark">{children}</ThemeProvider>;\n}\n```\n\nContract:\n- Export `scenarios` as a plain object of scenario-name \u2192 props-object, either as a named export or under `default.scenarios`\n- Export `wrapper` as a function, either as a named export or under `default.wrapper`\n- Non-object scenario values are skipped with a warning\n- If no scope file or no valid scenarios exist, Scope falls back to one bare render and inferred required-prop defaults\n- If 2+ scenarios exist, `render all` automatically runs a matrix and merges cells into each component JSON\n\nWhen a component renders blank with `{}` props, **the fix is usually to create a `.scope.tsx` file** with real props.\n\n### 4. Token Compliance\nThe compliance pipeline:\n1. `scope render all` captures `computedStyles` for every element in every component\n2. These are written to `.reactscope/compliance-styles.json`\n3. The token engine compares each computed CSS value against your `reactscope.tokens.json`\n4. `scope tokens compliance` reports the aggregate on-system percentage\n5. `scope ci` fails if the percentage is below `complianceThreshold` (default 90%)\n\n**On-system** means the value exactly matches a resolved token value. Off-system means it\'s a hardcoded value with no token backing it.\n\n---\n\n## Complexity Classes \u2014 Practical Guide\n\nThe `complexityClass` field determines which render engine runs. Scope auto-detects it, but agents should understand it:\n\n**`"simple"` components:**\n- Pure presentational, flexbox layout only\n- No CSS grid, no absolute/fixed/sticky positioning\n- No CSS animations, transitions, or transforms\n- No `className` values Scope can\'t statically trace (e.g. dynamic Tailwind classes)\n- Renders in ~8ms via Satori (SVG-based, no browser needed)\n\n**`"complex"` components:**\n- Anything using Tailwind (CSS injection required)\n- CSS grid, positioned elements, overflow, z-index\n- Components that read from context at render time\n- Any component Scope isn\'t sure about (conservative default)\n- Renders in ~200\u2013800ms via Playwright BrowserPool\n\nWhen in doubt: complex is always safe. Simple is an optimization.\n\n---\n\n## Required Contexts \u2014 Why Renders Fail\n\nIf `requiredContexts` is non-empty, the component calls `useContext` on one or more contexts. Without a provider, it will either render broken or throw entirely.\n\nTwo ways to fix:\n1. **Provider presets in config** (recommended): add provider names to `reactscope.config.json \u2192 components.wrappers.providers`\n2. **Scope file with wrapper**: wrap the component in a provider in the scenario itself\n\nBuilt-in mocks (always provided): `ThemeContext \u2192 { theme: \'light\' }`, `LocaleContext \u2192 { locale: \'en-US\' }`.\n\n---\n\n## `scope doctor` \u2014 Always Run This First\n\nBefore debugging any render issue, run:\n```bash\nscope doctor --json\n```\n\nIt checks:\n- `reactscope.config.json` is valid JSON\n- Token file exists and has a `version` field\n- Every path in `globalCSS` resolves on disk\n- Manifest is present and up to date (not stale relative to source)\n- Target-project dependencies are installed (`node_modules` exists)\n- Playwright Chromium is available for render/site/instrument commands\n\n**If `globalCSS` is empty or missing**: Tailwind styles won\'t apply to renders. Every component will look unstyled. This is the most common footgun. Fix: add your CSS entry file (the one with `@tailwind base; @tailwind components; @tailwind utilities;`) to `globalCSS` in config.\n\n---\n\n## Agent Decision Tree\n\n**"I want to know what props Component X accepts"**\n\u2192 `scope manifest get X --format json | jq \'.props\'`\n\n**"I want to know which components will break if I change a context"**\n\u2192 `scope manifest query --context MyContext --format json`\n\n**"I want to render a component to verify visual output"**\n\u2192 Create a `.scope.tsx` file with real props first, then `scope render component X --format json`\n\n**"I want to render all variants of a component"**\n\u2192 Define all variants in `.scope.tsx`, then `scope render all --format json --output-dir .reactscope/renders` (auto-matrix)\n\u2192 Or: `scope render matrix X --axes \'variant:primary,secondary,danger\' --format json`\n\n**"I want to audit token compliance"**\n\u2192 `scope render all` first (populates computedStyles), then `scope tokens compliance`\n\n**"Renders look unstyled / blank"**\n\u2192 Run `scope doctor` \u2014 likely missing `globalCSS`\n\u2192 If props are the issue: create/update the `.scope.tsx` file\n\n**"I want to understand blast radius of a token change"**\n\u2192 `scope tokens impact color.primary.500 --new-value \'#0077dd\'`\n\u2192 `scope tokens preview color.primary.500 --new-value \'#0077dd\'` for visual diff\n\n**"I need to set up Scope in a new project"**\n\u2192 `scope init --yes` (auto-detects Tailwind + CSS, generates manifest automatically)\n\u2192 `scope doctor` to validate\n\u2192 Create `.scope.tsx` files for key components\n\u2192 `scope render all`\n\n**"I want to profile a live SPA"**\n\u2192 Generate a manifest, then `scope instrument profile SearchPage --interaction \'[{"action":"click","target":"button"}]\'`\n\u2192 For auth/timing-heavy apps, follow `docs/profiling-production-spas.md`\n\n**"I want to run Scope in CI"**\n\u2192 `scope ci --json --output ci-result.json`\n\u2192 Exit code 0 = pass, non-zero = specific failure type\n\n---\n\n\n## Installation\n\nPublished package:\n\n```bash\nnpm install -g @agent-scope/cli # global\nnpm install --save-dev @agent-scope/cli # per-project\n```\n\nLocal-from-source quickstart for this repo:\n\n```bash\nbun install\nbun run build\nbunx playwright install chromium\ncd fixtures/tailwind-showcase\nbun install\n../../packages/cli/dist/cli.js init --yes\n../../packages/cli/dist/cli.js doctor --json\n../../packages/cli/dist/cli.js render all --format json --output-dir .reactscope/renders\n../../packages/cli/dist/cli.js site build --output .reactscope/site\n```\n\nBinary: `scope` (published install) or `packages/cli/dist/cli.js` (local build)\n\n---\n\n## Core Workflow\n\n```\nget-skill \u2192 init --yes \u2192 doctor --json \u2192 manifest query/get/list --format json \u2192 render all --format json \u2192 site build \u2192 instrument profile \u2192 ci\n```\n\n- **init**: Scaffold `reactscope.config.json` + token stub, auto-detect framework/globalCSS, **immediately runs `manifest generate`** so you see results right away.\n- **doctor**: Health-check command \u2014 validates config, token file, globalCSS presence, manifest staleness, target-project dependencies, and Playwright Chromium availability.\n- **generate**: Parse TypeScript AST and emit `.reactscope/manifest.json`. Run once per codebase change (or automatically via `scope init`).\n- **query / get / list**: Ask structural questions. No network required. Works from manifest alone. Supports filtering by `--collection` and `--internal`.\n- **render**: Produce PNGs of components via esbuild + Playwright (BrowserPool). Requires manifest for file paths. Auto-injects required prop defaults and globalCSS.\n- **token audit**: Validate design tokens via `@scope/tokens` CLI commands (`tokens list`, `tokens compliance`, `tokens impact`, `tokens preview`, `tokens export`).\n- **ci**: Run compliance checks and exit with code 0/1 for CI pipelines. `report pr-comment` posts results to GitHub PRs.\n\n---\n\n## Full CLI Reference\n\n### `scope init`\nScaffold config, detect framework, extract Tailwind tokens, detect globalCSS files, and **automatically run `scope manifest generate`**.\n\n```bash\nscope init\nscope init --force # overwrite existing config\n```\n\nAfter init completes, the manifest is already written \u2014 no manual `scope manifest generate` step needed.\n\n**Tailwind token extraction**: reads `tailwind.config.js`, extracts colors (with nested scale support), spacing, fontFamily, borderRadius. Stored in `reactscope.tokens.json`.\n\n**globalCSS detection**: checks 9 common patterns (`src/styles.css`, `src/index.css`, `app/globals.css`, etc.). Stored in `components.wrappers.globalCSS` in config.\n\n---\n\n### `scope doctor`\nValidate the Scope setup. Exits non-zero on errors, zero on warnings-only.\n\n```bash\nscope doctor --json\nscope doctor --json\n```\n\nChecks:\n- `config` \u2014 `reactscope.config.json` is valid JSON with required fields\n- `tokens` \u2014 token file is present and has a valid `version` field\n- `globalCSS` \u2014 globalCSS files listed in config exist on disk\n- `manifest` \u2014 manifest exists and is not stale (compares source file mtimes)\n- `dependencies` \u2014 `node_modules` exists in the target project root\n- `playwright` \u2014 Playwright Chromium is available before render/site/instrument\n\nIf `dependencies` fails, run `bun install` (or the package manager detected by your lockfile) in the target project root, then rerun `scope doctor --json`.\n\nIf `playwright` fails, run `bunx playwright install chromium`, then rerun `scope doctor --json` before retrying `scope render component`, `scope render all`, `scope site build`, or `scope instrument ...`.\n\n```\n$ scope doctor --json\nScope Doctor\n\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 [\u2713] config reactscope.config.json valid\n [\u2713] tokens Token file valid\n [\u2713] globalCSS 1 globalCSS file(s) present\n [!] manifest Manifest may be stale \u2014 5 source file(s) modified since last generate\n\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 1 warning(s) \u2014 everything works but could be better\n```\n\n---\n\n### `scope capture <url>`\nCapture a live React component tree from a running app URL.\n\n```bash\nscope capture http://localhost:3000\nscope capture http://localhost:3000 --output report.json --pretty\nscope capture http://localhost:3000 --timeout 15000 --wait 2000\n```\n\nFlags:\n| Flag | Type | Default | Description |\n|------|------|---------|-------------|\n| `-o, --output <path>` | string | stdout | Write JSON to file |\n| `--pretty` | bool | false | Pretty-print JSON |\n| `--timeout <ms>` | number | 10000 | Max wait for React to mount |\n| `--wait <ms>` | number | 0 | Additional wait after page load |\n\nOutput (stdout): serialized PageReport JSON (or path when `--output` is set)\n\n---\n\n### `scope tree <url>`\nPrint the React component tree from a live URL.\n\n```bash\nscope tree http://localhost:3000\nscope tree http://localhost:3000 --depth 3 --show-props --show-hooks\n```\n\nFlags:\n| Flag | Type | Default | Description |\n|------|------|---------|-------------|\n| `--depth <n>` | number | unlimited | Max depth to display |\n| `--show-props` | bool | false | Include prop names next to components |\n| `--show-hooks` | bool | false | Show hook counts per component |\n| `--timeout <ms>` | number | 10000 | Max wait for React to mount |\n| `--wait <ms>` | number | 0 | Additional wait after page load |\n\n---\n\n### `scope report <url>`\nCapture and print a human-readable summary of a React app.\n\n```bash\nscope report http://localhost:3000\nscope report http://localhost:3000 --json\n```\n\nFlags:\n| Flag | Type | Default | Description |\n|------|------|---------|-------------|\n| `--json` | bool | false | Emit structured JSON instead of text |\n| `--timeout <ms>` | number | 10000 | Max wait for React to mount |\n| `--wait <ms>` | number | 0 | Additional wait after page load |\n\n---\n\n### `scope report baseline`\nSave a baseline snapshot for future diff comparisons.\n\n```bash\nscope report baseline\nscope report baseline --output baselines/my-baseline.json\n```\n\n---\n\n### `scope report diff`\nDiff the current app state against a saved baseline.\n\n```bash\nscope report diff\nscope report diff --baseline baselines/my-baseline.json\nscope report diff --json\n```\n\n---\n\n### `scope report pr-comment`\nPost a Scope CI report as a GitHub PR comment. Used in CI pipelines via the reusable `scope-ci` workflow.\n\n```bash\nscope report pr-comment --report-path scope-ci-report.json\n```\n\nRequires `GITHUB_TOKEN`, `GITHUB_REPOSITORY`, and `GITHUB_PR_NUMBER` in environment.\n\n---\n\n### `scope manifest generate`\nScan source files and write `.reactscope/manifest.json`.\n\n```bash\nscope manifest generate\nscope manifest generate --root ./packages/ui\nscope manifest generate --include "src/**/*.tsx" --exclude "**/*.test.tsx"\nscope manifest generate --output custom/manifest.json\n```\n\nFlags:\n| Flag | Type | Default | Description |\n|------|------|---------|-------------|\n| `--root <path>` | string | cwd | Project root directory |\n| `--output <path>` | string | `.reactscope/manifest.json` | Output path |\n| `--include <globs>` | string | `src/**/*.tsx,src/**/*.ts` | Comma-separated include globs |\n| `--exclude <globs>` | string | `**/node_modules/**,...` | Comma-separated exclude globs |\n\n---\n\n### `scope manifest list`\nList all components in the manifest.\n\n```bash\nscope manifest list\nscope manifest list --filter "Button*"\nscope manifest list --format json\nscope manifest list --collection Forms # filter to named collection\nscope manifest list --internal # only internal components\nscope manifest list --no-internal # hide internal components\n```\n\nFlags:\n| Flag | Type | Default | Description |\n|------|------|---------|-------------|\n| `--format <fmt>` | `json\\|table` | auto (TTY\u2192table, pipe\u2192json) | Output format |\n| `--filter <glob>` | string | \u2014 | Filter component names by glob |\n| `--collection <name>` | string | \u2014 | Filter to named collection |\n| `--internal` | bool | false | Show only internal components |\n| `--no-internal` | bool | false | Hide internal components |\n| `--manifest <path>` | string | `.reactscope/manifest.json` | Manifest path |\n\nTTY table output (includes COLLECTION and INTERNAL columns):\n```\nNAME FILE COMPLEXITY HOOKS CONTEXTS COLLECTION INTERNAL\n------------ --------------------------- ---------- ----- -------- ---------- --------\nButton src/components/Button.tsx simple 1 0 \u2014 no\nThemeToggle src/components/Toggle.tsx complex 3 1 Forms no\n```\n\n---\n\n### `scope manifest get <name>`\nGet full details of a single component.\n\n```bash\nscope manifest get Button\nscope manifest get Button --format json\n```\n\nJSON output includes `collection` and `internal` fields:\n```json\n{\n "name": "Button",\n "filePath": "src/components/Button.tsx",\n "collection": "Primitives",\n "internal": false,\n ...\n}\n```\n\n---\n\n### `scope manifest query`\nQuery components by attributes.\n\n```bash\nscope manifest query --context ThemeContext\nscope manifest query --hook useEffect\nscope manifest query --complexity complex\nscope manifest query --side-effects\nscope manifest query --has-fetch\nscope manifest query --has-prop <propName>\nscope manifest query --composed-by <ComponentName>\nscope manifest query --internal\nscope manifest query --collection Forms\nscope manifest query --context ThemeContext --format json\n```\n\nFlags:\n| Flag | Type | Default | Description |\n|------|------|---------|-------------|\n| `--context <name>` | string | \u2014 | Find components consuming a context by name |\n| `--hook <name>` | string | \u2014 | Find components using a specific hook |\n| `--complexity <class>` | `simple\\|complex` | \u2014 | Filter by complexity class |\n| `--side-effects` | bool | false | Any side effects detected |\n| `--has-fetch` | bool | false | Components with fetch calls specifically |\n| `--has-prop <name>` | string | \u2014 | Components that accept a specific prop |\n| `--composed-by <name>` | string | \u2014 | Components rendered inside a specific parent |\n| `--internal` | bool | false | Only internal components |\n| `--collection <name>` | string | \u2014 | Filter to named collection |\n| `--format <fmt>` | `json\\|table` | auto | Output format |\n| `--manifest <path>` | string | `.reactscope/manifest.json` | Manifest path |\n\n---\n\n### `scope render <component>`\nRender a single component to PNG (TTY) or JSON (pipe).\n\n**Auto prop defaults**: if `--props` is omitted, Scope injects sensible defaults so required props don\'t produce blank renders: strings/nodes \u2192 component name, unions \u2192 first value, booleans \u2192 `false`, numbers \u2192 `0`.\n\n**globalCSS auto-injection**: reads `components.wrappers.globalCSS` from config and compiles/injects CSS (supports Tailwind v3 via PostCSS) into the render harness. A warning is printed to stderr if no globalCSS is configured (common cause of unstyled renders).\n\n```bash\nscope render component Button\nscope render component Button --props \'{"variant":"primary","children":"Click me"}\'\nscope render component Button --viewport 375x812\nscope render component Button --output button.png\nscope render component Button --format json\n```\n\nFlags:\n| Flag | Type | Default | Description |\n|------|------|---------|-------------|\n| `--props <json>` | string | `{}` | Inline props as JSON string |\n| `--viewport <WxH>` | string | `375x812` | Viewport size |\n| `--theme <name>` | string | \u2014 | Theme name from token system |\n| `-o, --output <path>` | string | \u2014 | Write PNG to specific path |\n| `--format <fmt>` | `png\\|json` | auto (TTY\u2192file, pipe\u2192json) | Output format |\n| `--manifest <path>` | string | `.reactscope/manifest.json` | Manifest path |\n\n---\n\n### `scope render matrix <component>`\nRender across a Cartesian product of prop axes. Accepts both `key:v1,v2` and `{"key":["v1","v2"]}` JSON format for `--axes`.\n\n```bash\nscope render matrix Button --axes \'variant:primary,secondary,danger\'\nscope render matrix Button --axes \'{"variant":["primary","secondary"]}\'\nscope render matrix Button --axes \'variant:primary,secondary size:sm,md,lg\'\nscope render matrix Button --sprite button-matrix.png --format json\n```\n\n---\n\n### `scope render all`\nRender every component in the manifest.\n\n```bash\nscope render all\nscope render all --concurrency 4 --output-dir renders/\n```\n\nHandles imports of CSS files in components (maps to empty loader so styles are injected at page level). SVG and font imports are handled via dataurl loaders.\n\n---\n\n### `scope instrument tree`\nCapture the live React component tree with instrumentation metadata.\n\n```bash\nscope instrument tree http://localhost:3000\nscope instrument tree http://localhost:3000 --depth 5 --show-props\n```\n\n**Implementation note**: uses a fresh `chromium.launch()` + `newContext()` + `newPage()` per call (not BrowserPool), with `addInitScript` called before `setContent` to ensure the Scope runtime is injected at document-start before React loads.\n\n---\n\n### `scope instrument hooks`\nProfile hook execution in live components.\n\n```bash\nscope instrument hooks http://localhost:3000\nscope instrument hooks http://localhost:3000 --component Button\n```\n\n**Implementation note**: requires `addInitScript({ content: getBrowserEntryScript() })` before `setContent` so `__REACT_DEVTOOLS_GLOBAL_HOOK__` is present when React loads its renderer.\n\n---\n\n### `scope instrument profile`\nProfile render performance of live components.\n\n```bash\nscope instrument profile SearchPage --interaction \'[{"action":"click","target":"button"}]\'\n```\n\n---\n\n### `scope instrument renders`\nRe-render causality analysis \u2014 what triggered each render.\n\n```bash\nscope instrument renders http://localhost:3000\n```\n\n---\n\n### `scope tokens get <name>`\nGet details of a single design token.\n\n### `scope tokens list`\nList all tokens. Token file must have a `version` field (written by `scope init`).\n\n```bash\nscope tokens list\nscope tokens list --type color\nscope tokens list --format json\n```\n\n### `scope tokens search <query>`\nFull-text search across token names/values.\n\n### `scope tokens resolve <value>`\nResolve a CSS value or alias back to its token name.\n\n### `scope tokens validate`\nValidate token file schema.\n\n### `scope tokens compliance`\nCheck rendered components for design token compliance.\n\n```bash\nscope tokens compliance\nscope tokens compliance --threshold 95\n```\n\n### `scope tokens impact <token>`\nAnalyze impact of changing a token \u2014 which components use it.\n\n```bash\nscope tokens impact --token color.primary.500\n```\n\n### `scope tokens preview <token>`\nPreview a token value change visually before committing.\n\n### `scope tokens export`\nExport tokens in multiple formats.\n\n```bash\nscope tokens export --format flat-json\nscope tokens export --format css\nscope tokens export --format scss\nscope tokens export --format ts\nscope tokens export --format tailwind\nscope tokens export --format style-dictionary\n```\n\n**Format aliases** (auto-corrected with "Did you mean?" hint):\n- `json` \u2192 `flat-json`\n- `js` \u2192 `ts`\n- `sass` \u2192 `scss`\n- `tw` \u2192 `tailwind`\n\n---\n\n### `scope ci`\nRun all CI checks (compliance, accessibility, console errors) and exit 0/1.\n\n```bash\nscope ci\nscope ci --json\nscope ci --threshold 90 # compliance threshold (default: 90)\n```\n\n```\n$ scope ci --json\n\u2192 CI passed in 3.2s\n\u2192 Compliance 100.0% >= threshold 90.0% \u2705\n\u2192 Accessibility audit not yet implemented \u2014 skipped \u2705\n\u2192 No console errors detected \u2705\n\u2192 Exit code 0\n```\n\nThe `scope-ci` **reusable GitHub Actions workflow** is available at `.github/workflows/scope-ci.yml` and can be included in any repo\'s CI to run `scope ci` and post results as a PR comment via `scope report pr-comment`.\n\n---\n\n### `scope site build`\nGenerate a static HTML component gallery site from the manifest.\n\n```bash\nscope site build\nscope site build --output ./dist/site\n```\n\n**Collections support**: components are grouped under named collection sections in the sidebar and index grid. Internal components are hidden from the sidebar and card grid but appear in composition detail sections with an `internal` badge.\n\n**Collection display rules**:\n- Sidebar: one section divider per collection + an "Ungrouped" section; internal components excluded\n- Index page: named sections with heading + optional description; internal components excluded\n- Component detail page: Composes/Composed By lists ALL components including internal ones (with subtle badge)\n- Falls back to flat list when no collections configured (backwards-compatible)\n\n### `scope site serve`\nServe the generated site locally.\n\n```bash\nscope site serve\nscope site serve --port 4000\n```\n\n---\n\n## Collections & Internal Components\n\nComponents can be organized into named **collections** and flagged as **internal** (library implementation details not shown in the public gallery).\n\n### Defining collections\n\n**1. TSDoc tag** (highest precedence):\n```tsx\n/**\n * @collection Forms\n */\nexport function Input() { ... }\n```\n\n**2. `.scope.ts` co-located file**:\n```ts\n// Input.scope.ts\nexport const collection = "Forms"\n```\n\n**3. Config-level glob patterns**:\n```json\n// reactscope.config.json\n{\n "collections": [\n { "name": "Forms", "description": "Form inputs and controls", "patterns": ["src/forms/**"] },\n { "name": "Primitives", "patterns": ["src/primitives/**"] }\n ]\n}\n```\n\nResolution precedence: TSDoc `@collection` > `.scope.ts` export > config pattern.\n\n### Flagging internal components\n\n**TSDoc tag**:\n```tsx\n/**\n * @internal\n */\nexport function InternalHelperButton() { ... }\n```\n\n**Config glob patterns**:\n```json\n{\n "internalPatterns": ["src/internal/**", "src/**/*Internal*"]\n}\n```\n\n---\n\n## Manifest Output Schema\n\nFile: `.reactscope/manifest.json`\n\n```typescript\n{\n version: "0.1",\n generatedAt: string, // ISO 8601\n collections: CollectionConfig[], // echoes config.collections, [] when not set\n components: Record<string, ComponentDescriptor>,\n tree: Record<string, { children: string[], parents: string[] }>\n}\n```\n\n### `ComponentDescriptor` fields\n\n| Field | Type | Description |\n|-------|------|-------------|\n| `filePath` | `string` | Relative path from project root to source file |\n| `exportType` | `"named" \\| "default" \\| "none"` | How the component is exported |\n| `displayName` | `string` | `displayName` if set, else function/class name |\n| `collection` | `string?` | Resolved collection name (`undefined` = ungrouped) |\n| `internal` | `boolean` | `true` if flagged as internal (default: `false`) |\n| `props` | `Record<string, PropDescriptor>` | Extracted prop types keyed by prop name |\n| `composes` | `string[]` | Components this one renders in its JSX |\n| `composedBy` | `string[]` | Components that render this one in their JSX |\n| `complexityClass` | `"simple" \\| "complex"` | Render path: simple = Satori-safe, complex = requires BrowserPool |\n| `requiredContexts` | `string[]` | React context names consumed |\n| `detectedHooks` | `string[]` | All hooks called, sorted alphabetically |\n| `sideEffects` | `SideEffects` | Side effect categories detected |\n| `memoized` | `boolean` | Wrapped with `React.memo` |\n| `forwardedRef` | `boolean` | Wrapped with `React.forwardRef` |\n| `hocWrappers` | `string[]` | HOC wrapper names (excluding memo/forwardRef) |\n| `loc` | `{ start: number, end: number }` | Line numbers in source file |\n\n---\n\n## Common Agent Workflows\n\n### Structural queries\n\n```bash\n# Which components use ThemeContext?\nscope manifest query --context ThemeContext\n\n# What props does Button accept?\nscope manifest get Button --format json | jq \'.props\'\n\n# Which components are safe to render without a provider?\nscope manifest query --complexity simple # + check requiredContexts === []\n\n# Show all components with side effects\nscope manifest query --side-effects\n\n# Which components make fetch calls?\nscope manifest query --has-fetch\n\n# Which components use useEffect?\nscope manifest query --hook useEffect\n\n# Which components accept a disabled prop?\nscope manifest query --has-prop disabled\n\n# Which components are composed inside Modal?\nscope manifest query --composed-by Modal\n\n# All components in the Forms collection\nscope manifest list --collection Forms\n\n# All internal components (library implementation details)\nscope manifest list --internal\n\n# Public components only (hide internals)\nscope manifest list --no-internal\n```\n\n### Render workflows\n\n```bash\n# Render Button in all variants (auto-defaults props if not provided)\nscope render matrix Button --axes \'variant:primary,secondary,danger\'\n\n# Render with JSON axes format\nscope render matrix Button --axes \'{"variant":["primary","secondary"]}\'\n\n# Render with explicit props\nscope render component Button --props \'{"variant":"primary","disabled":true}\'\n\n# Render all components (handles CSS/SVG/font imports automatically)\nscope render all --concurrency 8\n\n# Get render as JSON\nscope render component Button --format json | jq \'.screenshot\' | base64 -d > button.png\n```\n\n### Token workflows\n\n```bash\n# List all tokens\nscope tokens list\n\n# Check compliance\nscope tokens compliance --threshold 95\n\n# See what a token change impacts\nscope tokens impact --token color.primary.500\n\n# Export for Tailwind\nscope tokens export --format tailwind\n```\n\n### CI workflow\n\n```bash\n# Full compliance check\nscope ci --json\n\n# In GitHub Actions \u2014 use the reusable workflow\n# .github/workflows/ci.yml:\n# uses: FlatFilers/Scope/.github/workflows/scope-ci.yml@main\n```\n\n---\n\n## Error Patterns\n\n| Error | Cause | Fix |\n|-------|-------|-----|\n| `"React root not found"` | App not running, wrong URL, or Vite HMR interfering | Use `scope capture --wait 2000` |\n| `"Component not in manifest"` | Manifest is stale | Run `scope manifest generate` first |\n| `"Manifest not found"` | Missing manifest | Run `scope init` or `scope manifest generate` |\n| `"requiredContexts missing"` | Component needs a provider | Add provider presets to `reactscope.config.json` |\n| Blank PNG / 16\xD76px renders | No globalCSS injected (common with Tailwind) | Set `components.wrappers.globalCSS` in config; run `scope doctor` to verify |\n| `PLAYWRIGHT_BROWSERS_MISSING` / `browserType.launch: Executable doesn\'t exist` | Playwright Chromium is not installed in this sandbox | Run `bunx playwright install chromium`, then `scope doctor --json`, then retry render/site/instrument |\n| `TARGET_PROJECT_DEPENDENCIES_MISSING` / `Could not resolve "react"` | Target project dependencies are missing | Run `bun install` (or the detected package manager) in the target project root, then `scope doctor --json` |\n| `"Invalid props JSON"` | Malformed JSON in `--props` | Use single outer quotes: `--props \'{"key":"val"}\'` |\n| `"SCOPE_CAPTURE_JSON not available"` | Scope runtime not injected before React loaded | Fixed in PR #83 \u2014 update CLI |\n| `"No React DevTools hook found"` | Hook instrumentation init order bug | Fixed in PR #83 \u2014 update CLI |\n| `"ERR_MODULE_NOT_FOUND"` after tokens commands | Old Node shebang in CLI binary | Fixed in PR #90 \u2014 CLI now uses `#!/usr/bin/env bun` |\n| `"version" field missing in tokens` | Token stub written by old `scope init` | Re-run `scope init --force` or add `"version": "1"` to token file |\n| `"unknown option --has-prop"` | Old CLI version | Fixed in PR #90 \u2014 update CLI |\n| Format alias error (`json`, `js`, `sass`, `tw`) | Wrong format name for `tokens export` | Use `flat-json`, `ts`, `scss`, `tailwind`; CLI shows "Did you mean?" hint |\n\n---\n\n## `reactscope.config.json`\n\n```json\n{\n "components": {\n "wrappers": {\n "globalCSS": ["src/styles.css"]\n }\n },\n "tokens": {\n "file": "reactscope.tokens.json"\n },\n "collections": [\n { "name": "Forms", "description": "Form inputs and controls", "patterns": ["src/forms/**"] },\n { "name": "Primitives", "patterns": ["src/primitives/**"] }\n ],\n "internalPatterns": ["src/internal/**"],\n "providers": {\n "theme": { "component": "ThemeProvider", "props": { "theme": "light" } },\n "router": { "component": "MemoryRouter", "props": { "initialEntries": ["/"] } }\n }\n}\n```\n\n**Built-in mock providers** (always available, no config needed):\n- `ThemeContext` \u2192 `{ theme: \'light\' }` (or `--theme <name>`)\n- `LocaleContext` \u2192 `{ locale: \'en-US\' }`\n\n---\n\n## What Scope Cannot Do\n\n- **Runtime state**: `useState` values after user interaction\n- **Network requests**: `fetch`, `XHR`, `WebSocket`\n- **User interactions**: click, type, hover, drag\n- **Auth/session-gated components**: components that redirect or throw without a session\n- **Server components (RSC)**: React Server Components\n- **Dynamic CSS**: CSS-in-JS styles computed at runtime from props Scope can\'t infer\n\n---\n\n## Version History\n\n| Version | Date | Summary |\n|---------|------|---------|\n| v1.0 | 2026-03-11 | Initial SKILL.md (PR #36) \u2014 manifest, render, capture, tree, report, tokens, ci commands |\n| v1.1 | 2026-03-11 | Updated through PR #82 \u2014 Phase 2 CLI commands complete |\n| v1.2 | 2026-03-13 | PRs #83\u2013#95: runtime injection fix, dogfooding fixes (12 bugs), `scope doctor`, `scope init` auto-manifest, globalCSS render warning, collections & internal components feature |\n';
1236
1486
 
1237
1487
  // src/get-skill-command.ts
1238
1488
  function createGetSkillCommand() {
1239
1489
  return new Command3("get-skill").description(
1240
- 'Print the embedded Scope SKILL.md to stdout.\n\nAgents: pipe this command into your context loader to bootstrap Scope knowledge.\nThe skill covers: when to use each command, config requirements, output format,\nrender engine selection, and common failure modes.\n\nEMBEDDED AT BUILD TIME \u2014 works in any install context (global npm, npx, local).\n\nExamples:\n scope get-skill # raw markdown to stdout\n scope get-skill --json # { "skill": "..." } for structured ingestion\n scope get-skill | head -50 # preview the skill\n scope get-skill > /tmp/SKILL.md # save locally'
1490
+ 'Print the embedded Scope SKILL.md to stdout.\n\nAgents: load this first, then follow the canonical workflow in the skill:\n init --yes \u2192 doctor --json \u2192 manifest --format json \u2192 render all --format json \u2192 site build\nThe skill documents .scope.tsx discovery/exports, config paths, profiler usage,\nand failure recovery.\n\nEMBEDDED AT BUILD TIME \u2014 works in any install context (global npm, npx, local).\n\nExamples:\n scope get-skill # raw markdown to stdout\n scope get-skill --json # { "skill": "..." } for structured ingestion\n scope get-skill | head -50 # preview the skill\n scope get-skill > /tmp/SKILL.md # save locally'
1241
1491
  ).option("--json", "Wrap output in JSON { skill: string } instead of raw markdown").action((opts) => {
1242
1492
  if (opts.json) {
1243
1493
  process.stdout.write(`${JSON.stringify({ skill: SKILL_CONTENT }, null, 2)}
@@ -1249,15 +1499,15 @@ function createGetSkillCommand() {
1249
1499
  }
1250
1500
 
1251
1501
  // src/init/index.ts
1252
- import { appendFileSync, existsSync as existsSync5, mkdirSync, readFileSync as readFileSync5, writeFileSync as writeFileSync3 } from "fs";
1253
- import { join as join3 } from "path";
1502
+ import { appendFileSync, existsSync as existsSync6, mkdirSync, readFileSync as readFileSync6, writeFileSync as writeFileSync3 } from "fs";
1503
+ import { join as join4 } from "path";
1254
1504
  import * as readline from "readline";
1255
1505
 
1256
1506
  // src/init/detect.ts
1257
- import { existsSync as existsSync4, readdirSync as readdirSync2, readFileSync as readFileSync4 } from "fs";
1258
- import { join as join2 } from "path";
1507
+ import { existsSync as existsSync5, readdirSync as readdirSync2, readFileSync as readFileSync5 } from "fs";
1508
+ import { join as join3 } from "path";
1259
1509
  function hasConfigFile(dir, stem) {
1260
- if (!existsSync4(dir)) return false;
1510
+ if (!existsSync5(dir)) return false;
1261
1511
  try {
1262
1512
  const entries = readdirSync2(dir);
1263
1513
  return entries.some((f) => f === stem || f.startsWith(`${stem}.`));
@@ -1267,7 +1517,7 @@ function hasConfigFile(dir, stem) {
1267
1517
  }
1268
1518
  function readSafe(path) {
1269
1519
  try {
1270
- return readFileSync4(path, "utf-8");
1520
+ return readFileSync5(path, "utf-8");
1271
1521
  } catch {
1272
1522
  return null;
1273
1523
  }
@@ -1279,16 +1529,16 @@ function detectFramework(rootDir, packageDeps) {
1279
1529
  if ("react-scripts" in packageDeps) return "cra";
1280
1530
  return "unknown";
1281
1531
  }
1282
- function detectPackageManager(rootDir) {
1283
- if (existsSync4(join2(rootDir, "bun.lock"))) return "bun";
1284
- if (existsSync4(join2(rootDir, "yarn.lock"))) return "yarn";
1285
- if (existsSync4(join2(rootDir, "pnpm-lock.yaml"))) return "pnpm";
1286
- if (existsSync4(join2(rootDir, "package-lock.json"))) return "npm";
1532
+ function detectPackageManager2(rootDir) {
1533
+ if (existsSync5(join3(rootDir, "bun.lock"))) return "bun";
1534
+ if (existsSync5(join3(rootDir, "yarn.lock"))) return "yarn";
1535
+ if (existsSync5(join3(rootDir, "pnpm-lock.yaml"))) return "pnpm";
1536
+ if (existsSync5(join3(rootDir, "package-lock.json"))) return "npm";
1287
1537
  return "npm";
1288
1538
  }
1289
1539
  function detectTypeScript(rootDir) {
1290
- const candidate = join2(rootDir, "tsconfig.json");
1291
- if (existsSync4(candidate)) {
1540
+ const candidate = join3(rootDir, "tsconfig.json");
1541
+ if (existsSync5(candidate)) {
1292
1542
  return { typescript: true, tsconfigPath: candidate };
1293
1543
  }
1294
1544
  return { typescript: false, tsconfigPath: null };
@@ -1300,8 +1550,8 @@ function detectComponentPatterns(rootDir, typescript) {
1300
1550
  const ext = typescript ? "tsx" : "jsx";
1301
1551
  const altExt = typescript ? "jsx" : "jsx";
1302
1552
  for (const dir of COMPONENT_DIRS) {
1303
- const absDir = join2(rootDir, dir);
1304
- if (!existsSync4(absDir)) continue;
1553
+ const absDir = join3(rootDir, dir);
1554
+ if (!existsSync5(absDir)) continue;
1305
1555
  let hasComponents = false;
1306
1556
  try {
1307
1557
  const entries = readdirSync2(absDir, { withFileTypes: true });
@@ -1312,7 +1562,7 @@ function detectComponentPatterns(rootDir, typescript) {
1312
1562
  hasComponents = entries.some(
1313
1563
  (e) => e.isDirectory() && (() => {
1314
1564
  try {
1315
- return readdirSync2(join2(absDir, e.name)).some(
1565
+ return readdirSync2(join3(absDir, e.name)).some(
1316
1566
  (f) => COMPONENT_EXTS.some((x) => f.endsWith(x))
1317
1567
  );
1318
1568
  } catch {
@@ -1349,12 +1599,37 @@ var GLOBAL_CSS_CANDIDATES = [
1349
1599
  "styles/index.css"
1350
1600
  ];
1351
1601
  function detectGlobalCSSFiles(rootDir) {
1352
- return GLOBAL_CSS_CANDIDATES.filter((rel) => existsSync4(join2(rootDir, rel)));
1602
+ return GLOBAL_CSS_CANDIDATES.filter((rel) => existsSync5(join3(rootDir, rel)));
1353
1603
  }
1354
1604
  var TAILWIND_STEMS = ["tailwind.config"];
1355
1605
  var CSS_EXTS = [".css", ".scss", ".sass", ".less"];
1356
1606
  var THEME_SUFFIXES = [".theme.ts", ".theme.js", ".theme.tsx"];
1357
1607
  var CSS_CUSTOM_PROPS_RE = /:root\s*\{[^}]*--[a-zA-Z]/;
1608
+ var TAILWIND_V4_THEME_RE = /@theme\s*(?:inline\s*)?\{[^}]*--[a-zA-Z]/;
1609
+ var MAX_SCAN_DEPTH = 4;
1610
+ var SKIP_CSS_NAMES = ["compiled", ".min."];
1611
+ function collectCSSFiles(dir, depth) {
1612
+ if (depth > MAX_SCAN_DEPTH) return [];
1613
+ const results = [];
1614
+ try {
1615
+ const entries = readdirSync2(dir, { withFileTypes: true });
1616
+ for (const entry of entries) {
1617
+ if (entry.name === "node_modules" || entry.name === "dist" || entry.name === ".next") {
1618
+ continue;
1619
+ }
1620
+ const full = join3(dir, entry.name);
1621
+ if (entry.isFile() && CSS_EXTS.some((x) => entry.name.endsWith(x))) {
1622
+ if (!SKIP_CSS_NAMES.some((skip) => entry.name.includes(skip))) {
1623
+ results.push(full);
1624
+ }
1625
+ } else if (entry.isDirectory()) {
1626
+ results.push(...collectCSSFiles(full, depth + 1));
1627
+ }
1628
+ }
1629
+ } catch {
1630
+ }
1631
+ return results;
1632
+ }
1358
1633
  function detectTokenSources(rootDir) {
1359
1634
  const sources = [];
1360
1635
  for (const stem of TAILWIND_STEMS) {
@@ -1363,44 +1638,65 @@ function detectTokenSources(rootDir) {
1363
1638
  const entries = readdirSync2(rootDir);
1364
1639
  const match = entries.find((f) => f === stem || f.startsWith(`${stem}.`));
1365
1640
  if (match) {
1366
- sources.push({ kind: "tailwind-config", path: join2(rootDir, match) });
1641
+ sources.push({ kind: "tailwind-config", path: join3(rootDir, match) });
1367
1642
  }
1368
1643
  } catch {
1369
1644
  }
1370
1645
  }
1371
1646
  }
1372
- const srcDir = join2(rootDir, "src");
1373
- const dirsToScan = existsSync4(srcDir) ? [srcDir] : [];
1374
- for (const scanDir of dirsToScan) {
1375
- try {
1376
- const entries = readdirSync2(scanDir, { withFileTypes: true });
1377
- for (const entry of entries) {
1378
- if (entry.isFile() && CSS_EXTS.some((x) => entry.name.endsWith(x))) {
1379
- const filePath = join2(scanDir, entry.name);
1380
- const content = readSafe(filePath);
1381
- if (content !== null && CSS_CUSTOM_PROPS_RE.test(content)) {
1382
- sources.push({ kind: "css-custom-properties", path: filePath });
1383
- }
1384
- }
1647
+ const srcDir = join3(rootDir, "src");
1648
+ if (existsSync5(srcDir)) {
1649
+ const cssFiles = collectCSSFiles(srcDir, 0);
1650
+ const seen = /* @__PURE__ */ new Set();
1651
+ for (const filePath of cssFiles) {
1652
+ const content = readSafe(filePath);
1653
+ if (content === null) continue;
1654
+ if (TAILWIND_V4_THEME_RE.test(content) && !seen.has(filePath)) {
1655
+ sources.push({ kind: "tailwind-v4-theme", path: filePath });
1656
+ seen.add(filePath);
1657
+ }
1658
+ if (CSS_CUSTOM_PROPS_RE.test(content) && !seen.has(filePath)) {
1659
+ sources.push({ kind: "css-custom-properties", path: filePath });
1660
+ seen.add(filePath);
1385
1661
  }
1386
- } catch {
1387
1662
  }
1388
1663
  }
1389
- if (existsSync4(srcDir)) {
1390
- try {
1391
- const entries = readdirSync2(srcDir);
1392
- for (const entry of entries) {
1393
- if (THEME_SUFFIXES.some((s) => entry.endsWith(s))) {
1394
- sources.push({ kind: "theme-file", path: join2(srcDir, entry) });
1395
- }
1664
+ for (const tokenDir of ["tokens", "styles", "theme"]) {
1665
+ const dir = join3(rootDir, tokenDir);
1666
+ if (!existsSync5(dir)) continue;
1667
+ const cssFiles = collectCSSFiles(dir, 0);
1668
+ for (const filePath of cssFiles) {
1669
+ const content = readSafe(filePath);
1670
+ if (content === null) continue;
1671
+ if (TAILWIND_V4_THEME_RE.test(content)) {
1672
+ sources.push({ kind: "tailwind-v4-theme", path: filePath });
1673
+ } else if (CSS_CUSTOM_PROPS_RE.test(content)) {
1674
+ sources.push({ kind: "css-custom-properties", path: filePath });
1396
1675
  }
1397
- } catch {
1398
1676
  }
1399
1677
  }
1678
+ if (existsSync5(srcDir)) {
1679
+ const scanThemeFiles = (dir, depth) => {
1680
+ if (depth > MAX_SCAN_DEPTH) return;
1681
+ try {
1682
+ const entries = readdirSync2(dir, { withFileTypes: true });
1683
+ for (const entry of entries) {
1684
+ if (entry.name === "node_modules" || entry.name === "dist") continue;
1685
+ if (entry.isFile() && THEME_SUFFIXES.some((s) => entry.name.endsWith(s))) {
1686
+ sources.push({ kind: "theme-file", path: join3(dir, entry.name) });
1687
+ } else if (entry.isDirectory()) {
1688
+ scanThemeFiles(join3(dir, entry.name), depth + 1);
1689
+ }
1690
+ }
1691
+ } catch {
1692
+ }
1693
+ };
1694
+ scanThemeFiles(srcDir, 0);
1695
+ }
1400
1696
  return sources;
1401
1697
  }
1402
1698
  function detectProject(rootDir) {
1403
- const pkgPath = join2(rootDir, "package.json");
1699
+ const pkgPath = join3(rootDir, "package.json");
1404
1700
  let packageDeps = {};
1405
1701
  const pkgContent = readSafe(pkgPath);
1406
1702
  if (pkgContent !== null) {
@@ -1415,7 +1711,7 @@ function detectProject(rootDir) {
1415
1711
  }
1416
1712
  const framework = detectFramework(rootDir, packageDeps);
1417
1713
  const { typescript, tsconfigPath } = detectTypeScript(rootDir);
1418
- const packageManager = detectPackageManager(rootDir);
1714
+ const packageManager = detectPackageManager2(rootDir);
1419
1715
  const componentPatterns = detectComponentPatterns(rootDir, typescript);
1420
1716
  const tokenSources = detectTokenSources(rootDir);
1421
1717
  const globalCSSFiles = detectGlobalCSSFiles(rootDir);
@@ -1470,9 +1766,9 @@ function createRL() {
1470
1766
  });
1471
1767
  }
1472
1768
  async function ask(rl, question) {
1473
- return new Promise((resolve19) => {
1769
+ return new Promise((resolve21) => {
1474
1770
  rl.question(question, (answer) => {
1475
- resolve19(answer.trim());
1771
+ resolve21(answer.trim());
1476
1772
  });
1477
1773
  });
1478
1774
  }
@@ -1481,9 +1777,9 @@ async function askWithDefault(rl, label, defaultValue) {
1481
1777
  return answer.length > 0 ? answer : defaultValue;
1482
1778
  }
1483
1779
  function ensureGitignoreEntry(rootDir, entry) {
1484
- const gitignorePath = join3(rootDir, ".gitignore");
1485
- if (existsSync5(gitignorePath)) {
1486
- const content = readFileSync5(gitignorePath, "utf-8");
1780
+ const gitignorePath = join4(rootDir, ".gitignore");
1781
+ if (existsSync6(gitignorePath)) {
1782
+ const content = readFileSync6(gitignorePath, "utf-8");
1487
1783
  const normalised = entry.replace(/\/$/, "");
1488
1784
  const lines = content.split("\n").map((l) => l.trim());
1489
1785
  if (lines.includes(entry) || lines.includes(normalised)) {
@@ -1512,7 +1808,7 @@ function extractTailwindTokens(tokenSources) {
1512
1808
  return result;
1513
1809
  };
1514
1810
  var parseBlock = parseBlock2;
1515
- const raw = readFileSync5(tailwindSource.path, "utf-8");
1811
+ const raw = readFileSync6(tailwindSource.path, "utf-8");
1516
1812
  const tokens = {};
1517
1813
  const colorsKeyIdx = raw.indexOf("colors:");
1518
1814
  if (colorsKeyIdx !== -1) {
@@ -1593,14 +1889,14 @@ function extractTailwindTokens(tokenSources) {
1593
1889
  }
1594
1890
  }
1595
1891
  function scaffoldConfig(rootDir, config) {
1596
- const path = join3(rootDir, "reactscope.config.json");
1892
+ const path = join4(rootDir, "reactscope.config.json");
1597
1893
  writeFileSync3(path, `${JSON.stringify(config, null, 2)}
1598
1894
  `);
1599
1895
  return path;
1600
1896
  }
1601
1897
  function scaffoldTokenFile(rootDir, tokenFile, extractedTokens) {
1602
- const path = join3(rootDir, tokenFile);
1603
- if (!existsSync5(path)) {
1898
+ const path = join4(rootDir, tokenFile);
1899
+ if (!existsSync6(path)) {
1604
1900
  const stub = {
1605
1901
  $schema: "https://raw.githubusercontent.com/FlatFilers/Scope/main/packages/tokens/schema.json",
1606
1902
  version: "1.0.0",
@@ -1616,19 +1912,19 @@ function scaffoldTokenFile(rootDir, tokenFile, extractedTokens) {
1616
1912
  return path;
1617
1913
  }
1618
1914
  function scaffoldOutputDir(rootDir, outputDir) {
1619
- const dirPath = join3(rootDir, outputDir);
1915
+ const dirPath = join4(rootDir, outputDir);
1620
1916
  mkdirSync(dirPath, { recursive: true });
1621
- const keepPath = join3(dirPath, ".gitkeep");
1622
- if (!existsSync5(keepPath)) {
1917
+ const keepPath = join4(dirPath, ".gitkeep");
1918
+ if (!existsSync6(keepPath)) {
1623
1919
  writeFileSync3(keepPath, "");
1624
1920
  }
1625
1921
  return dirPath;
1626
1922
  }
1627
1923
  async function runInit(options) {
1628
1924
  const rootDir = options.cwd ?? process.cwd();
1629
- const configPath = join3(rootDir, "reactscope.config.json");
1925
+ const configPath = join4(rootDir, "reactscope.config.json");
1630
1926
  const created = [];
1631
- if (existsSync5(configPath) && !options.force) {
1927
+ if (existsSync6(configPath) && !options.force) {
1632
1928
  const msg = "reactscope.config.json already exists. Run with --force to overwrite.";
1633
1929
  process.stderr.write(`\u26A0\uFE0F ${msg}
1634
1930
  `);
@@ -1711,8 +2007,8 @@ async function runInit(options) {
1711
2007
  };
1712
2008
  const manifest = await generateManifest2(manifestConfig);
1713
2009
  const manifestCount = Object.keys(manifest.components).length;
1714
- const manifestOutPath = join3(rootDir, config.output.dir, "manifest.json");
1715
- mkdirSync(join3(rootDir, config.output.dir), { recursive: true });
2010
+ const manifestOutPath = join4(rootDir, config.output.dir, "manifest.json");
2011
+ mkdirSync(join4(rootDir, config.output.dir), { recursive: true });
1716
2012
  writeFileSync3(manifestOutPath, `${JSON.stringify(manifest, null, 2)}
1717
2013
  `);
1718
2014
  process.stdout.write(
@@ -1756,18 +2052,18 @@ import { BrowserPool as BrowserPool2 } from "@agent-scope/render";
1756
2052
  import { Command as Command7 } from "commander";
1757
2053
 
1758
2054
  // src/manifest-commands.ts
1759
- import { existsSync as existsSync6, mkdirSync as mkdirSync2, readFileSync as readFileSync6, writeFileSync as writeFileSync4 } from "fs";
2055
+ import { existsSync as existsSync7, mkdirSync as mkdirSync2, readFileSync as readFileSync7, writeFileSync as writeFileSync4 } from "fs";
1760
2056
  import { resolve as resolve4 } from "path";
1761
2057
  import { generateManifest as generateManifest3 } from "@agent-scope/manifest";
1762
2058
  import { Command as Command5 } from "commander";
1763
2059
  var MANIFEST_PATH = ".reactscope/manifest.json";
1764
2060
  function loadManifest(manifestPath = MANIFEST_PATH) {
1765
2061
  const absPath = resolve4(process.cwd(), manifestPath);
1766
- if (!existsSync6(absPath)) {
2062
+ if (!existsSync7(absPath)) {
1767
2063
  throw new Error(`Manifest not found at ${absPath}.
1768
2064
  Run \`scope manifest generate\` first.`);
1769
2065
  }
1770
- const raw = readFileSync6(absPath, "utf-8");
2066
+ const raw = readFileSync7(absPath, "utf-8");
1771
2067
  return JSON.parse(raw);
1772
2068
  }
1773
2069
  function resolveFormat(formatFlag) {
@@ -1946,27 +2242,70 @@ function registerQuery(manifestCmd) {
1946
2242
  }
1947
2243
  );
1948
2244
  }
2245
+ function loadReactScopeConfig(rootDir) {
2246
+ const configPath = resolve4(rootDir, "reactscope.config.json");
2247
+ if (!existsSync7(configPath)) return null;
2248
+ try {
2249
+ const raw = readFileSync7(configPath, "utf-8");
2250
+ const cfg = JSON.parse(raw);
2251
+ const result = {};
2252
+ const components = cfg.components;
2253
+ if (components !== void 0 && typeof components === "object" && components !== null) {
2254
+ if (Array.isArray(components.include)) {
2255
+ result.include = components.include;
2256
+ }
2257
+ if (Array.isArray(components.exclude)) {
2258
+ result.exclude = components.exclude;
2259
+ }
2260
+ }
2261
+ if (Array.isArray(cfg.internalPatterns)) {
2262
+ result.internalPatterns = cfg.internalPatterns;
2263
+ }
2264
+ if (Array.isArray(cfg.collections)) {
2265
+ result.collections = cfg.collections;
2266
+ }
2267
+ const icons = cfg.icons;
2268
+ if (icons !== void 0 && typeof icons === "object" && icons !== null) {
2269
+ if (Array.isArray(icons.patterns)) {
2270
+ result.iconPatterns = icons.patterns;
2271
+ }
2272
+ }
2273
+ return result;
2274
+ } catch {
2275
+ return null;
2276
+ }
2277
+ }
1949
2278
  function registerGenerate(manifestCmd) {
1950
2279
  manifestCmd.command("generate").description(
1951
- 'Scan source files and generate .reactscope/manifest.json.\n\nUses Babel static analysis \u2014 no runtime or bundler required.\nRe-run whenever components are added, removed, or significantly changed.\n\nWHAT IT CAPTURES per component:\n - File path and export name\n - All props with types and default values\n - Hook usage (useState, useEffect, useContext, custom hooks)\n - Side effects (fetch, timers, subscriptions)\n - Complexity class: simple | complex\n - Context dependencies and composed child components\n\nExamples:\n scope manifest generate\n scope manifest generate --root ./packages/ui\n scope manifest generate --include "src/components/**/*.tsx" --exclude "**/*.stories.tsx"\n scope manifest generate --output ./custom-manifest.json'
2280
+ 'Scan source files and generate .reactscope/manifest.json.\n\nUses Babel static analysis \u2014 no runtime or bundler required.\nRe-run whenever components are added, removed, or significantly changed.\n\nReads reactscope.config.json for:\n components.include glob patterns for source files\n components.exclude glob patterns to skip\n internalPatterns globs to flag components as internal\n collections named groups of components\n\nCLI flags (--include, --exclude) override config-file values.\n\nWHAT IT CAPTURES per component:\n - File path and export name\n - All props with types and default values\n - Hook usage (useState, useEffect, useContext, custom hooks)\n - Side effects (fetch, timers, subscriptions)\n - Complexity class: simple | complex\n - Context dependencies and composed child components\n\nExamples:\n scope manifest generate\n scope manifest generate --root ./packages/ui\n scope manifest generate --include "src/components/**/*.tsx" --exclude "**/*.stories.tsx"\n scope manifest generate --output ./custom-manifest.json'
1952
2281
  ).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) => {
1953
2282
  try {
1954
2283
  const rootDir = resolve4(process.cwd(), opts.root ?? ".");
1955
2284
  const outputPath = resolve4(process.cwd(), opts.output);
1956
- const include = opts.include?.split(",").map((s) => s.trim());
1957
- const exclude = opts.exclude?.split(",").map((s) => s.trim());
2285
+ const configValues = loadReactScopeConfig(rootDir);
2286
+ const include = opts.include?.split(",").map((s) => s.trim()) ?? configValues?.include;
2287
+ const exclude = opts.exclude?.split(",").map((s) => s.trim()) ?? configValues?.exclude;
1958
2288
  process.stderr.write(`Scanning ${rootDir} for React components...
1959
2289
  `);
1960
2290
  const manifest = await generateManifest3({
1961
2291
  rootDir,
1962
2292
  ...include !== void 0 && { include },
1963
- ...exclude !== void 0 && { exclude }
2293
+ ...exclude !== void 0 && { exclude },
2294
+ ...configValues?.internalPatterns !== void 0 && {
2295
+ internalPatterns: configValues.internalPatterns
2296
+ },
2297
+ ...configValues?.collections !== void 0 && {
2298
+ collections: configValues.collections
2299
+ },
2300
+ ...configValues?.iconPatterns !== void 0 && {
2301
+ iconPatterns: configValues.iconPatterns
2302
+ }
1964
2303
  });
1965
2304
  const componentCount = Object.keys(manifest.components).length;
1966
2305
  process.stderr.write(`Found ${componentCount} components.
1967
2306
  `);
1968
2307
  const outputDir = outputPath.replace(/\/[^/]+$/, "");
1969
- if (!existsSync6(outputDir)) {
2308
+ if (!existsSync7(outputDir)) {
1970
2309
  mkdirSync2(outputDir, { recursive: true });
1971
2310
  }
1972
2311
  writeFileSync4(outputPath, JSON.stringify(manifest, null, 2), "utf-8");
@@ -2355,7 +2694,7 @@ Available: ${available}`
2355
2694
  process.stdout.write(`${JSON.stringify(result, null, 2)}
2356
2695
  `);
2357
2696
  } catch (err) {
2358
- process.stderr.write(`Error: ${err instanceof Error ? err.message : String(err)}
2697
+ process.stderr.write(`${formatScopeDiagnostic(err)}
2359
2698
  `);
2360
2699
  process.exit(1);
2361
2700
  }
@@ -2464,13 +2803,11 @@ function buildProfilingCollectScript() {
2464
2803
  // mount commit), it *may* have been wasted if it didn't actually need to re-render.
2465
2804
  // For the initial snapshot we approximate: wastedRenders = max(0, totalCommits - 1) * 0.3
2466
2805
  // This is a heuristic \u2014 real wasted render detection needs shouldComponentUpdate/React.memo tracing.
2467
- var wastedRenders = Math.max(0, Math.round((totalCommits - 1) * uniqueNames.length * 0.3));
2468
-
2469
2806
  return {
2470
2807
  commitCount: totalCommits,
2471
2808
  uniqueComponents: uniqueNames.length,
2472
2809
  componentNames: uniqueNames,
2473
- wastedRenders: wastedRenders,
2810
+ wastedRenders: null,
2474
2811
  layoutTime: window.__scopeLayoutTime || 0,
2475
2812
  paintTime: window.__scopePaintTime || 0,
2476
2813
  layoutShifts: window.__scopeLayoutShifts || { count: 0, score: 0 }
@@ -2518,7 +2855,7 @@ async function replayInteraction(page, steps) {
2518
2855
  }
2519
2856
  function analyzeProfileFlags(totalRenders, wastedRenders, timing, layoutShifts) {
2520
2857
  const flags = /* @__PURE__ */ new Set();
2521
- if (wastedRenders > 0 && wastedRenders / Math.max(1, totalRenders) > 0.3) {
2858
+ if (wastedRenders !== null && wastedRenders > 0 && wastedRenders / Math.max(1, totalRenders) > 0.3) {
2522
2859
  flags.add("WASTED_RENDER");
2523
2860
  }
2524
2861
  if (totalRenders > 10) {
@@ -2589,13 +2926,18 @@ async function runInteractionProfile(componentName, filePath, props, interaction
2589
2926
  };
2590
2927
  const totalRenders = profileData.commitCount ?? 0;
2591
2928
  const uniqueComponents = profileData.uniqueComponents ?? 0;
2592
- const wastedRenders = profileData.wastedRenders ?? 0;
2929
+ const wastedRenders = profileData.wastedRenders ?? null;
2593
2930
  const flags = analyzeProfileFlags(totalRenders, wastedRenders, timing, layoutShifts);
2594
2931
  return {
2595
2932
  component: componentName,
2596
2933
  totalRenders,
2597
2934
  uniqueComponents,
2598
2935
  wastedRenders,
2936
+ wastedRendersHeuristic: {
2937
+ measured: false,
2938
+ value: null,
2939
+ note: "profile.wastedRenders is retained for compatibility but set to null because Scope does not directly measure wasted renders yet."
2940
+ },
2599
2941
  timing,
2600
2942
  layoutShifts,
2601
2943
  flags,
@@ -2670,7 +3012,7 @@ Available: ${available}`
2670
3012
  process.stdout.write(`${JSON.stringify(result, null, 2)}
2671
3013
  `);
2672
3014
  } catch (err) {
2673
- process.stderr.write(`Error: ${err instanceof Error ? err.message : String(err)}
3015
+ process.stderr.write(`${formatScopeDiagnostic(err)}
2674
3016
  `);
2675
3017
  process.exit(1);
2676
3018
  }
@@ -3023,7 +3365,7 @@ Available: ${available}`
3023
3365
  `);
3024
3366
  }
3025
3367
  } catch (err) {
3026
- process.stderr.write(`Error: ${err instanceof Error ? err.message : String(err)}
3368
+ process.stderr.write(`${formatScopeDiagnostic(err)}
3027
3369
  `);
3028
3370
  process.exit(1);
3029
3371
  }
@@ -3517,7 +3859,7 @@ Examples:
3517
3859
  }
3518
3860
  } catch (err) {
3519
3861
  await shutdownPool2();
3520
- process.stderr.write(`Error: ${err instanceof Error ? err.message : String(err)}
3862
+ process.stderr.write(`${formatScopeDiagnostic(err)}
3521
3863
  `);
3522
3864
  process.exit(1);
3523
3865
  }
@@ -3557,8 +3899,8 @@ Examples:
3557
3899
  }
3558
3900
 
3559
3901
  // src/render-commands.ts
3560
- import { existsSync as existsSync8, mkdirSync as mkdirSync4, readFileSync as readFileSync7, writeFileSync as writeFileSync5 } from "fs";
3561
- import { resolve as resolve10 } from "path";
3902
+ import { existsSync as existsSync9, mkdirSync as mkdirSync5, readFileSync as readFileSync9, writeFileSync as writeFileSync6 } from "fs";
3903
+ import { resolve as resolve11 } from "path";
3562
3904
  import {
3563
3905
  ALL_CONTEXT_IDS,
3564
3906
  ALL_STRESS_IDS,
@@ -3571,27 +3913,79 @@ import {
3571
3913
  } from "@agent-scope/render";
3572
3914
  import { Command as Command8 } from "commander";
3573
3915
 
3916
+ // src/run-summary.ts
3917
+ import { mkdirSync as mkdirSync3, readFileSync as readFileSync8, writeFileSync as writeFileSync5 } from "fs";
3918
+ import { dirname as dirname3, resolve as resolve9 } from "path";
3919
+ var RUN_SUMMARY_PATH = ".reactscope/run-summary.json";
3920
+ function buildNextActions(summary) {
3921
+ const actions = /* @__PURE__ */ new Set();
3922
+ for (const failure of summary.failures) {
3923
+ if (failure.stage === "render" || failure.stage === "matrix") {
3924
+ actions.add(
3925
+ `Inspect ${failure.outputPath ?? ".reactscope/renders"} and add/fix ${failure.component}.scope.tsx scenarios or wrappers.`
3926
+ );
3927
+ } else if (failure.stage === "playground") {
3928
+ actions.add(
3929
+ `Open the generated component page and inspect the playground bundling error for ${failure.component}.`
3930
+ );
3931
+ } else if (failure.stage === "compliance") {
3932
+ actions.add(
3933
+ "Run `scope render all` first, then inspect .reactscope/compliance-styles.json and reactscope.tokens.json."
3934
+ );
3935
+ } else if (failure.stage === "site") {
3936
+ actions.add(
3937
+ "Inspect .reactscope/site output and rerun `scope site build` after fixing render/playground failures."
3938
+ );
3939
+ }
3940
+ }
3941
+ if (summary.compliance && summary.compliance.auditedProperties === 0) {
3942
+ actions.add(
3943
+ "No CSS properties were audited. Verify renders produced computed styles and your token file contains matching token categories."
3944
+ );
3945
+ } else if (summary.compliance && summary.compliance.threshold !== void 0 && summary.compliance.score < summary.compliance.threshold) {
3946
+ actions.add(
3947
+ "Inspect .reactscope/compliance-report.json for off-system values and update tokens or component styles."
3948
+ );
3949
+ }
3950
+ if (actions.size === 0) {
3951
+ actions.add("No follow-up needed. Outputs are ready for inspection.");
3952
+ }
3953
+ return [...actions];
3954
+ }
3955
+ function writeRunSummary(summary, summaryPath = RUN_SUMMARY_PATH) {
3956
+ const outputPath = resolve9(process.cwd(), summaryPath);
3957
+ mkdirSync3(dirname3(outputPath), { recursive: true });
3958
+ const payload = {
3959
+ ...summary,
3960
+ generatedAt: summary.generatedAt ?? (/* @__PURE__ */ new Date()).toISOString(),
3961
+ nextActions: summary.nextActions ?? buildNextActions(summary)
3962
+ };
3963
+ writeFileSync5(outputPath, `${JSON.stringify(payload, null, 2)}
3964
+ `, "utf-8");
3965
+ return outputPath;
3966
+ }
3967
+
3574
3968
  // src/scope-file.ts
3575
- import { existsSync as existsSync7, mkdirSync as mkdirSync3, rmSync } from "fs";
3969
+ import { existsSync as existsSync8, mkdirSync as mkdirSync4, rmSync } from "fs";
3576
3970
  import { createRequire as createRequire2 } from "module";
3577
3971
  import { tmpdir } from "os";
3578
- import { dirname as dirname2, join as join4, resolve as resolve9 } from "path";
3972
+ import { dirname as dirname4, join as join5, resolve as resolve10 } from "path";
3579
3973
  import * as esbuild2 from "esbuild";
3580
3974
  var SCOPE_EXTENSIONS = [".scope.tsx", ".scope.ts", ".scope.jsx", ".scope.js"];
3581
3975
  function findScopeFile(componentFilePath) {
3582
- const dir = dirname2(componentFilePath);
3976
+ const dir = dirname4(componentFilePath);
3583
3977
  const stem = componentFilePath.replace(/\.(tsx?|jsx?)$/, "");
3584
3978
  const baseName = stem.slice(dir.length + 1);
3585
3979
  for (const ext of SCOPE_EXTENSIONS) {
3586
- const candidate = join4(dir, `${baseName}${ext}`);
3587
- if (existsSync7(candidate)) return candidate;
3980
+ const candidate = join5(dir, `${baseName}${ext}`);
3981
+ if (existsSync8(candidate)) return candidate;
3588
3982
  }
3589
3983
  return null;
3590
3984
  }
3591
3985
  async function loadScopeFile(scopeFilePath) {
3592
- const tmpDir = join4(tmpdir(), `scope-file-${Date.now()}-${Math.random().toString(36).slice(2)}`);
3593
- mkdirSync3(tmpDir, { recursive: true });
3594
- const outFile = join4(tmpDir, "scope-file.cjs");
3986
+ const tmpDir = join5(tmpdir(), `scope-file-${Date.now()}-${Math.random().toString(36).slice(2)}`);
3987
+ mkdirSync4(tmpDir, { recursive: true });
3988
+ const outFile = join5(tmpDir, "scope-file.cjs");
3595
3989
  try {
3596
3990
  const result = await esbuild2.build({
3597
3991
  entryPoints: [scopeFilePath],
@@ -3616,7 +4010,7 @@ async function loadScopeFile(scopeFilePath) {
3616
4010
  ${msg}`);
3617
4011
  }
3618
4012
  const req = createRequire2(import.meta.url);
3619
- delete req.cache[resolve9(outFile)];
4013
+ delete req.cache[resolve10(outFile)];
3620
4014
  const mod = req(outFile);
3621
4015
  const scenarios = extractScenarios(mod, scopeFilePath);
3622
4016
  const hasWrapper = typeof mod.wrapper === "function" || typeof mod.default?.wrapper === "function";
@@ -3666,7 +4060,7 @@ window.__SCOPE_WRAPPER__ = wrapper;
3666
4060
  const result = await esbuild2.build({
3667
4061
  stdin: {
3668
4062
  contents: wrapperEntry,
3669
- resolveDir: dirname2(scopeFilePath),
4063
+ resolveDir: dirname4(scopeFilePath),
3670
4064
  loader: "tsx",
3671
4065
  sourcefile: "__scope_wrapper_entry__.tsx"
3672
4066
  },
@@ -3694,16 +4088,73 @@ ${msg}`);
3694
4088
 
3695
4089
  // src/render-commands.ts
3696
4090
  function loadGlobalCssFilesFromConfig(cwd) {
3697
- const configPath = resolve10(cwd, "reactscope.config.json");
3698
- if (!existsSync8(configPath)) return [];
4091
+ const configPath = resolve11(cwd, "reactscope.config.json");
4092
+ if (!existsSync9(configPath)) return [];
3699
4093
  try {
3700
- const raw = readFileSync7(configPath, "utf-8");
4094
+ const raw = readFileSync9(configPath, "utf-8");
3701
4095
  const cfg = JSON.parse(raw);
3702
4096
  return cfg.components?.wrappers?.globalCSS ?? [];
3703
4097
  } catch {
3704
4098
  return [];
3705
4099
  }
3706
4100
  }
4101
+ var TAILWIND_CONFIG_FILES2 = [
4102
+ "tailwind.config.js",
4103
+ "tailwind.config.cjs",
4104
+ "tailwind.config.mjs",
4105
+ "tailwind.config.ts",
4106
+ "postcss.config.js",
4107
+ "postcss.config.cjs",
4108
+ "postcss.config.mjs",
4109
+ "postcss.config.ts"
4110
+ ];
4111
+ function shouldWarnForMissingGlobalCss(cwd) {
4112
+ if (TAILWIND_CONFIG_FILES2.some((file) => existsSync9(resolve11(cwd, file)))) {
4113
+ return true;
4114
+ }
4115
+ const packageJsonPath = resolve11(cwd, "package.json");
4116
+ if (!existsSync9(packageJsonPath)) return false;
4117
+ try {
4118
+ const pkg = JSON.parse(readFileSync9(packageJsonPath, "utf-8"));
4119
+ return [pkg.dependencies, pkg.devDependencies].some(
4120
+ (deps) => deps && Object.keys(deps).some(
4121
+ (name) => name === "tailwindcss" || name.startsWith("@tailwindcss/")
4122
+ )
4123
+ );
4124
+ } catch {
4125
+ return false;
4126
+ }
4127
+ }
4128
+ function loadIconPatternsFromConfig(cwd) {
4129
+ const configPath = resolve11(cwd, "reactscope.config.json");
4130
+ if (!existsSync9(configPath)) return [];
4131
+ try {
4132
+ const raw = readFileSync9(configPath, "utf-8");
4133
+ const cfg = JSON.parse(raw);
4134
+ return cfg.icons?.patterns ?? [];
4135
+ } catch {
4136
+ return [];
4137
+ }
4138
+ }
4139
+ function matchGlob2(pattern, value) {
4140
+ const escaped = pattern.replace(/[.+^${}()|[\]\\]/g, "\\$&");
4141
+ const regexStr = escaped.replace(/\*\*/g, "\xA7GLOBSTAR\xA7").replace(/\*/g, "[^/]*").replace(/§GLOBSTAR§/g, ".*");
4142
+ return new RegExp(`^${regexStr}$`, "i").test(value);
4143
+ }
4144
+ function isIconComponent(filePath, displayName, patterns) {
4145
+ return patterns.length > 0 && patterns.some((p) => matchGlob2(p, filePath) || matchGlob2(p, displayName));
4146
+ }
4147
+ function formatAggregateRenderFailureJson(componentName, failures, scenarioCount, runSummaryPath) {
4148
+ return {
4149
+ command: `scope render component ${componentName}`,
4150
+ status: "failed",
4151
+ component: componentName,
4152
+ scenarioCount,
4153
+ failureCount: failures.length,
4154
+ failures,
4155
+ runSummaryPath
4156
+ };
4157
+ }
3707
4158
  var MANIFEST_PATH6 = ".reactscope/manifest.json";
3708
4159
  var DEFAULT_OUTPUT_DIR = ".reactscope/renders";
3709
4160
  var _pool3 = null;
@@ -3724,7 +4175,7 @@ async function shutdownPool3() {
3724
4175
  _pool3 = null;
3725
4176
  }
3726
4177
  }
3727
- function buildRenderer(filePath, componentName, viewportWidth, viewportHeight, globalCssFiles = [], projectCwd = process.cwd(), wrapperScript) {
4178
+ function buildRenderer(filePath, componentName, viewportWidth, viewportHeight, globalCssFiles = [], projectCwd = process.cwd(), wrapperScript, iconMode = false) {
3728
4179
  const satori = new SatoriRenderer({
3729
4180
  defaultViewport: { width: viewportWidth, height: viewportHeight }
3730
4181
  });
@@ -3734,13 +4185,15 @@ function buildRenderer(filePath, componentName, viewportWidth, viewportHeight, g
3734
4185
  const startMs = performance.now();
3735
4186
  const pool = await getPool3(viewportWidth, viewportHeight);
3736
4187
  const projectCss = await loadGlobalCss(globalCssFiles, projectCwd);
4188
+ const PAD = 8;
3737
4189
  const htmlHarness = await buildComponentHarness(
3738
4190
  filePath,
3739
4191
  componentName,
3740
4192
  props,
3741
4193
  viewportWidth,
3742
4194
  projectCss ?? void 0,
3743
- wrapperScript
4195
+ wrapperScript,
4196
+ PAD
3744
4197
  );
3745
4198
  const slot = await pool.acquire();
3746
4199
  const { page } = slot;
@@ -3763,8 +4216,8 @@ function buildRenderer(filePath, componentName, viewportWidth, viewportHeight, g
3763
4216
  const classes = await page.evaluate(() => {
3764
4217
  const set = /* @__PURE__ */ new Set();
3765
4218
  document.querySelectorAll("[class]").forEach((el) => {
3766
- for (const c of el.className.split(/\s+/)) {
3767
- if (c) set.add(c);
4219
+ for (const c of getElementClassNames(el)) {
4220
+ set.add(c);
3768
4221
  }
3769
4222
  });
3770
4223
  return [...set];
@@ -3781,17 +4234,28 @@ function buildRenderer(filePath, componentName, viewportWidth, viewportHeight, g
3781
4234
  `Component "${componentName}" rendered with zero bounding box \u2014 it may be invisible or not mounted`
3782
4235
  );
3783
4236
  }
3784
- const PAD = 8;
3785
4237
  const clipX = Math.max(0, boundingBox.x - PAD);
3786
4238
  const clipY = Math.max(0, boundingBox.y - PAD);
3787
4239
  const rawW = boundingBox.width + PAD * 2;
3788
4240
  const rawH = boundingBox.height + PAD * 2;
3789
4241
  const safeW = Math.min(rawW, viewportWidth - clipX);
3790
4242
  const safeH = Math.min(rawH, viewportHeight - clipY);
3791
- const screenshot = await page.screenshot({
3792
- clip: { x: clipX, y: clipY, width: safeW, height: safeH },
3793
- type: "png"
3794
- });
4243
+ let svgContent;
4244
+ let screenshot;
4245
+ if (iconMode) {
4246
+ svgContent = await page.evaluate((sel) => {
4247
+ const root = document.querySelector(sel);
4248
+ const el = root?.firstElementChild;
4249
+ if (!el) return void 0;
4250
+ return el.outerHTML;
4251
+ }, "[data-reactscope-root]") ?? void 0;
4252
+ screenshot = Buffer.alloc(0);
4253
+ } else {
4254
+ screenshot = await page.screenshot({
4255
+ clip: { x: clipX, y: clipY, width: safeW, height: safeH },
4256
+ type: "png"
4257
+ });
4258
+ }
3795
4259
  const STYLE_PROPS = [
3796
4260
  "display",
3797
4261
  "width",
@@ -3914,7 +4378,7 @@ function buildRenderer(filePath, componentName, viewportWidth, viewportHeight, g
3914
4378
  name: a11yInfo.name,
3915
4379
  violations: imgViolations
3916
4380
  };
3917
- return {
4381
+ const renderResult = {
3918
4382
  screenshot,
3919
4383
  width: Math.round(safeW),
3920
4384
  height: Math.round(safeH),
@@ -3923,6 +4387,10 @@ function buildRenderer(filePath, componentName, viewportWidth, viewportHeight, g
3923
4387
  dom,
3924
4388
  accessibility
3925
4389
  };
4390
+ if (iconMode && svgContent) {
4391
+ renderResult.svgContent = svgContent;
4392
+ }
4393
+ return renderResult;
3926
4394
  } finally {
3927
4395
  pool.release(slot);
3928
4396
  }
@@ -4017,12 +4485,12 @@ Available: ${available}`
4017
4485
  }
4018
4486
  const { width, height } = parseViewport(opts.viewport);
4019
4487
  const rootDir = process.cwd();
4020
- const filePath = resolve10(rootDir, descriptor.filePath);
4488
+ const filePath = resolve11(rootDir, descriptor.filePath);
4021
4489
  const scopeData = await loadScopeFileForComponent(filePath);
4022
4490
  const wrapperScript = scopeData?.hasWrapper === true ? await buildWrapperScript(scopeData.filePath) : void 0;
4023
4491
  const scenarios = buildScenarioMap(opts, scopeData);
4024
4492
  const globalCssFiles = loadGlobalCssFilesFromConfig(rootDir);
4025
- if (globalCssFiles.length === 0) {
4493
+ if (globalCssFiles.length === 0 && shouldWarnForMissingGlobalCss(rootDir)) {
4026
4494
  process.stderr.write(
4027
4495
  "warning: No globalCSS files configured. Tailwind/CSS styles will not be applied to renders.\n Add `components.wrappers.globalCSS` to reactscope.config.json\n"
4028
4496
  );
@@ -4041,7 +4509,8 @@ Available: ${available}`
4041
4509
  `
4042
4510
  );
4043
4511
  const fmt2 = resolveSingleFormat(opts.format);
4044
- let anyFailed = false;
4512
+ const failures = [];
4513
+ const outputPaths = [];
4045
4514
  for (const [scenarioName, props2] of Object.entries(scenarios)) {
4046
4515
  const isNamed = scenarioName !== "__default__";
4047
4516
  const label = isNamed ? `${componentName}:${scenarioName}` : componentName;
@@ -4064,14 +4533,22 @@ Available: ${available}`
4064
4533
  process.stderr.write(` Hints: ${hintList}
4065
4534
  `);
4066
4535
  }
4067
- anyFailed = true;
4536
+ failures.push({
4537
+ component: componentName,
4538
+ scenario: isNamed ? scenarioName : void 0,
4539
+ stage: "render",
4540
+ message: outcome.error.message,
4541
+ outputPath: `${DEFAULT_OUTPUT_DIR}/${isNamed ? `${componentName}-${scenarioName}.error.json` : `${componentName}.error.json`}`,
4542
+ hints: outcome.error.heuristicFlags
4543
+ });
4068
4544
  continue;
4069
4545
  }
4070
4546
  const result = outcome.result;
4071
4547
  const outFileName = isNamed ? `${componentName}-${scenarioName}.png` : `${componentName}.png`;
4072
4548
  if (opts.output !== void 0 && !isNamed) {
4073
- const outPath = resolve10(process.cwd(), opts.output);
4074
- writeFileSync5(outPath, result.screenshot);
4549
+ const outPath = resolve11(process.cwd(), opts.output);
4550
+ writeFileSync6(outPath, result.screenshot);
4551
+ outputPaths.push(outPath);
4075
4552
  process.stdout.write(
4076
4553
  `\u2713 ${label} \u2192 ${opts.output} (${result.width}\xD7${result.height}, ${result.renderTimeMs.toFixed(0)}ms)
4077
4554
  `
@@ -4081,22 +4558,41 @@ Available: ${available}`
4081
4558
  process.stdout.write(`${JSON.stringify(json, null, 2)}
4082
4559
  `);
4083
4560
  } else {
4084
- const dir = resolve10(process.cwd(), DEFAULT_OUTPUT_DIR);
4085
- mkdirSync4(dir, { recursive: true });
4086
- const outPath = resolve10(dir, outFileName);
4087
- writeFileSync5(outPath, result.screenshot);
4561
+ const dir = resolve11(process.cwd(), DEFAULT_OUTPUT_DIR);
4562
+ mkdirSync5(dir, { recursive: true });
4563
+ const outPath = resolve11(dir, outFileName);
4564
+ writeFileSync6(outPath, result.screenshot);
4088
4565
  const relPath = `${DEFAULT_OUTPUT_DIR}/${outFileName}`;
4566
+ outputPaths.push(relPath);
4089
4567
  process.stdout.write(
4090
4568
  `\u2713 ${label} \u2192 ${relPath} (${result.width}\xD7${result.height}, ${result.renderTimeMs.toFixed(0)}ms)
4091
4569
  `
4092
4570
  );
4093
4571
  }
4094
4572
  }
4573
+ const summaryPath = writeRunSummary({
4574
+ command: `scope render ${componentName}`,
4575
+ status: failures.length > 0 ? "failed" : "success",
4576
+ outputPaths,
4577
+ failures
4578
+ });
4579
+ process.stderr.write(`[scope/render] Run summary written to ${summaryPath}
4580
+ `);
4581
+ if (fmt2 === "json" && failures.length > 0) {
4582
+ const aggregateFailure = formatAggregateRenderFailureJson(
4583
+ componentName,
4584
+ failures,
4585
+ Object.keys(scenarios).length,
4586
+ summaryPath
4587
+ );
4588
+ process.stderr.write(`${JSON.stringify(aggregateFailure, null, 2)}
4589
+ `);
4590
+ }
4095
4591
  await shutdownPool3();
4096
- if (anyFailed) process.exit(1);
4592
+ if (failures.length > 0) process.exit(1);
4097
4593
  } catch (err) {
4098
4594
  await shutdownPool3();
4099
- process.stderr.write(`Error: ${err instanceof Error ? err.message : String(err)}
4595
+ process.stderr.write(`${formatScopeDiagnostic(err)}
4100
4596
  `);
4101
4597
  process.exit(1);
4102
4598
  }
@@ -4127,7 +4623,7 @@ Available: ${available}`
4127
4623
  const concurrency = Math.max(1, parseInt(opts.concurrency, 10) || 8);
4128
4624
  const { width, height } = { width: 375, height: 812 };
4129
4625
  const rootDir = process.cwd();
4130
- const filePath = resolve10(rootDir, descriptor.filePath);
4626
+ const filePath = resolve11(rootDir, descriptor.filePath);
4131
4627
  const matrixCssFiles = loadGlobalCssFilesFromConfig(rootDir);
4132
4628
  const renderer = buildRenderer(
4133
4629
  filePath,
@@ -4220,8 +4716,8 @@ Available: ${available}`
4220
4716
  const { SpriteSheetGenerator: SpriteSheetGenerator2 } = await import("@agent-scope/render");
4221
4717
  const gen = new SpriteSheetGenerator2();
4222
4718
  const sheet = await gen.generate(result);
4223
- const spritePath = resolve10(process.cwd(), opts.sprite);
4224
- writeFileSync5(spritePath, sheet.png);
4719
+ const spritePath = resolve11(process.cwd(), opts.sprite);
4720
+ writeFileSync6(spritePath, sheet.png);
4225
4721
  process.stderr.write(`Sprite sheet saved to ${spritePath}
4226
4722
  `);
4227
4723
  }
@@ -4230,10 +4726,10 @@ Available: ${available}`
4230
4726
  const { SpriteSheetGenerator: SpriteSheetGenerator2 } = await import("@agent-scope/render");
4231
4727
  const gen = new SpriteSheetGenerator2();
4232
4728
  const sheet = await gen.generate(result);
4233
- const dir = resolve10(process.cwd(), DEFAULT_OUTPUT_DIR);
4234
- mkdirSync4(dir, { recursive: true });
4235
- const outPath = resolve10(dir, `${componentName}-matrix.png`);
4236
- writeFileSync5(outPath, sheet.png);
4729
+ const dir = resolve11(process.cwd(), DEFAULT_OUTPUT_DIR);
4730
+ mkdirSync5(dir, { recursive: true });
4731
+ const outPath = resolve11(dir, `${componentName}-matrix.png`);
4732
+ writeFileSync6(outPath, sheet.png);
4237
4733
  const relPath = `${DEFAULT_OUTPUT_DIR}/${componentName}-matrix.png`;
4238
4734
  process.stdout.write(
4239
4735
  `\u2713 ${componentName} matrix (${result.stats.totalCells} cells) \u2192 ${relPath} (${result.stats.wallClockTimeMs.toFixed(0)}ms total)
@@ -4257,7 +4753,7 @@ Available: ${available}`
4257
4753
  }
4258
4754
  } catch (err) {
4259
4755
  await shutdownPool3();
4260
- process.stderr.write(`Error: ${err instanceof Error ? err.message : String(err)}
4756
+ process.stderr.write(`${formatScopeDiagnostic(err)}
4261
4757
  `);
4262
4758
  process.exit(1);
4263
4759
  }
@@ -4275,22 +4771,40 @@ function registerRenderAll(renderCmd) {
4275
4771
  const total = componentNames.length;
4276
4772
  if (total === 0) {
4277
4773
  process.stderr.write("No components found in manifest.\n");
4278
- return;
4774
+ const summaryPath2 = writeRunSummary({
4775
+ command: "scope render all",
4776
+ status: "failed",
4777
+ outputPaths: [],
4778
+ failures: [
4779
+ {
4780
+ component: "*",
4781
+ stage: "render",
4782
+ message: "No components found in manifest; refusing to report a false-green batch render."
4783
+ }
4784
+ ]
4785
+ });
4786
+ process.stderr.write(`[scope/render] Run summary written to ${summaryPath2}
4787
+ `);
4788
+ process.exit(1);
4279
4789
  }
4280
4790
  const concurrency = Math.max(1, parseInt(opts.concurrency, 10) || 4);
4281
- const outputDir = resolve10(process.cwd(), opts.outputDir);
4282
- mkdirSync4(outputDir, { recursive: true });
4791
+ const outputDir = resolve11(process.cwd(), opts.outputDir);
4792
+ mkdirSync5(outputDir, { recursive: true });
4283
4793
  const rootDir = process.cwd();
4284
4794
  process.stderr.write(`Rendering ${total} components (concurrency: ${concurrency})\u2026
4285
4795
  `);
4286
4796
  const results = [];
4797
+ const failures = [];
4798
+ const outputPaths = [];
4287
4799
  const complianceStylesMap = {};
4288
4800
  let completed = 0;
4801
+ const iconPatterns = loadIconPatternsFromConfig(process.cwd());
4289
4802
  const renderOne = async (name) => {
4290
4803
  const descriptor = manifest.components[name];
4291
4804
  if (descriptor === void 0) return;
4292
- const filePath = resolve10(rootDir, descriptor.filePath);
4805
+ const filePath = resolve11(rootDir, descriptor.filePath);
4293
4806
  const allCssFiles = loadGlobalCssFilesFromConfig(process.cwd());
4807
+ const isIcon = isIconComponent(descriptor.filePath, name, iconPatterns);
4294
4808
  const scopeData = await loadScopeFileForComponent(filePath);
4295
4809
  const scenarioEntries = scopeData !== null ? Object.entries(scopeData.scenarios) : [];
4296
4810
  const defaultEntry = scenarioEntries.find(([k]) => k === "default") ?? scenarioEntries[0];
@@ -4303,7 +4817,8 @@ function registerRenderAll(renderCmd) {
4303
4817
  812,
4304
4818
  allCssFiles,
4305
4819
  process.cwd(),
4306
- wrapperScript
4820
+ wrapperScript,
4821
+ isIcon
4307
4822
  );
4308
4823
  const outcome = await safeRender2(
4309
4824
  () => renderer.renderCell(renderProps, descriptor.complexityClass),
@@ -4326,8 +4841,8 @@ function registerRenderAll(renderCmd) {
4326
4841
  success: false,
4327
4842
  errorMessage: outcome.error.message
4328
4843
  });
4329
- const errPath = resolve10(outputDir, `${name}.error.json`);
4330
- writeFileSync5(
4844
+ const errPath = resolve11(outputDir, `${name}.error.json`);
4845
+ writeFileSync6(
4331
4846
  errPath,
4332
4847
  JSON.stringify(
4333
4848
  {
@@ -4340,14 +4855,32 @@ function registerRenderAll(renderCmd) {
4340
4855
  2
4341
4856
  )
4342
4857
  );
4858
+ failures.push({
4859
+ component: name,
4860
+ stage: "render",
4861
+ message: outcome.error.message,
4862
+ outputPath: errPath,
4863
+ hints: outcome.error.heuristicFlags
4864
+ });
4865
+ outputPaths.push(errPath);
4343
4866
  return;
4344
4867
  }
4345
4868
  const result = outcome.result;
4346
4869
  results.push({ name, renderTimeMs: result.renderTimeMs, success: true });
4347
- const pngPath = resolve10(outputDir, `${name}.png`);
4348
- writeFileSync5(pngPath, result.screenshot);
4349
- const jsonPath = resolve10(outputDir, `${name}.json`);
4350
- writeFileSync5(jsonPath, JSON.stringify(formatRenderJson(name, {}, result), null, 2));
4870
+ if (!isIcon) {
4871
+ const pngPath = resolve11(outputDir, `${name}.png`);
4872
+ writeFileSync6(pngPath, result.screenshot);
4873
+ outputPaths.push(pngPath);
4874
+ }
4875
+ const jsonPath = resolve11(outputDir, `${name}.json`);
4876
+ const renderJson = formatRenderJson(name, {}, result);
4877
+ const extResult = result;
4878
+ if (isIcon && extResult.svgContent) {
4879
+ renderJson.svgContent = extResult.svgContent;
4880
+ delete renderJson.screenshot;
4881
+ }
4882
+ writeFileSync6(jsonPath, JSON.stringify(renderJson, null, 2));
4883
+ outputPaths.push(jsonPath);
4351
4884
  const rawStyles = result.computedStyles["[data-reactscope-root] > *"] ?? {};
4352
4885
  const compStyles = {
4353
4886
  colors: {},
@@ -4408,20 +4941,26 @@ function registerRenderAll(renderCmd) {
4408
4941
  height: cell.result.height,
4409
4942
  renderTimeMs: cell.result.renderTimeMs
4410
4943
  }));
4411
- const existingJson = JSON.parse(readFileSync7(jsonPath, "utf-8"));
4944
+ const existingJson = JSON.parse(readFileSync9(jsonPath, "utf-8"));
4412
4945
  existingJson.cells = matrixCells;
4413
4946
  existingJson.axisLabels = [scenarioAxis.values];
4414
- writeFileSync5(jsonPath, JSON.stringify(existingJson, null, 2));
4947
+ writeFileSync6(jsonPath, JSON.stringify(existingJson, null, 2));
4415
4948
  } catch (matrixErr) {
4416
- process.stderr.write(
4417
- ` [warn] Matrix render for ${name} failed: ${matrixErr instanceof Error ? matrixErr.message : String(matrixErr)}
4418
- `
4419
- );
4949
+ const message = matrixErr instanceof Error ? matrixErr.message : String(matrixErr);
4950
+ process.stderr.write(` [warn] Matrix render for ${name} failed: ${message}
4951
+ `);
4952
+ failures.push({
4953
+ component: name,
4954
+ stage: "matrix",
4955
+ message,
4956
+ outputPath: jsonPath
4957
+ });
4420
4958
  }
4421
4959
  }
4422
4960
  if (isTTY()) {
4961
+ const suffix = isIcon ? " [icon/svg]" : "";
4423
4962
  process.stdout.write(
4424
- `\u2713 ${name} \u2192 ${opts.outputDir}/${name}.png (${result.width}\xD7${result.height}, ${result.renderTimeMs.toFixed(0)}ms)
4963
+ `\u2713 ${name} \u2192 ${opts.outputDir}/${name}${isIcon ? ".json" : ".png"} (${result.width}\xD7${result.height}, ${result.renderTimeMs.toFixed(0)}ms)${suffix}
4425
4964
  `
4426
4965
  );
4427
4966
  }
@@ -4442,21 +4981,31 @@ function registerRenderAll(renderCmd) {
4442
4981
  }
4443
4982
  await Promise.all(workers);
4444
4983
  await shutdownPool3();
4445
- const compStylesPath = resolve10(
4446
- resolve10(process.cwd(), opts.outputDir),
4984
+ const compStylesPath = resolve11(
4985
+ resolve11(process.cwd(), opts.outputDir),
4447
4986
  "..",
4448
4987
  "compliance-styles.json"
4449
4988
  );
4450
- writeFileSync5(compStylesPath, JSON.stringify(complianceStylesMap, null, 2));
4989
+ writeFileSync6(compStylesPath, JSON.stringify(complianceStylesMap, null, 2));
4990
+ outputPaths.push(compStylesPath);
4451
4991
  process.stderr.write(`[scope/render] \u2713 Wrote compliance-styles.json
4452
4992
  `);
4453
4993
  process.stderr.write("\n");
4454
4994
  const summary = formatSummaryText(results, outputDir);
4455
4995
  process.stderr.write(`${summary}
4456
4996
  `);
4997
+ const summaryPath = writeRunSummary({
4998
+ command: "scope render all",
4999
+ status: failures.length > 0 ? "failed" : "success",
5000
+ outputPaths,
5001
+ failures
5002
+ });
5003
+ process.stderr.write(`[scope/render] Run summary written to ${summaryPath}
5004
+ `);
5005
+ if (failures.length > 0) process.exit(1);
4457
5006
  } catch (err) {
4458
5007
  await shutdownPool3();
4459
- process.stderr.write(`Error: ${err instanceof Error ? err.message : String(err)}
5008
+ process.stderr.write(`${formatScopeDiagnostic(err)}
4460
5009
  `);
4461
5010
  process.exit(1);
4462
5011
  }
@@ -4498,8 +5047,8 @@ function createRenderCommand() {
4498
5047
  }
4499
5048
 
4500
5049
  // src/report/baseline.ts
4501
- import { existsSync as existsSync9, mkdirSync as mkdirSync5, rmSync as rmSync2, writeFileSync as writeFileSync6 } from "fs";
4502
- import { resolve as resolve11 } from "path";
5050
+ import { existsSync as existsSync10, mkdirSync as mkdirSync6, rmSync as rmSync2, writeFileSync as writeFileSync7 } from "fs";
5051
+ import { resolve as resolve12 } from "path";
4503
5052
  import { generateManifest as generateManifest4 } from "@agent-scope/manifest";
4504
5053
  import { BrowserPool as BrowserPool4, safeRender as safeRender3 } from "@agent-scope/render";
4505
5054
  import { ComplianceEngine as ComplianceEngine2, TokenResolver as TokenResolver2 } from "@agent-scope/tokens";
@@ -4523,8 +5072,17 @@ async function shutdownPool4() {
4523
5072
  }
4524
5073
  }
4525
5074
  async function renderComponent2(filePath, componentName, props, viewportWidth, viewportHeight) {
5075
+ const PAD = 24;
4526
5076
  const pool = await getPool4(viewportWidth, viewportHeight);
4527
- const htmlHarness = await buildComponentHarness(filePath, componentName, props, viewportWidth);
5077
+ const htmlHarness = await buildComponentHarness(
5078
+ filePath,
5079
+ componentName,
5080
+ props,
5081
+ viewportWidth,
5082
+ void 0,
5083
+ void 0,
5084
+ PAD
5085
+ );
4528
5086
  const slot = await pool.acquire();
4529
5087
  const { page } = slot;
4530
5088
  try {
@@ -4546,8 +5104,8 @@ async function renderComponent2(filePath, componentName, props, viewportWidth, v
4546
5104
  const classes = await page.evaluate(() => {
4547
5105
  const set = /* @__PURE__ */ new Set();
4548
5106
  document.querySelectorAll("[class]").forEach((el) => {
4549
- for (const c of el.className.split(/\s+/)) {
4550
- if (c) set.add(c);
5107
+ for (const c of getElementClassNames(el)) {
5108
+ set.add(c);
4551
5109
  }
4552
5110
  });
4553
5111
  return [...set];
@@ -4564,7 +5122,6 @@ async function renderComponent2(filePath, componentName, props, viewportWidth, v
4564
5122
  `Component "${componentName}" rendered with zero bounding box \u2014 it may be invisible or not mounted`
4565
5123
  );
4566
5124
  }
4567
- const PAD = 24;
4568
5125
  const MIN_W = 320;
4569
5126
  const MIN_H = 200;
4570
5127
  const clipX = Math.max(0, boundingBox.x - PAD);
@@ -4648,20 +5205,20 @@ async function runBaseline(options = {}) {
4648
5205
  } = options;
4649
5206
  const startTime = performance.now();
4650
5207
  const rootDir = process.cwd();
4651
- const baselineDir = resolve11(rootDir, outputDir);
4652
- const rendersDir = resolve11(baselineDir, "renders");
4653
- if (existsSync9(baselineDir)) {
5208
+ const baselineDir = resolve12(rootDir, outputDir);
5209
+ const rendersDir = resolve12(baselineDir, "renders");
5210
+ if (existsSync10(baselineDir)) {
4654
5211
  rmSync2(baselineDir, { recursive: true, force: true });
4655
5212
  }
4656
- mkdirSync5(rendersDir, { recursive: true });
5213
+ mkdirSync6(rendersDir, { recursive: true });
4657
5214
  let manifest;
4658
5215
  if (manifestPath !== void 0) {
4659
- const { readFileSync: readFileSync14 } = await import("fs");
4660
- const absPath = resolve11(rootDir, manifestPath);
4661
- if (!existsSync9(absPath)) {
5216
+ const { readFileSync: readFileSync18 } = await import("fs");
5217
+ const absPath = resolve12(rootDir, manifestPath);
5218
+ if (!existsSync10(absPath)) {
4662
5219
  throw new Error(`Manifest not found at ${absPath}.`);
4663
5220
  }
4664
- manifest = JSON.parse(readFileSync14(absPath, "utf-8"));
5221
+ manifest = JSON.parse(readFileSync18(absPath, "utf-8"));
4665
5222
  process.stderr.write(`Loaded manifest from ${manifestPath}
4666
5223
  `);
4667
5224
  } else {
@@ -4671,7 +5228,7 @@ async function runBaseline(options = {}) {
4671
5228
  process.stderr.write(`Found ${count} components.
4672
5229
  `);
4673
5230
  }
4674
- writeFileSync6(resolve11(baselineDir, "manifest.json"), JSON.stringify(manifest, null, 2), "utf-8");
5231
+ writeFileSync7(resolve12(baselineDir, "manifest.json"), JSON.stringify(manifest, null, 2), "utf-8");
4675
5232
  let componentNames = Object.keys(manifest.components);
4676
5233
  if (componentsGlob !== void 0) {
4677
5234
  componentNames = componentNames.filter((name) => matchGlob(componentsGlob, name));
@@ -4691,8 +5248,8 @@ async function runBaseline(options = {}) {
4691
5248
  aggregateCompliance: 1,
4692
5249
  auditedAt: (/* @__PURE__ */ new Date()).toISOString()
4693
5250
  };
4694
- writeFileSync6(
4695
- resolve11(baselineDir, "compliance.json"),
5251
+ writeFileSync7(
5252
+ resolve12(baselineDir, "compliance.json"),
4696
5253
  JSON.stringify(emptyReport, null, 2),
4697
5254
  "utf-8"
4698
5255
  );
@@ -4713,7 +5270,7 @@ async function runBaseline(options = {}) {
4713
5270
  const renderOne = async (name) => {
4714
5271
  const descriptor = manifest.components[name];
4715
5272
  if (descriptor === void 0) return;
4716
- const filePath = resolve11(rootDir, descriptor.filePath);
5273
+ const filePath = resolve12(rootDir, descriptor.filePath);
4717
5274
  const outcome = await safeRender3(
4718
5275
  () => renderComponent2(filePath, name, {}, viewportWidth, viewportHeight),
4719
5276
  {
@@ -4732,8 +5289,8 @@ async function runBaseline(options = {}) {
4732
5289
  }
4733
5290
  if (outcome.crashed) {
4734
5291
  failureCount++;
4735
- const errPath = resolve11(rendersDir, `${name}.error.json`);
4736
- writeFileSync6(
5292
+ const errPath = resolve12(rendersDir, `${name}.error.json`);
5293
+ writeFileSync7(
4737
5294
  errPath,
4738
5295
  JSON.stringify(
4739
5296
  {
@@ -4750,10 +5307,10 @@ async function runBaseline(options = {}) {
4750
5307
  return;
4751
5308
  }
4752
5309
  const result = outcome.result;
4753
- writeFileSync6(resolve11(rendersDir, `${name}.png`), result.screenshot);
5310
+ writeFileSync7(resolve12(rendersDir, `${name}.png`), result.screenshot);
4754
5311
  const jsonOutput = formatRenderJson(name, {}, result);
4755
- writeFileSync6(
4756
- resolve11(rendersDir, `${name}.json`),
5312
+ writeFileSync7(
5313
+ resolve12(rendersDir, `${name}.json`),
4757
5314
  JSON.stringify(jsonOutput, null, 2),
4758
5315
  "utf-8"
4759
5316
  );
@@ -4780,8 +5337,8 @@ async function runBaseline(options = {}) {
4780
5337
  const resolver = new TokenResolver2([]);
4781
5338
  const engine = new ComplianceEngine2(resolver);
4782
5339
  const batchReport = engine.auditBatch(computedStylesMap);
4783
- writeFileSync6(
4784
- resolve11(baselineDir, "compliance.json"),
5340
+ writeFileSync7(
5341
+ resolve12(baselineDir, "compliance.json"),
4785
5342
  JSON.stringify(batchReport, null, 2),
4786
5343
  "utf-8"
4787
5344
  );
@@ -4824,22 +5381,22 @@ function registerBaselineSubCommand(reportCmd) {
4824
5381
  }
4825
5382
 
4826
5383
  // src/report/diff.ts
4827
- import { existsSync as existsSync10, readFileSync as readFileSync8, writeFileSync as writeFileSync7 } from "fs";
4828
- import { resolve as resolve12 } from "path";
5384
+ import { existsSync as existsSync11, readFileSync as readFileSync10, writeFileSync as writeFileSync8 } from "fs";
5385
+ import { resolve as resolve13 } from "path";
4829
5386
  import { generateManifest as generateManifest5 } from "@agent-scope/manifest";
4830
5387
  import { BrowserPool as BrowserPool5, safeRender as safeRender4 } from "@agent-scope/render";
4831
5388
  import { ComplianceEngine as ComplianceEngine3, TokenResolver as TokenResolver3 } from "@agent-scope/tokens";
4832
5389
  var DEFAULT_BASELINE_DIR2 = ".reactscope/baseline";
4833
5390
  function loadBaselineCompliance(baselineDir) {
4834
- const compliancePath = resolve12(baselineDir, "compliance.json");
4835
- if (!existsSync10(compliancePath)) return null;
4836
- const raw = JSON.parse(readFileSync8(compliancePath, "utf-8"));
5391
+ const compliancePath = resolve13(baselineDir, "compliance.json");
5392
+ if (!existsSync11(compliancePath)) return null;
5393
+ const raw = JSON.parse(readFileSync10(compliancePath, "utf-8"));
4837
5394
  return raw;
4838
5395
  }
4839
5396
  function loadBaselineRenderJson2(baselineDir, componentName) {
4840
- const jsonPath = resolve12(baselineDir, "renders", `${componentName}.json`);
4841
- if (!existsSync10(jsonPath)) return null;
4842
- return JSON.parse(readFileSync8(jsonPath, "utf-8"));
5397
+ const jsonPath = resolve13(baselineDir, "renders", `${componentName}.json`);
5398
+ if (!existsSync11(jsonPath)) return null;
5399
+ return JSON.parse(readFileSync10(jsonPath, "utf-8"));
4843
5400
  }
4844
5401
  var _pool5 = null;
4845
5402
  async function getPool5(viewportWidth, viewportHeight) {
@@ -4860,8 +5417,17 @@ async function shutdownPool5() {
4860
5417
  }
4861
5418
  }
4862
5419
  async function renderComponent3(filePath, componentName, props, viewportWidth, viewportHeight) {
5420
+ const PAD = 24;
4863
5421
  const pool = await getPool5(viewportWidth, viewportHeight);
4864
- const htmlHarness = await buildComponentHarness(filePath, componentName, props, viewportWidth);
5422
+ const htmlHarness = await buildComponentHarness(
5423
+ filePath,
5424
+ componentName,
5425
+ props,
5426
+ viewportWidth,
5427
+ void 0,
5428
+ void 0,
5429
+ PAD
5430
+ );
4865
5431
  const slot = await pool.acquire();
4866
5432
  const { page } = slot;
4867
5433
  try {
@@ -4883,8 +5449,8 @@ async function renderComponent3(filePath, componentName, props, viewportWidth, v
4883
5449
  const classes = await page.evaluate(() => {
4884
5450
  const set = /* @__PURE__ */ new Set();
4885
5451
  document.querySelectorAll("[class]").forEach((el) => {
4886
- for (const c of el.className.split(/\s+/)) {
4887
- if (c) set.add(c);
5452
+ for (const c of getElementClassNames(el)) {
5453
+ set.add(c);
4888
5454
  }
4889
5455
  });
4890
5456
  return [...set];
@@ -4901,7 +5467,6 @@ async function renderComponent3(filePath, componentName, props, viewportWidth, v
4901
5467
  `Component "${componentName}" rendered with zero bounding box \u2014 it may be invisible or not mounted`
4902
5468
  );
4903
5469
  }
4904
- const PAD = 24;
4905
5470
  const MIN_W = 320;
4906
5471
  const MIN_H = 200;
4907
5472
  const clipX = Math.max(0, boundingBox.x - PAD);
@@ -4998,6 +5563,7 @@ function classifyComponent(entry, regressionThreshold) {
4998
5563
  async function runDiff(options = {}) {
4999
5564
  const {
5000
5565
  baselineDir: baselineDirRaw = DEFAULT_BASELINE_DIR2,
5566
+ complianceTokens = [],
5001
5567
  componentsGlob,
5002
5568
  manifestPath,
5003
5569
  viewportWidth = 375,
@@ -5006,19 +5572,19 @@ async function runDiff(options = {}) {
5006
5572
  } = options;
5007
5573
  const startTime = performance.now();
5008
5574
  const rootDir = process.cwd();
5009
- const baselineDir = resolve12(rootDir, baselineDirRaw);
5010
- if (!existsSync10(baselineDir)) {
5575
+ const baselineDir = resolve13(rootDir, baselineDirRaw);
5576
+ if (!existsSync11(baselineDir)) {
5011
5577
  throw new Error(
5012
5578
  `Baseline directory not found at "${baselineDir}". Run \`scope report baseline\` first to create a baseline snapshot.`
5013
5579
  );
5014
5580
  }
5015
- const baselineManifestPath = resolve12(baselineDir, "manifest.json");
5016
- if (!existsSync10(baselineManifestPath)) {
5581
+ const baselineManifestPath = resolve13(baselineDir, "manifest.json");
5582
+ if (!existsSync11(baselineManifestPath)) {
5017
5583
  throw new Error(
5018
5584
  `Baseline manifest.json not found at "${baselineManifestPath}". The baseline directory may be incomplete \u2014 re-run \`scope report baseline\`.`
5019
5585
  );
5020
5586
  }
5021
- const baselineManifest = JSON.parse(readFileSync8(baselineManifestPath, "utf-8"));
5587
+ const baselineManifest = JSON.parse(readFileSync10(baselineManifestPath, "utf-8"));
5022
5588
  const baselineCompliance = loadBaselineCompliance(baselineDir);
5023
5589
  const baselineComponentNames = new Set(Object.keys(baselineManifest.components));
5024
5590
  process.stderr.write(
@@ -5027,11 +5593,11 @@ async function runDiff(options = {}) {
5027
5593
  );
5028
5594
  let currentManifest;
5029
5595
  if (manifestPath !== void 0) {
5030
- const absPath = resolve12(rootDir, manifestPath);
5031
- if (!existsSync10(absPath)) {
5596
+ const absPath = resolve13(rootDir, manifestPath);
5597
+ if (!existsSync11(absPath)) {
5032
5598
  throw new Error(`Manifest not found at "${absPath}".`);
5033
5599
  }
5034
- currentManifest = JSON.parse(readFileSync8(absPath, "utf-8"));
5600
+ currentManifest = JSON.parse(readFileSync10(absPath, "utf-8"));
5035
5601
  process.stderr.write(`Loaded manifest from ${manifestPath}
5036
5602
  `);
5037
5603
  } else {
@@ -5064,7 +5630,7 @@ async function runDiff(options = {}) {
5064
5630
  const renderOne = async (name) => {
5065
5631
  const descriptor = currentManifest.components[name];
5066
5632
  if (descriptor === void 0) return;
5067
- const filePath = resolve12(rootDir, descriptor.filePath);
5633
+ const filePath = resolve13(rootDir, descriptor.filePath);
5068
5634
  const outcome = await safeRender4(
5069
5635
  () => renderComponent3(filePath, name, {}, viewportWidth, viewportHeight),
5070
5636
  {
@@ -5113,7 +5679,7 @@ async function runDiff(options = {}) {
5113
5679
  if (isTTY() && total > 0) {
5114
5680
  process.stderr.write("\n");
5115
5681
  }
5116
- const resolver = new TokenResolver3([]);
5682
+ const resolver = new TokenResolver3(complianceTokens);
5117
5683
  const engine = new ComplianceEngine3(resolver);
5118
5684
  const currentBatchReport = engine.auditBatch(computedStylesMap);
5119
5685
  const entries = [];
@@ -5282,7 +5848,7 @@ function registerDiffSubCommand(reportCmd) {
5282
5848
  regressionThreshold
5283
5849
  });
5284
5850
  if (opts.output !== void 0) {
5285
- writeFileSync7(opts.output, JSON.stringify(result, null, 2), "utf-8");
5851
+ writeFileSync8(opts.output, JSON.stringify(result, null, 2), "utf-8");
5286
5852
  process.stderr.write(`Diff written to ${opts.output}
5287
5853
  `);
5288
5854
  }
@@ -5304,8 +5870,8 @@ function registerDiffSubCommand(reportCmd) {
5304
5870
  }
5305
5871
 
5306
5872
  // src/report/pr-comment.ts
5307
- import { existsSync as existsSync11, readFileSync as readFileSync9, writeFileSync as writeFileSync8 } from "fs";
5308
- import { resolve as resolve13 } from "path";
5873
+ import { existsSync as existsSync12, readFileSync as readFileSync11, writeFileSync as writeFileSync9 } from "fs";
5874
+ import { resolve as resolve14 } from "path";
5309
5875
  var STATUS_BADGE = {
5310
5876
  added: "\u2705 added",
5311
5877
  removed: "\u{1F5D1}\uFE0F removed",
@@ -5388,13 +5954,13 @@ function formatPrComment(diff) {
5388
5954
  return lines.join("\n");
5389
5955
  }
5390
5956
  function loadDiffResult(filePath) {
5391
- const abs = resolve13(filePath);
5392
- if (!existsSync11(abs)) {
5957
+ const abs = resolve14(filePath);
5958
+ if (!existsSync12(abs)) {
5393
5959
  throw new Error(`DiffResult file not found: ${abs}`);
5394
5960
  }
5395
5961
  let raw;
5396
5962
  try {
5397
- raw = readFileSync9(abs, "utf-8");
5963
+ raw = readFileSync11(abs, "utf-8");
5398
5964
  } catch (err) {
5399
5965
  throw new Error(
5400
5966
  `Failed to read DiffResult file: ${err instanceof Error ? err.message : String(err)}`
@@ -5421,7 +5987,7 @@ function registerPrCommentSubCommand(reportCmd) {
5421
5987
  const diff = loadDiffResult(opts.input);
5422
5988
  const comment = formatPrComment(diff);
5423
5989
  if (opts.output !== void 0) {
5424
- writeFileSync8(resolve13(opts.output), comment, "utf-8");
5990
+ writeFileSync9(resolve14(opts.output), comment, "utf-8");
5425
5991
  process.stderr.write(`PR comment written to ${opts.output}
5426
5992
  `);
5427
5993
  } else {
@@ -5716,60 +6282,678 @@ function buildStructuredReport(report) {
5716
6282
  }
5717
6283
 
5718
6284
  // src/site-commands.ts
5719
- import { createReadStream, existsSync as existsSync12, statSync as statSync2 } from "fs";
6285
+ import {
6286
+ createReadStream,
6287
+ existsSync as existsSync13,
6288
+ watch as fsWatch,
6289
+ readFileSync as readFileSync12,
6290
+ statSync as statSync2,
6291
+ writeFileSync as writeFileSync10
6292
+ } from "fs";
6293
+ import { mkdir, writeFile } from "fs/promises";
5720
6294
  import { createServer } from "http";
5721
- import { extname, join as join5, resolve as resolve14 } from "path";
6295
+ import { extname, join as join6, resolve as resolve15 } from "path";
6296
+ import { generateManifest as generateManifest6 } from "@agent-scope/manifest";
6297
+ import { safeRender as safeRender5 } from "@agent-scope/render";
5722
6298
  import { buildSite } from "@agent-scope/site";
5723
6299
  import { Command as Command9 } from "commander";
5724
- var MIME_TYPES = {
5725
- ".html": "text/html; charset=utf-8",
5726
- ".css": "text/css; charset=utf-8",
5727
- ".js": "application/javascript; charset=utf-8",
5728
- ".json": "application/json; charset=utf-8",
5729
- ".png": "image/png",
5730
- ".jpg": "image/jpeg",
5731
- ".jpeg": "image/jpeg",
5732
- ".svg": "image/svg+xml",
5733
- ".ico": "image/x-icon"
5734
- };
5735
- function registerBuild(siteCmd) {
5736
- siteCmd.command("build").description(
5737
- 'Build the static HTML site from manifest + render outputs.\n\nINPUT DIRECTORY (.reactscope/ by default) must contain:\n manifest.json component registry\n renders/ screenshots and render.json files from `scope render all`\n\nOPTIONAL:\n --compliance <path> include token compliance scores on detail pages\n --base-path <path> set if deploying to a subdirectory (e.g. /ui-docs)\n\nExamples:\n scope site build\n scope site build --title "Design System" -o .reactscope/site\n scope site build --compliance .reactscope/compliance-styles.json\n scope site build --base-path /ui'
5738
- ).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(
5739
- async (opts) => {
5740
- try {
5741
- const inputDir = resolve14(process.cwd(), opts.input);
5742
- const outputDir = resolve14(process.cwd(), opts.output);
5743
- if (!existsSync12(inputDir)) {
5744
- throw new Error(
5745
- `Input directory not found: ${inputDir}
5746
- Run \`scope manifest generate\` and \`scope render\` first.`
5747
- );
5748
- }
5749
- const manifestPath = join5(inputDir, "manifest.json");
5750
- if (!existsSync12(manifestPath)) {
5751
- throw new Error(
5752
- `Manifest not found at ${manifestPath}
5753
- Run \`scope manifest generate\` first.`
5754
- );
5755
- }
5756
- process.stderr.write(`Building site from ${inputDir}\u2026
5757
- `);
5758
- await buildSite({
5759
- inputDir,
5760
- outputDir,
5761
- basePath: opts.basePath,
5762
- ...opts.compliance !== void 0 && {
5763
- compliancePath: resolve14(process.cwd(), opts.compliance)
5764
- },
5765
- title: opts.title
5766
- });
6300
+
6301
+ // src/playground-bundler.ts
6302
+ import { dirname as dirname5 } from "path";
6303
+ import * as esbuild3 from "esbuild";
6304
+ async function buildPlaygroundHarness(filePath, componentName, projectCss, wrapperScript) {
6305
+ const bundledScript = await bundlePlaygroundIIFE(filePath, componentName);
6306
+ return wrapPlaygroundHtml(bundledScript, projectCss, wrapperScript);
6307
+ }
6308
+ async function bundlePlaygroundIIFE(filePath, componentName) {
6309
+ const wrapperCode = (
6310
+ /* ts */
6311
+ `
6312
+ import * as __scopeMod from ${JSON.stringify(filePath)};
6313
+ import { createRoot } from "react-dom/client";
6314
+ import { createElement, Component as ReactComponent } from "react";
6315
+
6316
+ (function scopePlaygroundHarness() {
6317
+ var Target =
6318
+ __scopeMod["default"] ||
6319
+ __scopeMod[${JSON.stringify(componentName)}] ||
6320
+ (Object.values(__scopeMod).find(
6321
+ function(v) { return typeof v === "function" && /^[A-Z]/.test(v.name || ""); }
6322
+ ));
6323
+
6324
+ if (!Target) {
6325
+ document.getElementById("scope-root").innerHTML =
6326
+ '<p style="color:#dc2626;font-family:system-ui;font-size:13px">No renderable component found.</p>';
6327
+ return;
6328
+ }
6329
+
6330
+ // Error boundary to catch async render errors (React unmounts the whole
6331
+ // root when an error is uncaught \u2014 this keeps the error visible instead).
6332
+ var errorStyle = "color:#dc2626;font-family:system-ui;font-size:13px;padding:12px";
6333
+ class ScopeBoundary extends ReactComponent {
6334
+ constructor(p) { super(p); this.state = { error: null }; }
6335
+ static getDerivedStateFromError(err) { return { error: err }; }
6336
+ render() {
6337
+ if (this.state.error) {
6338
+ return createElement("pre", { style: errorStyle },
6339
+ "Render error: " + (this.state.error.message || String(this.state.error)));
6340
+ }
6341
+ return this.props.children;
6342
+ }
6343
+ }
6344
+
6345
+ var rootEl = document.getElementById("scope-root");
6346
+ var root = createRoot(rootEl);
6347
+ var Wrapper = window.__SCOPE_WRAPPER__;
6348
+
6349
+ function render(props) {
6350
+ var inner = createElement(Target, props);
6351
+ if (Wrapper) inner = createElement(Wrapper, null, inner);
6352
+ root.render(createElement(ScopeBoundary, null, inner));
6353
+ }
6354
+
6355
+ // Render immediately with empty props
6356
+ render({});
6357
+
6358
+ // Listen for messages from the parent frame
6359
+ window.addEventListener("message", function(e) {
6360
+ if (!e.data) return;
6361
+ if (e.data.type === "scope-playground-props") {
6362
+ render(e.data.props || {});
6363
+ } else if (e.data.type === "scope-playground-theme") {
6364
+ document.documentElement.classList.toggle("dark", e.data.theme === "dark");
6365
+ }
6366
+ });
6367
+
6368
+ // Report content height changes to the parent frame
6369
+ var ro = new ResizeObserver(function() {
6370
+ var h = rootEl.scrollHeight;
6371
+ if (parent !== window) {
6372
+ parent.postMessage({ type: "scope-playground-height", height: h }, "*");
6373
+ }
6374
+ });
6375
+ ro.observe(rootEl);
6376
+ })();
6377
+ `
6378
+ );
6379
+ const result = await esbuild3.build({
6380
+ stdin: {
6381
+ contents: wrapperCode,
6382
+ resolveDir: dirname5(filePath),
6383
+ loader: "tsx",
6384
+ sourcefile: "__scope_playground__.tsx"
6385
+ },
6386
+ bundle: true,
6387
+ format: "iife",
6388
+ write: false,
6389
+ platform: "browser",
6390
+ jsx: "automatic",
6391
+ jsxImportSource: "react",
6392
+ target: "es2020",
6393
+ external: [],
6394
+ define: {
6395
+ "process.env.NODE_ENV": '"production"',
6396
+ global: "globalThis"
6397
+ },
6398
+ logLevel: "silent",
6399
+ banner: {
6400
+ js: "/* @agent-scope/cli playground harness */"
6401
+ },
6402
+ loader: {
6403
+ ".css": "empty",
6404
+ ".svg": "dataurl",
6405
+ ".png": "dataurl",
6406
+ ".jpg": "dataurl",
6407
+ ".jpeg": "dataurl",
6408
+ ".gif": "dataurl",
6409
+ ".webp": "dataurl",
6410
+ ".ttf": "dataurl",
6411
+ ".woff": "dataurl",
6412
+ ".woff2": "dataurl"
6413
+ }
6414
+ });
6415
+ if (result.errors.length > 0) {
6416
+ const msg = result.errors.map((e) => `${e.text}${e.location ? ` (${e.location.file}:${e.location.line})` : ""}`).join("\n");
6417
+ throw new Error(`esbuild failed to bundle playground component:
6418
+ ${msg}`);
6419
+ }
6420
+ const outputFile = result.outputFiles?.[0];
6421
+ if (outputFile === void 0 || outputFile.text.length === 0) {
6422
+ throw new Error("esbuild produced no playground output");
6423
+ }
6424
+ return outputFile.text;
6425
+ }
6426
+ function wrapPlaygroundHtml(bundledScript, projectCss, wrapperScript) {
6427
+ const projectStyleBlock = projectCss != null && projectCss.length > 0 ? `<style id="scope-project-css">
6428
+ ${projectCss.replace(/<\/style>/gi, "<\\/style>")}
6429
+ </style>` : "";
6430
+ const wrapperScriptBlock = wrapperScript != null && wrapperScript.length > 0 ? `<script id="scope-wrapper-script">${wrapperScript}</script>` : "";
6431
+ return `<!DOCTYPE html>
6432
+ <html lang="en">
6433
+ <head>
6434
+ <meta charset="UTF-8" />
6435
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6436
+ <script>
6437
+ window.__SCOPE_WRAPPER__ = null;
6438
+ // Prevent React DevTools from interfering with the embedded playground.
6439
+ // The hook causes render instability in same-origin iframes.
6440
+ delete window.__REACT_DEVTOOLS_GLOBAL_HOOK__;
6441
+ </script>
6442
+ <style>
6443
+ *, *::before, *::after { box-sizing: border-box; }
6444
+ html, body { margin: 0; padding: 0; font-family: system-ui, sans-serif; }
6445
+ #scope-root { padding: 16px; min-width: 1px; min-height: 1px; }
6446
+ </style>
6447
+ ${projectStyleBlock}
6448
+ <style>html, body { background: transparent !important; }</style>
6449
+ </head>
6450
+ <body>
6451
+ <div id="scope-root" data-reactscope-root></div>
6452
+ ${wrapperScriptBlock}
6453
+ <script>${bundledScript}</script>
6454
+ </body>
6455
+ </html>`;
6456
+ }
6457
+
6458
+ // src/site-commands.ts
6459
+ var MIME_TYPES = {
6460
+ ".html": "text/html; charset=utf-8",
6461
+ ".css": "text/css; charset=utf-8",
6462
+ ".js": "application/javascript; charset=utf-8",
6463
+ ".json": "application/json; charset=utf-8",
6464
+ ".png": "image/png",
6465
+ ".jpg": "image/jpeg",
6466
+ ".jpeg": "image/jpeg",
6467
+ ".svg": "image/svg+xml",
6468
+ ".ico": "image/x-icon"
6469
+ };
6470
+ function slugify(name) {
6471
+ return name.replace(/([A-Z])/g, (m) => `-${m.toLowerCase()}`).replace(/^-/, "").replace(/[^a-z0-9-]/g, "-").replace(/-+/g, "-").replace(/^-|-$/g, "");
6472
+ }
6473
+ function loadGlobalCssFilesFromConfig2(cwd) {
6474
+ const configPath = resolve15(cwd, "reactscope.config.json");
6475
+ if (!existsSync13(configPath)) return [];
6476
+ try {
6477
+ const raw = readFileSync12(configPath, "utf-8");
6478
+ const cfg = JSON.parse(raw);
6479
+ return cfg.components?.wrappers?.globalCSS ?? [];
6480
+ } catch {
6481
+ return [];
6482
+ }
6483
+ }
6484
+ function loadIconPatternsFromConfig2(cwd) {
6485
+ const configPath = resolve15(cwd, "reactscope.config.json");
6486
+ if (!existsSync13(configPath)) return [];
6487
+ try {
6488
+ const raw = readFileSync12(configPath, "utf-8");
6489
+ const cfg = JSON.parse(raw);
6490
+ return cfg.icons?.patterns ?? [];
6491
+ } catch {
6492
+ return [];
6493
+ }
6494
+ }
6495
+ var LIVERELOAD_SCRIPT = `<script>(function(){var s=new EventSource("/__livereload");s.onmessage=function(e){if(e.data==="reload")location.reload()};s.onerror=function(){setTimeout(function(){location.reload()},2000)}})()</script>`;
6496
+ function injectLiveReloadScript(html) {
6497
+ const idx = html.lastIndexOf("</body>");
6498
+ if (idx >= 0) return html.slice(0, idx) + LIVERELOAD_SCRIPT + html.slice(idx);
6499
+ return html + LIVERELOAD_SCRIPT;
6500
+ }
6501
+ function loadWatchConfig(rootDir) {
6502
+ const configPath = resolve15(rootDir, "reactscope.config.json");
6503
+ if (!existsSync13(configPath)) return null;
6504
+ try {
6505
+ const raw = readFileSync12(configPath, "utf-8");
6506
+ const cfg = JSON.parse(raw);
6507
+ const result = {};
6508
+ const components = cfg.components;
6509
+ if (components && typeof components === "object") {
6510
+ if (Array.isArray(components.include)) result.include = components.include;
6511
+ if (Array.isArray(components.exclude)) result.exclude = components.exclude;
6512
+ }
6513
+ if (Array.isArray(cfg.internalPatterns))
6514
+ result.internalPatterns = cfg.internalPatterns;
6515
+ if (Array.isArray(cfg.collections)) result.collections = cfg.collections;
6516
+ const icons = cfg.icons;
6517
+ if (icons && typeof icons === "object" && Array.isArray(icons.patterns)) {
6518
+ result.iconPatterns = icons.patterns;
6519
+ }
6520
+ return result;
6521
+ } catch {
6522
+ return null;
6523
+ }
6524
+ }
6525
+ function watchGlob(pattern, filePath) {
6526
+ const escaped = pattern.replace(/[.+^${}()|[\]\\]/g, "\\$&");
6527
+ const regexStr = escaped.replace(/\*\*/g, "\xA7GLOBSTAR\xA7").replace(/\*/g, "[^/]*").replace(/\u00a7GLOBSTAR\u00a7/g, ".*");
6528
+ return new RegExp(`^${regexStr}$`, "i").test(filePath);
6529
+ }
6530
+ function matchesWatchPatterns(filePath, include, exclude) {
6531
+ for (const pattern of exclude) {
6532
+ if (watchGlob(pattern, filePath)) return false;
6533
+ }
6534
+ for (const pattern of include) {
6535
+ if (watchGlob(pattern, filePath)) return true;
6536
+ }
6537
+ return false;
6538
+ }
6539
+ function findAffectedComponents(manifest, changedFiles, previousManifest) {
6540
+ const affected = /* @__PURE__ */ new Set();
6541
+ const normalised = changedFiles.map((f) => f.replace(/\\/g, "/"));
6542
+ for (const [name, descriptor] of Object.entries(manifest.components)) {
6543
+ const componentFile = descriptor.filePath.replace(/\\/g, "/");
6544
+ for (const changed of normalised) {
6545
+ if (componentFile === changed) {
6546
+ affected.add(name);
6547
+ break;
6548
+ }
6549
+ const scopeBase = changed.replace(/\.scope\.(ts|tsx|js|jsx)$/, "");
6550
+ const compBase = componentFile.replace(/\.(tsx|ts|jsx|js)$/, "");
6551
+ if (scopeBase !== changed && compBase === scopeBase) {
6552
+ affected.add(name);
6553
+ break;
6554
+ }
6555
+ }
6556
+ }
6557
+ if (previousManifest) {
6558
+ const oldNames = new Set(Object.keys(previousManifest.components));
6559
+ for (const name of Object.keys(manifest.components)) {
6560
+ if (!oldNames.has(name)) affected.add(name);
6561
+ }
6562
+ }
6563
+ return [...affected];
6564
+ }
6565
+ async function renderComponentsForWatch(manifest, componentNames, rootDir, inputDir) {
6566
+ if (componentNames.length === 0) return;
6567
+ const rendersDir = join6(inputDir, "renders");
6568
+ await mkdir(rendersDir, { recursive: true });
6569
+ const cssFiles = loadGlobalCssFilesFromConfig2(rootDir);
6570
+ const iconPatterns = loadIconPatternsFromConfig2(rootDir);
6571
+ const complianceStylesPath = join6(inputDir, "compliance-styles.json");
6572
+ let complianceStyles = {};
6573
+ if (existsSync13(complianceStylesPath)) {
6574
+ try {
6575
+ complianceStyles = JSON.parse(readFileSync12(complianceStylesPath, "utf-8"));
6576
+ } catch {
6577
+ }
6578
+ }
6579
+ for (const name of componentNames) {
6580
+ const descriptor = manifest.components[name];
6581
+ if (!descriptor) continue;
6582
+ const filePath = resolve15(rootDir, descriptor.filePath);
6583
+ const isIcon = isIconComponent(descriptor.filePath, name, iconPatterns);
6584
+ let scopeData = null;
6585
+ try {
6586
+ scopeData = await loadScopeFileForComponent(filePath);
6587
+ } catch {
6588
+ }
6589
+ const scenarioEntries = scopeData ? Object.entries(scopeData.scenarios) : [];
6590
+ const defaultEntry = scenarioEntries.find(([k]) => k === "default") ?? scenarioEntries[0];
6591
+ const renderProps = defaultEntry?.[1] ?? {};
6592
+ let wrapperScript;
6593
+ try {
6594
+ wrapperScript = scopeData?.hasWrapper ? await buildWrapperScript(scopeData.filePath) : void 0;
6595
+ } catch {
6596
+ }
6597
+ const renderer = buildRenderer(
6598
+ filePath,
6599
+ name,
6600
+ 375,
6601
+ 812,
6602
+ cssFiles,
6603
+ rootDir,
6604
+ wrapperScript,
6605
+ isIcon
6606
+ );
6607
+ const outcome = await safeRender5(
6608
+ () => renderer.renderCell(renderProps, descriptor.complexityClass),
6609
+ {
6610
+ props: renderProps,
6611
+ sourceLocation: { file: descriptor.filePath, line: descriptor.loc.start, column: 0 }
6612
+ }
6613
+ );
6614
+ if (outcome.crashed) {
6615
+ process.stderr.write(` \u2717 ${name}: ${outcome.error.message}
6616
+ `);
6617
+ continue;
6618
+ }
6619
+ const result = outcome.result;
6620
+ if (!isIcon) {
6621
+ writeFileSync10(join6(rendersDir, `${name}.png`), result.screenshot);
6622
+ }
6623
+ const renderJson = formatRenderJson(name, renderProps, result);
6624
+ const extResult = result;
6625
+ if (isIcon && extResult.svgContent) {
6626
+ renderJson.svgContent = extResult.svgContent;
6627
+ delete renderJson.screenshot;
6628
+ }
6629
+ writeFileSync10(join6(rendersDir, `${name}.json`), JSON.stringify(renderJson, null, 2));
6630
+ const rawStyles = result.computedStyles["[data-reactscope-root] > *"] ?? {};
6631
+ const compStyles = {
6632
+ colors: {},
6633
+ spacing: {},
6634
+ typography: {},
6635
+ borders: {},
6636
+ shadows: {}
6637
+ };
6638
+ for (const [prop, val] of Object.entries(rawStyles)) {
6639
+ if (!val || val === "none" || val === "") continue;
6640
+ const lower = prop.toLowerCase();
6641
+ if (lower.includes("color") || lower.includes("background")) {
6642
+ compStyles.colors[prop] = val;
6643
+ } else if (lower.includes("padding") || lower.includes("margin") || lower.includes("gap") || lower.includes("width") || lower.includes("height")) {
6644
+ compStyles.spacing[prop] = val;
6645
+ } else if (lower.includes("font") || lower.includes("lineheight") || lower.includes("letterspacing") || lower.includes("texttransform")) {
6646
+ compStyles.typography[prop] = val;
6647
+ } else if (lower.includes("border") || lower.includes("radius") || lower.includes("outline")) {
6648
+ compStyles.borders[prop] = val;
6649
+ } else if (lower.includes("shadow")) {
6650
+ compStyles.shadows[prop] = val;
6651
+ }
6652
+ }
6653
+ complianceStyles[name] = compStyles;
6654
+ process.stderr.write(` \u2713 ${name} (${result.renderTimeMs.toFixed(0)}ms)
6655
+ `);
6656
+ }
6657
+ await shutdownPool3();
6658
+ writeFileSync10(complianceStylesPath, JSON.stringify(complianceStyles, null, 2), "utf-8");
6659
+ }
6660
+ async function watchRebuildSite(inputDir, outputDir, title, basePath) {
6661
+ const rootDir = process.cwd();
6662
+ await generatePlaygrounds(inputDir, outputDir);
6663
+ const iconPatterns = loadIconPatternsFromConfig2(rootDir);
6664
+ let tokenFilePath;
6665
+ const autoPath = resolve15(rootDir, "reactscope.tokens.json");
6666
+ if (existsSync13(autoPath)) tokenFilePath = autoPath;
6667
+ let compliancePath;
6668
+ const crPath = join6(inputDir, "compliance-report.json");
6669
+ if (existsSync13(crPath)) compliancePath = crPath;
6670
+ await buildSite({
6671
+ inputDir,
6672
+ outputDir,
6673
+ basePath,
6674
+ ...compliancePath && { compliancePath },
6675
+ ...tokenFilePath && { tokenFilePath },
6676
+ title,
6677
+ iconPatterns
6678
+ });
6679
+ }
6680
+ function findStaleComponents(manifest, previousManifest, rendersDir) {
6681
+ const stale = [];
6682
+ for (const [name, descriptor] of Object.entries(manifest.components)) {
6683
+ const jsonPath = join6(rendersDir, `${name}.json`);
6684
+ if (!existsSync13(jsonPath)) {
6685
+ stale.push(name);
6686
+ continue;
6687
+ }
6688
+ if (!previousManifest) continue;
6689
+ const prev = previousManifest.components[name];
6690
+ if (!prev) {
6691
+ stale.push(name);
6692
+ continue;
6693
+ }
6694
+ if (JSON.stringify(prev) !== JSON.stringify(descriptor)) {
6695
+ stale.push(name);
6696
+ }
6697
+ }
6698
+ return stale;
6699
+ }
6700
+ async function runFullBuild(rootDir, inputDir, outputDir, title, basePath) {
6701
+ process.stderr.write("[watch] Starting\u2026\n");
6702
+ const config = loadWatchConfig(rootDir);
6703
+ const manifestPath = join6(inputDir, "manifest.json");
6704
+ let previousManifest = null;
6705
+ if (existsSync13(manifestPath)) {
6706
+ try {
6707
+ previousManifest = JSON.parse(readFileSync12(manifestPath, "utf-8"));
6708
+ } catch {
6709
+ }
6710
+ }
6711
+ process.stderr.write("[watch] Generating manifest\u2026\n");
6712
+ const manifest = await generateManifest6({
6713
+ rootDir,
6714
+ ...config?.include && { include: config.include },
6715
+ ...config?.exclude && { exclude: config.exclude },
6716
+ ...config?.internalPatterns && { internalPatterns: config.internalPatterns },
6717
+ ...config?.collections && { collections: config.collections },
6718
+ ...config?.iconPatterns && { iconPatterns: config.iconPatterns }
6719
+ });
6720
+ await mkdir(inputDir, { recursive: true });
6721
+ writeFileSync10(join6(inputDir, "manifest.json"), JSON.stringify(manifest, null, 2), "utf-8");
6722
+ const count = Object.keys(manifest.components).length;
6723
+ process.stderr.write(`[watch] Found ${count} components
6724
+ `);
6725
+ const rendersDir = join6(inputDir, "renders");
6726
+ const stale = findStaleComponents(manifest, previousManifest, rendersDir);
6727
+ if (stale.length > 0) {
6728
+ process.stderr.write(
6729
+ `[watch] Rendering ${stale.length} component(s) (${count - stale.length} already up-to-date)
6730
+ `
6731
+ );
6732
+ await renderComponentsForWatch(manifest, stale, rootDir, inputDir);
6733
+ } else {
6734
+ process.stderr.write("[watch] All renders up-to-date, skipping render step\n");
6735
+ }
6736
+ process.stderr.write("[watch] Building site\u2026\n");
6737
+ await watchRebuildSite(inputDir, outputDir, title, basePath);
6738
+ process.stderr.write("[watch] Ready\n");
6739
+ return manifest;
6740
+ }
6741
+ function startFileWatcher(opts) {
6742
+ const { rootDir, inputDir, outputDir, title, basePath, notifyReload } = opts;
6743
+ let previousManifest = opts.previousManifest;
6744
+ const config = loadWatchConfig(rootDir);
6745
+ const includePatterns = config?.include ?? ["src/**/*.tsx", "src/**/*.ts"];
6746
+ const excludePatterns = config?.exclude ?? [
6747
+ "**/node_modules/**",
6748
+ "**/*.test.*",
6749
+ "**/*.spec.*",
6750
+ "**/dist/**",
6751
+ "**/*.d.ts"
6752
+ ];
6753
+ let debounceTimer = null;
6754
+ const pendingFiles = /* @__PURE__ */ new Set();
6755
+ let isRunning = false;
6756
+ const IGNORE_PREFIXES = ["node_modules/", ".reactscope/", "dist/", ".git/", ".next/", ".turbo/"];
6757
+ const handleChange = async () => {
6758
+ if (isRunning) return;
6759
+ isRunning = true;
6760
+ const changedFiles = [...pendingFiles];
6761
+ pendingFiles.clear();
6762
+ try {
6763
+ process.stderr.write(`
6764
+ [watch] ${changedFiles.length} file(s) changed
6765
+ `);
6766
+ process.stderr.write("[watch] Regenerating manifest\u2026\n");
6767
+ const newManifest = await generateManifest6({
6768
+ rootDir,
6769
+ ...config?.include && { include: config.include },
6770
+ ...config?.exclude && { exclude: config.exclude },
6771
+ ...config?.internalPatterns && { internalPatterns: config.internalPatterns },
6772
+ ...config?.collections && { collections: config.collections },
6773
+ ...config?.iconPatterns && { iconPatterns: config.iconPatterns }
6774
+ });
6775
+ writeFileSync10(join6(inputDir, "manifest.json"), JSON.stringify(newManifest, null, 2), "utf-8");
6776
+ const affected = findAffectedComponents(newManifest, changedFiles, previousManifest);
6777
+ if (affected.length > 0) {
6778
+ process.stderr.write(`[watch] Re-rendering: ${affected.join(", ")}
6779
+ `);
6780
+ await renderComponentsForWatch(newManifest, affected, rootDir, inputDir);
6781
+ } else {
6782
+ process.stderr.write("[watch] No components directly affected\n");
6783
+ }
6784
+ process.stderr.write("[watch] Rebuilding site\u2026\n");
6785
+ await watchRebuildSite(inputDir, outputDir, title, basePath);
6786
+ previousManifest = newManifest;
6787
+ process.stderr.write("[watch] Done\n");
6788
+ notifyReload();
6789
+ } catch (err) {
6790
+ process.stderr.write(`[watch] Error: ${err instanceof Error ? err.message : String(err)}
6791
+ `);
6792
+ } finally {
6793
+ isRunning = false;
6794
+ if (pendingFiles.size > 0) {
6795
+ handleChange();
6796
+ }
6797
+ }
6798
+ };
6799
+ const onFileChange = (_eventType, filename) => {
6800
+ if (!filename) return;
6801
+ const normalised = filename.replace(/\\/g, "/");
6802
+ for (const prefix of IGNORE_PREFIXES) {
6803
+ if (normalised.startsWith(prefix)) return;
6804
+ }
6805
+ if (!matchesWatchPatterns(normalised, includePatterns, excludePatterns)) return;
6806
+ pendingFiles.add(normalised);
6807
+ if (debounceTimer) clearTimeout(debounceTimer);
6808
+ debounceTimer = setTimeout(() => {
6809
+ debounceTimer = null;
6810
+ handleChange();
6811
+ }, 500);
6812
+ };
6813
+ try {
6814
+ fsWatch(rootDir, { recursive: true }, onFileChange);
6815
+ process.stderr.write(`[watch] Watching for changes (${includePatterns.join(", ")})
6816
+ `);
6817
+ } catch (err) {
6818
+ process.stderr.write(
6819
+ `[watch] Warning: Could not start watcher: ${err instanceof Error ? err.message : String(err)}
6820
+ `
6821
+ );
6822
+ }
6823
+ }
6824
+ async function generatePlaygrounds(inputDir, outputDir) {
6825
+ const manifestPath = join6(inputDir, "manifest.json");
6826
+ const raw = readFileSync12(manifestPath, "utf-8");
6827
+ const manifest = JSON.parse(raw);
6828
+ const rootDir = process.cwd();
6829
+ const componentNames = Object.keys(manifest.components);
6830
+ if (componentNames.length === 0) return [];
6831
+ const playgroundDir = join6(outputDir, "playground");
6832
+ await mkdir(playgroundDir, { recursive: true });
6833
+ const cssFiles = loadGlobalCssFilesFromConfig2(rootDir);
6834
+ const projectCss = await loadGlobalCss(cssFiles, rootDir) ?? void 0;
6835
+ let succeeded = 0;
6836
+ const failures = [];
6837
+ const allDefaults = {};
6838
+ for (const name of componentNames) {
6839
+ const descriptor = manifest.components[name];
6840
+ if (!descriptor) continue;
6841
+ const filePath = resolve15(rootDir, descriptor.filePath);
6842
+ const slug = slugify(name);
6843
+ try {
6844
+ const scopeData = await loadScopeFileForComponent(filePath);
6845
+ if (scopeData) {
6846
+ const defaultScenario = scopeData.scenarios.default ?? Object.values(scopeData.scenarios)[0];
6847
+ if (defaultScenario) allDefaults[name] = defaultScenario;
6848
+ }
6849
+ } catch {
6850
+ }
6851
+ try {
6852
+ const html = await buildPlaygroundHarness(filePath, name, projectCss);
6853
+ await writeFile(join6(playgroundDir, `${slug}.html`), html, "utf-8");
6854
+ succeeded++;
6855
+ } catch (err) {
6856
+ process.stderr.write(
6857
+ `[scope/site] \u26A0 playground skip: ${name} \u2014 ${err instanceof Error ? err.message : String(err)}
6858
+ `
6859
+ );
6860
+ failures.push({
6861
+ component: name,
6862
+ stage: "playground",
6863
+ message: err instanceof Error ? err.message : String(err),
6864
+ outputPath: join6(playgroundDir, `${slug}.html`)
6865
+ });
6866
+ }
6867
+ }
6868
+ await writeFile(
6869
+ join6(inputDir, "playground-defaults.json"),
6870
+ JSON.stringify(allDefaults, null, 2),
6871
+ "utf-8"
6872
+ );
6873
+ process.stderr.write(
6874
+ `[scope/site] Playgrounds: ${succeeded} built${failures.length > 0 ? `, ${failures.length} failed` : ""}
6875
+ `
6876
+ );
6877
+ return failures;
6878
+ }
6879
+ function registerBuild(siteCmd) {
6880
+ siteCmd.command("build").description(
6881
+ 'Build the static HTML site from manifest + render outputs.\n\nINPUT DIRECTORY (.reactscope/ by default) must contain:\n manifest.json component registry\n renders/ screenshots and render.json files from `scope render all`\n\nOPTIONAL:\n --compliance <path> include token compliance scores on detail pages\n --base-path <path> set if deploying to a subdirectory (e.g. /ui-docs)\n\nExamples:\n scope site build\n scope site build --title "Design System" -o .reactscope/site\n scope site build --compliance .reactscope/compliance-report.json\n scope site build --tokens reactscope.tokens.json\n scope site build --base-path /ui'
6882
+ ).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("--tokens <path>", "Path to reactscope.tokens.json (enables token browser page)").option("--title <text>", "Site title", "Scope \u2014 Component Gallery").action(
6883
+ async (opts) => {
6884
+ try {
6885
+ const inputDir = resolve15(process.cwd(), opts.input);
6886
+ const outputDir = resolve15(process.cwd(), opts.output);
6887
+ if (!existsSync13(inputDir)) {
6888
+ throw new Error(
6889
+ `Input directory not found: ${inputDir}
6890
+ Run \`scope manifest generate\` and \`scope render\` first.`
6891
+ );
6892
+ }
6893
+ const manifestPath = join6(inputDir, "manifest.json");
6894
+ if (!existsSync13(manifestPath)) {
6895
+ throw new Error(
6896
+ `Manifest not found at ${manifestPath}
6897
+ Run \`scope manifest generate\` first.`
6898
+ );
6899
+ }
6900
+ process.stderr.write(`Building site from ${inputDir}\u2026
6901
+ `);
6902
+ process.stderr.write("Bundling playgrounds\u2026\n");
6903
+ const failures = await generatePlaygrounds(inputDir, outputDir);
6904
+ const iconPatterns = loadIconPatternsFromConfig2(process.cwd());
6905
+ let tokenFilePath = opts.tokens ? resolve15(process.cwd(), opts.tokens) : void 0;
6906
+ if (tokenFilePath === void 0) {
6907
+ const autoPath = resolve15(process.cwd(), "reactscope.tokens.json");
6908
+ if (existsSync13(autoPath)) {
6909
+ tokenFilePath = autoPath;
6910
+ }
6911
+ }
6912
+ await buildSite({
6913
+ inputDir,
6914
+ outputDir,
6915
+ basePath: opts.basePath,
6916
+ ...opts.compliance !== void 0 && {
6917
+ compliancePath: resolve15(process.cwd(), opts.compliance)
6918
+ },
6919
+ ...tokenFilePath !== void 0 && { tokenFilePath },
6920
+ title: opts.title,
6921
+ iconPatterns
6922
+ });
6923
+ const manifest = JSON.parse(readFileSync12(manifestPath, "utf-8"));
6924
+ const componentCount = Object.keys(manifest.components).length;
6925
+ const generatedPlaygroundCount = componentCount === 0 ? 0 : statSync2(join6(outputDir, "playground")).isDirectory() ? componentCount - failures.length : 0;
6926
+ const siteFailures = [...failures];
6927
+ if (componentCount === 0) {
6928
+ siteFailures.push({
6929
+ component: "*",
6930
+ stage: "site",
6931
+ message: "Manifest contains zero components; generated site is structurally degraded.",
6932
+ outputPath: manifestPath
6933
+ });
6934
+ } else if (generatedPlaygroundCount === 0) {
6935
+ siteFailures.push({
6936
+ component: "*",
6937
+ stage: "site",
6938
+ message: "No playground pages were generated successfully; site build is degraded and should not be treated as green.",
6939
+ outputPath: join6(outputDir, "playground")
6940
+ });
6941
+ }
6942
+ const summaryPath = writeRunSummary({
6943
+ command: "scope site build",
6944
+ status: siteFailures.length > 0 ? "failed" : "success",
6945
+ outputPaths: [outputDir, join6(outputDir, "index.html")],
6946
+ failures: siteFailures
6947
+ });
5767
6948
  process.stderr.write(`Site written to ${outputDir}
6949
+ `);
6950
+ process.stderr.write(`[scope/site] Run summary written to ${summaryPath}
5768
6951
  `);
5769
6952
  process.stdout.write(`${outputDir}
5770
6953
  `);
6954
+ if (siteFailures.length > 0) process.exit(1);
5771
6955
  } catch (err) {
5772
- process.stderr.write(`Error: ${err instanceof Error ? err.message : String(err)}
6956
+ process.stderr.write(`${formatScopeDiagnostic(err)}
5773
6957
  `);
5774
6958
  process.exit(1);
5775
6959
  }
@@ -5778,72 +6962,142 @@ Run \`scope manifest generate\` first.`
5778
6962
  }
5779
6963
  function registerServe(siteCmd) {
5780
6964
  siteCmd.command("serve").description(
5781
- "Start a local HTTP server for the built site directory.\n\nRun `scope site build` first.\nCtrl+C to stop.\n\nExamples:\n scope site serve\n scope site serve --port 8080\n scope site serve --dir ./my-site-output"
5782
- ).option("-p, --port <number>", "Port to listen on", "3000").option("-d, --dir <path>", "Directory to serve", ".reactscope/site").action((opts) => {
5783
- try {
5784
- const port = Number.parseInt(opts.port, 10);
5785
- if (Number.isNaN(port) || port < 1 || port > 65535) {
5786
- throw new Error(`Invalid port: ${opts.port}`);
5787
- }
5788
- const serveDir = resolve14(process.cwd(), opts.dir);
5789
- if (!existsSync12(serveDir)) {
5790
- throw new Error(
5791
- `Serve directory not found: ${serveDir}
5792
- Run \`scope site build\` first.`
5793
- );
5794
- }
5795
- const server = createServer((req, res) => {
5796
- const rawUrl = req.url ?? "/";
5797
- const urlPath = decodeURIComponent(rawUrl.split("?")[0] ?? "/");
5798
- const filePath = join5(serveDir, urlPath.endsWith("/") ? `${urlPath}index.html` : urlPath);
5799
- if (!filePath.startsWith(serveDir)) {
5800
- res.writeHead(403, { "Content-Type": "text/plain" });
5801
- res.end("Forbidden");
5802
- return;
6965
+ "Start a local HTTP server for the built site directory.\n\nRun `scope site build` first, or use --watch to auto-rebuild on changes.\nCtrl+C to stop.\n\nExamples:\n scope site serve\n scope site serve --port 8080\n scope site serve --dir ./my-site-output\n scope site serve --watch"
6966
+ ).option("-p, --port <number>", "Port to listen on", "3000").option("-d, --dir <path>", "Directory to serve", ".reactscope/site").option("-w, --watch", "Watch source files and rebuild on changes").option(
6967
+ "-i, --input <path>",
6968
+ "Input directory for .reactscope data (watch mode)",
6969
+ ".reactscope"
6970
+ ).option("--title <text>", "Site title (watch mode)", "Scope \u2014 Component Gallery").option("--base-path <path>", "Base URL path prefix (watch mode)", "/").action(
6971
+ async (opts) => {
6972
+ try {
6973
+ let notifyReload2 = function() {
6974
+ for (const client of sseClients) {
6975
+ client.write("data: reload\n\n");
6976
+ }
6977
+ };
6978
+ var notifyReload = notifyReload2;
6979
+ const port = Number.parseInt(opts.port, 10);
6980
+ if (Number.isNaN(port) || port < 1 || port > 65535) {
6981
+ throw new Error(`Invalid port: ${opts.port}`);
5803
6982
  }
5804
- if (existsSync12(filePath) && statSync2(filePath).isFile()) {
5805
- const ext = extname(filePath).toLowerCase();
5806
- const contentType = MIME_TYPES[ext] ?? "application/octet-stream";
5807
- res.writeHead(200, { "Content-Type": contentType });
5808
- createReadStream(filePath).pipe(res);
5809
- return;
6983
+ const serveDir = resolve15(process.cwd(), opts.dir);
6984
+ const watchMode = opts.watch === true;
6985
+ const sseClients = /* @__PURE__ */ new Set();
6986
+ if (watchMode) {
6987
+ await mkdir(serveDir, { recursive: true });
5810
6988
  }
5811
- const htmlPath = `${filePath}.html`;
5812
- if (existsSync12(htmlPath) && statSync2(htmlPath).isFile()) {
5813
- res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
5814
- createReadStream(htmlPath).pipe(res);
5815
- return;
6989
+ if (!watchMode && !existsSync13(serveDir)) {
6990
+ throw new Error(
6991
+ `Serve directory not found: ${serveDir}
6992
+ Run \`scope site build\` first.`
6993
+ );
5816
6994
  }
5817
- res.writeHead(404, { "Content-Type": "text/plain" });
5818
- res.end(`Not found: ${urlPath}`);
5819
- });
5820
- server.listen(port, () => {
5821
- process.stderr.write(`Scope site running at http://localhost:${port}
6995
+ const server = createServer((req, res) => {
6996
+ const rawUrl = req.url ?? "/";
6997
+ const urlPath = decodeURIComponent(rawUrl.split("?")[0] ?? "/");
6998
+ if (watchMode && urlPath === "/__livereload") {
6999
+ res.writeHead(200, {
7000
+ "Content-Type": "text/event-stream",
7001
+ "Cache-Control": "no-cache",
7002
+ Connection: "keep-alive",
7003
+ "Access-Control-Allow-Origin": "*"
7004
+ });
7005
+ res.write("data: connected\n\n");
7006
+ sseClients.add(res);
7007
+ req.on("close", () => sseClients.delete(res));
7008
+ return;
7009
+ }
7010
+ const filePath = join6(
7011
+ serveDir,
7012
+ urlPath.endsWith("/") ? `${urlPath}index.html` : urlPath
7013
+ );
7014
+ if (!filePath.startsWith(serveDir)) {
7015
+ res.writeHead(403, { "Content-Type": "text/plain" });
7016
+ res.end("Forbidden");
7017
+ return;
7018
+ }
7019
+ if (existsSync13(filePath) && statSync2(filePath).isFile()) {
7020
+ const ext = extname(filePath).toLowerCase();
7021
+ const contentType = MIME_TYPES[ext] ?? "application/octet-stream";
7022
+ if (watchMode && ext === ".html") {
7023
+ const html = injectLiveReloadScript(readFileSync12(filePath, "utf-8"));
7024
+ res.writeHead(200, { "Content-Type": contentType });
7025
+ res.end(html);
7026
+ return;
7027
+ }
7028
+ res.writeHead(200, { "Content-Type": contentType });
7029
+ createReadStream(filePath).pipe(res);
7030
+ return;
7031
+ }
7032
+ const htmlPath = `${filePath}.html`;
7033
+ if (existsSync13(htmlPath) && statSync2(htmlPath).isFile()) {
7034
+ if (watchMode) {
7035
+ const html = injectLiveReloadScript(readFileSync12(htmlPath, "utf-8"));
7036
+ res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
7037
+ res.end(html);
7038
+ return;
7039
+ }
7040
+ res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
7041
+ createReadStream(htmlPath).pipe(res);
7042
+ return;
7043
+ }
7044
+ res.writeHead(404, { "Content-Type": "text/plain" });
7045
+ res.end(`Not found: ${urlPath}`);
7046
+ });
7047
+ server.listen(port, () => {
7048
+ process.stderr.write(`Scope site running at http://localhost:${port}
5822
7049
  `);
5823
- process.stderr.write(`Serving ${serveDir}
7050
+ process.stderr.write(`Serving ${serveDir}
5824
7051
  `);
5825
- process.stderr.write("Press Ctrl+C to stop.\n");
5826
- });
5827
- server.on("error", (err) => {
5828
- if (err.code === "EADDRINUSE") {
5829
- process.stderr.write(`Error: Port ${port} is already in use.
7052
+ if (watchMode) {
7053
+ process.stderr.write(
7054
+ "Watch mode enabled \u2014 source changes trigger rebuild + browser reload\n"
7055
+ );
7056
+ }
7057
+ process.stderr.write("Press Ctrl+C to stop.\n");
7058
+ });
7059
+ server.on("error", (err) => {
7060
+ if (err.code === "EADDRINUSE") {
7061
+ process.stderr.write(`Error: Port ${port} is already in use.
5830
7062
  `);
5831
- } else {
5832
- process.stderr.write(`Server error: ${err.message}
7063
+ } else {
7064
+ process.stderr.write(`Server error: ${err.message}
5833
7065
  `);
7066
+ }
7067
+ process.exit(1);
7068
+ });
7069
+ if (watchMode) {
7070
+ const rootDir = process.cwd();
7071
+ const inputDir = resolve15(rootDir, opts.input);
7072
+ const initialManifest = await runFullBuild(
7073
+ rootDir,
7074
+ inputDir,
7075
+ serveDir,
7076
+ opts.title,
7077
+ opts.basePath
7078
+ );
7079
+ notifyReload2();
7080
+ startFileWatcher({
7081
+ rootDir,
7082
+ inputDir,
7083
+ outputDir: serveDir,
7084
+ title: opts.title,
7085
+ basePath: opts.basePath,
7086
+ previousManifest: initialManifest,
7087
+ notifyReload: notifyReload2
7088
+ });
5834
7089
  }
5835
- process.exit(1);
5836
- });
5837
- } catch (err) {
5838
- process.stderr.write(`Error: ${err instanceof Error ? err.message : String(err)}
7090
+ } catch (err) {
7091
+ process.stderr.write(`${formatScopeDiagnostic(err)}
5839
7092
  `);
5840
- process.exit(1);
7093
+ process.exit(1);
7094
+ }
5841
7095
  }
5842
- });
7096
+ );
5843
7097
  }
5844
7098
  function createSiteCommand() {
5845
7099
  const siteCmd = new Command9("site").description(
5846
- 'Build and serve the static HTML component gallery site.\n\nPREREQUISITES:\n scope manifest generate (manifest.json)\n scope render all (renders/ + compliance-styles.json)\n\nSITE CONTENTS:\n /index.html component gallery with screenshots + metadata\n /<component>/index.html detail page: props, renders, matrix, X-Ray, compliance\n\nExamples:\n scope site build && scope site serve\n scope site build --title "Acme UI" --compliance .reactscope/compliance-styles.json\n scope site serve --port 8080'
7100
+ 'Build and serve the static HTML component gallery site.\n\nPREREQUISITES:\n scope manifest generate (manifest.json)\n scope render all (renders/ + compliance-styles.json)\n\nSITE CONTENTS:\n /index.html component gallery with screenshots + metadata\n /<component>/index.html detail page: props, renders, matrix, X-Ray, compliance\n\nExamples:\n scope site build && scope site serve\n scope site build --title "Acme UI" --compliance .reactscope/compliance-report.json\n scope site serve --port 8080'
5847
7101
  );
5848
7102
  registerBuild(siteCmd);
5849
7103
  registerServe(siteCmd);
@@ -5851,8 +7105,8 @@ function createSiteCommand() {
5851
7105
  }
5852
7106
 
5853
7107
  // src/tokens/commands.ts
5854
- import { existsSync as existsSync15, readFileSync as readFileSync12 } from "fs";
5855
- import { resolve as resolve18 } from "path";
7108
+ import { existsSync as existsSync17, readFileSync as readFileSync16 } from "fs";
7109
+ import { resolve as resolve20 } from "path";
5856
7110
  import {
5857
7111
  parseTokenFileSync as parseTokenFileSync2,
5858
7112
  TokenParseError,
@@ -5863,23 +7117,23 @@ import {
5863
7117
  import { Command as Command11 } from "commander";
5864
7118
 
5865
7119
  // src/tokens/compliance.ts
5866
- import { existsSync as existsSync13, readFileSync as readFileSync10 } from "fs";
5867
- import { resolve as resolve15 } from "path";
7120
+ import { existsSync as existsSync14, readFileSync as readFileSync13, writeFileSync as writeFileSync11 } from "fs";
7121
+ import { resolve as resolve16 } from "path";
5868
7122
  import {
5869
7123
  ComplianceEngine as ComplianceEngine4,
5870
7124
  TokenResolver as TokenResolver4
5871
7125
  } from "@agent-scope/tokens";
5872
7126
  var DEFAULT_STYLES_PATH = ".reactscope/compliance-styles.json";
5873
7127
  function loadStylesFile(stylesPath) {
5874
- const absPath = resolve15(process.cwd(), stylesPath);
5875
- if (!existsSync13(absPath)) {
7128
+ const absPath = resolve16(process.cwd(), stylesPath);
7129
+ if (!existsSync14(absPath)) {
5876
7130
  throw new Error(
5877
7131
  `Compliance styles file not found at ${absPath}.
5878
7132
  Run \`scope render all\` first to generate component styles, or use --styles to specify a path.
5879
7133
  Expected format: { "ComponentName": { colors: {}, spacing: {}, typography: {}, borders: {}, shadows: {} } }`
5880
7134
  );
5881
7135
  }
5882
- const raw = readFileSync10(absPath, "utf-8");
7136
+ const raw = readFileSync13(absPath, "utf-8");
5883
7137
  let parsed;
5884
7138
  try {
5885
7139
  parsed = JSON.parse(raw);
@@ -5907,11 +7161,11 @@ function categoryForProperty(property) {
5907
7161
  }
5908
7162
  function buildCategorySummary(batch) {
5909
7163
  const cats = {
5910
- color: { total: 0, onSystem: 0, offSystem: 0, compliance: 1 },
5911
- spacing: { total: 0, onSystem: 0, offSystem: 0, compliance: 1 },
5912
- typography: { total: 0, onSystem: 0, offSystem: 0, compliance: 1 },
5913
- border: { total: 0, onSystem: 0, offSystem: 0, compliance: 1 },
5914
- shadow: { total: 0, onSystem: 0, offSystem: 0, compliance: 1 }
7164
+ color: { total: 0, onSystem: 0, offSystem: 0, compliance: 0 },
7165
+ spacing: { total: 0, onSystem: 0, offSystem: 0, compliance: 0 },
7166
+ typography: { total: 0, onSystem: 0, offSystem: 0, compliance: 0 },
7167
+ border: { total: 0, onSystem: 0, offSystem: 0, compliance: 0 },
7168
+ shadow: { total: 0, onSystem: 0, offSystem: 0, compliance: 0 }
5915
7169
  };
5916
7170
  for (const report of Object.values(batch.components)) {
5917
7171
  for (const [property, result] of Object.entries(report.properties)) {
@@ -5927,7 +7181,7 @@ function buildCategorySummary(batch) {
5927
7181
  }
5928
7182
  }
5929
7183
  for (const summary of Object.values(cats)) {
5930
- summary.compliance = summary.total === 0 ? 1 : summary.onSystem / summary.total;
7184
+ summary.compliance = summary.total === 0 ? 0 : summary.onSystem / summary.total;
5931
7185
  }
5932
7186
  return cats;
5933
7187
  }
@@ -5968,6 +7222,11 @@ function formatComplianceReport(batch, threshold) {
5968
7222
  const lines = [];
5969
7223
  const thresholdLabel = threshold !== void 0 ? pct >= threshold ? " \u2713 (pass)" : ` \u2717 (below threshold ${threshold}%)` : "";
5970
7224
  lines.push(`Overall compliance score: ${pct}%${thresholdLabel}`);
7225
+ if (batch.totalProperties === 0) {
7226
+ lines.push(
7227
+ "No CSS properties were audited; run `scope render all` and inspect .reactscope/compliance-styles.json before treating compliance as green."
7228
+ );
7229
+ }
5971
7230
  lines.push("");
5972
7231
  const cats = buildCategorySummary(batch);
5973
7232
  const catEntries = Object.entries(cats).filter(([, s]) => s.total > 0);
@@ -6002,47 +7261,89 @@ function formatComplianceReport(batch, threshold) {
6002
7261
  }
6003
7262
  function registerCompliance(tokensCmd) {
6004
7263
  tokensCmd.command("compliance").description(
6005
- "Compute a token compliance score across all rendered components.\n\nCompares computed CSS values from .reactscope/compliance-styles.json\nagainst the token file \u2014 reports what % of style values are on-token.\n\nPREREQUISITES:\n scope render all must have run first (produces compliance-styles.json)\n\nSCORING:\n compliant value exactly matches a token\n near-match value is within tolerance of a token (e.g. close color)\n off-token value not found in token file\n\nEXIT CODES:\n 0 compliance >= threshold (or no --threshold set)\n 1 compliance < threshold\n\nExamples:\n scope tokens compliance\n scope tokens compliance --threshold 90\n scope tokens compliance --format json | jq '.summary'\n scope tokens compliance --styles ./custom/compliance-styles.json"
6006
- ).option("--file <path>", "Path to token file (overrides config)").option("--styles <path>", `Path to compliance styles JSON (default: ${DEFAULT_STYLES_PATH})`).option("--threshold <n>", "Exit code 1 if compliance score is below this percentage (0-100)").option("--format <fmt>", "Output format: json or text (default: auto-detect)").action((opts) => {
6007
- try {
6008
- const tokenFilePath = resolveTokenFilePath(opts.file);
6009
- const { tokens } = loadTokens(tokenFilePath);
6010
- const resolver = new TokenResolver4(tokens);
6011
- const engine = new ComplianceEngine4(resolver);
6012
- const stylesPath = opts.styles ?? DEFAULT_STYLES_PATH;
6013
- const stylesFile = loadStylesFile(stylesPath);
6014
- const componentMap = /* @__PURE__ */ new Map();
6015
- for (const [name, styles] of Object.entries(stylesFile)) {
6016
- componentMap.set(name, styles);
6017
- }
6018
- if (componentMap.size === 0) {
6019
- process.stderr.write(`Warning: No components found in styles file at ${stylesPath}
7264
+ "Compute a token compliance score across all rendered components.\n\nCompares computed CSS values from .reactscope/compliance-styles.json\nagainst the token file \u2014 reports what % of style values are on-token.\n\nPREREQUISITES:\n scope render all must have run first (produces compliance-styles.json)\n\nSCORING:\n compliant value exactly matches a token\n near-match value is within tolerance of a token (e.g. close color)\n off-token value not found in token file\n\nEXIT CODES:\n 0 compliance >= threshold (or no --threshold set)\n 1 compliance < threshold\n\nExamples:\n scope tokens compliance\n scope tokens compliance --threshold 90\n scope tokens compliance --format json | jq '.summary'\n scope tokens compliance --out .reactscope/compliance-report.json\n scope tokens compliance --styles ./custom/compliance-styles.json"
7265
+ ).option("--file <path>", "Path to token file (overrides config)").option("--styles <path>", `Path to compliance styles JSON (default: ${DEFAULT_STYLES_PATH})`).option(
7266
+ "--out <path>",
7267
+ "Write JSON report to file (for use with scope site build --compliance)"
7268
+ ).option("--threshold <n>", "Exit code 1 if compliance score is below this percentage (0-100)").option("--format <fmt>", "Output format: json or text (default: auto-detect)").action(
7269
+ (opts) => {
7270
+ try {
7271
+ const tokenFilePath = resolveTokenFilePath(opts.file);
7272
+ const { tokens } = loadTokens(tokenFilePath);
7273
+ const resolver = new TokenResolver4(tokens);
7274
+ const engine = new ComplianceEngine4(resolver);
7275
+ const stylesPath = opts.styles ?? DEFAULT_STYLES_PATH;
7276
+ const stylesFile = loadStylesFile(stylesPath);
7277
+ const componentMap = /* @__PURE__ */ new Map();
7278
+ for (const [name, styles] of Object.entries(stylesFile)) {
7279
+ componentMap.set(name, styles);
7280
+ }
7281
+ if (componentMap.size === 0) {
7282
+ process.stderr.write(`Warning: No components found in styles file at ${stylesPath}
6020
7283
  `);
6021
- }
6022
- const batch = engine.auditBatch(componentMap);
6023
- const useJson = opts.format === "json" || opts.format !== "text" && !isTTY();
6024
- const threshold = opts.threshold !== void 0 ? Number.parseInt(opts.threshold, 10) : void 0;
6025
- if (useJson) {
6026
- process.stdout.write(`${JSON.stringify(batch, null, 2)}
7284
+ }
7285
+ const batch = engine.auditBatch(componentMap);
7286
+ const threshold = opts.threshold !== void 0 ? Number.parseInt(opts.threshold, 10) : void 0;
7287
+ const failures = [];
7288
+ if (batch.totalProperties === 0) {
7289
+ failures.push({
7290
+ component: "*",
7291
+ stage: "compliance",
7292
+ message: `No CSS properties were audited from ${stylesPath}; refusing to report silent success.`,
7293
+ outputPath: stylesPath
7294
+ });
7295
+ } else if (threshold !== void 0 && Math.round(batch.aggregateCompliance * 100) < threshold) {
7296
+ failures.push({
7297
+ component: "*",
7298
+ stage: "compliance",
7299
+ message: `Compliance ${Math.round(batch.aggregateCompliance * 100)}% is below threshold ${threshold}%.`,
7300
+ outputPath: opts.out ?? ".reactscope/compliance-report.json"
7301
+ });
7302
+ }
7303
+ if (opts.out !== void 0) {
7304
+ const outPath = resolve16(process.cwd(), opts.out);
7305
+ writeFileSync11(outPath, JSON.stringify(batch, null, 2), "utf-8");
7306
+ process.stderr.write(`Compliance report written to ${outPath}
6027
7307
  `);
6028
- } else {
6029
- process.stdout.write(`${formatComplianceReport(batch, threshold)}
7308
+ }
7309
+ const useJson = opts.format === "json" || opts.format !== "text" && !isTTY();
7310
+ if (useJson) {
7311
+ process.stdout.write(`${JSON.stringify(batch, null, 2)}
7312
+ `);
7313
+ } else {
7314
+ process.stdout.write(`${formatComplianceReport(batch, threshold)}
7315
+ `);
7316
+ }
7317
+ const summaryPath = writeRunSummary({
7318
+ command: "scope tokens compliance",
7319
+ status: failures.length > 0 ? "failed" : "success",
7320
+ outputPaths: [opts.out ?? ".reactscope/compliance-report.json", stylesPath],
7321
+ compliance: {
7322
+ auditedProperties: batch.totalProperties,
7323
+ onSystemProperties: batch.totalOnSystem,
7324
+ offSystemProperties: batch.totalOffSystem,
7325
+ score: Math.round(batch.aggregateCompliance * 100),
7326
+ threshold
7327
+ },
7328
+ failures
7329
+ });
7330
+ process.stderr.write(`[scope/tokens] Run summary written to ${summaryPath}
7331
+ `);
7332
+ if (failures.length > 0) {
7333
+ process.exit(1);
7334
+ }
7335
+ } catch (err) {
7336
+ process.stderr.write(`Error: ${err instanceof Error ? err.message : String(err)}
6030
7337
  `);
6031
- }
6032
- if (threshold !== void 0 && Math.round(batch.aggregateCompliance * 100) < threshold) {
6033
7338
  process.exit(1);
6034
7339
  }
6035
- } catch (err) {
6036
- process.stderr.write(`Error: ${err instanceof Error ? err.message : String(err)}
6037
- `);
6038
- process.exit(1);
6039
7340
  }
6040
- });
7341
+ );
6041
7342
  }
6042
7343
 
6043
7344
  // src/tokens/export.ts
6044
- import { existsSync as existsSync14, readFileSync as readFileSync11, writeFileSync as writeFileSync9 } from "fs";
6045
- import { resolve as resolve16 } from "path";
7345
+ import { existsSync as existsSync15, readFileSync as readFileSync14, writeFileSync as writeFileSync12 } from "fs";
7346
+ import { resolve as resolve17 } from "path";
6046
7347
  import {
6047
7348
  exportTokens,
6048
7349
  parseTokenFileSync,
@@ -6055,21 +7356,21 @@ var CONFIG_FILE = "reactscope.config.json";
6055
7356
  var SUPPORTED_FORMATS = ["css", "ts", "scss", "tailwind", "flat-json", "figma"];
6056
7357
  function resolveTokenFilePath2(fileFlag) {
6057
7358
  if (fileFlag !== void 0) {
6058
- return resolve16(process.cwd(), fileFlag);
7359
+ return resolve17(process.cwd(), fileFlag);
6059
7360
  }
6060
- const configPath = resolve16(process.cwd(), CONFIG_FILE);
6061
- if (existsSync14(configPath)) {
7361
+ const configPath = resolve17(process.cwd(), CONFIG_FILE);
7362
+ if (existsSync15(configPath)) {
6062
7363
  try {
6063
- const raw = readFileSync11(configPath, "utf-8");
7364
+ const raw = readFileSync14(configPath, "utf-8");
6064
7365
  const config = JSON.parse(raw);
6065
7366
  if (typeof config === "object" && config !== null && "tokens" in config && typeof config.tokens === "object" && config.tokens !== null && typeof config.tokens?.file === "string") {
6066
7367
  const file = config.tokens.file;
6067
- return resolve16(process.cwd(), file);
7368
+ return resolve17(process.cwd(), file);
6068
7369
  }
6069
7370
  } catch {
6070
7371
  }
6071
7372
  }
6072
- return resolve16(process.cwd(), DEFAULT_TOKEN_FILE);
7373
+ return resolve17(process.cwd(), DEFAULT_TOKEN_FILE);
6073
7374
  }
6074
7375
  function createTokensExportCommand() {
6075
7376
  return new Command10("export").description(
@@ -6100,13 +7401,13 @@ Supported formats: ${SUPPORTED_FORMATS.join(", ")}
6100
7401
  const format = opts.format;
6101
7402
  try {
6102
7403
  const filePath = resolveTokenFilePath2(opts.file);
6103
- if (!existsSync14(filePath)) {
7404
+ if (!existsSync15(filePath)) {
6104
7405
  throw new Error(
6105
7406
  `Token file not found at ${filePath}.
6106
7407
  Create a reactscope.tokens.json file or use --file to specify a path.`
6107
7408
  );
6108
7409
  }
6109
- const raw = readFileSync11(filePath, "utf-8");
7410
+ const raw = readFileSync14(filePath, "utf-8");
6110
7411
  const { tokens, rawFile } = parseTokenFileSync(raw);
6111
7412
  let themesMap;
6112
7413
  if (opts.theme !== void 0) {
@@ -6145,8 +7446,8 @@ Available themes: ${themeNames.join(", ")}`
6145
7446
  themes: themesMap
6146
7447
  });
6147
7448
  if (opts.out !== void 0) {
6148
- const outPath = resolve16(process.cwd(), opts.out);
6149
- writeFileSync9(outPath, output, "utf-8");
7449
+ const outPath = resolve17(process.cwd(), opts.out);
7450
+ writeFileSync12(outPath, output, "utf-8");
6150
7451
  process.stderr.write(`Exported ${tokens.length} tokens to ${outPath}
6151
7452
  `);
6152
7453
  } else {
@@ -6261,23 +7562,251 @@ ${formatImpactSummary(report)}
6261
7562
  );
6262
7563
  }
6263
7564
 
7565
+ // src/tokens/init.ts
7566
+ import { existsSync as existsSync16, readFileSync as readFileSync15, writeFileSync as writeFileSync13 } from "fs";
7567
+ import { resolve as resolve18 } from "path";
7568
+ var DEFAULT_TOKEN_FILE2 = "reactscope.tokens.json";
7569
+ var CONFIG_FILE2 = "reactscope.config.json";
7570
+ function resolveOutputPath(fileFlag) {
7571
+ if (fileFlag !== void 0) {
7572
+ return resolve18(process.cwd(), fileFlag);
7573
+ }
7574
+ const configPath = resolve18(process.cwd(), CONFIG_FILE2);
7575
+ if (existsSync16(configPath)) {
7576
+ try {
7577
+ const raw = readFileSync15(configPath, "utf-8");
7578
+ const config = JSON.parse(raw);
7579
+ if (typeof config === "object" && config !== null && "tokens" in config && typeof config.tokens === "object" && config.tokens !== null && typeof config.tokens?.file === "string") {
7580
+ const file = config.tokens.file;
7581
+ return resolve18(process.cwd(), file);
7582
+ }
7583
+ } catch {
7584
+ }
7585
+ }
7586
+ return resolve18(process.cwd(), DEFAULT_TOKEN_FILE2);
7587
+ }
7588
+ var CSS_VAR_RE = /--([\w-]+)\s*:\s*([^;]+)/g;
7589
+ var HEX_COLOR_RE = /^#(?:[0-9a-fA-F]{3,4}|[0-9a-fA-F]{6}|[0-9a-fA-F]{8})$/;
7590
+ var COLOR_FN_RE = /^(?:rgba?|hsla?|oklch|oklab|lch|lab|color|hwb)\(/;
7591
+ var DIMENSION_RE = /^-?\d+(?:\.\d+)?(?:px|rem|em|%|vw|vh|ch|ex|cap|lh|dvh|svh|lvh)$/;
7592
+ var DURATION_RE = /^-?\d+(?:\.\d+)?(?:ms|s)$/;
7593
+ var FONT_FAMILY_RE = /^["']|,\s*(?:sans-serif|serif|monospace|cursive|fantasy|system-ui)/;
7594
+ var NUMBER_RE = /^-?\d+(?:\.\d+)?$/;
7595
+ var CUBIC_BEZIER_RE = /^cubic-bezier\(/;
7596
+ var SHADOW_RE = /^\d.*(?:px|rem|em)\s+(?:#|rgba?|hsla?|oklch|oklab)/i;
7597
+ function inferTokenType(value) {
7598
+ const v = value.trim();
7599
+ if (HEX_COLOR_RE.test(v) || COLOR_FN_RE.test(v)) return "color";
7600
+ if (DURATION_RE.test(v)) return "duration";
7601
+ if (DIMENSION_RE.test(v)) return "dimension";
7602
+ if (FONT_FAMILY_RE.test(v)) return "fontFamily";
7603
+ if (CUBIC_BEZIER_RE.test(v)) return "cubicBezier";
7604
+ if (SHADOW_RE.test(v)) return "shadow";
7605
+ if (NUMBER_RE.test(v)) return "number";
7606
+ return "color";
7607
+ }
7608
+ function setNestedToken(root, segments, value, type) {
7609
+ let node = root;
7610
+ for (let i = 0; i < segments.length - 1; i++) {
7611
+ const seg = segments[i];
7612
+ if (seg === void 0) continue;
7613
+ if (!(seg in node) || typeof node[seg] !== "object" || node[seg] === null) {
7614
+ node[seg] = {};
7615
+ }
7616
+ node = node[seg];
7617
+ }
7618
+ const leaf = segments[segments.length - 1];
7619
+ if (leaf === void 0) return;
7620
+ node[leaf] = { value, type };
7621
+ }
7622
+ function extractBlockBody(css, openBrace) {
7623
+ let depth = 0;
7624
+ let end = -1;
7625
+ for (let i = openBrace; i < css.length; i++) {
7626
+ if (css[i] === "{") depth++;
7627
+ else if (css[i] === "}") {
7628
+ depth--;
7629
+ if (depth === 0) {
7630
+ end = i;
7631
+ break;
7632
+ }
7633
+ }
7634
+ }
7635
+ if (end === -1) return "";
7636
+ return css.slice(openBrace + 1, end);
7637
+ }
7638
+ function parseScopedBlocks(css) {
7639
+ const blocks = [];
7640
+ const blockRe = /(?::root|@theme(?:\s+inline)?|\.dark\.high-contrast|\.dark)\s*\{/g;
7641
+ let match = blockRe.exec(css);
7642
+ while (match !== null) {
7643
+ const selector = match[0];
7644
+ const braceIdx = css.indexOf("{", match.index);
7645
+ if (braceIdx === -1) {
7646
+ match = blockRe.exec(css);
7647
+ continue;
7648
+ }
7649
+ const body = extractBlockBody(css, braceIdx);
7650
+ let scope;
7651
+ if (selector.includes(".dark.high-contrast")) scope = "dark-high-contrast";
7652
+ else if (selector.includes(".dark")) scope = "dark";
7653
+ else if (selector.includes("@theme")) scope = "theme";
7654
+ else scope = "root";
7655
+ blocks.push({ scope, body });
7656
+ match = blockRe.exec(css);
7657
+ }
7658
+ return blocks;
7659
+ }
7660
+ function extractVarsFromBody(body) {
7661
+ const results = [];
7662
+ for (const m of body.matchAll(CSS_VAR_RE)) {
7663
+ const name = m[1];
7664
+ const value = m[2]?.trim();
7665
+ if (name === void 0 || value === void 0 || value.length === 0) continue;
7666
+ if (value.startsWith("var(") || value.startsWith("calc(")) continue;
7667
+ results.push({ name, value });
7668
+ }
7669
+ return results;
7670
+ }
7671
+ function extractCSSCustomProperties(tokenSources) {
7672
+ const cssSources = tokenSources.filter(
7673
+ (s) => s.kind === "css-custom-properties" || s.kind === "tailwind-v4-theme"
7674
+ );
7675
+ if (cssSources.length === 0) return null;
7676
+ const tokens = {};
7677
+ const themes = {};
7678
+ let found = false;
7679
+ for (const source of cssSources) {
7680
+ try {
7681
+ if (source.path.includes("compiled") || source.path.includes(".min.")) continue;
7682
+ const raw = readFileSync15(source.path, "utf-8");
7683
+ const blocks = parseScopedBlocks(raw);
7684
+ for (const block of blocks) {
7685
+ const vars = extractVarsFromBody(block.body);
7686
+ for (const { name, value } of vars) {
7687
+ const segments = name.split("-").filter(Boolean);
7688
+ if (segments.length === 0) continue;
7689
+ if (block.scope === "root" || block.scope === "theme") {
7690
+ const type = inferTokenType(value);
7691
+ setNestedToken(tokens, segments, value, type);
7692
+ found = true;
7693
+ } else {
7694
+ const themeName = block.scope;
7695
+ if (!themes[themeName]) themes[themeName] = {};
7696
+ const path = segments.join(".");
7697
+ themes[themeName][path] = value;
7698
+ found = true;
7699
+ }
7700
+ }
7701
+ }
7702
+ } catch {
7703
+ }
7704
+ }
7705
+ return found ? { tokens, themes } : null;
7706
+ }
7707
+ function registerTokensInit(tokensCmd) {
7708
+ tokensCmd.command("init").description(
7709
+ "Detect design-token sources in the project and generate a token file.\n\nDETECTED SOURCES:\n - Tailwind config (colors, spacing, fontFamily, borderRadius)\n - CSS custom properties (:root { --color-primary: #0070f3; })\n\nWill not overwrite an existing token file unless --force is passed.\n\nOUTPUT PATH RESOLUTION (in priority order):\n 1. --file <path> explicit override\n 2. tokens.file in reactscope.config.json\n 3. reactscope.tokens.json default (project root)\n\nExamples:\n scope tokens init\n scope tokens init --force\n scope tokens init --file tokens/brand.json\n scope tokens init --force --file custom-tokens.json"
7710
+ ).option("--file <path>", "Output path for the token file (overrides config)").option("--force", "Overwrite existing token file", false).action((opts) => {
7711
+ try {
7712
+ const outPath = resolveOutputPath(opts.file);
7713
+ if (existsSync16(outPath) && !opts.force) {
7714
+ process.stderr.write(
7715
+ `Token file already exists at ${outPath}.
7716
+ Run with --force to overwrite.
7717
+ `
7718
+ );
7719
+ process.exit(1);
7720
+ }
7721
+ const rootDir = process.cwd();
7722
+ const detected = detectProject(rootDir);
7723
+ const tailwindTokens = extractTailwindTokens(detected.tokenSources);
7724
+ const cssResult = extractCSSCustomProperties(detected.tokenSources);
7725
+ const mergedTokens = {};
7726
+ const mergedThemes = {};
7727
+ if (tailwindTokens !== null) {
7728
+ Object.assign(mergedTokens, tailwindTokens);
7729
+ }
7730
+ if (cssResult !== null) {
7731
+ for (const [key, value] of Object.entries(cssResult.tokens)) {
7732
+ if (!(key in mergedTokens)) {
7733
+ mergedTokens[key] = value;
7734
+ }
7735
+ }
7736
+ for (const [themeName, overrides] of Object.entries(cssResult.themes)) {
7737
+ if (!mergedThemes[themeName]) mergedThemes[themeName] = {};
7738
+ Object.assign(mergedThemes[themeName], overrides);
7739
+ }
7740
+ }
7741
+ const tokenFile = {
7742
+ $schema: "https://raw.githubusercontent.com/FlatFilers/Scope/main/packages/tokens/schema.json",
7743
+ version: "1.0.0",
7744
+ meta: {
7745
+ name: "Design Tokens",
7746
+ lastUpdated: (/* @__PURE__ */ new Date()).toISOString().split("T")[0]
7747
+ },
7748
+ tokens: mergedTokens
7749
+ };
7750
+ if (Object.keys(mergedThemes).length > 0) {
7751
+ tokenFile.themes = mergedThemes;
7752
+ }
7753
+ writeFileSync13(outPath, `${JSON.stringify(tokenFile, null, 2)}
7754
+ `);
7755
+ const tokenGroupCount = Object.keys(mergedTokens).length;
7756
+ const themeNames = Object.keys(mergedThemes);
7757
+ if (detected.tokenSources.length > 0) {
7758
+ process.stdout.write("Detected token sources:\n");
7759
+ for (const source of detected.tokenSources) {
7760
+ process.stdout.write(` ${source.kind}: ${source.path}
7761
+ `);
7762
+ }
7763
+ process.stdout.write("\n");
7764
+ }
7765
+ if (tokenGroupCount > 0) {
7766
+ process.stdout.write(`Extracted ${tokenGroupCount} token group(s) \u2192 ${outPath}
7767
+ `);
7768
+ if (themeNames.length > 0) {
7769
+ for (const name of themeNames) {
7770
+ const count = Object.keys(mergedThemes[name] ?? {}).length;
7771
+ process.stdout.write(` theme "${name}": ${count} override(s)
7772
+ `);
7773
+ }
7774
+ }
7775
+ } else {
7776
+ process.stdout.write(
7777
+ `No token sources detected. Created empty token file \u2192 ${outPath}
7778
+ Add tokens manually or re-run after configuring a design system.
7779
+ `
7780
+ );
7781
+ }
7782
+ } catch (err) {
7783
+ process.stderr.write(`Error: ${err instanceof Error ? err.message : String(err)}
7784
+ `);
7785
+ process.exit(1);
7786
+ }
7787
+ });
7788
+ }
7789
+
6264
7790
  // src/tokens/preview.ts
6265
- import { mkdirSync as mkdirSync6, writeFileSync as writeFileSync10 } from "fs";
6266
- import { resolve as resolve17 } from "path";
7791
+ import { mkdirSync as mkdirSync7, writeFileSync as writeFileSync14 } from "fs";
7792
+ import { resolve as resolve19 } from "path";
6267
7793
  import { BrowserPool as BrowserPool6, SpriteSheetGenerator } from "@agent-scope/render";
6268
7794
  import { ComplianceEngine as ComplianceEngine6, ImpactAnalyzer as ImpactAnalyzer2, TokenResolver as TokenResolver7 } from "@agent-scope/tokens";
6269
7795
  var DEFAULT_STYLES_PATH3 = ".reactscope/compliance-styles.json";
6270
7796
  var DEFAULT_MANIFEST_PATH = ".reactscope/manifest.json";
6271
7797
  var DEFAULT_OUTPUT_DIR2 = ".reactscope/previews";
6272
7798
  async function renderComponentWithCssOverride(filePath, componentName, cssOverride, vpWidth, vpHeight, timeoutMs) {
7799
+ const PAD = 16;
6273
7800
  const htmlHarness = await buildComponentHarness(
6274
7801
  filePath,
6275
7802
  componentName,
6276
7803
  {},
6277
7804
  // no props
6278
7805
  vpWidth,
6279
- cssOverride
7806
+ cssOverride,
6280
7807
  // injected as <style>
7808
+ void 0,
7809
+ PAD
6281
7810
  );
6282
7811
  const pool = new BrowserPool6({
6283
7812
  size: { browsers: 1, pagesPerBrowser: 1 },
@@ -6298,7 +7827,6 @@ async function renderComponentWithCssOverride(filePath, componentName, cssOverri
6298
7827
  );
6299
7828
  const rootLocator = page.locator("[data-reactscope-root]");
6300
7829
  const bb = await rootLocator.boundingBox();
6301
- const PAD = 16;
6302
7830
  const MIN_W = 320;
6303
7831
  const MIN_H = 120;
6304
7832
  const clipX = Math.max(0, (bb?.x ?? 0) - PAD);
@@ -6446,10 +7974,10 @@ function registerPreview(tokensCmd) {
6446
7974
  });
6447
7975
  const spriteResult = await generator.generate(matrixResult);
6448
7976
  const tokenLabel = tokenPath.replace(/\./g, "-");
6449
- const outputPath = opts.output ?? resolve17(process.cwd(), DEFAULT_OUTPUT_DIR2, `preview-${tokenLabel}.png`);
6450
- const outputDir = resolve17(outputPath, "..");
6451
- mkdirSync6(outputDir, { recursive: true });
6452
- writeFileSync10(outputPath, spriteResult.png);
7977
+ const outputPath = opts.output ?? resolve19(process.cwd(), DEFAULT_OUTPUT_DIR2, `preview-${tokenLabel}.png`);
7978
+ const outputDir = resolve19(outputPath, "..");
7979
+ mkdirSync7(outputDir, { recursive: true });
7980
+ writeFileSync14(outputPath, spriteResult.png);
6453
7981
  const useJson = opts.format === "json" || opts.format !== "text" && !isTTY();
6454
7982
  if (useJson) {
6455
7983
  process.stdout.write(
@@ -6487,8 +8015,8 @@ function registerPreview(tokensCmd) {
6487
8015
  }
6488
8016
 
6489
8017
  // src/tokens/commands.ts
6490
- var DEFAULT_TOKEN_FILE2 = "reactscope.tokens.json";
6491
- var CONFIG_FILE2 = "reactscope.config.json";
8018
+ var DEFAULT_TOKEN_FILE3 = "reactscope.tokens.json";
8019
+ var CONFIG_FILE3 = "reactscope.config.json";
6492
8020
  function isTTY2() {
6493
8021
  return process.stdout.isTTY === true;
6494
8022
  }
@@ -6508,30 +8036,30 @@ function buildTable2(headers, rows) {
6508
8036
  }
6509
8037
  function resolveTokenFilePath(fileFlag) {
6510
8038
  if (fileFlag !== void 0) {
6511
- return resolve18(process.cwd(), fileFlag);
8039
+ return resolve20(process.cwd(), fileFlag);
6512
8040
  }
6513
- const configPath = resolve18(process.cwd(), CONFIG_FILE2);
6514
- if (existsSync15(configPath)) {
8041
+ const configPath = resolve20(process.cwd(), CONFIG_FILE3);
8042
+ if (existsSync17(configPath)) {
6515
8043
  try {
6516
- const raw = readFileSync12(configPath, "utf-8");
8044
+ const raw = readFileSync16(configPath, "utf-8");
6517
8045
  const config = JSON.parse(raw);
6518
8046
  if (typeof config === "object" && config !== null && "tokens" in config && typeof config.tokens === "object" && config.tokens !== null && typeof config.tokens?.file === "string") {
6519
8047
  const file = config.tokens.file;
6520
- return resolve18(process.cwd(), file);
8048
+ return resolve20(process.cwd(), file);
6521
8049
  }
6522
8050
  } catch {
6523
8051
  }
6524
8052
  }
6525
- return resolve18(process.cwd(), DEFAULT_TOKEN_FILE2);
8053
+ return resolve20(process.cwd(), DEFAULT_TOKEN_FILE3);
6526
8054
  }
6527
8055
  function loadTokens(absPath) {
6528
- if (!existsSync15(absPath)) {
8056
+ if (!existsSync17(absPath)) {
6529
8057
  throw new Error(
6530
8058
  `Token file not found at ${absPath}.
6531
8059
  Create a reactscope.tokens.json file or use --file to specify a path.`
6532
8060
  );
6533
8061
  }
6534
- const raw = readFileSync12(absPath, "utf-8");
8062
+ const raw = readFileSync16(absPath, "utf-8");
6535
8063
  return parseTokenFileSync2(raw);
6536
8064
  }
6537
8065
  function getRawValue(node, segments) {
@@ -6774,13 +8302,13 @@ Examples:
6774
8302
  ).option("--file <path>", "Path to token file (overrides config)").option("--format <fmt>", "Output format: json or text (default: auto-detect)").action((opts) => {
6775
8303
  try {
6776
8304
  const filePath = resolveTokenFilePath(opts.file);
6777
- if (!existsSync15(filePath)) {
8305
+ if (!existsSync17(filePath)) {
6778
8306
  throw new Error(
6779
8307
  `Token file not found at ${filePath}.
6780
8308
  Create a reactscope.tokens.json file or use --file to specify a path.`
6781
8309
  );
6782
8310
  }
6783
- const raw = readFileSync12(filePath, "utf-8");
8311
+ const raw = readFileSync16(filePath, "utf-8");
6784
8312
  const useJson = opts.format === "json" || opts.format !== "text" && !isTTY2();
6785
8313
  const errors = [];
6786
8314
  let parsed;
@@ -6851,6 +8379,7 @@ function createTokensCommand() {
6851
8379
  const tokensCmd = new Command11("tokens").description(
6852
8380
  'Query, validate, and export design tokens from reactscope.tokens.json.\n\nTOKEN FILE RESOLUTION (in priority order):\n 1. --file <path> explicit override\n 2. tokens.file in reactscope.config.json\n 3. reactscope.tokens.json default (project root)\n\nTOKEN FILE FORMAT (reactscope.tokens.json):\n Nested JSON. Each leaf is a token with { value, type } or just a raw value.\n Paths use dot notation: color.primary.500, spacing.4, font.size.base\n References use {path.to.other.token} syntax.\n Themes: top-level "themes" key with named override maps.\n\nTOKEN TYPES: color | spacing | typography | shadow | radius | opacity | other\n\nExamples:\n scope tokens validate\n scope tokens list color\n scope tokens get color.primary.500\n scope tokens compliance\n scope tokens export --format css --out tokens.css'
6853
8381
  );
8382
+ registerTokensInit(tokensCmd);
6854
8383
  registerGet2(tokensCmd);
6855
8384
  registerList2(tokensCmd);
6856
8385
  registerSearch(tokensCmd);
@@ -6866,7 +8395,7 @@ function createTokensCommand() {
6866
8395
  // src/program.ts
6867
8396
  function createProgram(options = {}) {
6868
8397
  const program2 = new Command12("scope").version(options.version ?? "0.1.0").description(
6869
- 'Scope \u2014 static analysis + visual rendering toolkit for React component libraries.\n\nScope answers questions about React codebases \u2014 structure, props, visual output,\ndesign token compliance \u2014 without running the full application.\n\nQUICKSTART (new project):\n scope init # detect config, scaffold reactscope.config.json\n scope doctor # verify setup before doing anything else\n scope manifest generate # scan source and build component manifest\n scope render all # screenshot every component\n scope site build # build HTML gallery\n scope site serve # open at http://localhost:3000\n\nQUICKSTART (existing project / CI):\n scope ci # manifest \u2192 render \u2192 compliance \u2192 regression in one step\n\nAGENT BOOTSTRAP:\n scope get-skill # print SKILL.md to stdout \u2014 pipe into agent context\n\nCONFIG FILE: reactscope.config.json (created by `scope init`)\n components.include glob patterns for component files (e.g. "src/**/*.tsx")\n components.wrappers providers and globalCSS to wrap every render\n render.viewport default viewport width\xD7height in px\n tokens.file path to reactscope.tokens.json (default)\n output.dir output root (default: .reactscope/)\n ci.complianceThreshold fail threshold for `scope ci` (default: 0.90)\n\nOUTPUT DIRECTORY: .reactscope/\n manifest.json component registry \u2014 updated by `scope manifest generate`\n renders/<Name>/ PNGs + render.json per component\n compliance-styles.json computed-style map for token matching\n site/ static HTML gallery (built by `scope site build`)\n\nRun `scope <command> --help` for detailed flags and examples.'
8398
+ 'Scope \u2014 static analysis + visual rendering toolkit for React component libraries.\n\nScope answers questions about React codebases \u2014 structure, props, visual output,\ndesign token compliance \u2014 without running the full application.\n\nAGENT QUICKSTART:\n scope get-skill > /tmp/scope-skill.md\n scope init --yes\n scope doctor --json\n scope manifest list --format json\n scope render all --format json --output-dir .reactscope/renders\n scope site build --output .reactscope/site\n scope instrument profile http://localhost:5173\n\nCI QUICKSTART:\n scope ci --json --output .reactscope/ci-result.json\n\nCONFIG FILE: reactscope.config.json (created by `scope init`)\n components.include glob patterns for component files (e.g. "src/**/*.tsx")\n components.wrappers providers and globalCSS to wrap every render\n render.viewport default viewport width\xD7height in px\n tokens.file path to reactscope.tokens.json (default)\n output.dir output root (default: .reactscope/)\n ci.complianceThreshold fail threshold for `scope ci` (default: 0.90)\n\nOUTPUT DIRECTORY: .reactscope/\n manifest.json component registry \u2014 updated by `scope manifest generate`\n renders/<Name>/ PNGs + render.json per component\n compliance-styles.json computed-style map for token matching\n site/ static HTML gallery (built by `scope site build`)\n\nRun `scope <command> --help` for detailed flags and examples.'
6870
8399
  );
6871
8400
  program2.command("capture <url>").description(
6872
8401
  "Capture the live React component tree from a running app and emit it as JSON.\nRequires a running dev server at the given URL (e.g. http://localhost:5173).\n\nExamples:\n scope capture http://localhost:5173\n scope capture http://localhost:5173 -o report.json --pretty\n scope capture http://localhost:5173 --timeout 15000 --wait 500"
@@ -6948,7 +8477,7 @@ function createProgram(options = {}) {
6948
8477
  program2.command("generate").description(
6949
8478
  'Generate a Playwright test file from a Scope trace (.json).\nTraces are produced by scope instrument renders or scope capture.\n\nExamples:\n scope generate trace.json\n scope generate trace.json -o tests/scope.spec.ts -d "User login flow"'
6950
8479
  ).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) => {
6951
- const raw = readFileSync13(tracePath, "utf-8");
8480
+ const raw = readFileSync17(tracePath, "utf-8");
6952
8481
  const trace = loadTrace(raw);
6953
8482
  const source = generateTest(trace, {
6954
8483
  description: opts.description,