@agent-scope/cli 1.6.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
@@ -1,9 +1,9 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  // src/program.ts
4
- import { readFileSync as readFileSync3 } from "fs";
4
+ import { readFileSync as readFileSync4 } from "fs";
5
5
  import { generateTest, loadTrace } from "@agent-scope/playwright";
6
- import { Command as Command3 } 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();
@@ -568,54 +558,536 @@ function formatMatrixCsv(componentName, result) {
568
558
  return `${[headers.join(","), ...rows].join("\n")}
569
559
  `;
570
560
  }
571
- function renderProgressBar(completed, total, currentName, pct, barWidth = 20) {
572
- const filled = Math.round(pct / 100 * barWidth);
573
- const empty = barWidth - filled;
574
- const bar = "=".repeat(Math.max(0, filled - 1)) + (filled > 0 ? ">" : "") + " ".repeat(empty);
575
- const nameSlice = currentName.slice(0, 25).padEnd(25);
576
- return `Rendering ${completed}/${total} ${nameSlice} [${bar}] ${pct}%`;
561
+ function renderProgressBar(completed, total, currentName, pct, barWidth = 20) {
562
+ const filled = Math.round(pct / 100 * barWidth);
563
+ const empty = barWidth - filled;
564
+ const bar = "=".repeat(Math.max(0, filled - 1)) + (filled > 0 ? ">" : "") + " ".repeat(empty);
565
+ const nameSlice = currentName.slice(0, 25).padEnd(25);
566
+ return `Rendering ${completed}/${total} ${nameSlice} [${bar}] ${pct}%`;
567
+ }
568
+ function formatSummaryText(results, outputDir) {
569
+ const total = results.length;
570
+ const passed = results.filter((r) => r.success).length;
571
+ const failed = total - passed;
572
+ const successTimes = results.filter((r) => r.success).map((r) => r.renderTimeMs);
573
+ const avgMs = successTimes.length > 0 ? successTimes.reduce((a, b) => a + b, 0) / successTimes.length : 0;
574
+ const lines = [
575
+ "\u2500".repeat(60),
576
+ `Render Summary`,
577
+ "\u2500".repeat(60),
578
+ ` Total components : ${total}`,
579
+ ` Passed : ${passed}`,
580
+ ` Failed : ${failed}`,
581
+ ` Avg render time : ${avgMs.toFixed(1)}ms`,
582
+ ` Output dir : ${outputDir}`
583
+ ];
584
+ if (failed > 0) {
585
+ lines.push("", " Failed components:");
586
+ for (const r of results) {
587
+ if (!r.success) {
588
+ lines.push(` \u2717 ${r.name}: ${r.errorMessage ?? "unknown error"}`);
589
+ }
590
+ }
591
+ }
592
+ lines.push("\u2500".repeat(60));
593
+ return lines.join("\n");
594
+ }
595
+ function escapeHtml(str) {
596
+ return str.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#039;");
597
+ }
598
+ function csvEscape(value) {
599
+ if (value.includes(",") || value.includes('"') || value.includes("\n")) {
600
+ return `"${value.replace(/"/g, '""')}"`;
601
+ }
602
+ return value;
603
+ }
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
+ }
577
980
  }
578
- function formatSummaryText(results, outputDir) {
579
- const total = results.length;
580
- const passed = results.filter((r) => r.success).length;
581
- const failed = total - passed;
582
- const successTimes = results.filter((r) => r.success).map((r) => r.renderTimeMs);
583
- const avgMs = successTimes.length > 0 ? successTimes.reduce((a, b) => a + b, 0) / successTimes.length : 0;
584
- const lines = [
585
- "\u2500".repeat(60),
586
- `Render Summary`,
587
- "\u2500".repeat(60),
588
- ` Total components : ${total}`,
589
- ` Passed : ${passed}`,
590
- ` Failed : ${failed}`,
591
- ` Avg render time : ${avgMs.toFixed(1)}ms`,
592
- ` Output dir : ${outputDir}`
593
- ];
594
- if (failed > 0) {
595
- lines.push("", " Failed components:");
596
- for (const r of results) {
597
- if (!r.success) {
598
- lines.push(` \u2717 ${r.name}: ${r.errorMessage ?? "unknown error"}`);
599
- }
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}`);
600
1015
  }
601
1016
  }
602
- lines.push("\u2500".repeat(60));
603
1017
  return lines.join("\n");
604
1018
  }
605
- function escapeHtml(str) {
606
- return str.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#039;");
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
+ );
607
1063
  }
608
- function csvEscape(value) {
609
- if (value.includes(",") || value.includes('"') || value.includes("\n")) {
610
- return `"${value.replace(/"/g, '""')}"`;
611
- }
612
- return value;
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;
613
1070
  }
