@agent-scope/cli 1.13.0 → 1.14.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 readFileSync8 } from "fs";
4
+ import { readFileSync as readFileSync9 } from "fs";
5
5
  import { generateTest, loadTrace } from "@agent-scope/playwright";
6
6
  import { Command as Command8 } from "commander";
7
7
 
@@ -255,9 +255,9 @@ function createRL() {
255
255
  });
256
256
  }
257
257
  async function ask(rl, question) {
258
- return new Promise((resolve12) => {
258
+ return new Promise((resolve14) => {
259
259
  rl.question(question, (answer) => {
260
- resolve12(answer.trim());
260
+ resolve14(answer.trim());
261
261
  });
262
262
  });
263
263
  }
@@ -2874,8 +2874,8 @@ Available: ${available}`
2874
2874
  `
2875
2875
  );
2876
2876
  if (opts.sprite !== void 0) {
2877
- const { SpriteSheetGenerator } = await import("@agent-scope/render");
2878
- const gen = new SpriteSheetGenerator();
2877
+ const { SpriteSheetGenerator: SpriteSheetGenerator2 } = await import("@agent-scope/render");
2878
+ const gen = new SpriteSheetGenerator2();
2879
2879
  const sheet = await gen.generate(result);
2880
2880
  const spritePath = resolve7(process.cwd(), opts.sprite);
2881
2881
  writeFileSync4(spritePath, sheet.png);
@@ -2884,8 +2884,8 @@ Available: ${available}`
2884
2884
  }
2885
2885
  const fmt = resolveMatrixFormat(opts.format, opts.sprite !== void 0);
2886
2886
  if (fmt === "file") {
2887
- const { SpriteSheetGenerator } = await import("@agent-scope/render");
2888
- const gen = new SpriteSheetGenerator();
2887
+ const { SpriteSheetGenerator: SpriteSheetGenerator2 } = await import("@agent-scope/render");
2888
+ const gen = new SpriteSheetGenerator2();
2889
2889
  const sheet = await gen.generate(result);
2890
2890
  const dir = resolve7(process.cwd(), DEFAULT_OUTPUT_DIR);
2891
2891
  mkdirSync3(dir, { recursive: true });
@@ -2902,8 +2902,8 @@ Available: ${available}`
2902
2902
  } else if (fmt === "png") {
2903
2903
  if (opts.sprite !== void 0) {
2904
2904
  } else {
2905
- const { SpriteSheetGenerator } = await import("@agent-scope/render");
2906
- const gen = new SpriteSheetGenerator();
2905
+ const { SpriteSheetGenerator: SpriteSheetGenerator2 } = await import("@agent-scope/render");
2906
+ const gen = new SpriteSheetGenerator2();
2907
2907
  const sheet = await gen.generate(result);
2908
2908
  process.stdout.write(sheet.png);
2909
2909
  }
@@ -3217,12 +3217,12 @@ async function runBaseline(options = {}) {
3217
3217
  mkdirSync4(rendersDir, { recursive: true });
3218
3218
  let manifest;
3219
3219
  if (manifestPath !== void 0) {
3220
- const { readFileSync: readFileSync9 } = await import("fs");
3220
+ const { readFileSync: readFileSync10 } = await import("fs");
3221
3221
  const absPath = resolve8(rootDir, manifestPath);
3222
3222
  if (!existsSync5(absPath)) {
3223
3223
  throw new Error(`Manifest not found at ${absPath}.`);
3224
3224
  }
3225
- manifest = JSON.parse(readFileSync9(absPath, "utf-8"));
3225
+ manifest = JSON.parse(readFileSync10(absPath, "utf-8"));
3226
3226
  process.stderr.write(`Loaded manifest from ${manifestPath}
3227
3227
  `);
3228
3228
  } else {
@@ -4144,47 +4144,223 @@ function buildStructuredReport(report) {
4144
4144
  }
4145
4145
 
4146
4146
  // src/tokens/commands.ts
4147
- import { existsSync as existsSync8, readFileSync as readFileSync7 } from "fs";
4148
- import { resolve as resolve11 } from "path";
4147
+ import { existsSync as existsSync9, readFileSync as readFileSync8 } from "fs";
4148
+ import { resolve as resolve13 } from "path";
4149
4149
  import {
4150
4150
  parseTokenFileSync as parseTokenFileSync2,
4151
4151
  TokenParseError,
4152
- TokenResolver as TokenResolver4,
4152
+ TokenResolver as TokenResolver7,
4153
4153
  TokenValidationError,
4154
4154
  validateTokenFile
4155
4155
  } from "@agent-scope/tokens";
4156
4156
  import { Command as Command7 } from "commander";
4157
4157
 
4158
- // src/tokens/export.ts
4159
- import { existsSync as existsSync7, readFileSync as readFileSync6, writeFileSync as writeFileSync7 } from "fs";
4158
+ // src/tokens/compliance.ts
4159
+ import { existsSync as existsSync7, readFileSync as readFileSync6 } from "fs";
4160
4160
  import { resolve as resolve10 } from "path";
4161
+ import {
4162
+ ComplianceEngine as ComplianceEngine3,
4163
+ TokenResolver as TokenResolver3
4164
+ } from "@agent-scope/tokens";
4165
+ var DEFAULT_STYLES_PATH = ".reactscope/compliance-styles.json";
4166
+ function loadStylesFile(stylesPath) {
4167
+ const absPath = resolve10(process.cwd(), stylesPath);
4168
+ if (!existsSync7(absPath)) {
4169
+ throw new Error(
4170
+ `Compliance styles file not found at ${absPath}.
4171
+ Run \`scope render all\` first to generate component styles, or use --styles to specify a path.
4172
+ Expected format: { "ComponentName": { colors: {}, spacing: {}, typography: {}, borders: {}, shadows: {} } }`
4173
+ );
4174
+ }
4175
+ const raw = readFileSync6(absPath, "utf-8");
4176
+ let parsed;
4177
+ try {
4178
+ parsed = JSON.parse(raw);
4179
+ } catch (err) {
4180
+ throw new Error(`Failed to parse compliance styles file as JSON: ${String(err)}`);
4181
+ }
4182
+ if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
4183
+ throw new Error(
4184
+ `Compliance styles file must be a JSON object mapping component names to ComputedStyles.`
4185
+ );
4186
+ }
4187
+ return parsed;
4188
+ }
4189
+ function categoryForProperty(property) {
4190
+ const lower = property.toLowerCase();
4191
+ if (lower.includes("shadow")) return "shadow";
4192
+ if (lower.includes("color") || lower === "background" || lower === "fill" || lower === "stroke")
4193
+ return "color";
4194
+ if (lower.includes("padding") || lower.includes("margin") || lower === "gap" || lower === "width" || lower === "height" || lower === "top" || lower === "right" || lower === "bottom" || lower === "left")
4195
+ return "spacing";
4196
+ if (lower.includes("border")) return "border";
4197
+ if (lower.includes("font") || lower.includes("line") || lower.includes("letter") || lower === "texttransform" || lower === "textdecoration")
4198
+ return "typography";
4199
+ return "spacing";
4200
+ }
4201
+ function buildCategorySummary(batch) {
4202
+ const cats = {
4203
+ color: { total: 0, onSystem: 0, offSystem: 0, compliance: 1 },
4204
+ spacing: { total: 0, onSystem: 0, offSystem: 0, compliance: 1 },
4205
+ typography: { total: 0, onSystem: 0, offSystem: 0, compliance: 1 },
4206
+ border: { total: 0, onSystem: 0, offSystem: 0, compliance: 1 },
4207
+ shadow: { total: 0, onSystem: 0, offSystem: 0, compliance: 1 }
4208
+ };
4209
+ for (const report of Object.values(batch.components)) {
4210
+ for (const [property, result] of Object.entries(report.properties)) {
4211
+ const cat = categoryForProperty(property);
4212
+ const summary = cats[cat];
4213
+ if (summary === void 0) continue;
4214
+ summary.total++;
4215
+ if (result.status === "on_system") {
4216
+ summary.onSystem++;
4217
+ } else {
4218
+ summary.offSystem++;
4219
+ }
4220
+ }
4221
+ }
4222
+ for (const summary of Object.values(cats)) {
4223
+ summary.compliance = summary.total === 0 ? 1 : summary.onSystem / summary.total;
4224
+ }
4225
+ return cats;
4226
+ }
4227
+ function collectOffenders(batch, limit = 10) {
4228
+ const offenders = [];
4229
+ const componentEntries = Object.entries(batch.components).map(([name, report]) => ({
4230
+ name,
4231
+ report,
4232
+ offSystemCount: report.offSystem
4233
+ }));
4234
+ componentEntries.sort((a, b) => b.offSystemCount - a.offSystemCount);
4235
+ for (const { name, report, offSystemCount } of componentEntries) {
4236
+ if (offSystemCount === 0) continue;
4237
+ for (const [property, result] of Object.entries(report.properties)) {
4238
+ if (result.status !== "OFF_SYSTEM") continue;
4239
+ offenders.push({
4240
+ component: name,
4241
+ property,
4242
+ value: result.value,
4243
+ nearestToken: result.nearest?.token ?? "\u2014",
4244
+ nearestValue: result.nearest?.value ?? "\u2014",
4245
+ offSystemCount
4246
+ });
4247
+ if (offenders.length >= limit) break;
4248
+ }
4249
+ if (offenders.length >= limit) break;
4250
+ }
4251
+ return offenders;
4252
+ }
4253
+ function formatPct(n) {
4254
+ return `${Math.round(n * 100)}%`;
4255
+ }
4256
+ function truncate(s, max) {
4257
+ return s.length > max ? `${s.slice(0, max - 1)}\u2026` : s;
4258
+ }
4259
+ function formatComplianceReport(batch, threshold) {
4260
+ const pct = Math.round(batch.aggregateCompliance * 100);
4261
+ const lines = [];
4262
+ const thresholdLabel = threshold !== void 0 ? pct >= threshold ? " \u2713 (pass)" : ` \u2717 (below threshold ${threshold}%)` : "";
4263
+ lines.push(`Overall compliance score: ${pct}%${thresholdLabel}`);
4264
+ lines.push("");
4265
+ const cats = buildCategorySummary(batch);
4266
+ const catEntries = Object.entries(cats).filter(([, s]) => s.total > 0);
4267
+ if (catEntries.length > 0) {
4268
+ lines.push("By category:");
4269
+ const catWidth = Math.max(...catEntries.map(([k]) => k.length));
4270
+ for (const [cat, summary] of catEntries) {
4271
+ const label = cat.padEnd(catWidth);
4272
+ lines.push(
4273
+ ` ${label} ${formatPct(summary.compliance).padStart(4)} (${summary.offSystem} off-system value${summary.offSystem !== 1 ? "s" : ""})`
4274
+ );
4275
+ }
4276
+ lines.push("");
4277
+ }
4278
+ const offenders = collectOffenders(batch);
4279
+ if (offenders.length > 0) {
4280
+ lines.push("Top off-system offenders (sorted by count):");
4281
+ const nameWidth = Math.max(9, ...offenders.map((o) => o.component.length));
4282
+ const propWidth = Math.max(8, ...offenders.map((o) => o.property.length));
4283
+ const valWidth = Math.max(5, ...offenders.map((o) => truncate(o.value, 40).length));
4284
+ for (const offender of offenders) {
4285
+ const name = offender.component.padEnd(nameWidth);
4286
+ const prop = offender.property.padEnd(propWidth);
4287
+ const val = truncate(offender.value, 40).padEnd(valWidth);
4288
+ const nearest = `${offender.nearestToken} (${truncate(offender.nearestValue, 30)})`;
4289
+ lines.push(` ${name} ${prop}: ${val} \u2192 nearest: ${nearest}`);
4290
+ }
4291
+ } else {
4292
+ lines.push("No off-system values detected. \u{1F389}");
4293
+ }
4294
+ return lines.join("\n");
4295
+ }
4296
+ function registerCompliance(tokensCmd) {
4297
+ tokensCmd.command("compliance").description("Aggregate token compliance report across all components (Token Spec \xA73.3 format)").option("--file <path>", "Path to token file (overrides config)").option("--styles <path>", `Path to compliance styles JSON (default: ${DEFAULT_STYLES_PATH})`).option("--threshold <n>", "Exit code 1 if compliance score is below this percentage (0-100)").option("--format <fmt>", "Output format: json or text (default: auto-detect)").action((opts) => {
4298
+ try {
4299
+ const tokenFilePath = resolveTokenFilePath(opts.file);
4300
+ const { tokens } = loadTokens(tokenFilePath);
4301
+ const resolver = new TokenResolver3(tokens);
4302
+ const engine = new ComplianceEngine3(resolver);
4303
+ const stylesPath = opts.styles ?? DEFAULT_STYLES_PATH;
4304
+ const stylesFile = loadStylesFile(stylesPath);
4305
+ const componentMap = /* @__PURE__ */ new Map();
4306
+ for (const [name, styles] of Object.entries(stylesFile)) {
4307
+ componentMap.set(name, styles);
4308
+ }
4309
+ if (componentMap.size === 0) {
4310
+ process.stderr.write(`Warning: No components found in styles file at ${stylesPath}
4311
+ `);
4312
+ }
4313
+ const batch = engine.auditBatch(componentMap);
4314
+ const useJson = opts.format === "json" || opts.format !== "text" && !isTTY();
4315
+ const threshold = opts.threshold !== void 0 ? Number.parseInt(opts.threshold, 10) : void 0;
4316
+ if (useJson) {
4317
+ process.stdout.write(`${JSON.stringify(batch, null, 2)}
4318
+ `);
4319
+ } else {
4320
+ process.stdout.write(`${formatComplianceReport(batch, threshold)}
4321
+ `);
4322
+ }
4323
+ if (threshold !== void 0 && Math.round(batch.aggregateCompliance * 100) < threshold) {
4324
+ process.exit(1);
4325
+ }
4326
+ } catch (err) {
4327
+ process.stderr.write(`Error: ${err instanceof Error ? err.message : String(err)}
4328
+ `);
4329
+ process.exit(1);
4330
+ }
4331
+ });
4332
+ }
4333
+
4334
+ // src/tokens/export.ts
4335
+ import { existsSync as existsSync8, readFileSync as readFileSync7, writeFileSync as writeFileSync7 } from "fs";
4336
+ import { resolve as resolve11 } from "path";
4161
4337
  import {
4162
4338
  exportTokens,
4163
4339
  parseTokenFileSync,
4164
4340
  ThemeResolver,
4165
- TokenResolver as TokenResolver3
4341
+ TokenResolver as TokenResolver4
4166
4342
  } from "@agent-scope/tokens";
4167
4343
  import { Command as Command6 } from "commander";
4168
4344
  var DEFAULT_TOKEN_FILE = "reactscope.tokens.json";
4169
4345
  var CONFIG_FILE = "reactscope.config.json";
4170
4346
  var SUPPORTED_FORMATS = ["css", "ts", "scss", "tailwind", "flat-json", "figma"];
4171
- function resolveTokenFilePath(fileFlag) {
4347
+ function resolveTokenFilePath2(fileFlag) {
4172
4348
  if (fileFlag !== void 0) {
4173
- return resolve10(process.cwd(), fileFlag);
4349
+ return resolve11(process.cwd(), fileFlag);
4174
4350
  }
4175
- const configPath = resolve10(process.cwd(), CONFIG_FILE);
4176
- if (existsSync7(configPath)) {
4351
+ const configPath = resolve11(process.cwd(), CONFIG_FILE);
4352
+ if (existsSync8(configPath)) {
4177
4353
  try {
4178
- const raw = readFileSync6(configPath, "utf-8");
4354
+ const raw = readFileSync7(configPath, "utf-8");
4179
4355
  const config = JSON.parse(raw);
4180
4356
  if (typeof config === "object" && config !== null && "tokens" in config && typeof config.tokens === "object" && config.tokens !== null && typeof config.tokens?.file === "string") {
4181
4357
  const file = config.tokens.file;
4182
- return resolve10(process.cwd(), file);
4358
+ return resolve11(process.cwd(), file);
4183
4359
  }
4184
4360
  } catch {
4185
4361
  }
4186
4362
  }
4187
- return resolve10(process.cwd(), DEFAULT_TOKEN_FILE);
4363
+ return resolve11(process.cwd(), DEFAULT_TOKEN_FILE);
4188
4364
  }
4189
4365
  function createTokensExportCommand() {
4190
4366
  return new Command6("export").description("Export design tokens to a downstream format").requiredOption("--format <fmt>", `Output format: ${SUPPORTED_FORMATS.join(", ")}`).option("--file <path>", "Path to token file (overrides config)").option("--out <path>", "Write output to file instead of stdout").option("--prefix <prefix>", "CSS/SCSS: prefix for variable names (e.g. 'scope')").option("--selector <selector>", "CSS: custom root selector (default: ':root')").option(
@@ -4202,14 +4378,14 @@ Supported formats: ${SUPPORTED_FORMATS.join(", ")}
4202
4378
  }
4203
4379
  const format = opts.format;
4204
4380
  try {
4205
- const filePath = resolveTokenFilePath(opts.file);
4206
- if (!existsSync7(filePath)) {
4381
+ const filePath = resolveTokenFilePath2(opts.file);
4382
+ if (!existsSync8(filePath)) {
4207
4383
  throw new Error(
4208
4384
  `Token file not found at ${filePath}.
4209
4385
  Create a reactscope.tokens.json file or use --file to specify a path.`
4210
4386
  );
4211
4387
  }
4212
- const raw = readFileSync6(filePath, "utf-8");
4388
+ const raw = readFileSync7(filePath, "utf-8");
4213
4389
  const { tokens, rawFile } = parseTokenFileSync(raw);
4214
4390
  let themesMap;
4215
4391
  if (opts.theme !== void 0) {
@@ -4220,7 +4396,7 @@ Create a reactscope.tokens.json file or use --file to specify a path.`
4220
4396
  Available themes: ${available}`
4221
4397
  );
4222
4398
  }
4223
- const baseResolver = new TokenResolver3(tokens);
4399
+ const baseResolver = new TokenResolver4(tokens);
4224
4400
  const themeResolver = ThemeResolver.fromTokenFile(
4225
4401
  baseResolver,
4226
4402
  rawFile
@@ -4248,7 +4424,7 @@ Available themes: ${themeNames.join(", ")}`
4248
4424
  themes: themesMap
4249
4425
  });
4250
4426
  if (opts.out !== void 0) {
4251
- const outPath = resolve10(process.cwd(), opts.out);
4427
+ const outPath = resolve11(process.cwd(), opts.out);
4252
4428
  writeFileSync7(outPath, output, "utf-8");
4253
4429
  process.stderr.write(`Exported ${tokens.length} tokens to ${outPath}
4254
4430
  `);
@@ -4267,6 +4443,324 @@ Available themes: ${themeNames.join(", ")}`
4267
4443
  );
4268
4444
  }
4269
4445
 
4446
+ // src/tokens/impact.ts
4447
+ import {
4448
+ ComplianceEngine as ComplianceEngine4,
4449
+ ImpactAnalyzer,
4450
+ TokenResolver as TokenResolver5
4451
+ } from "@agent-scope/tokens";
4452
+ var DEFAULT_STYLES_PATH2 = ".reactscope/compliance-styles.json";
4453
+ var SEVERITY_EMOJI = {
4454
+ none: "\u25CB",
4455
+ subtle: "\u25D4",
4456
+ moderate: "\u25D1",
4457
+ significant: "\u25CF"
4458
+ };
4459
+ function formatImpactReport(report) {
4460
+ const lines = [];
4461
+ const newValueSuffix = report.newValue !== report.oldValue ? ` \u2192 ${report.newValue}` : "";
4462
+ lines.push(`Token: ${report.tokenPath} (${report.oldValue})${newValueSuffix}`);
4463
+ if (report.components.length === 0) {
4464
+ lines.push("");
4465
+ lines.push("No components reference this token.");
4466
+ return lines.join("\n");
4467
+ }
4468
+ lines.push("");
4469
+ const nameWidth = Math.max(9, ...report.components.map((c) => c.name.length));
4470
+ const propWidth = Math.max(
4471
+ 8,
4472
+ ...report.components.flatMap((c) => c.affectedProperties.map((p) => p.length))
4473
+ );
4474
+ for (const comp of report.components) {
4475
+ for (const property of comp.affectedProperties) {
4476
+ const name = comp.name.padEnd(nameWidth);
4477
+ const prop = property.padEnd(propWidth);
4478
+ const severityIcon2 = SEVERITY_EMOJI[comp.severity] ?? "?";
4479
+ lines.push(` ${name} ${prop} ${severityIcon2} ${comp.severity}`);
4480
+ }
4481
+ }
4482
+ lines.push("");
4483
+ const countLabel = `${report.affectedComponentCount} component${report.affectedComponentCount !== 1 ? "s" : ""}`;
4484
+ const severityIcon = SEVERITY_EMOJI[report.overallSeverity] ?? "?";
4485
+ lines.push(
4486
+ `${countLabel} affected \u2014 overall severity: ${severityIcon} ${report.overallSeverity}`
4487
+ );
4488
+ if (report.colorDelta !== void 0) {
4489
+ lines.push(`Color delta: \u0394E ${report.colorDelta.toFixed(2)}`);
4490
+ }
4491
+ return lines.join("\n");
4492
+ }
4493
+ function formatImpactSummary(report) {
4494
+ if (report.components.length === 0) {
4495
+ return `No components reference token "${report.tokenPath}".`;
4496
+ }
4497
+ const parts = report.components.map(
4498
+ (c) => `${c.name} (${c.affectedProperties.length} element${c.affectedProperties.length !== 1 ? "s" : ""})`
4499
+ );
4500
+ return `\u2192 ${parts.join(", ")}`;
4501
+ }
4502
+ function registerImpact(tokensCmd) {
4503
+ tokensCmd.command("impact <path>").description("List all components and elements that consume a given token (Token Spec \xA74.3)").option("--file <path>", "Path to token file (overrides config)").option("--styles <path>", `Path to compliance styles JSON (default: ${DEFAULT_STYLES_PATH2})`).option("--new-value <value>", "Proposed new value \u2014 report visual severity of the change").option("--format <fmt>", "Output format: json or text (default: auto-detect)").action(
4504
+ (tokenPath, opts) => {
4505
+ try {
4506
+ const tokenFilePath = resolveTokenFilePath(opts.file);
4507
+ const { tokens } = loadTokens(tokenFilePath);
4508
+ const resolver = new TokenResolver5(tokens);
4509
+ const engine = new ComplianceEngine4(resolver);
4510
+ const stylesPath = opts.styles ?? DEFAULT_STYLES_PATH2;
4511
+ const stylesFile = loadStylesFile(stylesPath);
4512
+ const componentMap = new Map(Object.entries(stylesFile));
4513
+ const batchReport = engine.auditBatch(componentMap);
4514
+ const complianceReports = new Map(Object.entries(batchReport.components));
4515
+ const analyzer = new ImpactAnalyzer(resolver, complianceReports);
4516
+ const currentValue = resolver.resolve(tokenPath);
4517
+ const newValue = opts.newValue ?? currentValue;
4518
+ const report = analyzer.impactOf(tokenPath, newValue);
4519
+ const useJson = opts.format === "json" || opts.format !== "text" && !isTTY();
4520
+ if (useJson) {
4521
+ process.stdout.write(`${JSON.stringify(report, null, 2)}
4522
+ `);
4523
+ } else {
4524
+ process.stdout.write(`${formatImpactReport(report)}
4525
+ `);
4526
+ if (isTTY()) {
4527
+ process.stdout.write(`
4528
+ ${formatImpactSummary(report)}
4529
+ `);
4530
+ }
4531
+ }
4532
+ } catch (err) {
4533
+ process.stderr.write(`Error: ${err instanceof Error ? err.message : String(err)}
4534
+ `);
4535
+ process.exit(1);
4536
+ }
4537
+ }
4538
+ );
4539
+ }
4540
+
4541
+ // src/tokens/preview.ts
4542
+ import { mkdirSync as mkdirSync5, writeFileSync as writeFileSync8 } from "fs";
4543
+ import { resolve as resolve12 } from "path";
4544
+ import { BrowserPool as BrowserPool6, SpriteSheetGenerator } from "@agent-scope/render";
4545
+ import { ComplianceEngine as ComplianceEngine5, ImpactAnalyzer as ImpactAnalyzer2, TokenResolver as TokenResolver6 } from "@agent-scope/tokens";
4546
+ var DEFAULT_STYLES_PATH3 = ".reactscope/compliance-styles.json";
4547
+ var DEFAULT_MANIFEST_PATH = ".reactscope/manifest.json";
4548
+ var DEFAULT_OUTPUT_DIR2 = ".reactscope/previews";
4549
+ async function renderComponentWithCssOverride(filePath, componentName, cssOverride, vpWidth, vpHeight, timeoutMs) {
4550
+ const htmlHarness = await buildComponentHarness(
4551
+ filePath,
4552
+ componentName,
4553
+ {},
4554
+ // no props
4555
+ vpWidth,
4556
+ cssOverride
4557
+ // injected as <style>
4558
+ );
4559
+ const pool = new BrowserPool6({
4560
+ size: { browsers: 1, pagesPerBrowser: 1 },
4561
+ viewportWidth: vpWidth,
4562
+ viewportHeight: vpHeight
4563
+ });
4564
+ await pool.init();
4565
+ const slot = await pool.acquire();
4566
+ const { page } = slot;
4567
+ try {
4568
+ await page.setContent(htmlHarness, { waitUntil: "load" });
4569
+ await page.waitForFunction(
4570
+ () => {
4571
+ const w = window;
4572
+ return w.__SCOPE_RENDER_COMPLETE__ === true;
4573
+ },
4574
+ { timeout: timeoutMs }
4575
+ );
4576
+ const rootLocator = page.locator("[data-reactscope-root]");
4577
+ const bb = await rootLocator.boundingBox();
4578
+ const PAD = 16;
4579
+ const MIN_W = 320;
4580
+ const MIN_H = 120;
4581
+ const clipX = Math.max(0, (bb?.x ?? 0) - PAD);
4582
+ const clipY = Math.max(0, (bb?.y ?? 0) - PAD);
4583
+ const rawW = (bb?.width ?? MIN_W) + PAD * 2;
4584
+ const rawH = (bb?.height ?? MIN_H) + PAD * 2;
4585
+ const clipW = Math.min(Math.max(rawW, MIN_W), vpWidth - clipX);
4586
+ const clipH = Math.min(Math.max(rawH, MIN_H), vpHeight - clipY);
4587
+ const screenshot = await page.screenshot({
4588
+ clip: { x: clipX, y: clipY, width: clipW, height: clipH },
4589
+ type: "png"
4590
+ });
4591
+ return { screenshot, width: Math.round(clipW), height: Math.round(clipH) };
4592
+ } finally {
4593
+ pool.release(slot);
4594
+ await pool.close().catch(() => void 0);
4595
+ }
4596
+ }
4597
+ function registerPreview(tokensCmd) {
4598
+ tokensCmd.command("preview <path>").description("Render before/after sprite sheet for components affected by a token change").requiredOption("--new-value <value>", "The proposed new resolved value for the token").option("--sprite", "Output a PNG sprite sheet (default when TTY)", false).option("-o, --output <path>", "Output PNG path (default: .reactscope/previews/<token>.png)").option("--file <path>", "Path to token file (overrides config)").option("--styles <path>", `Path to compliance styles JSON (default: ${DEFAULT_STYLES_PATH3})`).option("--manifest <path>", "Path to manifest.json", DEFAULT_MANIFEST_PATH).option("--format <fmt>", "Output format: json or text (default: auto-detect)").option("--timeout <ms>", "Browser timeout per render (ms)", "10000").option("--viewport-width <px>", "Viewport width in pixels", "1280").option("--viewport-height <px>", "Viewport height in pixels", "720").action(
4599
+ async (tokenPath, opts) => {
4600
+ try {
4601
+ const tokenFilePath = resolveTokenFilePath(opts.file);
4602
+ const { tokens } = loadTokens(tokenFilePath);
4603
+ const resolver = new TokenResolver6(tokens);
4604
+ const engine = new ComplianceEngine5(resolver);
4605
+ const stylesPath = opts.styles ?? DEFAULT_STYLES_PATH3;
4606
+ const stylesFile = loadStylesFile(stylesPath);
4607
+ const componentMap = new Map(Object.entries(stylesFile));
4608
+ const batchReport = engine.auditBatch(componentMap);
4609
+ const complianceReports = new Map(Object.entries(batchReport.components));
4610
+ const analyzer = new ImpactAnalyzer2(resolver, complianceReports);
4611
+ const currentValue = resolver.resolve(tokenPath);
4612
+ const impactReport = analyzer.impactOf(tokenPath, opts.newValue);
4613
+ if (impactReport.components.length === 0) {
4614
+ process.stdout.write(
4615
+ `No components reference token "${tokenPath}". Nothing to preview.
4616
+ `
4617
+ );
4618
+ return;
4619
+ }
4620
+ const affectedNames = impactReport.components.map((c) => c.name);
4621
+ process.stderr.write(
4622
+ `Rendering ${affectedNames.length} component(s): ${affectedNames.join(", ")}
4623
+ `
4624
+ );
4625
+ const manifest = loadManifest(opts.manifest);
4626
+ const vpWidth = Number.parseInt(opts.viewportWidth, 10);
4627
+ const vpHeight = Number.parseInt(opts.viewportHeight, 10);
4628
+ const timeout = Number.parseInt(opts.timeout, 10);
4629
+ const tokenCssVar = `--token-${tokenPath.replace(/\./g, "-")}`;
4630
+ const beforeCss = `:root { ${tokenCssVar}: ${currentValue}; }`;
4631
+ const afterCss = `:root { ${tokenCssVar}: ${opts.newValue}; }`;
4632
+ const renders = [];
4633
+ for (const componentName of affectedNames) {
4634
+ const descriptor = manifest.components[componentName];
4635
+ if (descriptor === void 0) {
4636
+ process.stderr.write(
4637
+ `Warning: "${componentName}" not found in manifest \u2014 skipping
4638
+ `
4639
+ );
4640
+ continue;
4641
+ }
4642
+ process.stderr.write(` Rendering ${componentName} (before)...
4643
+ `);
4644
+ const before = await renderComponentWithCssOverride(
4645
+ descriptor.filePath,
4646
+ componentName,
4647
+ beforeCss,
4648
+ vpWidth,
4649
+ vpHeight,
4650
+ timeout
4651
+ );
4652
+ process.stderr.write(` Rendering ${componentName} (after)...
4653
+ `);
4654
+ const after = await renderComponentWithCssOverride(
4655
+ descriptor.filePath,
4656
+ componentName,
4657
+ afterCss,
4658
+ vpWidth,
4659
+ vpHeight,
4660
+ timeout
4661
+ );
4662
+ renders.push({ name: componentName, before, after });
4663
+ }
4664
+ if (renders.length === 0) {
4665
+ process.stderr.write(
4666
+ "Warning: No components could be rendered (all missing from manifest).\n"
4667
+ );
4668
+ return;
4669
+ }
4670
+ const cellW = Math.max(...renders.flatMap((r) => [r.before.width, r.after.width]));
4671
+ const cellH = Math.max(...renders.flatMap((r) => [r.before.height, r.after.height]));
4672
+ const cells = renders.flatMap((r, colIdx) => [
4673
+ {
4674
+ props: { version: "before", component: r.name },
4675
+ result: {
4676
+ screenshot: r.before.screenshot,
4677
+ width: cellW,
4678
+ height: cellH,
4679
+ renderTimeMs: 0,
4680
+ computedStyles: {}
4681
+ },
4682
+ index: colIdx * 2,
4683
+ axisIndices: [0, colIdx]
4684
+ },
4685
+ {
4686
+ props: { version: "after", component: r.name },
4687
+ result: {
4688
+ screenshot: r.after.screenshot,
4689
+ width: cellW,
4690
+ height: cellH,
4691
+ renderTimeMs: 0,
4692
+ computedStyles: {}
4693
+ },
4694
+ index: colIdx * 2 + 1,
4695
+ axisIndices: [1, colIdx]
4696
+ }
4697
+ ]);
4698
+ const matrixResult = {
4699
+ cells,
4700
+ axes: [
4701
+ { name: "component", values: renders.map((r) => r.name) },
4702
+ { name: "version", values: ["before", "after"] }
4703
+ ],
4704
+ axisLabels: [renders.map((r) => r.name), ["before", "after"]],
4705
+ rows: 2,
4706
+ cols: renders.length,
4707
+ stats: {
4708
+ totalCells: cells.length,
4709
+ totalRenderTimeMs: 0,
4710
+ avgRenderTimeMs: 0,
4711
+ minRenderTimeMs: 0,
4712
+ maxRenderTimeMs: 0,
4713
+ wallClockTimeMs: 0
4714
+ }
4715
+ };
4716
+ const generator = new SpriteSheetGenerator({
4717
+ cellPadding: 8,
4718
+ borderWidth: 1,
4719
+ labelHeight: 32,
4720
+ labelWidth: 120
4721
+ });
4722
+ const spriteResult = await generator.generate(matrixResult);
4723
+ const tokenLabel = tokenPath.replace(/\./g, "-");
4724
+ const outputPath = opts.output ?? resolve12(process.cwd(), DEFAULT_OUTPUT_DIR2, `preview-${tokenLabel}.png`);
4725
+ const outputDir = resolve12(outputPath, "..");
4726
+ mkdirSync5(outputDir, { recursive: true });
4727
+ writeFileSync8(outputPath, spriteResult.png);
4728
+ const useJson = opts.format === "json" || opts.format !== "text" && !isTTY();
4729
+ if (useJson) {
4730
+ process.stdout.write(
4731
+ `${JSON.stringify(
4732
+ {
4733
+ tokenPath,
4734
+ oldValue: currentValue,
4735
+ newValue: opts.newValue,
4736
+ outputPath,
4737
+ width: spriteResult.width,
4738
+ height: spriteResult.height,
4739
+ components: renders.map((r) => r.name),
4740
+ cells: spriteResult.coordinates.length
4741
+ },
4742
+ null,
4743
+ 2
4744
+ )}
4745
+ `
4746
+ );
4747
+ } else {
4748
+ process.stdout.write(
4749
+ `Preview written to ${outputPath} (${spriteResult.width}\xD7${spriteResult.height}px)
4750
+ `
4751
+ );
4752
+ process.stdout.write(`Components: ${renders.map((r) => r.name).join(", ")}
4753
+ `);
4754
+ }
4755
+ } catch (err) {
4756
+ process.stderr.write(`Error: ${err instanceof Error ? err.message : String(err)}
4757
+ `);
4758
+ process.exit(1);
4759
+ }
4760
+ }
4761
+ );
4762
+ }
4763
+
4270
4764
  // src/tokens/commands.ts
4271
4765
  var DEFAULT_TOKEN_FILE2 = "reactscope.tokens.json";
4272
4766
  var CONFIG_FILE2 = "reactscope.config.json";
@@ -4287,32 +4781,32 @@ function buildTable2(headers, rows) {
4287
4781
  );
4288
4782
  return [headerRow, divider, ...dataRows].join("\n");
4289
4783
  }
4290
- function resolveTokenFilePath2(fileFlag) {
4784
+ function resolveTokenFilePath(fileFlag) {
4291
4785
  if (fileFlag !== void 0) {
4292
- return resolve11(process.cwd(), fileFlag);
4786
+ return resolve13(process.cwd(), fileFlag);
4293
4787
  }
4294
- const configPath = resolve11(process.cwd(), CONFIG_FILE2);
4295
- if (existsSync8(configPath)) {
4788
+ const configPath = resolve13(process.cwd(), CONFIG_FILE2);
4789
+ if (existsSync9(configPath)) {
4296
4790
  try {
4297
- const raw = readFileSync7(configPath, "utf-8");
4791
+ const raw = readFileSync8(configPath, "utf-8");
4298
4792
  const config = JSON.parse(raw);
4299
4793
  if (typeof config === "object" && config !== null && "tokens" in config && typeof config.tokens === "object" && config.tokens !== null && typeof config.tokens?.file === "string") {
4300
4794
  const file = config.tokens.file;
4301
- return resolve11(process.cwd(), file);
4795
+ return resolve13(process.cwd(), file);
4302
4796
  }
4303
4797
  } catch {
4304
4798
  }
4305
4799
  }
4306
- return resolve11(process.cwd(), DEFAULT_TOKEN_FILE2);
4800
+ return resolve13(process.cwd(), DEFAULT_TOKEN_FILE2);
4307
4801
  }
4308
4802
  function loadTokens(absPath) {
4309
- if (!existsSync8(absPath)) {
4803
+ if (!existsSync9(absPath)) {
4310
4804
  throw new Error(
4311
4805
  `Token file not found at ${absPath}.
4312
4806
  Create a reactscope.tokens.json file or use --file to specify a path.`
4313
4807
  );
4314
4808
  }
4315
- const raw = readFileSync7(absPath, "utf-8");
4809
+ const raw = readFileSync8(absPath, "utf-8");
4316
4810
  return parseTokenFileSync2(raw);
4317
4811
  }
4318
4812
  function getRawValue(node, segments) {
@@ -4350,9 +4844,9 @@ function buildResolutionChain(startPath, rawTokens) {
4350
4844
  function registerGet2(tokensCmd) {
4351
4845
  tokensCmd.command("get <path>").description("Resolve a token path to its computed value").option("--file <path>", "Path to token file (overrides config)").option("--format <fmt>", "Output format: json or text (default: auto-detect)").action((tokenPath, opts) => {
4352
4846
  try {
4353
- const filePath = resolveTokenFilePath2(opts.file);
4847
+ const filePath = resolveTokenFilePath(opts.file);
4354
4848
  const { tokens } = loadTokens(filePath);
4355
- const resolver = new TokenResolver4(tokens);
4849
+ const resolver = new TokenResolver7(tokens);
4356
4850
  const resolvedValue = resolver.resolve(tokenPath);
4357
4851
  const useJson = opts.format === "json" || opts.format !== "text" && !isTTY2();
4358
4852
  if (useJson) {
@@ -4376,9 +4870,9 @@ function registerList2(tokensCmd) {
4376
4870
  tokensCmd.command("list [category]").description("List tokens, optionally filtered by category or type").option("--type <type>", "Filter by token type (color, dimension, fontFamily, etc.)").option("--file <path>", "Path to token file (overrides config)").option("--format <fmt>", "Output format: json or table (default: auto-detect)").action(
4377
4871
  (category, opts) => {
4378
4872
  try {
4379
- const filePath = resolveTokenFilePath2(opts.file);
4873
+ const filePath = resolveTokenFilePath(opts.file);
4380
4874
  const { tokens } = loadTokens(filePath);
4381
- const resolver = new TokenResolver4(tokens);
4875
+ const resolver = new TokenResolver7(tokens);
4382
4876
  const filtered = resolver.list(opts.type, category);
4383
4877
  const useJson = opts.format === "json" || opts.format !== "table" && !isTTY2();
4384
4878
  if (useJson) {
@@ -4406,9 +4900,9 @@ function registerSearch(tokensCmd) {
4406
4900
  tokensCmd.command("search <value>").description("Find which token(s) match a computed value (supports fuzzy color matching)").option("--type <type>", "Restrict search to a specific token type").option("--fuzzy", "Return nearest match even if no exact match exists", false).option("--file <path>", "Path to token file (overrides config)").option("--format <fmt>", "Output format: json or table (default: auto-detect)").action(
4407
4901
  (value, opts) => {
4408
4902
  try {
4409
- const filePath = resolveTokenFilePath2(opts.file);
4903
+ const filePath = resolveTokenFilePath(opts.file);
4410
4904
  const { tokens } = loadTokens(filePath);
4411
- const resolver = new TokenResolver4(tokens);
4905
+ const resolver = new TokenResolver7(tokens);
4412
4906
  const useJson = opts.format === "json" || opts.format !== "table" && !isTTY2();
4413
4907
  const typesToSearch = opts.type ? [opts.type] : [
4414
4908
  "color",
@@ -4488,10 +4982,10 @@ Tip: use --fuzzy for nearest-match search.
4488
4982
  function registerResolve(tokensCmd) {
4489
4983
  tokensCmd.command("resolve <path>").description("Show the full resolution chain for a token").option("--file <path>", "Path to token file (overrides config)").option("--format <fmt>", "Output format: json or text (default: auto-detect)").action((tokenPath, opts) => {
4490
4984
  try {
4491
- const filePath = resolveTokenFilePath2(opts.file);
4985
+ const filePath = resolveTokenFilePath(opts.file);
4492
4986
  const absFilePath = filePath;
4493
4987
  const { tokens, rawFile } = loadTokens(absFilePath);
4494
- const resolver = new TokenResolver4(tokens);
4988
+ const resolver = new TokenResolver7(tokens);
4495
4989
  resolver.resolve(tokenPath);
4496
4990
  const chain = buildResolutionChain(tokenPath, rawFile.tokens);
4497
4991
  const useJson = opts.format === "json" || opts.format !== "text" && !isTTY2();
@@ -4525,14 +5019,14 @@ function registerValidate(tokensCmd) {
4525
5019
  "Validate the token file for errors (circular refs, missing refs, type mismatches)"
4526
5020
  ).option("--file <path>", "Path to token file (overrides config)").option("--format <fmt>", "Output format: json or text (default: auto-detect)").action((opts) => {
4527
5021
  try {
4528
- const filePath = resolveTokenFilePath2(opts.file);
4529
- if (!existsSync8(filePath)) {
5022
+ const filePath = resolveTokenFilePath(opts.file);
5023
+ if (!existsSync9(filePath)) {
4530
5024
  throw new Error(
4531
5025
  `Token file not found at ${filePath}.
4532
5026
  Create a reactscope.tokens.json file or use --file to specify a path.`
4533
5027
  );
4534
5028
  }
4535
- const raw = readFileSync7(filePath, "utf-8");
5029
+ const raw = readFileSync8(filePath, "utf-8");
4536
5030
  const useJson = opts.format === "json" || opts.format !== "text" && !isTTY2();
4537
5031
  const errors = [];
4538
5032
  let parsed;
@@ -4609,6 +5103,9 @@ function createTokensCommand() {
4609
5103
  registerResolve(tokensCmd);
4610
5104
  registerValidate(tokensCmd);
4611
5105
  tokensCmd.addCommand(createTokensExportCommand());
5106
+ registerCompliance(tokensCmd);
5107
+ registerImpact(tokensCmd);
5108
+ registerPreview(tokensCmd);
4612
5109
  return tokensCmd;
4613
5110
  }
4614
5111
 
@@ -4687,7 +5184,7 @@ function createProgram(options = {}) {
4687
5184
  }
4688
5185
  );
4689
5186
  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) => {
4690
- const raw = readFileSync8(tracePath, "utf-8");
5187
+ const raw = readFileSync9(tracePath, "utf-8");
4691
5188
  const trace = loadTrace(raw);
4692
5189
  const source = generateTest(trace, {
4693
5190
  description: opts.description,