@agent-scope/cli 1.6.0 → 1.7.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,9 +1,9 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  // src/program.ts
4
- import { readFileSync as readFileSync3 } from "fs";
4
+ import { readFileSync as readFileSync4 } from "fs";
5
5
  import { generateTest, loadTrace } from "@agent-scope/playwright";
6
- import { Command as Command3 } from "commander";
6
+ import { Command as Command4 } from "commander";
7
7
 
8
8
  // src/browser.ts
9
9
  import { writeFileSync } from "fs";
@@ -1499,9 +1499,363 @@ function buildStructuredReport(report) {
1499
1499
  };
1500
1500
  }
1501
1501
 
1502
+ // src/tokens/commands.ts
1503
+ import { existsSync as existsSync3, readFileSync as readFileSync3 } from "fs";
1504
+ import { resolve as resolve4 } from "path";
1505
+ import {
1506
+ parseTokenFileSync,
1507
+ TokenParseError,
1508
+ TokenResolver,
1509
+ TokenValidationError,
1510
+ validateTokenFile
1511
+ } from "@agent-scope/tokens";
1512
+ import { Command as Command3 } from "commander";
1513
+ var DEFAULT_TOKEN_FILE = "reactscope.tokens.json";
1514
+ var CONFIG_FILE = "reactscope.config.json";
1515
+ function isTTY2() {
1516
+ return process.stdout.isTTY === true;
1517
+ }
1518
+ function pad3(value, width) {
1519
+ return value.length >= width ? value.slice(0, width) : value + " ".repeat(width - value.length);
1520
+ }
1521
+ function buildTable2(headers, rows) {
1522
+ const colWidths = headers.map(
1523
+ (h, i) => Math.max(h.length, ...rows.map((r) => (r[i] ?? "").length))
1524
+ );
1525
+ const divider = colWidths.map((w) => "-".repeat(w)).join(" ");
1526
+ const headerRow = headers.map((h, i) => pad3(h, colWidths[i] ?? 0)).join(" ");
1527
+ const dataRows = rows.map(
1528
+ (row2) => row2.map((cell, i) => pad3(cell ?? "", colWidths[i] ?? 0)).join(" ")
1529
+ );
1530
+ return [headerRow, divider, ...dataRows].join("\n");
1531
+ }
1532
+ function resolveTokenFilePath(fileFlag) {
1533
+ if (fileFlag !== void 0) {
1534
+ return resolve4(process.cwd(), fileFlag);
1535
+ }
1536
+ const configPath = resolve4(process.cwd(), CONFIG_FILE);
1537
+ if (existsSync3(configPath)) {
1538
+ try {
1539
+ const raw = readFileSync3(configPath, "utf-8");
1540
+ const config = JSON.parse(raw);
1541
+ if (typeof config === "object" && config !== null && "tokens" in config && typeof config.tokens === "object" && config.tokens !== null && typeof config.tokens?.file === "string") {
1542
+ const file = config.tokens.file;
1543
+ return resolve4(process.cwd(), file);
1544
+ }
1545
+ } catch {
1546
+ }
1547
+ }
1548
+ return resolve4(process.cwd(), DEFAULT_TOKEN_FILE);
1549
+ }
1550
+ function loadTokens(absPath) {
1551
+ if (!existsSync3(absPath)) {
1552
+ throw new Error(
1553
+ `Token file not found at ${absPath}.
1554
+ Create a reactscope.tokens.json file or use --file to specify a path.`
1555
+ );
1556
+ }
1557
+ const raw = readFileSync3(absPath, "utf-8");
1558
+ return parseTokenFileSync(raw);
1559
+ }
1560
+ function getRawValue(node, segments) {
1561
+ const [head, ...rest] = segments;
1562
+ if (head === void 0) return null;
1563
+ const child = node[head];
1564
+ if (child === void 0 || child === null) return null;
1565
+ if (rest.length === 0) {
1566
+ if (typeof child === "object" && !Array.isArray(child) && "value" in child) {
1567
+ const v = child.value;
1568
+ return typeof v === "string" || typeof v === "number" ? v : null;
1569
+ }
1570
+ return null;
1571
+ }
1572
+ if (typeof child === "object" && !Array.isArray(child)) {
1573
+ return getRawValue(child, rest);
1574
+ }
1575
+ return null;
1576
+ }
1577
+ function buildResolutionChain(startPath, rawTokens) {
1578
+ const chain = [];
1579
+ const seen = /* @__PURE__ */ new Set();
1580
+ let current = startPath;
1581
+ while (!seen.has(current)) {
1582
+ seen.add(current);
1583
+ const rawValue = getRawValue(rawTokens, current.split("."));
1584
+ if (rawValue === null) break;
1585
+ chain.push({ path: current, rawValue: String(rawValue) });
1586
+ const refMatch = /^\{([^}]+)\}$/.exec(String(rawValue));
1587
+ if (refMatch === null) break;
1588
+ current = refMatch[1] ?? "";
1589
+ }
1590
+ return chain;
1591
+ }
1592
+ function registerGet2(tokensCmd) {
1593
+ 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) => {
1594
+ try {
1595
+ const filePath = resolveTokenFilePath(opts.file);
1596
+ const { tokens } = loadTokens(filePath);
1597
+ const resolver = new TokenResolver(tokens);
1598
+ const resolvedValue = resolver.resolve(tokenPath);
1599
+ const useJson = opts.format === "json" || opts.format !== "text" && !isTTY2();
1600
+ if (useJson) {
1601
+ const token = tokens.find((t) => t.path === tokenPath);
1602
+ process.stdout.write(
1603
+ `${JSON.stringify({ path: tokenPath, value: token?.value, resolvedValue, type: token?.type }, null, 2)}
1604
+ `
1605
+ );
1606
+ } else {
1607
+ process.stdout.write(`${resolvedValue}
1608
+ `);
1609
+ }
1610
+ } catch (err) {
1611
+ process.stderr.write(`Error: ${err instanceof Error ? err.message : String(err)}
1612
+ `);
1613
+ process.exit(1);
1614
+ }
1615
+ });
1616
+ }
1617
+ function registerList2(tokensCmd) {
1618
+ 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(
1619
+ (category, opts) => {
1620
+ try {
1621
+ const filePath = resolveTokenFilePath(opts.file);
1622
+ const { tokens } = loadTokens(filePath);
1623
+ const resolver = new TokenResolver(tokens);
1624
+ const filtered = resolver.list(opts.type, category);
1625
+ const useJson = opts.format === "json" || opts.format !== "table" && !isTTY2();
1626
+ if (useJson) {
1627
+ process.stdout.write(`${JSON.stringify(filtered, null, 2)}
1628
+ `);
1629
+ } else {
1630
+ if (filtered.length === 0) {
1631
+ process.stdout.write("No tokens found.\n");
1632
+ return;
1633
+ }
1634
+ const headers = ["PATH", "VALUE", "RESOLVED", "TYPE"];
1635
+ const rows = filtered.map((t) => [t.path, String(t.value), t.resolvedValue, t.type]);
1636
+ process.stdout.write(`${buildTable2(headers, rows)}
1637
+ `);
1638
+ }
1639
+ } catch (err) {
1640
+ process.stderr.write(`Error: ${err instanceof Error ? err.message : String(err)}
1641
+ `);
1642
+ process.exit(1);
1643
+ }
1644
+ }
1645
+ );
1646
+ }
1647
+ function registerSearch(tokensCmd) {
1648
+ 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(
1649
+ (value, opts) => {
1650
+ try {
1651
+ const filePath = resolveTokenFilePath(opts.file);
1652
+ const { tokens } = loadTokens(filePath);
1653
+ const resolver = new TokenResolver(tokens);
1654
+ const useJson = opts.format === "json" || opts.format !== "table" && !isTTY2();
1655
+ const typesToSearch = opts.type ? [opts.type] : [
1656
+ "color",
1657
+ "dimension",
1658
+ "fontFamily",
1659
+ "fontWeight",
1660
+ "number",
1661
+ "shadow",
1662
+ "duration",
1663
+ "cubicBezier"
1664
+ ];
1665
+ const exactMatches = [];
1666
+ const nearestMatches = [];
1667
+ for (const type of typesToSearch) {
1668
+ const exact = resolver.match(value, type);
1669
+ if (exact !== null) {
1670
+ exactMatches.push({
1671
+ path: exact.token.path,
1672
+ resolvedValue: exact.token.resolvedValue,
1673
+ type: exact.token.type,
1674
+ exact: true,
1675
+ distance: 0
1676
+ });
1677
+ }
1678
+ }
1679
+ if (exactMatches.length === 0 && opts.fuzzy) {
1680
+ for (const type of typesToSearch) {
1681
+ const typeTokens = tokens.filter((t) => t.type === type);
1682
+ if (typeTokens.length === 0) continue;
1683
+ try {
1684
+ const near = resolver.nearest(value, type);
1685
+ nearestMatches.push({
1686
+ path: near.token.path,
1687
+ resolvedValue: near.token.resolvedValue,
1688
+ type: near.token.type,
1689
+ exact: near.exact,
1690
+ distance: near.distance
1691
+ });
1692
+ } catch {
1693
+ }
1694
+ }
1695
+ nearestMatches.sort((a, b) => a.distance - b.distance);
1696
+ nearestMatches.splice(3);
1697
+ }
1698
+ const results = exactMatches.length > 0 ? exactMatches : nearestMatches;
1699
+ if (useJson) {
1700
+ process.stdout.write(`${JSON.stringify(results, null, 2)}
1701
+ `);
1702
+ } else {
1703
+ if (results.length === 0) {
1704
+ process.stdout.write(
1705
+ `No tokens found matching "${value}".
1706
+ Tip: use --fuzzy for nearest-match search.
1707
+ `
1708
+ );
1709
+ return;
1710
+ }
1711
+ const headers = ["PATH", "RESOLVED VALUE", "TYPE", "MATCH", "DISTANCE"];
1712
+ const rows = results.map((r) => [
1713
+ r.path,
1714
+ r.resolvedValue,
1715
+ r.type,
1716
+ r.exact ? "exact" : "nearest",
1717
+ r.exact ? "\u2014" : r.distance.toFixed(2)
1718
+ ]);
1719
+ process.stdout.write(`${buildTable2(headers, rows)}
1720
+ `);
1721
+ }
1722
+ } catch (err) {
1723
+ process.stderr.write(`Error: ${err instanceof Error ? err.message : String(err)}
1724
+ `);
1725
+ process.exit(1);
1726
+ }
1727
+ }
1728
+ );
1729
+ }
1730
+ function registerResolve(tokensCmd) {
1731
+ 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) => {
1732
+ try {
1733
+ const filePath = resolveTokenFilePath(opts.file);
1734
+ const absFilePath = filePath;
1735
+ const { tokens, rawFile } = loadTokens(absFilePath);
1736
+ const resolver = new TokenResolver(tokens);
1737
+ resolver.resolve(tokenPath);
1738
+ const chain = buildResolutionChain(tokenPath, rawFile.tokens);
1739
+ const useJson = opts.format === "json" || opts.format !== "text" && !isTTY2();
1740
+ if (useJson) {
1741
+ process.stdout.write(`${JSON.stringify({ path: tokenPath, chain }, null, 2)}
1742
+ `);
1743
+ } else {
1744
+ if (chain.length === 0) {
1745
+ process.stdout.write(`Token "${tokenPath}" not found.
1746
+ `);
1747
+ return;
1748
+ }
1749
+ const parts = chain.map((step, i) => {
1750
+ if (i < chain.length - 1) {
1751
+ return `${step.path} \u2192 ${step.rawValue}`;
1752
+ }
1753
+ return step.rawValue;
1754
+ });
1755
+ process.stdout.write(`${parts.join("\n ")}
1756
+ `);
1757
+ }
1758
+ } catch (err) {
1759
+ process.stderr.write(`Error: ${err instanceof Error ? err.message : String(err)}
1760
+ `);
1761
+ process.exit(1);
1762
+ }
1763
+ });
1764
+ }
1765
+ function registerValidate(tokensCmd) {
1766
+ tokensCmd.command("validate").description(
1767
+ "Validate the token file for errors (circular refs, missing refs, type mismatches)"
1768
+ ).option("--file <path>", "Path to token file (overrides config)").option("--format <fmt>", "Output format: json or text (default: auto-detect)").action((opts) => {
1769
+ try {
1770
+ const filePath = resolveTokenFilePath(opts.file);
1771
+ if (!existsSync3(filePath)) {
1772
+ throw new Error(
1773
+ `Token file not found at ${filePath}.
1774
+ Create a reactscope.tokens.json file or use --file to specify a path.`
1775
+ );
1776
+ }
1777
+ const raw = readFileSync3(filePath, "utf-8");
1778
+ const useJson = opts.format === "json" || opts.format !== "text" && !isTTY2();
1779
+ const errors = [];
1780
+ let parsed;
1781
+ try {
1782
+ parsed = JSON.parse(raw);
1783
+ } catch (err) {
1784
+ errors.push({
1785
+ code: "PARSE_ERROR",
1786
+ message: `Failed to parse token file as JSON: ${String(err)}`
1787
+ });
1788
+ outputValidationResult(filePath, errors, useJson);
1789
+ process.exit(1);
1790
+ }
1791
+ try {
1792
+ validateTokenFile(parsed);
1793
+ } catch (err) {
1794
+ if (err instanceof TokenValidationError) {
1795
+ for (const e of err.errors) {
1796
+ errors.push({ code: e.code, path: e.path, message: e.message });
1797
+ }
1798
+ outputValidationResult(filePath, errors, useJson);
1799
+ process.exit(1);
1800
+ }
1801
+ throw err;
1802
+ }
1803
+ try {
1804
+ parseTokenFileSync(raw);
1805
+ } catch (err) {
1806
+ if (err instanceof TokenParseError) {
1807
+ errors.push({ code: err.code, path: err.path, message: err.message });
1808
+ } else {
1809
+ errors.push({ code: "UNKNOWN", message: String(err) });
1810
+ }
1811
+ outputValidationResult(filePath, errors, useJson);
1812
+ process.exit(1);
1813
+ }
1814
+ outputValidationResult(filePath, errors, useJson);
1815
+ } catch (err) {
1816
+ process.stderr.write(`Error: ${err instanceof Error ? err.message : String(err)}
1817
+ `);
1818
+ process.exit(1);
1819
+ }
1820
+ });
1821
+ }
1822
+ function outputValidationResult(filePath, errors, useJson) {
1823
+ const valid = errors.length === 0;
1824
+ if (useJson) {
1825
+ process.stdout.write(`${JSON.stringify({ valid, file: filePath, errors }, null, 2)}
1826
+ `);
1827
+ } else {
1828
+ if (valid) {
1829
+ process.stdout.write(`\u2713 Token file is valid: ${filePath}
1830
+ `);
1831
+ } else {
1832
+ process.stderr.write(`\u2717 Token file has ${errors.length} error(s): ${filePath}
1833
+
1834
+ `);
1835
+ for (const e of errors) {
1836
+ const pathPrefix = e.path ? ` [${e.path}]` : "";
1837
+ process.stderr.write(` ${e.code}${pathPrefix}: ${e.message}
1838
+ `);
1839
+ }
1840
+ process.exit(1);
1841
+ }
1842
+ }
1843
+ }
1844
+ function createTokensCommand() {
1845
+ const tokensCmd = new Command3("tokens").description(
1846
+ "Query and validate design tokens from a reactscope.tokens.json file"
1847
+ );
1848
+ registerGet2(tokensCmd);
1849
+ registerList2(tokensCmd);
1850
+ registerSearch(tokensCmd);
1851
+ registerResolve(tokensCmd);
1852
+ registerValidate(tokensCmd);
1853
+ return tokensCmd;
1854
+ }
1855
+
1502
1856
  // src/program.ts