614
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);
@@ -1499,9 +1971,363 @@ function buildStructuredReport(report) {
1499
1971
  };
1500
1972
  }
1501
1973
 
1974
+ // src/tokens/commands.ts
1975
+ import { existsSync as existsSync3, readFileSync as readFileSync3 } from "fs";
1976
+ import { resolve as resolve5 } from "path";
1977
+ import {
1978
+ parseTokenFileSync,
1979
+ TokenParseError,
1980
+ TokenResolver,
1981
+ TokenValidationError,
1982
+ validateTokenFile
1983
+ } from "@agent-scope/tokens";
1984
+ import { Command as Command4 } from "commander";
1985
+ var DEFAULT_TOKEN_FILE = "reactscope.tokens.json";
1986
+ var CONFIG_FILE = "reactscope.config.json";
1987
+ function isTTY2() {
1988
+ return process.stdout.isTTY === true;
1989
+ }
1990
+ function pad3(value, width) {
1991
+ return value.length >= width ? value.slice(0, width) : value + " ".repeat(width - value.length);
1992
+ }
1993
+ function buildTable2(headers, rows) {
1994
+ const colWidths = headers.map(
1995
+ (h, i) => Math.max(h.length, ...rows.map((r) => (r[i] ?? "").length))
1996
+ );
1997
+ const divider = colWidths.map((w) => "-".repeat(w)).join(" ");
1998
+ const headerRow = headers.map((h, i) => pad3(h, colWidths[i] ?? 0)).join(" ");
1999
+ const dataRows = rows.map(
2000
+ (row2) => row2.map((cell, i) => pad3(cell ?? "", colWidths[i] ?? 0)).join(" ")
2001
+ );
2002
+ return [headerRow, divider, ...dataRows].join("\n");
2003
+ }
2004
+ function resolveTokenFilePath(fileFlag) {
2005
+ if (fileFlag !== void 0) {
2006
+ return resolve5(process.cwd(), fileFlag);
2007
+ }
2008
+ const configPath = resolve5(process.cwd(), CONFIG_FILE);
2009
+ if (existsSync3(configPath)) {
2010
+ try {
2011
+ const raw = readFileSync3(configPath, "utf-8");
2012
+ const config = JSON.parse(raw);
2013
+ if (typeof config === "object" && config !== null && "tokens" in config && typeof config.tokens === "object" && config.tokens !== null && typeof config.tokens?.file === "string") {
2014
+ const file = config.tokens.file;
2015
+ return resolve5(process.cwd(), file);
2016
+ }
2017
+ } catch {
2018
+ }
2019
+ }
2020
+ return resolve5(process.cwd(), DEFAULT_TOKEN_FILE);
2021
+ }
2022
+ function loadTokens(absPath) {
2023
+ if (!existsSync3(absPath)) {
2024
+ throw new Error(
2025
+ `Token file not found at ${absPath}.
2026
+ Create a reactscope.tokens.json file or use --file to specify a path.`
2027
+ );
2028
+ }
2029
+ const raw = readFileSync3(absPath, "utf-8");
2030
+ return parseTokenFileSync(raw);
2031
+ }
2032
+ function getRawValue(node, segments) {
2033
+ const [head, ...rest] = segments;
2034
+ if (head === void 0) return null;
2035
+ const child = node[head];
2036
+ if (child === void 0 || child === null) return null;
2037
+ if (rest.length === 0) {
2038
+ if (typeof child === "object" && !Array.isArray(child) && "value" in child) {
2039
+ const v = child.value;
2040
+ return typeof v === "string" || typeof v === "number" ? v : null;
2041
+ }
2042
+ return null;
2043
+ }
2044
+ if (typeof child === "object" && !Array.isArray(child)) {
2045
+ return getRawValue(child, rest);
2046
+ }
2047
+ return null;
2048
+ }
2049
+ function buildResolutionChain(startPath, rawTokens) {
2050
+ const chain = [];
2051
+ const seen = /* @__PURE__ */ new Set();
2052
+ let current = startPath;
2053
+ while (!seen.has(current)) {
2054
+ seen.add(current);
2055
+ const rawValue = getRawValue(rawTokens, current.split("."));
2056
+ if (rawValue === null) break;
2057
+ chain.push({ path: current, rawValue: String(rawValue) });
2058
+ const refMatch = /^\{([^}]+)\}$/.exec(String(rawValue));
2059
+ if (refMatch === null) break;
2060
+ current = refMatch[1] ?? "";
2061
+ }
2062
+ return chain;
2063
+ }
2064
+ function registerGet2(tokensCmd) {
2065
+ 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) => {
2066
+ try {
2067
+ const filePath = resolveTokenFilePath(opts.file);
2068
+ const { tokens } = loadTokens(filePath);
2069
+ const resolver = new TokenResolver(tokens);
2070
+ const resolvedValue = resolver.resolve(tokenPath);
2071
+ const useJson = opts.format === "json" || opts.format !== "text" && !isTTY2();
2072
+ if (useJson) {
2073
+ const token = tokens.find((t) => t.path === tokenPath);
2074
+ process.stdout.write(
2075
+ `${JSON.stringify({ path: tokenPath, value: token?.value, resolvedValue, type: token?.type }, null, 2)}
2076
+ `
2077
+ );
2078
+ } else {
2079
+ process.stdout.write(`${resolvedValue}
2080
+ `);
2081
+ }
2082
+ } catch (err) {
2083
+ process.stderr.write(`Error: ${err instanceof Error ? err.message : String(err)}
2084
+ `);
2085
+ process.exit(1);
2086
+ }
2087
+ });
2088
+ }
2089
+ function registerList2(tokensCmd) {
2090
+ 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(
2091
+ (category, opts) => {
2092
+ try {
2093
+ const filePath = resolveTokenFilePath(opts.file);
2094
+ const { tokens } = loadTokens(filePath);
2095
+ const resolver = new TokenResolver(tokens);
2096
+ const filtered = resolver.list(opts.type, category);
2097
+ const useJson = opts.format === "json" || opts.format !== "table" && !isTTY2();
2098
+ if (useJson) {
2099
+ process.stdout.write(`${JSON.stringify(filtered, null, 2)}
2100
+ `);
2101
+ } else {
2102
+ if (filtered.length === 0) {
2103
+ process.stdout.write("No tokens found.\n");
2104
+ return;
2105
+ }
2106
+ const headers = ["PATH", "VALUE", "RESOLVED", "TYPE"];
2107
+ const rows = filtered.map((t) => [t.path, String(t.value), t.resolvedValue, t.type]);
2108
+ process.stdout.write(`${buildTable2(headers, rows)}
2109
+ `);
2110
+ }
2111
+ } catch (err) {
2112
+ process.stderr.write(`Error: ${err instanceof Error ? err.message : String(err)}
2113
+ `);
2114
+ process.exit(1);
2115
+ }
2116
+ }
2117
+ );
2118
+ }
2119
+ function registerSearch(tokensCmd) {
2120
+ 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(
2121
+ (value, opts) => {
2122
+ try {
2123
+ const filePath = resolveTokenFilePath(opts.file);
2124
+ const { tokens } = loadTokens(filePath);
2125
+ const resolver = new TokenResolver(tokens);
2126
+ const useJson = opts.format === "json" || opts.format !== "table" && !isTTY2();
2127
+ const typesToSearch = opts.type ? [opts.type] : [
2128
+ "color",
2129
+ "dimension",
2130
+ "fontFamily",
2131
+ "fontWeight",
2132
+ "number",
2133
+ "shadow",
2134
+ "duration",
2135
+ "cubicBezier"
2136
+ ];
2137
+ const exactMatches = [];
2138
+ const nearestMatches = [];
2139
+ for (const type of typesToSearch) {
2140
+ const exact = resolver.match(value, type);
2141
+ if (exact !== null) {
2142
+ exactMatches.push({
2143
+ path: exact.token.path,
2144
+ resolvedValue: exact.token.resolvedValue,
2145
+ type: exact.token.type,
2146
+ exact: true,
2147
+ distance: 0
2148
+ });
2149
+ }
2150
+ }
2151
+ if (exactMatches.length === 0 && opts.fuzzy) {
2152
+ for (const type of typesToSearch) {
2153
+ const typeTokens = tokens.filter((t) => t.type === type);
2154
+ if (typeTokens.length === 0) continue;
2155
+ try {
2156
+ const near = resolver.nearest(value, type);
2157
+ nearestMatches.push({
2158
+ path: near.token.path,
2159
+ resolvedValue: near.token.resolvedValue,
2160
+ type: near.token.type,
2161
+ exact: near.exact,
2162
+ distance: near.distance
2163
+ });
2164
+ } catch {
2165
+ }
2166
+ }
2167
+ nearestMatches.sort((a, b) => a.distance - b.distance);
2168
+ nearestMatches.splice(3);
2169
+ }
2170
+ const results = exactMatches.length > 0 ? exactMatches : nearestMatches;
2171
+ if (useJson) {
2172
+ process.stdout.write(`${JSON.stringify(results, null, 2)}
2173
+ `);
2174
+ } else {
2175
+ if (results.length === 0) {
2176
+ process.stdout.write(
2177
+ `No tokens found matching "${value}".
2178
+ Tip: use --fuzzy for nearest-match search.
2179
+ `
2180
+ );
2181
+ return;
2182
+ }
2183
+ const headers = ["PATH", "RESOLVED VALUE", "TYPE", "MATCH", "DISTANCE"];
2184
+ const rows = results.map((r) => [
2185
+ r.path,
2186
+ r.resolvedValue,
2187
+ r.type,
2188
+ r.exact ? "exact" : "nearest",
2189
+ r.exact ? "\u2014" : r.distance.toFixed(2)
2190
+ ]);
2191
+ process.stdout.write(`${buildTable2(headers, rows)}
2192
+ `);
2193
+ }
2194
+ } catch (err) {
2195
+ process.stderr.write(`Error: ${err instanceof Error ? err.message : String(err)}
2196
+ `);
2197
+ process.exit(1);
2198
+ }
2199
+ }
2200
+ );
2201
+ }
2202
+ function registerResolve(tokensCmd) {
2203
+ 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) => {
2204
+ try {
2205
+ const filePath = resolveTokenFilePath(opts.file);
2206
+ const absFilePath = filePath;
2207
+ const { tokens, rawFile } = loadTokens(absFilePath);
2208
+ const resolver = new TokenResolver(tokens);
2209
+ resolver.resolve(tokenPath);
2210
+ const chain = buildResolutionChain(tokenPath, rawFile.tokens);
2211
+ const useJson = opts.format === "json" || opts.format !== "text" && !isTTY2();
2212
+ if (useJson) {
2213
+ process.stdout.write(`${JSON.stringify({ path: tokenPath, chain }, null, 2)}
2214
+ `);
2215
+ } else {
2216
+ if (chain.length === 0) {
2217
+ process.stdout.write(`Token "${tokenPath}" not found.
2218
+ `);
2219
+ return;
2220
+ }
2221
+ const parts = chain.map((step, i) => {
2222
+ if (i < chain.length - 1) {
2223
+ return `${step.path} \u2192 ${step.rawValue}`;
2224
+ }
2225
+ return step.rawValue;
2226
+ });
2227
+ process.stdout.write(`${parts.join("\n ")}
2228
+ `);
2229
+ }
2230
+ } catch (err) {
2231
+ process.stderr.write(`Error: ${err instanceof Error ? err.message : String(err)}
2232
+ `);
2233
+ process.exit(1);
2234
+ }
2235
+ });
2236
+ }
2237
+ function registerValidate(tokensCmd) {
2238
+ tokensCmd.command("validate").description(
2239
+ "Validate the token file for errors (circular refs, missing refs, type mismatches)"
2240
+ ).option("--file <path>", "Path to token file (overrides config)").option("--format <fmt>", "Output format: json or text (default: auto-detect)").action((opts) => {
2241
+ try {
2242
+ const filePath = resolveTokenFilePath(opts.file);
2243
+ if (!existsSync3(filePath)) {
2244
+ throw new Error(
2245
+ `Token file not found at ${filePath}.
2246
+ Create a reactscope.tokens.json file or use --file to specify a path.`
2247
+ );
2248
+ }
2249
+ const raw = readFileSync3(filePath, "utf-8");
2250
+ const useJson = opts.format === "json" || opts.format !== "text" && !isTTY2();
2251
+ const errors = [];
2252
+ let parsed;
2253
+ try {
2254
+ parsed = JSON.parse(raw);
2255
+ } catch (err) {
2256
+ errors.push({
2257
+ code: "PARSE_ERROR",
2258
+ message: `Failed to parse token file as JSON: ${String(err)}`
2259
+ });
2260
+ outputValidationResult(filePath, errors, useJson);
2261
+ process.exit(1);
2262
+ }
2263
+ try {
2264
+ validateTokenFile(parsed);
2265
+ } catch (err) {
2266
+ if (err instanceof TokenValidationError) {
2267
+ for (const e of err.errors) {
2268
+ errors.push({ code: e.code, path: e.path, message: e.message });
2269
+ }
2270
+ outputValidationResult(filePath, errors, useJson);
2271
+ process.exit(1);
2272
+ }
2273
+ throw err;
2274
+ }
2275
+ try {
2276
+ parseTokenFileSync(raw);
2277
+ } catch (err) {
2278
+ if (err instanceof TokenParseError) {
2279
+ errors.push({ code: err.code, path: err.path, message: err.message });
2280
+ } else {
2281
+ errors.push({ code: "UNKNOWN", message: String(err) });
2282
+ }
2283
+ outputValidationResult(filePath, errors, useJson);
2284
+ process.exit(1);
2285
+ }
2286
+ outputValidationResult(filePath, errors, useJson);
2287
+ } catch (err) {
2288
+ process.stderr.write(`Error: ${err instanceof Error ? err.message : String(err)}
2289
+ `);
2290
+ process.exit(1);
2291
+ }
2292
+ });
2293
+ }
2294
+ function outputValidationResult(filePath, errors, useJson) {
2295
+ const valid = errors.length === 0;
2296
+ if (useJson) {
2297
+ process.stdout.write(`${JSON.stringify({ valid, file: filePath, errors }, null, 2)}
2298
+ `);
2299
+ } else {
2300
+ if (valid) {
2301
+ process.stdout.write(`\u2713 Token file is valid: ${filePath}
2302
+ `);
2303
+ } else {
2304
+ process.stderr.write(`\u2717 Token file has ${errors.length} error(s): ${filePath}
2305
+
2306
+ `);
2307
+ for (const e of errors) {
2308
+ const pathPrefix = e.path ? ` [${e.path}]` : "";
2309
+ process.stderr.write(` ${e.code}${pathPrefix}: ${e.message}
2310
+ `);
2311
+ }
2312
+ process.exit(1);
2313
+ }
2314
+ }
2315
+ }
2316
+ function createTokensCommand() {
2317
+ const tokensCmd = new Command4("tokens").description(
2318
+ "Query and validate design tokens from a reactscope.tokens.json file"
2319
+ );
2320
+ registerGet2(tokensCmd);
2321
+ registerList2(tokensCmd);
2322
+ registerSearch(tokensCmd);
2323
+ registerResolve(tokensCmd);
2324
+ registerValidate(tokensCmd);
2325
+ return tokensCmd;
2326
+ }
2327
+
1502
2328
  // src/program.ts
1503
2329
  function createProgram(options = {}) {
1504
- const program2 = new Command3("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");
1505
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(
1506
2332
  async (url, opts) => {
1507
2333
  try {
@@ -1574,7 +2400,7 @@ function createProgram(options = {}) {
1574
2400
  }
1575
2401
  );
1576
2402
  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) => {
1577
- const raw = readFileSync3(tracePath, "utf-8");
2403
+ const raw = readFileSync4(tracePath, "utf-8");
1578
2404
  const trace = loadTrace(raw);
1579
2405
  const source = generateTest(trace, {
1580
2406
  description: opts.description,
@@ -1585,6 +2411,8 @@ function createProgram(options = {}) {
1585
2411
  });
1586
2412
  program2.addCommand(createManifestCommand());
1587
2413
  program2.addCommand(createRenderCommand());
2414
+ program2.addCommand(createTokensCommand());
2415
+ program2.addCommand(createInstrumentCommand());
1588
2416
  return program2;
1589
2417
  }
1590
2418