@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/index.js CHANGED
@@ -2,13 +2,13 @@ import { existsSync, readFileSync, writeFileSync, mkdirSync, appendFileSync, rea
2
2
  import { join, resolve, dirname } from 'path';
3
3
  import * as readline from 'readline';
4
4
  import { Command } from 'commander';
5
- import { safeRender, ALL_CONTEXT_IDS, contextAxis, stressAxis, ALL_STRESS_IDS, RenderMatrix, BrowserPool, SatoriRenderer } from '@agent-scope/render';
5
+ import { SpriteSheetGenerator, BrowserPool, safeRender, ALL_CONTEXT_IDS, contextAxis, stressAxis, ALL_STRESS_IDS, RenderMatrix, SatoriRenderer } from '@agent-scope/render';
6
6
  import * as esbuild from 'esbuild';
7
7
  import { generateManifest } from '@agent-scope/manifest';
8
8
  import { chromium } from 'playwright';
9
9
  import { loadTrace, generateTest, getBrowserEntryScript } from '@agent-scope/playwright';
10
10
  import { createRequire } from 'module';
11
- import { parseTokenFileSync, TokenResolver, ThemeResolver, exportTokens, validateTokenFile, TokenValidationError, TokenParseError, ComplianceEngine } from '@agent-scope/tokens';
11
+ import { parseTokenFileSync, TokenResolver, ThemeResolver, exportTokens, validateTokenFile, TokenValidationError, TokenParseError, ComplianceEngine, ImpactAnalyzer } from '@agent-scope/tokens';
12
12
 
13
13
  // src/init/index.ts
14
14
  function hasConfigFile(dir, stem) {
@@ -205,9 +205,9 @@ function createRL() {
205
205
  });
206
206
  }
