@agent-scope/cli 1.7.0 → 1.8.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
@@ -3,7 +3,7 @@
3
3
  // src/program.ts
4
4
  import { readFileSync as readFileSync4 } from "fs";
5
5
  import { generateTest, loadTrace } from "@agent-scope/playwright";
6
- import { Command as Command4 } from "commander";
6
+ import { Command as Command5 } from "commander";
7
7
 
8
8
  // src/browser.ts
9
9
  import { writeFileSync } from "fs";
@@ -50,6 +50,125 @@ function writeReportToFile(report, outputPath, pretty) {
50
50
  writeFileSync(outputPath, json, "utf-8");
51
51
  }
52
52
 
53
+ // src/instrument/renders.ts
54
+ import { resolve as resolve2 } from "path";
55
+ import { BrowserPool } from "@agent-scope/render";
56
+ import { Command as Command2 } from "commander";
57
+
58
+ // src/component-bundler.ts
59
+ import { dirname } from "path";
60
+ import * as esbuild from "esbuild";
61
+ async function buildComponentHarness(filePath, componentName, props, viewportWidth, projectCss) {
62
+ const bundledScript = await bundleComponentToIIFE(filePath, componentName, props);
63
+ return wrapInHtml(bundledScript, viewportWidth, projectCss);
64
+ }
65
+ async function bundleComponentToIIFE(filePath, componentName, props) {
66
+ const propsJson = JSON.stringify(props).replace(/<\/script>/gi, "<\\/script>");
67
+ const wrapperCode = (
68
+ /* ts */
69
+ `
70
+ import * as __scopeMod from ${JSON.stringify(filePath)};
71
+ import { createRoot } from "react-dom/client";
72
+ import { createElement } from "react";
73
+
74
+ (function scopeRenderHarness() {
75
+ var Component =
76
+ __scopeMod["default"] ||
77
+ __scopeMod[${JSON.stringify(componentName)}] ||
78
+ (Object.values(__scopeMod).find(
79
+ function(v) { return typeof v === "function" && /^[A-Z]/.test(v.name || ""); }
80
+ ));
81
+
82
+ if (!Component) {
83
+ window.__SCOPE_RENDER_ERROR__ =
84
+ "No renderable component found. Checked: default, " +
85
+ ${JSON.stringify(componentName)} + ", and PascalCase named exports. " +
86
+ "Available exports: " + Object.keys(__scopeMod).join(", ");
87
+ window.__SCOPE_RENDER_COMPLETE__ = true;
88
+ return;
89
+ }
90
+
91
+ try {
92
+ var props = ${propsJson};
93
+ var rootEl = document.getElementById("scope-root");
94
+ if (!rootEl) {
95
+ window.__SCOPE_RENDER_ERROR__ = "#scope-root element not found";
96
+ window.__SCOPE_RENDER_COMPLETE__ = true;
97
+ return;
98
+ }
99
+ createRoot(rootEl).render(createElement(Component, props));
100
+ // Use requestAnimationFrame to let React flush the render
101
+ requestAnimationFrame(function() {
102
+ window.__SCOPE_RENDER_COMPLETE__ = true;
103
+ });
104
+ } catch (err) {
105
+ window.__SCOPE_RENDER_ERROR__ = err instanceof Error ? err.message : String(err);
106
+ window.__SCOPE_RENDER_COMPLETE__ = true;
107
+ }
108
+ })();
109
+ `
110
+ );
111
+ const result = await esbuild.build({
112
+ stdin: {
113
+ contents: wrapperCode,
114
+ // Resolve relative imports (within the component's dir)
115
+ resolveDir: dirname(filePath),
116
+ loader: "tsx",
117
+ sourcefile: "__scope_harness__.tsx"
118
+ },
119
+ bundle: true,
120
+ format: "iife",
121
+ write: false,
122
+ platform: "browser",
123
+ jsx: "automatic",
124
+ jsxImportSource: "react",
125
+ target: "es2020",
126
+ // Bundle everything — no externals
127
+ external: [],
128
+ define: {
129
+ "process.env.NODE_ENV": '"development"',
130
+ global: "globalThis"
131
+ },
132
+ logLevel: "silent",
133
+ // Suppress "React must be in scope" warnings from old JSX (we use automatic)
134
+ banner: {
135
+ js: "/* @agent-scope/cli component harness */"
136
+ }
137
+ });
138
+ if (result.errors.length > 0) {
139
+ const msg = result.errors.map((e) => `${e.text}${e.location ? ` (${e.location.file}:${e.location.line})` : ""}`).join("\n");
140
+ throw new Error(`esbuild failed to bundle component:
141
+ ${msg}`);
142
+ }
143
+ const outputFile = result.outputFiles?.[0];
144
+ if (outputFile === void 0 || outputFile.text.length === 0) {
145
+ throw new Error("esbuild produced no output");
146
+ }
147
+ return outputFile.text;
148
+ }
149
+ function wrapInHtml(bundledScript, viewportWidth, projectCss) {
150
+ const projectStyleBlock = projectCss != null && projectCss.length > 0 ? `<style id="scope-project-css">
151
+ ${projectCss.replace(/<\/style>/gi, "<\\/style>")}
152
+ </style>` : "";
153
+ return `<!DOCTYPE html>
154
+ <html lang="en">
155
+ <head>
156
+ <meta charset="UTF-8" />
157
+ <meta name="viewport" content="width=${viewportWidth}, initial-scale=1.0" />
158
+ <style>
159
+ *, *::before, *::after { box-sizing: border-box; }
160
+ html, body { margin: 0; padding: 0; background: #fff; font-family: system-ui, sans-serif; }
161
+ #scope-root { display: inline-block; min-width: 1px; min-height: 1px; }
162
+ </style>
163
+ ${projectStyleBlock}
164
+ </head>
165
+ <body>
166
+ <div id="scope-root" data-reactscope-root></div>
167
+ <script>${bundledScript}</script>
168
+ </body>
169
+ </html>`;
170
+ }
171
+
53
172
  // src/manifest-commands.ts
54
173
  import { existsSync, mkdirSync, readFileSync, writeFileSync as writeFileSync2 } from "fs";
55
174
  import { resolve } from "path";
@@ -323,135 +442,6 @@ function createManifestCommand() {
323
442
  return manifestCmd;
324
443
  }
325
444
 