1503
1857
  function createProgram(options = {}) {
1504
- const program2 = new Command3("scope").version(options.version ?? "0.1.0").description("Scope \u2014 React instrumentation toolkit");
1858
+ const program2 = new Command4("scope").version(options.version ?? "0.1.0").description("Scope \u2014 React instrumentation toolkit");
1505
1859
  program2.command("capture <url>").description("Capture a React component tree from a live URL and output as JSON").option("-o, --output <path>", "Write JSON to file instead of stdout").option("--pretty", "Pretty-print JSON output (default: minified)", false).option("--timeout <ms>", "Max wait time for React to mount (ms)", "10000").option("--wait <ms>", "Additional wait after page load before capture (ms)", "0").action(
1506
1860
  async (url, opts) => {
1507
1861
  try {
@@ -1574,7 +1928,7 @@ function createProgram(options = {}) {
1574
1928
  }
1575
1929
  );
1576
1930
  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) => {
1577
- const raw = readFileSync3(tracePath, "utf-8");
1931
+ const raw = readFileSync4(tracePath, "utf-8");
1578
1932
  const trace = loadTrace(raw);
1579
1933
  const source = generateTest(trace, {
1580
1934
  description: opts.description,
@@ -1585,6 +1939,7 @@ function createProgram(options = {}) {
1585
1939
  });
1586
1940
  program2.addCommand(createManifestCommand());
1587
1941
  program2.addCommand(createRenderCommand());
1942
+ program2.addCommand(createTokensCommand());
1588
1943
  return program2;
1589
1944
  }
1590
1945