@agent-scope/cli 1.10.0 → 1.12.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.js CHANGED
@@ -1,9 +1,9 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  // src/program.ts
4
- import { readFileSync as readFileSync6 } from "fs";
4
+ import { readFileSync as readFileSync7 } from "fs";
5
5
  import { generateTest, loadTrace } from "@agent-scope/playwright";
6
- import { Command as Command6 } from "commander";
6
+ import { Command as Command8 } from "commander";
7
7
 
8
8
  // src/browser.ts
9
9
  import { writeFileSync } from "fs";
@@ -255,9 +255,9 @@ function createRL() {
255
255
  });
256
256
  }
257
257
  async function ask(rl, question) {
258
- return new Promise((resolve7) => {
258
+ return new Promise((resolve11) => {
259
259
  rl.question(question, (answer) => {
260
- resolve7(answer.trim());
260
+ resolve11(answer.trim());
261
261
  });
262
262
  });
263
263
  }
@@ -406,9 +406,9 @@ function createInitCommand() {
406
406
  }
407
407
 
408
408
  // src/instrument/renders.ts
409
- import { resolve as resolve2 } from "path";
410
- import { BrowserPool } from "@agent-scope/render";
411
- import { Command as Command3 } from "commander";
409
+ import { resolve as resolve5 } from "path";
410
+ import { BrowserPool as BrowserPool2 } from "@agent-scope/render";
411
+ import { Command as Command4 } from "commander";
412
412
 
413
413
  // src/component-bundler.ts
414
414
  import { dirname } from "path";
@@ -913,52 +913,1060 @@ function formatMatrixCsv(componentName, result) {
913
913
  return `${[headers.join(","), ...rows].join("\n")}
914
914
  `;
915
915
  }
916
- function renderProgressBar(completed, total, currentName, pct, barWidth = 20) {
917
- const filled = Math.round(pct / 100 * barWidth);
918
- const empty = barWidth - filled;
919
- const bar = "=".repeat(Math.max(0, filled - 1)) + (filled > 0 ? ">" : "") + " ".repeat(empty);
920
- const nameSlice = currentName.slice(0, 25).padEnd(25);
921
- return `Rendering ${completed}/${total} ${nameSlice} [${bar}] ${pct}%`;
916
+ function renderProgressBar(completed, total, currentName, pct, barWidth = 20) {
917
+ const filled = Math.round(pct / 100 * barWidth);
918
+ const empty = barWidth - filled;
919
+ const bar = "=".repeat(Math.max(0, filled - 1)) + (filled > 0 ? ">" : "") + " ".repeat(empty);
920
+ const nameSlice = currentName.slice(0, 25).padEnd(25);
921
+ return `Rendering ${completed}/${total} ${nameSlice} [${bar}] ${pct}%`;
922
+ }
923
+ function formatSummaryText(results, outputDir) {
924
+ const total = results.length;
925
+ const passed = results.filter((r) => r.success).length;
926
+ const failed = total - passed;
927
+ const successTimes = results.filter((r) => r.success).map((r) => r.renderTimeMs);
928
+ const avgMs = successTimes.length > 0 ? successTimes.reduce((a, b) => a + b, 0) / successTimes.length : 0;
929
+ const lines = [
930
+ "\u2500".repeat(60),
931
+ `Render Summary`,
932
+ "\u2500".repeat(60),
933
+ ` Total components : ${total}`,
934
+ ` Passed : ${passed}`,
935
+ ` Failed : ${failed}`,
936
+ ` Avg render time : ${avgMs.toFixed(1)}ms`,
937
+ ` Output dir : ${outputDir}`
938
+ ];
939
+ if (failed > 0) {
940
+ lines.push("", " Failed components:");
941
+ for (const r of results) {
942
+ if (!r.success) {
943
+ lines.push(` \u2717 ${r.name}: ${r.errorMessage ?? "unknown error"}`);
944
+ }
945
+ }
946
+ }
947
+ lines.push("\u2500".repeat(60));
948
+ return lines.join("\n");
949
+ }
950
+ function escapeHtml(str) {
951
+ return str.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#039;");
952
+ }
953
+ function csvEscape(value) {
954
+ if (value.includes(",") || value.includes('"') || value.includes("\n")) {
955
+ return `"${value.replace(/"/g, '""')}"`;
956
+ }
957
+ return value;
958
+ }
959
+
960
+ // src/instrument/hooks.ts
961
+ import { resolve as resolve2 } from "path";
962
+ import { Command as Cmd } from "commander";
963
+ import { chromium as chromium2 } from "playwright";
964
+ var MANIFEST_PATH2 = ".reactscope/manifest.json";
965
+ function buildHookInstrumentationScript() {
966
+ return `
967
+ (function __scopeHooksInstrument() {
968
+ // Locate the React DevTools hook installed by the browser-entry bundle.
969
+ // We use a lighter approach: walk __REACT_DEVTOOLS_GLOBAL_HOOK__ renderers.
970
+ var hook = window.__REACT_DEVTOOLS_GLOBAL_HOOK__;
971
+ if (!hook) return { error: "No React DevTools hook found" };
972
+
973
+ var renderers = hook._renderers || hook.renderers;
974
+ if (!renderers || renderers.size === 0) return { error: "No React renderers registered" };
975
+
976
+ // Get the first renderer's fiber roots
977
+ var renderer = null;
978
+ if (renderers.forEach) {
979
+ renderers.forEach(function(r) { if (!renderer) renderer = r; });
980
+ } else {
981
+ renderer = Object.values(renderers)[0];
982
+ }
983
+
984
+ // Handle both our wrapped format {id, renderer, fiberRoots} and raw renderer
985
+ var fiberRoots;
986
+ if (renderer && renderer.fiberRoots) {
987
+ fiberRoots = renderer.fiberRoots;
988
+ } else {
989
+ return { error: "No fiber roots found" };
990
+ }
991
+
992
+ var rootFiber = null;
993
+ fiberRoots.forEach(function(r) { if (!rootFiber) rootFiber = r; });
994
+ if (!rootFiber) return { error: "No fiber root" };
995
+
996
+ var current = rootFiber.current;
997
+ if (!current) return { error: "No current fiber" };
998
+
999
+ // Walk fiber tree and collect hook data per component
1000
+ var components = [];
1001
+
1002
+ function serializeValue(v) {
1003
+ if (v === null) return null;
1004
+ if (v === undefined) return undefined;
1005
+ var t = typeof v;
1006
+ if (t === "string" || t === "number" || t === "boolean") return v;
1007
+ if (t === "function") return "[function " + (v.name || "anonymous") + "]";
1008
+ if (Array.isArray(v)) {
1009
+ try { return v.map(serializeValue); } catch(e) { return "[Array]"; }
1010
+ }
1011
+ if (t === "object") {
1012
+ try {
1013
+ var keys = Object.keys(v).slice(0, 5);
1014
+ var out = {};
1015
+ for (var k of keys) { out[k] = serializeValue(v[k]); }
1016
+ if (Object.keys(v).length > 5) out["..."] = "(truncated)";
1017
+ return out;
1018
+ } catch(e) { return "[Object]"; }
1019
+ }
1020
+ return String(v);
1021
+ }
1022
+
1023
+ // Hook node classifiers (mirrors hooks-extractor.ts logic)
1024
+ var HookLayout = 0b00100;
1025
+
1026
+ function isEffectNode(node) {
1027
+ var ms = node.memoizedState;
1028
+ if (!ms || typeof ms !== "object") return false;
1029
+ return typeof ms.create === "function" && "deps" in ms && typeof ms.tag === "number";
1030
+ }
1031
+
1032
+ function isRefNode(node) {
1033
+ if (node.queue != null) return false;
1034
+ var ms = node.memoizedState;
1035
+ if (!ms || typeof ms !== "object" || Array.isArray(ms)) return false;
1036
+ var keys = Object.keys(ms);
1037
+ return keys.length === 1 && keys[0] === "current";
1038
+ }
1039
+
1040
+ function isMemoTuple(node) {
1041
+ if (node.queue != null) return false;
1042
+ var ms = node.memoizedState;
1043
+ if (!Array.isArray(ms) || ms.length !== 2) return false;
1044
+ return ms[1] === null || Array.isArray(ms[1]);
1045
+ }
1046
+
1047
+ function isStateOrReducer(node) {
1048
+ return node.queue != null &&
1049
+ typeof node.queue === "object" &&
1050
+ typeof node.queue.dispatch === "function";
1051
+ }
1052
+
1053
+ function isReducer(node) {
1054
+ if (!isStateOrReducer(node)) return false;
1055
+ var q = node.queue;
1056
+ if (typeof q.reducer === "function") return true;
1057
+ var lrr = q.lastRenderedReducer;
1058
+ if (typeof lrr !== "function") return false;
1059
+ var name = lrr.name || "";
1060
+ return name !== "basicStateReducer" && name !== "";
1061
+ }
1062
+
1063
+ function classifyHookNode(node, index) {
1064
+ var profile = { index: index, type: "custom" };
1065
+
1066
+ if (isEffectNode(node)) {
1067
+ var effect = node.memoizedState;
1068
+ profile.type = (effect.tag & HookLayout) ? "useLayoutEffect" : "useEffect";
1069
+ profile.dependencyValues = effect.deps ? effect.deps.map(serializeValue) : null;
1070
+ profile.cleanupPresence = typeof effect.destroy === "function";
1071
+ profile.fireCount = 1; // We can only observe the mount; runtime tracking would need injection
1072
+ profile.lastDuration = null;
1073
+ return profile;
1074
+ }
1075
+
1076
+ if (isRefNode(node)) {
1077
+ var ref = node.memoizedState;
1078
+ profile.type = "useRef";
1079
+ profile.currentRefValue = serializeValue(ref.current);
1080
+ profile.readCountDuringRender = 0; // static snapshot; read count requires instrumented wrapper
1081
+ return profile;
1082
+ }
1083
+
1084
+ if (isMemoTuple(node)) {
1085
+ var tuple = node.memoizedState;
1086
+ var val = tuple[0];
1087
+ var deps = tuple[1];
1088
+ profile.type = typeof val === "function" ? "useCallback" : "useMemo";
1089
+ profile.currentValue = serializeValue(val);
1090
+ profile.dependencyValues = deps ? deps.map(serializeValue) : null;
1091
+ // recomputeCount cannot be known from a single snapshot; set to 0 for mount
1092
+ profile.recomputeCount = 0;
1093
+ // On mount, first render always computes \u2192 cacheHitRate is 0 (no prior hits)
1094
+ profile.cacheHitRate = 0;
1095
+ return profile;
1096
+ }
1097
+
1098
+ if (isStateOrReducer(node)) {
1099
+ if (isReducer(node)) {
1100
+ profile.type = "useReducer";
1101
+ profile.currentValue = serializeValue(node.memoizedState);
1102
+ profile.updateCount = 0;
1103
+ profile.actionTypesDispatched = [];
1104
+ // stateTransitions: we record current state as first entry
1105
+ profile.stateTransitions = [serializeValue(node.memoizedState)];
1106
+ } else {
1107
+ profile.type = "useState";
1108
+ profile.currentValue = serializeValue(node.memoizedState);
1109
+ profile.updateCount = 0;
1110
+ profile.updateOrigins = [];
1111
+ }
1112
+ return profile;
1113
+ }
1114
+
1115
+ // useContext: detected via _currentValue / _currentValue2 property (React context internals)
1116
+ // Context consumers in React store the context VALUE in memoizedState directly, not
1117
+ // via a queue. Context objects themselves have _currentValue.
1118
+ // We check if memoizedState could be a context value by seeing if node has no queue
1119
+ // and memoizedState is not an array and not an effect.
1120
+ if (!node.queue && !isRefNode(node) && !isMemoTuple(node) && !isEffectNode(node)) {
1121
+ profile.type = "custom";
1122
+ profile.currentValue = serializeValue(node.memoizedState);
1123
+ return profile;
1124
+ }
1125
+
1126
+ return profile;
1127
+ }
1128
+
1129
+ // Walk the fiber tree
1130
+ var FunctionComponent = 0;
1131
+ var ClassComponent = 1;
1132
+ var ForwardRef = 11;
1133
+ var MemoComponent = 14;
1134
+ var SimpleMemoComponent = 15;
1135
+ var HostRoot = 3;
1136
+
1137
+ function getFiberName(fiber) {
1138
+ if (!fiber.type) return null;
1139
+ if (typeof fiber.type === "string") return fiber.type;
1140
+ if (typeof fiber.type === "function") return fiber.type.displayName || fiber.type.name || "Anonymous";
1141
+ if (fiber.type.displayName) return fiber.type.displayName;
1142
+ if (fiber.type.render) {
1143
+ return fiber.type.render.displayName || fiber.type.render.name || "ForwardRef";
1144
+ }
1145
+ if (fiber.type.type) {
1146
+ return (fiber.type.type.displayName || fiber.type.type.name || "Memo");
1147
+ }
1148
+ return "Unknown";
1149
+ }
1150
+
1151
+ function isComponentFiber(fiber) {
1152
+ var tag = fiber.tag;
1153
+ return tag === FunctionComponent || tag === ClassComponent ||
1154
+ tag === ForwardRef || tag === MemoComponent || tag === SimpleMemoComponent;
1155
+ }
1156
+
1157
+ function walkFiber(fiber) {
1158
+ if (!fiber) return;
1159
+
1160
+ if (isComponentFiber(fiber)) {
1161
+ var name = getFiberName(fiber);
1162
+ if (name) {
1163
+ var hooks = [];
1164
+ var hookNode = fiber.memoizedState;
1165
+ var idx = 0;
1166
+ while (hookNode !== null && hookNode !== undefined) {
1167
+ hooks.push(classifyHookNode(hookNode, idx));
1168
+ hookNode = hookNode.next;
1169
+ idx++;
1170
+ }
1171
+
1172
+ var source = null;
1173
+ if (fiber._debugSource) {
1174
+ source = { file: fiber._debugSource.fileName, line: fiber._debugSource.lineNumber };
1175
+ }
1176
+
1177
+ if (hooks.length > 0) {
1178
+ components.push({ name: name, source: source, hooks: hooks });
1179
+ }
1180
+ }
1181
+ }
1182
+
1183
+ walkFiber(fiber.child);
1184
+ walkFiber(fiber.sibling);
1185
+ }
1186
+
1187
+ walkFiber(current.child);
1188
+
1189
+ return { components: components };
1190
+ })();
1191
+ `;
1192
+ }
1193
+ function analyzeHookFlags(hooks) {
1194
+ const flags = /* @__PURE__ */ new Set();
1195
+ for (const hook of hooks) {
1196
+ if ((hook.type === "useEffect" || hook.type === "useLayoutEffect") && hook.dependencyValues !== void 0 && hook.dependencyValues === null) {
1197
+ flags.add("EFFECT_EVERY_RENDER");
1198
+ }
1199
+ if ((hook.type === "useEffect" || hook.type === "useLayoutEffect") && hook.cleanupPresence === false && hook.dependencyValues === null) {
1200
+ flags.add("MISSING_CLEANUP");
1201
+ }
1202
+ if ((hook.type === "useMemo" || hook.type === "useCallback") && hook.dependencyValues === null) {
1203
+ flags.add("MEMO_INEFFECTIVE");
1204
+ }
1205
+ if ((hook.type === "useMemo" || hook.type === "useCallback") && hook.cacheHitRate !== void 0 && hook.cacheHitRate === 0 && hook.recomputeCount !== void 0 && hook.recomputeCount > 1) {
1206
+ flags.add("MEMO_INEFFECTIVE");
1207
+ }
1208
+ if ((hook.type === "useState" || hook.type === "useReducer") && hook.updateCount !== void 0 && hook.updateCount > 10) {
1209
+ flags.add("STATE_UPDATE_LOOP");
1210
+ }
1211
+ }
1212
+ return [...flags];
1213
+ }
1214
+ async function runHooksProfiling(componentName, filePath, props) {
1215
+ const browser = await chromium2.launch({ headless: true });
1216
+ try {
1217
+ const context = await browser.newContext();
1218
+ const page = await context.newPage();
1219
+ const htmlHarness = await buildComponentHarness(filePath, componentName, props, 1280);
1220
+ await page.setContent(htmlHarness, { waitUntil: "load" });
1221
+ await page.waitForFunction(
1222
+ () => {
1223
+ const w = window;
1224
+ return w.__SCOPE_RENDER_COMPLETE__ === true;
1225
+ },
1226
+ { timeout: 15e3 }
1227
+ );
1228
+ const renderError = await page.evaluate(() => {
1229
+ return window.__SCOPE_RENDER_ERROR__ ?? null;
1230
+ });
1231
+ if (renderError !== null) {
1232
+ throw new Error(`Component render error: ${renderError}`);
1233
+ }
1234
+ const instrumentScript = buildHookInstrumentationScript();
1235
+ const raw = await page.evaluate(instrumentScript);
1236
+ const result = raw;
1237
+ if (result.error) {
1238
+ throw new Error(`Hook instrumentation failed: ${result.error}`);
1239
+ }
1240
+ const rawComponents = result.components ?? [];
1241
+ const components = rawComponents.map((c) => ({
1242
+ name: c.name,
1243
+ source: c.source,
1244
+ hooks: c.hooks,
1245
+ flags: analyzeHookFlags(c.hooks)
1246
+ }));
1247
+ const allFlags = /* @__PURE__ */ new Set();
1248
+ for (const comp of components) {
1249
+ for (const flag of comp.flags) {
1250
+ allFlags.add(flag);
1251
+ }
1252
+ }
1253
+ return {
1254
+ component: componentName,
1255
+ components,
1256
+ flags: [...allFlags]
1257
+ };
1258
+ } finally {
1259
+ await browser.close();
1260
+ }
1261
+ }
1262
+ function createInstrumentHooksCommand() {
1263
+ const cmd = new Cmd("hooks").description(
1264
+ "Profile per-hook-instance data for a component: update counts, cache hit rates, effect counts, and more"
1265
+ ).argument("<component>", "Component name (must exist in the manifest)").option("--props <json>", "Inline props JSON passed to the component", "{}").option("--manifest <path>", "Path to manifest.json", MANIFEST_PATH2).option("--format <fmt>", "Output format: json|text (default: auto)", "json").option("--show-flags", "Show heuristic flags only (useful for CI checks)", false).action(
1266
+ async (componentName, opts) => {
1267
+ try {
1268
+ const manifest = loadManifest(opts.manifest);
1269
+ const descriptor = manifest.components[componentName];
1270
+ if (descriptor === void 0) {
1271
+ const available = Object.keys(manifest.components).slice(0, 5).join(", ");
1272
+ throw new Error(
1273
+ `Component "${componentName}" not found in manifest.
1274
+ Available: ${available}`
1275
+ );
1276
+ }
1277
+ let props = {};
1278
+ try {
1279
+ props = JSON.parse(opts.props);
1280
+ } catch {
1281
+ throw new Error(`Invalid props JSON: ${opts.props}`);
1282
+ }
1283
+ const rootDir = process.cwd();
1284
+ const filePath = resolve2(rootDir, descriptor.filePath);
1285
+ process.stderr.write(`Instrumenting hooks for ${componentName}\u2026
1286
+ `);
1287
+ const result = await runHooksProfiling(componentName, filePath, props);
1288
+ if (opts.showFlags) {
1289
+ if (result.flags.length === 0) {
1290
+ process.stdout.write("No heuristic flags detected.\n");
1291
+ } else {
1292
+ for (const flag of result.flags) {
1293
+ process.stdout.write(`${flag}
1294
+ `);
1295
+ }
1296
+ }
1297
+ return;
1298
+ }
1299
+ process.stdout.write(`${JSON.stringify(result, null, 2)}
1300
+ `);
1301
+ } catch (err) {
1302
+ process.stderr.write(`Error: ${err instanceof Error ? err.message : String(err)}
1303
+ `);
1304
+ process.exit(1);
1305
+ }
1306
+ }
1307
+ );
1308
+ return cmd;
1309
+ }
1310
+
1311
+ // src/instrument/profile.ts
1312
+ import { resolve as resolve3 } from "path";
1313
+ import { Command as Cmd2 } from "commander";
1314
+ import { chromium as chromium3 } from "playwright";
1315
+ var MANIFEST_PATH3 = ".reactscope/manifest.json";
1316
+ function buildProfilingSetupScript() {
1317
+ return `
1318
+ (function __scopeProfileSetup() {
1319
+ window.__scopeProfileData = {
1320
+ commitCount: 0,
1321
+ componentNames: new Set(),
1322
+ fiberSnapshots: []
1323
+ };
1324
+
1325
+ var hook = window.__REACT_DEVTOOLS_GLOBAL_HOOK__;
1326
+ if (!hook) return { error: "No DevTools hook" };
1327
+
1328
+ // Wrap onCommitFiberRoot to count commits and collect component names
1329
+ var origCommit = hook.onCommitFiberRoot.bind(hook);
1330
+ hook.onCommitFiberRoot = function(rendererID, root, priorityLevel) {
1331
+ origCommit(rendererID, root, priorityLevel);
1332
+
1333
+ window.__scopeProfileData.commitCount++;
1334
+
1335
+ // Walk the committed tree to collect re-rendered component names
1336
+ var current = root && root.current;
1337
+ if (!current) return;
1338
+
1339
+ function walkFiber(fiber) {
1340
+ if (!fiber) return;
1341
+ var tag = fiber.tag;
1342
+ // FunctionComponent=0, ClassComponent=1, ForwardRef=11, Memo=14, SimpleMemo=15
1343
+ if (tag === 0 || tag === 1 || tag === 11 || tag === 14 || tag === 15) {
1344
+ var name = null;
1345
+ if (fiber.type) {
1346
+ if (typeof fiber.type === "function") name = fiber.type.displayName || fiber.type.name;
1347
+ else if (fiber.type.displayName) name = fiber.type.displayName;
1348
+ else if (fiber.type.render) name = fiber.type.render.displayName || fiber.type.render.name;
1349
+ else if (fiber.type.type) name = fiber.type.type.displayName || fiber.type.type.name;
1350
+ }
1351
+ // Only count fibers with a positive actualDuration (actually re-rendered this commit)
1352
+ if (name && typeof fiber.actualDuration === "number" && fiber.actualDuration >= 0) {
1353
+ window.__scopeProfileData.componentNames.add(name);
1354
+ }
1355
+ }
1356
+ walkFiber(fiber.child);
1357
+ walkFiber(fiber.sibling);
1358
+ }
1359
+
1360
+ var wip = root.current.alternate || root.current;
1361
+ walkFiber(wip.child);
1362
+ };
1363
+
1364
+ // Install PerformanceObserver for layout and paint timing
1365
+ window.__scopeLayoutTime = 0;
1366
+ window.__scopePaintTime = 0;
1367
+ window.__scopeLayoutShifts = { count: 0, score: 0 };
1368
+
1369
+ try {
1370
+ var observer = new PerformanceObserver(function(list) {
1371
+ for (var entry of list.getEntries()) {
1372
+ if (entry.entryType === "layout-shift") {
1373
+ window.__scopeLayoutShifts.count++;
1374
+ window.__scopeLayoutShifts.score += entry.value || 0;
1375
+ }
1376
+ if (entry.entryType === "longtask") {
1377
+ window.__scopeLayoutTime += entry.duration || 0;
1378
+ }
1379
+ if (entry.entryType === "paint") {
1380
+ window.__scopePaintTime += entry.startTime || 0;
1381
+ }
1382
+ }
1383
+ });
1384
+ observer.observe({ entryTypes: ["layout-shift", "longtask", "paint"] });
1385
+ } catch(e) {
1386
+ // PerformanceObserver may not be available in all Playwright contexts
1387
+ }
1388
+
1389
+ return { ok: true };
1390
+ })();
1391
+ `;
1392
+ }
1393
+ function buildProfilingCollectScript() {
1394
+ return `
1395
+ (function __scopeProfileCollect() {
1396
+ var data = window.__scopeProfileData;
1397
+ if (!data) return { error: "Profiling not set up" };
1398
+
1399
+ // Estimate wasted renders: use paint entries count as a heuristic for
1400
+ // "components that re-rendered but their subtree output was likely unchanged".
1401
+ // A more accurate method would require React's own wasted-render detection,
1402
+ // which requires React Profiler API. We use a conservative estimate here.
1403
+ var totalCommits = data.commitCount;
1404
+ var uniqueNames = Array.from(data.componentNames);
1405
+
1406
+ // Wasted renders heuristic: if a component is in a subsequent commit (not the initial
1407
+ // mount commit), it *may* have been wasted if it didn't actually need to re-render.
1408
+ // For the initial snapshot we approximate: wastedRenders = max(0, totalCommits - 1) * 0.3
1409
+ // This is a heuristic \u2014 real wasted render detection needs shouldComponentUpdate/React.memo tracing.
1410
+ var wastedRenders = Math.max(0, Math.round((totalCommits - 1) * uniqueNames.length * 0.3));
1411
+
1412
+ return {
1413
+ commitCount: totalCommits,
1414
+ uniqueComponents: uniqueNames.length,
1415
+ componentNames: uniqueNames,
1416
+ wastedRenders: wastedRenders,
1417
+ layoutTime: window.__scopeLayoutTime || 0,
1418
+ paintTime: window.__scopePaintTime || 0,
1419
+ layoutShifts: window.__scopeLayoutShifts || { count: 0, score: 0 }
1420
+ };
1421
+ })();
1422
+ `;
1423
+ }
1424
+ async function replayInteraction(page, steps) {
1425
+ for (const step of steps) {
1426
+ switch (step.action) {
1427
+ case "click":
1428
+ if (step.target) {
1429
+ await page.click(step.target, { timeout: 5e3 }).catch(() => {
1430
+ process.stderr.write(` \u26A0 click target "${step.target}" not found, skipping
1431
+ `);
1432
+ });
1433
+ }
1434
+ break;
1435
+ case "fill":
1436
+ if (step.target && step.value !== void 0) {
1437
+ await page.fill(step.target, step.value, { timeout: 5e3 }).catch(() => {
1438
+ process.stderr.write(` \u26A0 fill target "${step.target}" not found, skipping
1439
+ `);
1440
+ });
1441
+ }
1442
+ break;
1443
+ case "hover":
1444
+ if (step.target) {
1445
+ await page.hover(step.target, { timeout: 5e3 }).catch(() => {
1446
+ process.stderr.write(` \u26A0 hover target "${step.target}" not found, skipping
1447
+ `);
1448
+ });
1449
+ }
1450
+ break;
1451
+ case "press":
1452
+ if (step.value) {
1453
+ await page.keyboard.press(step.value);
1454
+ }
1455
+ break;
1456
+ case "wait":
1457
+ await page.waitForTimeout(step.delay ?? 500);
1458
+ break;
1459
+ }
1460
+ }
1461
+ }
1462
+ function analyzeProfileFlags(totalRenders, wastedRenders, timing, layoutShifts) {
1463
+ const flags = /* @__PURE__ */ new Set();
1464
+ if (wastedRenders > 0 && wastedRenders / Math.max(1, totalRenders) > 0.3) {
1465
+ flags.add("WASTED_RENDER");
1466
+ }
1467
+ if (totalRenders > 10) {
1468
+ flags.add("HIGH_RENDER_COUNT");
1469
+ }
1470
+ if (layoutShifts.cumulativeScore > 0.1) {
1471
+ flags.add("LAYOUT_SHIFT_DETECTED");
1472
+ }
1473
+ if (timing.js > 100) {
1474
+ flags.add("SLOW_INTERACTION");
1475
+ }
1476
+ return [...flags];
1477
+ }
1478
+ async function runInteractionProfile(componentName, filePath, props, interaction) {
1479
+ const browser = await chromium3.launch({ headless: true });
1480
+ try {
1481
+ const context = await browser.newContext();
1482
+ const page = await context.newPage();
1483
+ const htmlHarness = await buildComponentHarness(filePath, componentName, props, 1280);
1484
+ await page.setContent(htmlHarness, { waitUntil: "load" });
1485
+ await page.waitForFunction(
1486
+ () => {
1487
+ const w = window;
1488
+ return w.__SCOPE_RENDER_COMPLETE__ === true;
1489
+ },
1490
+ { timeout: 15e3 }
1491
+ );
1492
+ const renderError = await page.evaluate(() => {
1493
+ return window.__SCOPE_RENDER_ERROR__ ?? null;
1494
+ });
1495
+ if (renderError !== null) {
1496
+ throw new Error(`Component render error: ${renderError}`);
1497
+ }
1498
+ const setupResult = await page.evaluate(buildProfilingSetupScript());
1499
+ const setupData = setupResult;
1500
+ if (setupData.error) {
1501
+ throw new Error(`Profiling setup failed: ${setupData.error}`);
1502
+ }
1503
+ const jsStart = Date.now();
1504
+ if (interaction.length > 0) {
1505
+ process.stderr.write(` Replaying ${interaction.length} interaction step(s)\u2026
1506
+ `);
1507
+ await replayInteraction(page, interaction);
1508
+ await page.waitForTimeout(300);
1509
+ }
1510
+ const jsDuration = Date.now() - jsStart;
1511
+ const collected = await page.evaluate(buildProfilingCollectScript());
1512
+ const profileData = collected;
1513
+ if (profileData.error) {
1514
+ throw new Error(`Profile collection failed: ${profileData.error}`);
1515
+ }
1516
+ const timing = {
1517
+ js: jsDuration,
1518
+ layout: profileData.layoutTime ?? 0,
1519
+ paint: profileData.paintTime ?? 0
1520
+ };
1521
+ const layoutShifts = {
1522
+ count: profileData.layoutShifts?.count ?? 0,
1523
+ cumulativeScore: profileData.layoutShifts?.score ?? 0
1524
+ };
1525
+ const totalRenders = profileData.commitCount ?? 0;
1526
+ const uniqueComponents = profileData.uniqueComponents ?? 0;
1527
+ const wastedRenders = profileData.wastedRenders ?? 0;
1528
+ const flags = analyzeProfileFlags(totalRenders, wastedRenders, timing, layoutShifts);
1529
+ return {
1530
+ component: componentName,
1531
+ totalRenders,
1532
+ uniqueComponents,
1533
+ wastedRenders,
1534
+ timing,
1535
+ layoutShifts,
1536
+ flags,
1537
+ interaction
1538
+ };
1539
+ } finally {
1540
+ await browser.close();
1541
+ }
1542
+ }
1543
+ function createInstrumentProfileCommand() {
1544
+ const cmd = new Cmd2("profile").description(
1545
+ "Capture a full interaction-scoped performance profile: renders, timing, layout shifts"
1546
+ ).argument("<component>", "Component name (must exist in the manifest)").option(
1547
+ "--interaction <json>",
1548
+ `Interaction steps JSON, e.g. '[{"action":"click","target":"button.primary"}]'`,
1549
+ "[]"
1550
+ ).option("--props <json>", "Inline props JSON passed to the component", "{}").option("--manifest <path>", "Path to manifest.json", MANIFEST_PATH3).option("--format <fmt>", "Output format: json|text (default: json)", "json").option("--show-flags", "Show heuristic flags only (useful for CI checks)", false).action(
1551
+ async (componentName, opts) => {
1552
+ try {
1553
+ const manifest = loadManifest(opts.manifest);
1554
+ const descriptor = manifest.components[componentName];
1555
+ if (descriptor === void 0) {
1556
+ const available = Object.keys(manifest.components).slice(0, 5).join(", ");
1557
+ throw new Error(
1558
+ `Component "${componentName}" not found in manifest.
1559
+ Available: ${available}`
1560
+ );
1561
+ }
1562
+ let props = {};
1563
+ try {
1564
+ props = JSON.parse(opts.props);
1565
+ } catch {
1566
+ throw new Error(`Invalid props JSON: ${opts.props}`);
1567
+ }
1568
+ let interaction = [];
1569
+ try {
1570
+ interaction = JSON.parse(opts.interaction);
1571
+ if (!Array.isArray(interaction)) {
1572
+ throw new Error("Interaction must be a JSON array");
1573
+ }
1574
+ } catch {
1575
+ throw new Error(`Invalid interaction JSON: ${opts.interaction}`);
1576
+ }
1577
+ const rootDir = process.cwd();
1578
+ const filePath = resolve3(rootDir, descriptor.filePath);
1579
+ process.stderr.write(`Profiling interaction for ${componentName}\u2026
1580
+ `);
1581
+ const result = await runInteractionProfile(componentName, filePath, props, interaction);
1582
+ if (opts.showFlags) {
1583
+ if (result.flags.length === 0) {
1584
+ process.stdout.write("No heuristic flags detected.\n");
1585
+ } else {
1586
+ for (const flag of result.flags) {
1587
+ process.stdout.write(`${flag}
1588
+ `);
1589
+ }
1590
+ }
1591
+ return;
1592
+ }
1593
+ process.stdout.write(`${JSON.stringify(result, null, 2)}
1594
+ `);
1595
+ } catch (err) {
1596
+ process.stderr.write(`Error: ${err instanceof Error ? err.message : String(err)}
1597
+ `);
1598
+ process.exit(1);
1599
+ }
1600
+ }
1601
+ );
1602
+ return cmd;
1603
+ }
1604
+
1605
+ // src/instrument/tree.ts
1606
+ import { resolve as resolve4 } from "path";
1607
+ import { getBrowserEntryScript as getBrowserEntryScript2 } from "@agent-scope/playwright";
1608
+ import { BrowserPool } from "@agent-scope/render";
1609
+ import { Command as Command3 } from "commander";
1610
+ var MANIFEST_PATH4 = ".reactscope/manifest.json";
1611
+ var DEFAULT_VIEWPORT_WIDTH = 375;
1612
+ var DEFAULT_VIEWPORT_HEIGHT = 812;
1613
+ var _pool = null;
1614
+ async function getPool() {
1615
+ if (_pool === null) {
1616
+ _pool = new BrowserPool({
1617
+ size: { browsers: 1, pagesPerBrowser: 1 },
1618
+ viewportWidth: DEFAULT_VIEWPORT_WIDTH,
1619
+ viewportHeight: DEFAULT_VIEWPORT_HEIGHT
1620
+ });
1621
+ await _pool.init();
1622
+ }
1623
+ return _pool;
1624
+ }
1625
+ async function shutdownPool() {
1626
+ if (_pool !== null) {
1627
+ await _pool.close();
1628
+ _pool = null;
1629
+ }
1630
+ }
1631
+ function mapNodeType(node) {
1632
+ if (node.type === "forward_ref") return "forwardRef";
1633
+ if (node.type === "host") return "host";
1634
+ const name = node.name;
1635
+ if (name.endsWith(".Provider") || name === "Provider") return "context.provider";
1636
+ if (name.endsWith(".Consumer") || name === "Consumer") return "context.consumer";
1637
+ return node.type;
1638
+ }
1639
+ function flattenSerializedValue(sv) {
1640
+ if (sv === null || sv === void 0) return null;
1641
+ const v = sv;
1642
+ switch (v.type) {
1643
+ case "null":
1644
+ case "undefined":
1645
+ return null;
1646
+ case "string":
1647
+ case "number":
1648
+ case "boolean":
1649
+ return v.value;
1650
+ case "object": {
1651
+ if (!Array.isArray(v.entries)) return {};
1652
+ const result = {};
1653
+ for (const entry of v.entries) {
1654
+ result[entry.key] = flattenSerializedValue(entry.value);
1655
+ }
1656
+ return result;
1657
+ }
1658
+ case "array": {
1659
+ if (!Array.isArray(v.items)) return [];
1660
+ return v.items.map(flattenSerializedValue);
1661
+ }
1662
+ case "function":
1663
+ return "[Function]";
1664
+ case "symbol":
1665
+ return `[Symbol: ${v.description ?? ""}]`;
1666
+ case "circular":
1667
+ return "[Circular]";
1668
+ case "truncated":
1669
+ return `[Truncated: ${v.preview ?? ""}]`;
1670
+ default:
1671
+ return v.preview ?? null;
1672
+ }
1673
+ }
1674
+ function flattenHookState(hooks) {
1675
+ const result = {};
1676
+ for (let i = 0; i < hooks.length; i++) {
1677
+ const hook = hooks[i];
1678
+ if (hook === void 0) continue;
1679
+ const key = hook.name !== null && hook.name !== void 0 ? hook.name : `${hook.type}[${i}]`;
1680
+ result[key] = flattenSerializedValue(hook.value);
1681
+ }
1682
+ return result;
1683
+ }
1684
+ function extractContextNames(contexts) {
1685
+ const names = contexts.map((c) => c.contextName ?? "Unknown").filter((name, idx, arr) => arr.indexOf(name) === idx);
1686
+ return names;
1687
+ }
1688
+ function anyContextChanged(contexts) {
1689
+ return contexts.some((c) => c.didTriggerRender);
1690
+ }
1691
+ function convertToInstrumentNode(node, depth = 0) {
1692
+ const contexts = extractContextNames(node.context);
1693
+ const contextChanged = anyContextChanged(node.context);
1694
+ const state = flattenHookState(node.state);
1695
+ const propsFlat = flattenSerializedValue(node.props);
1696
+ const props = propsFlat !== null && typeof propsFlat === "object" && !Array.isArray(propsFlat) ? propsFlat : {};
1697
+ return {
1698
+ component: node.name,
1699
+ type: mapNodeType(node),
1700
+ renderCount: node.renderCount,
1701
+ lastRenderDuration: node.renderDuration,
1702
+ memoized: node.type === "memo",
1703
+ // memoSkipped requires tracking bail-outs across commits — not available from
1704
+ // a single-shot capture. Defaulted to 0.
1705
+ memoSkipped: 0,
1706
+ props,
1707
+ // propsChanged is not tracked in a single-shot capture — would need a diff
1708
+ // between two renders. Defaulted to false.
1709
+ propsChanged: false,
1710
+ state,
1711
+ stateChanged: false,
1712
+ contextChanged,
1713
+ contexts,
1714
+ depth,
1715
+ children: node.children.map((child) => convertToInstrumentNode(child, depth + 1))
1716
+ };
1717
+ }
1718
+ function filterByContext(node, contextName) {
1719
+ const filteredChildren = node.children.map((child) => filterByContext(child, contextName)).filter((c) => c !== null);
1720
+ const selfMatches = node.contexts.some((c) => c.toLowerCase() === contextName.toLowerCase());
1721
+ if (!selfMatches && filteredChildren.length === 0) return null;
1722
+ return { ...node, children: filteredChildren };
1723
+ }
1724
+ function filterWastedRenders(node) {
1725
+ const filteredChildren = node.children.map((child) => filterWastedRenders(child)).filter((c) => c !== null);
1726
+ const isWasted = !node.propsChanged && !node.stateChanged && !node.contextChanged && !node.memoized && node.renderCount > 1;
1727
+ if (!isWasted && filteredChildren.length === 0) return null;
1728
+ return { ...node, children: filteredChildren };
1729
+ }
1730
+ function sortTree(node, sortBy) {
1731
+ const sortedChildren = node.children.map((child) => sortTree(child, sortBy)).sort((a, b) => {
1732
+ if (sortBy === "renderCount") return b.renderCount - a.renderCount;
1733
+ return a.depth - b.depth;
1734
+ });
1735
+ return { ...node, children: sortedChildren };
1736
+ }
1737
+ function annotateProviderDepth(node, providerDepth = 0) {
1738
+ const isProvider = node.type === "context.provider";
1739
+ const childProviderDepth = isProvider ? providerDepth + 1 : providerDepth;
1740
+ return {
1741
+ ...node,
1742
+ _providerDepth: providerDepth,
1743
+ children: node.children.map((child) => annotateProviderDepth(child, childProviderDepth))
1744
+ };
1745
+ }
1746
+ function limitNodes(root, limit) {
1747
+ let remaining = limit;
1748
+ const clip = (node) => {
1749
+ if (remaining <= 0) return null;
1750
+ remaining--;
1751
+ const clippedChildren = [];
1752
+ for (const child of node.children) {
1753
+ const clipped = clip(child);
1754
+ if (clipped !== null) clippedChildren.push(clipped);
1755
+ }
1756
+ return { ...node, children: clippedChildren };
1757
+ };
1758
+ return clip(root) ?? root;
1759
+ }
1760
+ var BRANCH = "\u251C\u2500\u2500 ";
1761
+ var LAST_BRANCH = "\u2514\u2500\u2500 ";
1762
+ var VERTICAL = "\u2502 ";
1763
+ var EMPTY = " ";
1764
+ function buildTTYLabel(node, showProviderDepth) {
1765
+ const parts = [node.component];
1766
+ switch (node.type) {
1767
+ case "memo":
1768
+ parts.push("[memo]");
1769
+ break;
1770
+ case "forwardRef":
1771
+ parts.push("[forwardRef]");
1772
+ break;
1773
+ case "class":
1774
+ parts.push("[class]");
1775
+ break;
1776
+ case "context.provider":
1777
+ parts.push("[provider]");
1778
+ break;
1779
+ case "context.consumer":
1780
+ parts.push("[consumer]");
1781
+ break;
1782
+ default:
1783
+ break;
1784
+ }
1785
+ if (node.renderCount > 0) {
1786
+ const durationStr = node.lastRenderDuration > 0 ? ` ${node.lastRenderDuration.toFixed(2)}ms` : "";
1787
+ parts.push(`(renders:${node.renderCount}${durationStr})`);
1788
+ }
1789
+ if (node.contexts.length > 0) {
1790
+ parts.push(`[ctx:${node.contexts.join(",")}]`);
1791
+ }
1792
+ if (showProviderDepth) {
1793
+ const pd = node._providerDepth;
1794
+ if (pd !== void 0 && pd > 0) {
1795
+ parts.push(`[pd:${pd}]`);
1796
+ }
1797
+ }
1798
+ return parts.join(" ");
922
1799
  }
923
- function formatSummaryText(results, outputDir) {
924
- const total = results.length;
925
- const passed = results.filter((r) => r.success).length;
926
- const failed = total - passed;
927
- const successTimes = results.filter((r) => r.success).map((r) => r.renderTimeMs);
928
- const avgMs = successTimes.length > 0 ? successTimes.reduce((a, b) => a + b, 0) / successTimes.length : 0;
929
- const lines = [
930
- "\u2500".repeat(60),
931
- `Render Summary`,
932
- "\u2500".repeat(60),
933
- ` Total components : ${total}`,
934
- ` Passed : ${passed}`,
935
- ` Failed : ${failed}`,
936
- ` Avg render time : ${avgMs.toFixed(1)}ms`,
937
- ` Output dir : ${outputDir}`
938
- ];
939
- if (failed > 0) {
940
- lines.push("", " Failed components:");
941
- for (const r of results) {
942
- if (!r.success) {
943
- lines.push(` \u2717 ${r.name}: ${r.errorMessage ?? "unknown error"}`);
1800
+ function renderTTYNode(node, prefix, isLast, showProviderDepth, lines) {
1801
+ if (node.type === "host") {
1802
+ for (let i = 0; i < node.children.length; i++) {
1803
+ const child = node.children[i];
1804
+ if (child !== void 0) {
1805
+ renderTTYNode(child, prefix, i === node.children.length - 1, showProviderDepth, lines);
1806
+ }
1807
+ }
1808
+ return;
1809
+ }
1810
+ const connector = isLast ? LAST_BRANCH : BRANCH;
1811
+ lines.push(`${prefix}${connector}${buildTTYLabel(node, showProviderDepth)}`);
1812
+ const nextPrefix = prefix + (isLast ? EMPTY : VERTICAL);
1813
+ for (let i = 0; i < node.children.length; i++) {
1814
+ const child = node.children[i];
1815
+ if (child !== void 0) {
1816
+ renderTTYNode(child, nextPrefix, i === node.children.length - 1, showProviderDepth, lines);
1817
+ }
1818
+ }
1819
+ }
1820
+ function formatInstrumentTree(root, showProviderDepth = false) {
1821
+ const lines = [];
1822
+ if (root.type !== "host") {
1823
+ lines.push(buildTTYLabel(root, showProviderDepth));
1824
+ for (let i = 0; i < root.children.length; i++) {
1825
+ const child = root.children[i];
1826
+ if (child !== void 0) {
1827
+ renderTTYNode(child, "", i === root.children.length - 1, showProviderDepth, lines);
1828
+ }
1829
+ }
1830
+ } else {
1831
+ for (let i = 0; i < root.children.length; i++) {
1832
+ const child = root.children[i];
1833
+ if (child !== void 0) {
1834
+ renderTTYNode(child, "", i === root.children.length - 1, showProviderDepth, lines);
944
1835
  }
945
1836
  }
946
1837
  }
947
- lines.push("\u2500".repeat(60));
948
1838
  return lines.join("\n");
949
1839
  }
950
- function escapeHtml(str) {
951
- return str.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#039;");
1840
+ async function runInstrumentTree(options) {
1841
+ const { componentName, filePath } = options;
1842
+ const pool = await getPool();
1843
+ const slot = await pool.acquire();
1844
+ const { page } = slot;
1845
+ try {
1846
+ await page.addInitScript({ content: getBrowserEntryScript2() });
1847
+ const htmlHarness = await buildComponentHarness(
1848
+ filePath,
1849
+ componentName,
1850
+ {},
1851
+ DEFAULT_VIEWPORT_WIDTH
1852
+ );
1853
+ await page.setContent(htmlHarness, { waitUntil: "load" });
1854
+ await page.waitForFunction(
1855
+ () => {
1856
+ const w = window;
1857
+ return w.__SCOPE_RENDER_COMPLETE__ === true;
1858
+ },
1859
+ { timeout: 15e3 }
1860
+ );
1861
+ const renderError = await page.evaluate(
1862
+ () => window.__SCOPE_RENDER_ERROR__ ?? null
1863
+ );
1864
+ if (renderError !== null) {
1865
+ throw new Error(`Component render error: ${renderError}`);
1866
+ }
1867
+ const captureJson = await page.evaluate(async () => {
1868
+ const w = window;
1869
+ if (typeof w.__SCOPE_CAPTURE_JSON__ !== "function") {
1870
+ throw new Error("__SCOPE_CAPTURE_JSON__ not available \u2014 Scope runtime not injected");
1871
+ }
1872
+ return w.__SCOPE_CAPTURE_JSON__({ lightweight: false });
1873
+ });
1874
+ const captureResult = JSON.parse(captureJson);
1875
+ const componentTree = captureResult.tree;
1876
+ if (componentTree === void 0 || componentTree === null) {
1877
+ throw new Error(`No component tree found for "${componentName}"`);
1878
+ }
1879
+ let instrumentRoot = convertToInstrumentNode(componentTree, 0);
1880
+ if (options.usesContext !== void 0) {
1881
+ const filtered = filterByContext(instrumentRoot, options.usesContext);
1882
+ instrumentRoot = filtered !== null ? filtered : { ...instrumentRoot, children: [] };
1883
+ }
1884
+ if (options.wastedRenders === true) {
1885
+ const filtered = filterWastedRenders(instrumentRoot);
1886
+ instrumentRoot = filtered !== null ? filtered : { ...instrumentRoot, children: [] };
1887
+ }
1888
+ if (options.sortBy !== void 0) {
1889
+ instrumentRoot = sortTree(instrumentRoot, options.sortBy);
1890
+ }
1891
+ if (options.providerDepth === true) {
1892
+ instrumentRoot = annotateProviderDepth(instrumentRoot, 0);
1893
+ }
1894
+ if (options.limit !== void 0 && options.limit > 0) {
1895
+ instrumentRoot = limitNodes(instrumentRoot, options.limit);
1896
+ }
1897
+ return instrumentRoot;
1898
+ } finally {
1899
+ pool.release(slot);
1900
+ }
952
1901
  }
953
- function csvEscape(value) {
954
- if (value.includes(",") || value.includes('"') || value.includes("\n")) {
955
- return `"${value.replace(/"/g, '""')}"`;
1902
+ function createInstrumentTreeCommand() {
1903
+ return new Command3("tree").description("Render a component via BrowserPool and output a structured instrumentation tree").argument("<component>", "Component name to instrument (must exist in the manifest)").option("--sort-by <field>", "Sort nodes by field: renderCount | depth").option("--limit <n>", "Limit output to the first N nodes (depth-first)").option("--uses-context <name>", "Filter to components that use a specific context").option("--provider-depth", "Annotate each node with its context-provider nesting depth", false).option(
1904
+ "--wasted-renders",
1905
+ "Filter to components with wasted renders (no prop/state/context changes, not memoized)",
1906
+ false
1907
+ ).option("--format <fmt>", "Output format: json | tree (default: auto)").option("--manifest <path>", "Path to manifest.json", MANIFEST_PATH4).action(async (componentName, opts) => {
1908
+ try {
1909
+ const manifest = loadManifest(opts.manifest);
1910
+ const descriptor = manifest.components[componentName];
1911
+ if (descriptor === void 0) {
1912
+ const available = Object.keys(manifest.components).slice(0, 5).join(", ");
1913
+ throw new Error(
1914
+ `Component "${componentName}" not found in manifest.
1915
+ Available: ${available}`
1916
+ );
1917
+ }
1918
+ if (opts.sortBy !== void 0) {
1919
+ const allowed = ["renderCount", "depth"];
1920
+ if (!allowed.includes(opts.sortBy)) {
1921
+ throw new Error(
1922
+ `Unknown --sort-by value "${opts.sortBy}". Allowed: ${allowed.join(", ")}`
1923
+ );
1924
+ }
1925
+ }
1926
+ const rootDir = process.cwd();
1927
+ const filePath = resolve4(rootDir, descriptor.filePath);
1928
+ process.stderr.write(`Instrumenting ${componentName}\u2026
1929
+ `);
1930
+ const instrumentRoot = await runInstrumentTree({
1931
+ componentName,
1932
+ filePath,
1933
+ sortBy: opts.sortBy,
1934
+ limit: opts.limit !== void 0 ? Math.max(1, parseInt(opts.limit, 10)) : void 0,
1935
+ usesContext: opts.usesContext,
1936
+ providerDepth: opts.providerDepth,
1937
+ wastedRenders: opts.wastedRenders
1938
+ });
1939
+ await shutdownPool();
1940
+ const fmt = resolveFormat2(opts.format);
1941
+ if (fmt === "json") {
1942
+ process.stdout.write(`${JSON.stringify(instrumentRoot, null, 2)}
1943
+ `);
1944
+ } else {
1945
+ const tree = formatInstrumentTree(instrumentRoot, opts.providerDepth ?? false);
1946
+ process.stdout.write(`${tree}
1947
+ `);
1948
+ }
1949
+ } catch (err) {
1950
+ await shutdownPool();
1951
+ process.stderr.write(`Error: ${err instanceof Error ? err.message : String(err)}
1952
+ `);
1953
+ process.exit(1);
1954
+ }
1955
+ });
1956
+ }
1957
+ function resolveFormat2(formatFlag) {
1958
+ if (formatFlag !== void 0) {
1959
+ const lower = formatFlag.toLowerCase();
1960
+ if (lower !== "json" && lower !== "tree") {
1961
+ throw new Error(`Unknown format "${formatFlag}". Allowed: json, tree`);
1962
+ }
1963
+ return lower;
956
1964
  }
957
- return value;
1965
+ return isTTY() ? "tree" : "json";
958
1966
  }
959
1967
 
960
1968
  // src/instrument/renders.ts
961
- var MANIFEST_PATH2 = ".reactscope/manifest.json";
1969
+ var MANIFEST_PATH5 = ".reactscope/manifest.json";
962
1970
  function determineTrigger(event) {
963
1971
  if (event.forceUpdate) return "force_update";
964
1972
  if (event.stateChanged) return "state_change";
@@ -1203,7 +2211,7 @@ function buildInstrumentationScript() {
1203
2211
  `
1204
2212
  );
1205
2213
  }
1206
- async function replayInteraction(page, steps) {
2214
+ async function replayInteraction2(page, steps) {
1207
2215
  for (const step of steps) {
1208
2216
  switch (step.action) {
1209
2217
  case "click":
@@ -1259,26 +2267,26 @@ async function replayInteraction(page, steps) {
1259
2267
  }
1260
2268
  }
1261
2269
  }
1262
- var _pool = null;
1263
- async function getPool() {
1264
- if (_pool === null) {
1265
- _pool = new BrowserPool({
2270
+ var _pool2 = null;
2271
+ async function getPool2() {
2272
+ if (_pool2 === null) {
2273
+ _pool2 = new BrowserPool2({
1266
2274
  size: { browsers: 1, pagesPerBrowser: 2 },
1267
2275
  viewportWidth: 1280,
1268
2276
  viewportHeight: 800
1269
2277
  });
1270
- await _pool.init();
2278
+ await _pool2.init();
1271
2279
  }
1272
- return _pool;
2280
+ return _pool2;
1273
2281
  }
1274
- async function shutdownPool() {
1275
- if (_pool !== null) {
1276
- await _pool.close();
1277
- _pool = null;
2282
+ async function shutdownPool2() {
2283
+ if (_pool2 !== null) {
2284
+ await _pool2.close();
2285
+ _pool2 = null;
1278
2286
  }
1279
2287
  }
1280
2288
  async function analyzeRenders(options) {
1281
- const manifestPath = options.manifestPath ?? MANIFEST_PATH2;
2289
+ const manifestPath = options.manifestPath ?? MANIFEST_PATH5;
1282
2290
  const manifest = loadManifest(manifestPath);
1283
2291
  const descriptor = manifest.components[options.componentName];
1284
2292
  if (descriptor === void 0) {
@@ -1289,9 +2297,9 @@ Available: ${available}`
1289
2297
  );
1290
2298
  }
1291
2299
  const rootDir = process.cwd();
1292
- const filePath = resolve2(rootDir, descriptor.filePath);
2300
+ const filePath = resolve5(rootDir, descriptor.filePath);
1293
2301
  const htmlHarness = await buildComponentHarness(filePath, options.componentName, {}, 1280);
1294
- const pool = await getPool();
2302
+ const pool = await getPool2();
1295
2303
  const slot = await pool.acquire();
1296
2304
  const { page } = slot;
1297
2305
  const startMs = performance.now();
@@ -1307,7 +2315,7 @@ Available: ${available}`
1307
2315
  window.__SCOPE_RENDER_EVENTS__ = [];
1308
2316
  window.__SCOPE_RENDER_INDEX__ = 0;
1309
2317
  });
1310
- await replayInteraction(page, options.interaction);
2318
+ await replayInteraction2(page, options.interaction);
1311
2319
  await page.waitForTimeout(200);
1312
2320
  const interactionDurationMs = performance.now() - startMs;
1313
2321
  const rawEvents = await page.evaluate(() => {
@@ -1372,11 +2380,11 @@ function formatRendersTable(result) {
1372
2380
  return lines.join("\n");
1373
2381
  }
1374
2382
  function createInstrumentRendersCommand() {
1375
- return new Command3("renders").description("Trace re-render causality chains for a component during an interaction sequence").argument("<component>", "Component name to instrument (must be in manifest)").option(
2383
+ return new Command4("renders").description("Trace re-render causality chains for a component during an interaction sequence").argument("<component>", "Component name to instrument (must be in manifest)").option(
1376
2384
  "--interaction <json>",
1377
2385
  `Interaction sequence JSON, e.g. '[{"action":"click","target":"button"}]'`,
1378
2386
  "[]"
1379
- ).option("--json", "Output as JSON regardless of TTY", false).option("--manifest <path>", "Path to manifest.json", MANIFEST_PATH2).action(
2387
+ ).option("--json", "Output as JSON regardless of TTY", false).option("--manifest <path>", "Path to manifest.json", MANIFEST_PATH5).action(
1380
2388
  async (componentName, opts) => {
1381
2389
  let interaction = [];
1382
2390
  try {
@@ -1399,7 +2407,7 @@ function createInstrumentRendersCommand() {
1399
2407
  interaction,
1400
2408
  manifestPath: opts.manifest
1401
2409
  });
1402
- await shutdownPool();
2410
+ await shutdownPool2();
1403
2411
  if (opts.json || !isTTY()) {
1404
2412
  process.stdout.write(`${JSON.stringify(result, null, 2)}
1405
2413
  `);
@@ -1408,7 +2416,7 @@ function createInstrumentRendersCommand() {
1408
2416
  `);
1409
2417
  }
1410
2418
  } catch (err) {
1411
- await shutdownPool();
2419
+ await shutdownPool2();
1412
2420
  process.stderr.write(`Error: ${err instanceof Error ? err.message : String(err)}
1413
2421
  `);
1414
2422
  process.exit(1);
@@ -1417,32 +2425,35 @@ function createInstrumentRendersCommand() {
1417
2425
  );
1418
2426
  }
1419
2427
  function createInstrumentCommand() {
1420
- const instrumentCmd = new Command3("instrument").description(
2428
+ const instrumentCmd = new Command4("instrument").description(
1421
2429
  "Structured instrumentation commands for React component analysis"
1422
2430
  );
1423
2431
  instrumentCmd.addCommand(createInstrumentRendersCommand());
2432
+ instrumentCmd.addCommand(createInstrumentHooksCommand());
2433
+ instrumentCmd.addCommand(createInstrumentProfileCommand());
2434
+ instrumentCmd.addCommand(createInstrumentTreeCommand());
1424
2435
  return instrumentCmd;
1425
2436
  }
1426
2437
 
1427
2438
  // src/render-commands.ts
1428
2439
  import { mkdirSync as mkdirSync3, writeFileSync as writeFileSync4 } from "fs";
1429
- import { resolve as resolve4 } from "path";
2440
+ import { resolve as resolve7 } from "path";
1430
2441
  import {
1431
2442
  ALL_CONTEXT_IDS,
1432
2443
  ALL_STRESS_IDS,
1433
- BrowserPool as BrowserPool2,
2444
+ BrowserPool as BrowserPool3,
1434
2445
  contextAxis,
1435
2446
  RenderMatrix,
1436
2447
  SatoriRenderer,
1437
2448
  safeRender,
1438
2449
  stressAxis
1439
2450
  } from "@agent-scope/render";
1440
- import { Command as Command4 } from "commander";
2451
+ import { Command as Command5 } from "commander";
1441
2452
 
1442
2453
  // src/tailwind-css.ts
1443
2454
  import { existsSync as existsSync4, readFileSync as readFileSync4 } from "fs";
1444
2455
  import { createRequire } from "module";
1445
- import { resolve as resolve3 } from "path";
2456
+ import { resolve as resolve6 } from "path";
1446
2457
  var CONFIG_FILENAMES = [
1447
2458
  ".reactscope/config.json",
1448
2459
  ".reactscope/config.js",
@@ -1459,14 +2470,14 @@ var STYLE_ENTRY_CANDIDATES = [
1459
2470
  var TAILWIND_IMPORT = /@import\s+["']tailwindcss["']\s*;?/;
1460
2471
  var compilerCache = null;
1461
2472
  function getCachedBuild(cwd) {
1462
- if (compilerCache !== null && resolve3(compilerCache.cwd) === resolve3(cwd)) {
2473
+ if (compilerCache !== null && resolve6(compilerCache.cwd) === resolve6(cwd)) {
1463
2474
  return compilerCache.build;
1464
2475
  }
1465
2476
  return null;
1466
2477
  }
1467
2478
  function findStylesEntry(cwd) {
1468
2479
  for (const name of CONFIG_FILENAMES) {
1469
- const p = resolve3(cwd, name);
2480
+ const p = resolve6(cwd, name);
1470
2481
  if (!existsSync4(p)) continue;
1471
2482
  try {
1472
2483
  if (name.endsWith(".json")) {
@@ -1475,28 +2486,28 @@ function findStylesEntry(cwd) {
1475
2486
  const scope = data.scope;
1476
2487
  const entry = scope?.stylesEntry ?? data.stylesEntry;
1477
2488
  if (typeof entry === "string") {
1478
- const full = resolve3(cwd, entry);
2489
+ const full = resolve6(cwd, entry);
1479
2490
  if (existsSync4(full)) return full;
1480
2491
  }
1481
2492
  }
1482
2493
  } catch {
1483
2494
  }
1484
2495
  }
1485
- const pkgPath = resolve3(cwd, "package.json");
2496
+ const pkgPath = resolve6(cwd, "package.json");
1486
2497
  if (existsSync4(pkgPath)) {
1487
2498
  try {
1488
2499
  const raw = readFileSync4(pkgPath, "utf-8");
1489
2500
  const pkg = JSON.parse(raw);
1490
2501
  const entry = pkg.scope?.stylesEntry;
1491
2502
  if (typeof entry === "string") {
1492
- const full = resolve3(cwd, entry);
2503
+ const full = resolve6(cwd, entry);
1493
2504
  if (existsSync4(full)) return full;
1494
2505
  }
1495
2506
  } catch {
1496
2507
  }
1497
2508
  }
1498
2509
  for (const candidate of STYLE_ENTRY_CANDIDATES) {
1499
- const full = resolve3(cwd, candidate);
2510
+ const full = resolve6(cwd, candidate);
1500
2511
  if (existsSync4(full)) {
1501
2512
  try {
1502
2513
  const content = readFileSync4(full, "utf-8");
@@ -1514,7 +2525,7 @@ async function getTailwindCompiler(cwd) {
1514
2525
  if (entryPath === null) return null;
1515
2526
  let compile;
1516
2527
  try {
1517
- const require2 = createRequire(resolve3(cwd, "package.json"));
2528
+ const require2 = createRequire(resolve6(cwd, "package.json"));
1518
2529
  const tailwind = require2("tailwindcss");
1519
2530
  const fn = tailwind.compile;
1520
2531
  if (typeof fn !== "function") return null;
@@ -1525,8 +2536,8 @@ async function getTailwindCompiler(cwd) {
1525
2536
  const entryContent = readFileSync4(entryPath, "utf-8");
1526
2537
  const loadStylesheet = async (id, base) => {
1527
2538
  if (id === "tailwindcss") {
1528
- const nodeModules = resolve3(cwd, "node_modules");
1529
- const tailwindCssPath = resolve3(nodeModules, "tailwindcss", "index.css");
2539
+ const nodeModules = resolve6(cwd, "node_modules");
2540
+ const tailwindCssPath = resolve6(nodeModules, "tailwindcss", "index.css");
1530
2541
  if (!existsSync4(tailwindCssPath)) {
1531
2542
  throw new Error(
1532
2543
  `Tailwind v4: tailwindcss package not found at ${tailwindCssPath}. Install with: npm install tailwindcss`
@@ -1535,10 +2546,10 @@ async function getTailwindCompiler(cwd) {
1535
2546
  const content = readFileSync4(tailwindCssPath, "utf-8");
1536
2547
  return { path: "virtual:tailwindcss/index.css", base, content };
1537
2548
  }
1538
- const full = resolve3(base, id);
2549
+ const full = resolve6(base, id);
1539
2550
  if (existsSync4(full)) {
1540
2551
  const content = readFileSync4(full, "utf-8");
1541
- return { path: full, base: resolve3(full, ".."), content };
2552
+ return { path: full, base: resolve6(full, ".."), content };
1542
2553
  }
1543
2554
  throw new Error(`Tailwind v4: could not load stylesheet: ${id} (base: ${base})`);
1544
2555
  };
@@ -1560,24 +2571,24 @@ async function getCompiledCssForClasses(cwd, classes) {
1560
2571
  }
1561
2572
 
1562
2573
  // src/render-commands.ts
1563
- var MANIFEST_PATH3 = ".reactscope/manifest.json";
2574
+ var MANIFEST_PATH6 = ".reactscope/manifest.json";
1564
2575
  var DEFAULT_OUTPUT_DIR = ".reactscope/renders";
1565
- var _pool2 = null;
1566
- async function getPool2(viewportWidth, viewportHeight) {
1567
- if (_pool2 === null) {
1568
- _pool2 = new BrowserPool2({
2576
+ var _pool3 = null;
2577
+ async function getPool3(viewportWidth, viewportHeight) {
2578
+ if (_pool3 === null) {
2579
+ _pool3 = new BrowserPool3({
1569
2580
  size: { browsers: 1, pagesPerBrowser: 4 },
1570
2581
  viewportWidth,
1571
2582
  viewportHeight
1572
2583
  });
1573
- await _pool2.init();
2584
+ await _pool3.init();
1574
2585
  }
1575
- return _pool2;
2586
+ return _pool3;
1576
2587
  }
1577
- async function shutdownPool2() {
1578
- if (_pool2 !== null) {
1579
- await _pool2.close();
1580
- _pool2 = null;
2588
+ async function shutdownPool3() {
2589
+ if (_pool3 !== null) {
2590
+ await _pool3.close();
2591
+ _pool3 = null;
1581
2592
  }
1582
2593
  }
1583
2594
  function buildRenderer(filePath, componentName, viewportWidth, viewportHeight) {
@@ -1588,7 +2599,7 @@ function buildRenderer(filePath, componentName, viewportWidth, viewportHeight) {
1588
2599
  _satori: satori,
1589
2600
  async renderCell(props, _complexityClass) {
1590
2601
  const startMs = performance.now();
1591
- const pool = await getPool2(viewportWidth, viewportHeight);
2602
+ const pool = await getPool3(viewportWidth, viewportHeight);
1592
2603
  const htmlHarness = await buildComponentHarness(
1593
2604
  filePath,
1594
2605
  componentName,
@@ -1685,7 +2696,7 @@ function buildRenderer(filePath, componentName, viewportWidth, viewportHeight) {
1685
2696
  };
1686
2697
  }
1687
2698
  function registerRenderSingle(renderCmd) {
1688
- renderCmd.command("component <component>", { isDefault: true }).description("Render a single component to PNG or JSON").option("--props <json>", `Inline props JSON, e.g. '{"variant":"primary"}'`).option("--viewport <WxH>", "Viewport size e.g. 1280x720", "375x812").option("--theme <name>", "Theme name from the token system").option("-o, --output <path>", "Write PNG to file instead of stdout").option("--format <fmt>", "Output format: png or json (default: auto)").option("--manifest <path>", "Path to manifest.json", MANIFEST_PATH3).action(
2699
+ renderCmd.command("component <component>", { isDefault: true }).description("Render a single component to PNG or JSON").option("--props <json>", `Inline props JSON, e.g. '{"variant":"primary"}'`).option("--viewport <WxH>", "Viewport size e.g. 1280x720", "375x812").option("--theme <name>", "Theme name from the token system").option("-o, --output <path>", "Write PNG to file instead of stdout").option("--format <fmt>", "Output format: png or json (default: auto)").option("--manifest <path>", "Path to manifest.json", MANIFEST_PATH6).action(
1689
2700
  async (componentName, opts) => {
1690
2701
  try {
1691
2702
  const manifest = loadManifest(opts.manifest);
@@ -1707,7 +2718,7 @@ Available: ${available}`
1707
2718
  }
1708
2719
  const { width, height } = parseViewport(opts.viewport);
1709
2720
  const rootDir = process.cwd();
1710
- const filePath = resolve4(rootDir, descriptor.filePath);
2721
+ const filePath = resolve7(rootDir, descriptor.filePath);
1711
2722
  const renderer = buildRenderer(filePath, componentName, width, height);
1712
2723
  process.stderr.write(
1713
2724
  `Rendering ${componentName} [${descriptor.complexityClass}] at ${width}\xD7${height}\u2026
@@ -1724,7 +2735,7 @@ Available: ${available}`
1724
2735
  }
1725
2736
  }
1726
2737
  );
1727
- await shutdownPool2();
2738
+ await shutdownPool3();
1728
2739
  if (outcome.crashed) {
1729
2740
  process.stderr.write(`\u2717 Render failed: ${outcome.error.message}
1730
2741
  `);
@@ -1737,7 +2748,7 @@ Available: ${available}`
1737
2748
  }
1738
2749
  const result = outcome.result;
1739
2750
  if (opts.output !== void 0) {
1740
- const outPath = resolve4(process.cwd(), opts.output);
2751
+ const outPath = resolve7(process.cwd(), opts.output);
1741
2752
  writeFileSync4(outPath, result.screenshot);
1742
2753
  process.stdout.write(
1743
2754
  `\u2713 ${componentName} \u2192 ${opts.output} (${result.width}\xD7${result.height}, ${result.renderTimeMs.toFixed(0)}ms)
@@ -1751,9 +2762,9 @@ Available: ${available}`
1751
2762
  process.stdout.write(`${JSON.stringify(json, null, 2)}
1752
2763
  `);
1753
2764
  } else if (fmt === "file") {
1754
- const dir = resolve4(process.cwd(), DEFAULT_OUTPUT_DIR);
2765
+ const dir = resolve7(process.cwd(), DEFAULT_OUTPUT_DIR);
1755
2766
  mkdirSync3(dir, { recursive: true });
1756
- const outPath = resolve4(dir, `${componentName}.png`);
2767
+ const outPath = resolve7(dir, `${componentName}.png`);
1757
2768
  writeFileSync4(outPath, result.screenshot);
1758
2769
  const relPath = `${DEFAULT_OUTPUT_DIR}/${componentName}.png`;
1759
2770
  process.stdout.write(
@@ -1761,9 +2772,9 @@ Available: ${available}`
1761
2772
  `
1762
2773
  );
1763
2774
  } else {
1764
- const dir = resolve4(process.cwd(), DEFAULT_OUTPUT_DIR);
2775
+ const dir = resolve7(process.cwd(), DEFAULT_OUTPUT_DIR);
1765
2776
  mkdirSync3(dir, { recursive: true });
1766
- const outPath = resolve4(dir, `${componentName}.png`);
2777
+ const outPath = resolve7(dir, `${componentName}.png`);
1767
2778
  writeFileSync4(outPath, result.screenshot);
1768
2779
  const relPath = `${DEFAULT_OUTPUT_DIR}/${componentName}.png`;
1769
2780
  process.stdout.write(
@@ -1772,7 +2783,7 @@ Available: ${available}`
1772
2783
  );
1773
2784
  }
1774
2785
  } catch (err) {
1775
- await shutdownPool2();
2786
+ await shutdownPool3();
1776
2787
  process.stderr.write(`Error: ${err instanceof Error ? err.message : String(err)}
1777
2788
  `);
1778
2789
  process.exit(1);
@@ -1784,7 +2795,7 @@ function registerRenderMatrix(renderCmd) {
1784
2795
  renderCmd.command("matrix <component>").description("Render a component across a matrix of prop axes").option("--axes <spec>", "Axis definitions e.g. 'variant:primary,secondary size:sm,md,lg'").option(
1785
2796
  "--contexts <ids>",
1786
2797
  "Composition context IDs, comma-separated (e.g. centered,rtl,sidebar)"
1787
- ).option("--stress <ids>", "Stress preset IDs, comma-separated (e.g. text.long,text.unicode)").option("--sprite <path>", "Write sprite sheet PNG to file").option("--format <fmt>", "Output format: json|png|html|csv (default: auto)").option("--concurrency <n>", "Max parallel renders", "8").option("--manifest <path>", "Path to manifest.json", MANIFEST_PATH3).action(
2798
+ ).option("--stress <ids>", "Stress preset IDs, comma-separated (e.g. text.long,text.unicode)").option("--sprite <path>", "Write sprite sheet PNG to file").option("--format <fmt>", "Output format: json|png|html|csv (default: auto)").option("--concurrency <n>", "Max parallel renders", "8").option("--manifest <path>", "Path to manifest.json", MANIFEST_PATH6).action(
1788
2799
  async (componentName, opts) => {
1789
2800
  try {
1790
2801
  const manifest = loadManifest(opts.manifest);
@@ -1799,7 +2810,7 @@ Available: ${available}`
1799
2810
  const concurrency = Math.max(1, parseInt(opts.concurrency, 10) || 8);
1800
2811
  const { width, height } = { width: 375, height: 812 };
1801
2812
  const rootDir = process.cwd();
1802
- const filePath = resolve4(rootDir, descriptor.filePath);
2813
+ const filePath = resolve7(rootDir, descriptor.filePath);
1803
2814
  const renderer = buildRenderer(filePath, componentName, width, height);
1804
2815
  const axes = [];
1805
2816
  if (opts.axes !== void 0) {
@@ -1857,7 +2868,7 @@ Available: ${available}`
1857
2868
  concurrency
1858
2869
  });
1859
2870
  const result = await matrix.render();
1860
- await shutdownPool2();
2871
+ await shutdownPool3();
1861
2872
  process.stderr.write(
1862
2873
  `Done. ${result.stats.totalCells} cells, avg ${result.stats.avgRenderTimeMs.toFixed(1)}ms
1863
2874
  `
@@ -1866,7 +2877,7 @@ Available: ${available}`
1866
2877
  const { SpriteSheetGenerator } = await import("@agent-scope/render");
1867
2878
  const gen = new SpriteSheetGenerator();
1868
2879
  const sheet = await gen.generate(result);
1869
- const spritePath = resolve4(process.cwd(), opts.sprite);
2880
+ const spritePath = resolve7(process.cwd(), opts.sprite);
1870
2881
  writeFileSync4(spritePath, sheet.png);
1871
2882
  process.stderr.write(`Sprite sheet saved to ${spritePath}
1872
2883
  `);
@@ -1876,9 +2887,9 @@ Available: ${available}`
1876
2887
  const { SpriteSheetGenerator } = await import("@agent-scope/render");
1877
2888
  const gen = new SpriteSheetGenerator();
1878
2889
  const sheet = await gen.generate(result);
1879
- const dir = resolve4(process.cwd(), DEFAULT_OUTPUT_DIR);
2890
+ const dir = resolve7(process.cwd(), DEFAULT_OUTPUT_DIR);
1880
2891
  mkdirSync3(dir, { recursive: true });
1881
- const outPath = resolve4(dir, `${componentName}-matrix.png`);
2892
+ const outPath = resolve7(dir, `${componentName}-matrix.png`);
1882
2893
  writeFileSync4(outPath, sheet.png);
1883
2894
  const relPath = `${DEFAULT_OUTPUT_DIR}/${componentName}-matrix.png`;
1884
2895
  process.stdout.write(
@@ -1902,7 +2913,7 @@ Available: ${available}`
1902
2913
  process.stdout.write(formatMatrixCsv(componentName, result));
1903
2914
  }
1904
2915
  } catch (err) {
1905
- await shutdownPool2();
2916
+ await shutdownPool3();
1906
2917
  process.stderr.write(`Error: ${err instanceof Error ? err.message : String(err)}
1907
2918
  `);
1908
2919
  process.exit(1);
@@ -1911,7 +2922,7 @@ Available: ${available}`
1911
2922
  );
1912
2923
  }
1913
2924
  function registerRenderAll(renderCmd) {
1914
- renderCmd.command("all").description("Render all components from the manifest").option("--concurrency <n>", "Max parallel renders", "4").option("--output-dir <dir>", "Output directory for renders", DEFAULT_OUTPUT_DIR).option("--manifest <path>", "Path to manifest.json", MANIFEST_PATH3).option("--format <fmt>", "Output format: json|png (default: png)", "png").action(
2925
+ renderCmd.command("all").description("Render all components from the manifest").option("--concurrency <n>", "Max parallel renders", "4").option("--output-dir <dir>", "Output directory for renders", DEFAULT_OUTPUT_DIR).option("--manifest <path>", "Path to manifest.json", MANIFEST_PATH6).option("--format <fmt>", "Output format: json|png (default: png)", "png").action(
1915
2926
  async (opts) => {
1916
2927
  try {
1917
2928
  const manifest = loadManifest(opts.manifest);
@@ -1922,7 +2933,7 @@ function registerRenderAll(renderCmd) {
1922
2933
  return;
1923
2934
  }
1924
2935
  const concurrency = Math.max(1, parseInt(opts.concurrency, 10) || 4);
1925
- const outputDir = resolve4(process.cwd(), opts.outputDir);
2936
+ const outputDir = resolve7(process.cwd(), opts.outputDir);
1926
2937
  mkdirSync3(outputDir, { recursive: true });
1927
2938
  const rootDir = process.cwd();
1928
2939
  process.stderr.write(`Rendering ${total} components (concurrency: ${concurrency})\u2026
@@ -1932,7 +2943,7 @@ function registerRenderAll(renderCmd) {
1932
2943
  const renderOne = async (name) => {
1933
2944
  const descriptor = manifest.components[name];
1934
2945
  if (descriptor === void 0) return;
1935
- const filePath = resolve4(rootDir, descriptor.filePath);
2946
+ const filePath = resolve7(rootDir, descriptor.filePath);
1936
2947
  const renderer = buildRenderer(filePath, name, 375, 812);
1937
2948
  const outcome = await safeRender(
1938
2949
  () => renderer.renderCell({}, descriptor.complexityClass),
@@ -1955,7 +2966,7 @@ function registerRenderAll(renderCmd) {
1955
2966
  success: false,
1956
2967
  errorMessage: outcome.error.message
1957
2968
  });
1958
- const errPath = resolve4(outputDir, `${name}.error.json`);
2969
+ const errPath = resolve7(outputDir, `${name}.error.json`);
1959
2970
  writeFileSync4(
1960
2971
  errPath,
1961
2972
  JSON.stringify(
@@ -1973,9 +2984,9 @@ function registerRenderAll(renderCmd) {
1973
2984
  }
1974
2985
  const result = outcome.result;
1975
2986
  results.push({ name, renderTimeMs: result.renderTimeMs, success: true });
1976
- const pngPath = resolve4(outputDir, `${name}.png`);
2987
+ const pngPath = resolve7(outputDir, `${name}.png`);
1977
2988
  writeFileSync4(pngPath, result.screenshot);
1978
- const jsonPath = resolve4(outputDir, `${name}.json`);
2989
+ const jsonPath = resolve7(outputDir, `${name}.json`);
1979
2990
  writeFileSync4(jsonPath, JSON.stringify(formatRenderJson(name, {}, result), null, 2));
1980
2991
  if (isTTY()) {
1981
2992
  process.stdout.write(
@@ -1999,13 +3010,13 @@ function registerRenderAll(renderCmd) {
1999
3010
  workers.push(worker());
2000
3011
  }
2001
3012
  await Promise.all(workers);
2002
- await shutdownPool2();
3013
+ await shutdownPool3();
2003
3014
  process.stderr.write("\n");
2004
3015
  const summary = formatSummaryText(results, outputDir);
2005
3016
  process.stderr.write(`${summary}
2006
3017
  `);
2007
3018
  } catch (err) {
2008
- await shutdownPool2();
3019
+ await shutdownPool3();
2009
3020
  process.stderr.write(`Error: ${err instanceof Error ? err.message : String(err)}
2010
3021
  `);
2011
3022
  process.exit(1);
@@ -2038,7 +3049,7 @@ function resolveMatrixFormat(formatFlag, spriteAlreadyWritten) {
2038
3049
  return "json";
2039
3050
  }
2040
3051
  function createRenderCommand() {
2041
- const renderCmd = new Command4("render").description(
3052
+ const renderCmd = new Command5("render").description(
2042
3053
  "Render components to PNG or JSON via esbuild + BrowserPool"
2043
3054
  );
2044
3055
  registerRenderSingle(renderCmd);
@@ -2049,31 +3060,31 @@ function createRenderCommand() {
2049
3060
 
2050
3061
  // src/report/baseline.ts
2051
3062
  import { existsSync as existsSync5, mkdirSync as mkdirSync4, rmSync, writeFileSync as writeFileSync5 } from "fs";
2052
- import { resolve as resolve5 } from "path";
3063
+ import { resolve as resolve8 } from "path";
2053
3064
  import { generateManifest as generateManifest2 } from "@agent-scope/manifest";
2054
- import { BrowserPool as BrowserPool3, safeRender as safeRender2 } from "@agent-scope/render";
3065
+ import { BrowserPool as BrowserPool4, safeRender as safeRender2 } from "@agent-scope/render";
2055
3066
  import { ComplianceEngine, TokenResolver } from "@agent-scope/tokens";
2056
3067
  var DEFAULT_BASELINE_DIR = ".reactscope/baseline";
2057
- var _pool3 = null;
2058
- async function getPool3(viewportWidth, viewportHeight) {
2059
- if (_pool3 === null) {
2060
- _pool3 = new BrowserPool3({
3068
+ var _pool4 = null;
3069
+ async function getPool4(viewportWidth, viewportHeight) {
3070
+ if (_pool4 === null) {
3071
+ _pool4 = new BrowserPool4({
2061
3072
  size: { browsers: 1, pagesPerBrowser: 4 },
2062
3073
  viewportWidth,
2063
3074
  viewportHeight
2064
3075
  });
2065
- await _pool3.init();
3076
+ await _pool4.init();
2066
3077
  }
2067
- return _pool3;
3078
+ return _pool4;
2068
3079
  }
2069
- async function shutdownPool3() {
2070
- if (_pool3 !== null) {
2071
- await _pool3.close();
2072
- _pool3 = null;
3080
+ async function shutdownPool4() {
3081
+ if (_pool4 !== null) {
3082
+ await _pool4.close();
3083
+ _pool4 = null;
2073
3084
  }
2074
3085
  }
2075
3086
  async function renderComponent(filePath, componentName, props, viewportWidth, viewportHeight) {
2076
- const pool = await getPool3(viewportWidth, viewportHeight);
3087
+ const pool = await getPool4(viewportWidth, viewportHeight);
2077
3088
  const htmlHarness = await buildComponentHarness(filePath, componentName, props, viewportWidth);
2078
3089
  const slot = await pool.acquire();
2079
3090
  const { page } = slot;
@@ -2198,20 +3209,20 @@ async function runBaseline(options = {}) {
2198
3209
  } = options;
2199
3210
  const startTime = performance.now();
2200
3211
  const rootDir = process.cwd();
2201
- const baselineDir = resolve5(rootDir, outputDir);
2202
- const rendersDir = resolve5(baselineDir, "renders");
3212
+ const baselineDir = resolve8(rootDir, outputDir);
3213
+ const rendersDir = resolve8(baselineDir, "renders");
2203
3214
  if (existsSync5(baselineDir)) {
2204
3215
  rmSync(baselineDir, { recursive: true, force: true });
2205
3216
  }
2206
3217
  mkdirSync4(rendersDir, { recursive: true });
2207
3218
  let manifest;
2208
3219
  if (manifestPath !== void 0) {
2209
- const { readFileSync: readFileSync7 } = await import("fs");
2210
- const absPath = resolve5(rootDir, manifestPath);
3220
+ const { readFileSync: readFileSync8 } = await import("fs");
3221
+ const absPath = resolve8(rootDir, manifestPath);
2211
3222
  if (!existsSync5(absPath)) {
2212
3223
  throw new Error(`Manifest not found at ${absPath}.`);
2213
3224
  }
2214
- manifest = JSON.parse(readFileSync7(absPath, "utf-8"));
3225
+ manifest = JSON.parse(readFileSync8(absPath, "utf-8"));
2215
3226
  process.stderr.write(`Loaded manifest from ${manifestPath}
2216
3227
  `);
2217
3228
  } else {
@@ -2221,7 +3232,7 @@ async function runBaseline(options = {}) {
2221
3232
  process.stderr.write(`Found ${count} components.
2222
3233
  `);
2223
3234
  }
2224
- writeFileSync5(resolve5(baselineDir, "manifest.json"), JSON.stringify(manifest, null, 2), "utf-8");
3235
+ writeFileSync5(resolve8(baselineDir, "manifest.json"), JSON.stringify(manifest, null, 2), "utf-8");
2225
3236
  let componentNames = Object.keys(manifest.components);
2226
3237
  if (componentsGlob !== void 0) {
2227
3238
  componentNames = componentNames.filter((name) => matchGlob(componentsGlob, name));
@@ -2242,7 +3253,7 @@ async function runBaseline(options = {}) {
2242
3253
  auditedAt: (/* @__PURE__ */ new Date()).toISOString()
2243
3254
  };
2244
3255
  writeFileSync5(
2245
- resolve5(baselineDir, "compliance.json"),
3256
+ resolve8(baselineDir, "compliance.json"),
2246
3257
  JSON.stringify(emptyReport, null, 2),
2247
3258
  "utf-8"
2248
3259
  );
@@ -2263,7 +3274,7 @@ async function runBaseline(options = {}) {
2263
3274
  const renderOne = async (name) => {
2264
3275
  const descriptor = manifest.components[name];
2265
3276
  if (descriptor === void 0) return;
2266
- const filePath = resolve5(rootDir, descriptor.filePath);
3277
+ const filePath = resolve8(rootDir, descriptor.filePath);
2267
3278
  const outcome = await safeRender2(
2268
3279
  () => renderComponent(filePath, name, {}, viewportWidth, viewportHeight),
2269
3280
  {
@@ -2282,7 +3293,7 @@ async function runBaseline(options = {}) {
2282
3293
  }
2283
3294
  if (outcome.crashed) {
2284
3295
  failureCount++;
2285
- const errPath = resolve5(rendersDir, `${name}.error.json`);
3296
+ const errPath = resolve8(rendersDir, `${name}.error.json`);
2286
3297
  writeFileSync5(
2287
3298
  errPath,
2288
3299
  JSON.stringify(
@@ -2300,10 +3311,10 @@ async function runBaseline(options = {}) {
2300
3311
  return;
2301
3312
  }
2302
3313
  const result = outcome.result;
2303
- writeFileSync5(resolve5(rendersDir, `${name}.png`), result.screenshot);
3314
+ writeFileSync5(resolve8(rendersDir, `${name}.png`), result.screenshot);
2304
3315
  const jsonOutput = formatRenderJson(name, {}, result);
2305
3316
  writeFileSync5(
2306
- resolve5(rendersDir, `${name}.json`),
3317
+ resolve8(rendersDir, `${name}.json`),
2307
3318
  JSON.stringify(jsonOutput, null, 2),
2308
3319
  "utf-8"
2309
3320
  );
@@ -2323,7 +3334,7 @@ async function runBaseline(options = {}) {
2323
3334
  workers.push(worker());
2324
3335
  }
2325
3336
  await Promise.all(workers);
2326
- await shutdownPool3();
3337
+ await shutdownPool4();
2327
3338
  if (isTTY()) {
2328
3339
  process.stderr.write("\n");
2329
3340
  }
@@ -2331,7 +3342,7 @@ async function runBaseline(options = {}) {
2331
3342
  const engine = new ComplianceEngine(resolver);
2332
3343
  const batchReport = engine.auditBatch(computedStylesMap);
2333
3344
  writeFileSync5(
2334
- resolve5(baselineDir, "compliance.json"),
3345
+ resolve8(baselineDir, "compliance.json"),
2335
3346
  JSON.stringify(batchReport, null, 2),
2336
3347
  "utf-8"
2337
3348
  );
@@ -2374,10 +3385,10 @@ function registerBaselineSubCommand(reportCmd) {
2374
3385
  }
2375
3386
 
2376
3387
  // src/tree-formatter.ts
2377
- var BRANCH = "\u251C\u2500\u2500 ";
2378
- var LAST_BRANCH = "\u2514\u2500\u2500 ";
2379
- var VERTICAL = "\u2502 ";
2380
- var EMPTY = " ";
3388
+ var BRANCH2 = "\u251C\u2500\u2500 ";
3389
+ var LAST_BRANCH2 = "\u2514\u2500\u2500 ";
3390
+ var VERTICAL2 = "\u2502 ";
3391
+ var EMPTY2 = " ";
2381
3392
  function buildLabel(node, options) {
2382
3393
  const parts = [node.name];
2383
3394
  if (node.type === "memo") {
@@ -2425,19 +3436,19 @@ function renderNode(node, prefix, isLast, depth, options, lines) {
2425
3436
  }
2426
3437
  return;
2427
3438
  }
2428
- const connector = isLast ? LAST_BRANCH : BRANCH;
3439
+ const connector = isLast ? LAST_BRANCH2 : BRANCH2;
2429
3440
  const label = buildLabel(node, options);
2430
3441
  lines.push(`${prefix}${connector}${label}`);
2431
3442
  if (options.maxDepth !== void 0 && depth >= options.maxDepth) {
2432
3443
  const childCount = countVisibleDescendants(node, options);
2433
3444
  if (childCount > 0) {
2434
- const nextPrefix2 = prefix + (isLast ? EMPTY : VERTICAL);
2435
- lines.push(`${nextPrefix2}${LAST_BRANCH}\u2026 (${childCount} more)`);
3445
+ const nextPrefix2 = prefix + (isLast ? EMPTY2 : VERTICAL2);
3446
+ lines.push(`${nextPrefix2}${LAST_BRANCH2}\u2026 (${childCount} more)`);
2436
3447
  }
2437
3448
  return;
2438
3449
  }
2439
3450
  const visibleChildren = getVisibleChildren(node, options);
2440
- const nextPrefix = prefix + (isLast ? EMPTY : VERTICAL);
3451
+ const nextPrefix = prefix + (isLast ? EMPTY2 : VERTICAL2);
2441
3452
  for (let i = 0; i < visibleChildren.length; i++) {
2442
3453
  const child = visibleChildren[i];
2443
3454
  if (child !== void 0) {
@@ -2479,7 +3490,7 @@ function formatTree(root, options = {}) {
2479
3490
  if (options.maxDepth === 0) {
2480
3491
  const childCount = countVisibleDescendants(root, options);
2481
3492
  if (childCount > 0) {
2482
- lines.push(`${LAST_BRANCH}\u2026 (${childCount} more)`);
3493
+ lines.push(`${LAST_BRANCH2}\u2026 (${childCount} more)`);
2483
3494
  }
2484
3495
  } else {
2485
3496
  const visibleChildren = getVisibleChildren(root, options);
@@ -2653,18 +3664,132 @@ function buildStructuredReport(report) {
2653
3664
  }
2654
3665
 
2655
3666
  // src/tokens/commands.ts
2656
- import { existsSync as existsSync6, readFileSync as readFileSync5 } from "fs";
2657
- import { resolve as resolve6 } from "path";
3667
+ import { existsSync as existsSync7, readFileSync as readFileSync6 } from "fs";
3668
+ import { resolve as resolve10 } from "path";
2658
3669
  import {
2659
- parseTokenFileSync,
3670
+ parseTokenFileSync as parseTokenFileSync2,
2660
3671
  TokenParseError,
2661
- TokenResolver as TokenResolver2,
3672
+ TokenResolver as TokenResolver3,
2662
3673
  TokenValidationError,
2663
3674
  validateTokenFile
2664
3675
  } from "@agent-scope/tokens";
2665
- import { Command as Command5 } from "commander";
3676
+ import { Command as Command7 } from "commander";
3677
+
3678
+ // src/tokens/export.ts
3679
+ import { existsSync as existsSync6, readFileSync as readFileSync5, writeFileSync as writeFileSync6 } from "fs";
3680
+ import { resolve as resolve9 } from "path";
3681
+ import {
3682
+ exportTokens,
3683
+ parseTokenFileSync,
3684
+ ThemeResolver,
3685
+ TokenResolver as TokenResolver2
3686
+ } from "@agent-scope/tokens";
3687
+ import { Command as Command6 } from "commander";
2666
3688
  var DEFAULT_TOKEN_FILE = "reactscope.tokens.json";
2667
3689
  var CONFIG_FILE = "reactscope.config.json";
3690
+ var SUPPORTED_FORMATS = ["css", "ts", "scss", "tailwind", "flat-json", "figma"];
3691
+ function resolveTokenFilePath(fileFlag) {
3692
+ if (fileFlag !== void 0) {
3693
+ return resolve9(process.cwd(), fileFlag);
3694
+ }
3695
+ const configPath = resolve9(process.cwd(), CONFIG_FILE);
3696
+ if (existsSync6(configPath)) {
3697
+ try {
3698
+ const raw = readFileSync5(configPath, "utf-8");
3699
+ const config = JSON.parse(raw);
3700
+ if (typeof config === "object" && config !== null && "tokens" in config && typeof config.tokens === "object" && config.tokens !== null && typeof config.tokens?.file === "string") {
3701
+ const file = config.tokens.file;
3702
+ return resolve9(process.cwd(), file);
3703
+ }
3704
+ } catch {
3705
+ }
3706
+ }
3707
+ return resolve9(process.cwd(), DEFAULT_TOKEN_FILE);
3708
+ }
3709
+ function createTokensExportCommand() {
3710
+ return new Command6("export").description("Export design tokens to a downstream format").requiredOption("--format <fmt>", `Output format: ${SUPPORTED_FORMATS.join(", ")}`).option("--file <path>", "Path to token file (overrides config)").option("--out <path>", "Write output to file instead of stdout").option("--prefix <prefix>", "CSS/SCSS: prefix for variable names (e.g. 'scope')").option("--selector <selector>", "CSS: custom root selector (default: ':root')").option(
3711
+ "--theme <name>",
3712
+ "Include theme overrides for the named theme (applies to css, ts, scss, tailwind, figma)"
3713
+ ).action(
3714
+ (opts) => {
3715
+ if (!SUPPORTED_FORMATS.includes(opts.format)) {
3716
+ process.stderr.write(
3717
+ `Error: unsupported format "${opts.format}".
3718
+ Supported formats: ${SUPPORTED_FORMATS.join(", ")}
3719
+ `
3720
+ );
3721
+ process.exit(1);
3722
+ }
3723
+ const format = opts.format;
3724
+ try {
3725
+ const filePath = resolveTokenFilePath(opts.file);
3726
+ if (!existsSync6(filePath)) {
3727
+ throw new Error(
3728
+ `Token file not found at ${filePath}.
3729
+ Create a reactscope.tokens.json file or use --file to specify a path.`
3730
+ );
3731
+ }
3732
+ const raw = readFileSync5(filePath, "utf-8");
3733
+ const { tokens, rawFile } = parseTokenFileSync(raw);
3734
+ let themesMap;
3735
+ if (opts.theme !== void 0) {
3736
+ if (!rawFile.themes || !(opts.theme in rawFile.themes)) {
3737
+ const available = rawFile.themes ? Object.keys(rawFile.themes).join(", ") : "none";
3738
+ throw new Error(
3739
+ `Theme "${opts.theme}" not found in token file.
3740
+ Available themes: ${available}`
3741
+ );
3742
+ }
3743
+ const baseResolver = new TokenResolver2(tokens);
3744
+ const themeResolver = ThemeResolver.fromTokenFile(
3745
+ baseResolver,
3746
+ rawFile
3747
+ );
3748
+ const themeNames = themeResolver.listThemes();
3749
+ if (!themeNames.includes(opts.theme)) {
3750
+ throw new Error(
3751
+ `Theme "${opts.theme}" could not be resolved.
3752
+ Available themes: ${themeNames.join(", ")}`
3753
+ );
3754
+ }
3755
+ const themedTokens = themeResolver.buildThemedTokens(opts.theme);
3756
+ const overrideMap = /* @__PURE__ */ new Map();
3757
+ for (const themedToken of themedTokens) {
3758
+ const baseToken = tokens.find((t) => t.path === themedToken.path);
3759
+ if (baseToken !== void 0 && themedToken.resolvedValue !== baseToken.resolvedValue) {
3760
+ overrideMap.set(themedToken.path, themedToken.resolvedValue);
3761
+ }
3762
+ }
3763
+ themesMap = /* @__PURE__ */ new Map([[opts.theme, overrideMap]]);
3764
+ }
3765
+ const output = exportTokens(tokens, format, {
3766
+ prefix: opts.prefix,
3767
+ rootSelector: opts.selector,
3768
+ themes: themesMap
3769
+ });
3770
+ if (opts.out !== void 0) {
3771
+ const outPath = resolve9(process.cwd(), opts.out);
3772
+ writeFileSync6(outPath, output, "utf-8");
3773
+ process.stderr.write(`Exported ${tokens.length} tokens to ${outPath}
3774
+ `);
3775
+ } else {
3776
+ process.stdout.write(output);
3777
+ if (!output.endsWith("\n")) {
3778
+ process.stdout.write("\n");
3779
+ }
3780
+ }
3781
+ } catch (err) {
3782
+ process.stderr.write(`Error: ${err instanceof Error ? err.message : String(err)}
3783
+ `);
3784
+ process.exit(1);
3785
+ }
3786
+ }
3787
+ );
3788
+ }
3789
+
3790
+ // src/tokens/commands.ts
3791
+ var DEFAULT_TOKEN_FILE2 = "reactscope.tokens.json";
3792
+ var CONFIG_FILE2 = "reactscope.config.json";
2668
3793
  function isTTY2() {
2669
3794
  return process.stdout.isTTY === true;
2670
3795
  }
@@ -2682,33 +3807,33 @@ function buildTable2(headers, rows) {
2682
3807
  );
2683
3808
  return [headerRow, divider, ...dataRows].join("\n");
2684
3809
  }
2685
- function resolveTokenFilePath(fileFlag) {
3810
+ function resolveTokenFilePath2(fileFlag) {
2686
3811
  if (fileFlag !== void 0) {
2687
- return resolve6(process.cwd(), fileFlag);
3812
+ return resolve10(process.cwd(), fileFlag);
2688
3813
  }
2689
- const configPath = resolve6(process.cwd(), CONFIG_FILE);
2690
- if (existsSync6(configPath)) {
3814
+ const configPath = resolve10(process.cwd(), CONFIG_FILE2);
3815
+ if (existsSync7(configPath)) {
2691
3816
  try {
2692
- const raw = readFileSync5(configPath, "utf-8");
3817
+ const raw = readFileSync6(configPath, "utf-8");
2693
3818
  const config = JSON.parse(raw);
2694
3819
  if (typeof config === "object" && config !== null && "tokens" in config && typeof config.tokens === "object" && config.tokens !== null && typeof config.tokens?.file === "string") {
2695
3820
  const file = config.tokens.file;
2696
- return resolve6(process.cwd(), file);
3821
+ return resolve10(process.cwd(), file);
2697
3822
  }
2698
3823
  } catch {
2699
3824
  }
2700
3825
  }
2701
- return resolve6(process.cwd(), DEFAULT_TOKEN_FILE);
3826
+ return resolve10(process.cwd(), DEFAULT_TOKEN_FILE2);
2702
3827
  }
2703
3828
  function loadTokens(absPath) {
2704
- if (!existsSync6(absPath)) {
3829
+ if (!existsSync7(absPath)) {
2705
3830
  throw new Error(
2706
3831
  `Token file not found at ${absPath}.
2707
3832
  Create a reactscope.tokens.json file or use --file to specify a path.`
2708
3833
  );
2709
3834
  }
2710
- const raw = readFileSync5(absPath, "utf-8");
2711
- return parseTokenFileSync(raw);
3835
+ const raw = readFileSync6(absPath, "utf-8");
3836
+ return parseTokenFileSync2(raw);
2712
3837
  }
2713
3838
  function getRawValue(node, segments) {
2714
3839
  const [head, ...rest] = segments;
@@ -2745,9 +3870,9 @@ function buildResolutionChain(startPath, rawTokens) {
2745
3870
  function registerGet2(tokensCmd) {
2746
3871
  tokensCmd.command("get <path>").description("Resolve a token path to its computed value").option("--file <path>", "Path to token file (overrides config)").option("--format <fmt>", "Output format: json or text (default: auto-detect)").action((tokenPath, opts) => {
2747
3872
  try {
2748
- const filePath = resolveTokenFilePath(opts.file);
3873
+ const filePath = resolveTokenFilePath2(opts.file);
2749
3874
  const { tokens } = loadTokens(filePath);
2750
- const resolver = new TokenResolver2(tokens);
3875
+ const resolver = new TokenResolver3(tokens);
2751
3876
  const resolvedValue = resolver.resolve(tokenPath);
2752
3877
  const useJson = opts.format === "json" || opts.format !== "text" && !isTTY2();
2753
3878
  if (useJson) {
@@ -2771,9 +3896,9 @@ function registerList2(tokensCmd) {
2771
3896
  tokensCmd.command("list [category]").description("List tokens, optionally filtered by category or type").option("--type <type>", "Filter by token type (color, dimension, fontFamily, etc.)").option("--file <path>", "Path to token file (overrides config)").option("--format <fmt>", "Output format: json or table (default: auto-detect)").action(
2772
3897
  (category, opts) => {
2773
3898
  try {
2774
- const filePath = resolveTokenFilePath(opts.file);
3899
+ const filePath = resolveTokenFilePath2(opts.file);
2775
3900
  const { tokens } = loadTokens(filePath);
2776
- const resolver = new TokenResolver2(tokens);
3901
+ const resolver = new TokenResolver3(tokens);
2777
3902
  const filtered = resolver.list(opts.type, category);
2778
3903
  const useJson = opts.format === "json" || opts.format !== "table" && !isTTY2();
2779
3904
  if (useJson) {
@@ -2801,9 +3926,9 @@ function registerSearch(tokensCmd) {
2801
3926
  tokensCmd.command("search <value>").description("Find which token(s) match a computed value (supports fuzzy color matching)").option("--type <type>", "Restrict search to a specific token type").option("--fuzzy", "Return nearest match even if no exact match exists", false).option("--file <path>", "Path to token file (overrides config)").option("--format <fmt>", "Output format: json or table (default: auto-detect)").action(
2802
3927
  (value, opts) => {
2803
3928
  try {
2804
- const filePath = resolveTokenFilePath(opts.file);
3929
+ const filePath = resolveTokenFilePath2(opts.file);
2805
3930
  const { tokens } = loadTokens(filePath);
2806
- const resolver = new TokenResolver2(tokens);
3931
+ const resolver = new TokenResolver3(tokens);
2807
3932
  const useJson = opts.format === "json" || opts.format !== "table" && !isTTY2();
2808
3933
  const typesToSearch = opts.type ? [opts.type] : [
2809
3934
  "color",
@@ -2883,10 +4008,10 @@ Tip: use --fuzzy for nearest-match search.
2883
4008
  function registerResolve(tokensCmd) {
2884
4009
  tokensCmd.command("resolve <path>").description("Show the full resolution chain for a token").option("--file <path>", "Path to token file (overrides config)").option("--format <fmt>", "Output format: json or text (default: auto-detect)").action((tokenPath, opts) => {
2885
4010
  try {
2886
- const filePath = resolveTokenFilePath(opts.file);
4011
+ const filePath = resolveTokenFilePath2(opts.file);
2887
4012
  const absFilePath = filePath;
2888
4013
  const { tokens, rawFile } = loadTokens(absFilePath);
2889
- const resolver = new TokenResolver2(tokens);
4014
+ const resolver = new TokenResolver3(tokens);
2890
4015
  resolver.resolve(tokenPath);
2891
4016
  const chain = buildResolutionChain(tokenPath, rawFile.tokens);
2892
4017
  const useJson = opts.format === "json" || opts.format !== "text" && !isTTY2();
@@ -2920,14 +4045,14 @@ function registerValidate(tokensCmd) {
2920
4045
  "Validate the token file for errors (circular refs, missing refs, type mismatches)"
2921
4046
  ).option("--file <path>", "Path to token file (overrides config)").option("--format <fmt>", "Output format: json or text (default: auto-detect)").action((opts) => {
2922
4047
  try {
2923
- const filePath = resolveTokenFilePath(opts.file);
2924
- if (!existsSync6(filePath)) {
4048
+ const filePath = resolveTokenFilePath2(opts.file);
4049
+ if (!existsSync7(filePath)) {
2925
4050
  throw new Error(
2926
4051
  `Token file not found at ${filePath}.
2927
4052
  Create a reactscope.tokens.json file or use --file to specify a path.`
2928
4053
  );
2929
4054
  }
2930
- const raw = readFileSync5(filePath, "utf-8");
4055
+ const raw = readFileSync6(filePath, "utf-8");
2931
4056
  const useJson = opts.format === "json" || opts.format !== "text" && !isTTY2();
2932
4057
  const errors = [];
2933
4058
  let parsed;
@@ -2954,7 +4079,7 @@ Create a reactscope.tokens.json file or use --file to specify a path.`
2954
4079
  throw err;
2955
4080
  }
2956
4081
  try {
2957
- parseTokenFileSync(raw);
4082
+ parseTokenFileSync2(raw);
2958
4083
  } catch (err) {
2959
4084
  if (err instanceof TokenParseError) {
2960
4085
  errors.push({ code: err.code, path: err.path, message: err.message });
@@ -2995,7 +4120,7 @@ function outputValidationResult(filePath, errors, useJson) {
2995
4120
  }
2996
4121
  }
2997
4122
  function createTokensCommand() {
2998
- const tokensCmd = new Command5("tokens").description(
4123
+ const tokensCmd = new Command7("tokens").description(
2999
4124
  "Query and validate design tokens from a reactscope.tokens.json file"
3000
4125
  );
3001
4126
  registerGet2(tokensCmd);
@@ -3003,12 +4128,13 @@ function createTokensCommand() {
3003
4128
  registerSearch(tokensCmd);
3004
4129
  registerResolve(tokensCmd);
3005
4130
  registerValidate(tokensCmd);
4131
+ tokensCmd.addCommand(createTokensExportCommand());
3006
4132
  return tokensCmd;
3007
4133
  }
3008
4134
 
3009
4135
  // src/program.ts
3010
4136
  function createProgram(options = {}) {
3011
- const program2 = new Command6("scope").version(options.version ?? "0.1.0").description("Scope \u2014 React instrumentation toolkit");
4137
+ const program2 = new Command8("scope").version(options.version ?? "0.1.0").description("Scope \u2014 React instrumentation toolkit");
3012
4138
  program2.command("capture <url>").description("Capture a React component tree from a live URL and output as JSON").option("-o, --output <path>", "Write JSON to file instead of stdout").option("--pretty", "Pretty-print JSON output (default: minified)", false).option("--timeout <ms>", "Max wait time for React to mount (ms)", "10000").option("--wait <ms>", "Additional wait after page load before capture (ms)", "0").action(
3013
4139
  async (url, opts) => {
3014
4140
  try {
@@ -3081,7 +4207,7 @@ function createProgram(options = {}) {
3081
4207
  }
3082
4208
  );
3083
4209
  program2.command("generate").description("Generate a Playwright test from a Scope trace file").argument("<trace>", "Path to a serialized Scope trace (.json)").option("-o, --output <path>", "Output file path", "scope.spec.ts").option("-d, --description <text>", "Test description").action((tracePath, opts) => {
3084
- const raw = readFileSync6(tracePath, "utf-8");
4210
+ const raw = readFileSync7(tracePath, "utf-8");
3085
4211
  const trace = loadTrace(raw);
3086
4212
  const source = generateTest(trace, {
3087
4213
  description: opts.description,