@agent-scope/cli 1.9.0 → 1.10.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
@@ -255,9 +255,9 @@ function createRL() {
255
255
  });
256
256
  }
257
257
  async function ask(rl, question) {
258
- return new Promise((resolve6) => {
258
+ return new Promise((resolve7) => {
259
259
  rl.question(question, (answer) => {
260
- resolve6(answer.trim());
260
+ resolve7(answer.trim());
261
261
  });
262
262
  });
263
263
  }
@@ -2047,6 +2047,332 @@ function createRenderCommand() {
2047
2047
  return renderCmd;
2048
2048
  }
2049
2049
 
2050
+ // src/report/baseline.ts
2051
+ import { existsSync as existsSync5, mkdirSync as mkdirSync4, rmSync, writeFileSync as writeFileSync5 } from "fs";
2052
+ import { resolve as resolve5 } from "path";
2053
+ import { generateManifest as generateManifest2 } from "@agent-scope/manifest";
2054
+ import { BrowserPool as BrowserPool3, safeRender as safeRender2 } from "@agent-scope/render";
2055
+ import { ComplianceEngine, TokenResolver } from "@agent-scope/tokens";
2056
+ var DEFAULT_BASELINE_DIR = ".reactscope/baseline";
2057
+ var _pool3 = null;
2058
+ async function getPool3(viewportWidth, viewportHeight) {
2059
+ if (_pool3 === null) {
2060
+ _pool3 = new BrowserPool3({
2061
+ size: { browsers: 1, pagesPerBrowser: 4 },
2062
+ viewportWidth,
2063
+ viewportHeight
2064
+ });
2065
+ await _pool3.init();
2066
+ }
2067
+ return _pool3;
2068
+ }
2069
+ async function shutdownPool3() {
2070
+ if (_pool3 !== null) {
2071
+ await _pool3.close();
2072
+ _pool3 = null;
2073
+ }
2074
+ }
2075
+ async function renderComponent(filePath, componentName, props, viewportWidth, viewportHeight) {
2076
+ const pool = await getPool3(viewportWidth, viewportHeight);
2077
+ const htmlHarness = await buildComponentHarness(filePath, componentName, props, viewportWidth);
2078
+ const slot = await pool.acquire();
2079
+ const { page } = slot;
2080
+ try {
2081
+ await page.setContent(htmlHarness, { waitUntil: "load" });
2082
+ await page.waitForFunction(
2083
+ () => {
2084
+ const w = window;
2085
+ return w.__SCOPE_RENDER_COMPLETE__ === true;
2086
+ },
2087
+ { timeout: 15e3 }
2088
+ );
2089
+ const renderError = await page.evaluate(() => {
2090
+ return window.__SCOPE_RENDER_ERROR__ ?? null;
2091
+ });
2092
+ if (renderError !== null) {
2093
+ throw new Error(`Component render error: ${renderError}`);
2094
+ }
2095
+ const rootDir = process.cwd();
2096
+ const classes = await page.evaluate(() => {
2097
+ const set = /* @__PURE__ */ new Set();
2098
+ document.querySelectorAll("[class]").forEach((el) => {
2099
+ for (const c of el.className.split(/\s+/)) {
2100
+ if (c) set.add(c);
2101
+ }
2102
+ });
2103
+ return [...set];
2104
+ });
2105
+ const projectCss = await getCompiledCssForClasses(rootDir, classes);
2106
+ if (projectCss != null && projectCss.length > 0) {
2107
+ await page.addStyleTag({ content: projectCss });
2108
+ }
2109
+ const startMs = performance.now();
2110
+ const rootLocator = page.locator("[data-reactscope-root]");
2111
+ const boundingBox = await rootLocator.boundingBox();
2112
+ if (boundingBox === null || boundingBox.width === 0 || boundingBox.height === 0) {
2113
+ throw new Error(
2114
+ `Component "${componentName}" rendered with zero bounding box \u2014 it may be invisible or not mounted`
2115
+ );
2116
+ }
2117
+ const PAD = 24;
2118
+ const MIN_W = 320;
2119
+ const MIN_H = 200;
2120
+ const clipX = Math.max(0, boundingBox.x - PAD);
2121
+ const clipY = Math.max(0, boundingBox.y - PAD);
2122
+ const rawW = boundingBox.width + PAD * 2;
2123
+ const rawH = boundingBox.height + PAD * 2;
2124
+ const clipW = Math.max(rawW, MIN_W);
2125
+ const clipH = Math.max(rawH, MIN_H);
2126
+ const safeW = Math.min(clipW, viewportWidth - clipX);
2127
+ const safeH = Math.min(clipH, viewportHeight - clipY);
2128
+ const screenshot = await page.screenshot({
2129
+ clip: { x: clipX, y: clipY, width: safeW, height: safeH },
2130
+ type: "png"
2131
+ });
2132
+ const computedStylesRaw = {};
2133
+ const styles = await page.evaluate((sel) => {
2134
+ const el = document.querySelector(sel);
2135
+ if (el === null) return {};
2136
+ const computed = window.getComputedStyle(el);
2137
+ const out = {};
2138
+ for (const prop of [
2139
+ "display",
2140
+ "width",
2141
+ "height",
2142
+ "color",
2143
+ "backgroundColor",
2144
+ "fontSize",
2145
+ "fontFamily",
2146
+ "padding",
2147
+ "margin"
2148
+ ]) {
2149
+ out[prop] = computed.getPropertyValue(prop);
2150
+ }
2151
+ return out;
2152
+ }, "[data-reactscope-root] > *");
2153
+ computedStylesRaw["[data-reactscope-root] > *"] = styles;
2154
+ const renderTimeMs = performance.now() - startMs;
2155
+ return {
2156
+ screenshot,
2157
+ width: Math.round(safeW),
2158
+ height: Math.round(safeH),
2159
+ renderTimeMs,
2160
+ computedStyles: computedStylesRaw
2161
+ };
2162
+ } finally {
2163
+ pool.release(slot);
2164
+ }
2165
+ }
2166
+ function extractComputedStyles(computedStylesRaw) {
2167
+ const flat = {};
2168
+ for (const styles of Object.values(computedStylesRaw)) {
2169
+ Object.assign(flat, styles);
2170
+ }
2171
+ const colors = {};
2172
+ const spacing = {};
2173
+ const typography = {};
2174
+ const borders = {};
2175
+ const shadows = {};
2176
+ for (const [prop, value] of Object.entries(flat)) {
2177
+ if (prop === "color" || prop === "backgroundColor") {
2178
+ colors[prop] = value;
2179
+ } else if (prop === "padding" || prop === "margin") {
2180
+ spacing[prop] = value;
2181
+ } else if (prop === "fontSize" || prop === "fontFamily" || prop === "fontWeight" || prop === "lineHeight") {
2182
+ typography[prop] = value;
2183
+ } else if (prop === "borderRadius" || prop === "borderWidth") {
2184
+ borders[prop] = value;
2185
+ } else if (prop === "boxShadow") {
2186
+ shadows[prop] = value;
2187
+ }
2188
+ }
2189
+ return { colors, spacing, typography, borders, shadows };
2190
+ }
2191
+ async function runBaseline(options = {}) {
2192
+ const {
2193
+ outputDir = DEFAULT_BASELINE_DIR,
2194
+ componentsGlob,
2195
+ manifestPath,
2196
+ viewportWidth = 375,
2197
+ viewportHeight = 812
2198
+ } = options;
2199
+ const startTime = performance.now();
2200
+ const rootDir = process.cwd();
2201
+ const baselineDir = resolve5(rootDir, outputDir);
2202
+ const rendersDir = resolve5(baselineDir, "renders");
2203
+ if (existsSync5(baselineDir)) {
2204
+ rmSync(baselineDir, { recursive: true, force: true });
2205
+ }
2206
+ mkdirSync4(rendersDir, { recursive: true });
2207
+ let manifest;
2208
+ if (manifestPath !== void 0) {
2209
+ const { readFileSync: readFileSync7 } = await import("fs");
2210
+ const absPath = resolve5(rootDir, manifestPath);
2211
+ if (!existsSync5(absPath)) {
2212
+ throw new Error(`Manifest not found at ${absPath}.`);
2213
+ }
2214
+ manifest = JSON.parse(readFileSync7(absPath, "utf-8"));
2215
+ process.stderr.write(`Loaded manifest from ${manifestPath}
2216
+ `);
2217
+ } else {
2218
+ process.stderr.write("Scanning for React components\u2026\n");
2219
+ manifest = await generateManifest2({ rootDir });
2220
+ const count = Object.keys(manifest.components).length;
2221
+ process.stderr.write(`Found ${count} components.
2222
+ `);
2223
+ }
2224
+ writeFileSync5(resolve5(baselineDir, "manifest.json"), JSON.stringify(manifest, null, 2), "utf-8");
2225
+ let componentNames = Object.keys(manifest.components);
2226
+ if (componentsGlob !== void 0) {
2227
+ componentNames = componentNames.filter((name) => matchGlob(componentsGlob, name));
2228
+ process.stderr.write(
2229
+ `Filtered to ${componentNames.length} components matching "${componentsGlob}".
2230
+ `
2231
+ );
2232
+ }
2233
+ const total = componentNames.length;
2234
+ if (total === 0) {
2235
+ process.stderr.write("No components to baseline.\n");
2236
+ const emptyReport = {
2237
+ components: {},
2238
+ totalProperties: 0,
2239
+ totalOnSystem: 0,
2240
+ totalOffSystem: 0,
2241
+ aggregateCompliance: 1,
2242
+ auditedAt: (/* @__PURE__ */ new Date()).toISOString()
2243
+ };
2244
+ writeFileSync5(
2245
+ resolve5(baselineDir, "compliance.json"),
2246
+ JSON.stringify(emptyReport, null, 2),
2247
+ "utf-8"
2248
+ );
2249
+ return {
2250
+ baselineDir,
2251
+ componentCount: 0,
2252
+ failureCount: 0,
2253
+ wallClockMs: performance.now() - startTime
2254
+ };
2255
+ }
2256
+ process.stderr.write(`Rendering ${total} components\u2026
2257
+ `);
2258
+ const computedStylesMap = /* @__PURE__ */ new Map();
2259
+ let completed = 0;
2260
+ let failureCount = 0;
2261
+ const CONCURRENCY = 4;
2262
+ let nextIdx = 0;
2263
+ const renderOne = async (name) => {
2264
+ const descriptor = manifest.components[name];
2265
+ if (descriptor === void 0) return;
2266
+ const filePath = resolve5(rootDir, descriptor.filePath);
2267
+ const outcome = await safeRender2(
2268
+ () => renderComponent(filePath, name, {}, viewportWidth, viewportHeight),
2269
+ {
2270
+ props: {},
2271
+ sourceLocation: {
2272
+ file: descriptor.filePath,
2273
+ line: descriptor.loc.start,
2274
+ column: 0
2275
+ }
2276
+ }
2277
+ );
2278
+ completed++;
2279
+ const pct = Math.round(completed / total * 100);
2280
+ if (isTTY()) {
2281
+ process.stderr.write(`${renderProgressBar(completed, total, name, pct)}\r`);
2282
+ }
2283
+ if (outcome.crashed) {
2284
+ failureCount++;
2285
+ const errPath = resolve5(rendersDir, `${name}.error.json`);
2286
+ writeFileSync5(
2287
+ errPath,
2288
+ JSON.stringify(
2289
+ {
2290
+ component: name,
2291
+ errorMessage: outcome.error.message,
2292
+ heuristicFlags: outcome.error.heuristicFlags,
2293
+ propsAtCrash: outcome.error.propsAtCrash
2294
+ },
2295
+ null,
2296
+ 2
2297
+ ),
2298
+ "utf-8"
2299
+ );
2300
+ return;
2301
+ }
2302
+ const result = outcome.result;
2303
+ writeFileSync5(resolve5(rendersDir, `${name}.png`), result.screenshot);
2304
+ const jsonOutput = formatRenderJson(name, {}, result);
2305
+ writeFileSync5(
2306
+ resolve5(rendersDir, `${name}.json`),
2307
+ JSON.stringify(jsonOutput, null, 2),
2308
+ "utf-8"
2309
+ );
2310
+ computedStylesMap.set(name, extractComputedStyles(result.computedStyles));
2311
+ };
2312
+ const worker = async () => {
2313
+ while (nextIdx < componentNames.length) {
2314
+ const i = nextIdx++;
2315
+ const name = componentNames[i];
2316
+ if (name !== void 0) {
2317
+ await renderOne(name);
2318
+ }
2319
+ }
2320
+ };
2321
+ const workers = [];
2322
+ for (let w = 0; w < Math.min(CONCURRENCY, total); w++) {
2323
+ workers.push(worker());
2324
+ }
2325
+ await Promise.all(workers);
2326
+ await shutdownPool3();
2327
+ if (isTTY()) {
2328
+ process.stderr.write("\n");
2329
+ }
2330
+ const resolver = new TokenResolver([]);
2331
+ const engine = new ComplianceEngine(resolver);
2332
+ const batchReport = engine.auditBatch(computedStylesMap);
2333
+ writeFileSync5(
2334
+ resolve5(baselineDir, "compliance.json"),
2335
+ JSON.stringify(batchReport, null, 2),
2336
+ "utf-8"
2337
+ );
2338
+ const wallClockMs = performance.now() - startTime;
2339
+ const successCount = total - failureCount;
2340
+ process.stderr.write(
2341
+ `
2342
+ Baseline complete: ${successCount}/${total} components rendered` + (failureCount > 0 ? ` (${failureCount} failed)` : "") + ` in ${(wallClockMs / 1e3).toFixed(1)}s
2343
+ `
2344
+ );
2345
+ process.stderr.write(`Snapshot saved to ${baselineDir}
2346
+ `);
2347
+ return { baselineDir, componentCount: total, failureCount, wallClockMs };
2348
+ }
2349
+ function registerBaselineSubCommand(reportCmd) {
2350
+ reportCmd.command("baseline").description("Capture a baseline snapshot (manifest + renders + compliance) for later diffing").option(
2351
+ "-o, --output <dir>",
2352
+ "Output directory for the baseline snapshot",
2353
+ DEFAULT_BASELINE_DIR
2354
+ ).option("--components <glob>", "Glob pattern to baseline a subset of components").option("--manifest <path>", "Path to an existing manifest.json to use instead of regenerating").option("--viewport <WxH>", "Viewport size, e.g. 1280x720", "375x812").action(
2355
+ async (opts) => {
2356
+ try {
2357
+ const [wStr, hStr] = opts.viewport.split("x");
2358
+ const viewportWidth = Number.parseInt(wStr ?? "375", 10);
2359
+ const viewportHeight = Number.parseInt(hStr ?? "812", 10);
2360
+ await runBaseline({
2361
+ outputDir: opts.output,
2362
+ componentsGlob: opts.components,
2363
+ manifestPath: opts.manifest,
2364
+ viewportWidth,
2365
+ viewportHeight
2366
+ });
2367
+ } catch (err) {
2368
+ process.stderr.write(`Error: ${err instanceof Error ? err.message : String(err)}
2369
+ `);
2370
+ process.exit(1);
2371
+ }
2372
+ }
2373
+ );
2374
+ }
2375
+
2050
2376
  // src/tree-formatter.ts
2051
2377
  var BRANCH = "\u251C\u2500\u2500 ";
2052
2378
  var LAST_BRANCH = "\u2514\u2500\u2500 ";
@@ -2327,12 +2653,12 @@ function buildStructuredReport(report) {
2327
2653
  }
2328
2654
 
2329
2655
  // src/tokens/commands.ts
2330
- import { existsSync as existsSync5, readFileSync as readFileSync5 } from "fs";
2331
- import { resolve as resolve5 } from "path";
2656
+ import { existsSync as existsSync6, readFileSync as readFileSync5 } from "fs";
2657
+ import { resolve as resolve6 } from "path";
2332
2658
  import {
2333
2659
  parseTokenFileSync,
2334
2660
  TokenParseError,
2335
- TokenResolver,
2661
+ TokenResolver as TokenResolver2,
2336
2662
  TokenValidationError,
2337
2663
  validateTokenFile
2338
2664
  } from "@agent-scope/tokens";
@@ -2358,24 +2684,24 @@ function buildTable2(headers, rows) {
2358
2684
  }
2359
2685
  function resolveTokenFilePath(fileFlag) {
2360
2686
  if (fileFlag !== void 0) {
2361
- return resolve5(process.cwd(), fileFlag);
2687
+ return resolve6(process.cwd(), fileFlag);
2362
2688
  }
2363
- const configPath = resolve5(process.cwd(), CONFIG_FILE);
2364
- if (existsSync5(configPath)) {
2689
+ const configPath = resolve6(process.cwd(), CONFIG_FILE);
2690
+ if (existsSync6(configPath)) {
2365
2691
  try {
2366
2692
  const raw = readFileSync5(configPath, "utf-8");
2367
2693
  const config = JSON.parse(raw);
2368
2694
  if (typeof config === "object" && config !== null && "tokens" in config && typeof config.tokens === "object" && config.tokens !== null && typeof config.tokens?.file === "string") {
2369
2695
  const file = config.tokens.file;
2370
- return resolve5(process.cwd(), file);
2696
+ return resolve6(process.cwd(), file);
2371
2697
  }
2372
2698
  } catch {
2373
2699
  }
2374
2700
  }
2375
- return resolve5(process.cwd(), DEFAULT_TOKEN_FILE);
2701
+ return resolve6(process.cwd(), DEFAULT_TOKEN_FILE);
2376
2702
  }
2377
2703
  function loadTokens(absPath) {
2378
- if (!existsSync5(absPath)) {
2704
+ if (!existsSync6(absPath)) {
2379
2705
  throw new Error(
2380
2706
  `Token file not found at ${absPath}.
2381
2707
  Create a reactscope.tokens.json file or use --file to specify a path.`
@@ -2421,7 +2747,7 @@ function registerGet2(tokensCmd) {
2421
2747
  try {
2422
2748
  const filePath = resolveTokenFilePath(opts.file);
2423
2749
  const { tokens } = loadTokens(filePath);
2424
- const resolver = new TokenResolver(tokens);
2750
+ const resolver = new TokenResolver2(tokens);
2425
2751
  const resolvedValue = resolver.resolve(tokenPath);
2426
2752
  const useJson = opts.format === "json" || opts.format !== "text" && !isTTY2();
2427
2753
  if (useJson) {
@@ -2447,7 +2773,7 @@ function registerList2(tokensCmd) {
2447
2773
  try {
2448
2774
  const filePath = resolveTokenFilePath(opts.file);
2449
2775
  const { tokens } = loadTokens(filePath);
2450
- const resolver = new TokenResolver(tokens);
2776
+ const resolver = new TokenResolver2(tokens);
2451
2777
  const filtered = resolver.list(opts.type, category);
2452
2778
  const useJson = opts.format === "json" || opts.format !== "table" && !isTTY2();
2453
2779
  if (useJson) {
@@ -2477,7 +2803,7 @@ function registerSearch(tokensCmd) {
2477
2803
  try {
2478
2804
  const filePath = resolveTokenFilePath(opts.file);
2479
2805
  const { tokens } = loadTokens(filePath);
2480
- const resolver = new TokenResolver(tokens);
2806
+ const resolver = new TokenResolver2(tokens);
2481
2807
  const useJson = opts.format === "json" || opts.format !== "table" && !isTTY2();
2482
2808
  const typesToSearch = opts.type ? [opts.type] : [
2483
2809
  "color",
@@ -2560,7 +2886,7 @@ function registerResolve(tokensCmd) {
2560
2886
  const filePath = resolveTokenFilePath(opts.file);
2561
2887
  const absFilePath = filePath;
2562
2888
  const { tokens, rawFile } = loadTokens(absFilePath);
2563
- const resolver = new TokenResolver(tokens);
2889
+ const resolver = new TokenResolver2(tokens);
2564
2890
  resolver.resolve(tokenPath);
2565
2891
  const chain = buildResolutionChain(tokenPath, rawFile.tokens);
2566
2892
  const useJson = opts.format === "json" || opts.format !== "text" && !isTTY2();
@@ -2595,7 +2921,7 @@ function registerValidate(tokensCmd) {
2595
2921
  ).option("--file <path>", "Path to token file (overrides config)").option("--format <fmt>", "Output format: json or text (default: auto-detect)").action((opts) => {
2596
2922
  try {
2597
2923
  const filePath = resolveTokenFilePath(opts.file);
2598
- if (!existsSync5(filePath)) {
2924
+ if (!existsSync6(filePath)) {
2599
2925
  throw new Error(
2600
2926
  `Token file not found at ${filePath}.
2601
2927
  Create a reactscope.tokens.json file or use --file to specify a path.`
@@ -2769,6 +3095,10 @@ function createProgram(options = {}) {
2769
3095
  program2.addCommand(createTokensCommand());
2770
3096
  program2.addCommand(createInstrumentCommand());
2771
3097
  program2.addCommand(createInitCommand());
3098
+ const existingReportCmd = program2.commands.find((c) => c.name() === "report");
3099
+ if (existingReportCmd !== void 0) {
3100
+ registerBaselineSubCommand(existingReportCmd);
3101
+ }
2772
3102
  return program2;
2773
3103
  }
2774
3104