@agent-scope/cli 1.0.1

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 ADDED
@@ -0,0 +1,1422 @@
1
+ import { readFileSync, existsSync, mkdirSync, writeFileSync } from 'fs';
2
+ import { resolve, dirname } from 'path';
3
+ import { generateManifest } from '@agent-scope/manifest';
4
+ import { Command } from 'commander';
5
+ import { loadTrace, generateTest, getBrowserEntryScript } from '@agent-scope/playwright';
6
+ import { chromium } from 'playwright';
7
+ import { safeRender, ALL_CONTEXT_IDS, contextAxis, stressAxis, ALL_STRESS_IDS, RenderMatrix, SatoriRenderer, BrowserPool } from '@agent-scope/render';
8
+ import * as esbuild from 'esbuild';
9
+
10
+ // src/manifest-commands.ts
11
+
12
+ // src/manifest-formatter.ts
13
+ function isTTY() {
14
+ return process.stdout.isTTY === true;
15
+ }
16
+ function pad(value, width) {
17
+ return value.length >= width ? value.slice(0, width) : value + " ".repeat(width - value.length);
18
+ }
19
+ function buildTable(headers, rows) {
20
+ const colWidths = headers.map(
21
+ (h, i) => Math.max(h.length, ...rows.map((r) => (r[i] ?? "").length))
22
+ );
23
+ const divider = colWidths.map((w) => "-".repeat(w)).join(" ");
24
+ const headerRow = headers.map((h, i) => pad(h, colWidths[i] ?? 0)).join(" ");
25
+ const dataRows = rows.map(
26
+ (row2) => row2.map((cell, i) => pad(cell ?? "", colWidths[i] ?? 0)).join(" ")
27
+ );
28
+ return [headerRow, divider, ...dataRows].join("\n");
29
+ }
30
+ function formatListTable(rows) {
31
+ if (rows.length === 0) return "No components found.";
32
+ const headers = ["NAME", "FILE", "COMPLEXITY", "HOOKS", "CONTEXTS"];
33
+ const tableRows = rows.map((r) => [
34
+ r.name,
35
+ r.file,
36
+ r.complexityClass,
37
+ String(r.hookCount),
38
+ String(r.contextCount)
39
+ ]);
40
+ return buildTable(headers, tableRows);
41
+ }
42
+ function formatListJson(rows) {
43
+ return JSON.stringify(rows, null, 2);
44
+ }
45
+ function formatSideEffects(se) {
46
+ const parts = [];
47
+ if (se.fetches.length > 0) parts.push(`fetches: ${se.fetches.join(", ")}`);
48
+ if (se.timers) parts.push("timers");
49
+ if (se.subscriptions.length > 0) parts.push(`subscriptions: ${se.subscriptions.join(", ")}`);
50
+ if (se.globalListeners) parts.push("globalListeners");
51
+ return parts.length > 0 ? parts.join(" | ") : "none";
52
+ }
53
+ function formatGetTable(name, descriptor) {
54
+ const propNames = Object.keys(descriptor.props);
55
+ const lines = [
56
+ `Component: ${name}`,
57
+ ` File: ${descriptor.filePath}`,
58
+ ` Export: ${descriptor.exportType}`,
59
+ ` Display Name: ${descriptor.displayName}`,
60
+ ` Complexity: ${descriptor.complexityClass}`,
61
+ ` Memoized: ${descriptor.memoized}`,
62
+ ` Forwarded Ref: ${descriptor.forwardedRef}`,
63
+ ` HOC Wrappers: ${descriptor.hocWrappers.join(", ") || "none"}`,
64
+ ` Hooks: ${descriptor.detectedHooks.join(", ") || "none"}`,
65
+ ` Contexts: ${descriptor.requiredContexts.join(", ") || "none"}`,
66
+ ` Composes: ${descriptor.composes.join(", ") || "none"}`,
67
+ ` Composed By: ${descriptor.composedBy.join(", ") || "none"}`,
68
+ ` Side Effects: ${formatSideEffects(descriptor.sideEffects)}`,
69
+ "",
70
+ ` Props (${propNames.length}):`
71
+ ];
72
+ if (propNames.length === 0) {
73
+ lines.push(" (none)");
74
+ } else {
75
+ for (const propName of propNames) {
76
+ const p = descriptor.props[propName];
77
+ if (p === void 0) continue;
78
+ const req = p.required ? "required" : "optional";
79
+ const def = p.default !== void 0 ? ` [default: ${p.default}]` : "";
80
+ const vals = p.values !== void 0 ? ` (${p.values.join(" | ")})` : "";
81
+ lines.push(` ${propName}: ${p.rawType}${vals} \u2014 ${req}${def}`);
82
+ }
83
+ }
84
+ return lines.join("\n");
85
+ }
86
+ function formatGetJson(name, descriptor) {
87
+ return JSON.stringify({ name, ...descriptor }, null, 2);
88
+ }
89
+ function formatQueryTable(rows, queryDesc) {
90
+ if (rows.length === 0) return `No components match: ${queryDesc}`;
91
+ const headers = ["NAME", "FILE", "COMPLEXITY", "HOOKS", "CONTEXTS"];
92
+ const tableRows = rows.map((r) => [r.name, r.file, r.complexityClass, r.hooks, r.contexts]);
93
+ return `Query: ${queryDesc}
94
+
95
+ ${buildTable(headers, tableRows)}`;
96
+ }
97
+ function formatQueryJson(rows) {
98
+ return JSON.stringify(rows, null, 2);
99
+ }
100
+ function matchGlob(pattern, value) {
101
+ const escaped = pattern.replace(/[.+^${}()|[\]\\]/g, "\\$&");
102
+ const regexStr = escaped.replace(/\*\*/g, "\xA7GLOBSTAR\xA7").replace(/\*/g, "[^/]*").replace(/§GLOBSTAR§/g, ".*");
103
+ const regex = new RegExp(`^${regexStr}$`, "i");
104
+ return regex.test(value);
105
+ }
106
+
107
+ // src/manifest-commands.ts
108
+ var MANIFEST_PATH = ".reactscope/manifest.json";
109
+ function loadManifest(manifestPath = MANIFEST_PATH) {
110
+ const absPath = resolve(process.cwd(), manifestPath);
111
+ if (!existsSync(absPath)) {
112
+ throw new Error(`Manifest not found at ${absPath}.
113
+ Run \`scope manifest generate\` first.`);
114
+ }
115
+ const raw = readFileSync(absPath, "utf-8");
116
+ return JSON.parse(raw);
117
+ }
118
+ function resolveFormat(formatFlag) {
119
+ if (formatFlag === "json") return "json";
120
+ if (formatFlag === "table") return "table";
121
+ return isTTY() ? "table" : "json";
122
+ }
123
+ function registerList(manifestCmd) {
124
+ manifestCmd.command("list").description("List all components in the manifest").option("--format <fmt>", "Output format: json or table (default: auto-detect)").option("--filter <glob>", "Filter components by name glob pattern").option("--manifest <path>", "Path to manifest.json", MANIFEST_PATH).action((opts) => {
125
+ try {
126
+ const manifest = loadManifest(opts.manifest);
127
+ const format = resolveFormat(opts.format);
128
+ let entries = Object.entries(manifest.components);
129
+ if (opts.filter !== void 0) {
130
+ const filterPattern = opts.filter ?? "";
131
+ entries = entries.filter(([name]) => matchGlob(filterPattern, name));
132
+ }
133
+ const rows = entries.map(([name, descriptor]) => ({
134
+ name,
135
+ file: descriptor.filePath,
136
+ complexityClass: descriptor.complexityClass,
137
+ hookCount: descriptor.detectedHooks.length,
138
+ contextCount: descriptor.requiredContexts.length
139
+ }));
140
+ const output = format === "json" ? formatListJson(rows) : formatListTable(rows);
141
+ process.stdout.write(`${output}
142
+ `);
143
+ } catch (err) {
144
+ process.stderr.write(`Error: ${err instanceof Error ? err.message : String(err)}
145
+ `);
146
+ process.exit(1);
147
+ }
148
+ });
149
+ }
150
+ function registerGet(manifestCmd) {
151
+ manifestCmd.command("get <name>").description("Get full details of a single component by name").option("--format <fmt>", "Output format: json or table (default: auto-detect)").option("--manifest <path>", "Path to manifest.json", MANIFEST_PATH).action((name, opts) => {
152
+ try {
153
+ const manifest = loadManifest(opts.manifest);
154
+ const format = resolveFormat(opts.format);
155
+ const descriptor = manifest.components[name];
156
+ if (descriptor === void 0) {
157
+ const available = Object.keys(manifest.components).slice(0, 5).join(", ");
158
+ const hint = Object.keys(manifest.components).length > 5 ? ", \u2026" : "";
159
+ throw new Error(
160
+ `Component "${name}" not found in manifest.
161
+ Available: ${available}${hint}`
162
+ );
163
+ }
164
+ const output = format === "json" ? formatGetJson(name, descriptor) : formatGetTable(name, descriptor);
165
+ process.stdout.write(`${output}
166
+ `);
167
+ } catch (err) {
168
+ process.stderr.write(`Error: ${err instanceof Error ? err.message : String(err)}
169
+ `);
170
+ process.exit(1);
171
+ }
172
+ });
173
+ }
174
+ function registerQuery(manifestCmd) {
175
+ manifestCmd.command("query").description("Query components by attributes").option("--context <name>", "Find components consuming a context").option("--hook <name>", "Find components using a specific hook").option("--complexity <class>", "Filter by complexity class: simple or complex").option("--side-effects", "Find components with any side effects", false).option("--has-fetch", "Find components with fetch calls", false).option("--format <fmt>", "Output format: json or table (default: auto-detect)").option("--manifest <path>", "Path to manifest.json", MANIFEST_PATH).action(
176
+ (opts) => {
177
+ try {
178
+ const manifest = loadManifest(opts.manifest);
179
+ const format = resolveFormat(opts.format);
180
+ const queryParts = [];
181
+ if (opts.context !== void 0) queryParts.push(`context=${opts.context}`);
182
+ if (opts.hook !== void 0) queryParts.push(`hook=${opts.hook}`);
183
+ if (opts.complexity !== void 0) queryParts.push(`complexity=${opts.complexity}`);
184
+ if (opts.sideEffects) queryParts.push("side-effects");
185
+ if (opts.hasFetch) queryParts.push("has-fetch");
186
+ if (queryParts.length === 0) {
187
+ process.stderr.write(
188
+ "No query flags specified. Use --context, --hook, --complexity, --side-effects, or --has-fetch.\n"
189
+ );
190
+ process.exit(1);
191
+ }
192
+ const queryDesc = queryParts.join(", ");
193
+ let entries = Object.entries(manifest.components);
194
+ if (opts.context !== void 0) {
195
+ const ctx = opts.context;
196
+ entries = entries.filter(([, d]) => d.requiredContexts.includes(ctx));
197
+ }
198
+ if (opts.hook !== void 0) {
199
+ const hook = opts.hook;
200
+ entries = entries.filter(([, d]) => d.detectedHooks.includes(hook));
201
+ }
202
+ if (opts.complexity !== void 0) {
203
+ const cls = opts.complexity;
204
+ entries = entries.filter(([, d]) => d.complexityClass === cls);
205
+ }
206
+ if (opts.sideEffects) {
207
+ entries = entries.filter(([, d]) => {
208
+ const se = d.sideEffects;
209
+ return se.fetches.length > 0 || se.timers || se.subscriptions.length > 0 || se.globalListeners;
210
+ });
211
+ }
212
+ if (opts.hasFetch) {
213
+ entries = entries.filter(([, d]) => d.sideEffects.fetches.length > 0);
214
+ }
215
+ const rows = entries.map(([name, d]) => ({
216
+ name,
217
+ file: d.filePath,
218
+ complexityClass: d.complexityClass,
219
+ hooks: d.detectedHooks.join(", ") || "\u2014",
220
+ contexts: d.requiredContexts.join(", ") || "\u2014"
221
+ }));
222
+ const output = format === "json" ? formatQueryJson(rows) : formatQueryTable(rows, queryDesc);
223
+ process.stdout.write(`${output}
224
+ `);
225
+ } catch (err) {
226
+ process.stderr.write(`Error: ${err instanceof Error ? err.message : String(err)}
227
+ `);
228
+ process.exit(1);
229
+ }
230
+ }
231
+ );
232
+ }
233
+ function registerGenerate(manifestCmd) {
234
+ manifestCmd.command("generate").description(
235
+ "Generate the component manifest from source and write to .reactscope/manifest.json"
236
+ ).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) => {
237
+ try {
238
+ const rootDir = resolve(process.cwd(), opts.root ?? ".");
239
+ const outputPath = resolve(process.cwd(), opts.output);
240
+ const include = opts.include?.split(",").map((s) => s.trim());
241
+ const exclude = opts.exclude?.split(",").map((s) => s.trim());
242
+ process.stderr.write(`Scanning ${rootDir} for React components...
243
+ `);
244
+ const manifest = await generateManifest({
245
+ rootDir,
246
+ ...include !== void 0 && { include },
247
+ ...exclude !== void 0 && { exclude }
248
+ });
249
+ const componentCount = Object.keys(manifest.components).length;
250
+ process.stderr.write(`Found ${componentCount} components.
251
+ `);
252
+ const outputDir = outputPath.replace(/\/[^/]+$/, "");
253
+ if (!existsSync(outputDir)) {
254
+ mkdirSync(outputDir, { recursive: true });
255
+ }
256
+ writeFileSync(outputPath, JSON.stringify(manifest, null, 2), "utf-8");
257
+ process.stderr.write(`Manifest written to ${outputPath}
258
+ `);
259
+ process.stdout.write(`${outputPath}
260
+ `);
261
+ } catch (err) {
262
+ process.stderr.write(`Error: ${err instanceof Error ? err.message : String(err)}
263
+ `);
264
+ process.exit(1);
265
+ }
266
+ });
267
+ }
268
+ function createManifestCommand() {
269
+ const manifestCmd = new Command("manifest").description(
270
+ "Query and explore the component manifest"
271
+ );
272
+ registerList(manifestCmd);
273
+ registerGet(manifestCmd);
274
+ registerQuery(manifestCmd);
275
+ registerGenerate(manifestCmd);
276
+ return manifestCmd;
277
+ }
278
+ async function browserCapture(options) {
279
+ const { url, timeout = 1e4, wait = 0 } = options;
280
+ const browser = await chromium.launch({ headless: true });
281
+ try {
282
+ const context = await browser.newContext();
283
+ const page = await context.newPage();
284
+ await page.addInitScript({ content: getBrowserEntryScript() });
285
+ await page.goto(url, {
286
+ waitUntil: "networkidle",
287
+ timeout: timeout + 5e3
288
+ });
289
+ await page.waitForFunction(
290
+ () => {
291
+ return typeof window.__SCOPE_CAPTURE__ === "function" && (document.readyState === "complete" || document.readyState === "interactive");
292
+ },
293
+ { timeout }
294
+ );
295
+ if (wait > 0) {
296
+ await page.waitForTimeout(wait);
297
+ }
298
+ const raw = await page.evaluate(async () => {
299
+ const win = window;
300
+ if (typeof win.__SCOPE_CAPTURE__ !== "function") {
301
+ throw new Error("Scope runtime not injected");
302
+ }
303
+ return win.__SCOPE_CAPTURE__();
304
+ });
305
+ if (raw !== null && typeof raw === "object" && "error" in raw && typeof raw.error === "string") {
306
+ throw new Error(`Scope capture failed: ${raw.error}`);
307
+ }
308
+ const report = { ...raw, route: null };
309
+ return { report };
310
+ } finally {
311
+ await browser.close();
312
+ }
313
+ }
314
+ function writeReportToFile(report, outputPath, pretty) {
315
+ const json = pretty ? JSON.stringify(report, null, 2) : JSON.stringify(report);
316
+ writeFileSync(outputPath, json, "utf-8");
317
+ }
318
+ async function buildComponentHarness(filePath, componentName, props, viewportWidth) {
319
+ const bundledScript = await bundleComponentToIIFE(filePath, componentName, props);
320
+ return wrapInHtml(bundledScript, viewportWidth);
321
+ }
322
+ async function bundleComponentToIIFE(filePath, componentName, props) {
323
+ const propsJson = JSON.stringify(props).replace(/<\/script>/gi, "<\\/script>");
324
+ const wrapperCode = (
325
+ /* ts */
326
+ `
327
+ import * as __scopeMod from ${JSON.stringify(filePath)};
328
+ import { createRoot } from "react-dom/client";
329
+ import { createElement } from "react";
330
+
331
+ (function scopeRenderHarness() {
332
+ var Component =
333
+ __scopeMod["default"] ||
334
+ __scopeMod[${JSON.stringify(componentName)}] ||
335
+ (Object.values(__scopeMod).find(
336
+ function(v) { return typeof v === "function" && /^[A-Z]/.test(v.name || ""); }
337
+ ));
338
+
339
+ if (!Component) {
340
+ window.__SCOPE_RENDER_ERROR__ =
341
+ "No renderable component found. Checked: default, " +
342
+ ${JSON.stringify(componentName)} + ", and PascalCase named exports. " +
343
+ "Available exports: " + Object.keys(__scopeMod).join(", ");
344
+ window.__SCOPE_RENDER_COMPLETE__ = true;
345
+ return;
346
+ }
347
+
348
+ try {
349
+ var props = ${propsJson};
350
+ var rootEl = document.getElementById("scope-root");
351
+ if (!rootEl) {
352
+ window.__SCOPE_RENDER_ERROR__ = "#scope-root element not found";
353
+ window.__SCOPE_RENDER_COMPLETE__ = true;
354
+ return;
355
+ }
356
+ createRoot(rootEl).render(createElement(Component, props));
357
+ // Use requestAnimationFrame to let React flush the render
358
+ requestAnimationFrame(function() {
359
+ window.__SCOPE_RENDER_COMPLETE__ = true;
360
+ });
361
+ } catch (err) {
362
+ window.__SCOPE_RENDER_ERROR__ = err instanceof Error ? err.message : String(err);
363
+ window.__SCOPE_RENDER_COMPLETE__ = true;
364
+ }
365
+ })();
366
+ `
367
+ );
368
+ const result = await esbuild.build({
369
+ stdin: {
370
+ contents: wrapperCode,
371
+ // Resolve relative imports (within the component's dir)
372
+ resolveDir: dirname(filePath),
373
+ loader: "tsx",
374
+ sourcefile: "__scope_harness__.tsx"
375
+ },
376
+ bundle: true,
377
+ format: "iife",
378
+ write: false,
379
+ platform: "browser",
380
+ jsx: "automatic",
381
+ jsxImportSource: "react",
382
+ target: "es2020",
383
+ // Bundle everything — no externals
384
+ external: [],
385
+ define: {
386
+ "process.env.NODE_ENV": '"development"',
387
+ global: "globalThis"
388
+ },
389
+ logLevel: "silent",
390
+ // Suppress "React must be in scope" warnings from old JSX (we use automatic)
391
+ banner: {
392
+ js: "/* @agent-scope/cli component harness */"
393
+ }
394
+ });
395
+ if (result.errors.length > 0) {
396
+ const msg = result.errors.map((e) => `${e.text}${e.location ? ` (${e.location.file}:${e.location.line})` : ""}`).join("\n");
397
+ throw new Error(`esbuild failed to bundle component:
398
+ ${msg}`);
399
+ }
400
+ const outputFile = result.outputFiles?.[0];
401
+ if (outputFile === void 0 || outputFile.text.length === 0) {
402
+ throw new Error("esbuild produced no output");
403
+ }
404
+ return outputFile.text;
405
+ }
406
+ function wrapInHtml(bundledScript, viewportWidth) {
407
+ return `<!DOCTYPE html>
408
+ <html lang="en">
409
+ <head>
410
+ <meta charset="UTF-8" />
411
+ <meta name="viewport" content="width=${viewportWidth}, initial-scale=1.0" />
412
+ <style>
413
+ *, *::before, *::after { box-sizing: border-box; }
414
+ html, body { margin: 0; padding: 0; background: #fff; font-family: system-ui, sans-serif; }
415
+ #scope-root { display: inline-block; min-width: 1px; min-height: 1px; }
416
+ </style>
417
+ </head>
418
+ <body>
419
+ <div id="scope-root" data-reactscope-root></div>
420
+ <script>${bundledScript}</script>
421
+ </body>
422
+ </html>`;
423
+ }
424
+
425
+ // src/render-formatter.ts
426
+ function parseViewport(spec) {
427
+ const lower = spec.toLowerCase();
428
+ const match = /^(\d+)[x×](\d+)$/.exec(lower);
429
+ if (!match) {
430
+ throw new Error(`Invalid viewport "${spec}". Expected format: WIDTHxHEIGHT (e.g. 1280x720)`);
431
+ }
432
+ const width = parseInt(match[1] ?? "0", 10);
433
+ const height = parseInt(match[2] ?? "0", 10);
434
+ if (width <= 0 || height <= 0) {
435
+ throw new Error(`Viewport dimensions must be positive integers, got: ${spec}`);
436
+ }
437
+ return { width, height };
438
+ }
439
+ function formatRenderJson(componentName, props, result) {
440
+ return {
441
+ component: componentName,
442
+ props,
443
+ width: result.width,
444
+ height: result.height,
445
+ renderTimeMs: result.renderTimeMs,
446
+ computedStyles: result.computedStyles,
447
+ screenshot: result.screenshot.toString("base64"),
448
+ dom: result.dom,
449
+ console: result.console,
450
+ accessibility: result.accessibility
451
+ };
452
+ }
453
+ function formatMatrixJson(result) {
454
+ return {
455
+ axes: result.axes.map((axis) => ({
456
+ name: axis.name,
457
+ values: axis.values.map((v) => String(v))
458
+ })),
459
+ stats: { ...result.stats },
460
+ cells: result.cells.map((cell) => ({
461
+ index: cell.index,
462
+ axisIndices: cell.axisIndices,
463
+ props: cell.props,
464
+ renderTimeMs: cell.result.renderTimeMs,
465
+ width: cell.result.width,
466
+ height: cell.result.height,
467
+ screenshot: cell.result.screenshot.toString("base64")
468
+ }))
469
+ };
470
+ }
471
+ function formatMatrixHtml(componentName, result) {
472
+ const cellsHtml = result.cells.map((cell) => {
473
+ const b64 = cell.result.screenshot.toString("base64");
474
+ const propLabel = escapeHtml(
475
+ Object.entries(cell.props).map(([k, v]) => `${k}: ${String(v)}`).join(", ")
476
+ );
477
+ return ` <div class="cell">
478
+ <img src="data:image/png;base64,${b64}" alt="${propLabel}" width="${cell.result.width}" height="${cell.result.height}" />
479
+ <div class="label">${propLabel}</div>
480
+ <div class="meta">${cell.result.renderTimeMs.toFixed(1)}ms</div>
481
+ </div>`;
482
+ }).join("\n");
483
+ const axesDesc = result.axes.map((a) => `${a.name}: ${a.values.map((v) => String(v)).join(", ")}`).join(" | ");
484
+ return `<!DOCTYPE html>
485
+ <html lang="en">
486
+ <head>
487
+ <meta charset="UTF-8" />
488
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
489
+ <title>${escapeHtml(componentName)} \u2014 Render Matrix</title>
490
+ <style>
491
+ body { font-family: system-ui, sans-serif; background: #f8fafc; margin: 0; padding: 24px; }
492
+ h1 { font-size: 1.25rem; color: #1e293b; margin-bottom: 8px; }
493
+ .axes { font-size: 0.8rem; color: #64748b; margin-bottom: 20px; }
494
+ .grid { display: flex; flex-wrap: wrap; gap: 16px; }
495
+ .cell { background: #fff; border: 1px solid #e2e8f0; border-radius: 8px; overflow: hidden; }
496
+ .cell img { display: block; }
497
+ .label { padding: 6px 8px; font-size: 0.75rem; color: #374151; border-top: 1px solid #f1f5f9; }
498
+ .meta { padding: 2px 8px 6px; font-size: 0.7rem; color: #94a3b8; }
499
+ .stats { margin-top: 24px; font-size: 0.8rem; color: #64748b; }
500
+ </style>
501
+ </head>
502
+ <body>
503
+ <h1>${escapeHtml(componentName)} \u2014 Render Matrix</h1>
504
+ <div class="axes">Axes: ${escapeHtml(axesDesc)}</div>
505
+ <div class="grid">
506
+ ${cellsHtml}
507
+ </div>
508
+ <div class="stats">
509
+ ${result.stats.totalCells} cells \xB7
510
+ avg ${result.stats.avgRenderTimeMs.toFixed(1)}ms \xB7
511
+ min ${result.stats.minRenderTimeMs.toFixed(1)}ms \xB7
512
+ max ${result.stats.maxRenderTimeMs.toFixed(1)}ms \xB7
513
+ wall ${result.stats.wallClockTimeMs.toFixed(0)}ms
514
+ </div>
515
+ </body>
516
+ </html>
517
+ `;
518
+ }
519
+ function formatMatrixCsv(componentName, result) {
520
+ const axisNames = result.axes.map((a) => a.name);
521
+ const headers = ["component", ...axisNames, "renderTimeMs", "width", "height"];
522
+ const rows = result.cells.map((cell) => {
523
+ const axisVals = result.axes.map((_, i) => {
524
+ const axisIdx = cell.axisIndices[i];
525
+ const axis = result.axes[i];
526
+ if (axisIdx === void 0 || axis === void 0) return "";
527
+ const val = axis.values[axisIdx];
528
+ return val !== void 0 ? csvEscape(String(val)) : "";
529
+ });
530
+ return [
531
+ csvEscape(componentName),
532
+ ...axisVals,
533
+ cell.result.renderTimeMs.toFixed(3),
534
+ String(cell.result.width),
535
+ String(cell.result.height)
536
+ ].join(",");
537
+ });
538
+ return `${[headers.join(","), ...rows].join("\n")}
539
+ `;
540
+ }
541
+ function renderProgressBar(completed, total, currentName, pct, barWidth = 20) {
542
+ const filled = Math.round(pct / 100 * barWidth);
543
+ const empty = barWidth - filled;
544
+ const bar = "=".repeat(Math.max(0, filled - 1)) + (filled > 0 ? ">" : "") + " ".repeat(empty);
545
+ const nameSlice = currentName.slice(0, 25).padEnd(25);
546
+ return `Rendering ${completed}/${total} ${nameSlice} [${bar}] ${pct}%`;
547
+ }
548
+ function formatSummaryText(results, outputDir) {
549
+ const total = results.length;
550
+ const passed = results.filter((r) => r.success).length;
551
+ const failed = total - passed;
552
+ const successTimes = results.filter((r) => r.success).map((r) => r.renderTimeMs);
553
+ const avgMs = successTimes.length > 0 ? successTimes.reduce((a, b) => a + b, 0) / successTimes.length : 0;
554
+ const lines = [
555
+ "\u2500".repeat(60),
556
+ `Render Summary`,
557
+ "\u2500".repeat(60),
558
+ ` Total components : ${total}`,
559
+ ` Passed : ${passed}`,
560
+ ` Failed : ${failed}`,
561
+ ` Avg render time : ${avgMs.toFixed(1)}ms`,
562
+ ` Output dir : ${outputDir}`
563
+ ];
564
+ if (failed > 0) {
565
+ lines.push("", " Failed components:");
566
+ for (const r of results) {
567
+ if (!r.success) {
568
+ lines.push(` \u2717 ${r.name}: ${r.errorMessage ?? "unknown error"}`);
569
+ }
570
+ }
571
+ }
572
+ lines.push("\u2500".repeat(60));
573
+ return lines.join("\n");
574
+ }
575
+ function escapeHtml(str) {
576
+ return str.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#039;");
577
+ }
578
+ function csvEscape(value) {
579
+ if (value.includes(",") || value.includes('"') || value.includes("\n")) {
580
+ return `"${value.replace(/"/g, '""')}"`;
581
+ }
582
+ return value;
583
+ }
584
+
585
+ // src/render-commands.ts
586
+ var MANIFEST_PATH2 = ".reactscope/manifest.json";
587
+ var DEFAULT_OUTPUT_DIR = ".reactscope/renders";
588
+ var _pool = null;
589
+ async function getPool(viewportWidth, viewportHeight) {
590
+ if (_pool === null) {
591
+ _pool = new BrowserPool({
592
+ size: { browsers: 1, pagesPerBrowser: 4 },
593
+ viewportWidth,
594
+ viewportHeight
595
+ });
596
+ await _pool.init();
597
+ }
598
+ return _pool;
599
+ }
600
+ async function shutdownPool() {
601
+ if (_pool !== null) {
602
+ await _pool.close();
603
+ _pool = null;
604
+ }
605
+ }
606
+ function buildRenderer(filePath, componentName, viewportWidth, viewportHeight) {
607
+ const satori = new SatoriRenderer({
608
+ defaultViewport: { width: viewportWidth, height: viewportHeight }
609
+ });
610
+ return {
611
+ _satori: satori,
612
+ async renderCell(props, _complexityClass) {
613
+ const startMs = performance.now();
614
+ const pool = await getPool(viewportWidth, viewportHeight);
615
+ const htmlHarness = await buildComponentHarness(
616
+ filePath,
617
+ componentName,
618
+ props,
619
+ viewportWidth
620
+ );
621
+ const slot = await pool.acquire();
622
+ const { page } = slot;
623
+ try {
624
+ await page.setContent(htmlHarness, { waitUntil: "load" });
625
+ await page.waitForFunction(
626
+ () => {
627
+ const w = window;
628
+ return w.__SCOPE_RENDER_COMPLETE__ === true;
629
+ },
630
+ { timeout: 15e3 }
631
+ );
632
+ const renderError = await page.evaluate(() => {
633
+ return window.__SCOPE_RENDER_ERROR__ ?? null;
634
+ });
635
+ if (renderError !== null) {
636
+ throw new Error(`Component render error: ${renderError}`);
637
+ }
638
+ const renderTimeMs = performance.now() - startMs;
639
+ const rootLocator = page.locator("[data-reactscope-root]");
640
+ const boundingBox = await rootLocator.boundingBox();
641
+ if (boundingBox === null || boundingBox.width === 0 || boundingBox.height === 0) {
642
+ throw new Error(
643
+ `Component "${componentName}" rendered with zero bounding box \u2014 it may be invisible or not mounted`
644
+ );
645
+ }
646
+ const screenshot = await page.screenshot({
647
+ clip: {
648
+ x: boundingBox.x,
649
+ y: boundingBox.y,
650
+ width: boundingBox.width,
651
+ height: boundingBox.height
652
+ },
653
+ type: "png"
654
+ });
655
+ const computedStyles = {};
656
+ const styles = await page.evaluate((sel) => {
657
+ const el = document.querySelector(sel);
658
+ if (el === null) return {};
659
+ const computed = window.getComputedStyle(el);
660
+ const out = {};
661
+ for (const prop of [
662
+ "display",
663
+ "width",
664
+ "height",
665
+ "color",
666
+ "backgroundColor",
667
+ "fontSize",
668
+ "fontFamily",
669
+ "padding",
670
+ "margin"
671
+ ]) {
672
+ out[prop] = computed.getPropertyValue(prop);
673
+ }
674
+ return out;
675
+ }, "[data-reactscope-root] > *");
676
+ computedStyles["[data-reactscope-root] > *"] = styles;
677
+ return {
678
+ screenshot,
679
+ width: Math.round(boundingBox.width),
680
+ height: Math.round(boundingBox.height),
681
+ renderTimeMs,
682
+ computedStyles
683
+ };
684
+ } finally {
685
+ pool.release(slot);
686
+ }
687
+ }
688
+ };
689
+ }
690
+ function registerRenderSingle(renderCmd) {
691
+ 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(
692
+ async (componentName, opts) => {
693
+ try {
694
+ const manifest = loadManifest(opts.manifest);
695
+ const descriptor = manifest.components[componentName];
696
+ if (descriptor === void 0) {
697
+ const available = Object.keys(manifest.components).slice(0, 5).join(", ");
698
+ throw new Error(
699
+ `Component "${componentName}" not found in manifest.
700
+ Available: ${available}`
701
+ );
702
+ }
703
+ let props = {};
704
+ if (opts.props !== void 0) {
705
+ try {
706
+ props = JSON.parse(opts.props);
707
+ } catch {
708
+ throw new Error(`Invalid props JSON: ${opts.props}`);
709
+ }
710
+ }
711
+ const { width, height } = parseViewport(opts.viewport);
712
+ const rootDir = process.cwd();
713
+ const filePath = resolve(rootDir, descriptor.filePath);
714
+ const renderer = buildRenderer(filePath, componentName, width, height);
715
+ process.stderr.write(
716
+ `Rendering ${componentName} [${descriptor.complexityClass}] at ${width}\xD7${height}\u2026
717
+ `
718
+ );
719
+ const outcome = await safeRender(
720
+ () => renderer.renderCell(props, descriptor.complexityClass),
721
+ {
722
+ props,
723
+ sourceLocation: {
724
+ file: descriptor.filePath,
725
+ line: descriptor.loc.start,
726
+ column: 0
727
+ }
728
+ }
729
+ );
730
+ await shutdownPool();
731
+ if (outcome.crashed) {
732
+ process.stderr.write(`\u2717 Render failed: ${outcome.error.message}
733
+ `);
734
+ const hintList = outcome.error.heuristicFlags.join(", ");
735
+ if (hintList.length > 0) {
736
+ process.stderr.write(` Hints: ${hintList}
737
+ `);
738
+ }
739
+ process.exit(1);
740
+ }
741
+ const result = outcome.result;
742
+ if (opts.output !== void 0) {
743
+ const outPath = resolve(process.cwd(), opts.output);
744
+ writeFileSync(outPath, result.screenshot);
745
+ process.stdout.write(
746
+ `\u2713 ${componentName} \u2192 ${opts.output} (${result.width}\xD7${result.height}, ${result.renderTimeMs.toFixed(0)}ms)
747
+ `
748
+ );
749
+ return;
750
+ }
751
+ const fmt = resolveSingleFormat(opts.format);
752
+ if (fmt === "json") {
753
+ const json = formatRenderJson(componentName, props, result);
754
+ process.stdout.write(`${JSON.stringify(json, null, 2)}
755
+ `);
756
+ } else if (fmt === "file") {
757
+ const dir = resolve(process.cwd(), DEFAULT_OUTPUT_DIR);
758
+ mkdirSync(dir, { recursive: true });
759
+ const outPath = resolve(dir, `${componentName}.png`);
760
+ writeFileSync(outPath, result.screenshot);
761
+ const relPath = `${DEFAULT_OUTPUT_DIR}/${componentName}.png`;
762
+ process.stdout.write(
763
+ `\u2713 ${componentName} \u2192 ${relPath} (${result.width}\xD7${result.height}, ${result.renderTimeMs.toFixed(0)}ms)
764
+ `
765
+ );
766
+ } else {
767
+ const dir = resolve(process.cwd(), DEFAULT_OUTPUT_DIR);
768
+ mkdirSync(dir, { recursive: true });
769
+ const outPath = resolve(dir, `${componentName}.png`);
770
+ writeFileSync(outPath, result.screenshot);
771
+ const relPath = `${DEFAULT_OUTPUT_DIR}/${componentName}.png`;
772
+ process.stdout.write(
773
+ `\u2713 ${componentName} \u2192 ${relPath} (${result.width}\xD7${result.height}, ${result.renderTimeMs.toFixed(0)}ms)
774
+ `
775
+ );
776
+ }
777
+ } catch (err) {
778
+ await shutdownPool();
779
+ process.stderr.write(`Error: ${err instanceof Error ? err.message : String(err)}
780
+ `);
781
+ process.exit(1);
782
+ }
783
+ }
784
+ );
785
+ }
786
+ function registerRenderMatrix(renderCmd) {
787
+ 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(
788
+ "--contexts <ids>",
789
+ "Composition context IDs, comma-separated (e.g. centered,rtl,sidebar)"
790
+ ).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(
791
+ async (componentName, opts) => {
792
+ try {
793
+ const manifest = loadManifest(opts.manifest);
794
+ const descriptor = manifest.components[componentName];
795
+ if (descriptor === void 0) {
796
+ const available = Object.keys(manifest.components).slice(0, 5).join(", ");
797
+ throw new Error(
798
+ `Component "${componentName}" not found in manifest.
799
+ Available: ${available}`
800
+ );
801
+ }
802
+ const concurrency = Math.max(1, parseInt(opts.concurrency, 10) || 8);
803
+ const { width, height } = { width: 375, height: 812 };
804
+ const rootDir = process.cwd();
805
+ const filePath = resolve(rootDir, descriptor.filePath);
806
+ const renderer = buildRenderer(filePath, componentName, width, height);
807
+ const axes = [];
808
+ if (opts.axes !== void 0) {
809
+ const axisSpecs = opts.axes.trim().split(/\s+/);
810
+ for (const spec of axisSpecs) {
811
+ const colonIdx = spec.indexOf(":");
812
+ if (colonIdx < 0) {
813
+ throw new Error(`Invalid axis spec "${spec}". Expected format: name:val1,val2,...`);
814
+ }
815
+ const name = spec.slice(0, colonIdx);
816
+ const values = spec.slice(colonIdx + 1).split(",").map((v) => v.trim());
817
+ if (name.length === 0 || values.length === 0) {
818
+ throw new Error(`Invalid axis spec "${spec}"`);
819
+ }
820
+ axes.push({ name, values });
821
+ }
822
+ }
823
+ if (opts.contexts !== void 0) {
824
+ const ids = opts.contexts.split(",").map((s) => s.trim());
825
+ const validIds = new Set(ALL_CONTEXT_IDS);
826
+ for (const id of ids) {
827
+ if (!validIds.has(id)) {
828
+ throw new Error(
829
+ `Unknown context ID "${id}". Available: ${ALL_CONTEXT_IDS.join(", ")}`
830
+ );
831
+ }
832
+ }
833
+ const cAxis = contextAxis(ids);
834
+ axes.push({ name: cAxis.name, values: cAxis.values });
835
+ }
836
+ if (opts.stress !== void 0) {
837
+ const ids = opts.stress.split(",").map((s) => s.trim());
838
+ for (const id of ids) {
839
+ try {
840
+ const sAxis = stressAxis(id);
841
+ axes.push({ name: sAxis.name, values: sAxis.values });
842
+ } catch {
843
+ throw new Error(
844
+ `Unknown stress preset "${id}". Available: ${ALL_STRESS_IDS.join(", ")}`
845
+ );
846
+ }
847
+ }
848
+ }
849
+ if (axes.length === 0) {
850
+ throw new Error(
851
+ "No axes defined. Use --axes, --contexts, or --stress to specify matrix dimensions."
852
+ );
853
+ }
854
+ process.stderr.write(
855
+ `Rendering matrix for ${componentName}: ${axes.map((a) => `${a.name}(${a.values.length})`).join(" \xD7 ")}\u2026
856
+ `
857
+ );
858
+ const matrix = new RenderMatrix(renderer, axes, {
859
+ complexityClass: descriptor.complexityClass,
860
+ concurrency
861
+ });
862
+ const result = await matrix.render();
863
+ await shutdownPool();
864
+ process.stderr.write(
865
+ `Done. ${result.stats.totalCells} cells, avg ${result.stats.avgRenderTimeMs.toFixed(1)}ms
866
+ `
867
+ );
868
+ if (opts.sprite !== void 0) {
869
+ const { SpriteSheetGenerator } = await import('@agent-scope/render');
870
+ const gen = new SpriteSheetGenerator();
871
+ const sheet = await gen.generate(result);
872
+ const spritePath = resolve(process.cwd(), opts.sprite);
873
+ writeFileSync(spritePath, sheet.png);
874
+ process.stderr.write(`Sprite sheet saved to ${spritePath}
875
+ `);
876
+ }
877
+ const fmt = resolveMatrixFormat(opts.format, opts.sprite !== void 0);
878
+ if (fmt === "file") {
879
+ const { SpriteSheetGenerator } = await import('@agent-scope/render');
880
+ const gen = new SpriteSheetGenerator();
881
+ const sheet = await gen.generate(result);
882
+ const dir = resolve(process.cwd(), DEFAULT_OUTPUT_DIR);
883
+ mkdirSync(dir, { recursive: true });
884
+ const outPath = resolve(dir, `${componentName}-matrix.png`);
885
+ writeFileSync(outPath, sheet.png);
886
+ const relPath = `${DEFAULT_OUTPUT_DIR}/${componentName}-matrix.png`;
887
+ process.stdout.write(
888
+ `\u2713 ${componentName} matrix (${result.stats.totalCells} cells) \u2192 ${relPath} (${result.stats.wallClockTimeMs.toFixed(0)}ms total)
889
+ `
890
+ );
891
+ } else if (fmt === "json") {
892
+ process.stdout.write(`${JSON.stringify(formatMatrixJson(result), null, 2)}
893
+ `);
894
+ } else if (fmt === "png") {
895
+ if (opts.sprite !== void 0) {
896
+ } else {
897
+ const { SpriteSheetGenerator } = await import('@agent-scope/render');
898
+ const gen = new SpriteSheetGenerator();
899
+ const sheet = await gen.generate(result);
900
+ process.stdout.write(sheet.png);
901
+ }
902
+ } else if (fmt === "html") {
903
+ process.stdout.write(formatMatrixHtml(componentName, result));
904
+ } else if (fmt === "csv") {
905
+ process.stdout.write(formatMatrixCsv(componentName, result));
906
+ }
907
+ } catch (err) {
908
+ await shutdownPool();
909
+ process.stderr.write(`Error: ${err instanceof Error ? err.message : String(err)}
910
+ `);
911
+ process.exit(1);
912
+ }
913
+ }
914
+ );
915
+ }
916
+ function registerRenderAll(renderCmd) {
917
+ 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(
918
+ async (opts) => {
919
+ try {
920
+ const manifest = loadManifest(opts.manifest);
921
+ const componentNames = Object.keys(manifest.components);
922
+ const total = componentNames.length;
923
+ if (total === 0) {
924
+ process.stderr.write("No components found in manifest.\n");
925
+ return;
926
+ }
927
+ const concurrency = Math.max(1, parseInt(opts.concurrency, 10) || 4);
928
+ const outputDir = resolve(process.cwd(), opts.outputDir);
929
+ mkdirSync(outputDir, { recursive: true });
930
+ const rootDir = process.cwd();
931
+ process.stderr.write(`Rendering ${total} components (concurrency: ${concurrency})\u2026
932
+ `);
933
+ const results = [];
934
+ let completed = 0;
935
+ const renderOne = async (name) => {
936
+ const descriptor = manifest.components[name];
937
+ if (descriptor === void 0) return;
938
+ const filePath = resolve(rootDir, descriptor.filePath);
939
+ const renderer = buildRenderer(filePath, name, 375, 812);
940
+ const outcome = await safeRender(
941
+ () => renderer.renderCell({}, descriptor.complexityClass),
942
+ {
943
+ props: {},
944
+ sourceLocation: {
945
+ file: descriptor.filePath,
946
+ line: descriptor.loc.start,
947
+ column: 0
948
+ }
949
+ }
950
+ );
951
+ completed++;
952
+ const pct = Math.round(completed / total * 100);
953
+ process.stderr.write(`${renderProgressBar(completed, total, name, pct)}\r`);
954
+ if (outcome.crashed) {
955
+ results.push({
956
+ name,
957
+ renderTimeMs: 0,
958
+ success: false,
959
+ errorMessage: outcome.error.message
960
+ });
961
+ const errPath = resolve(outputDir, `${name}.error.json`);
962
+ writeFileSync(
963
+ errPath,
964
+ JSON.stringify(
965
+ {
966
+ component: name,
967
+ errorMessage: outcome.error.message,
968
+ heuristicFlags: outcome.error.heuristicFlags,
969
+ propsAtCrash: outcome.error.propsAtCrash
970
+ },
971
+ null,
972
+ 2
973
+ )
974
+ );
975
+ return;
976
+ }
977
+ const result = outcome.result;
978
+ results.push({ name, renderTimeMs: result.renderTimeMs, success: true });
979
+ const pngPath = resolve(outputDir, `${name}.png`);
980
+ writeFileSync(pngPath, result.screenshot);
981
+ const jsonPath = resolve(outputDir, `${name}.json`);
982
+ writeFileSync(jsonPath, JSON.stringify(formatRenderJson(name, {}, result), null, 2));
983
+ if (isTTY()) {
984
+ process.stdout.write(
985
+ `\u2713 ${name} \u2192 ${opts.outputDir}/${name}.png (${result.width}\xD7${result.height}, ${result.renderTimeMs.toFixed(0)}ms)
986
+ `
987
+ );
988
+ }
989
+ };
990
+ let nextIdx = 0;
991
+ const worker = async () => {
992
+ while (nextIdx < componentNames.length) {
993
+ const i = nextIdx++;
994
+ const name = componentNames[i];
995
+ if (name !== void 0) {
996
+ await renderOne(name);
997
+ }
998
+ }
999
+ };
1000
+ const workers = [];
1001
+ for (let w = 0; w < Math.min(concurrency, total); w++) {
1002
+ workers.push(worker());
1003
+ }
1004
+ await Promise.all(workers);
1005
+ await shutdownPool();
1006
+ process.stderr.write("\n");
1007
+ const summary = formatSummaryText(results, outputDir);
1008
+ process.stderr.write(`${summary}
1009
+ `);
1010
+ } catch (err) {
1011
+ await shutdownPool();
1012
+ process.stderr.write(`Error: ${err instanceof Error ? err.message : String(err)}
1013
+ `);
1014
+ process.exit(1);
1015
+ }
1016
+ }
1017
+ );
1018
+ }
1019
+ function resolveSingleFormat(formatFlag) {
1020
+ if (formatFlag !== void 0) {
1021
+ const lower = formatFlag.toLowerCase();
1022
+ if (lower !== "png" && lower !== "json") {
1023
+ throw new Error(`Unknown format "${formatFlag}". Allowed: png, json`);
1024
+ }
1025
+ return lower;
1026
+ }
1027
+ return isTTY() ? "file" : "json";
1028
+ }
1029
+ function resolveMatrixFormat(formatFlag, spriteAlreadyWritten) {
1030
+ if (formatFlag !== void 0) {
1031
+ const lower = formatFlag.toLowerCase();
1032
+ const allowed = ["json", "png", "html", "csv"];
1033
+ if (!allowed.includes(lower)) {
1034
+ throw new Error(`Unknown format "${formatFlag}". Allowed: ${allowed.join(", ")}`);
1035
+ }
1036
+ return lower;
1037
+ }
1038
+ if (isTTY() && !spriteAlreadyWritten) {
1039
+ return "file";
1040
+ }
1041
+ return "json";
1042
+ }
1043
+ function createRenderCommand() {
1044
+ const renderCmd = new Command("render").description(
1045
+ "Render components to PNG or JSON via esbuild + BrowserPool"
1046
+ );
1047
+ registerRenderSingle(renderCmd);
1048
+ registerRenderMatrix(renderCmd);
1049
+ registerRenderAll(renderCmd);
1050
+ return renderCmd;
1051
+ }
1052
+
1053
+ // src/tree-formatter.ts
1054
+ var BRANCH = "\u251C\u2500\u2500 ";
1055
+ var LAST_BRANCH = "\u2514\u2500\u2500 ";
1056
+ var VERTICAL = "\u2502 ";
1057
+ var EMPTY = " ";
1058
+ function buildLabel(node, options) {
1059
+ const parts = [node.name];
1060
+ if (node.type === "memo") {
1061
+ parts.push("[memo]");
1062
+ } else if (node.type === "forward_ref") {
1063
+ parts.push("[forwardRef]");
1064
+ } else if (node.type === "class") {
1065
+ parts.push("[class]");
1066
+ }
1067
+ if (options.showProps === true) {
1068
+ const props = node.props;
1069
+ if (props.type === "object" && "entries" in props && Array.isArray(props.entries)) {
1070
+ const entries = props.entries;
1071
+ const propNames = entries.map((e) => e.key).filter((k) => k !== "children").slice(0, 4);
1072
+ if (propNames.length > 0) {
1073
+ parts.push(`{${propNames.join(", ")}}`);
1074
+ }
1075
+ } else if (props.type === "undefined" || props.type === "null") ; else {
1076
+ const preview = props.preview;
1077
+ if (preview && preview !== "[object Object]") {
1078
+ parts.push(preview);
1079
+ }
1080
+ }
1081
+ }
1082
+ if (options.showHooks === true && node.state.length > 0) {
1083
+ const hookCounts = /* @__PURE__ */ new Map();
1084
+ for (const hook of node.state) {
1085
+ const label = hook.name ?? hook.type;
1086
+ hookCounts.set(label, (hookCounts.get(label) ?? 0) + 1);
1087
+ }
1088
+ const summary = Array.from(hookCounts.entries()).map(([k, v]) => v > 1 ? `${k}\xD7${v}` : k).join(", ");
1089
+ parts.push(`[${summary}]`);
1090
+ }
1091
+ return parts.join(" ");
1092
+ }
1093
+ function renderNode(node, prefix, isLast, depth, options, lines) {
1094
+ if (node.type === "host" && options.showHost !== true) {
1095
+ const visibleChildren2 = getVisibleChildren(node, options);
1096
+ for (let i = 0; i < visibleChildren2.length; i++) {
1097
+ const child = visibleChildren2[i];
1098
+ if (child !== void 0) {
1099
+ renderNode(child, prefix, i === visibleChildren2.length - 1, depth, options, lines);
1100
+ }
1101
+ }
1102
+ return;
1103
+ }
1104
+ const connector = isLast ? LAST_BRANCH : BRANCH;
1105
+ const label = buildLabel(node, options);
1106
+ lines.push(`${prefix}${connector}${label}`);
1107
+ if (options.maxDepth !== void 0 && depth >= options.maxDepth) {
1108
+ const childCount = countVisibleDescendants(node, options);
1109
+ if (childCount > 0) {
1110
+ const nextPrefix2 = prefix + (isLast ? EMPTY : VERTICAL);
1111
+ lines.push(`${nextPrefix2}${LAST_BRANCH}\u2026 (${childCount} more)`);
1112
+ }
1113
+ return;
1114
+ }
1115
+ const visibleChildren = getVisibleChildren(node, options);
1116
+ const nextPrefix = prefix + (isLast ? EMPTY : VERTICAL);
1117
+ for (let i = 0; i < visibleChildren.length; i++) {
1118
+ const child = visibleChildren[i];
1119
+ if (child !== void 0) {
1120
+ renderNode(child, nextPrefix, i === visibleChildren.length - 1, depth + 1, options, lines);
1121
+ }
1122
+ }
1123
+ }
1124
+ function getVisibleChildren(node, options) {
1125
+ if (options.showHost === true) {
1126
+ return node.children;
1127
+ }
1128
+ return flattenHostChildren(node.children);
1129
+ }
1130
+ function flattenHostChildren(children, options) {
1131
+ const result = [];
1132
+ for (const child of children) {
1133
+ if (child.type === "host") {
1134
+ result.push(...flattenHostChildren(child.children));
1135
+ } else {
1136
+ result.push(child);
1137
+ }
1138
+ }
1139
+ return result;
1140
+ }
1141
+ function countVisibleDescendants(node, options) {
1142
+ let count = 0;
1143
+ for (const child of node.children) {
1144
+ if (child.type !== "host" || options.showHost === true) {
1145
+ count += 1;
1146
+ }
1147
+ count += countVisibleDescendants(child, options);
1148
+ }
1149
+ return count;
1150
+ }
1151
+ function formatTree(root, options = {}) {
1152
+ const lines = [];
1153
+ if (root.type !== "host" || options.showHost === true) {
1154
+ lines.push(buildLabel(root, options));
1155
+ if (options.maxDepth === 0) {
1156
+ const childCount = countVisibleDescendants(root, options);
1157
+ if (childCount > 0) {
1158
+ lines.push(`${LAST_BRANCH}\u2026 (${childCount} more)`);
1159
+ }
1160
+ } else {
1161
+ const visibleChildren = getVisibleChildren(root, options);
1162
+ for (let i = 0; i < visibleChildren.length; i++) {
1163
+ const child = visibleChildren[i];
1164
+ if (child !== void 0) {
1165
+ renderNode(child, "", i === visibleChildren.length - 1, 1, options, lines);
1166
+ }
1167
+ }
1168
+ }
1169
+ } else {
1170
+ const visibleChildren = getVisibleChildren(root, options);
1171
+ for (let i = 0; i < visibleChildren.length; i++) {
1172
+ const child = visibleChildren[i];
1173
+ if (child !== void 0) {
1174
+ renderNode(child, "", i === visibleChildren.length - 1, 1, options, lines);
1175
+ }
1176
+ }
1177
+ }
1178
+ return lines.join("\n");
1179
+ }
1180
+ function treeDepth(node) {
1181
+ if (node.children.length === 0) return 0;
1182
+ return 1 + Math.max(...node.children.map(treeDepth));
1183
+ }
1184
+
1185
+ // src/report-formatter.ts
1186
+ function collectComponentStats(root) {
1187
+ const byType = {};
1188
+ function walk(node) {
1189
+ if (node.type !== "host") {
1190
+ byType[node.type] = (byType[node.type] ?? 0) + 1;
1191
+ }
1192
+ for (const child of node.children) {
1193
+ walk(child);
1194
+ }
1195
+ }
1196
+ walk(root);
1197
+ const total = Object.values(byType).reduce((s, n) => s + n, 0);
1198
+ return { total, byType };
1199
+ }
1200
+ function collectHookStats(root) {
1201
+ const byType = {};
1202
+ function walk(node) {
1203
+ for (const hook of node.state) {
1204
+ const label = hook.name ?? hook.type;
1205
+ byType[label] = (byType[label] ?? 0) + 1;
1206
+ }
1207
+ for (const child of node.children) {
1208
+ walk(child);
1209
+ }
1210
+ }
1211
+ walk(root);
1212
+ const total = Object.values(byType).reduce((s, n) => s + n, 0);
1213
+ return { total, byType };
1214
+ }
1215
+ function countErrorBoundaries(root) {
1216
+ let count = 0;
1217
+ function walk(node) {
1218
+ if (node.type === "class" && (node.name.toLowerCase().includes("error") || node.name.toLowerCase().includes("boundary"))) {
1219
+ count += 1;
1220
+ }
1221
+ for (const child of node.children) {
1222
+ walk(child);
1223
+ }
1224
+ }
1225
+ walk(root);
1226
+ return count;
1227
+ }
1228
+ function pad2(s, width) {
1229
+ return s.padEnd(width, " ");
1230
+ }
1231
+ function row(label, value, labelWidth = 22) {
1232
+ return `${pad2(label, labelWidth)}${value}`;
1233
+ }
1234
+ function rule(width) {
1235
+ return "\u2501".repeat(width);
1236
+ }
1237
+ function formatBreakdown(byType, limit = 5) {
1238
+ const entries = Object.entries(byType).sort(([, a], [, b]) => b - a).slice(0, limit);
1239
+ if (entries.length === 0) return "";
1240
+ return `(${entries.map(([k, v]) => `${v} ${k}`).join(", ")})`;
1241
+ }
1242
+ function formatReport(report, options = {}) {
1243
+ if (options.json === true) {
1244
+ return JSON.stringify(report, null, 2);
1245
+ }
1246
+ const url = report.url;
1247
+ const title = `Scope Report for ${url}`;
1248
+ const ruleWidth = Math.min(Math.max(title.length, 40), 72);
1249
+ const componentStats = collectComponentStats(report.tree);
1250
+ const hookStats = collectHookStats(report.tree);
1251
+ const maxDepth = treeDepth(report.tree);
1252
+ const errorBoundaries = countErrorBoundaries(report.tree);
1253
+ const suspendedCount = report.suspenseBoundaries.filter((b) => b.isSuspended).length;
1254
+ const resolvedCount = report.suspenseBoundaries.length - suspendedCount;
1255
+ const consoleLevels = {};
1256
+ for (const entry of report.consoleEntries) {
1257
+ consoleLevels[entry.level] = (consoleLevels[entry.level] ?? 0) + 1;
1258
+ }
1259
+ const typeNames = {
1260
+ function: "function",
1261
+ memo: "memo",
1262
+ forward_ref: "forwardRef",
1263
+ class: "class"
1264
+ };
1265
+ const componentBreakdown = Object.entries(componentStats.byType).sort(([, a], [, b]) => b - a).map(([k, v]) => `${v} ${typeNames[k] ?? k}`).join(", ");
1266
+ const hookBreakdown = formatBreakdown(hookStats.byType, 6);
1267
+ const consoleBreakdownParts = Object.entries(consoleLevels).sort(([, a], [, b]) => b - a).map(([k, v]) => `${v} ${k}`);
1268
+ const consoleBreakdown = consoleBreakdownParts.length > 0 ? ` (${consoleBreakdownParts.join(", ")})` : "";
1269
+ const suspenseSummary = report.suspenseBoundaries.length > 0 ? ` (${suspendedCount} pending, ${resolvedCount} resolved)` : "";
1270
+ const lines = [
1271
+ title,
1272
+ rule(ruleWidth),
1273
+ row(
1274
+ "Components:",
1275
+ `${componentStats.total} total${componentBreakdown ? ` (${componentBreakdown})` : ""}`
1276
+ ),
1277
+ row("Max depth:", String(maxDepth)),
1278
+ row("Hooks:", hookStats.total > 0 ? `${hookStats.total} total ${hookBreakdown}` : "none"),
1279
+ row("Error boundaries:", String(errorBoundaries)),
1280
+ row("Suspense boundaries:", `${report.suspenseBoundaries.length}${suspenseSummary}`),
1281
+ row("Console entries:", `${report.consoleEntries.length}${consoleBreakdown}`),
1282
+ row(
1283
+ "Errors captured:",
1284
+ report.errors.length > 0 ? `${report.errors.length} (see JSON output for details)` : "none"
1285
+ ),
1286
+ row("Capture time:", `${report.capturedIn}ms`)
1287
+ ];
1288
+ if (report.route !== null) {
1289
+ lines.push(row("Route:", report.route.pattern ?? report.url));
1290
+ }
1291
+ return lines.join("\n");
1292
+ }
1293
+ function buildStructuredReport(report) {
1294
+ const componentStats = collectComponentStats(report.tree);
1295
+ const hookStats = collectHookStats(report.tree);
1296
+ const maxDepth = treeDepth(report.tree);
1297
+ const errorBoundaries = countErrorBoundaries(report.tree);
1298
+ const suspendedCount = report.suspenseBoundaries.filter((b) => b.isSuspended).length;
1299
+ const consoleLevels = {};
1300
+ for (const entry of report.consoleEntries) {
1301
+ consoleLevels[entry.level] = (consoleLevels[entry.level] ?? 0) + 1;
1302
+ }
1303
+ return {
1304
+ url: report.url,
1305
+ timestamp: report.timestamp,
1306
+ capturedIn: report.capturedIn,
1307
+ components: {
1308
+ total: componentStats.total,
1309
+ byType: componentStats.byType,
1310
+ maxDepth
1311
+ },
1312
+ hooks: {
1313
+ total: hookStats.total,
1314
+ byType: hookStats.byType
1315
+ },
1316
+ errorBoundaries,
1317
+ suspenseBoundaries: {
1318
+ total: report.suspenseBoundaries.length,
1319
+ suspended: suspendedCount,
1320
+ resolved: report.suspenseBoundaries.length - suspendedCount
1321
+ },
1322
+ consoleEntries: {
1323
+ total: report.consoleEntries.length,
1324
+ byLevel: consoleLevels
1325
+ },
1326
+ errors: report.errors.length,
1327
+ route: report.route?.pattern ?? null
1328
+ };
1329
+ }
1330
+
1331
+ // src/program.ts
1332
+ function createProgram(options = {}) {
1333
+ const program = new Command("scope").version(options.version ?? "0.1.0").description("Scope \u2014 React instrumentation toolkit");
1334
+ program.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(
1335
+ async (url, opts) => {
1336
+ try {
1337
+ const { report } = await browserCapture({
1338
+ url,
1339
+ timeout: Number.parseInt(opts.timeout, 10),
1340
+ wait: Number.parseInt(opts.wait, 10)
1341
+ });
1342
+ if (opts.output !== void 0) {
1343
+ writeReportToFile(report, opts.output, opts.pretty);
1344
+ process.stderr.write(`Captured to ${opts.output}
1345
+ `);
1346
+ } else {
1347
+ const json = opts.pretty ? JSON.stringify(report, null, 2) : JSON.stringify(report);
1348
+ process.stdout.write(`${json}
1349
+ `);
1350
+ }
1351
+ } catch (err) {
1352
+ process.stderr.write(`Error: ${err instanceof Error ? err.message : String(err)}
1353
+ `);
1354
+ process.exit(1);
1355
+ }
1356
+ }
1357
+ );
1358
+ program.command("tree <url>").description("Display the React component tree from a live URL").option("--depth <n>", "Max depth to display (default: unlimited)").option("--show-props", "Include prop names next to components", false).option("--show-hooks", "Show hook counts per component", 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(
1359
+ async (url, opts) => {
1360
+ try {
1361
+ const { report } = await browserCapture({
1362
+ url,
1363
+ timeout: Number.parseInt(opts.timeout, 10),
1364
+ wait: Number.parseInt(opts.wait, 10)
1365
+ });
1366
+ const maxDepth = opts.depth !== void 0 ? Number.parseInt(opts.depth, 10) : void 0;
1367
+ const tree = formatTree(report.tree, {
1368
+ maxDepth,
1369
+ showProps: opts.showProps,
1370
+ showHooks: opts.showHooks
1371
+ });
1372
+ process.stdout.write(`${tree}
1373
+ `);
1374
+ } catch (err) {
1375
+ process.stderr.write(`Error: ${err instanceof Error ? err.message : String(err)}
1376
+ `);
1377
+ process.exit(1);
1378
+ }
1379
+ }
1380
+ );
1381
+ program.command("report <url>").description("Capture and display a human-readable summary of a React app").option("--json", "Output as structured JSON instead of human-readable text", 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(
1382
+ async (url, opts) => {
1383
+ try {
1384
+ const { report } = await browserCapture({
1385
+ url,
1386
+ timeout: Number.parseInt(opts.timeout, 10),
1387
+ wait: Number.parseInt(opts.wait, 10)
1388
+ });
1389
+ if (opts.json) {
1390
+ const structured = buildStructuredReport(report);
1391
+ process.stdout.write(`${JSON.stringify(structured, null, 2)}
1392
+ `);
1393
+ } else {
1394
+ const summary = formatReport(report);
1395
+ process.stdout.write(`${summary}
1396
+ `);
1397
+ }
1398
+ } catch (err) {
1399
+ process.stderr.write(`Error: ${err instanceof Error ? err.message : String(err)}
1400
+ `);
1401
+ process.exit(1);
1402
+ }
1403
+ }
1404
+ );
1405
+ program.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) => {
1406
+ const raw = readFileSync(tracePath, "utf-8");
1407
+ const trace = loadTrace(raw);
1408
+ const source = generateTest(trace, {
1409
+ description: opts.description,
1410
+ outputPath: opts.output
1411
+ });
1412
+ process.stdout.write(`${source}
1413
+ `);
1414
+ });
1415
+ program.addCommand(createManifestCommand());
1416
+ program.addCommand(createRenderCommand());
1417
+ return program;
1418
+ }
1419
+
1420
+ export { createManifestCommand, createProgram, isTTY, matchGlob };
1421
+ //# sourceMappingURL=index.js.map
1422
+ //# sourceMappingURL=index.js.map