@agent-scope/cli 1.7.0 → 1.9.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 readFileSync4 } from "fs";
4
+ import { readFileSync as readFileSync6 } from "fs";
5
5
  import { generateTest, loadTrace } from "@agent-scope/playwright";
6
- import { Command as Command4 } from "commander";
6
+ import { Command as Command6 } from "commander";
7
7
 
8
8
  // src/browser.ts
9
9
  import { writeFileSync } from "fs";
@@ -50,11 +50,485 @@ function writeReportToFile(report, outputPath, pretty) {
50
50
  writeFileSync(outputPath, json, "utf-8");
51
51
  }
52
52
 
53
+ // src/init/index.ts
54
+ import { appendFileSync, existsSync as existsSync2, mkdirSync, readFileSync as readFileSync2, writeFileSync as writeFileSync2 } from "fs";
55
+ import { join as join2 } from "path";
56
+ import * as readline from "readline";
57
+
58
+ // src/init/detect.ts
59
+ import { existsSync, readdirSync, readFileSync } from "fs";
60
+ import { join } from "path";
61
+ function hasConfigFile(dir, stem) {
62
+ if (!existsSync(dir)) return false;
63
+ try {
64
+ const entries = readdirSync(dir);
65
+ return entries.some((f) => f === stem || f.startsWith(`${stem}.`));
66
+ } catch {
67
+ return false;
68
+ }
69
+ }
70
+ function readSafe(path) {
71
+ try {
72
+ return readFileSync(path, "utf-8");
73
+ } catch {
74
+ return null;
75
+ }
76
+ }
77
+ function detectFramework(rootDir, packageDeps) {
78
+ if (hasConfigFile(rootDir, "next.config")) return "next";
79
+ if (hasConfigFile(rootDir, "vite.config")) return "vite";
80
+ if (hasConfigFile(rootDir, "remix.config")) return "remix";
81
+ if ("react-scripts" in packageDeps) return "cra";
82
+ return "unknown";
83
+ }
84
+ function detectPackageManager(rootDir) {
85
+ if (existsSync(join(rootDir, "bun.lock"))) return "bun";
86
+ if (existsSync(join(rootDir, "yarn.lock"))) return "yarn";
87
+ if (existsSync(join(rootDir, "pnpm-lock.yaml"))) return "pnpm";
88
+ if (existsSync(join(rootDir, "package-lock.json"))) return "npm";
89
+ return "npm";
90
+ }
91
+ function detectTypeScript(rootDir) {
92
+ const candidate = join(rootDir, "tsconfig.json");
93
+ if (existsSync(candidate)) {
94
+ return { typescript: true, tsconfigPath: candidate };
95
+ }
96
+ return { typescript: false, tsconfigPath: null };
97
+ }
98
+ var COMPONENT_DIRS = ["src/components", "src/app", "src/pages", "src/ui", "src/features", "src"];
99
+ var COMPONENT_EXTS = [".tsx", ".jsx"];
100
+ function detectComponentPatterns(rootDir, typescript) {
101
+ const patterns = [];
102
+ const ext = typescript ? "tsx" : "jsx";
103
+ const altExt = typescript ? "jsx" : "jsx";
104
+ for (const dir of COMPONENT_DIRS) {
105
+ const absDir = join(rootDir, dir);
106
+ if (!existsSync(absDir)) continue;
107
+ let hasComponents = false;
108
+ try {
109
+ const entries = readdirSync(absDir, { withFileTypes: true });
110
+ hasComponents = entries.some(
111
+ (e) => e.isFile() && COMPONENT_EXTS.some((x) => e.name.endsWith(x))
112
+ );
113
+ if (!hasComponents) {
114
+ hasComponents = entries.some(
115
+ (e) => e.isDirectory() && (() => {
116
+ try {
117
+ return readdirSync(join(absDir, e.name)).some(
118
+ (f) => COMPONENT_EXTS.some((x) => f.endsWith(x))
119
+ );
120
+ } catch {
121
+ return false;
122
+ }
123
+ })()
124
+ );
125
+ }
126
+ } catch {
127
+ continue;
128
+ }
129
+ if (hasComponents) {
130
+ patterns.push(`${dir}/**/*.${ext}`);
131
+ if (altExt !== ext) {
132
+ patterns.push(`${dir}/**/*.${altExt}`);
133
+ }
134
+ }
135
+ }
136
+ const unique = [...new Set(patterns)];
137
+ if (unique.length === 0) {
138
+ return [`**/*.${ext}`];
139
+ }
140
+ return unique;
141
+ }
142
+ var TAILWIND_STEMS = ["tailwind.config"];
143
+ var CSS_EXTS = [".css", ".scss", ".sass", ".less"];
144
+ var THEME_SUFFIXES = [".theme.ts", ".theme.js", ".theme.tsx"];
145
+ var CSS_CUSTOM_PROPS_RE = /:root\s*\{[^}]*--[a-zA-Z]/;
146
+ function detectTokenSources(rootDir) {
147
+ const sources = [];
148
+ for (const stem of TAILWIND_STEMS) {
149
+ if (hasConfigFile(rootDir, stem)) {
150
+ try {
151
+ const entries = readdirSync(rootDir);
152
+ const match = entries.find((f) => f === stem || f.startsWith(`${stem}.`));
153
+ if (match) {
154
+ sources.push({ kind: "tailwind-config", path: join(rootDir, match) });
155
+ }
156
+ } catch {
157
+ }
158
+ }
159
+ }
160
+ const srcDir = join(rootDir, "src");
161
+ const dirsToScan = existsSync(srcDir) ? [srcDir] : [];
162
+ for (const scanDir of dirsToScan) {
163
+ try {
164
+ const entries = readdirSync(scanDir, { withFileTypes: true });
165
+ for (const entry of entries) {
166
+ if (entry.isFile() && CSS_EXTS.some((x) => entry.name.endsWith(x))) {
167
+ const filePath = join(scanDir, entry.name);
168
+ const content = readSafe(filePath);
169
+ if (content !== null && CSS_CUSTOM_PROPS_RE.test(content)) {
170
+ sources.push({ kind: "css-custom-properties", path: filePath });
171
+ }
172
+ }
173
+ }
174
+ } catch {
175
+ }
176
+ }
177
+ if (existsSync(srcDir)) {
178
+ try {
179
+ const entries = readdirSync(srcDir);
180
+ for (const entry of entries) {
181
+ if (THEME_SUFFIXES.some((s) => entry.endsWith(s))) {
182
+ sources.push({ kind: "theme-file", path: join(srcDir, entry) });
183
+ }
184
+ }
185
+ } catch {
186
+ }
187
+ }
188
+ return sources;
189
+ }
190
+ function detectProject(rootDir) {
191
+ const pkgPath = join(rootDir, "package.json");
192
+ let packageDeps = {};
193
+ const pkgContent = readSafe(pkgPath);
194
+ if (pkgContent !== null) {
195
+ try {
196
+ const pkg = JSON.parse(pkgContent);
197
+ packageDeps = {
198
+ ...pkg.dependencies,
199
+ ...pkg.devDependencies
200
+ };
201
+ } catch {
202
+ }
203
+ }
204
+ const framework = detectFramework(rootDir, packageDeps);
205
+ const { typescript, tsconfigPath } = detectTypeScript(rootDir);
206
+ const packageManager = detectPackageManager(rootDir);
207
+ const componentPatterns = detectComponentPatterns(rootDir, typescript);
208
+ const tokenSources = detectTokenSources(rootDir);
209
+ return {
210
+ framework,
211
+ typescript,
212
+ tsconfigPath,
213
+ componentPatterns,
214
+ tokenSources,
215
+ packageManager
216
+ };
217
+ }
218
+
219
+ // src/init/index.ts
220
+ import { Command } from "commander";
221
+ function buildDefaultConfig(detected, tokenFile, outputDir) {
222
+ const include = detected.componentPatterns.length > 0 ? detected.componentPatterns : ["src/**/*.tsx"];
223
+ return {
224
+ components: {
225
+ include,
226
+ exclude: ["**/*.test.tsx", "**/*.stories.tsx"],
227
+ wrappers: { providers: [], globalCSS: [] }
228
+ },
229
+ render: {
230
+ viewport: { default: { width: 1280, height: 800 } },
231
+ theme: "light",
232
+ warmBrowser: true
233
+ },
234
+ tokens: {
235
+ file: tokenFile,
236
+ compliance: { threshold: 90 }
237
+ },
238
+ output: {
239
+ dir: outputDir,
240
+ sprites: { format: "png", cellPadding: 8, labelAxes: true },
241
+ json: { pretty: true }
242
+ },
243
+ ci: {
244
+ complianceThreshold: 90,
245
+ failOnA11yViolations: true,
246
+ failOnConsoleErrors: false,
247
+ baselinePath: `${outputDir}baseline/`
248
+ }
249
+ };
250
+ }
251
+ function createRL() {
252
+ return readline.createInterface({
253
+ input: process.stdin,
254
+ output: process.stdout
255
+ });
256
+ }
257
+ async function ask(rl, question) {
258
+ return new Promise((resolve6) => {
259
+ rl.question(question, (answer) => {
260
+ resolve6(answer.trim());
261
+ });
262
+ });
263
+ }
264
+ async function askWithDefault(rl, label, defaultValue) {
265
+ const answer = await ask(rl, ` ${label} [${defaultValue}]: `);
266
+ return answer.length > 0 ? answer : defaultValue;
267
+ }
268
+ function ensureGitignoreEntry(rootDir, entry) {
269
+ const gitignorePath = join2(rootDir, ".gitignore");
270
+ if (existsSync2(gitignorePath)) {
271
+ const content = readFileSync2(gitignorePath, "utf-8");
272
+ const normalised = entry.replace(/\/$/, "");
273
+ const lines = content.split("\n").map((l) => l.trim());
274
+ if (lines.includes(entry) || lines.includes(normalised)) {
275
+ return;
276
+ }
277
+ const suffix = content.endsWith("\n") ? "" : "\n";
278
+ appendFileSync(gitignorePath, `${suffix}${entry}
279
+ `);
280
+ } else {
281
+ writeFileSync2(gitignorePath, `${entry}
282
+ `);
283
+ }
284
+ }
285
+ function scaffoldConfig(rootDir, config) {
286
+ const path = join2(rootDir, "reactscope.config.json");
287
+ writeFileSync2(path, `${JSON.stringify(config, null, 2)}
288
+ `);
289
+ return path;
290
+ }
291
+ function scaffoldTokenFile(rootDir, tokenFile) {
292
+ const path = join2(rootDir, tokenFile);
293
+ if (!existsSync2(path)) {
294
+ const stub = {
295
+ $schema: "https://raw.githubusercontent.com/FlatFilers/Scope/main/packages/tokens/schema.json",
296
+ tokens: {}
297
+ };
298
+ writeFileSync2(path, `${JSON.stringify(stub, null, 2)}
299
+ `);
300
+ }
301
+ return path;
302
+ }
303
+ function scaffoldOutputDir(rootDir, outputDir) {
304
+ const dirPath = join2(rootDir, outputDir);
305
+ mkdirSync(dirPath, { recursive: true });
306
+ const keepPath = join2(dirPath, ".gitkeep");
307
+ if (!existsSync2(keepPath)) {
308
+ writeFileSync2(keepPath, "");
309
+ }
310
+ return dirPath;
311
+ }
312
+ async function runInit(options) {
313
+ const rootDir = options.cwd ?? process.cwd();
314
+ const configPath = join2(rootDir, "reactscope.config.json");
315
+ const created = [];
316
+ if (existsSync2(configPath) && !options.force) {
317
+ const msg = "reactscope.config.json already exists. Run with --force to overwrite.";
318
+ process.stderr.write(`\u26A0\uFE0F ${msg}
319
+ `);
320
+ return { success: false, message: msg, created: [], skipped: true };
321
+ }
322
+ const detected = detectProject(rootDir);
323
+ const defaultTokenFile = "reactscope.tokens.json";
324
+ const defaultOutputDir = ".reactscope/";
325
+ let config = buildDefaultConfig(detected, defaultTokenFile, defaultOutputDir);
326
+ if (options.yes) {
327
+ process.stdout.write("\n\u{1F50D} Detected project settings:\n");
328
+ process.stdout.write(` Framework : ${detected.framework}
329
+ `);
330
+ process.stdout.write(` TypeScript : ${detected.typescript}
331
+ `);
332
+ process.stdout.write(` Include globs : ${config.components.include.join(", ")}
333
+ `);
334
+ process.stdout.write(` Token file : ${config.tokens.file}
335
+ `);
336
+ process.stdout.write(` Output dir : ${config.output.dir}
337
+
338
+ `);
339
+ } else {
340
+ const rl = createRL();
341
+ process.stdout.write("\n\u{1F680} scope init \u2014 project configuration\n");
342
+ process.stdout.write(" Press Enter to accept the detected value shown in brackets.\n\n");
343
+ try {
344
+ process.stdout.write(` Detected framework: ${detected.framework}
345
+ `);
346
+ const includeRaw = await askWithDefault(
347
+ rl,
348
+ "Component include patterns (comma-separated)",
349
+ config.components.include.join(", ")
350
+ );
351
+ config.components.include = includeRaw.split(",").map((s) => s.trim()).filter(Boolean);
352
+ const excludeRaw = await askWithDefault(
353
+ rl,
354
+ "Component exclude patterns (comma-separated)",
355
+ config.components.exclude.join(", ")
356
+ );
357
+ config.components.exclude = excludeRaw.split(",").map((s) => s.trim()).filter(Boolean);
358
+ const tokenFile = await askWithDefault(rl, "Token file location", config.tokens.file);
359
+ config.tokens.file = tokenFile;
360
+ config.ci.baselinePath = `${config.output.dir}baseline/`;
361
+ const outputDir = await askWithDefault(rl, "Output directory", config.output.dir);
362
+ config.output.dir = outputDir.endsWith("/") ? outputDir : `${outputDir}/`;
363
+ config.ci.baselinePath = `${config.output.dir}baseline/`;
364
+ config = buildDefaultConfig(detected, config.tokens.file, config.output.dir);
365
+ config.components.include = includeRaw.split(",").map((s) => s.trim()).filter(Boolean);
366
+ config.components.exclude = excludeRaw.split(",").map((s) => s.trim()).filter(Boolean);
367
+ } finally {
368
+ rl.close();
369
+ }
370
+ process.stdout.write("\n");
371
+ }
372
+ const cfgPath = scaffoldConfig(rootDir, config);
373
+ created.push(cfgPath);
374
+ const tokPath = scaffoldTokenFile(rootDir, config.tokens.file);
375
+ created.push(tokPath);
376
+ const outDirPath = scaffoldOutputDir(rootDir, config.output.dir);
377
+ created.push(outDirPath);
378
+ ensureGitignoreEntry(rootDir, config.output.dir);
379
+ process.stdout.write("\u2705 Scope project initialised!\n\n");
380
+ process.stdout.write(" Created files:\n");
381
+ for (const p of created) {
382
+ process.stdout.write(` ${p}
383
+ `);
384
+ }
385
+ process.stdout.write("\n Next steps: run `scope manifest` to scan your components.\n\n");
386
+ return {
387
+ success: true,
388
+ message: "Project initialised successfully.",
389
+ created,
390
+ skipped: false
391
+ };
392
+ }
393
+ function createInitCommand() {
394
+ return new Command("init").description("Initialise a Scope project \u2014 scaffold reactscope.config.json and friends").option("-y, --yes", "Accept all detected defaults without prompting", false).option("--force", "Overwrite existing reactscope.config.json if present", false).action(async (opts) => {
395
+ try {
396
+ const result = await runInit({ yes: opts.yes, force: opts.force });
397
+ if (!result.success && !result.skipped) {
398
+ process.exit(1);
399
+ }
400
+ } catch (err) {
401
+ process.stderr.write(`Error: ${err instanceof Error ? err.message : String(err)}
402
+ `);
403
+ process.exit(1);
404
+ }
405
+ });
406
+ }
407
+
408
+ // src/instrument/renders.ts
409
+ import { resolve as resolve2 } from "path";
410
+ import { BrowserPool } from "@agent-scope/render";
411
+ import { Command as Command3 } from "commander";
412
+
413
+ // src/component-bundler.ts
414
+ import { dirname } from "path";
415
+ import * as esbuild from "esbuild";
416
+ async function buildComponentHarness(filePath, componentName, props, viewportWidth, projectCss) {
417
+ const bundledScript = await bundleComponentToIIFE(filePath, componentName, props);
418
+ return wrapInHtml(bundledScript, viewportWidth, projectCss);
419
+ }
420
+ async function bundleComponentToIIFE(filePath, componentName, props) {
421
+ const propsJson = JSON.stringify(props).replace(/<\/script>/gi, "<\\/script>");
422
+ const wrapperCode = (
423
+ /* ts */
424
+ `
425
+ import * as __scopeMod from ${JSON.stringify(filePath)};
426
+ import { createRoot } from "react-dom/client";
427
+ import { createElement } from "react";
428
+
429
+ (function scopeRenderHarness() {
430
+ var Component =
431
+ __scopeMod["default"] ||
432
+ __scopeMod[${JSON.stringify(componentName)}] ||
433
+ (Object.values(__scopeMod).find(
434
+ function(v) { return typeof v === "function" && /^[A-Z]/.test(v.name || ""); }
435
+ ));
436
+
437
+ if (!Component) {
438
+ window.__SCOPE_RENDER_ERROR__ =
439
+ "No renderable component found. Checked: default, " +
440
+ ${JSON.stringify(componentName)} + ", and PascalCase named exports. " +
441
+ "Available exports: " + Object.keys(__scopeMod).join(", ");
442
+ window.__SCOPE_RENDER_COMPLETE__ = true;
443
+ return;
444
+ }
445
+
446
+ try {
447
+ var props = ${propsJson};
448
+ var rootEl = document.getElementById("scope-root");
449
+ if (!rootEl) {
450
+ window.__SCOPE_RENDER_ERROR__ = "#scope-root element not found";
451
+ window.__SCOPE_RENDER_COMPLETE__ = true;
452
+ return;
453
+ }
454
+ createRoot(rootEl).render(createElement(Component, props));
455
+ // Use requestAnimationFrame to let React flush the render
456
+ requestAnimationFrame(function() {
457
+ window.__SCOPE_RENDER_COMPLETE__ = true;
458
+ });
459
+ } catch (err) {
460
+ window.__SCOPE_RENDER_ERROR__ = err instanceof Error ? err.message : String(err);
461
+ window.__SCOPE_RENDER_COMPLETE__ = true;
462
+ }
463
+ })();
464
+ `
465
+ );
466
+ const result = await esbuild.build({
467
+ stdin: {
468
+ contents: wrapperCode,
469
+ // Resolve relative imports (within the component's dir)
470
+ resolveDir: dirname(filePath),
471
+ loader: "tsx",
472
+ sourcefile: "__scope_harness__.tsx"
473
+ },
474
+ bundle: true,
475
+ format: "iife",
476
+ write: false,
477
+ platform: "browser",
478
+ jsx: "automatic",
479
+ jsxImportSource: "react",
480
+ target: "es2020",
481
+ // Bundle everything — no externals
482
+ external: [],
483
+ define: {
484
+ "process.env.NODE_ENV": '"development"',
485
+ global: "globalThis"
486
+ },
487
+ logLevel: "silent",
488
+ // Suppress "React must be in scope" warnings from old JSX (we use automatic)
489
+ banner: {
490
+ js: "/* @agent-scope/cli component harness */"
491
+ }
492
+ });
493
+ if (result.errors.length > 0) {
494
+ const msg = result.errors.map((e) => `${e.text}${e.location ? ` (${e.location.file}:${e.location.line})` : ""}`).join("\n");
495
+ throw new Error(`esbuild failed to bundle component:
496
+ ${msg}`);
497
+ }
498
+ const outputFile = result.outputFiles?.[0];
499
+ if (outputFile === void 0 || outputFile.text.length === 0) {
500
+ throw new Error("esbuild produced no output");
501
+ }
502
+ return outputFile.text;
503
+ }
504
+ function wrapInHtml(bundledScript, viewportWidth, projectCss) {
505
+ const projectStyleBlock = projectCss != null && projectCss.length > 0 ? `<style id="scope-project-css">
506
+ ${projectCss.replace(/<\/style>/gi, "<\\/style>")}
507
+ </style>` : "";
508
+ return `<!DOCTYPE html>
509
+ <html lang="en">
510
+ <head>
511
+ <meta charset="UTF-8" />
512
+ <meta name="viewport" content="width=${viewportWidth}, initial-scale=1.0" />
513
+ <style>
514
+ *, *::before, *::after { box-sizing: border-box; }
515
+ html, body { margin: 0; padding: 0; background: #fff; font-family: system-ui, sans-serif; }
516
+ #scope-root { display: inline-block; min-width: 1px; min-height: 1px; }
517
+ </style>
518
+ ${projectStyleBlock}
519
+ </head>
520
+ <body>
521
+ <div id="scope-root" data-reactscope-root></div>
522
+ <script>${bundledScript}</script>
523
+ </body>
524
+ </html>`;
525
+ }
526
+
53
527
  // src/manifest-commands.ts
