@agent-scope/cli 1.10.0 → 1.11.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 Command7 } 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((resolve10) => {
259
259
  rl.question(question, (answer) => {
260
- resolve7(answer.trim());
260
+ resolve10(answer.trim());
261
261
  });
262
262
  });
263
263
  }
@@ -406,7 +406,7 @@ function createInitCommand() {
406
406
  }
407
407
 
408
408
  // src/instrument/renders.ts
409
- import { resolve as resolve2 } from "path";
409
+ import { resolve as resolve4 } from "path";
410
410
  import { BrowserPool } from "@agent-scope/render";
411
411
  import { Command as Command3 } from "commander";
412
412
 
@@ -957,8 +957,653 @@ function csvEscape(value) {
957
957
  return value;
958
958
  }
959
959
 
960
- // src/instrument/renders.ts
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";
961
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/renders.ts
1606
+ var MANIFEST_PATH4 = ".reactscope/manifest.json";
962
1607
  function determineTrigger(event) {
963
1608
  if (event.forceUpdate) return "force_update";
964
1609
  if (event.stateChanged) return "state_change";
@@ -1203,7 +1848,7 @@ function buildInstrumentationScript() {
1203
1848
  `
1204
1849
  );
1205
1850
  }
1206
- async function replayInteraction(page, steps) {
1851
+ async function replayInteraction2(page, steps) {
1207
1852
  for (const step of steps) {
1208
1853
  switch (step.action) {
1209
1854
  case "click":
@@ -1278,7 +1923,7 @@ async function shutdownPool() {
1278
1923
  }
1279
1924
  }
1280
1925
  async function analyzeRenders(options) {
1281
- const manifestPath = options.manifestPath ?? MANIFEST_PATH2;
1926
+ const manifestPath = options.manifestPath ?? MANIFEST_PATH4;
1282
1927
  const manifest = loadManifest(manifestPath);
1283
1928
  const descriptor = manifest.components[options.componentName];
1284
1929
  if (descriptor === void 0) {
@@ -1289,7 +1934,7 @@ Available: ${available}`
1289
1934
  );
1290
1935
  }
1291
1936
  const rootDir = process.cwd();
1292
- const filePath = resolve2(rootDir, descriptor.filePath);
1937
+ const filePath = resolve4(rootDir, descriptor.filePath);
1293
1938
  const htmlHarness = await buildComponentHarness(filePath, options.componentName, {}, 1280);
1294
1939
  const pool = await getPool();
1295
1940
  const slot = await pool.acquire();
