@agent-scope/cli 1.18.0 → 1.18.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/cli.js CHANGED
@@ -1,7 +1,7 @@
1
- #!/usr/bin/env node
1
+ #!/usr/bin/env bun
2
2
 
3
3
  // src/program.ts
4
- import { readFileSync as readFileSync11 } from "fs";
4
+ import { readFileSync as readFileSync12 } from "fs";
5
5
  import { generateTest, loadTrace } from "@agent-scope/playwright";
6
6
  import { Command as Command10 } from "commander";
7
7
 
@@ -141,6 +141,21 @@ import { createElement } from "react";
141
141
  // Suppress "React must be in scope" warnings from old JSX (we use automatic)
142
142
  banner: {
143
143
  js: "/* @agent-scope/cli component harness */"
144
+ },
145
+ // CSS imports (e.g. `import './styles.css'`) are handled at the page level via
146
+ // globalCSS injection. Tell esbuild to treat CSS files as empty modules so
147
+ // components that import CSS directly (e.g. App.tsx) don't error during bundling.
148
+ loader: {
149
+ ".css": "empty",
150
+ ".svg": "dataurl",
151
+ ".png": "dataurl",
152
+ ".jpg": "dataurl",
153
+ ".jpeg": "dataurl",
154
+ ".gif": "dataurl",
155
+ ".webp": "dataurl",
156
+ ".ttf": "dataurl",
157
+ ".woff": "dataurl",
158
+ ".woff2": "dataurl"
144
159
  }
145
160
  });