54
- import { existsSync, mkdirSync, readFileSync, writeFileSync as writeFileSync2 } from "fs";
528
+ import { existsSync as existsSync3, mkdirSync as mkdirSync2, readFileSync as readFileSync3, writeFileSync as writeFileSync3 } from "fs";
55
529
  import { resolve } from "path";
56
530
  import { generateManifest } from "@agent-scope/manifest";
57
- import { Command } from "commander";
531
+ import { Command as Command2 } from "commander";
58
532
 
59
533
  // src/manifest-formatter.ts
60
534
  function isTTY() {
@@ -155,11 +629,11 @@ function matchGlob(pattern, value) {
155
629
  var MANIFEST_PATH = ".reactscope/manifest.json";
156
630
  function loadManifest(manifestPath = MANIFEST_PATH) {
157
631
  const absPath = resolve(process.cwd(), manifestPath);
158
- if (!existsSync(absPath)) {
632
+ if (!existsSync3(absPath)) {
159
633
  throw new Error(`Manifest not found at ${absPath}.
160
634
  Run \`scope manifest generate\` first.`);
161
635
  }
162
- const raw = readFileSync(absPath, "utf-8");
636
+ const raw = readFileSync3(absPath, "utf-8");
163
637
  return JSON.parse(raw);
164
638
  }
165
639
  function resolveFormat(formatFlag) {
@@ -278,178 +752,49 @@ function registerQuery(manifestCmd) {
278
752
  );
279
753
  }
280
754
  function registerGenerate(manifestCmd) {
281
- manifestCmd.command("generate").description(
282
- "Generate the component manifest from source and write to .reactscope/manifest.json"
283
- ).option("--root <path>", "Project root directory (default: cwd)").option("--output <path>", "Output path for manifest.json", MANIFEST_PATH).option("--include <globs>", "Comma-separated glob patterns to include").option("--exclude <globs>", "Comma-separated glob patterns to exclude").action(async (opts) => {
284
- try {
285
- const rootDir = resolve(process.cwd(), opts.root ?? ".");
286
- const outputPath = resolve(process.cwd(), opts.output);
287
- const include = opts.include?.split(",").map((s) => s.trim());
288
- const exclude = opts.exclude?.split(",").map((s) => s.trim());
289
- process.stderr.write(`Scanning ${rootDir} for React components...
290
- `);
291
- const manifest = await generateManifest({
292
- rootDir,
293
- ...include !== void 0 && { include },
294
- ...exclude !== void 0 && { exclude }
295
- });
296
- const componentCount = Object.keys(manifest.components).length;
297
- process.stderr.write(`Found ${componentCount} components.
298
- `);
299
- const outputDir = outputPath.replace(/\/[^/]+$/, "");
300
- if (!existsSync(outputDir)) {
301
- mkdirSync(outputDir, { recursive: true });
302
- }
303
- writeFileSync2(outputPath, JSON.stringify(manifest, null, 2), "utf-8");
304
- process.stderr.write(`Manifest written to ${outputPath}
305
- `);
306
- process.stdout.write(`${outputPath}
307
- `);
308
- } catch (err) {
309
- process.stderr.write(`Error: ${err instanceof Error ? err.message : String(err)}
310
- `);
311
- process.exit(1);
312
- }
313
- });
314
- }
315
- function createManifestCommand() {
316
- const manifestCmd = new Command("manifest").description(
317
- "Query and explore the component manifest"
318
- );
319
- registerList(manifestCmd);
320
- registerGet(manifestCmd);
321
- registerQuery(manifestCmd);
322
- registerGenerate(manifestCmd);
323
- return manifestCmd;
324
- }
325
-
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 */"
755
+ manifestCmd.command("generate").description(
756
+ "Generate the component manifest from source and write to .reactscope/manifest.json"
757
+ ).option("--root <path>", "Project root directory (default: cwd)").option("--output <path>", "Output path for manifest.json", MANIFEST_PATH).option("--include <globs>", "Comma-separated glob patterns to include").option("--exclude <globs>", "Comma-separated glob patterns to exclude").action(async (opts) => {
758
+ try {
759
+ const rootDir = resolve(process.cwd(), opts.root ?? ".");
760
+ const outputPath = resolve(process.cwd(), opts.output);
761
+ const include = opts.include?.split(",").map((s) => s.trim());
762
+ const exclude = opts.exclude?.split(",").map((s) => s.trim());
763
+ process.stderr.write(`Scanning ${rootDir} for React components...
764
+ `);
765
+ const manifest = await generateManifest({
766
+ rootDir,
767
+ ...include !== void 0 && { include },
768
+ ...exclude !== void 0 && { exclude }
769
+ });
770
+ const componentCount = Object.keys(manifest.components).length;
771
+ process.stderr.write(`Found ${componentCount} components.
772
+ `);
773
+ const outputDir = outputPath.replace(/\/[^/]+$/, "");
774
+ if (!existsSync3(outputDir)) {
775
+ mkdirSync2(outputDir, { recursive: true });
776
+ }
777
+ writeFileSync3(outputPath, JSON.stringify(manifest, null, 2), "utf-8");
778
+ process.stderr.write(`Manifest written to ${outputPath}
779
+ `);
780
+ process.stdout.write(`${outputPath}
781
+ `);
782
+ } catch (err) {
783
+ process.stderr.write(`Error: ${err instanceof Error ? err.message : String(err)}
784
+ `);
785
+ process.exit(1);
419
786
  }
420
787
  });
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
788
  }
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>`;
789
+ function createManifestCommand() {
790
+ const manifestCmd = new Command2("manifest").description(
791
+ "Query and explore the component manifest"
792
+ );
793
+ registerList(manifestCmd);
794
+ registerGet(manifestCmd);
795
+ registerQuery(manifestCmd);
796
+ registerGenerate(manifestCmd);
797
+ return manifestCmd;
453
798
  }
454
799
 
455
800
  // src/render-formatter.ts
@@ -612,10 +957,492 @@ function csvEscape(value) {
612
957
  return value;
613
958
  }
614
959
 
960
+ // src/instrument/renders.ts
961
+ var MANIFEST_PATH2 = ".reactscope/manifest.json";
962
+ function determineTrigger(event) {
963
+ if (event.forceUpdate) return "force_update";
964
+ if (event.stateChanged) return "state_change";
965
+ if (event.propsChanged) return "props_change";
966
+ if (event.contextChanged) return "context_change";
967
+ if (event.hookDepsChanged) return "hook_dependency";
968
+ return "parent_rerender";
969
+ }
970
+ function isWastedRender(event) {
971
+ return !event.propsChanged && !event.stateChanged && !event.contextChanged && !event.memoized;
972
+ }
973
+ function buildCausalityChains(rawEvents) {
974
+ const result = [];
975
+ const componentLastRender = /* @__PURE__ */ new Map();
976
+ for (const raw of rawEvents) {
977
+ const trigger = determineTrigger(raw);
978
+ const wasted = isWastedRender(raw);
979
+ const chain = [];
980
+ let current = raw;
981
+ const visited = /* @__PURE__ */ new Set();
982
+ while (true) {
983
+ if (visited.has(current.component)) break;
984
+ visited.add(current.component);
985
+ const currentTrigger = determineTrigger(current);
986
+ chain.unshift({
987
+ component: current.component,
988
+ trigger: currentTrigger,
989
+ propsChanged: current.propsChanged,
990
+ stateChanged: current.stateChanged,
991
+ contextChanged: current.contextChanged
992
+ });
993
+ if (currentTrigger !== "parent_rerender" || current.parentComponent === null) {
994
+ break;
995
+ }
996
+ const parentEvent = componentLastRender.get(current.parentComponent);
997
+ if (parentEvent === void 0) break;
998
+ current = parentEvent;
999
+ }
1000
+ const rootCause = chain[0];
1001
+ const cascadeRenders = rawEvents.filter((e) => {
1002
+ if (rootCause === void 0) return false;
1003
+ return e.component !== rootCause.component && !e.stateChanged && !e.propsChanged;
1004
+ });
1005
+ result.push({
1006
+ component: raw.component,
1007
+ renderIndex: raw.renderIndex,
1008
+ trigger,
1009
+ propsChanged: raw.propsChanged,
1010
+ stateChanged: raw.stateChanged,
1011
+ contextChanged: raw.contextChanged,
1012
+ memoized: raw.memoized,
1013
+ wasted,
1014
+ chain,
1015
+ cascade: {
1016
+ totalRendersTriggered: cascadeRenders.length,
1017
+ uniqueComponents: new Set(cascadeRenders.map((e) => e.component)).size,
1018
+ unchangedPropRenders: cascadeRenders.filter((e) => !e.propsChanged).length
1019
+ }
1020
+ });
1021
+ componentLastRender.set(raw.component, raw);
1022
+ }
1023
+ return result;
1024
+ }
1025
+ function applyHeuristicFlags(renders) {
1026
+ const flags = [];
1027
+ const byComponent = /* @__PURE__ */ new Map();
1028
+ for (const r of renders) {
1029
+ if (!byComponent.has(r.component)) byComponent.set(r.component, []);
1030
+ byComponent.get(r.component).push(r);
1031
+ }
1032
+ for (const [component, events] of byComponent) {
1033
+ const wastedCount = events.filter((e) => e.wasted).length;
1034
+ const totalCount = events.length;
1035
+ if (wastedCount > 0) {
1036
+ flags.push({
1037
+ id: "WASTED_RENDER",
1038
+ severity: "warning",
1039
+ component,
1040
+ detail: `${wastedCount}/${totalCount} renders were wasted \u2014 unchanged props/state/context, not memoized`,
1041
+ data: { wastedCount, totalCount, wastedRatio: wastedCount / totalCount }
1042
+ });
1043
+ }
1044
+ for (const event of events) {
1045
+ if (event.cascade.totalRendersTriggered > 50) {
1046
+ flags.push({
1047
+ id: "RENDER_CASCADE",
1048
+ severity: "warning",
1049
+ component,
1050
+ detail: `State change in ${component} triggered ${event.cascade.totalRendersTriggered} downstream re-renders`,
1051
+ data: {
1052
+ totalRendersTriggered: event.cascade.totalRendersTriggered,
1053
+ uniqueComponents: event.cascade.uniqueComponents,
1054
+ unchangedPropRenders: event.cascade.unchangedPropRenders
1055
+ }
1056
+ });
1057
+ break;
1058
+ }
1059
+ }
1060
+ }
1061
+ return flags;
1062
+ }
1063
+ function buildInstrumentationScript() {
1064
+ return (
1065
+ /* js */
1066
+ `
1067
+ (function installScopeRenderInstrumentation() {
1068
+ window.__SCOPE_RENDER_EVENTS__ = [];
1069
+ window.__SCOPE_RENDER_INDEX__ = 0;
1070
+
1071
+ var hook = window.__REACT_DEVTOOLS_GLOBAL_HOOK__;
1072
+ if (!hook) return;
1073
+
1074
+ var originalOnCommit = hook.onCommitFiberRoot;
1075
+ var renderedComponents = new Map(); // componentName -> { lastProps, lastState }
1076
+
1077
+ function extractName(fiber) {
1078
+ if (!fiber) return 'Unknown';
1079
+ var type = fiber.type;
1080
+ if (!type) return 'Unknown';
1081
+ if (typeof type === 'string') return type; // host element
1082
+ if (typeof type === 'function') return type.displayName || type.name || 'Anonymous';
1083
+ if (type.displayName) return type.displayName;
1084
+ if (type.render && typeof type.render === 'function') {
1085
+ return type.render.displayName || type.render.name || 'Anonymous';
1086
+ }
1087
+ return 'Anonymous';
1088
+ }
1089
+
1090
+ function isMemoized(fiber) {
1091
+ // MemoComponent = 14, SimpleMemoComponent = 15
1092
+ return fiber.tag === 14 || fiber.tag === 15;
1093
+ }
1094
+
1095
+ function isComponent(fiber) {
1096
+ // FunctionComponent=0, ClassComponent=1, MemoComponent=14, SimpleMemoComponent=15
1097
+ return fiber.tag === 0 || fiber.tag === 1 || fiber.tag === 14 || fiber.tag === 15;
1098
+ }
1099
+
1100
+ function shallowEqual(a, b) {
1101
+ if (a === b) return true;
1102
+ if (!a || !b) return a === b;
1103
+ var keysA = Object.keys(a);
1104
+ var keysB = Object.keys(b);
1105
+ if (keysA.length !== keysB.length) return false;
1106
+ for (var i = 0; i < keysA.length; i++) {
1107
+ var k = keysA[i];
1108
+ if (k === 'children') continue; // ignore children prop
1109
+ if (a[k] !== b[k]) return false;
1110
+ }
1111
+ return true;
1112
+ }
1113
+
1114
+ function getParentComponentName(fiber) {
1115
+ var parent = fiber.return;
1116
+ while (parent) {
1117
+ if (isComponent(parent)) {
1118
+ var name = extractName(parent);
1119
+ if (name && name !== 'Unknown' && !/^[a-z]/.test(name)) return name;
1120
+ }
1121
+ parent = parent.return;
1122
+ }
1123
+ return null;
1124
+ }
1125
+
1126
+ function walkCommit(fiber) {
1127
+ if (!fiber) return;
1128
+
1129
+ if (isComponent(fiber)) {
1130
+ var name = extractName(fiber);
1131
+ if (name && name !== 'Unknown' && !/^[a-z]/.test(name)) {
1132
+ var memoized = isMemoized(fiber);
1133
+ var currentProps = fiber.memoizedProps || {};
1134
+ var prev = renderedComponents.get(name);
1135
+
1136
+ var propsChanged = true;
1137
+ var stateChanged = false;
1138
+ var contextChanged = false;
1139
+ var hookDepsChanged = false;
1140
+ var forceUpdate = false;
1141
+
1142
+ if (prev) {
1143
+ propsChanged = !shallowEqual(prev.lastProps, currentProps);
1144
+ }
1145
+
1146
+ // State: check memoizedState chain
1147
+ var memoizedState = fiber.memoizedState;
1148
+ if (prev && prev.lastStateSerialized !== undefined) {
1149
+ try {
1150
+ var stateSig = JSON.stringify(memoizedState ? memoizedState.memoizedState : null);
1151
+ stateChanged = stateSig !== prev.lastStateSerialized;
1152
+ } catch (_) {
1153
+ stateChanged = false;
1154
+ }
1155
+ }
1156
+
1157
+ // Context: use _debugHookTypes or check dependencies
1158
+ var deps = fiber.dependencies;
1159
+ if (deps && deps.firstContext) {
1160
+ contextChanged = true; // conservative: context dep present = may have changed
1161
+ }
1162
+
1163
+ var stateSig;
1164
+ try {
1165
+ stateSig = JSON.stringify(memoizedState ? memoizedState.memoizedState : null);
1166
+ } catch (_) {
1167
+ stateSig = null;
1168
+ }
1169
+
1170
+ renderedComponents.set(name, {
1171
+ lastProps: currentProps,
1172
+ lastStateSerialized: stateSig,
1173
+ });
1174
+
1175
+ var parentName = getParentComponentName(fiber);
1176
+
1177
+ window.__SCOPE_RENDER_EVENTS__.push({
1178
+ component: name,
1179
+ renderIndex: window.__SCOPE_RENDER_INDEX__++,
1180
+ propsChanged: prev ? propsChanged : false,
1181
+ stateChanged: stateChanged,
1182
+ contextChanged: contextChanged,
1183
+ memoized: memoized,
1184
+ parentComponent: parentName,
1185
+ hookDepsChanged: hookDepsChanged,
1186
+ forceUpdate: forceUpdate,
1187
+ });
1188
+ }
1189
+ }
1190
+
1191
+ walkCommit(fiber.child);
1192
+ walkCommit(fiber.sibling);
1193
+ }
1194
+
1195
+ hook.onCommitFiberRoot = function(rendererID, root, priorityLevel) {
1196
+ if (typeof originalOnCommit === 'function') {
1197
+ originalOnCommit.call(hook, rendererID, root, priorityLevel);
1198
+ }
1199
+ var wipRoot = root && root.current && root.current.alternate;
1200
+ if (wipRoot) walkCommit(wipRoot);
1201
+ };
1202
+ })();
1203
+ `
1204
+ );
1205
+ }
1206
+ async function replayInteraction(page, steps) {
1207
+ for (const step of steps) {
1208
+ switch (step.action) {
1209
+ case "click":
1210
+ if (step.target !== void 0) {
1211
+ await page.click(step.target, { timeout: 5e3 }).catch(() => {
1212
+ });
1213
+ }
1214
+ break;
1215
+ case "type":
1216
+ if (step.target !== void 0 && step.text !== void 0) {
1217
+ await page.fill(step.target, step.text, { timeout: 5e3 }).catch(async () => {
1218
+ await page.type(step.target, step.text, { timeout: 5e3 }).catch(() => {
1219
+ });
1220
+ });
1221
+ }
1222
+ break;
1223
+ case "hover":
1224
+ if (step.target !== void 0) {
1225
+ await page.hover(step.target, { timeout: 5e3 }).catch(() => {
1226
+ });
1227
+ }
1228
+ break;
1229
+ case "blur":
1230
+ if (step.target !== void 0) {
1231
+ await page.locator(step.target).blur({ timeout: 5e3 }).catch(() => {
1232
+ });
1233
+ }
1234
+ break;
1235
+ case "focus":
1236
+ if (step.target !== void 0) {
1237
+ await page.focus(step.target, { timeout: 5e3 }).catch(() => {
1238
+ });
1239
+ }
1240
+ break;
1241
+ case "scroll":
1242
+ if (step.target !== void 0) {
1243
+ await page.locator(step.target).scrollIntoViewIfNeeded({ timeout: 5e3 }).catch(() => {
1244
+ });
1245
+ }
1246
+ break;
1247
+ case "wait": {
1248
+ const timeout = step.timeout ?? 1e3;
1249
+ if (step.condition === "idle") {
1250
+ await page.waitForLoadState("networkidle", { timeout }).catch(() => {
1251
+ });
1252
+ } else {
1253
+ await page.waitForTimeout(timeout);
1254
+ }
1255
+ break;
1256
+ }
1257
+ default:
1258
+ break;
1259
+ }
1260
+ }
1261
+ }
1262
+ var _pool = null;
1263
+ async function getPool() {
1264
+ if (_pool === null) {
1265
+ _pool = new BrowserPool({
1266
+ size: { browsers: 1, pagesPerBrowser: 2 },
1267
+ viewportWidth: 1280,
1268
+ viewportHeight: 800
1269
+ });
1270
+ await _pool.init();
1271
+ }
1272
+ return _pool;
1273
+ }
1274
+ async function shutdownPool() {
1275
+ if (_pool !== null) {
1276
+ await _pool.close();
1277
+ _pool = null;
1278
+ }
1279
+ }
1280
+ async function analyzeRenders(options) {
1281
+ const manifestPath = options.manifestPath ?? MANIFEST_PATH2;
1282
+ const manifest = loadManifest(manifestPath);
1283
+ const descriptor = manifest.components[options.componentName];
1284
+ if (descriptor === void 0) {
1285
+ const available = Object.keys(manifest.components).slice(0, 5).join(", ");
1286
+ throw new Error(
1287
+ `Component "${options.componentName}" not found in manifest.
1288
+ Available: ${available}`
1289
+ );
1290
+ }
1291
+ const rootDir = process.cwd();
1292
+ const filePath = resolve2(rootDir, descriptor.filePath);
1293
+ const htmlHarness = await buildComponentHarness(filePath, options.componentName, {}, 1280);
1294
+ const pool = await getPool();
1295
+ const slot = await pool.acquire();
1296
+ const { page } = slot;
1297
+ const startMs = performance.now();
1298
+ try {
1299
+ await page.addInitScript(buildInstrumentationScript());
1300
+ await page.setContent(htmlHarness, { waitUntil: "load" });
1301
+ await page.waitForFunction(
1302
+ () => window.__SCOPE_RENDER_COMPLETE__ === true,
1303
+ { timeout: 15e3 }
1304
+ );
1305
+ await page.waitForTimeout(100);
1306
+ await page.evaluate(() => {
1307
+ window.__SCOPE_RENDER_EVENTS__ = [];
1308
+ window.__SCOPE_RENDER_INDEX__ = 0;
1309
+ });
1310
+ await replayInteraction(page, options.interaction);
1311
+ await page.waitForTimeout(200);
1312
+ const interactionDurationMs = performance.now() - startMs;
1313
+ const rawEvents = await page.evaluate(() => {
1314
+ return window.__SCOPE_RENDER_EVENTS__ ?? [];
1315
+ });
1316
+ const renders = buildCausalityChains(rawEvents);
1317
+ const flags = applyHeuristicFlags(renders);
1318
+ const uniqueComponents = new Set(renders.map((r) => r.component)).size;
1319
+ const wastedRenders = renders.filter((r) => r.wasted).length;
1320
+ return {
1321
+ component: options.componentName,
1322
+ interaction: options.interaction,
1323
+ summary: {
1324
+ totalRenders: renders.length,
1325
+ uniqueComponents,
1326
+ wastedRenders,
1327
+ interactionDurationMs: Math.round(interactionDurationMs)
1328
+ },
1329
+ renders,
1330
+ flags
1331
+ };
1332
+ } finally {
1333
+ pool.release(slot);
1334
+ }
1335
+ }
1336
+ function formatRendersTable(result) {
1337
+ const lines = [];
1338
+ lines.push(`
1339
+ \u{1F50D} Re-render Analysis: ${result.component}`);
1340
+ lines.push(`${"\u2500".repeat(60)}`);
1341
+ lines.push(`Total renders: ${result.summary.totalRenders}`);
1342
+ lines.push(`Unique components: ${result.summary.uniqueComponents}`);
1343
+ lines.push(`Wasted renders: ${result.summary.wastedRenders}`);
1344
+ lines.push(`Duration: ${result.summary.interactionDurationMs}ms`);
1345
+ lines.push("");
1346
+ if (result.renders.length === 0) {
1347
+ lines.push("No re-renders captured during interaction.");
1348
+ } else {
1349
+ lines.push("Re-renders:");
1350
+ lines.push(
1351
+ `${"#".padEnd(4)} ${"Component".padEnd(30)} ${"Trigger".padEnd(18)} ${"Wasted".padEnd(7)} ${"Chain Depth"}`
1352
+ );
1353
+ lines.push("\u2500".repeat(80));
1354
+ for (const r of result.renders) {
1355
+ const wasted = r.wasted ? "\u26A0 yes" : "no";
1356
+ const idx = String(r.renderIndex).padEnd(4);
1357
+ const comp = r.component.slice(0, 29).padEnd(30);
1358
+ const trig = r.trigger.padEnd(18);
1359
+ const w = wasted.padEnd(7);
1360
+ const depth = r.chain.length;
1361
+ lines.push(`${idx} ${comp} ${trig} ${w} ${depth}`);
1362
+ }
1363
+ }
1364
+ if (result.flags.length > 0) {
1365
+ lines.push("");
1366
+ lines.push("Flags:");
1367
+ for (const flag of result.flags) {
1368
+ const icon = flag.severity === "error" ? "\u2717" : flag.severity === "warning" ? "\u26A0" : "\u2139";
1369
+ lines.push(` ${icon} [${flag.id}] ${flag.component}: ${flag.detail}`);
1370
+ }
1371
+ }
1372
+ return lines.join("\n");
1373
+ }
1374
+ function createInstrumentRendersCommand() {
1375
+ return new Command3("renders").description("Trace re-render causality chains for a component during an interaction sequence").argument("<component>", "Component name to instrument (must be in manifest)").option(
1376
+ "--interaction <json>",
1377
+ `Interaction sequence JSON, e.g. '[{"action":"click","target":"button"}]'`,
1378
+ "[]"
1379
+ ).option("--json", "Output as JSON regardless of TTY", false).option("--manifest <path>", "Path to manifest.json", MANIFEST_PATH2).action(
1380
+ async (componentName, opts) => {
1381
+ let interaction = [];
1382
+ try {
1383
+ interaction = JSON.parse(opts.interaction);
1384
+ if (!Array.isArray(interaction)) {
1385
+ throw new Error("Interaction must be a JSON array");
1386
+ }
1387
+ } catch {
1388
+ process.stderr.write(`Error: Invalid --interaction JSON: ${opts.interaction}
1389
+ `);
1390
+ process.exit(1);
1391
+ }
1392
+ try {
1393
+ process.stderr.write(
1394
+ `Instrumenting ${componentName} (${interaction.length} interaction steps)\u2026
1395
+ `
1396
+ );
1397
+ const result = await analyzeRenders({
1398
+ componentName,
1399
+ interaction,
1400
+ manifestPath: opts.manifest
1401
+ });
1402
+ await shutdownPool();
1403
+ if (opts.json || !isTTY()) {
1404
+ process.stdout.write(`${JSON.stringify(result, null, 2)}
1405
+ `);
1406
+ } else {
1407
+ process.stdout.write(`${formatRendersTable(result)}
1408
+ `);
1409
+ }
1410
+ } catch (err) {
1411
+ await shutdownPool();
1412
+ process.stderr.write(`Error: ${err instanceof Error ? err.message : String(err)}
1413
+ `);
1414
+ process.exit(1);
1415
+ }
1416
+ }
1417
+ );
1418
+ }
1419
+ function createInstrumentCommand() {
1420
+ const instrumentCmd = new Command3("instrument").description(
1421
+ "Structured instrumentation commands for React component analysis"
1422
+ );
1423
+ instrumentCmd.addCommand(createInstrumentRendersCommand());
1424
+ return instrumentCmd;
1425
+ }
1426
+
1427
+ // src/render-commands.ts
1428
+ import { mkdirSync as mkdirSync3, writeFileSync as writeFileSync4 } from "fs";
1429
+ import { resolve as resolve4 } from "path";
1430
+ import {
1431
+ ALL_CONTEXT_IDS,
1432
+ ALL_STRESS_IDS,
1433
+ BrowserPool as BrowserPool2,
1434
+ contextAxis,
1435
+ RenderMatrix,
1436
+ SatoriRenderer,
1437
+ safeRender,
1438
+ stressAxis
1439
+ } from "@agent-scope/render";
1440
+ import { Command as Command4 } from "commander";
1441
+
615
1442
  // src/tailwind-css.ts
616
- import { existsSync as existsSync2, readFileSync as readFileSync2 } from "fs";
1443
+ import { existsSync as existsSync4, readFileSync as readFileSync4 } from "fs";
617
1444
  import { createRequire } from "module";
618
- import { resolve as resolve2 } from "path";
1445
+ import { resolve as resolve3 } from "path";
619
1446
  var CONFIG_FILENAMES = [
620
1447
  ".reactscope/config.json",
621
1448
  ".reactscope/config.js",
@@ -632,47 +1459,47 @@ var STYLE_ENTRY_CANDIDATES = [
632
1459
  var TAILWIND_IMPORT = /@import\s+["']tailwindcss["']\s*;?/;
633
1460
  var compilerCache = null;
634
1461
  function getCachedBuild(cwd) {
635
- if (compilerCache !== null && resolve2(compilerCache.cwd) === resolve2(cwd)) {
1462
+ if (compilerCache !== null && resolve3(compilerCache.cwd) === resolve3(cwd)) {
636
1463
  return compilerCache.build;
637
1464
  }
638
1465
  return null;
639
1466
  }
640
1467
  function findStylesEntry(cwd) {
641
1468
  for (const name of CONFIG_FILENAMES) {
642
- const p = resolve2(cwd, name);
643
- if (!existsSync2(p)) continue;
1469
+ const p = resolve3(cwd, name);
1470
+ if (!existsSync4(p)) continue;
644
1471
  try {
645
1472
  if (name.endsWith(".json")) {
646
- const raw = readFileSync2(p, "utf-8");
1473
+ const raw = readFileSync4(p, "utf-8");
647
1474
  const data = JSON.parse(raw);
648
1475
  const scope = data.scope;
649
1476
  const entry = scope?.stylesEntry ?? data.stylesEntry;
650
1477
  if (typeof entry === "string") {
651
- const full = resolve2(cwd, entry);
652
- if (existsSync2(full)) return full;
1478
+ const full = resolve3(cwd, entry);
1479
+ if (existsSync4(full)) return full;
653
1480
  }
654
1481
  }
655
1482
  } catch {
656
1483
  }
657
1484
  }
658
- const pkgPath = resolve2(cwd, "package.json");
659
- if (existsSync2(pkgPath)) {
1485
+ const pkgPath = resolve3(cwd, "package.json");
1486
+ if (existsSync4(pkgPath)) {
660
1487
  try {
661
- const raw = readFileSync2(pkgPath, "utf-8");
1488
+ const raw = readFileSync4(pkgPath, "utf-8");
662
1489
  const pkg = JSON.parse(raw);
663
1490
  const entry = pkg.scope?.stylesEntry;
664
1491
  if (typeof entry === "string") {
665
- const full = resolve2(cwd, entry);
666
- if (existsSync2(full)) return full;
1492
+ const full = resolve3(cwd, entry);
1493
+ if (existsSync4(full)) return full;
667
1494
  }
668
1495
  } catch {
669
1496
  }
670
1497
  }
671
1498
  for (const candidate of STYLE_ENTRY_CANDIDATES) {
672
- const full = resolve2(cwd, candidate);
673
- if (existsSync2(full)) {
1499
+ const full = resolve3(cwd, candidate);
1500
+ if (existsSync4(full)) {
674
1501
  try {
675
- const content = readFileSync2(full, "utf-8");
1502
+ const content = readFileSync4(full, "utf-8");
676
1503
  if (TAILWIND_IMPORT.test(content)) return full;
677
1504
  } catch {
678
1505
  }
@@ -687,7 +1514,7 @@ async function getTailwindCompiler(cwd) {
687
1514
  if (entryPath === null) return null;
688
1515
  let compile;
689
1516
  try {
690
- const require2 = createRequire(resolve2(cwd, "package.json"));
1517
+ const require2 = createRequire(resolve3(cwd, "package.json"));
691
1518
  const tailwind = require2("tailwindcss");
692
1519
  const fn = tailwind.compile;
693
1520
  if (typeof fn !== "function") return null;
@@ -695,23 +1522,23 @@ async function getTailwindCompiler(cwd) {
695
1522
  } catch {
696
1523
  return null;
697
1524
  }
698
- const entryContent = readFileSync2(entryPath, "utf-8");
1525
+ const entryContent = readFileSync4(entryPath, "utf-8");
699
1526
  const loadStylesheet = async (id, base) => {
700
1527
  if (id === "tailwindcss") {
701
- const nodeModules = resolve2(cwd, "node_modules");
702
- const tailwindCssPath = resolve2(nodeModules, "tailwindcss", "index.css");
703
- if (!existsSync2(tailwindCssPath)) {
1528
+ const nodeModules = resolve3(cwd, "node_modules");
1529
+ const tailwindCssPath = resolve3(nodeModules, "tailwindcss", "index.css");
1530
+ if (!existsSync4(tailwindCssPath)) {
704
1531
  throw new Error(
705
1532
  `Tailwind v4: tailwindcss package not found at ${tailwindCssPath}. Install with: npm install tailwindcss`
706
1533
  );
707
1534
  }
708
- const content = readFileSync2(tailwindCssPath, "utf-8");
1535
+ const content = readFileSync4(tailwindCssPath, "utf-8");
709
1536
  return { path: "virtual:tailwindcss/index.css", base, content };
710
1537
  }
711
- const full = resolve2(base, id);
712
- if (existsSync2(full)) {
713
- const content = readFileSync2(full, "utf-8");
714
- return { path: full, base: resolve2(full, ".."), content };
1538
+ const full = resolve3(base, id);
1539
+ if (existsSync4(full)) {
1540
+ const content = readFileSync4(full, "utf-8");
1541
+ return { path: full, base: resolve3(full, ".."), content };
715
1542
  }
716
1543
  throw new Error(`Tailwind v4: could not load stylesheet: ${id} (base: ${base})`);
717
1544
  };
@@ -733,24 +1560,24 @@ async function getCompiledCssForClasses(cwd, classes) {
733
1560
  }
734
1561
 
735
1562
  // src/render-commands.ts
736
- var MANIFEST_PATH2 = ".reactscope/manifest.json";
1563
+ var MANIFEST_PATH3 = ".reactscope/manifest.json";
737
1564
  var DEFAULT_OUTPUT_DIR = ".reactscope/renders";
738
- var _pool = null;
739
- async function getPool(viewportWidth, viewportHeight) {
740
- if (_pool === null) {
741
- _pool = new BrowserPool({
1565
+ var _pool2 = null;
1566
+ async function getPool2(viewportWidth, viewportHeight) {
1567
+ if (_pool2 === null) {
1568
+ _pool2 = new BrowserPool2({
742
1569
  size: { browsers: 1, pagesPerBrowser: 4 },
743
1570
  viewportWidth,
744
1571
  viewportHeight
745
1572
  });
746
- await _pool.init();
1573
+ await _pool2.init();
747
1574
  }
748
- return _pool;
1575
+ return _pool2;
749
1576
  }
750
- async function shutdownPool() {
751
- if (_pool !== null) {
752
- await _pool.close();
753
- _pool = null;
1577
+ async function shutdownPool2() {
1578
+ if (_pool2 !== null) {
1579
+ await _pool2.close();
1580
+ _pool2 = null;
754
1581
  }
755
1582
  }
756
1583
  function buildRenderer(filePath, componentName, viewportWidth, viewportHeight) {
@@ -761,7 +1588,7 @@ function buildRenderer(filePath, componentName, viewportWidth, viewportHeight) {
761
1588
  _satori: satori,
762
1589
  async renderCell(props, _complexityClass) {
763
1590
  const startMs = performance.now();
764
- const pool = await getPool(viewportWidth, viewportHeight);
1591
+ const pool = await getPool2(viewportWidth, viewportHeight);
765
1592
  const htmlHarness = await buildComponentHarness(
766
1593
  filePath,
767
1594
  componentName,
@@ -858,7 +1685,7 @@ function buildRenderer(filePath, componentName, viewportWidth, viewportHeight) {
858
1685
  };
859
1686
  }
860
1687
  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(
1688
+ renderCmd.command("component <component>", { isDefault: true }).description("Render a single component to PNG or JSON").option("--props <json>", `Inline props JSON, e.g. '{"variant":"primary"}'`).option("--viewport <WxH>", "Viewport size e.g. 1280x720", "375x812").option("--theme <name>", "Theme name from the token system").option("-o, --output <path>", "Write PNG to file instead of stdout").option("--format <fmt>", "Output format: png or json (default: auto)").option("--manifest <path>", "Path to manifest.json", MANIFEST_PATH3).action(
862
1689
  async (componentName, opts) => {
863
1690
  try {
864
1691
  const manifest = loadManifest(opts.manifest);
@@ -880,7 +1707,7 @@ Available: ${available}`
880
1707
  }
881
1708
  const { width, height } = parseViewport(opts.viewport);
882
1709
  const rootDir = process.cwd();
883
- const filePath = resolve3(rootDir, descriptor.filePath);
1710
+ const filePath = resolve4(rootDir, descriptor.filePath);
884
1711
  const renderer = buildRenderer(filePath, componentName, width, height);
885
1712
  process.stderr.write(
886
1713
  `Rendering ${componentName} [${descriptor.complexityClass}] at ${width}\xD7${height}\u2026
@@ -897,7 +1724,7 @@ Available: ${available}`
897
1724
  }
898
1725
  }
