nxds-cli 0.2.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 +594 -91
  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 path11 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";
@@ -49,6 +49,11 @@ var setupStepSchema = z.enum([
49
49
  "wrap-root-with-provider"
50
50
  ]);
51
51
  var npmPackagesSchema = z.record(npmNameSchema, rangeSchema);
52
+ var reactMajorSchema = z.union([z.literal(18), z.literal(19)]);
53
+ var react18Schema = z.object({
54
+ files: z.array(catalogFileSchema),
55
+ packages: z.object({ npm: npmPackagesSchema })
56
+ });
52
57
  function variantWith(packages) {
53
58
  return z.object({
54
59
  /** Other items this one needs, installed first. */
@@ -57,12 +62,35 @@ function variantWith(packages) {
57
62
  files: z.array(catalogFileSchema),
58
63
  /** Files in `files` the nxds CSS entry imports. */
59
64
  css: z.array(relativePathSchema),
60
- steps: z.array(setupStepSchema)
65
+ steps: z.array(setupStepSchema),
66
+ react18: react18Schema.optional()
61
67
  }).refine(
62
68
  ({ css, files }) => css.every((entry) => files.some((file) => file.path === entry)),
63
69
  { path: ["css"], message: "must name files in `files`" }
70
+ ).refine(
71
+ ({ files, react18 }) => (react18?.files ?? []).every(
72
+ (file) => files.some((own) => own.path === file.path)
73
+ ),
74
+ {
75
+ path: ["react18", "files"],
76
+ message: "must replace files in `files`"
77
+ }
64
78
  );
65
79
  }
80
+ function variantForReact(variant, react) {
81
+ if (react === 19 || !variant.react18) return variant;
82
+ const replaced = new Map(
83
+ variant.react18.files.map((file) => [file.path, file])
84
+ );
85
+ return {
86
+ ...variant,
87
+ files: variant.files.map((file) => replaced.get(file.path) ?? file),
88
+ packages: {
89
+ ...variant.packages,
90
+ npm: { ...variant.packages.npm, ...variant.react18.packages.npm }
91
+ }
92
+ };
93
+ }
66
94
  var variantSchema = variantWith(z.object({ npm: npmPackagesSchema }));
67
95
  var setupVariantSchema = variantWith(
68
96
  z.object({
@@ -97,6 +125,8 @@ var catalogSchema = z.object({
97
125
  version: versionSchema,
98
126
  /** The frameworks the CLI installs. The docs copy also carries variants for others; a release catalog drops them. Names this version doesn't know are dropped too. */
99
127
  frameworks: z.array(z.string()).transform((frameworks) => frameworks.filter(isFramework)),
128
+ /** The React majors this release has files for. Releases from before React 18 support leave it out, and have React 19 files only. */
129
+ reactMajors: z.array(reactMajorSchema).nonempty().default([19]),
100
130
  items: z.array(catalogItemSchema)
101
131
  });
102
132
  function lines(issues, prefix) {
@@ -214,7 +244,7 @@ async function fetchRelease(registry, version) {
214
244
  if (isNewerSchema(value)) {
215
245
  const release = version === "latest" ? "The latest nxds release" : `nxds ${version}`;
216
246
  throw new CliError(
217
- `${release} needs a newer nxds-cli than ${"0.2.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.`
218
248
  );
219
249
  }
220
250
  const invalid = (problems) => new CliError(
@@ -229,11 +259,18 @@ Check \`--registry\`, or run the command again later.`
229
259
  }
230
260
  return parsed.data;
231
261
  }
232
- async function loadRelease(registry, version) {
262
+ async function loadRelease(registry, version, react) {
233
263
  const catalog = await fetchRelease(registry, version);
234
- if (catalog) return catalog;
264
+ if (!catalog) {
265
+ throw new CliError(
266
+ version === "latest" ? `${registry} has no nxds release.` : `${registry} has no nxds ${version}, the release this app is on. Run \`${CLI_COMMAND} update\` to move to the latest release.`
267
+ );
268
+ }
269
+ if (catalog.reactMajors.includes(react)) return catalog;
270
+ const latest = version === "latest" ? null : await fetchRelease(registry, "latest");
271
+ const fix = latest?.reactMajors.includes(react) ? `Upgrade the app to React 19, or run \`${CLI_COMMAND} update\` to move to nxds ${latest.version}, which has React ${react} files.` : "Upgrade the app to React 19.";
235
272
  throw new CliError(
236
- version === "latest" ? `${registry} has no nxds release.` : `${registry} has no nxds ${version}, the release this app is on. Run \`${CLI_COMMAND} update\` to move to the latest release.`
273
+ `nxds ${catalog.version} has no React ${react} files. ${fix}`
237
274
  );
238
275
  }
239
276
  function findItem(catalog, name) {
@@ -248,12 +285,15 @@ function variantOf(item, framework) {
248
285
  if (variant) return variant;
249
286
  throw new CliError(`${item.title} is not available for ${framework} yet.`);
250
287
  }
251
- function setupVariant(catalog, framework) {
288
+ function variantFor(item, target) {
289
+ return variantForReact(variantOf(item, target.framework), target.react);
290
+ }
291
+ function setupVariant(catalog, target) {
252
292
  const setup = catalog.items.find((item) => item.name === SETUP);
253
293
  if (setup?.kind !== "setup") {
254
294
  throw new Error(`nxds ${catalog.version} has no "${SETUP}" setup item.`);
255
295
  }
256
- return variantOf(setup, framework);
296
+ return variantFor(setup, target);
257
297
  }
258
298
  function resolveItems(catalog, framework, names) {
259
299
  const ordered = [];
@@ -268,10 +308,10 @@ function resolveItems(catalog, framework, names) {
268
308
  names.forEach(visit);
269
309
  return ordered;
270
310
  }
271
- function releaseFiles(catalog, framework) {
311
+ function releaseFiles(catalog, target) {
272
312
  return new Map(
273
- catalog.items.flatMap(
274
- (item) => (item.variants[framework]?.files ?? []).map(
313
+ catalog.items.filter((item) => item.variants[target.framework]).flatMap(
314
+ (item) => variantFor(item, target).files.map(
275
315
  (file) => [file.path, file.content]
276
316
  )
277
317
  )
@@ -285,6 +325,10 @@ function splitInstalled(catalog, installed) {
285
325
  };
286
326
  }
287
327
 
328
+ // src/commands/browse.ts
329
+ import { existsSync as existsSync5 } from "fs";
330
+ import path6 from "path";
331
+
288
332
  // src/config.ts
289
333
  import { existsSync as existsSync2, writeFileSync } from "fs";
290
334
  import path3 from "path";
@@ -293,6 +337,8 @@ var CONFIG_FILE = "nxds.json";
293
337
  var configSchema = z2.looseObject({
294
338
  /** The CLI installs React only. */
295
339
  framework: z2.literal("react"),
340
+ /** The React major the installed files are for. */
341
+ react: reactMajorSchema,
296
342
  /** The nxds release every installed file comes from. */
297
343
  version: versionSchema,
298
344
  /** The nxds folder, relative to the project root. */
@@ -333,7 +379,7 @@ function writeConfig(root, config) {
333
379
  // src/project.ts
334
380
  import { existsSync as existsSync4 } from "fs";
335
381
  import path5 from "path";
336
- import { major, minVersion as minVersion2, validRange as validRange3 } from "semver";
382
+ import { major, minVersion as minVersion2, parse, validRange as validRange3 } from "semver";
337
383
 
338
384
  // src/packages.ts
339
385
  import { spawnSync } from "child_process";
@@ -448,6 +494,27 @@ function readProject(root) {
448
494
  function findFile(project, candidates) {
449
495
  return firstExisting(project.root, candidates);
450
496
  }
497
+ function reactMajor(project) {
498
+ const declared = project.dependencies.react;
499
+ if (declared === void 0) {
500
+ throw new CliError(
501
+ "This app has no react dependency. nxds needs React 18.3 or 19."
502
+ );
503
+ }
504
+ const installed = installedVersion(project, "react");
505
+ const range = validRange3(declared) === null ? null : minVersion2(declared);
506
+ const version = installed === null ? range : parse(installed);
507
+ if (version === null) {
508
+ throw new CliError(
509
+ `This app declares react ${declared}, which names no version, and none is installed. Install your dependencies, then run the command again.`
510
+ );
511
+ }
512
+ if (version.major >= 19) return 19;
513
+ if (version.major === 18 && version.minor >= 3) return 18;
514
+ throw new CliError(
515
+ `nxds needs React 18.3 or 19, and this app uses react ${version.version}. Upgrade React, then run the command again.`
516
+ );
517
+ }
451
518
  function majorVersion(project, name) {
452
519
  const declared = project.dependencies[name];
453
520
  if (declared === void 0) return null;
@@ -457,8 +524,8 @@ function majorVersion(project, name) {
457
524
  }
458
525
 
459
526
  // src/commands/browse.ts
460
- function appRelease(cwd, registry) {
461
- return loadRelease(registry, readConfig(cwd)?.version ?? "latest");
527
+ function appRelease(registry, config, react) {
528
+ return loadRelease(registry, config?.version ?? "latest", react);
462
529
  }
463
530
  function summary(item) {
464
531
  return {
@@ -478,7 +545,12 @@ function summaryLines(items) {
478
545
  );
479
546
  }
480
547
  async function list(cwd, registry, kind) {
481
- const items = (await appRelease(cwd, registry)).items.filter(
548
+ const catalog = await appRelease(
549
+ registry,
550
+ readConfig(cwd),
551
+ projectReact(cwd)
552
+ );
553
+ const items = catalog.items.filter(
482
554
  (item) => item.name !== SETUP && (kind === void 0 || item.kind === kind)
483
555
  );
484
556
  return { data: items.map(summary), lines: summaryLines(items), ok: true };
@@ -491,7 +563,12 @@ async function search(cwd, registry, query) {
491
563
  if (item.description.toLowerCase().includes(needle)) return 2;
492
564
  return void 0;
493
565
  };
494
- const items = (await appRelease(cwd, registry)).items.filter((item) => item.name !== SETUP).flatMap((item) => {
566
+ const catalog = await appRelease(
567
+ registry,
568
+ readConfig(cwd),
569
+ projectReact(cwd)
570
+ );
571
+ const items = catalog.items.filter((item) => item.name !== SETUP).flatMap((item) => {
495
572
  const score = rank(item);
496
573
  return score === void 0 ? [] : [{ item, score }];
497
574
  }).sort((a, b) => a.score - b.score).map(({ item }) => item);
@@ -501,19 +578,28 @@ async function search(cwd, registry, query) {
501
578
  ok: true
502
579
  };
503
580
  }
504
- function projectFramework(cwd, catalog) {
505
- const configured = readConfig(cwd)?.framework;
506
- if (configured) return configured;
581
+ function projectFramework(config, catalog) {
582
+ if (config) return config.framework;
507
583
  const [only, ...others] = catalog.frameworks;
508
584
  if (only && others.length === 0) return only;
509
585
  throw new CliError(
510
586
  `This release has several frameworks. Run \`${CLI_COMMAND} init\` first, so nxds knows which one your app uses.`
511
587
  );
512
588
  }
589
+ function projectReact(cwd) {
590
+ if (!existsSync5(path6.join(cwd, "package.json"))) return 19;
591
+ const project = readProject(cwd);
592
+ return "react" in project.dependencies ? reactMajor(project) : 19;
593
+ }
513
594
  async function view(cwd, registry, name) {
514
- const catalog = await appRelease(cwd, registry);
595
+ const config = readConfig(cwd);
596
+ const react = projectReact(cwd);
597
+ const catalog = await appRelease(registry, config, react);
515
598
  const item = findItem(catalog, name);
516
- const variant = variantOf(item, projectFramework(cwd, catalog));
599
+ const variant = variantFor(item, {
600
+ framework: projectFramework(config, catalog),
601
+ react
602
+ });
517
603
  const packages = Object.entries(variant.packages.npm).map(
518
604
  ([pkg, range]) => `${pkg}@${range}`
519
605
  );
@@ -573,13 +659,13 @@ function info(cwd) {
573
659
 
574
660
  // src/commands/check.ts
575
661
  import { spawnSync as spawnSync2 } from "child_process";
576
- import { existsSync as existsSync7, readFileSync as readFileSync4 } from "fs";
577
- import path8 from "path";
578
- import { satisfies as satisfies2 } from "semver";
662
+ import { existsSync as existsSync8, readFileSync as readFileSync4 } from "fs";
663
+ import path9 from "path";
664
+ import { major as major2, satisfies as satisfies2 } from "semver";
579
665
 
580
666
  // src/files.ts
581
- import { existsSync as existsSync5, mkdirSync, readFileSync as readFileSync2, writeFileSync as writeFileSync2 } from "fs";
582
- import path6 from "path";
667
+ import { existsSync as existsSync6, mkdirSync, readFileSync as readFileSync2, writeFileSync as writeFileSync2 } from "fs";
668
+ import path7 from "path";
583
669
  import { merge } from "node-diff3";
584
670
  function baseOf(previous, file) {
585
671
  if (!previous) return void 0;
@@ -604,13 +690,13 @@ function hasConflictMarkers(content) {
604
690
  }
605
691
  function conflictedFiles(root, config, files) {
606
692
  return [...files].filter((file) => {
607
- const target = path6.join(root, config.dir, file);
608
- return existsSync5(target) && hasConflictMarkers(readFileSync2(target, "utf8"));
693
+ const target = path7.join(root, config.dir, file);
694
+ return existsSync6(target) && hasConflictMarkers(readFileSync2(target, "utf8"));
609
695
  });
610
696
  }
611
697
  function fileState(root, config, file, previous) {
612
- const target = path6.join(root, config.dir, file.path);
613
- if (!existsSync5(target)) return "missing";
698
+ const target = path7.join(root, config.dir, file.path);
699
+ if (!existsSync6(target)) return "missing";
614
700
  const onDisk = readFileSync2(target, "utf8");
615
701
  if (hasConflictMarkers(onDisk)) return "conflicted";
616
702
  if (sameContent(onDisk, file.content)) return "current";
@@ -634,8 +720,8 @@ function writeFiles(root, config, files, previous, { overwrite, dryRun, version
634
720
  };
635
721
  const write = (file, content) => {
636
722
  if (dryRun) return;
637
- const target = path6.join(root, config.dir, file);
638
- mkdirSync(path6.dirname(target), { recursive: true });
723
+ const target = path7.join(root, config.dir, file);
724
+ mkdirSync(path7.dirname(target), { recursive: true });
639
725
  writeFileSync2(target, content);
640
726
  };
641
727
  for (const file of files) {
@@ -658,7 +744,7 @@ function writeFiles(root, config, files, previous, { overwrite, dryRun, version
658
744
  result.skipped.push(file.path);
659
745
  continue;
660
746
  }
661
- const target = path6.join(root, config.dir, file.path);
747
+ const target = path7.join(root, config.dir, file.path);
662
748
  const ours = readFileSync2(target, "utf8");
663
749
  const merged = mergeFile(ours, base, file.content, version);
664
750
  if (sameContent(ours, merged.content)) {
@@ -671,7 +757,7 @@ function writeFiles(root, config, files, previous, { overwrite, dryRun, version
671
757
  return result;
672
758
  }
673
759
  function relativeImport(fromFile, toFile) {
674
- const relative = path6.relative(path6.dirname(fromFile), toFile).split(path6.sep).join("/");
760
+ const relative = path7.relative(path7.dirname(fromFile), toFile).split(path7.sep).join("/");
675
761
  return relative.startsWith(".") ? relative : `./${relative}`;
676
762
  }
677
763
 
@@ -687,11 +773,11 @@ import {
687
773
  isObjectProperty,
688
774
  isSpreadElement
689
775
  } from "@babel/types";
690
- import { existsSync as existsSync6, readFileSync as readFileSync3 } from "fs";
691
- import path7 from "path";
776
+ import { existsSync as existsSync7, readFileSync as readFileSync3 } from "fs";
777
+ import path8 from "path";
692
778
 
693
779
  // src/setup/source-edit.ts
694
- import { parse } from "@babel/parser";
780
+ import { parse as parse2 } from "@babel/parser";
695
781
  import {
696
782
  isImportDeclaration,
697
783
  isJSXAttribute,
@@ -701,7 +787,7 @@ import {
701
787
  } from "@babel/types";
702
788
  function parseModule(source, file) {
703
789
  try {
704
- return parse(source, {
790
+ return parse2(source, {
705
791
  sourceType: "module",
706
792
  plugins: ["typescript", "jsx"]
707
793
  }).program;
@@ -794,7 +880,7 @@ function tailwindPackages(setup, range) {
794
880
  }
795
881
  function appDir(project) {
796
882
  const dir = APP_DIRS.find(
797
- (candidate) => existsSync6(path7.join(project.root, candidate, "layout.tsx"))
883
+ (candidate) => existsSync7(path8.join(project.root, candidate, "layout.tsx"))
798
884
  );
799
885
  if (!dir)
800
886
  throw new CliError("No app/layout.tsx or src/app/layout.tsx found.");
@@ -810,7 +896,7 @@ function cssEntryPath(project) {
810
896
  return `${entryDir(project)}/nxds.css`;
811
897
  }
812
898
  function read(project, file) {
813
- return readFileSync3(path7.join(project.root, file), "utf8");
899
+ return readFileSync3(path8.join(project.root, file), "utf8");
814
900
  }
815
901
  function cssEntry(config, withPreflight) {
816
902
  const tokens = relativeImport(config.css, `${config.dir}/tailwind/nxds.css`);
@@ -1021,9 +1107,9 @@ var NXDS_VITE_PLUGIN = {
1021
1107
  var VITE_ALIAS = "{ '@': '/src' }";
1022
1108
  function wireViteConfig(project, addTailwind, steps) {
1023
1109
  const configFile = findFile(project, VITE_CONFIGS) ?? "vite.config.ts";
1024
- const addScript = steps.has("add-head-script") && !existsSync6(path7.join(project.root, "nxds.vite.ts"));
1110
+ const addScript = steps.has("add-head-script") && !existsSync7(path8.join(project.root, "nxds.vite.ts"));
1025
1111
  const changes = addScript ? [{ path: "nxds.vite.ts", content: VITE_PLUGIN, created: true }] : [];
1026
- const source = existsSync6(path7.join(project.root, configFile)) ? read(project, configFile) : "";
1112
+ const source = existsSync7(path8.join(project.root, configFile)) ? read(project, configFile) : "";
1027
1113
  const program2 = source ? parseModule(source, configFile) : void 0;
1028
1114
  const object = program2 && configObject(program2);
1029
1115
  const plugins = object && pluginsArray(object);
@@ -1184,7 +1270,7 @@ function wireNextTailwind(project) {
1184
1270
  };
1185
1271
  }
1186
1272
  function createdUnlessExists(project, file, content) {
1187
- if (existsSync6(path7.join(project.root, file))) return NO_CHANGES;
1273
+ if (existsSync7(path8.join(project.root, file))) return NO_CHANGES;
1188
1274
  return { changes: [{ path: file, content, created: true }], manual: [] };
1189
1275
  }
1190
1276
  function planWiring(project, config, addTailwind, setupSteps) {
@@ -1221,9 +1307,10 @@ function planWiring(project, config, addTailwind, setupSteps) {
1221
1307
  // src/commands/check.ts
1222
1308
  async function diff(cwd, registry, names) {
1223
1309
  const config = requireConfig(cwd);
1224
- const latest = await loadRelease(registry, "latest");
1310
+ const target = { ...config, react: reactMajor(readProject(cwd)) };
1311
+ const latest = await loadRelease(registry, "latest", target.react);
1225
1312
  const previous = await fetchRelease(registry, config.version);
1226
- const previousFiles = previous && releaseFiles(previous, config.framework);
1313
+ const previousFiles = previous && releaseFiles(previous, config);
1227
1314
  const { known, removed } = splitInstalled(latest, config.items);
1228
1315
  const items = resolveItems(
1229
1316
  latest,
@@ -1248,7 +1335,7 @@ async function diff(cwd, registry, names) {
1248
1335
  return `has your edits; ${update2} merges nxds's changes into it, ${overwrite} replaces it`;
1249
1336
  }
1250
1337
  const files = items.flatMap(
1251
- (item) => variantOf(item, config.framework).files.map((file) => {
1338
+ (item) => variantFor(item, target).files.map((file) => {
1252
1339
  const state = fileState(cwd, config, file, previousFiles);
1253
1340
  return {
1254
1341
  item: item.name,
@@ -1286,14 +1373,14 @@ function tailwindMessage(project) {
1286
1373
  return `tailwindcss ${declared}. nxds needs Tailwind CSS 4.`;
1287
1374
  }
1288
1375
  function read2(root, file) {
1289
- if (!file || !existsSync7(path8.join(root, file))) return "";
1290
- return readFileSync4(path8.join(root, file), "utf8");
1376
+ if (!file || !existsSync8(path9.join(root, file))) return "";
1377
+ return readFileSync4(path9.join(root, file), "utf8");
1291
1378
  }
1292
1379
  function sourcesNxdsDir(config, css) {
1293
- const entryDir2 = path8.posix.dirname(config.css);
1380
+ const entryDir2 = path9.posix.dirname(config.css);
1294
1381
  return [...css.matchAll(/@source\s+(['"])(.*?)\1/g)].some(([, , source]) => {
1295
1382
  const base = (source ?? "").split(/[*{[]/)[0] ?? "";
1296
- const folder = path8.posix.join(entryDir2, base).replace(/\/+$/, "");
1383
+ const folder = path9.posix.join(entryDir2, base).replace(/\/+$/, "");
1297
1384
  return folder === "." || /^\.\.(\/\.\.)*$/.test(folder) || config.dir === folder || config.dir.startsWith(`${folder}/`);
1298
1385
  });
1299
1386
  }
@@ -1307,10 +1394,42 @@ function unscannedReason(cwd, config, css) {
1307
1394
  }
1308
1395
  return null;
1309
1396
  }
1397
+ function installedMajor(project, name) {
1398
+ const version = installedVersion(project, name);
1399
+ return version === null ? null : major2(version);
1400
+ }
1401
+ function reactChecks(project, config) {
1402
+ let react;
1403
+ try {
1404
+ react = reactMajor(project);
1405
+ } catch (error) {
1406
+ if (!(error instanceof CliError)) throw error;
1407
+ return [{ name: "react", ok: false, message: error.message }];
1408
+ }
1409
+ const checks = [
1410
+ {
1411
+ name: "react",
1412
+ ok: config.react === react,
1413
+ message: config.react === react ? `The files are for React ${react}, which this app uses` : `The files are for React ${config.react}, and this app uses React ${react}. Run \`${CLI_COMMAND} update\` to switch them.`
1414
+ }
1415
+ ];
1416
+ const reactInstalled = installedMajor(project, "react");
1417
+ if (reactInstalled === null) return checks;
1418
+ for (const name of ["react-dom", "@types/react", "@types/react-dom"]) {
1419
+ if (name.startsWith("@types/") && !(name in project.dependencies)) continue;
1420
+ const installed = installedMajor(project, name);
1421
+ checks.push({
1422
+ name,
1423
+ ok: installed === reactInstalled,
1424
+ message: installed === null ? `${name} is not installed` : installed === reactInstalled ? `${name} ${installed} matches react ${reactInstalled}` : `${name} ${installed} is installed with react ${reactInstalled}. Install the same major as react.`
1425
+ });
1426
+ }
1427
+ return checks;
1428
+ }
1310
1429
  function packageProblems(project, config, items) {
1311
1430
  const needed = Object.assign(
1312
1431
  {},
1313
- ...items.map((item) => variantOf(item, config.framework).packages.npm)
1432
+ ...items.map((item) => variantFor(item, config).packages.npm)
1314
1433
  );
1315
1434
  return Object.entries(needed).flatMap(([name, range]) => {
1316
1435
  if (!(name in project.dependencies))
@@ -1350,6 +1469,7 @@ async function doctor(cwd, registry) {
1350
1469
  majorVersion(project, "tailwindcss") === 4,
1351
1470
  tailwindMessage(project)
1352
1471
  );
1472
+ checks.push(...reactChecks(project, config));
1353
1473
  const css = read2(cwd, config.css);
1354
1474
  const importsTokens = css.includes("tailwind/nxds.css");
1355
1475
  check(
@@ -1365,7 +1485,7 @@ async function doctor(cwd, registry) {
1365
1485
  );
1366
1486
  const entry = project.setup === "next-app" ? `${entryDir(project)}/layout.tsx` : findFile(project, VITE_ENTRIES);
1367
1487
  const entrySource = read2(cwd, entry);
1368
- const cssFile = path8.basename(config.css);
1488
+ const cssFile = path9.basename(config.css);
1369
1489
  const importsCss = entrySource.includes(cssFile);
1370
1490
  check(
1371
1491
  "css import",
@@ -1393,8 +1513,8 @@ async function doctor(cwd, registry) {
1393
1513
  );
1394
1514
  const items = resolveItems(catalog, config.framework, known);
1395
1515
  const missingFiles = items.flatMap(
1396
- (item) => variantOf(item, config.framework).files.filter(
1397
- (file) => !existsSync7(path8.join(cwd, config.dir, file.path))
1516
+ (item) => variantFor(item, config).files.filter(
1517
+ (file) => !existsSync8(path9.join(cwd, config.dir, file.path))
1398
1518
  ).map((file) => `${item.name}: ${file.path}`)
1399
1519
  );
1400
1520
  check(
@@ -1406,7 +1526,7 @@ async function doctor(cwd, registry) {
1406
1526
  cwd,
1407
1527
  config,
1408
1528
  items.flatMap(
1409
- (item) => variantOf(item, config.framework).files.map((file) => file.path)
1529
+ (item) => variantFor(item, config).files.map((file) => file.path)
1410
1530
  )
1411
1531
  ).map((file) => `${config.dir}/${file}`);
1412
1532
  check(
@@ -1436,13 +1556,347 @@ function report(checks) {
1436
1556
  };
1437
1557
  }
1438
1558
 
1439
- // src/commands/install.ts
1440
- import { existsSync as existsSync9, mkdirSync as mkdirSync2, writeFileSync as writeFileSync4 } from "fs";
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";
1441
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
+
1893
+ // src/commands/install.ts
1894
+ import { existsSync as existsSync11, mkdirSync as mkdirSync3, writeFileSync as writeFileSync5 } from "fs";
1895
+ import path12 from "path";
1442
1896
 
1443
1897
  // src/install.ts
1444
- import { existsSync as existsSync8, readFileSync as readFileSync5, writeFileSync as writeFileSync3 } from "fs";
1445
- import path9 from "path";
1898
+ import { existsSync as existsSync10, readFileSync as readFileSync6, writeFileSync as writeFileSync4 } from "fs";
1899
+ import path11 from "path";
1446
1900
  function emptyInstallResult() {
1447
1901
  return {
1448
1902
  items: [],
@@ -1458,11 +1912,11 @@ function emptyInstallResult() {
1458
1912
  }
1459
1913
  function addCssImports(project, config, files, dryRun) {
1460
1914
  if (files.length === 0) return [];
1461
- const entry = path9.join(project.root, config.css);
1462
- const current = existsSync8(entry) ? readFileSync5(entry, "utf8") : "";
1915
+ const entry = path11.join(project.root, config.css);
1916
+ const current = existsSync10(entry) ? readFileSync6(entry, "utf8") : "";
1463
1917
  const added = [...new Set(files)].map((file) => cssImportLine(config, file)).filter((line) => !current.includes(line));
1464
1918
  if (added.length > 0 && !dryRun) {
1465
- writeFileSync3(entry, `${current.trimEnd()}
1919
+ writeFileSync4(entry, `${current.trimEnd()}
1466
1920
  ${added.join("\n")}
1467
1921
  `);
1468
1922
  }
@@ -1470,7 +1924,7 @@ ${added.join("\n")}
1470
1924
  }
1471
1925
  function installItems(catalog, project, config, names, previous, options) {
1472
1926
  const items = resolveItems(catalog, config.framework, names);
1473
- const variants = items.map((item) => variantOf(item, config.framework));
1927
+ const variants = items.map((item) => variantFor(item, config));
1474
1928
  const files = new Map(
1475
1929
  variants.flatMap((variant) => variant.files).map((file) => [file.path, file])
1476
1930
  );
@@ -1536,6 +1990,12 @@ function installLines(config, result, options) {
1536
1990
  function settled(result) {
1537
1991
  return result.skipped.length === 0 && result.conflicted.length === 0 && result.unresolved.length === 0;
1538
1992
  }
1993
+ function assertFilesMatchReact(config, react) {
1994
+ if (config.react === react) return;
1995
+ throw new CliError(
1996
+ `nxds.json has React ${config.react} files, and this app uses React ${react}. Run \`${CLI_COMMAND} update\` to switch them, then run this command again.`
1997
+ );
1998
+ }
1539
1999
  function nxdsDir(dir) {
1540
2000
  const normalized = relativePath(dir);
1541
2001
  if (normalized) return normalized;
@@ -1568,11 +2028,17 @@ async function init(cwd, registry, options) {
1568
2028
  "nxds components are TypeScript. Add a tsconfig.json to your app, then run init again."
1569
2029
  );
1570
2030
  }
2031
+ const react = reactMajor(project);
1571
2032
  const addTailwind = !("tailwindcss" in project.dependencies);
1572
2033
  if (!addTailwind) assertTailwind4(project);
1573
2034
  const dir = options.dir ? nxdsDir(options.dir) : void 0;
1574
2035
  const existing = readConfig(cwd);
1575
- const catalog = await loadRelease(registry, existing?.version ?? "latest");
2036
+ const catalog = await loadRelease(
2037
+ registry,
2038
+ existing?.version ?? "latest",
2039
+ react
2040
+ );
2041
+ if (existing) assertFilesMatchReact(existing, react);
1576
2042
  if (existing && dir && dir !== existing.dir) {
1577
2043
  throw new CliError(
1578
2044
  `nxds is already set up in ${existing.dir}. Run init without --dir, or move the folder and change "dir" in nxds.json.`
@@ -1580,12 +2046,13 @@ async function init(cwd, registry, options) {
1580
2046
  }
1581
2047
  const config = existing ?? {
1582
2048
  framework: "react",
2049
+ react,
1583
2050
  version: catalog.version,
1584
2051
  dir: dir ?? defaultDir(project),
1585
2052
  css: cssEntryPath(project),
1586
2053
  items: []
1587
2054
  };
1588
- const setup = setupVariant(catalog, config.framework);
2055
+ const setup = setupVariant(catalog, config);
1589
2056
  const wiring = planWiring(project, config, addTailwind, setup.steps);
1590
2057
  const tailwind = packagesToInstall(
1591
2058
  project,
@@ -1604,9 +2071,9 @@ async function init(cwd, registry, options) {
1604
2071
  );
1605
2072
  if (!options.dryRun) {
1606
2073
  for (const change of wiring.changes) {
1607
- const target = path10.join(project.root, change.path);
1608
- mkdirSync2(path10.dirname(target), { recursive: true });
1609
- 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);
1610
2077
  }
1611
2078
  writeConfig(cwd, config);
1612
2079
  }
@@ -1616,6 +2083,7 @@ async function init(cwd, registry, options) {
1616
2083
  return {
1617
2084
  data: {
1618
2085
  setup: project.setup,
2086
+ react: config.react,
1619
2087
  version: config.version,
1620
2088
  dir: config.dir,
1621
2089
  css: config.css,
@@ -1627,7 +2095,7 @@ async function init(cwd, registry, options) {
1627
2095
  manual: wiring.manual
1628
2096
  },
1629
2097
  lines: [
1630
- `nxds ${config.version} in ${config.dir}`,
2098
+ `nxds ${config.version} in ${config.dir}, for React ${config.react}`,
1631
2099
  ...installLines(config, { ...installed, packages }, options),
1632
2100
  ...wiring.changes.map(
1633
2101
  (change) => `${change.created ? createVerb : editVerb} ${change.path}`
@@ -1649,10 +2117,12 @@ function manualLines(manual) {
1649
2117
  }
1650
2118
  async function add(cwd, registry, names, options) {
1651
2119
  const config = requireConfig(cwd);
1652
- const catalog = await loadRelease(registry, config.version);
2120
+ const project = readProject(cwd);
2121
+ assertFilesMatchReact(config, reactMajor(project));
2122
+ const catalog = await loadRelease(registry, config.version, config.react);
1653
2123
  const installed = installItems(
1654
2124
  catalog,
1655
- readProject(cwd),
2125
+ project,
1656
2126
  config,
1657
2127
  names,
1658
2128
  null,
@@ -1670,32 +2140,34 @@ async function add(cwd, registry, names, options) {
1670
2140
  ok: settled(installed)
1671
2141
  };
1672
2142
  }
1673
- function filesOf(catalog, framework, names) {
2143
+ function filesOf(catalog, target, names) {
1674
2144
  return new Set(
1675
- resolveItems(catalog, framework, names).flatMap(
1676
- (item) => variantOf(item, framework).files.map((file) => file.path)
2145
+ resolveItems(catalog, target.framework, names).flatMap(
2146
+ (item) => variantFor(item, target).files.map((file) => file.path)
1677
2147
  )
1678
2148
  );
1679
2149
  }
1680
2150
  async function update(cwd, registry, options) {
1681
2151
  const config = requireConfig(cwd);
2152
+ const project = readProject(cwd);
1682
2153
  const from = config.version;
1683
- const latest = await loadRelease(registry, "latest");
2154
+ const fromReact = config.react;
2155
+ const react = reactMajor(project);
2156
+ const latest = await loadRelease(registry, "latest", react);
1684
2157
  const { known, removed } = splitInstalled(latest, config.items);
1685
- const current = filesOf(latest, config.framework, known);
2158
+ const current = filesOf(latest, { ...config, react }, known);
1686
2159
  const previous = await fetchRelease(registry, from);
1687
- const installedBefore = previous ? filesOf(
1688
- previous,
1689
- config.framework,
1690
- splitInstalled(previous, config.items).known
1691
- ) : /* @__PURE__ */ new Set();
1692
- const unresolved = from === latest.version || options.overwrite ? [] : conflictedFiles(cwd, config, /* @__PURE__ */ new Set([...current, ...installedBefore]));
2160
+ const installedBefore = previous ? filesOf(previous, config, splitInstalled(previous, config.items).known) : /* @__PURE__ */ new Set();
2161
+ const unchanged = from === latest.version && fromReact === react;
2162
+ const unresolved = unchanged || options.overwrite ? [] : conflictedFiles(cwd, config, /* @__PURE__ */ new Set([...current, ...installedBefore]));
1693
2163
  if (unresolved.length > 0) {
1694
2164
  const data2 = {
1695
2165
  ...emptyInstallResult(),
1696
2166
  dryRun: options.dryRun,
1697
2167
  from,
1698
2168
  version: from,
2169
+ fromReact,
2170
+ react: fromReact,
1699
2171
  unresolved,
1700
2172
  removed: [],
1701
2173
  orphaned: []
@@ -1710,24 +2182,30 @@ async function update(cwd, registry, options) {
1710
2182
  };
1711
2183
  }
1712
2184
  const orphaned = [...installedBefore].filter(
1713
- (file) => !current.has(file) && existsSync9(path10.join(cwd, config.dir, file))
2185
+ (file) => !current.has(file) && existsSync11(path12.join(cwd, config.dir, file))
1714
2186
  );
2187
+ const previousFiles = previous && releaseFiles(previous, config);
1715
2188
  config.items = known;
1716
2189
  const installed = installItems(
1717
2190
  latest,
1718
- readProject(cwd),
1719
- config,
2191
+ project,
2192
+ { ...config, react },
1720
2193
  known,
1721
- previous && releaseFiles(previous, config.framework),
2194
+ previousFiles,
1722
2195
  options
1723
2196
  );
1724
- const stays = from !== latest.version && installed.skipped.length > 0;
1725
- if (!stays) config.version = latest.version;
2197
+ const stays = !unchanged && installed.skipped.length > 0;
2198
+ if (!stays) {
2199
+ config.version = latest.version;
2200
+ config.react = react;
2201
+ }
1726
2202
  if (!options.dryRun) writeConfig(cwd, config);
1727
2203
  const data = {
1728
2204
  dryRun: options.dryRun,
1729
2205
  from,
1730
2206
  version: config.version,
2207
+ fromReact,
2208
+ react: config.react,
1731
2209
  ...installed,
1732
2210
  removed,
1733
2211
  orphaned
@@ -1745,6 +2223,9 @@ async function update(cwd, registry, options) {
1745
2223
  ...orphaned.map(
1746
2224
  (file) => `No installed item uses ${config.dir}/${file} any more. Delete it once nothing in your app imports it.`
1747
2225
  ),
2226
+ ...fromReact !== react && !stays ? [
2227
+ `${options.dryRun ? "Would switch" : "Switched"} the files from React ${fromReact} to React ${react}.`
2228
+ ] : [],
1748
2229
  stays ? `nxds.json stays on ${from} until every file above is resolved or replaced.` : `nxds ${from === latest.version ? "is on" : `${options.dryRun ? "would move" : "moved"} from ${from} to`} ${latest.version}.`
1749
2230
  ],
1750
2231
  ok: settled(installed)
@@ -1755,7 +2236,7 @@ async function update(cwd, registry, options) {
1755
2236
  function context(command) {
1756
2237
  const { cwd, registry } = command.optsWithGlobals();
1757
2238
  return {
1758
- cwd: path11.resolve(cwd ?? process.cwd()),
2239
+ cwd: path13.resolve(cwd ?? process.cwd()),
1759
2240
  registry: resolveRegistry(registry)
1760
2241
  };
1761
2242
  }
@@ -1777,7 +2258,7 @@ function program(done, write) {
1777
2258
  // Errors are printed once, by `main`, as text or JSON.
1778
2259
  outputError: () => {
1779
2260
  }
1780
- }).exitOverride().name(CLI_COMMAND).description("Add nxds to your app.").version("0.2.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(
1781
2262
  new Option(
1782
2263
  "--registry <url|path>",
1783
2264
  "where to get nxds releases (default: https://nxds.dev/r)"
@@ -1837,6 +2318,28 @@ Docs: https://nxds.dev/getting-started/install`
1837
2318
  const { cwd, registry } = context(command);
1838
2319
  done(await diff(cwd, registry, items));
1839
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
+ });
1840
2343
  cli.command("doctor").description("check that nxds is wired up correctly").action(async (_options, command) => {
1841
2344
  const { cwd, registry } = context(command);
1842
2345
  done(await doctor(cwd, registry));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nxds-cli",
3
- "version": "0.2.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",