207
207
  async function ask(rl, question) {
208
- return new Promise((resolve12) => {
208
+ return new Promise((resolve14) => {
209
209
  rl.question(question, (answer) => {
210
- resolve12(answer.trim());
210
+ resolve14(answer.trim());
211
211
  });
212
212
  });
213
213
  }
@@ -356,7 +356,7 @@ function createInitCommand() {
356
356
  }
357
357
  async function buildComponentHarness(filePath, componentName, props, viewportWidth, projectCss) {
358
358
  const bundledScript = await bundleComponentToIIFE(filePath, componentName, props);
359
- return wrapInHtml(bundledScript, viewportWidth);
359
+ return wrapInHtml(bundledScript, viewportWidth, projectCss);
360
360
  }
361
361
  async function bundleComponentToIIFE(filePath, componentName, props) {
362
362
  const propsJson = JSON.stringify(props).replace(/<\/script>/gi, "<\\/script>");
@@ -443,7 +443,9 @@ ${msg}`);
443
443
  return outputFile.text;
444
444
  }
445
445
  function wrapInHtml(bundledScript, viewportWidth, projectCss) {
446
- const projectStyleBlock = "";
446
+ const projectStyleBlock = projectCss != null && projectCss.length > 0 ? `<style id="scope-project-css">
447
+ ${projectCss.replace(/<\/style>/gi, "<\\/style>")}
448
+ </style>` : "";
447
449
  return `<!DOCTYPE html>
448
450
  <html lang="en">
449
451
  <head>
@@ -2807,8 +2809,8 @@ Available: ${available}`
2807
2809
  `
2808
2810
  );
2809
2811
  if (opts.sprite !== void 0) {
2810
- const { SpriteSheetGenerator } = await import('@agent-scope/render');
2811
- const gen = new SpriteSheetGenerator();
2812
+ const { SpriteSheetGenerator: SpriteSheetGenerator2 } = await import('@agent-scope/render');
2813
+ const gen = new SpriteSheetGenerator2();
2812
2814
  const sheet = await gen.generate(result);
2813
2815
  const spritePath = resolve(process.cwd(), opts.sprite);
2814
2816
  writeFileSync(spritePath, sheet.png);
@@ -2817,8 +2819,8 @@ Available: ${available}`
2817
2819
  }
2818
2820
  const fmt = resolveMatrixFormat(opts.format, opts.sprite !== void 0);
2819
2821
  if (fmt === "file") {
2820
- const { SpriteSheetGenerator } = await import('@agent-scope/render');
2821
- const gen = new SpriteSheetGenerator();
2822
+ const { SpriteSheetGenerator: SpriteSheetGenerator2 } = await import('@agent-scope/render');
2823
+ const gen = new SpriteSheetGenerator2();
2822
2824
  const sheet = await gen.generate(result);
2823
2825
  const dir = resolve(process.cwd(), DEFAULT_OUTPUT_DIR);
2824
2826
  mkdirSync(dir, { recursive: true });
@@ -2835,8 +2837,8 @@ Available: ${available}`
2835
2837
  } else if (fmt === "png") {
2836
2838
  if (opts.sprite !== void 0) {
2837
2839
  } else {
2838
- const { SpriteSheetGenerator } = await import('@agent-scope/render');
2839
- const gen = new SpriteSheetGenerator();
2840
+ const { SpriteSheetGenerator: SpriteSheetGenerator2 } = await import('@agent-scope/render');
2841
+ const gen = new SpriteSheetGenerator2();
2840
2842
  const sheet = await gen.generate(result);
2841
2843
  process.stdout.write(sheet.png);
2842
2844
  }
@@ -3143,12 +3145,12 @@ async function runBaseline(options = {}) {
3143
3145
  mkdirSync(rendersDir, { recursive: true });
3144
3146
  let manifest;
3145
3147
  if (manifestPath !== void 0) {
3146
- const { readFileSync: readFileSync9 } = await import('fs');
3148
+ const { readFileSync: readFileSync10 } = await import('fs');
3147
3149
  const absPath = resolve(rootDir, manifestPath);
3148
3150
  if (!existsSync(absPath)) {
3149
3151
  throw new Error(`Manifest not found at ${absPath}.`);
3150
3152
  }
3151
- manifest = JSON.parse(readFileSync9(absPath, "utf-8"));
3153
+ manifest = JSON.parse(readFileSync10(absPath, "utf-8"));
3152
3154
  process.stderr.write(`Loaded manifest from ${manifestPath}
3153
3155
  `);
3154
3156
  } else {
@@ -4060,10 +4062,178 @@ function buildStructuredReport(report) {
4060
4062
  route: report.route?.pattern ?? null
4061
4063
  };
4062
4064
  }
4065
+ var DEFAULT_STYLES_PATH = ".reactscope/compliance-styles.json";
4066
+ function loadStylesFile(stylesPath) {
4067
+ const absPath = resolve(process.cwd(), stylesPath);
4068
+ if (!existsSync(absPath)) {
4069
+ throw new Error(
4070
+ `Compliance styles file not found at ${absPath}.
4071
+ Run \`scope render all\` first to generate component styles, or use --styles to specify a path.
4072
+ Expected format: { "ComponentName": { colors: {}, spacing: {}, typography: {}, borders: {}, shadows: {} } }`
4073
+ );
4074
+ }
4075
+ const raw = readFileSync(absPath, "utf-8");
4076
+ let parsed;
4077
+ try {
4078
+ parsed = JSON.parse(raw);
4079
+ } catch (err) {
4080
+ throw new Error(`Failed to parse compliance styles file as JSON: ${String(err)}`);
4081
+ }
4082
+ if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
4083
+ throw new Error(
4084
+ `Compliance styles file must be a JSON object mapping component names to ComputedStyles.`
4085
+ );
4086
+ }
4087
+ return parsed;
4088
+ }
4089
+ function categoryForProperty(property) {
4090
+ const lower = property.toLowerCase();
4091
+ if (lower.includes("shadow")) return "shadow";
4092
+ if (lower.includes("color") || lower === "background" || lower === "fill" || lower === "stroke")
4093
+ return "color";
4094
+ if (lower.includes("padding") || lower.includes("margin") || lower === "gap" || lower === "width" || lower === "height" || lower === "top" || lower === "right" || lower === "bottom" || lower === "left")
4095
+ return "spacing";
4096
+ if (lower.includes("border")) return "border";
4097
+ if (lower.includes("font") || lower.includes("line") || lower.includes("letter") || lower === "texttransform" || lower === "textdecoration")
4098
+ return "typography";
4099
+ return "spacing";
4100
+ }
4101
+ function buildCategorySummary(batch) {
4102
+ const cats = {
4103
+ color: { total: 0, onSystem: 0, offSystem: 0, compliance: 1 },
4104
+ spacing: { total: 0, onSystem: 0, offSystem: 0, compliance: 1 },
4105
+ typography: { total: 0, onSystem: 0, offSystem: 0, compliance: 1 },
4106
+ border: { total: 0, onSystem: 0, offSystem: 0, compliance: 1 },
4107
+ shadow: { total: 0, onSystem: 0, offSystem: 0, compliance: 1 }
4108
+ };
4109
+ for (const report of Object.values(batch.components)) {
4110
+ for (const [property, result] of Object.entries(report.properties)) {
4111
+ const cat = categoryForProperty(property);
4112
+ const summary = cats[cat];
4113
+ if (summary === void 0) continue;
4114
+ summary.total++;
4115
+ if (result.status === "on_system") {
4116
+ summary.onSystem++;
4117
+ } else {
4118
+ summary.offSystem++;
4119
+ }
4120
+ }
4121
+ }
4122
+ for (const summary of Object.values(cats)) {
4123
+ summary.compliance = summary.total === 0 ? 1 : summary.onSystem / summary.total;
4124
+ }
4125
+ return cats;
4126
+ }
4127
+ function collectOffenders(batch, limit = 10) {
4128
+ const offenders = [];
4129
+ const componentEntries = Object.entries(batch.components).map(([name, report]) => ({
4130
+ name,
4131
+ report,
4132
+ offSystemCount: report.offSystem
4133
+ }));
4134
+ componentEntries.sort((a, b) => b.offSystemCount - a.offSystemCount);
4135
+ for (const { name, report, offSystemCount } of componentEntries) {
4136
+ if (offSystemCount === 0) continue;
4137
+ for (const [property, result] of Object.entries(report.properties)) {
4138
+ if (result.status !== "OFF_SYSTEM") continue;
4139
+ offenders.push({
4140
+ component: name,
4141
+ property,
4142
+ value: result.value,
4143
+ nearestToken: result.nearest?.token ?? "\u2014",
4144
+ nearestValue: result.nearest?.value ?? "\u2014",
4145
+ offSystemCount
4146
+ });
4147
+ if (offenders.length >= limit) break;
4148
+ }
4149
+ if (offenders.length >= limit) break;
4150
+ }
4151
+ return offenders;
4152
+ }
4153
+ function formatPct(n) {
4154
+ return `${Math.round(n * 100)}%`;
4155
+ }
4156
+ function truncate(s, max) {
4157
+ return s.length > max ? `${s.slice(0, max - 1)}\u2026` : s;
4158
+ }
4159
+ function formatComplianceReport(batch, threshold) {
4160
+ const pct = Math.round(batch.aggregateCompliance * 100);
4161
+ const lines = [];
4162
+ const thresholdLabel = threshold !== void 0 ? pct >= threshold ? " \u2713 (pass)" : ` \u2717 (below threshold ${threshold}%)` : "";
4163
+ lines.push(`Overall compliance score: ${pct}%${thresholdLabel}`);
4164
+ lines.push("");
4165
+ const cats = buildCategorySummary(batch);
4166
+ const catEntries = Object.entries(cats).filter(([, s]) => s.total > 0);
4167
+ if (catEntries.length > 0) {
4168
+ lines.push("By category:");
4169
+ const catWidth = Math.max(...catEntries.map(([k]) => k.length));
4170
+ for (const [cat, summary] of catEntries) {
4171
+ const label = cat.padEnd(catWidth);
4172
+ lines.push(
4173
+ ` ${label} ${formatPct(summary.compliance).padStart(4)} (${summary.offSystem} off-system value${summary.offSystem !== 1 ? "s" : ""})`
4174
+ );
4175
+ }
4176
+ lines.push("");
4177
+ }
4178
+ const offenders = collectOffenders(batch);
4179
+ if (offenders.length > 0) {
4180
+ lines.push("Top off-system offenders (sorted by count):");
4181
+ const nameWidth = Math.max(9, ...offenders.map((o) => o.component.length));
4182
+ const propWidth = Math.max(8, ...offenders.map((o) => o.property.length));
4183
+ const valWidth = Math.max(5, ...offenders.map((o) => truncate(o.value, 40).length));
4184
+ for (const offender of offenders) {
4185
+ const name = offender.component.padEnd(nameWidth);
4186
+ const prop = offender.property.padEnd(propWidth);
4187
+ const val = truncate(offender.value, 40).padEnd(valWidth);
4188
+ const nearest = `${offender.nearestToken} (${truncate(offender.nearestValue, 30)})`;
4189
+ lines.push(` ${name} ${prop}: ${val} \u2192 nearest: ${nearest}`);
4190
+ }
4191
+ } else {
4192
+ lines.push("No off-system values detected. \u{1F389}");
4193
+ }
4194
+ return lines.join("\n");
4195
+ }
4196
+ function registerCompliance(tokensCmd) {
4197
+ 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) => {
4198
+ try {
4199
+ const tokenFilePath = resolveTokenFilePath(opts.file);
4200
+ const { tokens } = loadTokens(tokenFilePath);
4201
+ const resolver = new TokenResolver(tokens);
4202
+ const engine = new ComplianceEngine(resolver);
4203
+ const stylesPath = opts.styles ?? DEFAULT_STYLES_PATH;
4204
+ const stylesFile = loadStylesFile(stylesPath);
4205
+ const componentMap = /* @__PURE__ */ new Map();
4206
+ for (const [name, styles] of Object.entries(stylesFile)) {
4207
+ componentMap.set(name, styles);
4208
+ }
4209
+ if (componentMap.size === 0) {
4210
+ process.stderr.write(`Warning: No components found in styles file at ${stylesPath}
4211
+ `);
4212
+ }
4213
+ const batch = engine.auditBatch(componentMap);
4214
+ const useJson = opts.format === "json" || opts.format !== "text" && !isTTY();
4215
+ const threshold = opts.threshold !== void 0 ? Number.parseInt(opts.threshold, 10) : void 0;
4216
+ if (useJson) {
4217
+ process.stdout.write(`${JSON.stringify(batch, null, 2)}
4218
+ `);
4219
+ } else {
4220
+ process.stdout.write(`${formatComplianceReport(batch, threshold)}
4221
+ `);
4222
+ }
4223
+ if (threshold !== void 0 && Math.round(batch.aggregateCompliance * 100) < threshold) {
4224
+ process.exit(1);
4225
+ }
4226
+ } catch (err) {
4227
+ process.stderr.write(`Error: ${err instanceof Error ? err.message : String(err)}
4228
+ `);
4229
+ process.exit(1);
4230
+ }
4231
+ });
4232
+ }
4063
4233
  var DEFAULT_TOKEN_FILE = "reactscope.tokens.json";
4064
4234
  var CONFIG_FILE = "reactscope.config.json";
4065
4235
  var SUPPORTED_FORMATS = ["css", "ts", "scss", "tailwind", "flat-json", "figma"];
4066
- function resolveTokenFilePath(fileFlag) {
4236
+ function resolveTokenFilePath2(fileFlag) {
4067
4237
  if (fileFlag !== void 0) {
4068
4238
  return resolve(process.cwd(), fileFlag);
4069
4239
  }
@@ -4097,7 +4267,7 @@ Supported formats: ${SUPPORTED_FORMATS.join(", ")}
4097
4267
  }
4098
4268
  const format = opts.format;
4099
4269
  try {
4100
- const filePath = resolveTokenFilePath(opts.file);
4270
+ const filePath = resolveTokenFilePath2(opts.file);
4101
4271
  if (!existsSync(filePath)) {
4102
4272
  throw new Error(
4103
4273
  `Token file not found at ${filePath}.
@@ -4161,6 +4331,311 @@ Available themes: ${themeNames.join(", ")}`
4161
4331
  }
4162
4332
  );
4163
4333
  }
4334
+ var DEFAULT_STYLES_PATH2 = ".reactscope/compliance-styles.json";
4335
+ var SEVERITY_EMOJI = {
4336
+ none: "\u25CB",
4337
+ subtle: "\u25D4",
4338
+ moderate: "\u25D1",
4339
+ significant: "\u25CF"
4340
+ };
4341
+ function formatImpactReport(report) {
4342
+ const lines = [];
4343
+ const newValueSuffix = report.newValue !== report.oldValue ? ` \u2192 ${report.newValue}` : "";
4344
+ lines.push(`Token: ${report.tokenPath} (${report.oldValue})${newValueSuffix}`);
4345
+ if (report.components.length === 0) {
4346
+ lines.push("");
4347
+ lines.push("No components reference this token.");
4348
+ return lines.join("\n");
4349
+ }
4350
+ lines.push("");
4351
+ const nameWidth = Math.max(9, ...report.components.map((c) => c.name.length));
4352
+ const propWidth = Math.max(
4353
+ 8,
4354
+ ...report.components.flatMap((c) => c.affectedProperties.map((p) => p.length))
4355
+ );
4356
+ for (const comp of report.components) {
4357
+ for (const property of comp.affectedProperties) {
4358
+ const name = comp.name.padEnd(nameWidth);
4359
+ const prop = property.padEnd(propWidth);
4360
+ const severityIcon2 = SEVERITY_EMOJI[comp.severity] ?? "?";
4361
+ lines.push(` ${name} ${prop} ${severityIcon2} ${comp.severity}`);
4362
+ }
4363
+ }
4364
+ lines.push("");
4365
+ const countLabel = `${report.affectedComponentCount} component${report.affectedComponentCount !== 1 ? "s" : ""}`;
4366
+ const severityIcon = SEVERITY_EMOJI[report.overallSeverity] ?? "?";
4367
+ lines.push(
4368
+ `${countLabel} affected \u2014 overall severity: ${severityIcon} ${report.overallSeverity}`
4369
+ );
4370
+ if (report.colorDelta !== void 0) {
4371
+ lines.push(`Color delta: \u0394E ${report.colorDelta.toFixed(2)}`);
4372
+ }
4373
+ return lines.join("\n");
4374
+ }
4375
+ function formatImpactSummary(report) {
4376
+ if (report.components.length === 0) {
4377
+ return `No components reference token "${report.tokenPath}".`;
4378
+ }
4379
+ const parts = report.components.map(
4380
+ (c) => `${c.name} (${c.affectedProperties.length} element${c.affectedProperties.length !== 1 ? "s" : ""})`
4381
+ );
4382
+ return `\u2192 ${parts.join(", ")}`;
4383
+ }
4384
+ function registerImpact(tokensCmd) {
4385
+ 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(
4386
+ (tokenPath, opts) => {
4387
+ try {
4388
+ const tokenFilePath = resolveTokenFilePath(opts.file);
4389
+ const { tokens } = loadTokens(tokenFilePath);
4390
+ const resolver = new TokenResolver(tokens);
4391
+ const engine = new ComplianceEngine(resolver);
4392
+ const stylesPath = opts.styles ?? DEFAULT_STYLES_PATH2;
4393
+ const stylesFile = loadStylesFile(stylesPath);
4394
+ const componentMap = new Map(Object.entries(stylesFile));
4395
+ const batchReport = engine.auditBatch(componentMap);
4396
+ const complianceReports = new Map(Object.entries(batchReport.components));
4397
+ const analyzer = new ImpactAnalyzer(resolver, complianceReports);
4398
+ const currentValue = resolver.resolve(tokenPath);
4399
+ const newValue = opts.newValue ?? currentValue;
4400
+ const report = analyzer.impactOf(tokenPath, newValue);
4401
+ const useJson = opts.format === "json" || opts.format !== "text" && !isTTY();
4402
+ if (useJson) {
4403
+ process.stdout.write(`${JSON.stringify(report, null, 2)}
4404
+ `);
4405
+ } else {
4406
+ process.stdout.write(`${formatImpactReport(report)}
4407
+ `);
4408
+ if (isTTY()) {
4409
+ process.stdout.write(`
4410
+ ${formatImpactSummary(report)}
4411
+ `);
4412
+ }
4413
+ }
4414
+ } catch (err) {
4415
+ process.stderr.write(`Error: ${err instanceof Error ? err.message : String(err)}
4416
+ `);
4417
+ process.exit(1);
4418
+ }
4419
+ }
4420
+ );
4421
+ }
4422
+ var DEFAULT_STYLES_PATH3 = ".reactscope/compliance-styles.json";
4423
+ var DEFAULT_MANIFEST_PATH = ".reactscope/manifest.json";
4424
+ var DEFAULT_OUTPUT_DIR2 = ".reactscope/previews";
4425
+ async function renderComponentWithCssOverride(filePath, componentName, cssOverride, vpWidth, vpHeight, timeoutMs) {
4426
+ const htmlHarness = await buildComponentHarness(
4427
+ filePath,
4428
+ componentName,
4429
+ {},
4430
+ // no props
4431
+ vpWidth,
4432
+ cssOverride
4433
+ // injected as <style>
4434
+ );
4435
+ const pool = new BrowserPool({
4436
+ size: { browsers: 1, pagesPerBrowser: 1 },
4437
+ viewportWidth: vpWidth,
4438
+ viewportHeight: vpHeight
4439
+ });
4440
+ await pool.init();
4441
+ const slot = await pool.acquire();
4442
+ const { page } = slot;
4443
+ try {
4444
+ await page.setContent(htmlHarness, { waitUntil: "load" });
4445
+ await page.waitForFunction(
4446
+ () => {
4447
+ const w = window;
4448
+ return w.__SCOPE_RENDER_COMPLETE__ === true;
4449
+ },
4450
+ { timeout: timeoutMs }
4451
+ );
4452
+ const rootLocator = page.locator("[data-reactscope-root]");
4453
+ const bb = await rootLocator.boundingBox();
4454
+ const PAD = 16;
4455
+ const MIN_W = 320;
4456
+ const MIN_H = 120;
4457
+ const clipX = Math.max(0, (bb?.x ?? 0) - PAD);
4458
+ const clipY = Math.max(0, (bb?.y ?? 0) - PAD);
4459
+ const rawW = (bb?.width ?? MIN_W) + PAD * 2;
4460
+ const rawH = (bb?.height ?? MIN_H) + PAD * 2;
4461
+ const clipW = Math.min(Math.max(rawW, MIN_W), vpWidth - clipX);
4462
+ const clipH = Math.min(Math.max(rawH, MIN_H), vpHeight - clipY);
4463
+ const screenshot = await page.screenshot({
4464
+ clip: { x: clipX, y: clipY, width: clipW, height: clipH },
4465
+ type: "png"
4466
+ });
4467
+ return { screenshot, width: Math.round(clipW), height: Math.round(clipH) };
4468
+ } finally {
4469
+ pool.release(slot);
4470
+ await pool.close().catch(() => void 0);
4471
+ }
4472
+ }
4473
+ function registerPreview(tokensCmd) {
4474
+ 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(
4475
+ async (tokenPath, opts) => {
4476
+ try {
4477
+ const tokenFilePath = resolveTokenFilePath(opts.file);
4478
+ const { tokens } = loadTokens(tokenFilePath);
4479
+ const resolver = new TokenResolver(tokens);
4480
+ const engine = new ComplianceEngine(resolver);
4481
+ const stylesPath = opts.styles ?? DEFAULT_STYLES_PATH3;
4482
+ const stylesFile = loadStylesFile(stylesPath);
4483
+ const componentMap = new Map(Object.entries(stylesFile));
4484
+ const batchReport = engine.auditBatch(componentMap);
4485
+ const complianceReports = new Map(Object.entries(batchReport.components));
4486
+ const analyzer = new ImpactAnalyzer(resolver, complianceReports);
4487
+ const currentValue = resolver.resolve(tokenPath);
4488
+ const impactReport = analyzer.impactOf(tokenPath, opts.newValue);
4489
+ if (impactReport.components.length === 0) {
4490
+ process.stdout.write(
4491
+ `No components reference token "${tokenPath}". Nothing to preview.
4492
+ `
4493
+ );
4494
+ return;
4495
+ }
4496
+ const affectedNames = impactReport.components.map((c) => c.name);
4497
+ process.stderr.write(
4498
+ `Rendering ${affectedNames.length} component(s): ${affectedNames.join(", ")}
4499
+ `
4500
+ );
4501
+ const manifest = loadManifest(opts.manifest);
4502
+ const vpWidth = Number.parseInt(opts.viewportWidth, 10);
4503
+ const vpHeight = Number.parseInt(opts.viewportHeight, 10);
4504
+ const timeout = Number.parseInt(opts.timeout, 10);
4505
+ const tokenCssVar = `--token-${tokenPath.replace(/\./g, "-")}`;
4506
+ const beforeCss = `:root { ${tokenCssVar}: ${currentValue}; }`;
4507
+ const afterCss = `:root { ${tokenCssVar}: ${opts.newValue}; }`;
4508
+ const renders = [];
4509
+ for (const componentName of affectedNames) {
4510
+ const descriptor = manifest.components[componentName];
4511
+ if (descriptor === void 0) {
4512
+ process.stderr.write(
4513
+ `Warning: "${componentName}" not found in manifest \u2014 skipping
4514
+ `
4515
+ );
4516
+ continue;
4517
+ }
4518
+ process.stderr.write(` Rendering ${componentName} (before)...
4519
+ `);
4520
+ const before = await renderComponentWithCssOverride(
4521
+ descriptor.filePath,
4522
+ componentName,
4523
+ beforeCss,
4524
+ vpWidth,
4525
+ vpHeight,
4526
+ timeout
4527
+ );
4528
+ process.stderr.write(` Rendering ${componentName} (after)...
4529
+ `);
4530
+ const after = await renderComponentWithCssOverride(
4531
+ descriptor.filePath,
4532
+ componentName,
4533
+ afterCss,
4534
+ vpWidth,
4535
+ vpHeight,
4536
+ timeout
4537
+ );
4538
+ renders.push({ name: componentName, before, after });
4539
+ }
4540
+ if (renders.length === 0) {
4541
+ process.stderr.write(
4542
+ "Warning: No components could be rendered (all missing from manifest).\n"
4543
+ );
4544
+ return;
4545
+ }
4546
+ const cellW = Math.max(...renders.flatMap((r) => [r.before.width, r.after.width]));
4547
+ const cellH = Math.max(...renders.flatMap((r) => [r.before.height, r.after.height]));
4548
+ const cells = renders.flatMap((r, colIdx) => [
4549
+ {
4550
+ props: { version: "before", component: r.name },
4551
+ result: {
4552
+ screenshot: r.before.screenshot,
4553
+ width: cellW,
4554
+ height: cellH,
4555
+ renderTimeMs: 0,
4556
+ computedStyles: {}
4557
+ },
4558
+ index: colIdx * 2,
4559
+ axisIndices: [0, colIdx]
4560
+ },
4561
+ {
4562
+ props: { version: "after", component: r.name },
4563
+ result: {
4564
+ screenshot: r.after.screenshot,
4565
+ width: cellW,
4566
+ height: cellH,
4567
+ renderTimeMs: 0,
4568
+ computedStyles: {}
4569
+ },
4570
+ index: colIdx * 2 + 1,
4571
+ axisIndices: [1, colIdx]
4572
+ }
4573
+ ]);
4574
+ const matrixResult = {
4575
+ cells,
4576
+ axes: [
4577
+ { name: "component", values: renders.map((r) => r.name) },
4578
+ { name: "version", values: ["before", "after"] }
4579
+ ],
4580
+ axisLabels: [renders.map((r) => r.name), ["before", "after"]],
4581
+ rows: 2,
4582
+ cols: renders.length,
4583
+ stats: {
4584
+ totalCells: cells.length,
4585
+ totalRenderTimeMs: 0,
4586
+ avgRenderTimeMs: 0,
4587
+ minRenderTimeMs: 0,
4588
+ maxRenderTimeMs: 0,
4589
+ wallClockTimeMs: 0
4590
+ }
4591
+ };
4592
+ const generator = new SpriteSheetGenerator({
4593
+ cellPadding: 8,
4594
+ borderWidth: 1,
4595
+ labelHeight: 32,
4596
+ labelWidth: 120
4597
+ });
4598
+ const spriteResult = await generator.generate(matrixResult);
4599
+ const tokenLabel = tokenPath.replace(/\./g, "-");
4600
+ const outputPath = opts.output ?? resolve(process.cwd(), DEFAULT_OUTPUT_DIR2, `preview-${tokenLabel}.png`);
4601
+ const outputDir = resolve(outputPath, "..");
4602
+ mkdirSync(outputDir, { recursive: true });
4603
+ writeFileSync(outputPath, spriteResult.png);
4604
+ const useJson = opts.format === "json" || opts.format !== "text" && !isTTY();
4605
+ if (useJson) {
4606
+ process.stdout.write(
4607
+ `${JSON.stringify(
4608
+ {
4609
+ tokenPath,
4610
+ oldValue: currentValue,
4611
+ newValue: opts.newValue,
4612
+ outputPath,
4613
+ width: spriteResult.width,
4614
+ height: spriteResult.height,
4615
+ components: renders.map((r) => r.name),
4616
+ cells: spriteResult.coordinates.length
4617
+ },
4618
+ null,
4619
+ 2
4620
+ )}
4621
+ `
4622
+ );
4623
+ } else {
4624
+ process.stdout.write(
4625
+ `Preview written to ${outputPath} (${spriteResult.width}\xD7${spriteResult.height}px)
4626
+ `
4627
+ );
4628
+ process.stdout.write(`Components: ${renders.map((r) => r.name).join(", ")}
4629
+ `);
4630
+ }
4631
+ } catch (err) {
4632
+ process.stderr.write(`Error: ${err instanceof Error ? err.message : String(err)}
4633
+ `);
4634
+ process.exit(1);
4635
+ }
4636
+ }
4637
+ );
4638
+ }
4164
4639
 
4165
4640
  // src/tokens/commands.ts
4166
4641
  var DEFAULT_TOKEN_FILE2 = "reactscope.tokens.json";
@@ -4182,7 +4657,7 @@ function buildTable2(headers, rows) {
4182
4657
  );
4183
4658
  return [headerRow, divider, ...dataRows].join("\n");
4184
4659
  }
4185
- function resolveTokenFilePath2(fileFlag) {
4660
+ function resolveTokenFilePath(fileFlag) {
4186
4661
  if (fileFlag !== void 0) {
4187
4662
  return resolve(process.cwd(), fileFlag);
4188
4663
  }
@@ -4245,7 +4720,7 @@ function buildResolutionChain(startPath, rawTokens) {
4245
4720
  function registerGet2(tokensCmd) {
4246
4721
  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) => {
4247
4722
  try {
4248
- const filePath = resolveTokenFilePath2(opts.file);
4723
+ const filePath = resolveTokenFilePath(opts.file);
4249
4724
  const { tokens } = loadTokens(filePath);
4250
4725
  const resolver = new TokenResolver(tokens);
4251
4726
  const resolvedValue = resolver.resolve(tokenPath);
@@ -4271,7 +4746,7 @@ function registerList2(tokensCmd) {
4271
4746
  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(
4272
4747
  (category, opts) => {
4273
4748
  try {
4274
- const filePath = resolveTokenFilePath2(opts.file);
4749
+ const filePath = resolveTokenFilePath(opts.file);
4275
4750
  const { tokens } = loadTokens(filePath);
4276
4751
  const resolver = new TokenResolver(tokens);
4277
4752
  const filtered = resolver.list(opts.type, category);
@@ -4301,7 +4776,7 @@ function registerSearch(tokensCmd) {
4301
4776
  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(
4302
4777
  (value, opts) => {
4303
4778
  try {
4304
- const filePath = resolveTokenFilePath2(opts.file);
4779
+ const filePath = resolveTokenFilePath(opts.file);
4305
4780
  const { tokens } = loadTokens(filePath);
4306
4781
  const resolver = new TokenResolver(tokens);
4307
4782
  const useJson = opts.format === "json" || opts.format !== "table" && !isTTY2();
@@ -4383,7 +4858,7 @@ Tip: use --fuzzy for nearest-match search.
4383
4858
  function registerResolve(tokensCmd) {
4384
4859
  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) => {
4385
4860
  try {
4386
- const filePath = resolveTokenFilePath2(opts.file);
4861
+ const filePath = resolveTokenFilePath(opts.file);
4387
4862
  const absFilePath = filePath;
4388
4863
  const { tokens, rawFile } = loadTokens(absFilePath);
4389
4864
  const resolver = new TokenResolver(tokens);
@@ -4420,7 +4895,7 @@ function registerValidate(tokensCmd) {
4420
4895
  "Validate the token file for errors (circular refs, missing refs, type mismatches)"
4421
4896
  ).option("--file <path>", "Path to token file (overrides config)").option("--format <fmt>", "Output format: json or text (default: auto-detect)").action((opts) => {
4422
4897
  try {
4423
- const filePath = resolveTokenFilePath2(opts.file);
4898
+ const filePath = resolveTokenFilePath(opts.file);
4424
4899
  if (!existsSync(filePath)) {
4425
4900
  throw new Error(
4426
4901
  `Token file not found at ${filePath}.
@@ -4504,6 +4979,9 @@ function createTokensCommand() {
4504
4979
  registerResolve(tokensCmd);
4505
4980
  registerValidate(tokensCmd);
4506
4981
  tokensCmd.addCommand(createTokensExportCommand());
4982
+ registerCompliance(tokensCmd);
4983
+ registerImpact(tokensCmd);
4984
+ registerPreview(tokensCmd);
4507
4985
  return tokensCmd;
4508
4986
  }
4509
4987
 
@@ -4604,6 +5082,6 @@ function createProgram(options = {}) {
4604
5082
  return program;
4605
5083
  }
4606
5084
 
4607
- export { createInitCommand, createInstrumentCommand, createManifestCommand, createProgram, createTokensCommand, createTokensExportCommand, isTTY, matchGlob, resolveTokenFilePath, runInit };
5085
+ export { createInitCommand, createInstrumentCommand, createManifestCommand, createProgram, createTokensCommand, createTokensExportCommand, isTTY, matchGlob, resolveTokenFilePath2 as resolveTokenFilePath, runInit };
4608
5086
  //# sourceMappingURL=index.js.map
4609
5087
  //# sourceMappingURL=index.js.map