899
1726
  );
900
- await shutdownPool();
1727
+ await shutdownPool2();
901
1728
  if (outcome.crashed) {
902
1729
  process.stderr.write(`\u2717 Render failed: ${outcome.error.message}
903
1730
  `);
@@ -910,8 +1737,8 @@ Available: ${available}`
910
1737
  }
911
1738
  const result = outcome.result;
912
1739
  if (opts.output !== void 0) {
913
- const outPath = resolve3(process.cwd(), opts.output);
914
- writeFileSync3(outPath, result.screenshot);
1740
+ const outPath = resolve4(process.cwd(), opts.output);
1741
+ writeFileSync4(outPath, result.screenshot);
915
1742
  process.stdout.write(
916
1743
  `\u2713 ${componentName} \u2192 ${opts.output} (${result.width}\xD7${result.height}, ${result.renderTimeMs.toFixed(0)}ms)
917
1744
  `
@@ -924,20 +1751,20 @@ Available: ${available}`
924
1751
  process.stdout.write(`${JSON.stringify(json, null, 2)}
925
1752
  `);
926
1753
  } else if (fmt === "file") {
927
- const dir = resolve3(process.cwd(), DEFAULT_OUTPUT_DIR);
928
- mkdirSync2(dir, { recursive: true });
929
- const outPath = resolve3(dir, `${componentName}.png`);
930
- writeFileSync3(outPath, result.screenshot);
1754
+ const dir = resolve4(process.cwd(), DEFAULT_OUTPUT_DIR);
1755
+ mkdirSync3(dir, { recursive: true });
1756
+ const outPath = resolve4(dir, `${componentName}.png`);
1757
+ writeFileSync4(outPath, result.screenshot);
931
1758
  const relPath = `${DEFAULT_OUTPUT_DIR}/${componentName}.png`;
932
1759
  process.stdout.write(
933
1760
  `\u2713 ${componentName} \u2192 ${relPath} (${result.width}\xD7${result.height}, ${result.renderTimeMs.toFixed(0)}ms)
934
1761
  `
935
1762
  );
936
1763
  } else {
937
- const dir = resolve3(process.cwd(), DEFAULT_OUTPUT_DIR);
938
- mkdirSync2(dir, { recursive: true });
939
- const outPath = resolve3(dir, `${componentName}.png`);
940
- writeFileSync3(outPath, result.screenshot);
1764
+ const dir = resolve4(process.cwd(), DEFAULT_OUTPUT_DIR);
1765
+ mkdirSync3(dir, { recursive: true });
1766
+ const outPath = resolve4(dir, `${componentName}.png`);
1767
+ writeFileSync4(outPath, result.screenshot);
941
1768
  const relPath = `${DEFAULT_OUTPUT_DIR}/${componentName}.png`;
942
1769
  process.stdout.write(
943
1770
  `\u2713 ${componentName} \u2192 ${relPath} (${result.width}\xD7${result.height}, ${result.renderTimeMs.toFixed(0)}ms)
@@ -945,7 +1772,7 @@ Available: ${available}`
945
1772
  );
946
1773
  }
