@agent-scope/cli 1.15.0 → 1.17.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/index.js CHANGED
@@ -1,5 +1,5 @@
1
- import { existsSync, writeFileSync, readFileSync, mkdirSync, appendFileSync, readdirSync, rmSync } from 'fs';
2
- import { resolve, join, dirname } from 'path';
1
+ import { existsSync, writeFileSync, readFileSync, mkdirSync, appendFileSync, readdirSync, rmSync, statSync, createReadStream } from 'fs';
2
+ import { resolve, join, extname, dirname } from 'path';
3
3
  import { generateManifest } from '@agent-scope/manifest';
4
4
  import { SpriteSheetGenerator, safeRender, BrowserPool, ALL_CONTEXT_IDS, contextAxis, stressAxis, ALL_STRESS_IDS, RenderMatrix, SatoriRenderer } from '@agent-scope/render';
5
5
  import { TokenResolver, ComplianceEngine, parseTokenFileSync, ThemeResolver, exportTokens, validateTokenFile, TokenValidationError, TokenParseError, ImpactAnalyzer } from '@agent-scope/tokens';
@@ -9,6 +9,8 @@ import { createRequire } from 'module';
9
9
  import * as readline from 'readline';
10
10
  import { chromium } from 'playwright';
11
11
  import { loadTrace, generateTest, getBrowserEntryScript } from '@agent-scope/playwright';
12
+ import { createServer } from 'http';
13
+ import { buildSite } from '@agent-scope/site';
12
14
 
13
15
  // src/ci/commands.ts
14
16
  async function buildComponentHarness(filePath, componentName, props, viewportWidth, projectCss) {
@@ -1107,9 +1109,9 @@ function createRL() {
1107
1109
  });
1108
1110
  }
1109
1111
  async function ask(rl, question) {
1110
- return new Promise((resolve15) => {
1112
+ return new Promise((resolve17) => {
1111
1113
  rl.question(question, (answer) => {
1112
- resolve15(answer.trim());
1114
+ resolve17(answer.trim());
1113
1115
  });
1114
1116
  });
1115
1117
  }
@@ -2389,8 +2391,8 @@ Available: ${available}`
2389
2391
  wastedRenders: opts.wastedRenders
2390
2392
  });
2391
2393
  await shutdownPool2();
2392
- const fmt = resolveFormat2(opts.format);
2393
- if (fmt === "json") {
2394
+ const fmt2 = resolveFormat2(opts.format);
2395
+ if (fmt2 === "json") {
2394
2396
  process.stdout.write(`${JSON.stringify(instrumentRoot, null, 2)}
2395
2397
  `);
2396
2398
  } else {
@@ -3109,12 +3111,12 @@ Available: ${available}`
3109
3111
  );
3110
3112
  return;
3111
3113
  }
