@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/index.js CHANGED
@@ -7,6 +7,7 @@ import { chromium } from 'playwright';
7
7
  import { safeRender, ALL_CONTEXT_IDS, contextAxis, stressAxis, ALL_STRESS_IDS, RenderMatrix, SatoriRenderer, BrowserPool } from '@agent-scope/render';
8
8
  import * as esbuild from 'esbuild';
9
9
  import { createRequire } from 'module';
10
+ import { TokenResolver, validateTokenFile, TokenValidationError, parseTokenFileSync, TokenParseError } from '@agent-scope/tokens';
10
11
 
11
12
  // src/manifest-commands.ts
12
13
 
@@ -584,6 +585,471 @@ function csvEscape(value) {
584
585
  }
585
586
  return value;
586
587
  }
588
+
589
+ // src/instrument/renders.ts
590
+ var MANIFEST_PATH2 = ".reactscope/manifest.json";
591
+ function determineTrigger(event) {
592
+ if (event.forceUpdate) return "force_update";
593
+ if (event.stateChanged) return "state_change";
594
+ if (event.propsChanged) return "props_change";
595
+ if (event.contextChanged) return "context_change";
596
+ if (event.hookDepsChanged) return "hook_dependency";
597
+ return "parent_rerender";
598
+ }
599
+ function isWastedRender(event) {
600
+ return !event.propsChanged && !event.stateChanged && !event.contextChanged && !event.memoized;
601
+ }
602
+ function buildCausalityChains(rawEvents) {
603
+ const result = [];
604
+ const componentLastRender = /* @__PURE__ */ new Map();
605
+ for (const raw of rawEvents) {
606
+ const trigger = determineTrigger(raw);
607
+ const wasted = isWastedRender(raw);
608
+ const chain = [];
609
+ let current = raw;
610
+ const visited = /* @__PURE__ */ new Set();
611
+ while (true) {
612
+ if (visited.has(current.component)) break;
613
+ visited.add(current.component);
614
+ const currentTrigger = determineTrigger(current);
615
+ chain.unshift({
616
+ component: current.component,
617
+ trigger: currentTrigger,
618
+ propsChanged: current.propsChanged,
619
+ stateChanged: current.stateChanged,
620
+ contextChanged: current.contextChanged
621
+ });
622
+ if (currentTrigger !== "parent_rerender" || current.parentComponent === null) {
623
+ break;
624
+ }
625
+ const parentEvent = componentLastRender.get(current.parentComponent);
626
+ if (parentEvent === void 0) break;
627
+ current = parentEvent;
628
+ }
629
+ const rootCause = chain[0];
630
+ const cascadeRenders = rawEvents.filter((e) => {
631
+ if (rootCause === void 0) return false;
632
+ return e.component !== rootCause.component && !e.stateChanged && !e.propsChanged;
633
+ });
634
+ result.push({
635
+ component: raw.component,
636
+ renderIndex: raw.renderIndex,
637
+ trigger,
638
+ propsChanged: raw.propsChanged,
639
+ stateChanged: raw.stateChanged,
640
+ contextChanged: raw.contextChanged,
641
+ memoized: raw.memoized,
642
+ wasted,
643
+ chain,
644
+ cascade: {
645
+ totalRendersTriggered: cascadeRenders.length,
646
+ uniqueComponents: new Set(cascadeRenders.map((e) => e.component)).size,
647
+ unchangedPropRenders: cascadeRenders.filter((e) => !e.propsChanged).length
648
+ }
649
+ });
650
+ componentLastRender.set(raw.component, raw);
651
+ }
652
+ return result;
653
+ }
654
+ function applyHeuristicFlags(renders) {
655
+ const flags = [];
656
+ const byComponent = /* @__PURE__ */ new Map();
657
+ for (const r of renders) {
658
+ if (!byComponent.has(r.component)) byComponent.set(r.component, []);
659
+ byComponent.get(r.component).push(r);
660
+ }
661
+ for (const [component, events] of byComponent) {
662
+ const wastedCount = events.filter((e) => e.wasted).length;
663
+ const totalCount = events.length;
664
+ if (wastedCount > 0) {
665
+ flags.push({
666
+ id: "WASTED_RENDER",
667
+ severity: "warning",
668
+ component,
669
+ detail: `${wastedCount}/${totalCount} renders were wasted \u2014 unchanged props/state/context, not memoized`,
670
+ data: { wastedCount, totalCount, wastedRatio: wastedCount / totalCount }
671
+ });
672
+ }
673
+ for (const event of events) {
674
+ if (event.cascade.totalRendersTriggered > 50) {
675
+ flags.push({
676
+ id: "RENDER_CASCADE",
677
+ severity: "warning",
678
+ component,
679
+ detail: `State change in ${component} triggered ${event.cascade.totalRendersTriggered} downstream re-renders`,
680
+ data: {
681
+ totalRendersTriggered: event.cascade.totalRendersTriggered,
682
+ uniqueComponents: event.cascade.uniqueComponents,
683
+ unchangedPropRenders: event.cascade.unchangedPropRenders
684
+ }
685
+ });
686
+ break;
687
+ }
688
+ }
689
+ }
690
+ return flags;
691
+ }
692
+ function buildInstrumentationScript() {
693
+ return (
694
+ /* js */
695
+ `
696
+ (function installScopeRenderInstrumentation() {
697
+ window.__SCOPE_RENDER_EVENTS__ = [];
698
+ window.__SCOPE_RENDER_INDEX__ = 0;
699
+
700
+ var hook = window.__REACT_DEVTOOLS_GLOBAL_HOOK__;
701
+ if (!hook) return;
702
+
703
+ var originalOnCommit = hook.onCommitFiberRoot;
704
+ var renderedComponents = new Map(); // componentName -> { lastProps, lastState }
705
+
706
+ function extractName(fiber) {
707
+ if (!fiber) return 'Unknown';
708
+ var type = fiber.type;
709
+ if (!type) return 'Unknown';
710
+ if (typeof type === 'string') return type; // host element
711
+ if (typeof type === 'function') return type.displayName || type.name || 'Anonymous';
712
+ if (type.displayName) return type.displayName;
713
+ if (type.render && typeof type.render === 'function') {
714
+ return type.render.displayName || type.render.name || 'Anonymous';
715
+ }
716
+ return 'Anonymous';
717
+ }
718
+
719
+ function isMemoized(fiber) {
720
+ // MemoComponent = 14, SimpleMemoComponent = 15
721
+ return fiber.tag === 14 || fiber.tag === 15;
722
+ }
723
+
724
+ function isComponent(fiber) {
725
+ // FunctionComponent=0, ClassComponent=1, MemoComponent=14, SimpleMemoComponent=15
726
+ return fiber.tag === 0 || fiber.tag === 1 || fiber.tag === 14 || fiber.tag === 15;
727
+ }
728
+
729
+ function shallowEqual(a, b) {
730
+ if (a === b) return true;
731
+ if (!a || !b) return a === b;
732
+ var keysA = Object.keys(a);
733
+ var keysB = Object.keys(b);
734
+ if (keysA.length !== keysB.length) return false;
735
+ for (var i = 0; i < keysA.length; i++) {
736
+ var k = keysA[i];
737
+ if (k === 'children') continue; // ignore children prop
738
+ if (a[k] !== b[k]) return false;
739
+ }
740
+ return true;
741
+ }
742
+
743
+ function getParentComponentName(fiber) {
744
+ var parent = fiber.return;
745
+ while (parent) {
746
+ if (isComponent(parent)) {
747
+ var name = extractName(parent);
748
+ if (name && name !== 'Unknown' && !/^[a-z]/.test(name)) return name;
749
+ }
750
+ parent = parent.return;
751
+ }
752
+ return null;
753
+ }
754
+
755
+ function walkCommit(fiber) {
756
+ if (!fiber) return;
757
+
758
+ if (isComponent(fiber)) {
759
+ var name = extractName(fiber);
760
+ if (name && name !== 'Unknown' && !/^[a-z]/.test(name)) {
761
+ var memoized = isMemoized(fiber);
762
+ var currentProps = fiber.memoizedProps || {};
763
+ var prev = renderedComponents.get(name);
764
+
765
+ var propsChanged = true;
766
+ var stateChanged = false;
767
+ var contextChanged = false;
768
+ var hookDepsChanged = false;
769
+ var forceUpdate = false;
770
+
771
+ if (prev) {
772
+ propsChanged = !shallowEqual(prev.lastProps, currentProps);
773
+ }
774
+
775
+ // State: check memoizedState chain
776
+ var memoizedState = fiber.memoizedState;
777
+ if (prev && prev.lastStateSerialized !== undefined) {
778
+ try {
779
+ var stateSig = JSON.stringify(memoizedState ? memoizedState.memoizedState : null);
780
+ stateChanged = stateSig !== prev.lastStateSerialized;
781
+ } catch (_) {
782
+ stateChanged = false;
783
+ }
784
+ }
785
+
786
+ // Context: use _debugHookTypes or check dependencies
787
+ var deps = fiber.dependencies;
788
+ if (deps && deps.firstContext) {
789
+ contextChanged = true; // conservative: context dep present = may have changed
790
+ }
791
+
792
+ var stateSig;
793
+ try {
794
+ stateSig = JSON.stringify(memoizedState ? memoizedState.memoizedState : null);
795
+ } catch (_) {
796
+ stateSig = null;
797
+ }
798
+
799
+ renderedComponents.set(name, {
800
+ lastProps: currentProps,
801
+ lastStateSerialized: stateSig,
802
+ });
803
+
804
+ var parentName = getParentComponentName(fiber);
805
+
806
+ window.__SCOPE_RENDER_EVENTS__.push({
807
+ component: name,
808
+ renderIndex: window.__SCOPE_RENDER_INDEX__++,
809
+ propsChanged: prev ? propsChanged : false,
810
+ stateChanged: stateChanged,
811
+ contextChanged: contextChanged,
812
+ memoized: memoized,
813
+ parentComponent: parentName,
814
+ hookDepsChanged: hookDepsChanged,
815
+ forceUpdate: forceUpdate,
816
+ });
817
+ }
818
+ }
819
+
820
+ walkCommit(fiber.child);
821
+ walkCommit(fiber.sibling);
822
+ }
823
+
824
+ hook.onCommitFiberRoot = function(rendererID, root, priorityLevel) {
825
+ if (typeof originalOnCommit === 'function') {
826
+ originalOnCommit.call(hook, rendererID, root, priorityLevel);
827
+ }
828
+ var wipRoot = root && root.current && root.current.alternate;
829
+ if (wipRoot) walkCommit(wipRoot);
830
+ };
831
+ })();
832
+ `
833
+ );
834
+ }
835
+ async function replayInteraction(page, steps) {
836
+ for (const step of steps) {
837
+ switch (step.action) {
838
+ case "click":
839
+ if (step.target !== void 0) {
840
+ await page.click(step.target, { timeout: 5e3 }).catch(() => {
841
+ });
842
+ }
843
+ break;
844
+ case "type":
845
+ if (step.target !== void 0 && step.text !== void 0) {
846
+ await page.fill(step.target, step.text, { timeout: 5e3 }).catch(async () => {
847
+ await page.type(step.target, step.text, { timeout: 5e3 }).catch(() => {
848
+ });
849
+ });
850
+ }
851
+ break;
852
+ case "hover":
853
+ if (step.target !== void 0) {
854
+ await page.hover(step.target, { timeout: 5e3 }).catch(() => {
855
+ });
856
+ }
857
+ break;
858
+ case "blur":
859
+ if (step.target !== void 0) {
860
+ await page.locator(step.target).blur({ timeout: 5e3 }).catch(() => {
861
+ });
862
+ }
863
+ break;
864
+ case "focus":
865
+ if (step.target !== void 0) {
866
+ await page.focus(step.target, { timeout: 5e3 }).catch(() => {
867
+ });
868
+ }
869
+ break;
870
+ case "scroll":
871
+ if (step.target !== void 0) {
872
+ await page.locator(step.target).scrollIntoViewIfNeeded({ timeout: 5e3 }).catch(() => {
873
+ });
874
+ }
875
+ break;
876
+ case "wait": {
877
+ const timeout = step.timeout ?? 1e3;
878
+ if (step.condition === "idle") {
879
+ await page.waitForLoadState("networkidle", { timeout }).catch(() => {
880
+ });
881
+ } else {
882
+ await page.waitForTimeout(timeout);
883
+ }
884
+ break;
885
+ }
886
+ }
887
+ }
888
+ }
889
+ var _pool = null;
890
+ async function getPool() {
891
+ if (_pool === null) {
892
+ _pool = new BrowserPool({
893
+ size: { browsers: 1, pagesPerBrowser: 2 },
894
+ viewportWidth: 1280,
895
+ viewportHeight: 800
896
+ });
897
+ await _pool.init();
898
+ }
899
+ return _pool;
900
+ }
901
+ async function shutdownPool() {
902
+ if (_pool !== null) {
903
+ await _pool.close();
904
+ _pool = null;
905
+ }
906
+ }
907
+ async function analyzeRenders(options) {
908
+ const manifestPath = options.manifestPath ?? MANIFEST_PATH2;
909
+ const manifest = loadManifest(manifestPath);
910
+ const descriptor = manifest.components[options.componentName];
911
+ if (descriptor === void 0) {
912
+ const available = Object.keys(manifest.components).slice(0, 5).join(", ");
913
+ throw new Error(
914
+ `Component "${options.componentName}" not found in manifest.
915
+ Available: ${available}`
916
+ );
917
+ }
918
+ const rootDir = process.cwd();
919
+ const filePath = resolve(rootDir, descriptor.filePath);
920
+ const htmlHarness = await buildComponentHarness(filePath, options.componentName, {}, 1280);
921
+ const pool = await getPool();
922
+ const slot = await pool.acquire();
923
+ const { page } = slot;
924
+ const startMs = performance.now();
925
+ try {
926
+ await page.addInitScript(buildInstrumentationScript());
927
+ await page.setContent(htmlHarness, { waitUntil: "load" });
928
+ await page.waitForFunction(
929
+ () => window.__SCOPE_RENDER_COMPLETE__ === true,
930
+ { timeout: 15e3 }
931
+ );
932
+ await page.waitForTimeout(100);
933
+ await page.evaluate(() => {
934
+ window.__SCOPE_RENDER_EVENTS__ = [];
935
+ window.__SCOPE_RENDER_INDEX__ = 0;
936
+ });
937
+ await replayInteraction(page, options.interaction);
938
+ await page.waitForTimeout(200);
939
+ const interactionDurationMs = performance.now() - startMs;
940
+ const rawEvents = await page.evaluate(() => {
941
+ return window.__SCOPE_RENDER_EVENTS__ ?? [];
942
+ });
943
+ const renders = buildCausalityChains(rawEvents);
944
+ const flags = applyHeuristicFlags(renders);
945
+ const uniqueComponents = new Set(renders.map((r) => r.component)).size;
946
+ const wastedRenders = renders.filter((r) => r.wasted).length;
947
+ return {
948
+ component: options.componentName,
949
+ interaction: options.interaction,
950
+ summary: {
951
+ totalRenders: renders.length,
952
+ uniqueComponents,
953
+ wastedRenders,
954
+ interactionDurationMs: Math.round(interactionDurationMs)
955
+ },
956
+ renders,
957
+ flags
958
+ };
959
+ } finally {
960
+ pool.release(slot);
961
+ }
962
+ }
963
+ function formatRendersTable(result) {
964
+ const lines = [];
965
+ lines.push(`
966
+ \u{1F50D} Re-render Analysis: ${result.component}`);
967
+ lines.push(`${"\u2500".repeat(60)}`);
968
+ lines.push(`Total renders: ${result.summary.totalRenders}`);
969
+ lines.push(`Unique components: ${result.summary.uniqueComponents}`);
970
+ lines.push(`Wasted renders: ${result.summary.wastedRenders}`);
971
+ lines.push(`Duration: ${result.summary.interactionDurationMs}ms`);
972
+ lines.push("");
973
+ if (result.renders.length === 0) {
974
+ lines.push("No re-renders captured during interaction.");
975
+ } else {
976
+ lines.push("Re-renders:");
977
+ lines.push(
978
+ `${"#".padEnd(4)} ${"Component".padEnd(30)} ${"Trigger".padEnd(18)} ${"Wasted".padEnd(7)} ${"Chain Depth"}`
979
+ );
980
+ lines.push("\u2500".repeat(80));
981
+ for (const r of result.renders) {
982
+ const wasted = r.wasted ? "\u26A0 yes" : "no";
983
+ const idx = String(r.renderIndex).padEnd(4);
984
+ const comp = r.component.slice(0, 29).padEnd(30);
985
+ const trig = r.trigger.padEnd(18);
986
+ const w = wasted.padEnd(7);
987
+ const depth = r.chain.length;
988
+ lines.push(`${idx} ${comp} ${trig} ${w} ${depth}`);
989
+ }
990
+ }
991
+ if (result.flags.length > 0) {
992
+ lines.push("");
993
+ lines.push("Flags:");
994
+ for (const flag of result.flags) {
995
+ const icon = flag.severity === "error" ? "\u2717" : flag.severity === "warning" ? "\u26A0" : "\u2139";
996
+ lines.push(` ${icon} [${flag.id}] ${flag.component}: ${flag.detail}`);
997
+ }
998
+ }
999
+ return lines.join("\n");
1000
+ }
1001
+ function createInstrumentRendersCommand() {
1002
+ return new Command("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(
1003
+ "--interaction <json>",
1004
+ `Interaction sequence JSON, e.g. '[{"action":"click","target":"button"}]'`,
1005
+ "[]"
1006
+ ).option("--json", "Output as JSON regardless of TTY", false).option("--manifest <path>", "Path to manifest.json", MANIFEST_PATH2).action(
1007
+ async (componentName, opts) => {
1008
+ let interaction = [];
1009
+ try {
1010
+ interaction = JSON.parse(opts.interaction);
1011
+ if (!Array.isArray(interaction)) {
1012
+ throw new Error("Interaction must be a JSON array");
1013
+ }
1014
+ } catch {
1015
+ process.stderr.write(`Error: Invalid --interaction JSON: ${opts.interaction}
1016
+ `);
1017
+ process.exit(1);
1018
+ }
1019
+ try {
1020
+ process.stderr.write(
1021
+ `Instrumenting ${componentName} (${interaction.length} interaction steps)\u2026
1022
+ `
1023
+ );
1024
+ const result = await analyzeRenders({
1025
+ componentName,
1026
+ interaction,
1027
+ manifestPath: opts.manifest
1028
+ });
1029
+ await shutdownPool();
1030
+ if (opts.json || !isTTY()) {
1031
+ process.stdout.write(`${JSON.stringify(result, null, 2)}
1032
+ `);
1033
+ } else {
1034
+ process.stdout.write(`${formatRendersTable(result)}
1035
+ `);
1036
+ }
1037
+ } catch (err) {
1038
+ await shutdownPool();
1039
+ process.stderr.write(`Error: ${err instanceof Error ? err.message : String(err)}
1040
+ `);
1041
+ process.exit(1);
1042
+ }
1043
+ }
1044
+ );
1045
+ }
1046
+ function createInstrumentCommand() {
1047
+ const instrumentCmd = new Command("instrument").description(
1048
+ "Structured instrumentation commands for React component analysis"
1049
+ );
1050
+ instrumentCmd.addCommand(createInstrumentRendersCommand());
1051
+ return instrumentCmd;
1052
+ }
587
1053
  var CONFIG_FILENAMES = [
588
1054
  ".reactscope/config.json",
589
1055
  ".reactscope/config.js",
@@ -701,24 +1167,24 @@ async function getCompiledCssForClasses(cwd, classes) {
701
1167
  }
702
1168
 
703
1169
  // src/render-commands.ts
704
- var MANIFEST_PATH2 = ".reactscope/manifest.json";
1170
+ var MANIFEST_PATH3 = ".reactscope/manifest.json";
705
1171
  var DEFAULT_OUTPUT_DIR = ".reactscope/renders";
706
- var _pool = null;
707
- async function getPool(viewportWidth, viewportHeight) {
708
- if (_pool === null) {
709
- _pool = new BrowserPool({
1172
+ var _pool2 = null;
1173
+ async function getPool2(viewportWidth, viewportHeight) {
1174
+ if (_pool2 === null) {
1175
+ _pool2 = new BrowserPool({
710
1176
  size: { browsers: 1, pagesPerBrowser: 4 },
711
1177
  viewportWidth,
712
1178
  viewportHeight
713
1179
  });
714
- await _pool.init();
1180
+ await _pool2.init();
715
1181
  }
716
- return _pool;
1182
+ return _pool2;
717
1183
  }
718
- async function shutdownPool() {
719
- if (_pool !== null) {
720
- await _pool.close();
721
- _pool = null;
1184
+ async function shutdownPool2() {
1185
+ if (_pool2 !== null) {
1186
+ await _pool2.close();
1187
+ _pool2 = null;
722
1188
  }
723
1189
  }
724
1190
  function buildRenderer(filePath, componentName, viewportWidth, viewportHeight) {
@@ -729,7 +1195,7 @@ function buildRenderer(filePath, componentName, viewportWidth, viewportHeight) {
729
1195
  _satori: satori,
730
1196
  async renderCell(props, _complexityClass) {
731
1197
  const startMs = performance.now();
732
- const pool = await getPool(viewportWidth, viewportHeight);
1198
+ const pool = await getPool2(viewportWidth, viewportHeight);
733
1199
  const htmlHarness = await buildComponentHarness(
734
1200
  filePath,
735
1201
  componentName,
@@ -826,7 +1292,7 @@ function buildRenderer(filePath, componentName, viewportWidth, viewportHeight) {
826
1292
  };
827
1293
  }
828
1294
  function registerRenderSingle(renderCmd) {
829
- 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(
1295
+ 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(
830
1296
  async (componentName, opts) => {
831
1297
  try {
832
1298
  const manifest = loadManifest(opts.manifest);
@@ -865,7 +1331,7 @@ Available: ${available}`
865
1331
  }
866
1332
  }
867
1333
  );
868
- await shutdownPool();
1334
+ await shutdownPool2();
869
1335
  if (outcome.crashed) {
870
1336
  process.stderr.write(`\u2717 Render failed: ${outcome.error.message}
871
1337
  `);
@@ -913,7 +1379,7 @@ Available: ${available}`
913
1379
  );
914
1380
  }
915
1381
  } catch (err) {
916
- await shutdownPool();
1382
+ await shutdownPool2();
917
1383
  process.stderr.write(`Error: ${err instanceof Error ? err.message : String(err)}
918
1384
  `);
919
1385
  process.exit(1);
@@ -925,7 +1391,7 @@ function registerRenderMatrix(renderCmd) {
925
1391
  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(
926
1392
  "--contexts <ids>",
927
1393
  "Composition context IDs, comma-separated (e.g. centered,rtl,sidebar)"
928
- ).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(
1394
+ ).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(
929
1395
  async (componentName, opts) => {
930
1396
  try {
931
1397
  const manifest = loadManifest(opts.manifest);
@@ -998,7 +1464,7 @@ Available: ${available}`
998
1464
  concurrency
999
1465
  });
1000
1466
  const result = await matrix.render();
1001
- await shutdownPool();
1467
+ await shutdownPool2();
1002
1468
  process.stderr.write(
1003
1469
  `Done. ${result.stats.totalCells} cells, avg ${result.stats.avgRenderTimeMs.toFixed(1)}ms
1004
1470
  `
@@ -1043,7 +1509,7 @@ Available: ${available}`
1043
1509
  process.stdout.write(formatMatrixCsv(componentName, result));
1044
1510
  }
1045
1511
  } catch (err) {
1046
- await shutdownPool();
1512
+ await shutdownPool2();
1047
1513
  process.stderr.write(`Error: ${err instanceof Error ? err.message : String(err)}
1048
1514
  `);
1049
1515
  process.exit(1);
@@ -1052,7 +1518,7 @@ Available: ${available}`
1052
1518
  );
1053
1519
  }
1054
1520
  function registerRenderAll(renderCmd) {
1055
- 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(
1521
+ 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(
1056
1522
  async (opts) => {
1057
1523
  try {
1058
1524
  const manifest = loadManifest(opts.manifest);
@@ -1140,13 +1606,13 @@ function registerRenderAll(renderCmd) {
1140
1606
  workers.push(worker());
1141
1607
  }
1142
1608
  await Promise.all(workers);
1143
- await shutdownPool();
1609
+ await shutdownPool2();
1144
1610
  process.stderr.write("\n");
1145
1611
  const summary = formatSummaryText(results, outputDir);
1146
1612
  process.stderr.write(`${summary}
1147
1613
  `);
1148
1614
  } catch (err) {
1149
- await shutdownPool();
1615
+ await shutdownPool2();
1150
1616
  process.stderr.write(`Error: ${err instanceof Error ? err.message : String(err)}
1151
1617
  `);
1152
1618
  process.exit(1);
@@ -1465,6 +1931,348 @@ function buildStructuredReport(report) {
1465
1931
  route: report.route?.pattern ?? null
1466
1932
  };
1467
1933
  }
1934
+ var DEFAULT_TOKEN_FILE = "reactscope.tokens.json";
1935
+ var CONFIG_FILE = "reactscope.config.json";
1936
+ function isTTY2() {
1937
+ return process.stdout.isTTY === true;
1938
+ }
1939
+ function pad3(value, width) {
1940
+ return value.length >= width ? value.slice(0, width) : value + " ".repeat(width - value.length);
1941
+ }
1942
+ function buildTable2(headers, rows) {
1943
+ const colWidths = headers.map(
1944
+ (h, i) => Math.max(h.length, ...rows.map((r) => (r[i] ?? "").length))
1945
+ );
1946
+ const divider = colWidths.map((w) => "-".repeat(w)).join(" ");
1947
+ const headerRow = headers.map((h, i) => pad3(h, colWidths[i] ?? 0)).join(" ");
1948
+ const dataRows = rows.map(
1949
+ (row2) => row2.map((cell, i) => pad3(cell ?? "", colWidths[i] ?? 0)).join(" ")
1950
+ );
1951
+ return [headerRow, divider, ...dataRows].join("\n");
1952
+ }
1953
+ function resolveTokenFilePath(fileFlag) {
1954
+ if (fileFlag !== void 0) {
1955
+ return resolve(process.cwd(), fileFlag);
1956
+ }
1957
+ const configPath = resolve(process.cwd(), CONFIG_FILE);
1958
+ if (existsSync(configPath)) {
1959
+ try {
1960
+ const raw = readFileSync(configPath, "utf-8");
1961
+ const config = JSON.parse(raw);
1962
+ if (typeof config === "object" && config !== null && "tokens" in config && typeof config.tokens === "object" && config.tokens !== null && typeof config.tokens?.file === "string") {
1963
+ const file = config.tokens.file;
1964
+ return resolve(process.cwd(), file);
1965
+ }
1966
+ } catch {
1967
+ }
1968
+ }
1969
+ return resolve(process.cwd(), DEFAULT_TOKEN_FILE);
1970
+ }
1971
+ function loadTokens(absPath) {
1972
+ if (!existsSync(absPath)) {
1973
+ throw new Error(
1974
+ `Token file not found at ${absPath}.
1975
+ Create a reactscope.tokens.json file or use --file to specify a path.`
1976
+ );
1977
+ }
1978
+ const raw = readFileSync(absPath, "utf-8");
1979
+ return parseTokenFileSync(raw);
1980
+ }
1981
+ function getRawValue(node, segments) {
1982
+ const [head, ...rest] = segments;
1983
+ if (head === void 0) return null;
1984
+ const child = node[head];
1985
+ if (child === void 0 || child === null) return null;
1986
+ if (rest.length === 0) {
1987
+ if (typeof child === "object" && !Array.isArray(child) && "value" in child) {
1988
+ const v = child.value;
1989
+ return typeof v === "string" || typeof v === "number" ? v : null;
1990
+ }
1991
+ return null;
1992
+ }
1993
+ if (typeof child === "object" && !Array.isArray(child)) {
1994
+ return getRawValue(child, rest);
1995
+ }
1996
+ return null;
1997
+ }
1998
+ function buildResolutionChain(startPath, rawTokens) {
1999
+ const chain = [];
2000
+ const seen = /* @__PURE__ */ new Set();
2001
+ let current = startPath;
2002
+ while (!seen.has(current)) {
2003
+ seen.add(current);
2004
+ const rawValue = getRawValue(rawTokens, current.split("."));
2005
+ if (rawValue === null) break;
2006
+ chain.push({ path: current, rawValue: String(rawValue) });
2007
+ const refMatch = /^\{([^}]+)\}$/.exec(String(rawValue));
2008
+ if (refMatch === null) break;
2009
+ current = refMatch[1] ?? "";
2010
+ }
2011
+ return chain;
2012
+ }
2013
+ function registerGet2(tokensCmd) {
2014
+ 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) => {
2015
+ try {
2016
+ const filePath = resolveTokenFilePath(opts.file);
2017
+ const { tokens } = loadTokens(filePath);
2018
+ const resolver = new TokenResolver(tokens);
2019
+ const resolvedValue = resolver.resolve(tokenPath);
2020
+ const useJson = opts.format === "json" || opts.format !== "text" && !isTTY2();
2021
+ if (useJson) {
2022
+ const token = tokens.find((t) => t.path === tokenPath);
2023
+ process.stdout.write(
2024
+ `${JSON.stringify({ path: tokenPath, value: token?.value, resolvedValue, type: token?.type }, null, 2)}
2025
+ `
2026
+ );
2027
+ } else {
2028
+ process.stdout.write(`${resolvedValue}
2029
+ `);
2030
+ }
2031
+ } catch (err) {
2032
+ process.stderr.write(`Error: ${err instanceof Error ? err.message : String(err)}
2033
+ `);
2034
+ process.exit(1);
2035
+ }
2036
+ });
2037
+ }
2038
+ function registerList2(tokensCmd) {
2039
+ 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(
2040
+ (category, opts) => {
2041
+ try {
2042
+ const filePath = resolveTokenFilePath(opts.file);
2043
+ const { tokens } = loadTokens(filePath);
2044
+ const resolver = new TokenResolver(tokens);
2045
+ const filtered = resolver.list(opts.type, category);
2046
+ const useJson = opts.format === "json" || opts.format !== "table" && !isTTY2();
2047
+ if (useJson) {
2048
+ process.stdout.write(`${JSON.stringify(filtered, null, 2)}
2049
+ `);
2050
+ } else {
2051
+ if (filtered.length === 0) {
2052
+ process.stdout.write("No tokens found.\n");
2053
+ return;
2054
+ }
2055
+ const headers = ["PATH", "VALUE", "RESOLVED", "TYPE"];
2056
+ const rows = filtered.map((t) => [t.path, String(t.value), t.resolvedValue, t.type]);
2057
+ process.stdout.write(`${buildTable2(headers, rows)}
2058
+ `);
2059
+ }
2060
+ } catch (err) {
2061
+ process.stderr.write(`Error: ${err instanceof Error ? err.message : String(err)}
2062
+ `);
2063
+ process.exit(1);
2064
+ }
2065
+ }
2066
+ );
2067
+ }
2068
+ function registerSearch(tokensCmd) {
2069
+ 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(
2070
+ (value, opts) => {
2071
+ try {
2072
+ const filePath = resolveTokenFilePath(opts.file);
2073
+ const { tokens } = loadTokens(filePath);
2074
+ const resolver = new TokenResolver(tokens);
2075
+ const useJson = opts.format === "json" || opts.format !== "table" && !isTTY2();
2076
+ const typesToSearch = opts.type ? [opts.type] : [
2077
+ "color",
2078
+ "dimension",
2079
+ "fontFamily",
2080
+ "fontWeight",
2081
+ "number",
2082
+ "shadow",
2083
+ "duration",
2084
+ "cubicBezier"
2085
+ ];
2086
+ const exactMatches = [];
2087
+ const nearestMatches = [];
2088
+ for (const type of typesToSearch) {
2089
+ const exact = resolver.match(value, type);
2090
+ if (exact !== null) {
2091
+ exactMatches.push({
2092
+ path: exact.token.path,
2093
+ resolvedValue: exact.token.resolvedValue,
2094
+ type: exact.token.type,
2095
+ exact: true,
2096
+ distance: 0
2097
+ });
2098
+ }
2099
+ }
2100
+ if (exactMatches.length === 0 && opts.fuzzy) {
2101
+ for (const type of typesToSearch) {
2102
+ const typeTokens = tokens.filter((t) => t.type === type);
2103
+ if (typeTokens.length === 0) continue;
2104
+ try {
2105
+ const near = resolver.nearest(value, type);
2106
+ nearestMatches.push({
2107
+ path: near.token.path,
2108
+ resolvedValue: near.token.resolvedValue,
2109
+ type: near.token.type,
2110
+ exact: near.exact,
2111
+ distance: near.distance
2112
+ });
2113
+ } catch {
2114
+ }
2115
+ }
2116
+ nearestMatches.sort((a, b) => a.distance - b.distance);
2117
+ nearestMatches.splice(3);
2118
+ }
2119
+ const results = exactMatches.length > 0 ? exactMatches : nearestMatches;
2120
+ if (useJson) {
2121
+ process.stdout.write(`${JSON.stringify(results, null, 2)}
2122
+ `);
2123
+ } else {
2124
+ if (results.length === 0) {
2125
+ process.stdout.write(
2126
+ `No tokens found matching "${value}".
2127
+ Tip: use --fuzzy for nearest-match search.
2128
+ `
2129
+ );
2130
+ return;
2131
+ }
2132
+ const headers = ["PATH", "RESOLVED VALUE", "TYPE", "MATCH", "DISTANCE"];
2133
+ const rows = results.map((r) => [
2134
+ r.path,
2135
+ r.resolvedValue,
2136
+ r.type,
2137
+ r.exact ? "exact" : "nearest",
2138
+ r.exact ? "\u2014" : r.distance.toFixed(2)
2139
+ ]);
2140
+ process.stdout.write(`${buildTable2(headers, rows)}
2141
+ `);
2142
+ }
2143
+ } catch (err) {
2144
+ process.stderr.write(`Error: ${err instanceof Error ? err.message : String(err)}
2145
+ `);
2146
+ process.exit(1);
2147
+ }
2148
+ }
2149
+ );
2150
+ }
2151
+ function registerResolve(tokensCmd) {
2152
+ 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) => {
2153
+ try {
2154
+ const filePath = resolveTokenFilePath(opts.file);
2155
+ const absFilePath = filePath;
2156
+ const { tokens, rawFile } = loadTokens(absFilePath);
2157
+ const resolver = new TokenResolver(tokens);
2158
+ resolver.resolve(tokenPath);
2159
+ const chain = buildResolutionChain(tokenPath, rawFile.tokens);
2160
+ const useJson = opts.format === "json" || opts.format !== "text" && !isTTY2();
2161
+ if (useJson) {
2162
+ process.stdout.write(`${JSON.stringify({ path: tokenPath, chain }, null, 2)}
2163
+ `);
2164
+ } else {
2165
+ if (chain.length === 0) {
2166
+ process.stdout.write(`Token "${tokenPath}" not found.
2167
+ `);
2168
+ return;
2169
+ }
2170
+ const parts = chain.map((step, i) => {
2171
+ if (i < chain.length - 1) {
2172
+ return `${step.path} \u2192 ${step.rawValue}`;
2173
+ }
2174
+ return step.rawValue;
2175
+ });
2176
+ process.stdout.write(`${parts.join("\n ")}
2177
+ `);
2178
+ }
2179
+ } catch (err) {
2180
+ process.stderr.write(`Error: ${err instanceof Error ? err.message : String(err)}
2181
+ `);
2182
+ process.exit(1);
2183
+ }
2184
+ });
2185
+ }
2186
+ function registerValidate(tokensCmd) {
2187
+ tokensCmd.command("validate").description(
2188
+ "Validate the token file for errors (circular refs, missing refs, type mismatches)"
2189
+ ).option("--file <path>", "Path to token file (overrides config)").option("--format <fmt>", "Output format: json or text (default: auto-detect)").action((opts) => {
2190
+ try {
2191
+ const filePath = resolveTokenFilePath(opts.file);
2192
+ if (!existsSync(filePath)) {
2193
+ throw new Error(
2194
+ `Token file not found at ${filePath}.
2195
+ Create a reactscope.tokens.json file or use --file to specify a path.`
2196
+ );
2197
+ }
2198
+ const raw = readFileSync(filePath, "utf-8");
2199
+ const useJson = opts.format === "json" || opts.format !== "text" && !isTTY2();
2200
+ const errors = [];
2201
+ let parsed;
2202
+ try {
2203
+ parsed = JSON.parse(raw);
2204
+ } catch (err) {
2205
+ errors.push({
2206
+ code: "PARSE_ERROR",
2207
+ message: `Failed to parse token file as JSON: ${String(err)}`
2208
+ });
2209
+ outputValidationResult(filePath, errors, useJson);
2210
+ process.exit(1);
2211
+ }
2212
+ try {
2213
+ validateTokenFile(parsed);
2214
+ } catch (err) {
2215
+ if (err instanceof TokenValidationError) {
2216
+ for (const e of err.errors) {
2217
+ errors.push({ code: e.code, path: e.path, message: e.message });
2218
+ }
2219
+ outputValidationResult(filePath, errors, useJson);
2220
+ process.exit(1);
2221
+ }
2222
+ throw err;
2223
+ }
2224
+ try {
2225
+ parseTokenFileSync(raw);
2226
+ } catch (err) {
2227
+ if (err instanceof TokenParseError) {
2228
+ errors.push({ code: err.code, path: err.path, message: err.message });
2229
+ } else {
2230
+ errors.push({ code: "UNKNOWN", message: String(err) });
2231
+ }
2232
+ outputValidationResult(filePath, errors, useJson);
2233
+ process.exit(1);
2234
+ }
2235
+ outputValidationResult(filePath, errors, useJson);
2236
+ } catch (err) {
2237
+ process.stderr.write(`Error: ${err instanceof Error ? err.message : String(err)}
2238
+ `);
2239
+ process.exit(1);
2240
+ }
2241
+ });
2242
+ }
2243
+ function outputValidationResult(filePath, errors, useJson) {
2244
+ const valid = errors.length === 0;
2245
+ if (useJson) {
2246
+ process.stdout.write(`${JSON.stringify({ valid, file: filePath, errors }, null, 2)}
2247
+ `);
2248
+ } else {
2249
+ if (valid) {
2250
+ process.stdout.write(`\u2713 Token file is valid: ${filePath}
2251
+ `);
2252
+ } else {
2253
+ process.stderr.write(`\u2717 Token file has ${errors.length} error(s): ${filePath}
2254
+
2255
+ `);
2256
+ for (const e of errors) {
2257
+ const pathPrefix = e.path ? ` [${e.path}]` : "";
2258
+ process.stderr.write(` ${e.code}${pathPrefix}: ${e.message}
2259
+ `);
2260
+ }
2261
+ process.exit(1);
2262
+ }
2263
+ }
2264
+ }
2265
+ function createTokensCommand() {
2266
+ const tokensCmd = new Command("tokens").description(
2267
+ "Query and validate design tokens from a reactscope.tokens.json file"
2268
+ );
2269
+ registerGet2(tokensCmd);
2270
+ registerList2(tokensCmd);
2271
+ registerSearch(tokensCmd);
2272
+ registerResolve(tokensCmd);
2273
+ registerValidate(tokensCmd);
2274
+ return tokensCmd;
2275
+ }
1468
2276
 
1469
2277
  // src/program.ts
1470
2278
  function createProgram(options = {}) {
@@ -1552,9 +2360,11 @@ function createProgram(options = {}) {
1552
2360
  });
1553
2361
  program.addCommand(createManifestCommand());
1554
2362
  program.addCommand(createRenderCommand());
2363
+ program.addCommand(createTokensCommand());
2364
+ program.addCommand(createInstrumentCommand());
1555
2365
  return program;
1556
2366
  }
1557
2367
 
1558
- export { createManifestCommand, createProgram, isTTY, matchGlob };
2368
+ export { createManifestCommand, createProgram, createTokensCommand, isTTY, matchGlob };
1559
2369
  //# sourceMappingURL=index.js.map
1560
2370
  //# sourceMappingURL=index.js.map