@@ -1307,7 +1952,7 @@ Available: ${available}`
1307
1952
  window.__SCOPE_RENDER_EVENTS__ = [];
1308
1953
  window.__SCOPE_RENDER_INDEX__ = 0;
1309
1954
  });
1310
- await replayInteraction(page, options.interaction);
1955
+ await replayInteraction2(page, options.interaction);
1311
1956
  await page.waitForTimeout(200);
1312
1957
  const interactionDurationMs = performance.now() - startMs;
1313
1958
  const rawEvents = await page.evaluate(() => {
@@ -1376,7 +2021,7 @@ function createInstrumentRendersCommand() {
1376
2021
  "--interaction <json>",
1377
2022
  `Interaction sequence JSON, e.g. '[{"action":"click","target":"button"}]'`,
1378
2023
  "[]"
1379
- ).option("--json", "Output as JSON regardless of TTY", false).option("--manifest <path>", "Path to manifest.json", MANIFEST_PATH2).action(
2024
+ ).option("--json", "Output as JSON regardless of TTY", false).option("--manifest <path>", "Path to manifest.json", MANIFEST_PATH4).action(
1380
2025
  async (componentName, opts) => {
1381
2026
  let interaction = [];
1382
2027
  try {
@@ -1421,12 +2066,14 @@ function createInstrumentCommand() {
1421
2066
  "Structured instrumentation commands for React component analysis"
1422
2067
  );
1423
2068
  instrumentCmd.addCommand(createInstrumentRendersCommand());
2069
+ instrumentCmd.addCommand(createInstrumentHooksCommand());
2070
+ instrumentCmd.addCommand(createInstrumentProfileCommand());
1424
2071
  return instrumentCmd;
1425
2072
  }
1426
2073
 
1427
2074
  // src/render-commands.ts
1428
2075
  import { mkdirSync as mkdirSync3, writeFileSync as writeFileSync4 } from "fs";
1429
- import { resolve as resolve4 } from "path";
2076
+ import { resolve as resolve6 } from "path";
1430
2077
  import {
1431
2078
  ALL_CONTEXT_IDS,
1432
2079
  ALL_STRESS_IDS,
@@ -1442,7 +2089,7 @@ import { Command as Command4 } from "commander";
1442
2089
  // src/tailwind-css.ts
1443
2090
  import { existsSync as existsSync4, readFileSync as readFileSync4 } from "fs";
1444
2091
  import { createRequire } from "module";
1445
- import { resolve as resolve3 } from "path";
2092
+ import { resolve as resolve5 } from "path";
1446
2093
  var CONFIG_FILENAMES = [
1447
2094
  ".reactscope/config.json",
1448
2095
  ".reactscope/config.js",
@@ -1459,14 +2106,14 @@ var STYLE_ENTRY_CANDIDATES = [
1459
2106
  var TAILWIND_IMPORT = /@import\s+["']tailwindcss["']\s*;?/;
1460
2107
  var compilerCache = null;
1461
2108
  function getCachedBuild(cwd) {
1462
- if (compilerCache !== null && resolve3(compilerCache.cwd) === resolve3(cwd)) {
2109
+ if (compilerCache !== null && resolve5(compilerCache.cwd) === resolve5(cwd)) {
1463
2110
  return compilerCache.build;
1464
2111
  }
1465
2112
  return null;
1466
2113
  }
1467
2114
  function findStylesEntry(cwd) {
1468
2115
  for (const name of CONFIG_FILENAMES) {
1469
- const p = resolve3(cwd, name);
2116
+ const p = resolve5(cwd, name);
1470
2117
  if (!existsSync4(p)) continue;
1471
2118
  try {
1472
2119
  if (name.endsWith(".json")) {
@@ -1475,28 +2122,28 @@ function findStylesEntry(cwd) {
1475
2122
  const scope = data.scope;
1476
2123
  const entry = scope?.stylesEntry ?? data.stylesEntry;
1477
2124
  if (typeof entry === "string") {
1478
- const full = resolve3(cwd, entry);
2125
+ const full = resolve5(cwd, entry);
1479
2126
  if (existsSync4(full)) return full;
1480
2127
  }
1481
2128
  }
1482
2129
  } catch {
1483
2130
  }
1484
2131
  }
1485
- const pkgPath = resolve3(cwd, "package.json");
2132
+ const pkgPath = resolve5(cwd, "package.json");
1486
2133
  if (existsSync4(pkgPath)) {
1487
2134
  try {
1488
2135
  const raw = readFileSync4(pkgPath, "utf-8");
1489
2136
  const pkg = JSON.parse(raw);
1490
2137
  const entry = pkg.scope?.stylesEntry;
1491
2138
  if (typeof entry === "string") {
1492
- const full = resolve3(cwd, entry);
2139
+ const full = resolve5(cwd, entry);
1493
2140
  if (existsSync4(full)) return full;
1494
2141
  }
1495
2142
  } catch {
1496
2143
  }
1497
2144
  }
1498
2145
  for (const candidate of STYLE_ENTRY_CANDIDATES) {
1499
- const full = resolve3(cwd, candidate);
2146
+ const full = resolve5(cwd, candidate);
1500
2147
  if (existsSync4(full)) {
1501
2148
  try {
1502
2149
  const content = readFileSync4(full, "utf-8");
@@ -1514,7 +2161,7 @@ async function getTailwindCompiler(cwd) {
1514
2161
  if (entryPath === null) return null;
1515
2162
  let compile;
1516
2163
  try {
1517
- const require2 = createRequire(resolve3(cwd, "package.json"));
2164
+ const require2 = createRequire(resolve5(cwd, "package.json"));
1518
2165
  const tailwind = require2("tailwindcss");
1519
2166
  const fn = tailwind.compile;
1520
2167
  if (typeof fn !== "function") return null;
@@ -1525,8 +2172,8 @@ async function getTailwindCompiler(cwd) {
1525
2172
  const entryContent = readFileSync4(entryPath, "utf-8");
1526
2173
  const loadStylesheet = async (id, base) => {
1527
2174
  if (id === "tailwindcss") {
1528
- const nodeModules = resolve3(cwd, "node_modules");
1529
- const tailwindCssPath = resolve3(nodeModules, "tailwindcss", "index.css");
2175
+ const nodeModules = resolve5(cwd, "node_modules");
2176
+ const tailwindCssPath = resolve5(nodeModules, "tailwindcss", "index.css");
1530
2177
  if (!existsSync4(tailwindCssPath)) {
1531
2178
  throw new Error(
1532
2179
  `Tailwind v4: tailwindcss package not found at ${tailwindCssPath}. Install with: npm install tailwindcss`
@@ -1535,10 +2182,10 @@ async function getTailwindCompiler(cwd) {
1535
2182
  const content = readFileSync4(tailwindCssPath, "utf-8");
1536
2183
  return { path: "virtual:tailwindcss/index.css", base, content };
1537
2184
  }
1538
- const full = resolve3(base, id);
2185
+ const full = resolve5(base, id);
1539
2186
  if (existsSync4(full)) {
1540
2187
  const content = readFileSync4(full, "utf-8");
1541
- return { path: full, base: resolve3(full, ".."), content };
2188
+ return { path: full, base: resolve5(full, ".."), content };
1542
2189
  }
1543
2190
  throw new Error(`Tailwind v4: could not load stylesheet: ${id} (base: ${base})`);
1544
2191
  };
@@ -1560,7 +2207,7 @@ async function getCompiledCssForClasses(cwd, classes) {
1560
2207
  }
1561
2208
 
1562
2209
  // src/render-commands.ts
1563
- var MANIFEST_PATH3 = ".reactscope/manifest.json";
2210
+ var MANIFEST_PATH5 = ".reactscope/manifest.json";
1564
2211
  var DEFAULT_OUTPUT_DIR = ".reactscope/renders";
1565
2212
  var _pool2 = null;
1566
2213
  async function getPool2(viewportWidth, viewportHeight) {
@@ -1685,7 +2332,7 @@ function buildRenderer(filePath, componentName, viewportWidth, viewportHeight) {
1685
2332
  };
1686
2333
  }
1687
2334
  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(
2335
+ 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_PATH5).action(
1689
2336
  async (componentName, opts) => {
1690
2337
  try {
1691
2338
  const manifest = loadManifest(opts.manifest);
@@ -1707,7 +2354,7 @@ Available: ${available}`
1707
2354
  }