947
1774
  } catch (err) {
948
- await shutdownPool();
1775
+ await shutdownPool2();
949
1776
  process.stderr.write(`Error: ${err instanceof Error ? err.message : String(err)}
950
1777
  `);
951
1778
  process.exit(1);
@@ -957,7 +1784,7 @@ function registerRenderMatrix(renderCmd) {
957
1784
  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
1785
  "--contexts <ids>",
959
1786
  "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(
1787
+ ).option("--stress <ids>", "Stress preset IDs, comma-separated (e.g. text.long,text.unicode)").option("--sprite <path>", "Write sprite sheet PNG to file").option("--format <fmt>", "Output format: json|png|html|csv (default: auto)").option("--concurrency <n>", "Max parallel renders", "8").option("--manifest <path>", "Path to manifest.json", MANIFEST_PATH3).action(
961
1788
  async (componentName, opts) => {
962
1789
  try {
963
1790
  const manifest = loadManifest(opts.manifest);
@@ -972,7 +1799,7 @@ Available: ${available}`
972
1799
  const concurrency = Math.max(1, parseInt(opts.concurrency, 10) || 8);
973
1800
  const { width, height } = { width: 375, height: 812 };
974
1801
  const rootDir = process.cwd();
975
- const filePath = resolve3(rootDir, descriptor.filePath);
1802
+ const filePath = resolve4(rootDir, descriptor.filePath);
976
1803
  const renderer = buildRenderer(filePath, componentName, width, height);
977
1804
  const axes = [];
978
1805
  if (opts.axes !== void 0) {
@@ -1030,7 +1857,7 @@ Available: ${available}`
1030
1857
  concurrency
1031
1858
  });
1032
1859
  const result = await matrix.render();
1033
- await shutdownPool();
1860
+ await shutdownPool2();
1034
1861
  process.stderr.write(
1035
1862
  `Done. ${result.stats.totalCells} cells, avg ${result.stats.avgRenderTimeMs.toFixed(1)}ms
1036
1863
  `
@@ -1039,8 +1866,8 @@ Available: ${available}`
1039
1866
  const { SpriteSheetGenerator } = await import("@agent-scope/render");
1040
1867
  const gen = new SpriteSheetGenerator();
1041
1868
  const sheet = await gen.generate(result);
1042
- const spritePath = resolve3(process.cwd(), opts.sprite);
1043
- writeFileSync3(spritePath, sheet.png);
1869
+ const spritePath = resolve4(process.cwd(), opts.sprite);
1870
+ writeFileSync4(spritePath, sheet.png);
1044
1871
  process.stderr.write(`Sprite sheet saved to ${spritePath}
1045
1872
  `);
1046
1873
  }
@@ -1049,10 +1876,10 @@ Available: ${available}`
1049
1876
  const { SpriteSheetGenerator } = await import("@agent-scope/render");
1050
1877
  const gen = new SpriteSheetGenerator();
1051
1878
  const sheet = await gen.generate(result);
1052
- const dir = resolve3(process.cwd(), DEFAULT_OUTPUT_DIR);
1053
- mkdirSync2(dir, { recursive: true });
1054
- const outPath = resolve3(dir, `${componentName}-matrix.png`);
1055
- writeFileSync3(outPath, sheet.png);
1879
+ const dir = resolve4(process.cwd(), DEFAULT_OUTPUT_DIR);
1880
+ mkdirSync3(dir, { recursive: true });
1881
+ const outPath = resolve4(dir, `${componentName}-matrix.png`);
1882
+ writeFileSync4(outPath, sheet.png);
1056
1883
  const relPath = `${DEFAULT_OUTPUT_DIR}/${componentName}-matrix.png`;
1057
1884
  process.stdout.write(
1058
1885
  `\u2713 ${componentName} matrix (${result.stats.totalCells} cells) \u2192 ${relPath} (${result.stats.wallClockTimeMs.toFixed(0)}ms total)
@@ -1075,7 +1902,7 @@ Available: ${available}`
1075
1902
  process.stdout.write(formatMatrixCsv(componentName, result));
1076
1903
  }
1077
1904
  } catch (err) {
1078
- await shutdownPool();
1905
+ await shutdownPool2();
1079
1906
  process.stderr.write(`Error: ${err instanceof Error ? err.message : String(err)}
1080
1907
  `);
1081
1908
  process.exit(1);
@@ -1084,7 +1911,7 @@ Available: ${available}`
1084
1911
  );
1085
1912
  }
1086
1913
  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(
1914
+ renderCmd.command("all").description("Render all components from the manifest").option("--concurrency <n>", "Max parallel renders", "4").option("--output-dir <dir>", "Output directory for renders", DEFAULT_OUTPUT_DIR).option("--manifest <path>", "Path to manifest.json", MANIFEST_PATH3).option("--format <fmt>", "Output format: json|png (default: png)", "png").action(
1088
1915
  async (opts) => {
1089
1916
  try {
1090
1917
  const manifest = loadManifest(opts.manifest);
@@ -1095,8 +1922,8 @@ function registerRenderAll(renderCmd) {
1095
1922
  return;
1096
1923
  }
1097
1924
  const concurrency = Math.max(1, parseInt(opts.concurrency, 10) || 4);
1098
- const outputDir = resolve3(process.cwd(), opts.outputDir);
1099
- mkdirSync2(outputDir, { recursive: true });
1925
+ const outputDir = resolve4(process.cwd(), opts.outputDir);
1926
+ mkdirSync3(outputDir, { recursive: true });
1100
1927
  const rootDir = process.cwd();
1101
1928
  process.stderr.write(`Rendering ${total} components (concurrency: ${concurrency})\u2026
1102
1929
  `);
@@ -1105,7 +1932,7 @@ function registerRenderAll(renderCmd) {
1105
1932
  const renderOne = async (name) => {
1106
1933
  const descriptor = manifest.components[name];
1107
1934
  if (descriptor === void 0) return;
1108
- const filePath = resolve3(rootDir, descriptor.filePath);
1935
+ const filePath = resolve4(rootDir, descriptor.filePath);
1109
1936
  const renderer = buildRenderer(filePath, name, 375, 812);
1110
1937
  const outcome = await safeRender(
1111
1938
  () => renderer.renderCell({}, descriptor.complexityClass),
@@ -1128,8 +1955,8 @@ function registerRenderAll(renderCmd) {
1128
1955
  success: false,
1129
1956
  errorMessage: outcome.error.message
1130
1957
  });
1131
- const errPath = resolve3(outputDir, `${name}.error.json`);
1132
- writeFileSync3(
1958
+ const errPath = resolve4(outputDir, `${name}.error.json`);
1959
+ writeFileSync4(
1133
1960
  errPath,
1134
1961
  JSON.stringify(
1135
1962
  {
@@ -1146,10 +1973,10 @@ function registerRenderAll(renderCmd) {
1146
1973
  }
1147
1974
  const result = outcome.result;
1148
1975
  results.push({ name, renderTimeMs: result.renderTimeMs, success: true });
1149
- const pngPath = resolve3(outputDir, `${name}.png`);
1150
- writeFileSync3(pngPath, result.screenshot);
1151
- const jsonPath = resolve3(outputDir, `${name}.json`);
1152
- writeFileSync3(jsonPath, JSON.stringify(formatRenderJson(name, {}, result), null, 2));
1976
+ const pngPath = resolve4(outputDir, `${name}.png`);
1977
+ writeFileSync4(pngPath, result.screenshot);
1978
+ const jsonPath = resolve4(outputDir, `${name}.json`);
1979
+ writeFileSync4(jsonPath, JSON.stringify(formatRenderJson(name, {}, result), null, 2));
1153
1980
  if (isTTY()) {
1154
1981
  process.stdout.write(
1155
1982
  `\u2713 ${name} \u2192 ${opts.outputDir}/${name}.png (${result.width}\xD7${result.height}, ${result.renderTimeMs.toFixed(0)}ms)
@@ -1172,13 +1999,13 @@ function registerRenderAll(renderCmd) {
1172
1999
  workers.push(worker());
1173
2000
  }
1174
2001
  await Promise.all(workers);
1175
- await shutdownPool();
2002
+ await shutdownPool2();
1176
2003
  process.stderr.write("\n");
1177
2004
  const summary = formatSummaryText(results, outputDir);
1178
2005
  process.stderr.write(`${summary}
1179
2006
  `);
1180
2007
  } catch (err) {
1181
- await shutdownPool();
2008
+ await shutdownPool2();
1182
2009
  process.stderr.write(`Error: ${err instanceof Error ? err.message : String(err)}
1183
2010
  `);
1184
2011
  process.exit(1);
@@ -1211,7 +2038,7 @@ function resolveMatrixFormat(formatFlag, spriteAlreadyWritten) {
1211
2038
  return "json";
1212
2039
  }
1213
2040
  function createRenderCommand() {
1214
- const renderCmd = new Command2("render").description(
2041
+ const renderCmd = new Command4("render").description(
1215
2042
  "Render components to PNG or JSON via esbuild + BrowserPool"
1216
2043
  );
1217
2044
  registerRenderSingle(renderCmd);
@@ -1500,8 +2327,8 @@ function buildStructuredReport(report) {
1500
2327
  }
1501
2328
 
1502
2329
  // src/tokens/commands.ts
1503
- import { existsSync as existsSync3, readFileSync as readFileSync3 } from "fs";
1504
- import { resolve as resolve4 } from "path";
2330
+ import { existsSync as existsSync5, readFileSync as readFileSync5 } from "fs";
2331
+ import { resolve as resolve5 } from "path";
1505
2332
  import {
1506
2333
  parseTokenFileSync,
1507
2334
  TokenParseError,
@@ -1509,7 +2336,7 @@ import {
1509
2336
  TokenValidationError,
1510
2337
  validateTokenFile
1511
2338
  } from "@agent-scope/tokens";
1512
- import { Command as Command3 } from "commander";
2339
+ import { Command as Command5 } from "commander";
1513
2340
  var DEFAULT_TOKEN_FILE = "reactscope.tokens.json";
1514
2341
  var CONFIG_FILE = "reactscope.config.json";
1515
2342
  function isTTY2() {
@@ -1531,30 +2358,30 @@ function buildTable2(headers, rows) {
1531
2358
  }
1532
2359
  function resolveTokenFilePath(fileFlag) {
1533
2360
  if (fileFlag !== void 0) {
1534
- return resolve4(process.cwd(), fileFlag);
2361
+ return resolve5(process.cwd(), fileFlag);
1535
2362
  }
1536
- const configPath = resolve4(process.cwd(), CONFIG_FILE);
1537
- if (existsSync3(configPath)) {
2363
+ const configPath = resolve5(process.cwd(), CONFIG_FILE);
2364
+ if (existsSync5(configPath)) {
1538
2365
  try {
1539
- const raw = readFileSync3(configPath, "utf-8");
2366
+ const raw = readFileSync5(configPath, "utf-8");
1540
2367
  const config = JSON.parse(raw);
1541
2368
  if (typeof config === "object" && config !== null && "tokens" in config && typeof config.tokens === "object" && config.tokens !== null && typeof config.tokens?.file === "string") {
1542
2369
  const file = config.tokens.file;
1543
- return resolve4(process.cwd(), file);
2370
+ return resolve5(process.cwd(), file);
1544
2371
  }
1545
2372
  } catch {
1546
2373
  }
1547
2374
  }
1548
- return resolve4(process.cwd(), DEFAULT_TOKEN_FILE);
2375
+ return resolve5(process.cwd(), DEFAULT_TOKEN_FILE);
1549
2376
  }
1550
2377
  function loadTokens(absPath) {
1551
- if (!existsSync3(absPath)) {
2378
+ if (!existsSync5(absPath)) {
1552
2379
  throw new Error(
1553
2380
  `Token file not found at ${absPath}.
1554
2381
  Create a reactscope.tokens.json file or use --file to specify a path.`
1555
2382
  );
1556
2383
  }
1557
- const raw = readFileSync3(absPath, "utf-8");
2384
+ const raw = readFileSync5(absPath, "utf-8");
1558
2385
  return parseTokenFileSync(raw);
1559
2386
  }
1560
2387
  function getRawValue(node, segments) {
@@ -1768,13 +2595,13 @@ function registerValidate(tokensCmd) {
1768
2595
  ).option("--file <path>", "Path to token file (overrides config)").option("--format <fmt>", "Output format: json or text (default: auto-detect)").action((opts) => {
1769
2596
  try {
1770
2597
  const filePath = resolveTokenFilePath(opts.file);
1771
- if (!existsSync3(filePath)) {
2598
+ if (!existsSync5(filePath)) {
1772
2599
  throw new Error(
1773
2600
  `Token file not found at ${filePath}.
1774
2601
  Create a reactscope.tokens.json file or use --file to specify a path.`
1775
2602
  );
1776
2603
  }
1777
- const raw = readFileSync3(filePath, "utf-8");
2604
+ const raw = readFileSync5(filePath, "utf-8");
1778
2605
  const useJson = opts.format === "json" || opts.format !== "text" && !isTTY2();
1779
2606
  const errors = [];
1780
2607
  let parsed;
@@ -1842,7 +2669,7 @@ function outputValidationResult(filePath, errors, useJson) {
1842
2669
  }
1843
2670
  }
1844
2671
  function createTokensCommand() {
1845
- const tokensCmd = new Command3("tokens").description(
2672
+ const tokensCmd = new Command5("tokens").description(
1846
2673
  "Query and validate design tokens from a reactscope.tokens.json file"
1847
2674
  );
1848
2675
  registerGet2(tokensCmd);
@@ -1855,7 +2682,7 @@ function createTokensCommand() {
1855
2682
 
1856
2683
  // src/program.ts
1857
2684
  function createProgram(options = {}) {
1858
- const program2 = new Command4("scope").version(options.version ?? "0.1.0").description("Scope \u2014 React instrumentation toolkit");
2685
+ const program2 = new Command6("scope").version(options.version ?? "0.1.0").description("Scope \u2014 React instrumentation toolkit");
1859
2686
  program2.command("capture <url>").description("Capture a React component tree from a live URL and output as JSON").option("-o, --output <path>", "Write JSON to file instead of stdout").option("--pretty", "Pretty-print JSON output (default: minified)", false).option("--timeout <ms>", "Max wait time for React to mount (ms)", "10000").option("--wait <ms>", "Additional wait after page load before capture (ms)", "0").action(
1860
2687
  async (url, opts) => {
1861
2688
  try {
@@ -1928,7 +2755,7 @@ function createProgram(options = {}) {
1928
2755
  }
1929
2756
  );
1930
2757
  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) => {
1931
- const raw = readFileSync4(tracePath, "utf-8");
2758
+ const raw = readFileSync6(tracePath, "utf-8");
1932
2759
  const trace = loadTrace(raw);
1933
2760
  const source = generateTest(trace, {
1934
2761
  description: opts.description,
@@ -1940,6 +2767,8 @@ function createProgram(options = {}) {
1940
2767
  program2.addCommand(createManifestCommand());
1941
2768
  program2.addCommand(createRenderCommand());
1942
2769
  program2.addCommand(createTokensCommand());
2770
+ program2.addCommand(createInstrumentCommand());
2771
+ program2.addCommand(createInitCommand());
1943
2772
  return program2;
1944
2773
  }
1945
2774