nxds-cli 0.3.0 → 0.4.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.
Files changed (2) hide show
  1. package/dist/index.js +372 -16
  2. package/package.json +4 -2
package/dist/index.js CHANGED
@@ -1,8 +1,8 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  // src/cli.ts
4
- import { Command, CommanderError, Option } from "commander";
5
- import path12 from "path";
4
+ import { Argument, Command, CommanderError, Option } from "commander";
5
+ import path13 from "path";
6
6
 
7
7
  // ../catalog/scripts/catalog-schema.ts
8
8
  import path from "path";
@@ -244,7 +244,7 @@ async function fetchRelease(registry, version) {
244
244
  if (isNewerSchema(value)) {
245
245
  const release = version === "latest" ? "The latest nxds release" : `nxds ${version}`;
246
246
  throw new CliError(
247
- `${release} needs a newer nxds-cli than ${"0.3.0"}. Run \`${CLI_COMMAND}\` to get the newest one.`
247
+ `${release} needs a newer nxds-cli than ${"0.4.0"}. Run \`${CLI_COMMAND}\` to get the newest one.`
248
248
  );
249
249
  }
250
250
  const invalid = (problems) => new CliError(
@@ -1556,13 +1556,347 @@ function report(checks) {
1556
1556
  };
1557
1557
  }
1558
1558
 
1559
+ // ../claude-design/src/system-index.ts
1560
+ var IDENTIFIER = /^[A-Za-z_$][A-Za-z0-9_$]*$/;
1561
+ var PAGE_PATHS = ["manifest.json", "api/"];
1562
+ function mergeSystemIndex(current, exported) {
1563
+ return {
1564
+ ...current,
1565
+ namespace: exported.namespace,
1566
+ editing: exported.editing,
1567
+ libraries: exported.libraries,
1568
+ lastChange: exported.lastChange
1569
+ };
1570
+ }
1571
+ function isPageGenerated(file) {
1572
+ return PAGE_PATHS.some((page) => file === page || file.startsWith(page));
1573
+ }
1574
+ function validateIndex(index) {
1575
+ const where = "design-system.json";
1576
+ const errors = [];
1577
+ if (index["v"] !== 3 || index["layout"] !== "files") {
1578
+ errors.push(`${where}: needs "v": 3 and "layout": "files".`);
1579
+ }
1580
+ if (!index["createdOnFiles"] && !index["convertedFrom"]) {
1581
+ errors.push(`${where}: needs the createdOnFiles marker.`);
1582
+ }
1583
+ if (typeof index["title"] !== "string" || !index["title"]) {
1584
+ errors.push(`${where}: needs a title.`);
1585
+ }
1586
+ if (typeof index["namespace"] !== "string" || !IDENTIFIER.test(index["namespace"])) {
1587
+ errors.push(`${where}: namespace must be a JS identifier.`);
1588
+ }
1589
+ const lastChange = index["lastChange"];
1590
+ if (!lastChange || typeof lastChange["by"] !== "string" || Number.isNaN(Date.parse(String(lastChange["at"])))) {
1591
+ errors.push(`${where}: lastChange needs "by" and an ISO-8601 "at".`);
1592
+ }
1593
+ return errors;
1594
+ }
1595
+
1596
+ // src/commands/export.ts
1597
+ import { createHash } from "crypto";
1598
+ import {
1599
+ existsSync as existsSync9,
1600
+ lstatSync,
1601
+ mkdirSync as mkdirSync2,
1602
+ mkdtempSync,
1603
+ readdirSync,
1604
+ readFileSync as readFileSync5,
1605
+ renameSync,
1606
+ rmSync,
1607
+ writeFileSync as writeFileSync3
1608
+ } from "fs";
1609
+ import { tmpdir } from "os";
1610
+ import path10 from "path";
1611
+ import { x as extract } from "tar";
1612
+ import { EnvHttpProxyAgent as EnvHttpProxyAgent2, fetch as fetch2 } from "undici";
1613
+ var CLAUDE_DESIGN_PACKAGE = "@nxds/claude-design";
1614
+ var DEFAULT_NPM_REGISTRY = "https://registry.npmjs.org";
1615
+ var DEFAULT_EXPORT_DIR = "nxds-design-system";
1616
+ var FETCH_TIMEOUT_MS2 = 6e4;
1617
+ var dispatcher2 = new EnvHttpProxyAgent2();
1618
+ async function download(url) {
1619
+ let response;
1620
+ try {
1621
+ response = await fetch2(url, {
1622
+ dispatcher: dispatcher2,
1623
+ signal: AbortSignal.timeout(FETCH_TIMEOUT_MS2)
1624
+ });
1625
+ } catch (error) {
1626
+ const { message, cause } = error;
1627
+ const reason = cause instanceof Error ? cause.message : message;
1628
+ throw new CliError(
1629
+ `Could not reach ${url} (${reason}). Check your connection, or set HTTPS_PROXY if you are behind a proxy, then run the command again.`
1630
+ );
1631
+ }
1632
+ if (!response.ok) {
1633
+ throw new CliError(
1634
+ `${url} answered ${response.status}. Run the command again later.`
1635
+ );
1636
+ }
1637
+ return Buffer.from(await response.arrayBuffer());
1638
+ }
1639
+ function packageUrl(registry) {
1640
+ return `${registry.replace(/\/+$/, "")}/${CLAUDE_DESIGN_PACKAGE.replace("/", "%2f")}`;
1641
+ }
1642
+ async function resolveTarball(registry, release) {
1643
+ const url = packageUrl(registry);
1644
+ const body = (await download(url)).toString("utf8");
1645
+ let packument;
1646
+ try {
1647
+ packument = JSON.parse(body);
1648
+ } catch {
1649
+ throw new CliError(
1650
+ `${url} didn't answer with JSON. Check --npm-registry, or your proxy, then run the command again.`
1651
+ );
1652
+ }
1653
+ const version = packument["dist-tags"]?.[release] ?? release;
1654
+ const dist = packument.versions?.[version]?.dist;
1655
+ if (!dist?.tarball || !dist.integrity) {
1656
+ const known = Object.keys(packument.versions ?? {}).slice(-5).join(", ");
1657
+ throw new UsageError(
1658
+ `${CLAUDE_DESIGN_PACKAGE} has no release ${release}. Recent releases: ${known || "none"}.`
1659
+ );
1660
+ }
1661
+ return { version, tarball: dist.tarball, integrity: dist.integrity };
1662
+ }
1663
+ function checkIntegrity(bytes, integrity, tarball) {
1664
+ const expected = integrity.split(/\s+/).find((entry) => entry.startsWith("sha512-"));
1665
+ if (!expected) {
1666
+ throw new CliError(
1667
+ `${tarball} has no sha512 integrity to check it against.`
1668
+ );
1669
+ }
1670
+ const actual = `sha512-${createHash("sha512").update(bytes).digest("base64")}`;
1671
+ if (actual !== expected) {
1672
+ throw new CliError(
1673
+ `${tarball} doesn't match its published integrity. Run the command again; if it keeps failing, report it.`
1674
+ );
1675
+ }
1676
+ }
1677
+ async function fetchPackage(registry, release) {
1678
+ const { version, tarball, integrity } = await resolveTarball(
1679
+ registry,
1680
+ release
1681
+ );
1682
+ const bytes = await download(tarball);
1683
+ checkIntegrity(bytes, integrity, tarball);
1684
+ const folder = mkdtempSync(path10.join(tmpdir(), "nxds-claude-design-"));
1685
+ const cleanup = () => rmSync(folder, { recursive: true, force: true });
1686
+ try {
1687
+ const file = path10.join(folder, "package.tgz");
1688
+ writeFileSync3(file, bytes);
1689
+ const unpacked = path10.join(folder, "package");
1690
+ mkdirSync2(unpacked);
1691
+ await extract({
1692
+ file,
1693
+ cwd: unpacked,
1694
+ strip: 1,
1695
+ filter: (_, entry) => "type" in entry && (entry.type === "File" || entry.type === "Directory")
1696
+ });
1697
+ return { version, root: unpacked, cleanup };
1698
+ } catch (error) {
1699
+ cleanup();
1700
+ throw error;
1701
+ }
1702
+ }
1703
+ function localPackage(source) {
1704
+ if (!existsSync9(path10.join(source, "project"))) {
1705
+ throw new UsageError(
1706
+ `${source} has no project/ folder. Point --source at a built @nxds/claude-design dist folder.`
1707
+ );
1708
+ }
1709
+ const manifest = existsSync9(path10.join(source, "package.json")) ? readJson(path10.join(source, "package.json")) : {};
1710
+ return {
1711
+ version: manifest.version ?? "local",
1712
+ root: source,
1713
+ cleanup: () => {
1714
+ }
1715
+ };
1716
+ }
1717
+ function assertWritable(out, overwrite) {
1718
+ if (!existsSync9(out) || readdirSync(out).length === 0 || overwrite) return;
1719
+ throw new UsageError(
1720
+ `${out} already has files. Pass --overwrite to replace its project/ folder, or choose another --out.`
1721
+ );
1722
+ }
1723
+ function readPaths(file) {
1724
+ const value = readJson(file);
1725
+ if (!Array.isArray(value) || !value.every((entry) => typeof entry === "string")) {
1726
+ throw new UsageError(
1727
+ `${file} must be a JSON array of the system's file paths.`
1728
+ );
1729
+ }
1730
+ return value;
1731
+ }
1732
+ function stalePaths(existing, exported) {
1733
+ return existing.filter((file) => file.startsWith("project/")).filter((file) => !isPageGenerated(file.slice("project/".length))).filter((file) => !exported.has(file)).sort();
1734
+ }
1735
+ function readManifest(root) {
1736
+ const file = path10.join(root, "files.json");
1737
+ if (!existsSync9(file)) {
1738
+ throw new CliError(
1739
+ `${root} has no files.json. Export from an @nxds/claude-design release or a built dist folder.`
1740
+ );
1741
+ }
1742
+ const value = readJson(file);
1743
+ if (!Array.isArray(value) || !value.every(
1744
+ (entry) => typeof entry?.path === "string" && typeof entry.sha256 === "string"
1745
+ )) {
1746
+ throw new CliError(
1747
+ `${file} must be a JSON array of { path, sha256 } entries.`
1748
+ );
1749
+ }
1750
+ return value;
1751
+ }
1752
+ function readProject2(root) {
1753
+ const projectDir = path10.join(root, "project");
1754
+ const files = /* @__PURE__ */ new Map();
1755
+ for (const entry of readManifest(root)) {
1756
+ const file = path10.join(projectDir, entry.path);
1757
+ const relative = path10.relative(projectDir, file);
1758
+ if (relative.split(path10.sep)[0] === ".." || path10.isAbsolute(relative)) {
1759
+ throw new CliError(`files.json lists ${entry.path}, outside project/.`);
1760
+ }
1761
+ if (!existsSync9(file) || !lstatSync(file).isFile()) {
1762
+ throw new CliError(
1763
+ `project/${entry.path} is missing from the release, or isn't a regular file.`
1764
+ );
1765
+ }
1766
+ const bytes = readFileSync5(file);
1767
+ if (createHash("sha256").update(bytes).digest("hex") !== entry.sha256) {
1768
+ throw new CliError(
1769
+ `project/${entry.path} doesn't match the release's files.json, so nothing was written.`
1770
+ );
1771
+ }
1772
+ files.set(entry.path, bytes);
1773
+ }
1774
+ return files;
1775
+ }
1776
+ function mergeIndex(files, currentFile) {
1777
+ const exported = files.get("design-system.json");
1778
+ if (!exported) throw new CliError("The release has no design-system.json.");
1779
+ const merged = mergeSystemIndex(
1780
+ readJson(currentFile),
1781
+ JSON.parse(exported.toString("utf8"))
1782
+ );
1783
+ const errors = validateIndex(merged);
1784
+ if (errors.length > 0) {
1785
+ throw new UsageError(
1786
+ `${currentFile} merges into an index the page can't read, so nothing was written:
1787
+ ${errors.map((error) => ` ${error}`).join("\n")}`
1788
+ );
1789
+ }
1790
+ files.set(
1791
+ "design-system.json",
1792
+ Buffer.from(`${JSON.stringify(merged, null, 2)}
1793
+ `)
1794
+ );
1795
+ }
1796
+ function writeProject(out, files) {
1797
+ mkdirSync2(out, { recursive: true });
1798
+ const staging = makeStaging(path10.dirname(out), out);
1799
+ const next = path10.join(staging, "next");
1800
+ const previous = path10.join(staging, "previous");
1801
+ const projectDir = path10.join(out, "project");
1802
+ try {
1803
+ for (const [file, bytes] of files) {
1804
+ mkdirSync2(path10.dirname(path10.join(next, file)), { recursive: true });
1805
+ writeFileSync3(path10.join(next, file), bytes);
1806
+ }
1807
+ if (existsSync9(projectDir)) renameSync(projectDir, previous);
1808
+ renameSync(next, projectDir);
1809
+ } catch (error) {
1810
+ restore(previous, projectDir, error);
1811
+ rmSync(staging, { recursive: true, force: true });
1812
+ throw renameError(error, out);
1813
+ }
1814
+ rmSync(staging, { recursive: true, force: true });
1815
+ }
1816
+ function makeStaging(parent, out) {
1817
+ try {
1818
+ return mkdtempSync(path10.join(parent, ".nxds-project-"));
1819
+ } catch (error) {
1820
+ const { code, message } = error;
1821
+ if (code !== "EACCES" && code !== "EPERM" && code !== "EROFS") throw error;
1822
+ throw new UsageError(
1823
+ `The export stages project/ in ${parent}, beside ${out}, so ${parent} must be writable (${message}).`
1824
+ );
1825
+ }
1826
+ }
1827
+ function restore(previous, projectDir, cause) {
1828
+ if (!existsSync9(previous)) return;
1829
+ try {
1830
+ renameSync(previous, projectDir);
1831
+ } catch (error) {
1832
+ throw new CliError(
1833
+ `The export failed (${cause.message}) and couldn't put the old project/ back (${error.message}). It is in ${previous}.`
1834
+ );
1835
+ }
1836
+ }
1837
+ function renameError(error, out) {
1838
+ if (error.code !== "EXDEV") return error;
1839
+ return new UsageError(
1840
+ `${out} is on another filesystem from its parent folder, so project/ can't be moved into it. Pass an --out folder inside ${out}.`
1841
+ );
1842
+ }
1843
+ async function exportClaudeDesign(cwd, options) {
1844
+ const out = path10.resolve(cwd, options.out ?? DEFAULT_EXPORT_DIR);
1845
+ assertWritable(out, options.overwrite);
1846
+ const pkg = options.source ? localPackage(path10.resolve(cwd, options.source)) : await fetchPackage(
1847
+ options.npmRegistry ?? DEFAULT_NPM_REGISTRY,
1848
+ options.release ?? "latest"
1849
+ );
1850
+ try {
1851
+ const files = readProject2(pkg.root);
1852
+ if (options.index) mergeIndex(files, path10.resolve(cwd, options.index));
1853
+ const exported = new Set(
1854
+ [...files.keys()].map((file) => `project/${file}`)
1855
+ );
1856
+ const removed = options.existing ? stalePaths(readPaths(path10.resolve(cwd, options.existing)), exported) : [];
1857
+ const coverageFile = path10.join(pkg.root, "coverage.md");
1858
+ const coverage = existsSync9(coverageFile) ? readFileSync5(coverageFile) : void 0;
1859
+ writeProject(out, files);
1860
+ if (coverage) writeFileSync3(path10.join(out, "coverage.md"), coverage);
1861
+ if (options.existing) {
1862
+ writeFileSync3(
1863
+ path10.join(out, "removed.json"),
1864
+ `${JSON.stringify(removed, null, 2)}
1865
+ `
1866
+ );
1867
+ }
1868
+ const relative = path10.relative(cwd, out) || ".";
1869
+ const lines2 = [
1870
+ `Exported nxds ${pkg.version} as a Claude Design System: ${files.size} files in ${path10.join(relative, "project")}.`,
1871
+ `Publish ${relative} as a Design System artifact in Claude, with ${relative} as the root.`
1872
+ ];
1873
+ if (removed.length > 0) {
1874
+ lines2.push(
1875
+ `Remove the ${removed.length} files the system has and this release doesn't: they're listed in ${path10.join(relative, "removed.json")}.`
1876
+ );
1877
+ }
1878
+ if (coverage) {
1879
+ lines2.push(
1880
+ `The props the canvas can't set are in ${path10.join(relative, "coverage.md")}.`
1881
+ );
1882
+ }
1883
+ return {
1884
+ ok: true,
1885
+ lines: lines2,
1886
+ data: { version: pkg.version, out, files: files.size, removed }
1887
+ };
1888
+ } finally {
1889
+ pkg.cleanup();
1890
+ }
1891
+ }
1892
+
1559
1893
  // src/commands/install.ts
1560
- import { existsSync as existsSync10, mkdirSync as mkdirSync2, writeFileSync as writeFileSync4 } from "fs";
1561
- import path11 from "path";
1894
+ import { existsSync as existsSync11, mkdirSync as mkdirSync3, writeFileSync as writeFileSync5 } from "fs";
1895
+ import path12 from "path";
1562
1896
 
1563
1897
  // src/install.ts
1564
- import { existsSync as existsSync9, readFileSync as readFileSync5, writeFileSync as writeFileSync3 } from "fs";
1565
- import path10 from "path";
1898
+ import { existsSync as existsSync10, readFileSync as readFileSync6, writeFileSync as writeFileSync4 } from "fs";
1899
+ import path11 from "path";
1566
1900
  function emptyInstallResult() {
1567
1901
  return {
1568
1902
  items: [],
@@ -1578,11 +1912,11 @@ function emptyInstallResult() {
1578
1912
  }
1579
1913
  function addCssImports(project, config, files, dryRun) {
1580
1914
  if (files.length === 0) return [];
1581
- const entry = path10.join(project.root, config.css);
1582
- const current = existsSync9(entry) ? readFileSync5(entry, "utf8") : "";
1915
+ const entry = path11.join(project.root, config.css);
1916
+ const current = existsSync10(entry) ? readFileSync6(entry, "utf8") : "";
1583
1917
  const added = [...new Set(files)].map((file) => cssImportLine(config, file)).filter((line) => !current.includes(line));
1584
1918
  if (added.length > 0 && !dryRun) {
1585
- writeFileSync3(entry, `${current.trimEnd()}
1919
+ writeFileSync4(entry, `${current.trimEnd()}
1586
1920
  ${added.join("\n")}
1587
1921
  `);
1588
1922
  }
@@ -1737,9 +2071,9 @@ async function init(cwd, registry, options) {
1737
2071
  );
1738
2072
  if (!options.dryRun) {
1739
2073
  for (const change of wiring.changes) {
1740
- const target = path11.join(project.root, change.path);
1741
- mkdirSync2(path11.dirname(target), { recursive: true });
1742
- writeFileSync4(target, change.content);
2074
+ const target = path12.join(project.root, change.path);
2075
+ mkdirSync3(path12.dirname(target), { recursive: true });
2076
+ writeFileSync5(target, change.content);
1743
2077
  }
1744
2078
  writeConfig(cwd, config);
1745
2079
  }
@@ -1848,7 +2182,7 @@ async function update(cwd, registry, options) {
1848
2182
  };
1849
2183
  }
1850
2184
  const orphaned = [...installedBefore].filter(
1851
- (file) => !current.has(file) && existsSync10(path11.join(cwd, config.dir, file))
2185
+ (file) => !current.has(file) && existsSync11(path12.join(cwd, config.dir, file))
1852
2186
  );
1853
2187
  const previousFiles = previous && releaseFiles(previous, config);
1854
2188
  config.items = known;
@@ -1902,7 +2236,7 @@ async function update(cwd, registry, options) {
1902
2236
  function context(command) {
1903
2237
  const { cwd, registry } = command.optsWithGlobals();
1904
2238
  return {
1905
- cwd: path12.resolve(cwd ?? process.cwd()),
2239
+ cwd: path13.resolve(cwd ?? process.cwd()),
1906
2240
  registry: resolveRegistry(registry)
1907
2241
  };
1908
2242
  }
@@ -1924,7 +2258,7 @@ function program(done, write) {
1924
2258
  // Errors are printed once, by `main`, as text or JSON.
1925
2259
  outputError: () => {
1926
2260
  }
1927
- }).exitOverride().name(CLI_COMMAND).description("Add nxds to your app.").version("0.3.0", "-v, --version", "print the version").helpOption("-h, --help", "show help").helpCommand("help [command]", "show help for a command").option("--json", "print machine-readable JSON").option("--cwd <path>", "run in another folder").addOption(
2261
+ }).exitOverride().name(CLI_COMMAND).description("Add nxds to your app.").version("0.4.0", "-v, --version", "print the version").helpOption("-h, --help", "show help").helpCommand("help [command]", "show help for a command").option("--json", "print machine-readable JSON").option("--cwd <path>", "run in another folder").addOption(
1928
2262
  new Option(
1929
2263
  "--registry <url|path>",
1930
2264
  "where to get nxds releases (default: https://nxds.dev/r)"
@@ -1984,6 +2318,28 @@ Docs: https://nxds.dev/getting-started/install`
1984
2318
  const { cwd, registry } = context(command);
1985
2319
  done(await diff(cwd, registry, items));
1986
2320
  });
2321
+ cli.command("export").description(
2322
+ "export nxds as a Claude Design System folder, ready to publish"
2323
+ ).addArgument(
2324
+ new Argument("<target>", "what to export").choices(["claude-design"])
2325
+ ).option("--out <path>", `folder to write (default: ${DEFAULT_EXPORT_DIR})`).option("--release <version>", "nxds release to export (default: latest)").option(
2326
+ "--index <path>",
2327
+ "the system's current design-system.json, to keep its title and creation date"
2328
+ ).option(
2329
+ "--existing <path>",
2330
+ "a JSON list of the system's current file paths, to list the ones this release removes"
2331
+ ).option("--overwrite", "replace the project/ folder in --out", false).option(
2332
+ "--source <path>",
2333
+ "a built @nxds/claude-design dist folder, instead of the npm release"
2334
+ ).addOption(
2335
+ new Option(
2336
+ "--npm-registry <url>",
2337
+ `npm registry to download from (default: ${DEFAULT_NPM_REGISTRY})`
2338
+ ).env("npm_config_registry")
2339
+ ).action(async (_target, _options, command) => {
2340
+ const { cwd } = context(command);
2341
+ done(await exportClaudeDesign(cwd, command.opts()));
2342
+ });
1987
2343
  cli.command("doctor").description("check that nxds is wired up correctly").action(async (_options, command) => {
1988
2344
  const { cwd, registry } = context(command);
1989
2345
  done(await doctor(cwd, registry));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nxds-cli",
3
- "version": "0.3.0",
3
+ "version": "0.4.0",
4
4
  "type": "module",
5
5
  "description": "Install nxds components, blocks and setup into your app.",
6
6
  "keywords": [
@@ -36,6 +36,7 @@
36
36
  "commander": "^14.0.3",
37
37
  "node-diff3": "^3.2.1",
38
38
  "semver": "^7.8.5",
39
+ "tar": "^7.5.22",
39
40
  "undici": "^7.30.1",
40
41
  "zod": "^4.6.7"
41
42
  },
@@ -44,7 +45,8 @@
44
45
  "@types/semver": "^7.8.0",
45
46
  "tsup": "^8.5.1",
46
47
  "typescript": "^5.9.3",
47
- "@nxds/catalog": "0.0.0"
48
+ "@nxds/catalog": "0.0.0",
49
+ "@nxds/claude-design": "0.6.0"
48
50
  },
49
51
  "scripts": {
50
52
  "build": "tsup",