3112
- const fmt = resolveSingleFormat(opts.format);
3113
- if (fmt === "json") {
3114
+ const fmt2 = resolveSingleFormat(opts.format);
3115
+ if (fmt2 === "json") {
3114
3116
  const json = formatRenderJson(componentName, props, result);
3115
3117
  process.stdout.write(`${JSON.stringify(json, null, 2)}
3116
3118
  `);
3117
- } else if (fmt === "file") {
3119
+ } else if (fmt2 === "file") {
3118
3120
  const dir = resolve(process.cwd(), DEFAULT_OUTPUT_DIR);
3119
3121
  mkdirSync(dir, { recursive: true });
3120
3122
  const outPath = resolve(dir, `${componentName}.png`);
@@ -3235,8 +3237,8 @@ Available: ${available}`
3235
3237
  process.stderr.write(`Sprite sheet saved to ${spritePath}
3236
3238
  `);
3237
3239
  }
3238
- const fmt = resolveMatrixFormat(opts.format, opts.sprite !== void 0);
3239
- if (fmt === "file") {
3240
+ const fmt2 = resolveMatrixFormat(opts.format, opts.sprite !== void 0);
3241
+ if (fmt2 === "file") {
3240
3242
  const { SpriteSheetGenerator: SpriteSheetGenerator2 } = await import('@agent-scope/render');
3241
3243
  const gen = new SpriteSheetGenerator2();
3242
3244
  const sheet = await gen.generate(result);
@@ -3249,10 +3251,10 @@ Available: ${available}`
3249
3251
  `\u2713 ${componentName} matrix (${result.stats.totalCells} cells) \u2192 ${relPath} (${result.stats.wallClockTimeMs.toFixed(0)}ms total)
3250
3252
  `
3251
3253
  );
3252
- } else if (fmt === "json") {
3254
+ } else if (fmt2 === "json") {
3253
3255
  process.stdout.write(`${JSON.stringify(formatMatrixJson(result), null, 2)}
3254
3256
  `);
3255
- } else if (fmt === "png") {
3257
+ } else if (fmt2 === "png") {
3256
3258
  if (opts.sprite !== void 0) {
3257
3259
  } else {
3258
3260
  const { SpriteSheetGenerator: SpriteSheetGenerator2 } = await import('@agent-scope/render');
@@ -3260,9 +3262,9 @@ Available: ${available}`
3260
3262
  const sheet = await gen.generate(result);
3261
3263
  process.stdout.write(sheet.png);
3262
3264
  }
3263
- } else if (fmt === "html") {
3265
+ } else if (fmt2 === "html") {
3264
3266
  process.stdout.write(formatMatrixHtml(componentName, result));
3265
- } else if (fmt === "csv") {
3267
+ } else if (fmt2 === "csv") {
3266
3268
  process.stdout.write(formatMatrixCsv(componentName, result));
3267
3269
  }
3268
3270
  } catch (err) {
@@ -3563,12 +3565,12 @@ async function runBaseline(options = {}) {
3563
3565
  mkdirSync(rendersDir, { recursive: true });
3564
3566
  let manifest;
3565
3567
  if (manifestPath !== void 0) {
3566
- const { readFileSync: readFileSync11 } = await import('fs');
3568
+ const { readFileSync: readFileSync12 } = await import('fs');
3567
3569
  const absPath = resolve(rootDir, manifestPath);
3568
3570
  if (!existsSync(absPath)) {
3569
3571
  throw new Error(`Manifest not found at ${absPath}.`);
3570
3572
  }
3571
- manifest = JSON.parse(readFileSync11(absPath, "utf-8"));
3573
+ manifest = JSON.parse(readFileSync12(absPath, "utf-8"));
3572
3574
  process.stderr.write(`Loaded manifest from ${manifestPath}
3573
3575
  `);
3574
3576
  } else {
@@ -4202,6 +4204,135 @@ function registerDiffSubCommand(reportCmd) {
4202
4204
  }
4203
4205
  );
4204
4206
  }
4207
+ var STATUS_BADGE = {
4208
+ added: "\u2705 added",
4209
+ removed: "\u{1F5D1}\uFE0F removed",
4210
+ unchanged: "\u2014 unchanged",
4211
+ compliance_regressed: "\u274C regressed",
4212
+ compliance_improved: "\u{1F4C8} improved",
4213
+ size_changed: "\u{1F4D0} resized"
4214
+ };
4215
+ function fmt(n) {
4216
+ return `${(n * 100).toFixed(1)}%`;
4217
+ }
4218
+ function fmtDelta(delta) {
4219
+ if (delta === null) return "\u2014";
4220
+ const sign = delta >= 0 ? "+" : "";
4221
+ return `${sign}${(delta * 100).toFixed(1)}%`;
4222
+ }
4223
+ function fmtDimensions(d) {
4224
+ if (d === null) return "\u2014";
4225
+ return `${d.width} \xD7 ${d.height}`;
4226
+ }
4227
+ function complianceDeltaArrow(diff) {
4228
+ const delta = diff.currentAggregateCompliance - diff.baselineAggregateCompliance;
4229
+ if (Math.abs(delta) < 5e-4) return `\u2192 ${fmt(diff.currentAggregateCompliance)} (no change)`;
4230
+ const arrow = delta > 0 ? "\u2191" : "\u2193";
4231
+ return `${arrow} ${fmtDelta(delta)}`;
4232
+ }
4233
+ function formatPrComment(diff) {
4234
+ const { summary, components } = diff;
4235
+ const lines = [];
4236
+ const hasRegressions = diff.hasRegressions;
4237
+ const headerEmoji = hasRegressions ? "\u26A0\uFE0F" : "\u2705";
4238
+ lines.push(`## ${headerEmoji} Scope Report`);
4239
+ lines.push("");
4240
+ lines.push("| Metric | Value |");
4241
+ lines.push("|---|---|");
4242
+ lines.push(`| Baseline compliance | ${fmt(diff.baselineAggregateCompliance)} |`);
4243
+ lines.push(`| Current compliance | ${fmt(diff.currentAggregateCompliance)} |`);
4244
+ lines.push(`| Delta | ${complianceDeltaArrow(diff)} |`);
4245
+ lines.push(
4246
+ `| Components | ${summary.total} total \xB7 ${summary.added} added \xB7 ${summary.removed} removed \xB7 ${summary.complianceRegressed} regressed |`
4247
+ );
4248
+ if (summary.renderFailed > 0) {
4249
+ lines.push(`| Render failures | ${summary.renderFailed} |`);
4250
+ }
4251
+ lines.push("");
4252
+ const changed = components.filter((c) => c.status !== "unchanged");
4253
+ const unchanged = components.filter((c) => c.status === "unchanged");
4254
+ if (changed.length > 0) {
4255
+ lines.push("### Changes");
4256
+ lines.push("");
4257
+ lines.push("| Component | Status | Compliance \u0394 | Dimensions |");
4258
+ lines.push("|---|---|---|---|");
4259
+ for (const c of changed) {
4260
+ const badge = STATUS_BADGE[c.status];
4261
+ const delta = fmtDelta(c.complianceDelta);
4262
+ const dims = fmtDimensions(c.currentDimensions ?? c.baselineDimensions);
4263
+ lines.push(`| \`${c.name}\` | ${badge} | ${delta} | ${dims} |`);
4264
+ }
4265
+ lines.push("");
4266
+ }
4267
+ if (unchanged.length > 0) {
4268
+ lines.push(
4269
+ `<details><summary>${unchanged.length} unchanged component${unchanged.length === 1 ? "" : "s"}</summary>`
4270
+ );
4271
+ lines.push("");
4272
+ lines.push("| Component | Compliance |");
4273
+ lines.push("|---|---|");
4274
+ for (const c of unchanged) {
4275
+ lines.push(
4276
+ `| \`${c.name}\` | ${c.currentCompliance !== null ? fmt(c.currentCompliance) : "\u2014"} |`
4277
+ );
4278
+ }
4279
+ lines.push("");
4280
+ lines.push("</details>");
4281
+ lines.push("");
4282
+ }
4283
+ lines.push(
4284
+ `> Generated by [Scope](https://github.com/FlatFilers/Scope) \xB7 diffed at ${diff.diffedAt}`
4285
+ );
4286
+ return lines.join("\n");
4287
+ }
4288
+ function loadDiffResult(filePath) {
4289
+ const abs = resolve(filePath);
4290
+ if (!existsSync(abs)) {
4291
+ throw new Error(`DiffResult file not found: ${abs}`);
4292
+ }
4293
+ let raw;
4294
+ try {
4295
+ raw = readFileSync(abs, "utf-8");
4296
+ } catch (err) {
4297
+ throw new Error(
4298
+ `Failed to read DiffResult file: ${err instanceof Error ? err.message : String(err)}`
4299
+ );
4300
+ }
4301
+ let parsed;
4302
+ try {
4303
+ parsed = JSON.parse(raw);
4304
+ } catch {
4305
+ throw new Error(`DiffResult file is not valid JSON: ${abs}`);
4306
+ }
4307
+ if (typeof parsed !== "object" || parsed === null || !("diffedAt" in parsed) || !("components" in parsed) || !("summary" in parsed)) {
4308
+ throw new Error(
4309
+ `DiffResult file does not match expected shape (missing diffedAt/components/summary): ${abs}`
4310
+ );
4311
+ }
4312
+ return parsed;
4313
+ }
4314
+ function registerPrCommentSubCommand(reportCmd) {
4315
+ reportCmd.command("pr-comment").description(
4316
+ "Format a DiffResult JSON file as a GitHub PR comment (Markdown, written to stdout)"
4317
+ ).requiredOption("-i, --input <path>", "Path to DiffResult JSON (from scope report diff --json)").option("-o, --output <path>", "Write comment to file instead of stdout").action(async (opts) => {
4318
+ try {
4319
+ const diff = loadDiffResult(opts.input);
4320
+ const comment = formatPrComment(diff);
4321
+ if (opts.output !== void 0) {
4322
+ writeFileSync(resolve(opts.output), comment, "utf-8");
4323
+ process.stderr.write(`PR comment written to ${opts.output}
4324
+ `);
4325
+ } else {
4326
+ process.stdout.write(`${comment}
4327
+ `);
4328
+ }
4329
+ } catch (err) {
4330
+ process.stderr.write(`Error: ${err instanceof Error ? err.message : String(err)}
4331
+ `);
4332
+ process.exit(1);
4333
+ }
4334
+ });
4335
+ }
4205
4336
 
4206
4337
  // src/tree-formatter.ts
4207
4338
  var BRANCH2 = "\u251C\u2500\u2500 ";
@@ -4480,6 +4611,130 @@ function buildStructuredReport(report) {
4480
4611
  route: report.route?.pattern ?? null
4481
4612
  };
4482
4613
  }