326
- // src/render-commands.ts
327
- import { mkdirSync as mkdirSync2, writeFileSync as writeFileSync3 } from "fs";
328
- import { resolve as resolve3 } from "path";
329
- import {
330
- ALL_CONTEXT_IDS,
331
- ALL_STRESS_IDS,
332
- BrowserPool,
333
- contextAxis,
334
- RenderMatrix,
335
- SatoriRenderer,
336
- safeRender,
337
- stressAxis
338
- } from "@agent-scope/render";
339
- import { Command as Command2 } from "commander";
340
-
341
- // src/component-bundler.ts
342
- import { dirname } from "path";
343
- import * as esbuild from "esbuild";
344
- async function buildComponentHarness(filePath, componentName, props, viewportWidth, projectCss) {
345
- const bundledScript = await bundleComponentToIIFE(filePath, componentName, props);
346
- return wrapInHtml(bundledScript, viewportWidth, projectCss);
347
- }
348
- async function bundleComponentToIIFE(filePath, componentName, props) {
349
- const propsJson = JSON.stringify(props).replace(/<\/script>/gi, "<\\/script>");
350
- const wrapperCode = (
351
- /* ts */
352
- `
353
- import * as __scopeMod from ${JSON.stringify(filePath)};
354
- import { createRoot } from "react-dom/client";
355
- import { createElement } from "react";
356
-
357
- (function scopeRenderHarness() {
358
- var Component =
359
- __scopeMod["default"] ||
360
- __scopeMod[${JSON.stringify(componentName)}] ||
361
- (Object.values(__scopeMod).find(
362
- function(v) { return typeof v === "function" && /^[A-Z]/.test(v.name || ""); }
363
- ));
364
-
365
- if (!Component) {
366
- window.__SCOPE_RENDER_ERROR__ =
367
- "No renderable component found. Checked: default, " +
368
- ${JSON.stringify(componentName)} + ", and PascalCase named exports. " +
369
- "Available exports: " + Object.keys(__scopeMod).join(", ");
370
- window.__SCOPE_RENDER_COMPLETE__ = true;
371
- return;
372
- }
373
-
374
- try {
375
- var props = ${propsJson};
376
- var rootEl = document.getElementById("scope-root");
377
- if (!rootEl) {
378
- window.__SCOPE_RENDER_ERROR__ = "#scope-root element not found";
379
- window.__SCOPE_RENDER_COMPLETE__ = true;
380
- return;
381
- }
382
- createRoot(rootEl).render(createElement(Component, props));
383
- // Use requestAnimationFrame to let React flush the render
384
- requestAnimationFrame(function() {
385
- window.__SCOPE_RENDER_COMPLETE__ = true;
386
- });
387
- } catch (err) {
388
- window.__SCOPE_RENDER_ERROR__ = err instanceof Error ? err.message : String(err);
389
- window.__SCOPE_RENDER_COMPLETE__ = true;
390
- }
391
- })();
392
- `
393
- );
394
- const result = await esbuild.build({
395
- stdin: {
396
- contents: wrapperCode,
397
- // Resolve relative imports (within the component's dir)
398
- resolveDir: dirname(filePath),
399
- loader: "tsx",
400
- sourcefile: "__scope_harness__.tsx"
401
- },
402
- bundle: true,
403
- format: "iife",
404
- write: false,
405
- platform: "browser",
406
- jsx: "automatic",
407
- jsxImportSource: "react",
408
- target: "es2020",
409
- // Bundle everything — no externals
410
- external: [],
411
- define: {
412
- "process.env.NODE_ENV": '"development"',
413
- global: "globalThis"
414
- },
415
- logLevel: "silent",
416
- // Suppress "React must be in scope" warnings from old JSX (we use automatic)
417
- banner: {
418
- js: "/* @agent-scope/cli component harness */"
419
- }
420
- });
421
- if (result.errors.length > 0) {
422
- const msg = result.errors.map((e) => `${e.text}${e.location ? ` (${e.location.file}:${e.location.line})` : ""}`).join("\n");
423
- throw new Error(`esbuild failed to bundle component:
424
- ${msg}`);
425
- }
426
- const outputFile = result.outputFiles?.[0];
427
- if (outputFile === void 0 || outputFile.text.length === 0) {
428
- throw new Error("esbuild produced no output");
429
- }
430
- return outputFile.text;
431
- }
432
- function wrapInHtml(bundledScript, viewportWidth, projectCss) {
433
- const projectStyleBlock = projectCss != null && projectCss.length > 0 ? `<style id="scope-project-css">
434
- ${projectCss.replace(/<\/style>/gi, "<\\/style>")}
435
- </style>` : "";
436
- return `<!DOCTYPE html>
437
- <html lang="en">
438
- <head>
439
- <meta charset="UTF-8" />
440
- <meta name="viewport" content="width=${viewportWidth}, initial-scale=1.0" />
441
- <style>
442
- *, *::before, *::after { box-sizing: border-box; }
443
- html, body { margin: 0; padding: 0; background: #fff; font-family: system-ui, sans-serif; }
444
- #scope-root { display: inline-block; min-width: 1px; min-height: 1px; }
445
- </style>
446
- ${projectStyleBlock}
447
- </head>
448
- <body>
449
- <div id="scope-root" data-reactscope-root></div>
450
- <script>${bundledScript}</script>
451
- </body>
452
- </html>`;
453
- }
454
-
455
445
  // src/render-formatter.ts
