@agent-scope/cli 1.0.1 → 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.js CHANGED
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  // src/program.ts
4
- import { readFileSync as readFileSync2 } from "fs";
4
+ import { readFileSync as readFileSync3 } from "fs";
5
5
  import { generateTest, loadTrace } from "@agent-scope/playwright";
6
6
  import { Command as Command3 } from "commander";
7
7
 
@@ -325,7 +325,7 @@ function createManifestCommand() {
325
325
 
326
326
  // src/render-commands.ts
327
327
  import { mkdirSync as mkdirSync2, writeFileSync as writeFileSync3 } from "fs";
328
- import { resolve as resolve2 } from "path";
328
+ import { resolve as resolve3 } from "path";
329
329
  import {
330
330
  ALL_CONTEXT_IDS,
331
331
  ALL_STRESS_IDS,
@@ -341,9 +341,9 @@ import { Command as Command2 } from "commander";
341
341
  // src/component-bundler.ts
342
342
  import { dirname } from "path";
343
343
  import * as esbuild from "esbuild";
344
- async function buildComponentHarness(filePath, componentName, props, viewportWidth) {
344
+ async function buildComponentHarness(filePath, componentName, props, viewportWidth, projectCss) {
345
345
  const bundledScript = await bundleComponentToIIFE(filePath, componentName, props);
346
- return wrapInHtml(bundledScript, viewportWidth);
346
+ return wrapInHtml(bundledScript, viewportWidth, projectCss);
347
347
  }
348
348
  async function bundleComponentToIIFE(filePath, componentName, props) {
349
349
  const propsJson = JSON.stringify(props).replace(/<\/script>/gi, "<\\/script>");
@@ -429,7 +429,10 @@ ${msg}`);
429
429
  }
430
430
  return outputFile.text;
431
431
  }
432
- function wrapInHtml(bundledScript, viewportWidth) {
432
+ function wrapInHtml(bundledScript, viewportWidth, projectCss) {
433
+ const projectStyleBlock = projectCss != null && projectCss.length > 0 ? `<style id="scope-project-css">
434
+ ${projectCss.replace(/<\/style>/gi, "<\\/style>")}
435
+ </style>` : "";
433
436
  return `<!DOCTYPE html>
434
437
  <html lang="en">
435
438
  <head>
@@ -440,6 +443,7 @@ function wrapInHtml(bundledScript, viewportWidth) {
440
443
  html, body { margin: 0; padding: 0; background: #fff; font-family: system-ui, sans-serif; }
441
444
  #scope-root { display: inline-block; min-width: 1px; min-height: 1px; }
442
445
  </style>
446
+ ${projectStyleBlock}
443
447
  </head>
444
448
  <body>
445
449
  <div id="scope-root" data-reactscope-root></div>
@@ -608,6 +612,126 @@ function csvEscape(value) {
608
612
  return value;
609
613
  }
610
614
 
615
+ // src/tailwind-css.ts
616
+ import { existsSync as existsSync2, readFileSync as readFileSync2 } from "fs";
617
+ import { createRequire } from "module";
618
+ import { resolve as resolve2 } from "path";
619
+ var CONFIG_FILENAMES = [
620
+ ".reactscope/config.json",
621
+ ".reactscope/config.js",
622
+ ".reactscope/config.mjs"
623
+ ];
624
+ var STYLE_ENTRY_CANDIDATES = [
625
+ "src/index.css",
626
+ "src/globals.css",
627
+ "app/globals.css",
628
+ "app/index.css",
629
+ "styles/index.css",
630
+ "index.css"
631
+ ];
632
+ var TAILWIND_IMPORT = /@import\s+["']tailwindcss["']\s*;?/;
633
+ var compilerCache = null;
634
+ function getCachedBuild(cwd) {
635
+ if (compilerCache !== null && resolve2(compilerCache.cwd) === resolve2(cwd)) {
636
+ return compilerCache.build;
637
+ }
638
+ return null;
639
+ }
640
+ function findStylesEntry(cwd) {
641
+ for (const name of CONFIG_FILENAMES) {
642
+ const p = resolve2(cwd, name);
643
+ if (!existsSync2(p)) continue;
644
+ try {
645
+ if (name.endsWith(".json")) {
646
+ const raw = readFileSync2(p, "utf-8");
647
+ const data = JSON.parse(raw);
648
+ const scope = data.scope;
649
+ const entry = scope?.stylesEntry ?? data.stylesEntry;
650
+ if (typeof entry === "string") {
651
+ const full = resolve2(cwd, entry);
652
+ if (existsSync2(full)) return full;
653
+ }
654
+ }
655
+ } catch {
656
+ }
657
+ }
658
+ const pkgPath = resolve2(cwd, "package.json");
659
+ if (existsSync2(pkgPath)) {
660
+ try {
661
+ const raw = readFileSync2(pkgPath, "utf-8");
662
+ const pkg = JSON.parse(raw);
663
+ const entry = pkg.scope?.stylesEntry;
664
+ if (typeof entry === "string") {
665
+ const full = resolve2(cwd, entry);
666
+ if (existsSync2(full)) return full;
667
+ }
668
+ } catch {
669
+ }
670
+ }
671
+ for (const candidate of STYLE_ENTRY_CANDIDATES) {
672
+ const full = resolve2(cwd, candidate);
673
+ if (existsSync2(full)) {
674
+ try {
675
+ const content = readFileSync2(full, "utf-8");
676
+ if (TAILWIND_IMPORT.test(content)) return full;
677
+ } catch {
678
+ }
679
+ }
680
+ }
681
+ return null;
682
+ }
683
+ async function getTailwindCompiler(cwd) {
684
+ const cached = getCachedBuild(cwd);
685
+ if (cached !== null) return cached;
686
+ const entryPath = findStylesEntry(cwd);
687
+ if (entryPath === null) return null;
688
+ let compile;
689
+ try {
690
+ const require2 = createRequire(resolve2(cwd, "package.json"));
691
+ const tailwind = require2("tailwindcss");
692
+ const fn = tailwind.compile;
693
+ if (typeof fn !== "function") return null;
694
+ compile = fn;
695
+ } catch {
696
+ return null;
697
+ }
698
+ const entryContent = readFileSync2(entryPath, "utf-8");
699
+ const loadStylesheet = async (id, base) => {
700
+ if (id === "tailwindcss") {
701
+ const nodeModules = resolve2(cwd, "node_modules");
702
+ const tailwindCssPath = resolve2(nodeModules, "tailwindcss", "index.css");
703
+ if (!existsSync2(tailwindCssPath)) {
704
+ throw new Error(
705
+ `Tailwind v4: tailwindcss package not found at ${tailwindCssPath}. Install with: npm install tailwindcss`
706
+ );
707
+ }
708
+ const content = readFileSync2(tailwindCssPath, "utf-8");
709
+ return { path: "virtual:tailwindcss/index.css", base, content };
710
+ }
711
+ const full = resolve2(base, id);
712
+ if (existsSync2(full)) {
713
+ const content = readFileSync2(full, "utf-8");
714
+ return { path: full, base: resolve2(full, ".."), content };
715
+ }
716
+ throw new Error(`Tailwind v4: could not load stylesheet: ${id} (base: ${base})`);
717
+ };
718
+ const result = await compile(entryContent, {
719
+ base: cwd,
720
+ from: entryPath,
721
+ loadStylesheet
722
+ });
723
+ const build2 = result.build.bind(result);
724
+ compilerCache = { cwd, build: build2 };
725
+ return build2;
726
+ }
727
+ async function getCompiledCssForClasses(cwd, classes) {
728
+ const build2 = await getTailwindCompiler(cwd);
729
+ if (build2 === null) return null;
730
+ const deduped = [...new Set(classes)].filter(Boolean);
731
+ if (deduped.length === 0) return null;
732
+ return build2(deduped);
733
+ }
734
+
611
735
  // src/render-commands.ts
612
736
  var MANIFEST_PATH2 = ".reactscope/manifest.json";
613
737
  var DEFAULT_OUTPUT_DIR = ".reactscope/renders";
@@ -661,6 +785,20 @@ function buildRenderer(filePath, componentName, viewportWidth, viewportHeight) {
661
785
  if (renderError !== null) {
662
786
  throw new Error(`Component render error: ${renderError}`);
663
787
  }
788
+ const rootDir = process.cwd();
789
+ const classes = await page.evaluate(() => {
790
+ const set = /* @__PURE__ */ new Set();
791
+ document.querySelectorAll("[class]").forEach((el) => {
792
+ for (const c of el.className.split(/\s+/)) {
793
+ if (c) set.add(c);
794
+ }
795
+ });
796
+ return [...set];
797
+ });
798
+ const projectCss = await getCompiledCssForClasses(rootDir, classes);
799
+ if (projectCss != null && projectCss.length > 0) {
800
+ await page.addStyleTag({ content: projectCss });
801
+ }
664
802
  const renderTimeMs = performance.now() - startMs;
665
803
  const rootLocator = page.locator("[data-reactscope-root]");
666
804
  const boundingBox = await rootLocator.boundingBox();
@@ -669,13 +807,19 @@ function buildRenderer(filePath, componentName, viewportWidth, viewportHeight) {
669
807
  `Component "${componentName}" rendered with zero bounding box \u2014 it may be invisible or not mounted`
670
808
  );
671
809
  }
810
+ const PAD = 24;
811
+ const MIN_W = 320;
812
+ const MIN_H = 200;
813
+ const clipX = Math.max(0, boundingBox.x - PAD);
814
+ const clipY = Math.max(0, boundingBox.y - PAD);
815
+ const rawW = boundingBox.width + PAD * 2;
816
+ const rawH = boundingBox.height + PAD * 2;
817
+ const clipW = Math.max(rawW, MIN_W);
818
+ const clipH = Math.max(rawH, MIN_H);
819
+ const safeW = Math.min(clipW, viewportWidth - clipX);
820
+ const safeH = Math.min(clipH, viewportHeight - clipY);
672
821
  const screenshot = await page.screenshot({
673
- clip: {
674
- x: boundingBox.x,
675
- y: boundingBox.y,
676
- width: boundingBox.width,
677
- height: boundingBox.height
678
- },
822
+ clip: { x: clipX, y: clipY, width: safeW, height: safeH },
679
823
  type: "png"
680
824
  });
681
825
  const computedStyles = {};
@@ -702,8 +846,8 @@ function buildRenderer(filePath, componentName, viewportWidth, viewportHeight) {
702
846
  computedStyles["[data-reactscope-root] > *"] = styles;
703
847
  return {
704
848
  screenshot,
705
- width: Math.round(boundingBox.width),
706
- height: Math.round(boundingBox.height),
849
+ width: Math.round(safeW),
850
+ height: Math.round(safeH),
707
851
  renderTimeMs,
708
852
  computedStyles
709
853
  };
@@ -736,7 +880,7 @@ Available: ${available}`
736
880
  }
737
881
  const { width, height } = parseViewport(opts.viewport);
738
882
  const rootDir = process.cwd();
739
- const filePath = resolve2(rootDir, descriptor.filePath);
883
+ const filePath = resolve3(rootDir, descriptor.filePath);
740
884
  const renderer = buildRenderer(filePath, componentName, width, height);
741
885
  process.stderr.write(
742
886
  `Rendering ${componentName} [${descriptor.complexityClass}] at ${width}\xD7${height}\u2026
@@ -766,7 +910,7 @@ Available: ${available}`
766
910
  }
767
911
  const result = outcome.result;
768
912
  if (opts.output !== void 0) {
769
- const outPath = resolve2(process.cwd(), opts.output);
913
+ const outPath = resolve3(process.cwd(), opts.output);
770
914
  writeFileSync3(outPath, result.screenshot);
771
915
  process.stdout.write(
772
916
  `\u2713 ${componentName} \u2192 ${opts.output} (${result.width}\xD7${result.height}, ${result.renderTimeMs.toFixed(0)}ms)
@@ -780,9 +924,9 @@ Available: ${available}`
780
924
  process.stdout.write(`${JSON.stringify(json, null, 2)}
781
925
  `);
782
926
  } else if (fmt === "file") {
783
- const dir = resolve2(process.cwd(), DEFAULT_OUTPUT_DIR);
927
+ const dir = resolve3(process.cwd(), DEFAULT_OUTPUT_DIR);
784
928
  mkdirSync2(dir, { recursive: true });
785
- const outPath = resolve2(dir, `${componentName}.png`);
929
+ const outPath = resolve3(dir, `${componentName}.png`);
786
930
  writeFileSync3(outPath, result.screenshot);
787
931
  const relPath = `${DEFAULT_OUTPUT_DIR}/${componentName}.png`;
788
932
  process.stdout.write(
@@ -790,9 +934,9 @@ Available: ${available}`
790
934
  `
791
935
  );
792
936
  } else {
793
- const dir = resolve2(process.cwd(), DEFAULT_OUTPUT_DIR);
937
+ const dir = resolve3(process.cwd(), DEFAULT_OUTPUT_DIR);
794
938
  mkdirSync2(dir, { recursive: true });
795
- const outPath = resolve2(dir, `${componentName}.png`);
939
+ const outPath = resolve3(dir, `${componentName}.png`);
796
940
  writeFileSync3(outPath, result.screenshot);
797
941
  const relPath = `${DEFAULT_OUTPUT_DIR}/${componentName}.png`;
798
942
  process.stdout.write(
@@ -828,7 +972,7 @@ Available: ${available}`
828
972
  const concurrency = Math.max(1, parseInt(opts.concurrency, 10) || 8);
829
973
  const { width, height } = { width: 375, height: 812 };
830
974
  const rootDir = process.cwd();
831
- const filePath = resolve2(rootDir, descriptor.filePath);
975
+ const filePath = resolve3(rootDir, descriptor.filePath);
832
976
  const renderer = buildRenderer(filePath, componentName, width, height);
833
977
  const axes = [];
834
978
  if (opts.axes !== void 0) {
@@ -895,7 +1039,7 @@ Available: ${available}`
895
1039
  const { SpriteSheetGenerator } = await import("@agent-scope/render");
896
1040
  const gen = new SpriteSheetGenerator();
897
1041
  const sheet = await gen.generate(result);
898
- const spritePath = resolve2(process.cwd(), opts.sprite);
1042
+ const spritePath = resolve3(process.cwd(), opts.sprite);
899
1043
  writeFileSync3(spritePath, sheet.png);
900
1044
  process.stderr.write(`Sprite sheet saved to ${spritePath}
901
1045
  `);
@@ -905,9 +1049,9 @@ Available: ${available}`
905
1049
  const { SpriteSheetGenerator } = await import("@agent-scope/render");
906
1050
  const gen = new SpriteSheetGenerator();
907
1051
  const sheet = await gen.generate(result);
908
- const dir = resolve2(process.cwd(), DEFAULT_OUTPUT_DIR);
1052
+ const dir = resolve3(process.cwd(), DEFAULT_OUTPUT_DIR);
909
1053
  mkdirSync2(dir, { recursive: true });
910
- const outPath = resolve2(dir, `${componentName}-matrix.png`);
1054
+ const outPath = resolve3(dir, `${componentName}-matrix.png`);
911
1055
  writeFileSync3(outPath, sheet.png);
912
1056
  const relPath = `${DEFAULT_OUTPUT_DIR}/${componentName}-matrix.png`;
913
1057
  process.stdout.write(
@@ -951,7 +1095,7 @@ function registerRenderAll(renderCmd) {
951
1095
  return;
952
1096
  }
953
1097
  const concurrency = Math.max(1, parseInt(opts.concurrency, 10) || 4);
954
- const outputDir = resolve2(process.cwd(), opts.outputDir);
1098
+ const outputDir = resolve3(process.cwd(), opts.outputDir);
955
1099
  mkdirSync2(outputDir, { recursive: true });
956
1100
  const rootDir = process.cwd();
957
1101
  process.stderr.write(`Rendering ${total} components (concurrency: ${concurrency})\u2026
@@ -961,7 +1105,7 @@ function registerRenderAll(renderCmd) {
961
1105
  const renderOne = async (name) => {
962
1106
  const descriptor = manifest.components[name];
963
1107
  if (descriptor === void 0) return;
964
- const filePath = resolve2(rootDir, descriptor.filePath);
1108
+ const filePath = resolve3(rootDir, descriptor.filePath);
965
1109
  const renderer = buildRenderer(filePath, name, 375, 812);
966
1110
  const outcome = await safeRender(
967
1111
  () => renderer.renderCell({}, descriptor.complexityClass),
@@ -984,7 +1128,7 @@ function registerRenderAll(renderCmd) {
984
1128
  success: false,
985
1129
  errorMessage: outcome.error.message
986
1130
  });
987
- const errPath = resolve2(outputDir, `${name}.error.json`);
1131
+ const errPath = resolve3(outputDir, `${name}.error.json`);
988
1132
  writeFileSync3(
989
1133
  errPath,
990
1134
  JSON.stringify(
@@ -1002,9 +1146,9 @@ function registerRenderAll(renderCmd) {
1002
1146
  }
1003
1147
  const result = outcome.result;
1004
1148
  results.push({ name, renderTimeMs: result.renderTimeMs, success: true });
1005
- const pngPath = resolve2(outputDir, `${name}.png`);
1149
+ const pngPath = resolve3(outputDir, `${name}.png`);
1006
1150
  writeFileSync3(pngPath, result.screenshot);
1007
- const jsonPath = resolve2(outputDir, `${name}.json`);
1151
+ const jsonPath = resolve3(outputDir, `${name}.json`);
1008
1152
  writeFileSync3(jsonPath, JSON.stringify(formatRenderJson(name, {}, result), null, 2));
1009
1153
  if (isTTY()) {
1010
1154
  process.stdout.write(
@@ -1430,7 +1574,7 @@ function createProgram(options = {}) {
1430
1574
  }
1431
1575
  );
1432
1576
  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) => {
1433
- const raw = readFileSync2(tracePath, "utf-8");
1577
+ const raw = readFileSync3(tracePath, "utf-8");
1434
1578
  const trace = loadTrace(raw);
1435
1579
  const source = generateTest(trace, {
1436
1580
  description: opts.description,