4614
+ var MIME_TYPES = {
4615
+ ".html": "text/html; charset=utf-8",
4616
+ ".css": "text/css; charset=utf-8",
4617
+ ".js": "application/javascript; charset=utf-8",
4618
+ ".json": "application/json; charset=utf-8",
4619
+ ".png": "image/png",
4620
+ ".jpg": "image/jpeg",
4621
+ ".jpeg": "image/jpeg",
4622
+ ".svg": "image/svg+xml",
4623
+ ".ico": "image/x-icon"
4624
+ };
4625
+ function registerBuild(siteCmd) {
4626
+ siteCmd.command("build").description("Build a static HTML gallery from .reactscope/ output").option("-i, --input <path>", "Path to .reactscope input directory", ".reactscope").option("-o, --output <path>", "Output directory for generated site", ".reactscope/site").option("--base-path <path>", "Base URL path prefix for subdirectory deployment", "/").option("--compliance <path>", "Path to compliance batch report JSON").option("--title <text>", "Site title", "Scope \u2014 Component Gallery").action(
4627
+ async (opts) => {
4628
+ try {
4629
+ const inputDir = resolve(process.cwd(), opts.input);
4630
+ const outputDir = resolve(process.cwd(), opts.output);
4631
+ if (!existsSync(inputDir)) {
4632
+ throw new Error(
4633
+ `Input directory not found: ${inputDir}
4634
+ Run \`scope manifest generate\` and \`scope render\` first.`
4635
+ );
4636
+ }
4637
+ const manifestPath = join(inputDir, "manifest.json");
4638
+ if (!existsSync(manifestPath)) {
4639
+ throw new Error(
4640
+ `Manifest not found at ${manifestPath}
4641
+ Run \`scope manifest generate\` first.`
4642
+ );
4643
+ }
4644
+ process.stderr.write(`Building site from ${inputDir}\u2026
4645
+ `);
4646
+ await buildSite({
4647
+ inputDir,
4648
+ outputDir,
4649
+ basePath: opts.basePath,
4650
+ ...opts.compliance !== void 0 && {
4651
+ compliancePath: resolve(process.cwd(), opts.compliance)
4652
+ },
4653
+ title: opts.title
4654
+ });
4655
+ process.stderr.write(`Site written to ${outputDir}
4656
+ `);
4657
+ process.stdout.write(`${outputDir}
4658
+ `);
4659
+ } catch (err) {
4660
+ process.stderr.write(`Error: ${err instanceof Error ? err.message : String(err)}
4661
+ `);
4662
+ process.exit(1);
4663
+ }
4664
+ }
4665
+ );
4666
+ }
4667
+ function registerServe(siteCmd) {
4668
+ siteCmd.command("serve").description("Serve the built static site locally").option("-p, --port <number>", "Port to listen on", "3000").option("-d, --dir <path>", "Directory to serve", ".reactscope/site").action((opts) => {
4669
+ try {
4670
+ const port = Number.parseInt(opts.port, 10);
4671
+ if (Number.isNaN(port) || port < 1 || port > 65535) {
4672
+ throw new Error(`Invalid port: ${opts.port}`);
4673
+ }
4674
+ const serveDir = resolve(process.cwd(), opts.dir);
4675
+ if (!existsSync(serveDir)) {
4676
+ throw new Error(
4677
+ `Serve directory not found: ${serveDir}
4678
+ Run \`scope site build\` first.`
4679
+ );
4680
+ }
4681
+ const server = createServer((req, res) => {
4682
+ const rawUrl = req.url ?? "/";
4683
+ const urlPath = decodeURIComponent(rawUrl.split("?")[0] ?? "/");
4684
+ const filePath = join(serveDir, urlPath.endsWith("/") ? `${urlPath}index.html` : urlPath);
4685
+ if (!filePath.startsWith(serveDir)) {
4686
+ res.writeHead(403, { "Content-Type": "text/plain" });
4687
+ res.end("Forbidden");
4688
+ return;
4689
+ }
4690
+ if (existsSync(filePath) && statSync(filePath).isFile()) {
4691
+ const ext = extname(filePath).toLowerCase();
4692
+ const contentType = MIME_TYPES[ext] ?? "application/octet-stream";
4693
+ res.writeHead(200, { "Content-Type": contentType });
4694
+ createReadStream(filePath).pipe(res);
4695
+ return;
4696
+ }
4697
+ const htmlPath = `${filePath}.html`;
4698
+ if (existsSync(htmlPath) && statSync(htmlPath).isFile()) {
4699
+ res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
4700
+ createReadStream(htmlPath).pipe(res);
4701
+ return;
4702
+ }
4703
+ res.writeHead(404, { "Content-Type": "text/plain" });
4704
+ res.end(`Not found: ${urlPath}`);
4705
+ });
4706
+ server.listen(port, () => {
4707
+ process.stderr.write(`Scope site running at http://localhost:${port}
4708
+ `);
4709
+ process.stderr.write(`Serving ${serveDir}
4710
+ `);
4711
+ process.stderr.write("Press Ctrl+C to stop.\n");
4712
+ });
4713
+ server.on("error", (err) => {
4714
+ if (err.code === "EADDRINUSE") {
4715
+ process.stderr.write(`Error: Port ${port} is already in use.
4716
+ `);
4717
+ } else {
4718
+ process.stderr.write(`Server error: ${err.message}
4719
+ `);
4720
+ }
4721
+ process.exit(1);
4722
+ });
4723
+ } catch (err) {
4724
+ process.stderr.write(`Error: ${err instanceof Error ? err.message : String(err)}
4725
+ `);
4726
+ process.exit(1);
4727
+ }
4728
+ });
4729
+ }
4730
+ function createSiteCommand() {
4731
+ const siteCmd = new Command("site").description(
4732
+ "Build and serve the static component gallery site"
4733
+ );
4734
+ registerBuild(siteCmd);
4735
+ registerServe(siteCmd);
4736
+ return siteCmd;
4737
+ }
4483
4738
  var DEFAULT_STYLES_PATH = ".reactscope/compliance-styles.json";
4484
4739
  function loadStylesFile(stylesPath) {
4485
4740
  const absPath = resolve(process.cwd(), stylesPath);
@@ -5497,7 +5752,9 @@ function createProgram(options = {}) {
5497
5752
  if (existingReportCmd !== void 0) {
5498
5753
  registerBaselineSubCommand(existingReportCmd);
5499
5754
  registerDiffSubCommand(existingReportCmd);
5755
+ registerPrCommentSubCommand(existingReportCmd);
5500
5756
  }
5757
+ program.addCommand(createSiteCommand());
5501
5758
  return program;
5502
5759
  }
5503
5760