1708
2355
  const { width, height } = parseViewport(opts.viewport);
1709
2356
  const rootDir = process.cwd();
1710
- const filePath = resolve4(rootDir, descriptor.filePath);
2357
+ const filePath = resolve6(rootDir, descriptor.filePath);
1711
2358
  const renderer = buildRenderer(filePath, componentName, width, height);
1712
2359
  process.stderr.write(
1713
2360
  `Rendering ${componentName} [${descriptor.complexityClass}] at ${width}\xD7${height}\u2026
@@ -1737,7 +2384,7 @@ Available: ${available}`
1737
2384
  }
1738
2385
  const result = outcome.result;
1739
2386
  if (opts.output !== void 0) {
1740
- const outPath = resolve4(process.cwd(), opts.output);
2387
+ const outPath = resolve6(process.cwd(), opts.output);
1741
2388
  writeFileSync4(outPath, result.screenshot);
1742
2389
  process.stdout.write(
1743
2390
  `\u2713 ${componentName} \u2192 ${opts.output} (${result.width}\xD7${result.height}, ${result.renderTimeMs.toFixed(0)}ms)
@@ -1751,9 +2398,9 @@ Available: ${available}`
1751
2398
  process.stdout.write(`${JSON.stringify(json, null, 2)}
1752
2399
  `);
1753
2400
  } else if (fmt === "file") {
1754
- const dir = resolve4(process.cwd(), DEFAULT_OUTPUT_DIR);
2401
+ const dir = resolve6(process.cwd(), DEFAULT_OUTPUT_DIR);
1755
2402
  mkdirSync3(dir, { recursive: true });
1756
- const outPath = resolve4(dir, `${componentName}.png`);
2403
+ const outPath = resolve6(dir, `${componentName}.png`);
1757
2404
  writeFileSync4(outPath, result.screenshot);
1758
2405
  const relPath = `${DEFAULT_OUTPUT_DIR}/${componentName}.png`;
1759
2406
  process.stdout.write(
@@ -1761,9 +2408,9 @@ Available: ${available}`
1761
2408
  `
1762
2409
  );
1763
2410
  } else {
1764
- const dir = resolve4(process.cwd(), DEFAULT_OUTPUT_DIR);
2411
+ const dir = resolve6(process.cwd(), DEFAULT_OUTPUT_DIR);
1765
2412
  mkdirSync3(dir, { recursive: true });
1766
- const outPath = resolve4(dir, `${componentName}.png`);
2413
+ const outPath = resolve6(dir, `${componentName}.png`);
1767
2414
  writeFileSync4(outPath, result.screenshot);
1768
2415
  const relPath = `${DEFAULT_OUTPUT_DIR}/${componentName}.png`;
1769
2416
  process.stdout.write(
@@ -1784,7 +2431,7 @@ function registerRenderMatrix(renderCmd) {
1784
2431
  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
2432
  "--contexts <ids>",
1786
2433
  "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(
2434
+ ).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_PATH5).action(
1788
2435
  async (componentName, opts) => {
1789
2436
  try {
1790
2437
  const manifest = loadManifest(opts.manifest);
@@ -1799,7 +2446,7 @@ Available: ${available}`
1799
2446
  const concurrency = Math.max(1, parseInt(opts.concurrency, 10) || 8);
1800
2447
  const { width, height } = { width: 375, height: 812 };
1801
2448
  const rootDir = process.cwd();
1802
- const filePath = resolve4(rootDir, descriptor.filePath);
2449
+ const filePath = resolve6(rootDir, descriptor.filePath);
1803
2450
  const renderer = buildRenderer(filePath, componentName, width, height);
1804
2451
  const axes = [];
1805
2452
  if (opts.axes !== void 0) {
@@ -1866,7 +2513,7 @@ Available: ${available}`
1866
2513
  const { SpriteSheetGenerator } = await import("@agent-scope/render");
1867
2514
  const gen = new SpriteSheetGenerator();
1868
2515
  const sheet = await gen.generate(result);
1869
- const spritePath = resolve4(process.cwd(), opts.sprite);
2516
+ const spritePath = resolve6(process.cwd(), opts.sprite);
1870
2517
  writeFileSync4(spritePath, sheet.png);
1871
2518
  process.stderr.write(`Sprite sheet saved to ${spritePath}
1872
2519
  `);
@@ -1876,9 +2523,9 @@ Available: ${available}`
1876
2523
  const { SpriteSheetGenerator } = await import("@agent-scope/render");
1877
2524
  const gen = new SpriteSheetGenerator();
1878
2525
  const sheet = await gen.generate(result);
1879
- const dir = resolve4(process.cwd(), DEFAULT_OUTPUT_DIR);
2526
+ const dir = resolve6(process.cwd(), DEFAULT_OUTPUT_DIR);
1880
2527
  mkdirSync3(dir, { recursive: true });
1881
- const outPath = resolve4(dir, `${componentName}-matrix.png`);
2528
+ const outPath = resolve6(dir, `${componentName}-matrix.png`);
1882
2529
  writeFileSync4(outPath, sheet.png);
1883
2530
  const relPath = `${DEFAULT_OUTPUT_DIR}/${componentName}-matrix.png`;
1884
2531
  process.stdout.write(
@@ -1911,7 +2558,7 @@ Available: ${available}`
1911
2558
  );
1912
2559
  }
1913
2560
  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(
2561
+ 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_PATH5).option("--format <fmt>", "Output format: json|png (default: png)", "png").action(
1915
2562
  async (opts) => {
1916
2563
  try {
1917
2564
  const manifest = loadManifest(opts.manifest);
@@ -1922,7 +2569,7 @@ function registerRenderAll(renderCmd) {
1922
2569
  return;
1923
2570
  }
1924
2571
  const concurrency = Math.max(1, parseInt(opts.concurrency, 10) || 4);
1925
- const outputDir = resolve4(process.cwd(), opts.outputDir);
2572
+ const outputDir = resolve6(process.cwd(), opts.outputDir);
1926
2573
  mkdirSync3(outputDir, { recursive: true });
1927
2574
  const rootDir = process.cwd();
1928
2575
  process.stderr.write(`Rendering ${total} components (concurrency: ${concurrency})\u2026
@@ -1932,7 +2579,7 @@ function registerRenderAll(renderCmd) {
1932
2579
  const renderOne = async (name) => {
1933
2580
  const descriptor = manifest.components[name];
1934
2581
  if (descriptor === void 0) return;
1935
- const filePath = resolve4(rootDir, descriptor.filePath);
2582
+ const filePath = resolve6(rootDir, descriptor.filePath);
1936
2583
  const renderer = buildRenderer(filePath, name, 375, 812);
1937
2584
  const outcome = await safeRender(
1938
2585
  () => renderer.renderCell({}, descriptor.complexityClass),
@@ -1955,7 +2602,7 @@ function registerRenderAll(renderCmd) {
1955
2602
  success: false,
1956
2603
  errorMessage: outcome.error.message
1957
2604
  });
1958
- const errPath = resolve4(outputDir, `${name}.error.json`);
2605
+ const errPath = resolve6(outputDir, `${name}.error.json`);
1959
2606
  writeFileSync4(
1960
2607
  errPath,
1961
2608
  JSON.stringify(
@@ -1973,9 +2620,9 @@ function registerRenderAll(renderCmd) {
1973
2620
  }
1974
2621
  const result = outcome.result;
1975
2622
  results.push({ name, renderTimeMs: result.renderTimeMs, success: true });
1976
- const pngPath = resolve4(outputDir, `${name}.png`);
2623
+ const pngPath = resolve6(outputDir, `${name}.png`);
1977
2624
  writeFileSync4(pngPath, result.screenshot);
1978
- const jsonPath = resolve4(outputDir, `${name}.json`);
2625
+ const jsonPath = resolve6(outputDir, `${name}.json`);
1979
2626
  writeFileSync4(jsonPath, JSON.stringify(formatRenderJson(name, {}, result), null, 2));
1980
2627
  if (isTTY()) {
1981
2628
  process.stdout.write(
@@ -2049,7 +2696,7 @@ function createRenderCommand() {
2049
2696
 
2050
2697
  // src/report/baseline.ts
2051
2698
  import { existsSync as existsSync5, mkdirSync as mkdirSync4, rmSync, writeFileSync as writeFileSync5 } from "fs";
2052
- import { resolve as resolve5 } from "path";
2699
+ import { resolve as resolve7 } from "path";
2053
2700
  import { generateManifest as generateManifest2 } from "@agent-scope/manifest";
2054
2701
  import { BrowserPool as BrowserPool3, safeRender as safeRender2 } from "@agent-scope/render";
2055
2702
  import { ComplianceEngine, TokenResolver } from "@agent-scope/tokens";
@@ -2198,20 +2845,20 @@ async function runBaseline(options = {}) {
2198
2845
  } = options;
2199
2846
  const startTime = performance.now();
2200
2847
  const rootDir = process.cwd();
2201
- const baselineDir = resolve5(rootDir, outputDir);
2202
- const rendersDir = resolve5(baselineDir, "renders");
2848
+ const baselineDir = resolve7(rootDir, outputDir);
2849
+ const rendersDir = resolve7(baselineDir, "renders");
2203
2850
  if (existsSync5(baselineDir)) {
2204
2851
  rmSync(baselineDir, { recursive: true, force: true });
2205
2852
  }
2206
2853
  mkdirSync4(rendersDir, { recursive: true });
2207
2854
  let manifest;
2208
2855
  if (manifestPath !== void 0) {
2209
- const { readFileSync: readFileSync7 } = await import("fs");
2210
- const absPath = resolve5(rootDir, manifestPath);
2856
+ const { readFileSync: readFileSync8 } = await import("fs");
2857
+ const absPath = resolve7(rootDir, manifestPath);
2211
2858
  if (!existsSync5(absPath)) {
2212
2859
  throw new Error(`Manifest not found at ${absPath}.`);
2213
2860
  }
2214
- manifest = JSON.parse(readFileSync7(absPath, "utf-8"));
2861
+ manifest = JSON.parse(readFileSync8(absPath, "utf-8"));
2215
2862
  process.stderr.write(`Loaded manifest from ${manifestPath}
2216
2863
  `);
2217
2864
  } else {
@@ -2221,7 +2868,7 @@ async function runBaseline(options = {}) {
2221
2868
  process.stderr.write(`Found ${count} components.
2222
2869
  `);
2223
2870
  }
2224
- writeFileSync5(resolve5(baselineDir, "manifest.json"), JSON.stringify(manifest, null, 2), "utf-8");
2871
+ writeFileSync5(resolve7(baselineDir, "manifest.json"), JSON.stringify(manifest, null, 2), "utf-8");
2225
2872
  let componentNames = Object.keys(manifest.components);
2226
2873
  if (componentsGlob !== void 0) {
2227
2874
  componentNames = componentNames.filter((name) => matchGlob(componentsGlob, name));
@@ -2242,7 +2889,7 @@ async function runBaseline(options = {}) {
2242
2889
  auditedAt: (/* @__PURE__ */ new Date()).toISOString()
2243
2890
  };
2244
2891
  writeFileSync5(
2245
- resolve5(baselineDir, "compliance.json"),
2892
+ resolve7(baselineDir, "compliance.json"),
2246
2893
  JSON.stringify(emptyReport, null, 2),
2247
2894
  "utf-8"
2248
2895
  );
@@ -2263,7 +2910,7 @@ async function runBaseline(options = {}) {
2263
2910
  const renderOne = async (name) => {
2264
2911
  const descriptor = manifest.components[name];
2265
2912
  if (descriptor === void 0) return;
2266
- const filePath = resolve5(rootDir, descriptor.filePath);
2913
+ const filePath = resolve7(rootDir, descriptor.filePath);
2267
2914
  const outcome = await safeRender2(
2268
2915
  () => renderComponent(filePath, name, {}, viewportWidth, viewportHeight),
2269
2916
  {
@@ -2282,7 +2929,7 @@ async function runBaseline(options = {}) {
2282
2929
  }
2283
2930
  if (outcome.crashed) {
2284
2931
  failureCount++;
2285
- const errPath = resolve5(rendersDir, `${name}.error.json`);
2932
+ const errPath = resolve7(rendersDir, `${name}.error.json`);
2286
2933
  writeFileSync5(
2287
2934
  errPath,
2288
2935
  JSON.stringify(
@@ -2300,10 +2947,10 @@ async function runBaseline(options = {}) {
2300
2947
  return;
2301
2948
  }
2302
2949
  const result = outcome.result;
2303
- writeFileSync5(resolve5(rendersDir, `${name}.png`), result.screenshot);
2950
+ writeFileSync5(resolve7(rendersDir, `${name}.png`), result.screenshot);
2304
2951
  const jsonOutput = formatRenderJson(name, {}, result);
2305
2952
  writeFileSync5(
2306
- resolve5(rendersDir, `${name}.json`),
2953
+ resolve7(rendersDir, `${name}.json`),
2307
2954
  JSON.stringify(jsonOutput, null, 2),
2308
2955
  "utf-8"
2309
2956
  );
@@ -2331,7 +2978,7 @@ async function runBaseline(options = {}) {
2331
2978
  const engine = new ComplianceEngine(resolver);
2332
2979
  const batchReport = engine.auditBatch(computedStylesMap);
2333
2980
  writeFileSync5(
2334
- resolve5(baselineDir, "compliance.json"),
2981
+ resolve7(baselineDir, "compliance.json"),
2335
2982
  JSON.stringify(batchReport, null, 2),
2336
2983
  "utf-8"
2337
2984
  );
@@ -2653,18 +3300,132 @@ function buildStructuredReport(report) {
2653
3300
  }
2654
3301
 
2655
3302
  // src/tokens/commands.ts
2656
- import { existsSync as existsSync6, readFileSync as readFileSync5 } from "fs";
2657
- import { resolve as resolve6 } from "path";
3303
+ import { existsSync as existsSync7, readFileSync as readFileSync6 } from "fs";
3304
+ import { resolve as resolve9 } from "path";
2658
3305
  import {
2659
- parseTokenFileSync,
3306
+ parseTokenFileSync as parseTokenFileSync2,
2660
3307
  TokenParseError,
2661
- TokenResolver as TokenResolver2,
3308
+ TokenResolver as TokenResolver3,
2662
3309
  TokenValidationError,
2663
3310
  validateTokenFile
2664
3311
  } from "@agent-scope/tokens";
3312
+ import { Command as Command6 } from "commander";
3313
+
3314
+ // src/tokens/export.ts
3315
+ import { existsSync as existsSync6, readFileSync as readFileSync5, writeFileSync as writeFileSync6 } from "fs";
3316
+ import { resolve as resolve8 } from "path";
3317
+ import {
3318
+ exportTokens,
3319
+ parseTokenFileSync,
3320
+ ThemeResolver,
3321
+ TokenResolver as TokenResolver2
3322
+ } from "@agent-scope/tokens";
2665
3323
  import { Command as Command5 } from "commander";
2666
3324
  var DEFAULT_TOKEN_FILE = "reactscope.tokens.json";
2667
3325
  var CONFIG_FILE = "reactscope.config.json";
3326
+ var SUPPORTED_FORMATS = ["css", "ts", "scss", "tailwind", "flat-json", "figma"];
3327
+ function resolveTokenFilePath(fileFlag) {
3328
+ if (fileFlag !== void 0) {
3329
+ return resolve8(process.cwd(), fileFlag);
3330
+ }
3331
+ const configPath = resolve8(process.cwd(), CONFIG_FILE);
3332
+ if (existsSync6(configPath)) {
3333
+ try {
3334
+ const raw = readFileSync5(configPath, "utf-8");
3335
+ const config = JSON.parse(raw);
3336
+ if (typeof config === "object" && config !== null && "tokens" in config && typeof config.tokens === "object" && config.tokens !== null && typeof config.tokens?.file === "string") {
3337
+ const file = config.tokens.file;
3338
+ return resolve8(process.cwd(), file);
3339
+ }
3340
+ } catch {
3341
+ }
3342
+ }
3343
+ return resolve8(process.cwd(), DEFAULT_TOKEN_FILE);
3344
+ }
3345
+ function createTokensExportCommand() {
3346
+ return new Command5("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(
3347
+ "--theme <name>",
3348
+ "Include theme overrides for the named theme (applies to css, ts, scss, tailwind, figma)"
3349
+ ).action(
3350
+ (opts) => {
3351
+ if (!SUPPORTED_FORMATS.includes(opts.format)) {
3352
+ process.stderr.write(
3353
+ `Error: unsupported format "${opts.format}".
3354
+ Supported formats: ${SUPPORTED_FORMATS.join(", ")}
3355
+ `
3356
+ );
3357
+ process.exit(1);
3358
+ }
3359
+ const format = opts.format;
3360
+ try {
3361
+ const filePath = resolveTokenFilePath(opts.file);
3362
+ if (!existsSync6(filePath)) {
3363
+ throw new Error(
3364
+ `Token file not found at ${filePath}.
3365
+ Create a reactscope.tokens.json file or use --file to specify a path.`
3366
+ );
3367
+ }
3368
+ const raw = readFileSync5(filePath, "utf-8");
3369
+ const { tokens, rawFile } = parseTokenFileSync(raw);
3370
+ let themesMap;
3371
+ if (opts.theme !== void 0) {
3372
+ if (!rawFile.themes || !(opts.theme in rawFile.themes)) {
3373
+ const available = rawFile.themes ? Object.keys(rawFile.themes).join(", ") : "none";
3374
+ throw new Error(
3375
+ `Theme "${opts.theme}" not found in token file.
3376
+ Available themes: ${available}`
3377
+ );
3378
+ }
3379
+ const baseResolver = new TokenResolver2(tokens);
3380
+ const themeResolver = ThemeResolver.fromTokenFile(
3381
+ baseResolver,
3382
+ rawFile
3383
+ );
3384
+ const themeNames = themeResolver.listThemes();
3385
+ if (!themeNames.includes(opts.theme)) {
3386
+ throw new Error(
3387
+ `Theme "${opts.theme}" could not be resolved.
3388
+ Available themes: ${themeNames.join(", ")}`
3389
+ );
3390
+ }
3391
+ const themedTokens = themeResolver.buildThemedTokens(opts.theme);
3392
+ const overrideMap = /* @__PURE__ */ new Map();
3393
+ for (const themedToken of themedTokens) {
3394
+ const baseToken = tokens.find((t) => t.path === themedToken.path);
3395
+ if (baseToken !== void 0 && themedToken.resolvedValue !== baseToken.resolvedValue) {
3396
+ overrideMap.set(themedToken.path, themedToken.resolvedValue);
3397
+ }
3398
+ }
3399
+ themesMap = /* @__PURE__ */ new Map([[opts.theme, overrideMap]]);
3400
+ }
3401
+ const output = exportTokens(tokens, format, {
3402
+ prefix: opts.prefix,
3403
+ rootSelector: opts.selector,
3404
+ themes: themesMap
3405
+ });
3406
+ if (opts.out !== void 0) {
3407
+ const outPath = resolve8(process.cwd(), opts.out);
3408
+ writeFileSync6(outPath, output, "utf-8");
3409
+ process.stderr.write(`Exported ${tokens.length} tokens to ${outPath}
3410
+ `);
3411
+ } else {
3412
+ process.stdout.write(output);
3413
+ if (!output.endsWith("\n")) {
3414
+ process.stdout.write("\n");
3415
+ }
3416
+ }
3417
+ } catch (err) {
3418
+ process.stderr.write(`Error: ${err instanceof Error ? err.message : String(err)}
3419
+ `);
3420
+ process.exit(1);
3421
+ }
3422
+ }
3423
+ );
3424
+ }
3425
+
3426
+ // src/tokens/commands.ts
3427
+ var DEFAULT_TOKEN_FILE2 = "reactscope.tokens.json";
3428
+ var CONFIG_FILE2 = "reactscope.config.json";
2668
3429
  function isTTY2() {
2669
3430
  return process.stdout.isTTY === true;
2670
3431
  }
@@ -2682,33 +3443,33 @@ function buildTable2(headers, rows) {
2682
3443
  );
2683
3444
  return [headerRow, divider, ...dataRows].join("\n");
2684
3445
  }
2685
- function resolveTokenFilePath(fileFlag) {
3446
+ function resolveTokenFilePath2(fileFlag) {
2686
3447
  if (fileFlag !== void 0) {
2687
- return resolve6(process.cwd(), fileFlag);
3448
+ return resolve9(process.cwd(), fileFlag);
2688
3449
  }
2689
- const configPath = resolve6(process.cwd(), CONFIG_FILE);
2690
- if (existsSync6(configPath)) {
3450
+ const configPath = resolve9(process.cwd(), CONFIG_FILE2);
3451
+ if (existsSync7(configPath)) {
2691
3452
  try {
2692
- const raw = readFileSync5(configPath, "utf-8");
3453
+ const raw = readFileSync6(configPath, "utf-8");
2693
3454
  const config = JSON.parse(raw);
2694
3455
  if (typeof config === "object" && config !== null && "tokens" in config && typeof config.tokens === "object" && config.tokens !== null && typeof config.tokens?.file === "string") {
2695
3456
  const file = config.tokens.file;
2696
- return resolve6(process.cwd(), file);
3457
+ return resolve9(process.cwd(), file);
2697
3458
  }
2698
3459
  } catch {
2699
3460
  }
2700
3461
  }
2701
- return resolve6(process.cwd(), DEFAULT_TOKEN_FILE);
3462
+ return resolve9(process.cwd(), DEFAULT_TOKEN_FILE2);
2702
3463
  }
2703
3464
  function loadTokens(absPath) {
2704
- if (!existsSync6(absPath)) {
3465
+ if (!existsSync7(absPath)) {
2705
3466
  throw new Error(
2706
3467
  `Token file not found at ${absPath}.
2707
3468
  Create a reactscope.tokens.json file or use --file to specify a path.`
2708
3469
  );
2709
3470
  }
2710
- const raw = readFileSync5(absPath, "utf-8");
2711
- return parseTokenFileSync(raw);
3471
+ const raw = readFileSync6(absPath, "utf-8");
3472
+ return parseTokenFileSync2(raw);
2712
3473
  }
2713
3474
  function getRawValue(node, segments) {
2714
3475
  const [head, ...rest] = segments;
@@ -2745,9 +3506,9 @@ function buildResolutionChain(startPath, rawTokens) {
2745
3506
  function registerGet2(tokensCmd) {
2746
3507
  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
3508
  try {
2748
- const filePath = resolveTokenFilePath(opts.file);
3509
+ const filePath = resolveTokenFilePath2(opts.file);
2749
3510
  const { tokens } = loadTokens(filePath);
2750
- const resolver = new TokenResolver2(tokens);
3511
+ const resolver = new TokenResolver3(tokens);
2751
3512
  const resolvedValue = resolver.resolve(tokenPath);
2752
3513
  const useJson = opts.format === "json" || opts.format !== "text" && !isTTY2();
2753
3514
  if (useJson) {
@@ -2771,9 +3532,9 @@ function registerList2(tokensCmd) {
2771
3532
  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
3533
  (category, opts) => {
2773
3534
  try {
2774
- const filePath = resolveTokenFilePath(opts.file);
3535
+ const filePath = resolveTokenFilePath2(opts.file);
2775
3536
  const { tokens } = loadTokens(filePath);
2776
- const resolver = new TokenResolver2(tokens);
3537
+ const resolver = new TokenResolver3(tokens);
2777
3538
  const filtered = resolver.list(opts.type, category);
2778
3539
  const useJson = opts.format === "json" || opts.format !== "table" && !isTTY2();
2779
3540
  if (useJson) {
@@ -2801,9 +3562,9 @@ function registerSearch(tokensCmd) {
2801
3562
  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
3563
  (value, opts) => {
2803
3564
  try {
2804
- const filePath = resolveTokenFilePath(opts.file);
3565
+ const filePath = resolveTokenFilePath2(opts.file);
2805
3566
  const { tokens } = loadTokens(filePath);
2806
- const resolver = new TokenResolver2(tokens);
3567
+ const resolver = new TokenResolver3(tokens);
2807
3568
  const useJson = opts.format === "json" || opts.format !== "table" && !isTTY2();
2808
3569
  const typesToSearch = opts.type ? [opts.type] : [
2809
3570
  "color",
@@ -2883,10 +3644,10 @@ Tip: use --fuzzy for nearest-match search.
2883
3644
  function registerResolve(tokensCmd) {
2884
3645
  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
3646
  try {
2886
- const filePath = resolveTokenFilePath(opts.file);
3647
+ const filePath = resolveTokenFilePath2(opts.file);
2887
3648
  const absFilePath = filePath;
2888
3649
  const { tokens, rawFile } = loadTokens(absFilePath);
2889
- const resolver = new TokenResolver2(tokens);
3650
+ const resolver = new TokenResolver3(tokens);
2890
3651
  resolver.resolve(tokenPath);
2891
3652
  const chain = buildResolutionChain(tokenPath, rawFile.tokens);
2892
3653
  const useJson = opts.format === "json" || opts.format !== "text" && !isTTY2();
@@ -2920,14 +3681,14 @@ function registerValidate(tokensCmd) {
2920
3681
  "Validate the token file for errors (circular refs, missing refs, type mismatches)"
2921
3682
  ).option("--file <path>", "Path to token file (overrides config)").option("--format <fmt>", "Output format: json or text (default: auto-detect)").action((opts) => {
2922
3683
  try {
2923
- const filePath = resolveTokenFilePath(opts.file);
2924
- if (!existsSync6(filePath)) {
3684
+ const filePath = resolveTokenFilePath2(opts.file);
3685
+ if (!existsSync7(filePath)) {
2925
3686
  throw new Error(
2926
3687
  `Token file not found at ${filePath}.
2927
3688
  Create a reactscope.tokens.json file or use --file to specify a path.`
2928
3689
  );
2929
3690
  }
2930
- const raw = readFileSync5(filePath, "utf-8");
3691
+ const raw = readFileSync6(filePath, "utf-8");
2931
3692
  const useJson = opts.format === "json" || opts.format !== "text" && !isTTY2();
2932
3693
  const errors = [];
2933
3694
  let parsed;
@@ -2954,7 +3715,7 @@ Create a reactscope.tokens.json file or use --file to specify a path.`
2954
3715
  throw err;
2955
3716
  }
2956
3717
  try {
2957
- parseTokenFileSync(raw);
3718
+ parseTokenFileSync2(raw);
2958
3719
  } catch (err) {
2959
3720
  if (err instanceof TokenParseError) {
2960
3721
  errors.push({ code: err.code, path: err.path, message: err.message });
@@ -2995,7 +3756,7 @@ function outputValidationResult(filePath, errors, useJson) {
2995
3756
  }
2996
3757
  }
2997
3758
  function createTokensCommand() {
2998
- const tokensCmd = new Command5("tokens").description(
3759
+ const tokensCmd = new Command6("tokens").description(
2999
3760
  "Query and validate design tokens from a reactscope.tokens.json file"
3000
3761
  );
3001
3762
  registerGet2(tokensCmd);
@@ -3003,12 +3764,13 @@ function createTokensCommand() {
3003
3764
  registerSearch(tokensCmd);
3004
3765
  registerResolve(tokensCmd);
3005
3766
  registerValidate(tokensCmd);
3767
+ tokensCmd.addCommand(createTokensExportCommand());
3006
3768
  return tokensCmd;
3007
3769
  }
3008
3770
 
3009
3771
  // src/program.ts
3010
3772
  function createProgram(options = {}) {
3011
- const program2 = new Command6("scope").version(options.version ?? "0.1.0").description("Scope \u2014 React instrumentation toolkit");
3773
+ const program2 = new Command7("scope").version(options.version ?? "0.1.0").description("Scope \u2014 React instrumentation toolkit");
3012
3774
  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
3775
  async (url, opts) => {
3014
3776
  try {
@@ -3081,7 +3843,7 @@ function createProgram(options = {}) {
3081
3843
  }
3082
3844
  );
3083
3845
  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");
3846
+ const raw = readFileSync7(tracePath, "utf-8");
3085
3847
  const trace = loadTrace(raw);
3086
3848
  const source = generateTest(trace, {
3087
3849
  description: opts.description,