456
446
  function parseViewport(spec) {
457
447
  const lower = spec.toLowerCase();
@@ -612,10 +602,492 @@ function csvEscape(value) {
612
602
  return value;
613
603
  }
614
604
 
605
+ // src/instrument/renders.ts
606
+ var MANIFEST_PATH2 = ".reactscope/manifest.json";
607
+ function determineTrigger(event) {
608
+ if (event.forceUpdate) return "force_update";
609
+ if (event.stateChanged) return "state_change";
610
+ if (event.propsChanged) return "props_change";
611
+ if (event.contextChanged) return "context_change";
612
+ if (event.hookDepsChanged) return "hook_dependency";
613
+ return "parent_rerender";
614
+ }
615
+ function isWastedRender(event) {
616
+ return !event.propsChanged && !event.stateChanged && !event.contextChanged && !event.memoized;
617
+ }
618
+ function buildCausalityChains(rawEvents) {
619
+ const result = [];
620
+ const componentLastRender = /* @__PURE__ */ new Map();
621
+ for (const raw of rawEvents) {
622
+ const trigger = determineTrigger(raw);
623
+ const wasted = isWastedRender(raw);
624
+ const chain = [];
625
+ let current = raw;
626
+ const visited = /* @__PURE__ */ new Set();
627
+ while (true) {
628
+ if (visited.has(current.component)) break;
629
+ visited.add(current.component);
630
+ const currentTrigger = determineTrigger(current);
631
+ chain.unshift({
632
+ component: current.component,
633
+ trigger: currentTrigger,
634
+ propsChanged: current.propsChanged,
635
+ stateChanged: current.stateChanged,
636
+ contextChanged: current.contextChanged
637
+ });
638
+ if (currentTrigger !== "parent_rerender" || current.parentComponent === null) {
639
+ break;
640
+ }
641
+ const parentEvent = componentLastRender.get(current.parentComponent);
642
+ if (parentEvent === void 0) break;
643
+ current = parentEvent;
644
+ }
645
+ const rootCause = chain[0];
646
+ const cascadeRenders = rawEvents.filter((e) => {
647
+ if (rootCause === void 0) return false;
648
+ return e.component !== rootCause.component && !e.stateChanged && !e.propsChanged;
649
+ });
650
+ result.push({
651
+ component: raw.component,
652
+ renderIndex: raw.renderIndex,
653
+ trigger,
654
+ propsChanged: raw.propsChanged,
655
+ stateChanged: raw.stateChanged,
656
+ contextChanged: raw.contextChanged,
657
+ memoized: raw.memoized,
658
+ wasted,
659
+ chain,
660
+ cascade: {
661
+ totalRendersTriggered: cascadeRenders.length,
662
+ uniqueComponents: new Set(cascadeRenders.map((e) => e.component)).size,
663
+ unchangedPropRenders: cascadeRenders.filter((e) => !e.propsChanged).length
664
+ }
665
+ });
666
+ componentLastRender.set(raw.component, raw);
667
+ }
668
+ return result;
669
+ }
670
+ function applyHeuristicFlags(renders) {
671
+ const flags = [];
672
+ const byComponent = /* @__PURE__ */ new Map();
673
+ for (const r of renders) {
674
+ if (!byComponent.has(r.component)) byComponent.set(r.component, []);
675
+ byComponent.get(r.component).push(r);
676
+ }
677
+ for (const [component, events] of byComponent) {
678
+ const wastedCount = events.filter((e) => e.wasted).length;
679
+ const totalCount = events.length;
680
+ if (wastedCount > 0) {
681
+ flags.push({
682
+ id: "WASTED_RENDER",
683
+ severity: "warning",
684
+ component,
685
+ detail: `${wastedCount}/${totalCount} renders were wasted \u2014 unchanged props/state/context, not memoized`,
686
+ data: { wastedCount, totalCount, wastedRatio: wastedCount / totalCount }
687
+ });
688
+ }
689
+ for (const event of events) {
690
+ if (event.cascade.totalRendersTriggered > 50) {
691
+ flags.push({
692
+ id: "RENDER_CASCADE",
693
+ severity: "warning",
694
+ component,
695
+ detail: `State change in ${component} triggered ${event.cascade.totalRendersTriggered} downstream re-renders`,
696
+ data: {
697
+ totalRendersTriggered: event.cascade.totalRendersTriggered,
698
+ uniqueComponents: event.cascade.uniqueComponents,
699
+ unchangedPropRenders: event.cascade.unchangedPropRenders
700
+ }
701
+ });
702
+ break;
703
+ }
704
+ }
705
+ }
706
+ return flags;
707
+ }
708
+ function buildInstrumentationScript() {
709
+ return (
710
+ /* js */
711
+ `
712
+ (function installScopeRenderInstrumentation() {
713
+ window.__SCOPE_RENDER_EVENTS__ = [];
714
+ window.__SCOPE_RENDER_INDEX__ = 0;
715
+
716
+ var hook = window.__REACT_DEVTOOLS_GLOBAL_HOOK__;
717
+ if (!hook) return;
718
+
719
+ var originalOnCommit = hook.onCommitFiberRoot;
720
+ var renderedComponents = new Map(); // componentName -> { lastProps, lastState }
721
+
722
+ function extractName(fiber) {
723
+ if (!fiber) return 'Unknown';
724
+ var type = fiber.type;
725
+ if (!type) return 'Unknown';
726
+ if (typeof type === 'string') return type; // host element
727
+ if (typeof type === 'function') return type.displayName || type.name || 'Anonymous';
728
+ if (type.displayName) return type.displayName;
729
+ if (type.render && typeof type.render === 'function') {
730
+ return type.render.displayName || type.render.name || 'Anonymous';
731
+ }
732
+ return 'Anonymous';
733
+ }
734
+
735
+ function isMemoized(fiber) {
736
+ // MemoComponent = 14, SimpleMemoComponent = 15
737
+ return fiber.tag === 14 || fiber.tag === 15;
738
+ }
739
+
740
+ function isComponent(fiber) {
741
+ // FunctionComponent=0, ClassComponent=1, MemoComponent=14, SimpleMemoComponent=15
742
+ return fiber.tag === 0 || fiber.tag === 1 || fiber.tag === 14 || fiber.tag === 15;
743
+ }
744
+
745
+ function shallowEqual(a, b) {
746
+ if (a === b) return true;
747
+ if (!a || !b) return a === b;
748
+ var keysA = Object.keys(a);
749
+ var keysB = Object.keys(b);
750
+ if (keysA.length !== keysB.length) return false;
751
+ for (var i = 0; i < keysA.length; i++) {
752
+ var k = keysA[i];
753
+ if (k === 'children') continue; // ignore children prop
754
+ if (a[k] !== b[k]) return false;
755
+ }
756
+ return true;
757
+ }
758
+
759
+ function getParentComponentName(fiber) {
760
+ var parent = fiber.return;
761
+ while (parent) {
762
+ if (isComponent(parent)) {
763
+ var name = extractName(parent);
764
+ if (name && name !== 'Unknown' && !/^[a-z]/.test(name)) return name;
765
+ }
766
+ parent = parent.return;
767
+ }
768
+ return null;
769
+ }
770
+
771
+ function walkCommit(fiber) {
772
+ if (!fiber) return;
773
+
774
+ if (isComponent(fiber)) {
775
+ var name = extractName(fiber);
776
+ if (name && name !== 'Unknown' && !/^[a-z]/.test(name)) {
777
+ var memoized = isMemoized(fiber);
778
+ var currentProps = fiber.memoizedProps || {};
779
+ var prev = renderedComponents.get(name);
780
+
781
+ var propsChanged = true;
782
+ var stateChanged = false;
783
+ var contextChanged = false;
784
+ var hookDepsChanged = false;
785
+ var forceUpdate = false;
786
+
787
+ if (prev) {
788
+ propsChanged = !shallowEqual(prev.lastProps, currentProps);
789
+ }
790
+
791
+ // State: check memoizedState chain
792
+ var memoizedState = fiber.memoizedState;
793
+ if (prev && prev.lastStateSerialized !== undefined) {
794
+ try {
795
+ var stateSig = JSON.stringify(memoizedState ? memoizedState.memoizedState : null);
796
+ stateChanged = stateSig !== prev.lastStateSerialized;
797
+ } catch (_) {
798
+ stateChanged = false;
799
+ }
800
+ }
801
+
802
+ // Context: use _debugHookTypes or check dependencies
803
+ var deps = fiber.dependencies;
804
+ if (deps && deps.firstContext) {
805
+ contextChanged = true; // conservative: context dep present = may have changed
806
+ }
807
+
808
+ var stateSig;
809
+ try {
810
+ stateSig = JSON.stringify(memoizedState ? memoizedState.memoizedState : null);
811
+ } catch (_) {
812
+ stateSig = null;
813
+ }
814
+
815
+ renderedComponents.set(name, {
816
+ lastProps: currentProps,
817
+ lastStateSerialized: stateSig,
818
+ });
819
+
820
+ var parentName = getParentComponentName(fiber);
821
+
822
+ window.__SCOPE_RENDER_EVENTS__.push({
823
+ component: name,
824
+ renderIndex: window.__SCOPE_RENDER_INDEX__++,
825
+ propsChanged: prev ? propsChanged : false,
826
+ stateChanged: stateChanged,
827
+ contextChanged: contextChanged,
828
+ memoized: memoized,
829
+ parentComponent: parentName,
830
+ hookDepsChanged: hookDepsChanged,
831
+ forceUpdate: forceUpdate,
832
+ });
833
+ }
834
+ }
835
+
836
+ walkCommit(fiber.child);
837
+ walkCommit(fiber.sibling);
838
+ }
839
+
840
+ hook.onCommitFiberRoot = function(rendererID, root, priorityLevel) {
841
+ if (typeof originalOnCommit === 'function') {
842
+ originalOnCommit.call(hook, rendererID, root, priorityLevel);
843
+ }
844
+ var wipRoot = root && root.current && root.current.alternate;
845
+ if (wipRoot) walkCommit(wipRoot);
846
+ };
847
+ })();
848
+ `
849
+ );
850
+ }
851
+ async function replayInteraction(page, steps) {
852
+ for (const step of steps) {
853
+ switch (step.action) {
854
+ case "click":
855
+ if (step.target !== void 0) {
856
+ await page.click(step.target, { timeout: 5e3 }).catch(() => {
857
+ });
858
+ }
859
+ break;
860
+ case "type":
861
+ if (step.target !== void 0 && step.text !== void 0) {
862
+ await page.fill(step.target, step.text, { timeout: 5e3 }).catch(async () => {
863
+ await page.type(step.target, step.text, { timeout: 5e3 }).catch(() => {
864
+ });
865
+ });
866
+ }
867
+ break;
868
+ case "hover":
869
+ if (step.target !== void 0) {
870
+ await page.hover(step.target, { timeout: 5e3 }).catch(() => {
871
+ });
872
+ }
873
+ break;
874
+ case "blur":
875
+ if (step.target !== void 0) {
876
+ await page.locator(step.target).blur({ timeout: 5e3 }).catch(() => {
877
+ });
878
+ }
879
+ break;
880
+ case "focus":
881
+ if (step.target !== void 0) {
882
+ await page.focus(step.target, { timeout: 5e3 }).catch(() => {
883
+ });
884
+ }
885
+ break;
886
+ case "scroll":
887
+ if (step.target !== void 0) {
888
+ await page.locator(step.target).scrollIntoViewIfNeeded({ timeout: 5e3 }).catch(() => {
889
+ });
890
+ }
891
+ break;
892
+ case "wait": {
893
+ const timeout = step.timeout ?? 1e3;
894
+ if (step.condition === "idle") {
895
+ await page.waitForLoadState("networkidle", { timeout }).catch(() => {
896
+ });
897
+ } else {
898
+ await page.waitForTimeout(timeout);
899
+ }
900
+ break;
901
+ }
902
+ default:
903
+ break;
904
+ }
905
+ }
906
+ }
907
+ var _pool = null;
908
+ async function getPool() {
909
+ if (_pool === null) {
910
+ _pool = new BrowserPool({
911
+ size: { browsers: 1, pagesPerBrowser: 2 },
912
+ viewportWidth: 1280,
913
+ viewportHeight: 800
914
+ });
915
+ await _pool.init();
916
+ }
917
+ return _pool;
918
+ }
919
+ async function shutdownPool() {
920
+ if (_pool !== null) {
921
+ await _pool.close();
922
+ _pool = null;
923
+ }
924
+ }
925
+ async function analyzeRenders(options) {
926
+ const manifestPath = options.manifestPath ?? MANIFEST_PATH2;
927
+ const manifest = loadManifest(manifestPath);
928
+ const descriptor = manifest.components[options.componentName];
929
+ if (descriptor === void 0) {
930
+ const available = Object.keys(manifest.components).slice(0, 5).join(", ");
931
+ throw new Error(
932
+ `Component "${options.componentName}" not found in manifest.
933
+ Available: ${available}`
934
+ );
935
+ }
936
+ const rootDir = process.cwd();
937
+ const filePath = resolve2(rootDir, descriptor.filePath);
938
+ const htmlHarness = await buildComponentHarness(filePath, options.componentName, {}, 1280);
939
+ const pool = await getPool();
940
+ const slot = await pool.acquire();
941
+ const { page } = slot;
942
+ const startMs = performance.now();
943
+ try {
944
+ await page.addInitScript(buildInstrumentationScript());
945
+ await page.setContent(htmlHarness, { waitUntil: "load" });
946
+ await page.waitForFunction(
947
+ () => window.__SCOPE_RENDER_COMPLETE__ === true,
948
+ { timeout: 15e3 }
949
+ );
950
+ await page.waitForTimeout(100);
951
+ await page.evaluate(() => {
952
+ window.__SCOPE_RENDER_EVENTS__ = [];
953
+ window.__SCOPE_RENDER_INDEX__ = 0;
954
+ });
955
+ await replayInteraction(page, options.interaction);
956
+ await page.waitForTimeout(200);
957
+ const interactionDurationMs = performance.now() - startMs;
958
+ const rawEvents = await page.evaluate(() => {
959
+ return window.__SCOPE_RENDER_EVENTS__ ?? [];
960
+ });
961
+ const renders = buildCausalityChains(rawEvents);
962
+ const flags = applyHeuristicFlags(renders);
963
+ const uniqueComponents = new Set(renders.map((r) => r.component)).size;
964
+ const wastedRenders = renders.filter((r) => r.wasted).length;
965
+ return {
966
+ component: options.componentName,
967
+ interaction: options.interaction,
968
+ summary: {
969
+ totalRenders: renders.length,
970
+ uniqueComponents,
971
+ wastedRenders,
972
+ interactionDurationMs: Math.round(interactionDurationMs)
973
+ },
974
+ renders,
975
+ flags
976
+ };
977
+ } finally {
978
+ pool.release(slot);
979
+ }
980
+ }
981
+ function formatRendersTable(result) {
982
+ const lines = [];
983
+ lines.push(`
984
+ \u{1F50D} Re-render Analysis: ${result.component}`);
985
+ lines.push(`${"\u2500".repeat(60)}`);
986
+ lines.push(`Total renders: ${result.summary.totalRenders}`);
987
+ lines.push(`Unique components: ${result.summary.uniqueComponents}`);
988
+ lines.push(`Wasted renders: ${result.summary.wastedRenders}`);
989
+ lines.push(`Duration: ${result.summary.interactionDurationMs}ms`);
990
+ lines.push("");
991
+ if (result.renders.length === 0) {
992
+ lines.push("No re-renders captured during interaction.");
993
+ } else {
994
+ lines.push("Re-renders:");
995
+ lines.push(
996
+ `${"#".padEnd(4)} ${"Component".padEnd(30)} ${"Trigger".padEnd(18)} ${"Wasted".padEnd(7)} ${"Chain Depth"}`
997
+ );
998
+ lines.push("\u2500".repeat(80));
999
+ for (const r of result.renders) {
1000
+ const wasted = r.wasted ? "\u26A0 yes" : "no";
1001
+ const idx = String(r.renderIndex).padEnd(4);
1002
+ const comp = r.component.slice(0, 29).padEnd(30);
1003
+ const trig = r.trigger.padEnd(18);
1004
+ const w = wasted.padEnd(7);
1005
+ const depth = r.chain.length;
1006
+ lines.push(`${idx} ${comp} ${trig} ${w} ${depth}`);
1007
+ }
1008
+ }
1009
+ if (result.flags.length > 0) {
1010
+ lines.push("");
1011
+ lines.push("Flags:");
1012
+ for (const flag of result.flags) {
1013
+ const icon = flag.severity === "error" ? "\u2717" : flag.severity === "warning" ? "\u26A0" : "\u2139";
1014
+ lines.push(` ${icon} [${flag.id}] ${flag.component}: ${flag.detail}`);
1015
+ }
1016
+ }
1017
+ return lines.join("\n");
1018
+ }
1019
+ function createInstrumentRendersCommand() {
1020
+ return new Command2("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(
1021
+ "--interaction <json>",
1022
+ `Interaction sequence JSON, e.g. '[{"action":"click","target":"button"}]'`,
1023
+ "[]"
1024
+ ).option("--json", "Output as JSON regardless of TTY", false).option("--manifest <path>", "Path to manifest.json", MANIFEST_PATH2).action(
1025
+ async (componentName, opts) => {
1026
+ let interaction = [];
1027
+ try {
1028
+ interaction = JSON.parse(opts.interaction);
1029
+ if (!Array.isArray(interaction)) {
1030
+ throw new Error("Interaction must be a JSON array");
1031
+ }
1032
+ } catch {
1033
+ process.stderr.write(`Error: Invalid --interaction JSON: ${opts.interaction}
1034
+ `);
1035
+ process.exit(1);
1036
+ }
1037
+ try {
1038
+ process.stderr.write(
1039
+ `Instrumenting ${componentName} (${interaction.length} interaction steps)\u2026
1040
+ `
1041
+ );
1042
+ const result = await analyzeRenders({
1043
+ componentName,
1044
+ interaction,
1045
+ manifestPath: opts.manifest
1046
+ });
1047
+ await shutdownPool();
1048
+ if (opts.json || !isTTY()) {
1049
+ process.stdout.write(`${JSON.stringify(result, null, 2)}
1050
+ `);
1051
+ } else {
1052
+ process.stdout.write(`${formatRendersTable(result)}
1053
+ `);
1054
+ }
1055
+ } catch (err) {
1056
+ await shutdownPool();
1057
+ process.stderr.write(`Error: ${err instanceof Error ? err.message : String(err)}
1058
+ `);
1059
+ process.exit(1);
1060
+ }
1061
+ }
1062
+ );
1063
+ }
1064
+ function createInstrumentCommand() {
1065
+ const instrumentCmd = new Command2("instrument").description(
1066
+ "Structured instrumentation commands for React component analysis"
1067
+ );
1068
+ instrumentCmd.addCommand(createInstrumentRendersCommand());
1069
+ return instrumentCmd;
1070
+ }
1071
+
1072
+ // src/render-commands.ts
1073
+ import { mkdirSync as mkdirSync2, writeFileSync as writeFileSync3 } from "fs";
1074
+ import { resolve as resolve4 } from "path";
1075
+ import {
1076
+ ALL_CONTEXT_IDS,
1077
+ ALL_STRESS_IDS,
1078
+ BrowserPool as BrowserPool2,
1079
+ contextAxis,
1080
+ RenderMatrix,
1081
+ SatoriRenderer,
1082
+ safeRender,
1083
+ stressAxis
1084
+ } from "@agent-scope/render";
1085
+ import { Command as Command3 } from "commander";
1086
+
615
1087
  // src/tailwind-css.ts
616
1088
  import { existsSync as existsSync2, readFileSync as readFileSync2 } from "fs";
617
1089
  import { createRequire } from "module";
618
- import { resolve as resolve2 } from "path";
1090
+ import { resolve as resolve3 } from "path";
619
1091
  var CONFIG_FILENAMES = [
620
1092
  ".reactscope/config.json",
621
1093
  ".reactscope/config.js",
@@ -632,14 +1104,14 @@ var STYLE_ENTRY_CANDIDATES = [
632
1104
  var TAILWIND_IMPORT = /@import\s+["']tailwindcss["']\s*;?/;
633
1105
  var compilerCache = null;
634
1106
  function getCachedBuild(cwd) {
635
- if (compilerCache !== null && resolve2(compilerCache.cwd) === resolve2(cwd)) {
1107
+ if (compilerCache !== null && resolve3(compilerCache.cwd) === resolve3(cwd)) {
636
1108
  return compilerCache.build;
637
1109
  }
638
1110
  return null;
639
1111
  }
640
1112
  function findStylesEntry(cwd) {
641
1113
  for (const name of CONFIG_FILENAMES) {
642
- const p = resolve2(cwd, name);
1114
+ const p = resolve3(cwd, name);
643
1115
  if (!existsSync2(p)) continue;
644
1116
  try {
645
1117
  if (name.endsWith(".json")) {
@@ -648,28 +1120,28 @@ function findStylesEntry(cwd) {
648
1120
  const scope = data.scope;
649
1121
  const entry = scope?.stylesEntry ?? data.stylesEntry;
650
1122
  if (typeof entry === "string") {
651
- const full = resolve2(cwd, entry);
1123
+ const full = resolve3(cwd, entry);
652
1124
  if (existsSync2(full)) return full;
653
1125
  }
654
1126
  }
655
1127
  } catch {
656
1128
  }
657
1129
  }
658
- const pkgPath = resolve2(cwd, "package.json");
1130
+ const pkgPath = resolve3(cwd, "package.json");
659
1131
  if (existsSync2(pkgPath)) {
660
1132
  try {
661
1133
  const raw = readFileSync2(pkgPath, "utf-8");
662
1134
  const pkg = JSON.parse(raw);
663
1135
  const entry = pkg.scope?.stylesEntry;
664
1136
  if (typeof entry === "string") {
665
- const full = resolve2(cwd, entry);
1137
+ const full = resolve3(cwd, entry);
666
1138
  if (existsSync2(full)) return full;
667
1139
  }
668
1140
  } catch {
669
1141
  }
670
1142
  }
671
1143
  for (const candidate of STYLE_ENTRY_CANDIDATES) {
672
- const full = resolve2(cwd, candidate);
1144
+ const full = resolve3(cwd, candidate);
673
1145
  if (existsSync2(full)) {
674
1146
  try {
675
1147
  const content = readFileSync2(full, "utf-8");
@@ -687,7 +1159,7 @@ async function getTailwindCompiler(cwd) {
687
1159
  if (entryPath === null) return null;
688
1160
  let compile;
689
1161
  try {
690
- const require2 = createRequire(resolve2(cwd, "package.json"));
1162
+ const require2 = createRequire(resolve3(cwd, "package.json"));
691
1163
  const tailwind = require2("tailwindcss");
692
1164
  const fn = tailwind.compile;
693
1165
  if (typeof fn !== "function") return null;
@@ -698,8 +1170,8 @@ async function getTailwindCompiler(cwd) {
698
1170
  const entryContent = readFileSync2(entryPath, "utf-8");
699
1171
  const loadStylesheet = async (id, base) => {
700
1172
  if (id === "tailwindcss") {
701
- const nodeModules = resolve2(cwd, "node_modules");
702
- const tailwindCssPath = resolve2(nodeModules, "tailwindcss", "index.css");
1173
+ const nodeModules = resolve3(cwd, "node_modules");
1174
+ const tailwindCssPath = resolve3(nodeModules, "tailwindcss", "index.css");
703
1175
  if (!existsSync2(tailwindCssPath)) {
704
1176
  throw new Error(
705
1177
  `Tailwind v4: tailwindcss package not found at ${tailwindCssPath}. Install with: npm install tailwindcss`
@@ -708,10 +1180,10 @@ async function getTailwindCompiler(cwd) {
708
1180
  const content = readFileSync2(tailwindCssPath, "utf-8");
709
1181
  return { path: "virtual:tailwindcss/index.css", base, content };
710
1182
  }
711
- const full = resolve2(base, id);
1183
+ const full = resolve3(base, id);
712
1184
  if (existsSync2(full)) {
713
1185
  const content = readFileSync2(full, "utf-8");
714
- return { path: full, base: resolve2(full, ".."), content };
1186
+ return { path: full, base: resolve3(full, ".."), content };
715
1187
  }
716
1188
  throw new Error(`Tailwind v4: could not load stylesheet: ${id} (base: ${base})`);
717
1189
  };
@@ -733,24 +1205,24 @@ async function getCompiledCssForClasses(cwd, classes) {
733
1205
  }
734
1206
 
735
1207
  // src/render-commands.ts
736
- var MANIFEST_PATH2 = ".reactscope/manifest.json";
1208
+ var MANIFEST_PATH3 = ".reactscope/manifest.json";
737
1209
  var DEFAULT_OUTPUT_DIR = ".reactscope/renders";
738
- var _pool = null;
739
- async function getPool(viewportWidth, viewportHeight) {
740
- if (_pool === null) {
741
- _pool = new BrowserPool({
1210
+ var _pool2 = null;
1211
+ async function getPool2(viewportWidth, viewportHeight) {
1212
+ if (_pool2 === null) {
1213
+ _pool2 = new BrowserPool2({
742
1214
  size: { browsers: 1, pagesPerBrowser: 4 },
743
1215
  viewportWidth,
744
1216
  viewportHeight
745
1217
  });
746
- await _pool.init();
1218
+ await _pool2.init();
747
1219
  }
748
- return _pool;
1220
+ return _pool2;
749
1221
  }
750
- async function shutdownPool() {
751
- if (_pool !== null) {
752
- await _pool.close();
753
- _pool = null;
1222
+ async function shutdownPool2() {
1223
+ if (_pool2 !== null) {
1224
+ await _pool2.close();
1225
+ _pool2 = null;
754
1226
  }
755
1227
  }
756
1228
  function buildRenderer(filePath, componentName, viewportWidth, viewportHeight) {
@@ -761,7 +1233,7 @@ function buildRenderer(filePath, componentName, viewportWidth, viewportHeight) {
761
1233
  _satori: satori,
762
1234
  async renderCell(props, _complexityClass) {
763
1235
  const startMs = performance.now();
764
- const pool = await getPool(viewportWidth, viewportHeight);
1236
+ const pool = await getPool2(viewportWidth, viewportHeight);
765
1237
  const htmlHarness = await buildComponentHarness(
766
1238
  filePath,
767
1239
  componentName,
@@ -858,7 +1330,7 @@ function buildRenderer(filePath, componentName, viewportWidth, viewportHeight) {
858
1330
  };
859
1331
  }
860
1332
  function registerRenderSingle(renderCmd) {
861
- 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_PATH2).action(
1333
+ 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(
862
1334
  async (componentName, opts) => {
863
1335
  try {
864
1336
  const manifest = loadManifest(opts.manifest);
@@ -880,7 +1352,7 @@ Available: ${available}`
880
1352
  }
881
1353
  const { width, height } = parseViewport(opts.viewport);
882
1354
  const rootDir = process.cwd();
883
- const filePath = resolve3(rootDir, descriptor.filePath);
1355
+ const filePath = resolve4(rootDir, descriptor.filePath);
884
1356
  const renderer = buildRenderer(filePath, componentName, width, height);
885
1357
  process.stderr.write(
886
1358
  `Rendering ${componentName} [${descriptor.complexityClass}] at ${width}\xD7${height}\u2026
@@ -897,7 +1369,7 @@ Available: ${available}`
897
1369
  }
898
1370
  }
899
1371
  );
900
- await shutdownPool();
1372
+ await shutdownPool2();
901
1373
  if (outcome.crashed) {
902
1374
  process.stderr.write(`\u2717 Render failed: ${outcome.error.message}
903
1375
  `);
@@ -910,7 +1382,7 @@ Available: ${available}`
910
1382
  }
911
1383
  const result = outcome.result;
912
1384
  if (opts.output !== void 0) {
913
- const outPath = resolve3(process.cwd(), opts.output);
1385
+ const outPath = resolve4(process.cwd(), opts.output);
914
1386
  writeFileSync3(outPath, result.screenshot);
915
1387
  process.stdout.write(
916
1388
  `\u2713 ${componentName} \u2192 ${opts.output} (${result.width}\xD7${result.height}, ${result.renderTimeMs.toFixed(0)}ms)
@@ -924,9 +1396,9 @@ Available: ${available}`
924
1396
  process.stdout.write(`${JSON.stringify(json, null, 2)}
925
1397
  `);
926
1398
  } else if (fmt === "file") {
927
- const dir = resolve3(process.cwd(), DEFAULT_OUTPUT_DIR);
1399
+ const dir = resolve4(process.cwd(), DEFAULT_OUTPUT_DIR);
928
1400
  mkdirSync2(dir, { recursive: true });
929
- const outPath = resolve3(dir, `${componentName}.png`);
1401
+ const outPath = resolve4(dir, `${componentName}.png`);
930
1402
  writeFileSync3(outPath, result.screenshot);
931
1403
  const relPath = `${DEFAULT_OUTPUT_DIR}/${componentName}.png`;
932
1404
  process.stdout.write(
@@ -934,9 +1406,9 @@ Available: ${available}`
934
1406
  `
935
1407
  );
936
1408
  } else {
937
- const dir = resolve3(process.cwd(), DEFAULT_OUTPUT_DIR);
1409
+ const dir = resolve4(process.cwd(), DEFAULT_OUTPUT_DIR);
938
1410
  mkdirSync2(dir, { recursive: true });
939
- const outPath = resolve3(dir, `${componentName}.png`);
1411
+ const outPath = resolve4(dir, `${componentName}.png`);
940
1412
  writeFileSync3(outPath, result.screenshot);
941
1413
  const relPath = `${DEFAULT_OUTPUT_DIR}/${componentName}.png`;
942
1414
  process.stdout.write(
@@ -945,7 +1417,7 @@ Available: ${available}`
945
1417
  );
946
1418
  }
947
1419
  } catch (err) {
948
- await shutdownPool();
1420
+ await shutdownPool2();
949
1421
  process.stderr.write(`Error: ${err instanceof Error ? err.message : String(err)}
950
1422
  `);
951
1423
  process.exit(1);
@@ -957,7 +1429,7 @@ function registerRenderMatrix(renderCmd) {
957
1429
  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(
958
1430
  "--contexts <ids>",
959
1431
  "Composition context IDs, comma-separated (e.g. centered,rtl,sidebar)"
960
- ).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_PATH2).action(
1432
+ ).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(
961
1433
  async (componentName, opts) => {
962
1434
  try {
963
1435
  const manifest = loadManifest(opts.manifest);
@@ -972,7 +1444,7 @@ Available: ${available}`
972
1444
  const concurrency = Math.max(1, parseInt(opts.concurrency, 10) || 8);
973
1445
  const { width, height } = { width: 375, height: 812 };
974
1446
  const rootDir = process.cwd();
975
- const filePath = resolve3(rootDir, descriptor.filePath);
1447
+ const filePath = resolve4(rootDir, descriptor.filePath);
976
1448
  const renderer = buildRenderer(filePath, componentName, width, height);
977
1449
  const axes = [];
978
1450
  if (opts.axes !== void 0) {
@@ -1030,7 +1502,7 @@ Available: ${available}`
1030
1502
  concurrency
1031
1503
  });
1032
1504
  const result = await matrix.render();
1033
- await shutdownPool();
1505
+ await shutdownPool2();
1034
1506
  process.stderr.write(
1035
1507
  `Done. ${result.stats.totalCells} cells, avg ${result.stats.avgRenderTimeMs.toFixed(1)}ms
1036
1508
  `
@@ -1039,7 +1511,7 @@ Available: ${available}`
1039
1511
  const { SpriteSheetGenerator } = await import("@agent-scope/render");
1040
1512
  const gen = new SpriteSheetGenerator();
1041
1513
  const sheet = await gen.generate(result);
1042
- const spritePath = resolve3(process.cwd(), opts.sprite);
1514
+ const spritePath = resolve4(process.cwd(), opts.sprite);
1043
1515
  writeFileSync3(spritePath, sheet.png);
1044
1516
  process.stderr.write(`Sprite sheet saved to ${spritePath}
1045
1517
  `);
@@ -1049,9 +1521,9 @@ Available: ${available}`
1049
1521
  const { SpriteSheetGenerator } = await import("@agent-scope/render");
1050
1522
  const gen = new SpriteSheetGenerator();
1051
1523
  const sheet = await gen.generate(result);
1052
- const dir = resolve3(process.cwd(), DEFAULT_OUTPUT_DIR);
1524
+ const dir = resolve4(process.cwd(), DEFAULT_OUTPUT_DIR);
1053
1525
  mkdirSync2(dir, { recursive: true });
1054
- const outPath = resolve3(dir, `${componentName}-matrix.png`);
1526
+ const outPath = resolve4(dir, `${componentName}-matrix.png`);
1055
1527
  writeFileSync3(outPath, sheet.png);
1056
1528
  const relPath = `${DEFAULT_OUTPUT_DIR}/${componentName}-matrix.png`;
1057
1529
  process.stdout.write(
@@ -1075,7 +1547,7 @@ Available: ${available}`
1075
1547
  process.stdout.write(formatMatrixCsv(componentName, result));
1076
1548
  }
1077
1549
  } catch (err) {
1078
- await shutdownPool();
1550
+ await shutdownPool2();
1079
1551
  process.stderr.write(`Error: ${err instanceof Error ? err.message : String(err)}
1080
1552
  `);
1081
1553
  process.exit(1);
@@ -1084,7 +1556,7 @@ Available: ${available}`
1084
1556
  );
1085
1557
  }
1086
1558
  function registerRenderAll(renderCmd) {
1087
- 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_PATH2).option("--format <fmt>", "Output format: json|png (default: png)", "png").action(
1559
+ 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(
1088
1560
  async (opts) => {
1089
1561
  try {
1090
1562
  const manifest = loadManifest(opts.manifest);
@@ -1095,7 +1567,7 @@ function registerRenderAll(renderCmd) {
1095
1567
  return;
1096
1568
  }
1097
1569
  const concurrency = Math.max(1, parseInt(opts.concurrency, 10) || 4);
1098
- const outputDir = resolve3(process.cwd(), opts.outputDir);
1570
+ const outputDir = resolve4(process.cwd(), opts.outputDir);
1099
1571
  mkdirSync2(outputDir, { recursive: true });
1100
1572
  const rootDir = process.cwd();
1101
1573
  process.stderr.write(`Rendering ${total} components (concurrency: ${concurrency})\u2026
@@ -1105,7 +1577,7 @@ function registerRenderAll(renderCmd) {
1105
1577
  const renderOne = async (name) => {
1106
1578
  const descriptor = manifest.components[name];
1107
1579
  if (descriptor === void 0) return;
1108
- const filePath = resolve3(rootDir, descriptor.filePath);
1580
+ const filePath = resolve4(rootDir, descriptor.filePath);
1109
1581
  const renderer = buildRenderer(filePath, name, 375, 812);
1110
1582
  const outcome = await safeRender(
1111
1583
  () => renderer.renderCell({}, descriptor.complexityClass),
@@ -1128,7 +1600,7 @@ function registerRenderAll(renderCmd) {
1128
1600
  success: false,
1129
1601
  errorMessage: outcome.error.message
1130
1602
  });
1131
- const errPath = resolve3(outputDir, `${name}.error.json`);
1603
+ const errPath = resolve4(outputDir, `${name}.error.json`);
1132
1604
  writeFileSync3(
1133
1605
  errPath,
1134
1606
  JSON.stringify(
@@ -1146,9 +1618,9 @@ function registerRenderAll(renderCmd) {
1146
1618
  }
1147
1619
  const result = outcome.result;
1148
1620
  results.push({ name, renderTimeMs: result.renderTimeMs, success: true });
1149
- const pngPath = resolve3(outputDir, `${name}.png`);
1621
+ const pngPath = resolve4(outputDir, `${name}.png`);
1150
1622
  writeFileSync3(pngPath, result.screenshot);
1151
- const jsonPath = resolve3(outputDir, `${name}.json`);
1623
+ const jsonPath = resolve4(outputDir, `${name}.json`);
1152
1624
  writeFileSync3(jsonPath, JSON.stringify(formatRenderJson(name, {}, result), null, 2));
1153
1625
  if (isTTY()) {
1154
1626
  process.stdout.write(
@@ -1172,13 +1644,13 @@ function registerRenderAll(renderCmd) {
1172
1644
  workers.push(worker());
1173
1645
  }
1174
1646
  await Promise.all(workers);
1175
- await shutdownPool();
1647
+ await shutdownPool2();
1176
1648
  process.stderr.write("\n");
1177
1649
  const summary = formatSummaryText(results, outputDir);
1178
1650
  process.stderr.write(`${summary}
1179
1651
  `);
1180
1652
  } catch (err) {
1181
- await shutdownPool();
1653
+ await shutdownPool2();
1182
1654
  process.stderr.write(`Error: ${err instanceof Error ? err.message : String(err)}
1183
1655
  `);
1184
1656
  process.exit(1);
@@ -1211,7 +1683,7 @@ function resolveMatrixFormat(formatFlag, spriteAlreadyWritten) {
1211
1683
  return "json";
1212
1684
  }
1213
1685
  function createRenderCommand() {
1214
- const renderCmd = new Command2("render").description(
1686
+ const renderCmd = new Command3("render").description(
1215
1687
  "Render components to PNG or JSON via esbuild + BrowserPool"
1216
1688
  );
1217
1689
  registerRenderSingle(renderCmd);
@@ -1501,7 +1973,7 @@ function buildStructuredReport(report) {
1501
1973
 
1502
1974
  // src/tokens/commands.ts
1503
1975
  import { existsSync as existsSync3, readFileSync as readFileSync3 } from "fs";
1504
- import { resolve as resolve4 } from "path";
1976
+ import { resolve as resolve5 } from "path";
1505
1977
  import {
1506
1978
  parseTokenFileSync,
1507
1979
  TokenParseError,
@@ -1509,7 +1981,7 @@ import {
1509
1981
  TokenValidationError,
1510
1982
  validateTokenFile
1511
1983
  } from "@agent-scope/tokens";
1512
- import { Command as Command3 } from "commander";
1984
+ import { Command as Command4 } from "commander";
1513
1985
  var DEFAULT_TOKEN_FILE = "reactscope.tokens.json";
1514
1986
  var CONFIG_FILE = "reactscope.config.json";
1515
1987
  function isTTY2() {
@@ -1531,21 +2003,21 @@ function buildTable2(headers, rows) {
1531
2003
  }
1532
2004
  function resolveTokenFilePath(fileFlag) {
1533
2005
  if (fileFlag !== void 0) {
1534
- return resolve4(process.cwd(), fileFlag);
2006
+ return resolve5(process.cwd(), fileFlag);
1535
2007
  }
1536
- const configPath = resolve4(process.cwd(), CONFIG_FILE);
2008
+ const configPath = resolve5(process.cwd(), CONFIG_FILE);
1537
2009
  if (existsSync3(configPath)) {
1538
2010
  try {
1539
2011
  const raw = readFileSync3(configPath, "utf-8");
1540
2012
  const config = JSON.parse(raw);
1541
2013
  if (typeof config === "object" && config !== null && "tokens" in config && typeof config.tokens === "object" && config.tokens !== null && typeof config.tokens?.file === "string") {
1542
2014
  const file = config.tokens.file;
1543
- return resolve4(process.cwd(), file);
2015
+ return resolve5(process.cwd(), file);
1544
2016
  }
1545
2017
  } catch {
1546
2018
  }
1547
2019
  }
1548
- return resolve4(process.cwd(), DEFAULT_TOKEN_FILE);
2020
+ return resolve5(process.cwd(), DEFAULT_TOKEN_FILE);
1549
2021
  }
1550
2022
  function loadTokens(absPath) {
1551
2023
  if (!existsSync3(absPath)) {
@@ -1842,7 +2314,7 @@ function outputValidationResult(filePath, errors, useJson) {
1842
2314
  }
1843
2315
  }
1844
2316
  function createTokensCommand() {
1845
- const tokensCmd = new Command3("tokens").description(
2317
+ const tokensCmd = new Command4("tokens").description(
1846
2318
  "Query and validate design tokens from a reactscope.tokens.json file"
1847
2319
  );
1848
2320
  registerGet2(tokensCmd);
@@ -1855,7 +2327,7 @@ function createTokensCommand() {
1855
2327
 
1856
2328
  // src/program.ts
1857
2329
  function createProgram(options = {}) {
1858
- const program2 = new Command4("scope").version(options.version ?? "0.1.0").description("Scope \u2014 React instrumentation toolkit");
2330
+ const program2 = new Command5("scope").version(options.version ?? "0.1.0").description("Scope \u2014 React instrumentation toolkit");
1859
2331
  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(
1860
2332
  async (url, opts) => {
1861
2333
  try {
@@ -1940,6 +2412,7 @@ function createProgram(options = {}) {
1940
2412
  program2.addCommand(createManifestCommand());
1941
2413
  program2.addCommand(createRenderCommand());
1942
2414
  program2.addCommand(createTokensCommand());
2415
+ program2.addCommand(createInstrumentCommand());
1943
2416
  return program2;
1944
2417
  }
1945
2418