146
161
  if (result.errors.length > 0) {
@@ -553,6 +568,57 @@ async function getCompiledCssForClasses(cwd, classes) {
553
568
  if (deduped.length === 0) return null;
554
569
  return build3(deduped);
555
570
  }
571
+ async function compileGlobalCssFile(cssFilePath, cwd) {
572
+ const { existsSync: existsSync15, readFileSync: readFileSync13 } = await import("fs");
573
+ const { createRequire: createRequire3 } = await import("module");
574
+ if (!existsSync15(cssFilePath)) return null;
575
+ const raw = readFileSync13(cssFilePath, "utf-8");
576
+ const needsCompile = /@tailwind|@import\s+['"]tailwindcss/.test(raw);
577
+ if (!needsCompile) {
578
+ return raw;
579
+ }
580
+ try {
581
+ const require2 = createRequire3(resolve(cwd, "package.json"));
582
+ let postcss;
583
+ let twPlugin;
584
+ try {
585
+ postcss = require2("postcss");
586
+ twPlugin = require2("tailwindcss");
587
+ } catch {
588
+ return raw;
589
+ }
590
+ let autoprefixerPlugin;
591
+ try {
592
+ autoprefixerPlugin = require2("autoprefixer");
593
+ } catch {
594
+ autoprefixerPlugin = null;
595
+ }
596
+ const plugins = autoprefixerPlugin ? [twPlugin, autoprefixerPlugin] : [twPlugin];
597
+ const result = await postcss(plugins).process(raw, {
598
+ from: cssFilePath,
599
+ to: cssFilePath
600
+ });
601
+ return result.css;
602
+ } catch (err) {
603
+ process.stderr.write(
604
+ `[scope/render] Warning: CSS compilation failed for ${cssFilePath}: ${err instanceof Error ? err.message : String(err)}
605
+ `
606
+ );
607
+ return raw;
608
+ }
609
+ }
610
+ async function loadGlobalCss(globalCssFiles, cwd) {
611
+ if (globalCssFiles.length === 0) return null;
612
+ const parts = [];
613
+ for (const relPath of globalCssFiles) {
614
+ const absPath = resolve(cwd, relPath);
615
+ const css = await compileGlobalCssFile(absPath, cwd);
616
+ if (css !== null && css.trim().length > 0) {
617
+ parts.push(css);
618
+ }
619
+ }
620
+ return parts.length > 0 ? parts.join("\n") : null;
621
+ }
556
622
 
557
623
  // src/ci/commands.ts
558
624
  var CI_EXIT = {
@@ -1065,6 +1131,20 @@ function detectComponentPatterns(rootDir, typescript) {
1065
1131
  }
1066
1132
  return unique;
1067
1133
  }
1134
+ var GLOBAL_CSS_CANDIDATES = [
1135
+ "src/styles.css",
1136
+ "src/index.css",
1137
+ "src/global.css",
1138
+ "src/globals.css",
1139
+ "src/app.css",
1140
+ "src/main.css",
1141
+ "styles/globals.css",
1142
+ "styles/global.css",
1143
+ "styles/index.css"
1144
+ ];
1145
+ function detectGlobalCSSFiles(rootDir) {
1146
+ return GLOBAL_CSS_CANDIDATES.filter((rel) => existsSync3(join(rootDir, rel)));
1147
+ }
1068
1148
  var TAILWIND_STEMS = ["tailwind.config"];
1069
1149
  var CSS_EXTS = [".css", ".scss", ".sass", ".less"];
1070
1150
  var THEME_SUFFIXES = [".theme.ts", ".theme.js", ".theme.tsx"];
@@ -1132,13 +1212,15 @@ function detectProject(rootDir) {
1132
1212
  const packageManager = detectPackageManager(rootDir);
1133
1213
  const componentPatterns = detectComponentPatterns(rootDir, typescript);
1134
1214
  const tokenSources = detectTokenSources(rootDir);
1215
+ const globalCSSFiles = detectGlobalCSSFiles(rootDir);
1135
1216
  return {
1136
1217
  framework,
1137
1218
  typescript,
1138
1219
  tsconfigPath,
1139
1220
  componentPatterns,
1140
1221
  tokenSources,
1141
- packageManager
1222
+ packageManager,
1223
+ globalCSSFiles
1142
1224
  };
1143
1225
  }
1144
1226
 
@@ -1150,7 +1232,7 @@ function buildDefaultConfig(detected, tokenFile, outputDir) {
1150
1232
  components: {
1151
1233
  include,
1152
1234
  exclude: ["**/*.test.tsx", "**/*.stories.tsx"],
1153
- wrappers: { providers: [], globalCSS: [] }
1235
+ wrappers: { providers: [], globalCSS: detected.globalCSSFiles ?? [] }
1154
1236
  },
1155
1237
  render: {
1156
1238
  viewport: { default: { width: 1280, height: 800 } },
@@ -1208,18 +1290,118 @@ function ensureGitignoreEntry(rootDir, entry) {
1208
1290
  `);
1209
1291
  }
1210
1292
  }
1293
+ function extractTailwindTokens(tokenSources) {
1294
+ const tailwindSource = tokenSources.find((s) => s.kind === "tailwind-config");
1295
+ if (!tailwindSource) return null;
1296
+ try {
1297
+ let parseBlock2 = function(block) {
1298
+ const result = {};
1299
+ const lineRe = /['"]?(\w[\w.-]*|\d+)['"]?\s*:\s*['"]?(#[0-9a-fA-F]{3,8}|\d+(?:px|rem|em|%)|[\w-]+(?:\/[\w]+)?)['"]?/g;
1300
+ for (const m of block.matchAll(lineRe)) {
1301
+ if (m[1] !== void 0 && m[2] !== void 0) {
1302
+ result[m[1]] = m[2];
1303
+ }
1304
+ }
1305
+ return result;
1306
+ };
1307
+ var parseBlock = parseBlock2;
1308
+ const raw = readFileSync4(tailwindSource.path, "utf-8");
1309
+ const tokens = {};
1310
+ const colorsKeyIdx = raw.indexOf("colors:");
1311
+ if (colorsKeyIdx !== -1) {
1312
+ const colorsBraceStart = raw.indexOf("{", colorsKeyIdx);
1313
+ if (colorsBraceStart !== -1) {
1314
+ let colorDepth = 0;
1315
+ let colorsBraceEnd = -1;
1316
+ for (let ci = colorsBraceStart; ci < raw.length; ci++) {
1317
+ if (raw[ci] === "{") colorDepth++;
1318
+ else if (raw[ci] === "}") {
1319
+ colorDepth--;
1320
+ if (colorDepth === 0) {
1321
+ colorsBraceEnd = ci;
1322
+ break;
1323
+ }
1324
+ }
1325
+ }
1326
+ if (colorsBraceEnd > colorsBraceStart) {
1327
+ const colorSection = raw.slice(colorsBraceStart + 1, colorsBraceEnd);
1328
+ const scaleRe = /(\w+)\s*:\s*\{([^}]+)\}/g;
1329
+ const colorTokens = {};
1330
+ for (const sm of colorSection.matchAll(scaleRe)) {
1331
+ if (sm[1] === void 0 || sm[2] === void 0) continue;
1332
+ const scaleName = sm[1];
1333
+ const scaleValues = parseBlock2(sm[2]);
1334
+ if (Object.keys(scaleValues).length > 0) {
1335
+ const scaleTokens = {};
1336
+ for (const [step, hex] of Object.entries(scaleValues)) {
1337
+ scaleTokens[step] = { value: hex, type: "color" };
1338
+ }
1339
+ colorTokens[scaleName] = scaleTokens;
1340
+ }
1341
+ }
1342
+ if (Object.keys(colorTokens).length > 0) {
1343
+ tokens["color"] = colorTokens;
1344
+ }
1345
+ }
1346
+ }
1347
+ }
1348
+ const spacingMatch = raw.match(/spacing\s*:\s*\{([\s\S]*?)\n\s*\}/);
1349
+ if (spacingMatch?.[1] !== void 0) {
1350
+ const spacingValues = parseBlock2(spacingMatch[1]);
1351
+ if (Object.keys(spacingValues).length > 0) {
1352
+ const spacingTokens = {};
1353
+ for (const [key, val] of Object.entries(spacingValues)) {
1354
+ spacingTokens[key] = { value: val, type: "dimension" };
1355
+ }
1356
+ tokens["spacing"] = spacingTokens;
1357
+ }
1358
+ }
1359
+ const fontFamilyMatch = raw.match(/fontFamily\s*:\s*\{([\s\S]*?)\n\s*\}/);
1360
+ if (fontFamilyMatch?.[1] !== void 0) {
1361
+ const fontFamilyRe = /(\w+)\s*:\s*\[\s*['"]([^'"]+)['"]/g;
1362
+ const fontTokens = {};
1363
+ for (const fm of fontFamilyMatch[1].matchAll(fontFamilyRe)) {
1364
+ if (fm[1] !== void 0 && fm[2] !== void 0) {
1365
+ fontTokens[fm[1]] = { value: fm[2], type: "fontFamily" };
1366
+ }
1367
+ }
1368
+ if (Object.keys(fontTokens).length > 0) {
1369
+ tokens["font"] = fontTokens;
1370
+ }
1371
+ }
1372
+ const borderRadiusMatch = raw.match(/borderRadius\s*:\s*\{([\s\S]*?)\n\s*\}/);
1373
+ if (borderRadiusMatch?.[1] !== void 0) {
1374
+ const radiusValues = parseBlock2(borderRadiusMatch[1]);
1375
+ if (Object.keys(radiusValues).length > 0) {
1376
+ const radiusTokens = {};
1377
+ for (const [key, val] of Object.entries(radiusValues)) {
1378
+ radiusTokens[key] = { value: val, type: "dimension" };
1379
+ }
1380
+ tokens["radius"] = radiusTokens;
1381
+ }
1382
+ }
1383
+ return Object.keys(tokens).length > 0 ? tokens : null;
1384
+ } catch {
1385
+ return null;
1386
+ }
1387
+ }
1211
1388
  function scaffoldConfig(rootDir, config) {
1212
1389
  const path = join2(rootDir, "reactscope.config.json");
1213
1390
  writeFileSync3(path, `${JSON.stringify(config, null, 2)}
1214
1391
  `);
1215
1392
  return path;
1216
1393
  }
1217
- function scaffoldTokenFile(rootDir, tokenFile) {
1394
+ function scaffoldTokenFile(rootDir, tokenFile, extractedTokens) {
1218
1395
  const path = join2(rootDir, tokenFile);
1219
1396
  if (!existsSync4(path)) {
1220
1397
  const stub = {
1221
1398
  $schema: "https://raw.githubusercontent.com/FlatFilers/Scope/main/packages/tokens/schema.json",
1222
- tokens: {}
1399
+ version: "1.0.0",
1400
+ meta: {
1401
+ name: "Design Tokens",
1402
+ lastUpdated: (/* @__PURE__ */ new Date()).toISOString().split("T")[0]
1403
+ },
1404
+ tokens: extractedTokens ?? {}
1223
1405
  };
1224
1406
  writeFileSync3(path, `${JSON.stringify(stub, null, 2)}
1225
1407
  `);
@@ -1297,7 +1479,13 @@ async function runInit(options) {
1297
1479
  }
1298
1480
  const cfgPath = scaffoldConfig(rootDir, config);
1299
1481
  created.push(cfgPath);
1300
- const tokPath = scaffoldTokenFile(rootDir, config.tokens.file);
1482
+ const extractedTokens = extractTailwindTokens(detected.tokenSources);
1483
+ if (extractedTokens !== null) {
1484
+ const tokenGroupCount = Object.keys(extractedTokens).length;
1485
+ process.stdout.write(` Extracted ${tokenGroupCount} token group(s) from Tailwind config
1486
+ `);
1487
+ }
1488
+ const tokPath = scaffoldTokenFile(rootDir, config.tokens.file, extractedTokens ?? void 0);
1301
1489
  created.push(tokPath);
1302
1490
  const outDirPath = scaffoldOutputDir(rootDir, config.output.dir);
1303
1491
  created.push(outDirPath);
@@ -1409,7 +1597,10 @@ Available: ${available}${hint}`
1409
1597
  });
1410
1598
  }
1411
1599
  function registerQuery(manifestCmd) {
1412
- 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(
1600
+ 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(
1601
+ "--has-prop <spec>",
1602
+ "Find components with a prop matching name or name:type (e.g. 'loading' or 'variant:union')"
1603
+ ).option("--composed-by <name>", "Find components that compose the named component").option("--format <fmt>", "Output format: json or table (default: auto-detect)").option("--manifest <path>", "Path to manifest.json", MANIFEST_PATH).action(
1413
1604
  (opts) => {
1414
1605
  try {
1415
1606
  const manifest = loadManifest(opts.manifest);
@@ -1420,9 +1611,11 @@ function registerQuery(manifestCmd) {
1420
1611
  if (opts.complexity !== void 0) queryParts.push(`complexity=${opts.complexity}`);
1421
1612
  if (opts.sideEffects) queryParts.push("side-effects");
1422
1613
  if (opts.hasFetch) queryParts.push("has-fetch");
1614
+ if (opts.hasProp !== void 0) queryParts.push(`has-prop=${opts.hasProp}`);
1615
+ if (opts.composedBy !== void 0) queryParts.push(`composed-by=${opts.composedBy}`);
1423
1616
  if (queryParts.length === 0) {
1424
1617
  process.stderr.write(
1425
- "No query flags specified. Use --context, --hook, --complexity, --side-effects, or --has-fetch.\n"
1618
+ "No query flags specified. Use --context, --hook, --complexity, --side-effects, --has-fetch, --has-prop, or --composed-by.\n"
1426
1619
  );
1427
1620
  process.exit(1);
1428
1621
  }
@@ -1449,6 +1642,27 @@ function registerQuery(manifestCmd) {
1449
1642
  if (opts.hasFetch) {
1450
1643
  entries = entries.filter(([, d]) => d.sideEffects.fetches.length > 0);
1451
1644
  }
1645
+ if (opts.hasProp !== void 0) {
1646
+ const spec = opts.hasProp;
1647
+ const colonIdx = spec.indexOf(":");
1648
+ const propName = colonIdx >= 0 ? spec.slice(0, colonIdx) : spec;
1649
+ const propType = colonIdx >= 0 ? spec.slice(colonIdx + 1) : void 0;
1650
+ entries = entries.filter(([, d]) => {
1651
+ const props = d.props;
1652
+ if (!props || !(propName in props)) return false;
1653
+ if (propType !== void 0) {
1654
+ return props[propName]?.type === propType;
1655
+ }
1656
+ return true;
1657
+ });
1658
+ }
1659
+ if (opts.composedBy !== void 0) {
1660
+ const targetName = opts.composedBy;
1661
+ entries = entries.filter(([, d]) => {
1662
+ const composedBy = d.composedBy;
1663
+ return composedBy !== void 0 && composedBy.includes(targetName);
1664
+ });
1665
+ }
1452
1666
  const rows = entries.map(([name, d]) => ({
1453
1667
  name,
1454
1668
  file: d.filePath,
@@ -3001,7 +3215,7 @@ function createInstrumentCommand() {
3001
3215
  }
3002
3216
 
3003
3217
  // src/render-commands.ts
3004
- import { mkdirSync as mkdirSync4, writeFileSync as writeFileSync5 } from "fs";
3218
+ import { existsSync as existsSync7, mkdirSync as mkdirSync4, readFileSync as readFileSync6, writeFileSync as writeFileSync5 } from "fs";
3005
3219
  import { resolve as resolve9 } from "path";
3006
3220
  import {
3007
3221
  ALL_CONTEXT_IDS,
@@ -3137,6 +3351,17 @@ ${msg}`);
3137
3351
  }
3138
3352
 
3139
3353
  // src/render-commands.ts
3354
+ function loadGlobalCssFilesFromConfig(cwd) {
3355
+ const configPath = resolve9(cwd, "reactscope.config.json");
3356
+ if (!existsSync7(configPath)) return [];
3357
+ try {
3358
+ const raw = readFileSync6(configPath, "utf-8");
3359
+ const cfg = JSON.parse(raw);
3360
+ return cfg.components?.wrappers?.globalCSS ?? [];
3361
+ } catch {
3362
+ return [];
3363
+ }
3364
+ }
3140
3365
  var MANIFEST_PATH6 = ".reactscope/manifest.json";
3141
3366
  var DEFAULT_OUTPUT_DIR = ".reactscope/renders";
3142
3367
  var _pool3 = null;
@@ -3157,7 +3382,7 @@ async function shutdownPool3() {
3157
3382
  _pool3 = null;
3158
3383
  }
3159
3384
  }
3160
- function buildRenderer(filePath, componentName, viewportWidth, viewportHeight, wrapperScript) {
3385
+ function buildRenderer(filePath, componentName, viewportWidth, viewportHeight, globalCssFiles = [], projectCwd = process.cwd(), wrapperScript) {
3161
3386
  const satori = new SatoriRenderer({
3162
3387
  defaultViewport: { width: viewportWidth, height: viewportHeight }
3163
3388
  });
@@ -3166,13 +3391,13 @@ function buildRenderer(filePath, componentName, viewportWidth, viewportHeight, w
3166
3391
  async renderCell(props, _complexityClass) {
3167
3392
  const startMs = performance.now();
3168
3393
  const pool = await getPool3(viewportWidth, viewportHeight);
3394
+ const projectCss = await loadGlobalCss(globalCssFiles, projectCwd);
3169
3395
  const htmlHarness = await buildComponentHarness(
3170
3396
  filePath,
3171
3397
  componentName,
3172
3398
  props,
3173
3399
  viewportWidth,
3174
- void 0,
3175
- // projectCss (handled separately)
3400
+ projectCss ?? void 0,
3176
3401
  wrapperScript
3177
3402
  );
3178
3403
  const slot = await pool.acquire();
@@ -3202,9 +3427,9 @@ function buildRenderer(filePath, componentName, viewportWidth, viewportHeight, w
3202
3427
  });
3203
3428
  return [...set];
3204
3429
  });
3205
- const projectCss = await getCompiledCssForClasses(rootDir, classes);
3206
- if (projectCss != null && projectCss.length > 0) {
3207
- await page.addStyleTag({ content: projectCss });
3430
+ const projectCss2 = await getCompiledCssForClasses(rootDir, classes);
3431
+ if (projectCss2 != null && projectCss2.length > 0) {
3432
+ await page.addStyleTag({ content: projectCss2 });
3208
3433
  }
3209
3434
  const renderTimeMs = performance.now() - startMs;
3210
3435
  const rootLocator = page.locator("[data-reactscope-root]");
@@ -3306,26 +3531,59 @@ function registerRenderSingle(renderCmd) {
3306
3531
  Available: ${available}`
3307
3532
  );
3308
3533
  }
3534
+ let props = {};
3535
+ if (opts.props !== void 0) {
3536
+ try {
3537
+ props = JSON.parse(opts.props);
3538
+ } catch {
3539
+ throw new Error(`Invalid props JSON: ${opts.props}`);
3540
+ }
3541
+ }
3542
+ if (descriptor.props !== void 0) {
3543
+ const propDefs = descriptor.props;
3544
+ for (const [propName, propDef] of Object.entries(propDefs)) {
3545
+ if (propName in props) continue;
3546
+ if (!propDef.required && propDef.default !== void 0) continue;
3547
+ if (propDef.type === "node" || propDef.type === "string") {
3548
+ props[propName] = propName === "children" ? componentName : propName;
3549
+ } else if (propDef.type === "union" && propDef.values && propDef.values.length > 0) {
3550
+ props[propName] = propDef.values[0];
3551
+ } else if (propDef.type === "boolean") {
3552
+ props[propName] = false;
3553
+ } else if (propDef.type === "number") {
3554
+ props[propName] = 0;
3555
+ }
3556
+ }
3557
+ }
3309
3558
  const { width, height } = parseViewport(opts.viewport);
3310
3559
  const rootDir = process.cwd();
3311
3560
  const filePath = resolve9(rootDir, descriptor.filePath);
3312
3561
  const scopeData = await loadScopeFileForComponent(filePath);
3313
3562
  const wrapperScript = scopeData?.hasWrapper === true ? await buildWrapperScript(scopeData.filePath) : void 0;
3314
3563
  const scenarios = buildScenarioMap(opts, scopeData);
3315
- const renderer = buildRenderer(filePath, componentName, width, height, wrapperScript);
3564
+ const globalCssFiles = loadGlobalCssFilesFromConfig(rootDir);
3565
+ const renderer = buildRenderer(
3566
+ filePath,
3567
+ componentName,
3568
+ width,
3569
+ height,
3570
+ globalCssFiles,
3571
+ rootDir,
3572
+ wrapperScript
3573
+ );
3316
3574
  process.stderr.write(
3317
3575
  `Rendering ${componentName} [${descriptor.complexityClass}] at ${width}\xD7${height}\u2026
3318
3576
  `
3319
3577
  );
3320
3578
  const fmt2 = resolveSingleFormat(opts.format);
3321
3579
  let anyFailed = false;
3322
- for (const [scenarioName, props] of Object.entries(scenarios)) {
3580
+ for (const [scenarioName, props2] of Object.entries(scenarios)) {
3323
3581
  const isNamed = scenarioName !== "__default__";
3324
3582
  const label = isNamed ? `${componentName}:${scenarioName}` : componentName;
3325
3583
  const outcome = await safeRender2(
3326
- () => renderer.renderCell(props, descriptor.complexityClass),
3584
+ () => renderer.renderCell(props2, descriptor.complexityClass),
3327
3585
  {
3328
- props,
3586
+ props: props2,
3329
3587
  sourceLocation: {
3330
3588
  file: descriptor.filePath,
3331
3589
  line: descriptor.loc.start,
@@ -3354,7 +3612,7 @@ Available: ${available}`
3354
3612
  `
3355
3613
  );
3356
3614
  } else if (fmt2 === "json") {
3357
- const json = formatRenderJson(label, props, result);
3615
+ const json = formatRenderJson(label, props2, result);
3358
3616
  process.stdout.write(`${JSON.stringify(json, null, 2)}
3359
3617
  `);
3360
3618
  } else {
@@ -3381,7 +3639,10 @@ Available: ${available}`
3381
3639
  );
3382
3640
  }
3383
3641
  function registerRenderMatrix(renderCmd) {
3384
- 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(
3642
+ renderCmd.command("matrix <component>").description("Render a component across a matrix of prop axes").option(
3643
+ "--axes <spec>",
3644
+ `Axis definitions: key:v1,v2 space-separated OR JSON object e.g. 'variant:primary,ghost size:sm,lg' or '{"variant":["primary","ghost"],"size":["sm","lg"]}'`
3645
+ ).option(
3385
3646
  "--contexts <ids>",
3386
3647
  "Composition context IDs, comma-separated (e.g. centered,rtl,sidebar)"
3387
3648
  ).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_PATH6).action(
@@ -3400,21 +3661,47 @@ Available: ${available}`
3400
3661
  const { width, height } = { width: 375, height: 812 };
3401
3662
  const rootDir = process.cwd();
3402
3663
  const filePath = resolve9(rootDir, descriptor.filePath);
3403
- const renderer = buildRenderer(filePath, componentName, width, height);
3664
+ const matrixCssFiles = loadGlobalCssFilesFromConfig(rootDir);
3665
+ const renderer = buildRenderer(
3666
+ filePath,
3667
+ componentName,
3668
+ width,
3669
+ height,
3670
+ matrixCssFiles,
3671
+ rootDir
3672
+ );
3404
3673
  const axes = [];
3405
3674
  if (opts.axes !== void 0) {
3406
- const axisSpecs = opts.axes.trim().split(/\s+/);
3407
- for (const spec of axisSpecs) {
3408
- const colonIdx = spec.indexOf(":");
3409
- if (colonIdx < 0) {
3410
- throw new Error(`Invalid axis spec "${spec}". Expected format: name:val1,val2,...`);
3675
+ const axesRaw = opts.axes.trim();
3676
+ if (axesRaw.startsWith("{")) {
3677
+ let parsed;
3678
+ try {
3679
+ parsed = JSON.parse(axesRaw);
3680
+ } catch {
3681
+ throw new Error(`Invalid JSON in --axes: ${axesRaw}`);
3411
3682
  }
3412
- const name = spec.slice(0, colonIdx);
3413
- const values = spec.slice(colonIdx + 1).split(",").map((v) => v.trim());
3414
- if (name.length === 0 || values.length === 0) {
3415
- throw new Error(`Invalid axis spec "${spec}"`);
3683
+ for (const [name, vals] of Object.entries(parsed)) {
3684
+ if (!Array.isArray(vals)) {
3685
+ throw new Error(`Axis "${name}" must be an array of values in JSON format`);
3686
+ }
3687
+ axes.push({ name, values: vals.map(String) });
3688
+ }
3689
+ } else {
3690
+ const axisSpecs = axesRaw.split(/\s+/);
3691
+ for (const spec of axisSpecs) {
3692
+ const colonIdx = spec.indexOf(":");
3693
+ if (colonIdx < 0) {
3694
+ throw new Error(
3695
+ `Invalid axis spec "${spec}". Expected format: name:val1,val2,...`
3696
+ );
3697
+ }
3698
+ const name = spec.slice(0, colonIdx);
3699
+ const values = spec.slice(colonIdx + 1).split(",").map((v) => v.trim());
3700
+ if (name.length === 0 || values.length === 0) {
3701
+ throw new Error(`Invalid axis spec "${spec}"`);
3702
+ }
3703
+ axes.push({ name, values });
3416
3704
  }
3417
- axes.push({ name, values });
3418
3705
  }
3419
3706
  }
3420
3707
  if (opts.contexts !== void 0) {
@@ -3533,7 +3820,8 @@ function registerRenderAll(renderCmd) {
3533
3820
  const descriptor = manifest.components[name];
3534
3821
  if (descriptor === void 0) return;
3535
3822
  const filePath = resolve9(rootDir, descriptor.filePath);
3536
- const renderer = buildRenderer(filePath, name, 375, 812);
3823
+ const allCssFiles = loadGlobalCssFilesFromConfig(process.cwd());
3824
+ const renderer = buildRenderer(filePath, name, 375, 812, allCssFiles, process.cwd());
3537
3825
  const outcome = await safeRender2(
3538
3826
  () => renderer.renderCell({}, descriptor.complexityClass),
3539
3827
  {
@@ -3648,7 +3936,7 @@ function createRenderCommand() {
3648
3936
  }
3649
3937
 
3650
3938
  // src/report/baseline.ts
3651
- import { existsSync as existsSync7, mkdirSync as mkdirSync5, rmSync as rmSync2, writeFileSync as writeFileSync6 } from "fs";
3939
+ import { existsSync as existsSync8, mkdirSync as mkdirSync5, rmSync as rmSync2, writeFileSync as writeFileSync6 } from "fs";
3652
3940
  import { resolve as resolve10 } from "path";
3653
3941
  import { generateManifest as generateManifest3 } from "@agent-scope/manifest";
3654
3942
  import { BrowserPool as BrowserPool4, safeRender as safeRender3 } from "@agent-scope/render";
@@ -3800,18 +4088,18 @@ async function runBaseline(options = {}) {
3800
4088
  const rootDir = process.cwd();
3801
4089
  const baselineDir = resolve10(rootDir, outputDir);
3802
4090
  const rendersDir = resolve10(baselineDir, "renders");
3803
- if (existsSync7(baselineDir)) {
4091
+ if (existsSync8(baselineDir)) {
3804
4092
  rmSync2(baselineDir, { recursive: true, force: true });
3805
4093
  }
3806
4094
  mkdirSync5(rendersDir, { recursive: true });
3807
4095
  let manifest;
3808
4096
  if (manifestPath !== void 0) {
3809
- const { readFileSync: readFileSync12 } = await import("fs");
4097
+ const { readFileSync: readFileSync13 } = await import("fs");
3810
4098
  const absPath = resolve10(rootDir, manifestPath);
3811
- if (!existsSync7(absPath)) {
4099
+ if (!existsSync8(absPath)) {
3812
4100
  throw new Error(`Manifest not found at ${absPath}.`);
3813
4101
  }
3814
- manifest = JSON.parse(readFileSync12(absPath, "utf-8"));
4102
+ manifest = JSON.parse(readFileSync13(absPath, "utf-8"));
3815
4103
  process.stderr.write(`Loaded manifest from ${manifestPath}
3816
4104
  `);
3817
4105
  } else {
@@ -3974,7 +4262,7 @@ function registerBaselineSubCommand(reportCmd) {
3974
4262
  }
3975
4263
 
3976
4264
  // src/report/diff.ts
3977
- import { existsSync as existsSync8, readFileSync as readFileSync6, writeFileSync as writeFileSync7 } from "fs";
4265
+ import { existsSync as existsSync9, readFileSync as readFileSync7, writeFileSync as writeFileSync7 } from "fs";
3978
4266
  import { resolve as resolve11 } from "path";
3979
4267
  import { generateManifest as generateManifest4 } from "@agent-scope/manifest";
3980
4268
  import { BrowserPool as BrowserPool5, safeRender as safeRender4 } from "@agent-scope/render";
@@ -3982,14 +4270,14 @@ import { ComplianceEngine as ComplianceEngine3, TokenResolver as TokenResolver3
3982
4270
  var DEFAULT_BASELINE_DIR2 = ".reactscope/baseline";
3983
4271
  function loadBaselineCompliance(baselineDir) {
3984
4272
  const compliancePath = resolve11(baselineDir, "compliance.json");
3985
- if (!existsSync8(compliancePath)) return null;
3986
- const raw = JSON.parse(readFileSync6(compliancePath, "utf-8"));
4273
+ if (!existsSync9(compliancePath)) return null;
4274
+ const raw = JSON.parse(readFileSync7(compliancePath, "utf-8"));
3987
4275
  return raw;
3988
4276
  }
3989
4277
  function loadBaselineRenderJson2(baselineDir, componentName) {
3990
4278
  const jsonPath = resolve11(baselineDir, "renders", `${componentName}.json`);
3991
- if (!existsSync8(jsonPath)) return null;
3992
- return JSON.parse(readFileSync6(jsonPath, "utf-8"));
4279
+ if (!existsSync9(jsonPath)) return null;
4280
+ return JSON.parse(readFileSync7(jsonPath, "utf-8"));
3993
4281
  }
3994
4282
  var _pool5 = null;
3995
4283
  async function getPool5(viewportWidth, viewportHeight) {
@@ -4157,18 +4445,18 @@ async function runDiff(options = {}) {
4157
4445
  const startTime = performance.now();
4158
4446
  const rootDir = process.cwd();
4159
4447
  const baselineDir = resolve11(rootDir, baselineDirRaw);
4160
- if (!existsSync8(baselineDir)) {
4448
+ if (!existsSync9(baselineDir)) {
4161
4449
  throw new Error(
4162
4450
  `Baseline directory not found at "${baselineDir}". Run \`scope report baseline\` first to create a baseline snapshot.`
4163
4451
  );
4164
4452
  }
4165
4453
  const baselineManifestPath = resolve11(baselineDir, "manifest.json");
4166
- if (!existsSync8(baselineManifestPath)) {
4454
+ if (!existsSync9(baselineManifestPath)) {
4167
4455
  throw new Error(
4168
4456
  `Baseline manifest.json not found at "${baselineManifestPath}". The baseline directory may be incomplete \u2014 re-run \`scope report baseline\`.`
4169
4457
  );
4170
4458
  }
4171
- const baselineManifest = JSON.parse(readFileSync6(baselineManifestPath, "utf-8"));
4459
+ const baselineManifest = JSON.parse(readFileSync7(baselineManifestPath, "utf-8"));
4172
4460
  const baselineCompliance = loadBaselineCompliance(baselineDir);
4173
4461
  const baselineComponentNames = new Set(Object.keys(baselineManifest.components));
4174
4462
  process.stderr.write(
@@ -4178,10 +4466,10 @@ async function runDiff(options = {}) {
4178
4466
  let currentManifest;
4179
4467
  if (manifestPath !== void 0) {
4180
4468
  const absPath = resolve11(rootDir, manifestPath);
4181
- if (!existsSync8(absPath)) {
4469
+ if (!existsSync9(absPath)) {
4182
4470
  throw new Error(`Manifest not found at "${absPath}".`);
4183
4471
  }
4184
- currentManifest = JSON.parse(readFileSync6(absPath, "utf-8"));
4472
+ currentManifest = JSON.parse(readFileSync7(absPath, "utf-8"));
4185
4473
  process.stderr.write(`Loaded manifest from ${manifestPath}
4186
4474
  `);
4187
4475
  } else {
@@ -4454,7 +4742,7 @@ function registerDiffSubCommand(reportCmd) {
4454
4742
  }
4455
4743
 
4456
4744
  // src/report/pr-comment.ts
4457
- import { existsSync as existsSync9, readFileSync as readFileSync7, writeFileSync as writeFileSync8 } from "fs";
4745
+ import { existsSync as existsSync10, readFileSync as readFileSync8, writeFileSync as writeFileSync8 } from "fs";
4458
4746
  import { resolve as resolve12 } from "path";
4459
4747
  var STATUS_BADGE = {
4460
4748
  added: "\u2705 added",
@@ -4539,12 +4827,12 @@ function formatPrComment(diff) {
4539
4827
  }
4540
4828
  function loadDiffResult(filePath) {
4541
4829
  const abs = resolve12(filePath);
4542
- if (!existsSync9(abs)) {
4830
+ if (!existsSync10(abs)) {
4543
4831
  throw new Error(`DiffResult file not found: ${abs}`);
4544
4832
  }
4545
4833
  let raw;
4546
4834
  try {
4547
- raw = readFileSync7(abs, "utf-8");
4835
+ raw = readFileSync8(abs, "utf-8");
4548
4836
  } catch (err) {
4549
4837
  throw new Error(
4550
4838
  `Failed to read DiffResult file: ${err instanceof Error ? err.message : String(err)}`
@@ -4866,7 +5154,7 @@ function buildStructuredReport(report) {
4866
5154
  }
4867
5155
 
4868
5156
  // src/site-commands.ts
4869
- import { createReadStream, existsSync as existsSync10, statSync } from "fs";
5157
+ import { createReadStream, existsSync as existsSync11, statSync } from "fs";
4870
5158
  import { createServer } from "http";
4871
5159
  import { extname, join as join4, resolve as resolve13 } from "path";
4872
5160
  import { buildSite } from "@agent-scope/site";
@@ -4888,14 +5176,14 @@ function registerBuild(siteCmd) {
4888
5176
  try {
4889
5177
  const inputDir = resolve13(process.cwd(), opts.input);
4890
5178
  const outputDir = resolve13(process.cwd(), opts.output);
4891
- if (!existsSync10(inputDir)) {
5179
+ if (!existsSync11(inputDir)) {
4892
5180
  throw new Error(
4893
5181
  `Input directory not found: ${inputDir}
4894
5182
  Run \`scope manifest generate\` and \`scope render\` first.`
4895
5183
  );
4896
5184
  }
4897
5185
  const manifestPath = join4(inputDir, "manifest.json");
4898
- if (!existsSync10(manifestPath)) {
5186
+ if (!existsSync11(manifestPath)) {
4899
5187
  throw new Error(
4900
5188
  `Manifest not found at ${manifestPath}
4901
5189
  Run \`scope manifest generate\` first.`
@@ -4932,7 +5220,7 @@ function registerServe(siteCmd) {
4932
5220
  throw new Error(`Invalid port: ${opts.port}`);
4933
5221
  }
4934
5222
  const serveDir = resolve13(process.cwd(), opts.dir);
4935
- if (!existsSync10(serveDir)) {
5223
+ if (!existsSync11(serveDir)) {
4936
5224
  throw new Error(
4937
5225
  `Serve directory not found: ${serveDir}
4938
5226
  Run \`scope site build\` first.`
@@ -4947,7 +5235,7 @@ Run \`scope site build\` first.`
4947
5235
  res.end("Forbidden");
4948
5236
  return;
4949
5237
  }
4950
- if (existsSync10(filePath) && statSync(filePath).isFile()) {
5238
+ if (existsSync11(filePath) && statSync(filePath).isFile()) {
4951
5239
  const ext = extname(filePath).toLowerCase();
4952
5240
  const contentType = MIME_TYPES[ext] ?? "application/octet-stream";
4953
5241
  res.writeHead(200, { "Content-Type": contentType });
@@ -4955,7 +5243,7 @@ Run \`scope site build\` first.`
4955
5243
  return;
4956
5244
  }
4957
5245
  const htmlPath = `${filePath}.html`;
4958
- if (existsSync10(htmlPath) && statSync(htmlPath).isFile()) {
5246
+ if (existsSync11(htmlPath) && statSync(htmlPath).isFile()) {
4959
5247
  res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
4960
5248
  createReadStream(htmlPath).pipe(res);
4961
5249
  return;
@@ -4997,7 +5285,7 @@ function createSiteCommand() {
4997
5285
  }
4998
5286
 
4999
5287
  // src/tokens/commands.ts
5000
- import { existsSync as existsSync13, readFileSync as readFileSync10 } from "fs";
5288
+ import { existsSync as existsSync14, readFileSync as readFileSync11 } from "fs";
5001
5289
  import { resolve as resolve17 } from "path";
5002
5290
  import {
5003
5291
  parseTokenFileSync as parseTokenFileSync2,
@@ -5009,7 +5297,7 @@ import {
5009
5297
  import { Command as Command9 } from "commander";
5010
5298
 
5011
5299
  // src/tokens/compliance.ts
5012
- import { existsSync as existsSync11, readFileSync as readFileSync8 } from "fs";
5300
+ import { existsSync as existsSync12, readFileSync as readFileSync9 } from "fs";
5013
5301
  import { resolve as resolve14 } from "path";
5014
5302
  import {
5015
5303
  ComplianceEngine as ComplianceEngine4,
@@ -5018,14 +5306,14 @@ import {
5018
5306
  var DEFAULT_STYLES_PATH = ".reactscope/compliance-styles.json";
5019
5307
  function loadStylesFile(stylesPath) {
5020
5308
  const absPath = resolve14(process.cwd(), stylesPath);
5021
- if (!existsSync11(absPath)) {
5309
+ if (!existsSync12(absPath)) {
5022
5310
  throw new Error(
5023
5311
  `Compliance styles file not found at ${absPath}.
5024
5312
  Run \`scope render all\` first to generate component styles, or use --styles to specify a path.
5025
5313
  Expected format: { "ComponentName": { colors: {}, spacing: {}, typography: {}, borders: {}, shadows: {} } }`
5026
5314
  );
5027
5315
  }
5028
- const raw = readFileSync8(absPath, "utf-8");
5316
+ const raw = readFileSync9(absPath, "utf-8");
5029
5317
  let parsed;
5030
5318
  try {
5031
5319
  parsed = JSON.parse(raw);
@@ -5185,7 +5473,7 @@ function registerCompliance(tokensCmd) {
5185
5473
  }
5186
5474
 
5187
5475
  // src/tokens/export.ts
5188
- import { existsSync as existsSync12, readFileSync as readFileSync9, writeFileSync as writeFileSync9 } from "fs";
5476
+ import { existsSync as existsSync13, readFileSync as readFileSync10, writeFileSync as writeFileSync9 } from "fs";
5189
5477
  import { resolve as resolve15 } from "path";
5190
5478
  import {
5191
5479
  exportTokens,
@@ -5202,9 +5490,9 @@ function resolveTokenFilePath2(fileFlag) {
5202
5490
  return resolve15(process.cwd(), fileFlag);
5203
5491
  }
5204
5492
  const configPath = resolve15(process.cwd(), CONFIG_FILE);
5205
- if (existsSync12(configPath)) {
5493
+ if (existsSync13(configPath)) {
5206
5494
  try {
5207
- const raw = readFileSync9(configPath, "utf-8");
5495
+ const raw = readFileSync10(configPath, "utf-8");
5208
5496
  const config = JSON.parse(raw);
5209
5497
  if (typeof config === "object" && config !== null && "tokens" in config && typeof config.tokens === "object" && config.tokens !== null && typeof config.tokens?.file === "string") {
5210
5498
  const file = config.tokens.file;
@@ -5222,23 +5510,33 @@ function createTokensExportCommand() {
5222
5510
  ).action(
5223
5511
  (opts) => {
5224
5512
  if (!SUPPORTED_FORMATS.includes(opts.format)) {
5513
+ const FORMAT_ALIASES = {
5514
+ json: "flat-json",
5515
+ "json-flat": "flat-json",
5516
+ javascript: "ts",
5517
+ js: "ts",
5518
+ sass: "scss",
5519
+ tw: "tailwind"
5520
+ };
5521
+ const hint = FORMAT_ALIASES[opts.format.toLowerCase()];
5225
5522
  process.stderr.write(
5226
5523
  `Error: unsupported format "${opts.format}".
5227
5524
  Supported formats: ${SUPPORTED_FORMATS.join(", ")}
5228
- `
5525
+ ` + (hint ? `Did you mean "${hint}"?
5526
+ ` : "")
5229
5527
  );
5230
5528
  process.exit(1);
5231
5529
  }
5232
5530
  const format = opts.format;
5233
5531
  try {
5234
5532
  const filePath = resolveTokenFilePath2(opts.file);
5235
- if (!existsSync12(filePath)) {
5533
+ if (!existsSync13(filePath)) {
5236
5534
  throw new Error(
5237
5535
  `Token file not found at ${filePath}.
5238
5536
  Create a reactscope.tokens.json file or use --file to specify a path.`
5239
5537
  );
5240
5538
  }
5241
- const raw = readFileSync9(filePath, "utf-8");
5539
+ const raw = readFileSync10(filePath, "utf-8");
5242
5540
  const { tokens, rawFile } = parseTokenFileSync(raw);
5243
5541
  let themesMap;
5244
5542
  if (opts.theme !== void 0) {
@@ -5639,9 +5937,9 @@ function resolveTokenFilePath(fileFlag) {
5639
5937
  return resolve17(process.cwd(), fileFlag);
5640
5938
  }
5641
5939
  const configPath = resolve17(process.cwd(), CONFIG_FILE2);
5642
- if (existsSync13(configPath)) {
5940
+ if (existsSync14(configPath)) {
5643
5941
  try {
5644
- const raw = readFileSync10(configPath, "utf-8");
5942
+ const raw = readFileSync11(configPath, "utf-8");
5645
5943
  const config = JSON.parse(raw);
5646
5944
  if (typeof config === "object" && config !== null && "tokens" in config && typeof config.tokens === "object" && config.tokens !== null && typeof config.tokens?.file === "string") {
5647
5945
  const file = config.tokens.file;
@@ -5653,13 +5951,13 @@ function resolveTokenFilePath(fileFlag) {
5653
5951
  return resolve17(process.cwd(), DEFAULT_TOKEN_FILE2);
5654
5952
  }
5655
5953
  function loadTokens(absPath) {
5656
- if (!existsSync13(absPath)) {
5954
+ if (!existsSync14(absPath)) {
5657
5955
  throw new Error(
5658
5956
  `Token file not found at ${absPath}.
5659
5957
  Create a reactscope.tokens.json file or use --file to specify a path.`
5660
5958
  );
5661
5959
  }
5662
- const raw = readFileSync10(absPath, "utf-8");
5960
+ const raw = readFileSync11(absPath, "utf-8");
5663
5961
  return parseTokenFileSync2(raw);
5664
5962
  }
5665
5963
  function getRawValue(node, segments) {
@@ -5873,13 +6171,13 @@ function registerValidate(tokensCmd) {
5873
6171
  ).option("--file <path>", "Path to token file (overrides config)").option("--format <fmt>", "Output format: json or text (default: auto-detect)").action((opts) => {
5874
6172
  try {
5875
6173
  const filePath = resolveTokenFilePath(opts.file);
5876
- if (!existsSync13(filePath)) {
6174
+ if (!existsSync14(filePath)) {
5877
6175
  throw new Error(
5878
6176
  `Token file not found at ${filePath}.
5879
6177
  Create a reactscope.tokens.json file or use --file to specify a path.`
5880
6178
  );
5881
6179
  }
5882
- const raw = readFileSync10(filePath, "utf-8");
6180
+ const raw = readFileSync11(filePath, "utf-8");
5883
6181
  const useJson = opts.format === "json" || opts.format !== "text" && !isTTY2();
5884
6182
  const errors = [];
5885
6183
  let parsed;
@@ -6037,7 +6335,7 @@ function createProgram(options = {}) {
6037
6335
  }
6038
6336
  );
6039
6337
  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) => {
6040
- const raw = readFileSync11(tracePath, "utf-8");
6338
+ const raw = readFileSync12(tracePath, "utf-8");
6041
6339
  const trace = loadTrace(raw);
6042
6340
  const source = generateTest(trace, {
6043
6341
  description: opts.description,