nxds-cli 0.0.0 → 0.3.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 +833 -428
  2. package/package.json +15 -11
package/dist/index.js CHANGED
@@ -1,27 +1,176 @@
1
1
  #!/usr/bin/env node
2
2
 
3
- // src/index.ts
4
- import path10 from "path";
5
- import { parseArgs } from "util";
3
+ // src/cli.ts
4
+ import { Command, CommanderError, Option } from "commander";
5
+ import path12 from "path";
6
+
7
+ // ../catalog/scripts/catalog-schema.ts
8
+ import path from "path";
9
+ import { valid, validRange } from "semver";
10
+ import { z } from "zod";
11
+ function relativePath(value) {
12
+ const normalized = path.posix.normalize(value.replaceAll("\\", "/")).replace(/\/+$/, "");
13
+ if ([...normalized].some((char) => char < " " || char === "\x7F") || path.posix.isAbsolute(normalized) || /^[A-Za-z]:/.test(normalized) || normalized === "." || normalized === ".." || normalized.startsWith("../")) {
14
+ return null;
15
+ }
16
+ return normalized;
17
+ }
18
+ var relativePathSchema = z.string().refine(
19
+ (value) => relativePath(value) === value,
20
+ "must be a relative path that stays inside its folder"
21
+ );
22
+ var versionSchema = z.string().refine((value) => valid(value) === value, "must be a release version");
23
+ var rangeSchema = z.string().refine(
24
+ (value) => /^[ -~]+$/.test(value) && value === value.trim() && validRange(value) !== null,
25
+ "must be a semver range"
26
+ );
27
+ var npmNameSchema = z.string().regex(
28
+ /^(@[a-z0-9-~][a-z0-9-._~]*\/)?[a-z0-9-~][a-z0-9-._~]*$/,
29
+ "must be an npm package name"
30
+ );
31
+ var frameworkSchema = z.enum(["react", "vue"]);
32
+ function isFramework(value) {
33
+ return frameworkSchema.safeParse(value).success;
34
+ }
35
+ function variantsSchema(variant) {
36
+ return z.object({
37
+ react: variant.optional(),
38
+ vue: variant.optional()
39
+ });
40
+ }
41
+ var catalogFileSchema = z.object({
42
+ /** Relative to the project's nxds folder. */
43
+ path: relativePathSchema,
44
+ content: z.string()
45
+ });
46
+ var setupStepSchema = z.enum([
47
+ "add-css-import",
48
+ "add-head-script",
49
+ "wrap-root-with-provider"
50
+ ]);
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
+ });
57
+ function variantWith(packages) {
58
+ return z.object({
59
+ /** Other items this one needs, installed first. */
60
+ items: z.array(z.string()),
61
+ packages,
62
+ files: z.array(catalogFileSchema),
63
+ /** Files in `files` the nxds CSS entry imports. */
64
+ css: z.array(relativePathSchema),
65
+ steps: z.array(setupStepSchema),
66
+ react18: react18Schema.optional()
67
+ }).refine(
68
+ ({ css, files }) => css.every((entry) => files.some((file) => file.path === entry)),
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
+ }
78
+ );
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
+ }
94
+ var variantSchema = variantWith(z.object({ npm: npmPackagesSchema }));
95
+ var setupVariantSchema = variantWith(
96
+ z.object({
97
+ npm: npmPackagesSchema,
98
+ /** The Tailwind range `init` installs, with the app's build plugin, when the app has no Tailwind. */
99
+ tailwind: rangeSchema
100
+ })
101
+ );
102
+ var itemInfo = {
103
+ name: z.string(),
104
+ title: z.string(),
105
+ description: z.string(),
106
+ docs: z.string()
107
+ };
108
+ var setupItemSchema = z.object({
109
+ ...itemInfo,
110
+ kind: z.literal("setup"),
111
+ variants: variantsSchema(setupVariantSchema)
112
+ });
113
+ var catalogItemSchema = z.discriminatedUnion("kind", [
114
+ setupItemSchema,
115
+ z.object({
116
+ ...itemInfo,
117
+ kind: z.enum(["component", "block"]),
118
+ variants: variantsSchema(variantSchema)
119
+ })
120
+ ]);
121
+ var CATALOG_SCHEMA = 1;
122
+ var catalogSchema = z.object({
123
+ schema: z.literal(CATALOG_SCHEMA),
124
+ /** The `@nxds/react` version this catalog was built from. */
125
+ version: versionSchema,
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. */
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]),
130
+ items: z.array(catalogItemSchema)
131
+ });
132
+ function lines(issues, prefix) {
133
+ return issues.flatMap((issue) => {
134
+ const at = [...prefix, ...issue.path];
135
+ if (issue.code === "invalid_key") return lines(issue.issues, at);
136
+ return [
137
+ `${at.length > 0 ? at.map(String).join(".") : "(root)"}: ${issue.message}`
138
+ ];
139
+ });
140
+ }
141
+ function issueLines(error) {
142
+ return lines(error.issues, []);
143
+ }
6
144
 
7
145
  // src/catalog.ts
8
- import { existsSync as existsSync2 } from "fs";
146
+ import { existsSync } from "fs";
9
147
  import path2 from "path";
10
- import { valid as valid2, validRange } from "semver";
148
+ import { EnvHttpProxyAgent, fetch } from "undici";
11
149
 
12
- // src/config.ts
13
- import { existsSync, writeFileSync } from "fs";
14
- import path from "path";
15
- import { valid } from "semver";
150
+ // src/cli-command.ts
151
+ var CLI_COMMAND = "npx nxds-cli@latest";
16
152
 
17
153
  // src/errors.ts
18
154
  import { readFileSync } from "fs";
155
+ var EXIT_CODES = {
156
+ ok: 0,
157
+ /** A network or registry failure, an invalid file, or a bug in nxds. */
158
+ error: 1,
159
+ /** A bad flag, unknown command or item, or a missing argument. */
160
+ usage: 2,
161
+ /** The command ran, and left something for the person to do: conflicts, kept files, manual steps or failed checks. */
162
+ attention: 3
163
+ };
19
164
  var CliError = class extends Error {
165
+ exitCode = EXIT_CODES.error;
166
+ };
167
+ var UsageError = class extends CliError {
168
+ exitCode = EXIT_CODES.usage;
20
169
  };
21
170
  function readJson(file) {
22
- const text2 = readFileSync(file, "utf8");
171
+ const text = readFileSync(file, "utf8");
23
172
  try {
24
- return JSON.parse(text2);
173
+ return JSON.parse(text);
25
174
  } catch (error) {
26
175
  throw new CliError(
27
176
  `${file} is not valid JSON (${error.message}). Fix it, then run the command again.`
@@ -29,57 +178,11 @@ function readJson(file) {
29
178
  }
30
179
  }
31
180
 
32
- // src/config.ts
33
- var CONFIG_FILE = "nxds.json";
34
- function configPath(root) {
35
- return path.join(root, CONFIG_FILE);
36
- }
37
- function projectPath(value) {
38
- const normalized = path.posix.normalize(value.replaceAll("\\", "/")).replace(/\/+$/, "");
39
- if (path.posix.isAbsolute(normalized) || /^[A-Za-z]:/.test(normalized) || normalized === "." || normalized === ".." || normalized.startsWith("../")) {
40
- return null;
41
- }
42
- return normalized;
43
- }
44
- function invalidField(value) {
45
- if (value.framework !== "react") return "framework";
46
- if (typeof value.version !== "string" || valid(value.version) !== value.version)
47
- return "version";
48
- for (const field of ["dir", "css"]) {
49
- const file = value[field];
50
- if (typeof file !== "string" || projectPath(file) !== file) return field;
51
- }
52
- const { items } = value;
53
- if (!Array.isArray(items) || !items.every((item) => typeof item === "string"))
54
- return "items";
55
- return null;
56
- }
57
- function readConfig(root) {
58
- const file = configPath(root);
59
- if (!existsSync(file)) return null;
60
- const value = readJson(file);
61
- if (typeof value !== "object" || value === null || Array.isArray(value)) {
62
- throw new CliError(`${CONFIG_FILE} must be a JSON object.`);
63
- }
64
- const field = invalidField(value);
65
- if (field === null) return value;
66
- throw new CliError(
67
- `${CONFIG_FILE} has a missing or invalid "${field}". Fix it, or delete ${CONFIG_FILE} and run \`nxds init\` again.`
68
- );
69
- }
70
- function requireConfig(root) {
71
- const config = readConfig(root);
72
- if (config) return config;
73
- throw new CliError(`nxds is not set up here. Run \`nxds init\` first.`);
74
- }
75
- function writeConfig(root, config) {
76
- writeFileSync(configPath(root), `${JSON.stringify(config, null, 2)}
77
- `);
78
- }
79
-
80
181
  // src/catalog.ts
81
182
  var SETUP = "setup";
82
183
  var DEFAULT_REGISTRY = "https://nxds.dev/r";
184
+ var FETCH_TIMEOUT_MS = 3e4;
185
+ var dispatcher = new EnvHttpProxyAgent();
83
186
  function isUrl(registry) {
84
187
  return /^https?:\/\//.test(registry);
85
188
  }
@@ -87,13 +190,26 @@ function resolveRegistry(option) {
87
190
  if (!option) return DEFAULT_REGISTRY;
88
191
  return isUrl(option) ? option.replace(/\/+$/, "") : path2.resolve(option);
89
192
  }
193
+ function timeoutError(url, error) {
194
+ if (!(error instanceof Error) || error.name !== "TimeoutError") return null;
195
+ return new CliError(
196
+ `${url} did not answer within ${FETCH_TIMEOUT_MS / 1e3}s. Check your connection, then run the command again.`
197
+ );
198
+ }
90
199
  async function fetchJson(url) {
91
200
  let response;
92
201
  try {
93
- response = await fetch(url);
202
+ response = await fetch(url, {
203
+ dispatcher,
204
+ signal: AbortSignal.timeout(FETCH_TIMEOUT_MS)
205
+ });
94
206
  } catch (error) {
207
+ const timeout = timeoutError(url, error);
208
+ if (timeout) throw timeout;
209
+ const { message, cause } = error;
210
+ const reason = cause instanceof Error ? cause.message : message;
95
211
  throw new CliError(
96
- `Could not reach ${url} (${error.message}). Check your connection, then run the command again.`
212
+ `Could not reach ${url} (${reason}). Check your connection, or set HTTPS_PROXY if you are behind a proxy, then run the command again.`
97
213
  );
98
214
  }
99
215
  if (response.status === 404) return null;
@@ -105,7 +221,7 @@ async function fetchJson(url) {
105
221
  try {
106
222
  return await response.json();
107
223
  } catch (error) {
108
- throw new CliError(
224
+ throw timeoutError(url, error) ?? new CliError(
109
225
  `${url} is not valid JSON (${error.message}). Run the command again later.`
110
226
  );
111
227
  }
@@ -116,104 +232,52 @@ function registryLocation(registry, file) {
116
232
  async function readRegistryFile(registry, file) {
117
233
  const location = registryLocation(registry, file);
118
234
  if (isUrl(registry)) return fetchJson(location);
119
- return existsSync2(location) ? readJson(location) : null;
120
- }
121
- var KINDS = /* @__PURE__ */ new Set(["setup", "component", "block"]);
122
- var STEPS = new Set(
123
- Object.keys({
124
- "add-css-import": true,
125
- "add-head-script": true,
126
- "wrap-root-with-provider": true
127
- })
128
- );
129
- var NPM_NAME = /^(@[a-z0-9-~][a-z0-9-._~]*\/)?[a-z0-9-~][a-z0-9-._~]*$/;
130
- function isRange(value) {
131
- return typeof value === "string" && /^[ -~]+$/.test(value) && value === value.trim() && validRange(value) !== null;
132
- }
133
- function isRecord(value) {
134
- return typeof value === "object" && value !== null && !Array.isArray(value);
135
- }
136
- function isStrings(value) {
137
- return Array.isArray(value) && value.every((entry) => typeof entry === "string");
138
- }
139
- function fileProblem(file) {
140
- if (!isRecord(file) || typeof file.content !== "string") {
141
- return "has a file without content";
142
- }
143
- if (typeof file.path !== "string" || projectPath(file.path) !== file.path) {
144
- return `has a file path outside the nxds folder: ${String(file.path)}`;
145
- }
146
- return null;
235
+ return existsSync(location) ? readJson(location) : null;
147
236
  }
148
- function variantProblem(variant, kind) {
149
- if (!isRecord(variant)) return "has a variant that is not an object";
150
- const { packages } = variant;
151
- if (!isRecord(packages) || !isRecord(packages.npm) || !Object.entries(packages.npm).every(
152
- ([name, range]) => NPM_NAME.test(name) && isRange(range)
153
- ) || kind === "setup" && !isRange(packages.tailwind)) {
154
- return 'has invalid "packages"';
155
- }
156
- for (const field of ["items", "css"]) {
157
- if (!isStrings(variant[field])) return `has invalid "${field}"`;
158
- }
159
- const { steps } = variant;
160
- if (!Array.isArray(steps) || !steps.every((step) => STEPS.has(step))) {
161
- return 'has invalid "steps"';
162
- }
163
- if (!Array.isArray(variant.files)) return 'has invalid "files"';
164
- return variant.files.map(fileProblem).find(Boolean) ?? null;
165
- }
166
- function itemProblem(item) {
167
- if (!isRecord(item) || typeof item.name !== "string") {
168
- return "has an item without a name";
169
- }
170
- const fields = ["title", "description", "docs"];
171
- if (!KINDS.has(item.kind) || fields.some((field) => typeof item[field] !== "string") || !isRecord(item.variants)) {
172
- return `"${item.name}" is not a catalog item`;
173
- }
174
- const problem = Object.values(item.variants).map((variant) => variantProblem(variant, item.kind)).find(Boolean);
175
- return problem ? `"${item.name}" ${problem}` : null;
176
- }
177
- function releaseProblem(value, version) {
178
- if (!isRecord(value)) return "it is not a JSON object";
179
- if (value.schema !== 1) return 'its "schema" is not 1';
180
- if (typeof value.version !== "string" || valid2(value.version) !== value.version) {
181
- return 'its "version" is not a release version';
182
- }
183
- if (version !== "latest" && value.version !== version) {
184
- return `it holds nxds ${value.version}`;
185
- }
186
- if (!isStrings(value.frameworks)) return 'its "frameworks" is invalid';
187
- if (!Array.isArray(value.items)) return 'its "items" is invalid';
188
- return value.items.map(itemProblem).find(Boolean) ?? null;
237
+ function isNewerSchema(value) {
238
+ return typeof value === "object" && value !== null && "schema" in value && typeof value.schema === "number" && value.schema > CATALOG_SCHEMA;
189
239
  }
190
240
  async function fetchRelease(registry, version) {
191
241
  const file = `${version}.json`;
192
242
  const value = await readRegistryFile(registry, file);
193
243
  if (value === null) return null;
194
- if (isRecord(value) && typeof value.schema === "number" && value.schema > 1) {
244
+ if (isNewerSchema(value)) {
245
+ const release = version === "latest" ? "The latest nxds release" : `nxds ${version}`;
195
246
  throw new CliError(
196
- `nxds ${version} needs a newer CLI. Run \`npx nxds@latest\` instead.`
247
+ `${release} needs a newer nxds-cli than ${"0.3.0"}. Run \`${CLI_COMMAND}\` to get the newest one.`
197
248
  );
198
249
  }
199
- const problem = releaseProblem(value, version);
200
- if (problem === null) return value;
201
- throw new CliError(
202
- `${registryLocation(registry, file)} is not a valid nxds release: ${problem}. Check \`--registry\`, or run the command again later.`
250
+ const invalid = (problems) => new CliError(
251
+ `${registryLocation(registry, file)} is not a valid nxds release:
252
+ ${problems.map((line) => ` ${line}`).join("\n")}
253
+ Check \`--registry\`, or run the command again later.`
203
254
  );
255
+ const parsed = catalogSchema.safeParse(value);
256
+ if (!parsed.success) throw invalid(issueLines(parsed.error));
257
+ if (version !== "latest" && parsed.data.version !== version) {
258
+ throw invalid([`version: it holds nxds ${parsed.data.version}`]);
259
+ }
260
+ return parsed.data;
204
261
  }
205
- async function loadRelease(registry, version) {
262
+ async function loadRelease(registry, version, react) {
206
263
  const catalog = await fetchRelease(registry, version);
207
- 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.";
208
272
  throw new CliError(
209
- version === "latest" ? `${registry} has no nxds release.` : `${registry} has no nxds ${version}, the release this app is on. Run \`nxds update\` to move to the latest release.`
273
+ `nxds ${catalog.version} has no React ${react} files. ${fix}`
210
274
  );
211
275
  }
212
276
  function findItem(catalog, name) {
213
277
  const item = catalog.items.find((candidate) => candidate.name === name);
214
278
  if (item) return item;
215
- throw new CliError(
216
- `nxds ${catalog.version} has no item named "${name}". Run \`nxds search ${name}\` to find the right name, or \`nxds update\` if it's newer.`
279
+ throw new UsageError(
280
+ `nxds ${catalog.version} has no item named "${name}". Run \`${CLI_COMMAND} search ${name}\` to find the right name, or \`${CLI_COMMAND} update\` if it's newer.`
217
281
  );
218
282
  }
219
283
  function variantOf(item, framework) {
@@ -221,12 +285,15 @@ function variantOf(item, framework) {
221
285
  if (variant) return variant;
222
286
  throw new CliError(`${item.title} is not available for ${framework} yet.`);
223
287
  }
224
- function setupVariant(catalog, framework) {
288
+ function variantFor(item, target) {
289
+ return variantForReact(variantOf(item, target.framework), target.react);
290
+ }
291
+ function setupVariant(catalog, target) {
225
292
  const setup = catalog.items.find((item) => item.name === SETUP);
226
293
  if (setup?.kind !== "setup") {
227
294
  throw new Error(`nxds ${catalog.version} has no "${SETUP}" setup item.`);
228
295
  }
229
- return variantOf(setup, framework);
296
+ return variantFor(setup, target);
230
297
  }
231
298
  function resolveItems(catalog, framework, names) {
232
299
  const ordered = [];
@@ -241,10 +308,10 @@ function resolveItems(catalog, framework, names) {
241
308
  names.forEach(visit);
242
309
  return ordered;
243
310
  }
244
- function releaseFiles(catalog, framework) {
311
+ function releaseFiles(catalog, target) {
245
312
  return new Map(
246
- catalog.items.flatMap(
247
- (item) => (item.variants[framework]?.files ?? []).map(
313
+ catalog.items.filter((item) => item.variants[target.framework]).flatMap(
314
+ (item) => variantFor(item, target).files.map(
248
315
  (file) => [file.path, file.content]
249
316
  )
250
317
  )
@@ -258,15 +325,66 @@ function splitInstalled(catalog, installed) {
258
325
  };
259
326
  }
260
327
 
328
+ // src/commands/browse.ts
329
+ import { existsSync as existsSync5 } from "fs";
330
+ import path6 from "path";
331
+
332
+ // src/config.ts
333
+ import { existsSync as existsSync2, writeFileSync } from "fs";
334
+ import path3 from "path";
335
+ import { z as z2 } from "zod";
336
+ var CONFIG_FILE = "nxds.json";
337
+ var configSchema = z2.looseObject({
338
+ /** The CLI installs React only. */
339
+ framework: z2.literal("react"),
340
+ /** The React major the installed files are for. */
341
+ react: reactMajorSchema,
342
+ /** The nxds release every installed file comes from. */
343
+ version: versionSchema,
344
+ /** The nxds folder, relative to the project root. */
345
+ dir: relativePathSchema,
346
+ /** The nxds CSS entry, relative to the project root. */
347
+ css: relativePathSchema,
348
+ /** Installed items, in install order. */
349
+ items: z2.array(z2.string())
350
+ });
351
+ function configPath(root) {
352
+ return path3.join(root, CONFIG_FILE);
353
+ }
354
+ function readConfig(root) {
355
+ const file = configPath(root);
356
+ if (!existsSync2(file)) return null;
357
+ const parsed = configSchema.safeParse(readJson(file));
358
+ if (parsed.success) return parsed.data;
359
+ throw new CliError(
360
+ `${CONFIG_FILE} is not valid:
361
+ ${issueLines(parsed.error).map((line) => ` ${line}`).join(
362
+ "\n"
363
+ )}
364
+ Fix it, or delete ${CONFIG_FILE} and run \`${CLI_COMMAND} init\` again.`
365
+ );
366
+ }
367
+ function requireConfig(root) {
368
+ const config = readConfig(root);
369
+ if (config) return config;
370
+ throw new CliError(
371
+ `nxds is not set up here. Run \`${CLI_COMMAND} init\` first.`
372
+ );
373
+ }
374
+ function writeConfig(root, config) {
375
+ writeFileSync(configPath(root), `${JSON.stringify(config, null, 2)}
376
+ `);
377
+ }
378
+
261
379
  // src/project.ts
262
380
  import { existsSync as existsSync4 } from "fs";
263
- import path4 from "path";
264
- import { major, minVersion as minVersion2, validRange as validRange3 } from "semver";
381
+ import path5 from "path";
382
+ import { major, minVersion as minVersion2, parse, validRange as validRange3 } from "semver";
265
383
 
266
384
  // src/packages.ts
267
385
  import { spawnSync } from "child_process";
268
386
  import { existsSync as existsSync3 } from "fs";
269
- import path3 from "path";
387
+ import path4 from "path";
270
388
  import { minVersion, satisfies, validRange as validRange2 } from "semver";
271
389
  var ADD_COMMAND = {
272
390
  npm: ["install"],
@@ -287,11 +405,11 @@ function packagesToInstall(project, packages) {
287
405
  function installedVersion(project, name) {
288
406
  let dir = project.root;
289
407
  for (; ; ) {
290
- const manifest = path3.join(dir, "node_modules", name, "package.json");
408
+ const manifest = path4.join(dir, "node_modules", name, "package.json");
291
409
  if (existsSync3(manifest)) {
292
410
  return readJson(manifest).version ?? null;
293
411
  }
294
- const parent = path3.dirname(dir);
412
+ const parent = path4.dirname(dir);
295
413
  if (parent === dir) return null;
296
414
  dir = parent;
297
415
  }
@@ -307,11 +425,11 @@ function installPackages(project, specs, { dev }) {
307
425
  cwd: project.root,
308
426
  stdio: ["ignore", 2, 2]
309
427
  };
310
- const run2 = process.platform === "win32" ? spawnSync(
428
+ const run = process.platform === "win32" ? spawnSync(
311
429
  [project.packageManager, ...args.map((arg) => `"${arg}"`)].join(" "),
312
430
  { ...options, shell: true }
313
431
  ) : spawnSync(project.packageManager, args, options);
314
- if (run2.status !== 0) {
432
+ if (run.status !== 0) {
315
433
  throw new CliError(
316
434
  `\`${project.packageManager} ${args.join(" ")}\` failed. Fix the error above, then run the same nxds command again.`
317
435
  );
@@ -338,7 +456,7 @@ var VITE_CONFIGS = [
338
456
  "vite.config.js"
339
457
  ];
340
458
  function firstExisting(root, candidates) {
341
- return candidates.find((file) => existsSync4(path4.join(root, file)));
459
+ return candidates.find((file) => existsSync4(path5.join(root, file)));
342
460
  }
343
461
  function detectSetup(root, dependencies) {
344
462
  if ("next" in dependencies && firstExisting(
@@ -353,10 +471,10 @@ function detectSetup(root, dependencies) {
353
471
  return null;
354
472
  }
355
473
  function readProject(root) {
356
- const manifestPath = path4.join(root, "package.json");
474
+ const manifestPath = path5.join(root, "package.json");
357
475
  if (!existsSync4(manifestPath)) {
358
476
  throw new CliError(
359
- `No package.json in ${root}. Run nxds from your app's root folder, or pass --cwd.`
477
+ `No package.json in ${root}. Run \`${CLI_COMMAND}\` from your app's root folder, or pass --cwd.`
360
478
  );
361
479
  }
362
480
  const manifest = readJson(manifestPath);
@@ -367,15 +485,36 @@ function readProject(root) {
367
485
  return {
368
486
  root,
369
487
  setup: detectSetup(root, dependencies),
370
- packageManager: LOCKFILES.find(([file]) => existsSync4(path4.join(root, file)))?.[1] ?? "npm",
371
- usesSrc: existsSync4(path4.join(root, "src")),
372
- typescript: existsSync4(path4.join(root, "tsconfig.json")),
488
+ packageManager: LOCKFILES.find(([file]) => existsSync4(path5.join(root, file)))?.[1] ?? "npm",
489
+ usesSrc: existsSync4(path5.join(root, "src")),
490
+ typescript: existsSync4(path5.join(root, "tsconfig.json")),
373
491
  dependencies
374
492
  };
375
493
  }
376
494
  function findFile(project, candidates) {
377
495
  return firstExisting(project.root, candidates);
378
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
+ }
379
518
  function majorVersion(project, name) {
380
519
  const declared = project.dependencies[name];
381
520
  if (declared === void 0) return null;
@@ -385,9 +524,8 @@ function majorVersion(project, name) {
385
524
  }
386
525
 
387
526
  // src/commands/browse.ts
388
- var KINDS2 = ["component", "block"];
389
- function appRelease(cwd, registry) {
390
- return loadRelease(registry, readConfig(cwd)?.version ?? "latest");
527
+ function appRelease(registry, config, react) {
528
+ return loadRelease(registry, config?.version ?? "latest", react);
391
529
  }
392
530
  function summary(item) {
393
531
  return {
@@ -397,8 +535,8 @@ function summary(item) {
397
535
  description: item.description
398
536
  };
399
537
  }
400
- function firstSentence(text2) {
401
- return text2.split(/(?<=\.)\s/)[0]?.replaceAll("`", "") ?? text2;
538
+ function firstSentence(text) {
539
+ return text.split(/(?<=\.)\s/)[0]?.replaceAll("`", "") ?? text;
402
540
  }
403
541
  function summaryLines(items) {
404
542
  const width = Math.max(...items.map((item) => item.name.length));
@@ -407,17 +545,17 @@ function summaryLines(items) {
407
545
  );
408
546
  }
409
547
  async function list(cwd, registry, kind) {
410
- if (kind !== void 0 && !KINDS2.includes(kind)) {
411
- throw new CliError(`--kind takes ${KINDS2.join(" or ")}.`);
412
- }
413
- 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(
414
554
  (item) => item.name !== SETUP && (kind === void 0 || item.kind === kind)
415
555
  );
416
556
  return { data: items.map(summary), lines: summaryLines(items), ok: true };
417
557
  }
418
558
  async function search(cwd, registry, query) {
419
- if (!query)
420
- throw new CliError("Give a search term, such as `nxds search date`.");
421
559
  const needle = query.toLowerCase();
422
560
  const rank = (item) => {
423
561
  if (item.name.includes(needle)) return 0;
@@ -425,7 +563,12 @@ async function search(cwd, registry, query) {
425
563
  if (item.description.toLowerCase().includes(needle)) return 2;
426
564
  return void 0;
427
565
  };
428
- 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) => {
429
572
  const score = rank(item);
430
573
  return score === void 0 ? [] : [{ item, score }];
431
574
  }).sort((a, b) => a.score - b.score).map(({ item }) => item);
@@ -435,20 +578,28 @@ async function search(cwd, registry, query) {
435
578
  ok: true
436
579
  };
437
580
  }
438
- function projectFramework(cwd, catalog) {
439
- const configured = readConfig(cwd)?.framework;
440
- if (configured) return configured;
581
+ function projectFramework(config, catalog) {
582
+ if (config) return config.framework;
441
583
  const [only, ...others] = catalog.frameworks;
442
584
  if (only && others.length === 0) return only;
443
585
  throw new CliError(
444
- "This release has several frameworks. Run `nxds init` first, so nxds knows which one your app uses."
586
+ `This release has several frameworks. Run \`${CLI_COMMAND} init\` first, so nxds knows which one your app uses.`
445
587
  );
446
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
+ }
447
594
  async function view(cwd, registry, name) {
448
- if (!name) throw new CliError("Name an item, such as `nxds view button`.");
449
- const catalog = await appRelease(cwd, registry);
595
+ const config = readConfig(cwd);
596
+ const react = projectReact(cwd);
597
+ const catalog = await appRelease(registry, config, react);
450
598
  const item = findItem(catalog, name);
451
- const variant = variantOf(item, projectFramework(cwd, catalog));
599
+ const variant = variantFor(item, {
600
+ framework: projectFramework(config, catalog),
601
+ react
602
+ });
452
603
  const packages = Object.entries(variant.packages.npm).map(
453
604
  ([pkg, range]) => `${pkg}@${range}`
454
605
  );
@@ -497,7 +648,7 @@ function info(cwd) {
497
648
  `Package manager: ${project.packageManager}`,
498
649
  `TypeScript: ${project.typescript ? "yes" : "no"}`,
499
650
  `Tailwind: ${project.dependencies.tailwindcss ?? "not installed"}`,
500
- config ? `nxds: ${config.version} in ${config.dir}, CSS entry ${config.css}` : "nxds: not set up. Run `nxds init`.",
651
+ config ? `nxds: ${config.version} in ${config.dir}, CSS entry ${config.css}` : `nxds: not set up. Run \`${CLI_COMMAND} init\`.`,
501
652
  ...config && config.items.length > 0 ? [
502
653
  `Installed: ${config.items.filter((item) => item !== SETUP).join(", ") || "setup only"}`
503
654
  ] : []
@@ -508,43 +659,105 @@ function info(cwd) {
508
659
 
509
660
  // src/commands/check.ts
510
661
  import { spawnSync as spawnSync2 } from "child_process";
511
- import { existsSync as existsSync7, readFileSync as readFileSync4 } from "fs";
512
- import path7 from "path";
513
- 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";
514
665
 
515
666
  // src/files.ts
516
- import { existsSync as existsSync5, mkdirSync, readFileSync as readFileSync2, writeFileSync as writeFileSync2 } from "fs";
517
- import path5 from "path";
518
- var NO_PREVIOUS = /* @__PURE__ */ new Map();
667
+ import { existsSync as existsSync6, mkdirSync, readFileSync as readFileSync2, writeFileSync as writeFileSync2 } from "fs";
668
+ import path7 from "path";
669
+ import { merge } from "node-diff3";
670
+ function baseOf(previous, file) {
671
+ if (!previous) return void 0;
672
+ return previous.get(file) ?? "";
673
+ }
674
+ function withLf(content) {
675
+ return `${content.replace(/\r\n?/g, "\n").replace(/\n+$/, "")}
676
+ `;
677
+ }
678
+ function normalize(content) {
679
+ const lines2 = withLf(content).split("\n").map((line) => line.trimEnd());
680
+ return `${lines2.join("\n").trimEnd()}
681
+ `;
682
+ }
683
+ function sameContent(a, b) {
684
+ return b !== void 0 && normalize(a) === normalize(b);
685
+ }
686
+ var OURS_LABEL = "your file";
687
+ var CONFLICT_START = `<<<<<<< ${OURS_LABEL}`;
688
+ function hasConflictMarkers(content) {
689
+ return content.split(/\r?\n/).includes(CONFLICT_START);
690
+ }
691
+ function conflictedFiles(root, config, files) {
692
+ return [...files].filter((file) => {
693
+ const target = path7.join(root, config.dir, file);
694
+ return existsSync6(target) && hasConflictMarkers(readFileSync2(target, "utf8"));
695
+ });
696
+ }
519
697
  function fileState(root, config, file, previous) {
520
- const target = path5.join(root, config.dir, file.path);
521
- if (!existsSync5(target)) return "missing";
698
+ const target = path7.join(root, config.dir, file.path);
699
+ if (!existsSync6(target)) return "missing";
522
700
  const onDisk = readFileSync2(target, "utf8");
523
- if (onDisk === file.content) return "current";
524
- return previous.get(file.path) === onDisk ? "outdated" : "changed";
525
- }
526
- function writeFiles(root, config, files, previous, { overwrite, dryRun }) {
527
- const result = { written: [], unchanged: [], skipped: [] };
701
+ if (hasConflictMarkers(onDisk)) return "conflicted";
702
+ if (sameContent(onDisk, file.content)) return "current";
703
+ return sameContent(onDisk, previous?.get(file.path)) ? "outdated" : "changed";
704
+ }
705
+ function mergeFile(ours, base, theirs, version) {
706
+ const lines2 = (content) => withLf(content).split("\n");
707
+ const { conflict, result } = merge(lines2(ours), lines2(base), lines2(theirs), {
708
+ label: { a: OURS_LABEL, b: `nxds ${version}` }
709
+ });
710
+ return { content: result.join("\n"), conflict };
711
+ }
712
+ function writeFiles(root, config, files, previous, { overwrite, dryRun, version }) {
713
+ const result = {
714
+ written: [],
715
+ unchanged: [],
716
+ merged: [],
717
+ conflicted: [],
718
+ unresolved: [],
719
+ skipped: []
720
+ };
721
+ const write = (file, content) => {
722
+ if (dryRun) return;
723
+ const target = path7.join(root, config.dir, file);
724
+ mkdirSync(path7.dirname(target), { recursive: true });
725
+ writeFileSync2(target, content);
726
+ };
528
727
  for (const file of files) {
529
728
  const state = fileState(root, config, file, previous);
530
729
  if (state === "current") {
531
730
  result.unchanged.push(file.path);
532
731
  continue;
533
732
  }
534
- if (state === "changed" && !overwrite) {
733
+ if (overwrite || state === "missing" || state === "outdated") {
734
+ result.written.push(file.path);
735
+ write(file.path, file.content);
736
+ continue;
737
+ }
738
+ if (state === "conflicted") {
739
+ result.unresolved.push(file.path);
740
+ continue;
741
+ }
742
+ const base = baseOf(previous, file.path);
743
+ if (base === void 0) {
535
744
  result.skipped.push(file.path);
536
745
  continue;
537
746
  }
538
- result.written.push(file.path);
539
- if (dryRun) continue;
540
- const target = path5.join(root, config.dir, file.path);
541
- mkdirSync(path5.dirname(target), { recursive: true });
542
- writeFileSync2(target, file.content);
747
+ const target = path7.join(root, config.dir, file.path);
748
+ const ours = readFileSync2(target, "utf8");
749
+ const merged = mergeFile(ours, base, file.content, version);
750
+ if (sameContent(ours, merged.content)) {
751
+ result.unchanged.push(file.path);
752
+ continue;
753
+ }
754
+ result[merged.conflict ? "conflicted" : "merged"].push(file.path);
755
+ write(file.path, merged.content);
543
756
  }
544
757
  return result;
545
758
  }
546
759
  function relativeImport(fromFile, toFile) {
547
- const relative = path5.relative(path5.dirname(fromFile), toFile).split(path5.sep).join("/");
760
+ const relative = path7.relative(path7.dirname(fromFile), toFile).split(path7.sep).join("/");
548
761
  return relative.startsWith(".") ? relative : `./${relative}`;
549
762
  }
550
763
 
@@ -560,11 +773,11 @@ import {
560
773
  isObjectProperty,
561
774
  isSpreadElement
562
775
  } from "@babel/types";
563
- import { existsSync as existsSync6, readFileSync as readFileSync3 } from "fs";
564
- import path6 from "path";
776
+ import { existsSync as existsSync7, readFileSync as readFileSync3 } from "fs";
777
+ import path8 from "path";
565
778
 
566
779
  // src/setup/source-edit.ts
567
- import { parse } from "@babel/parser";
780
+ import { parse as parse2 } from "@babel/parser";
568
781
  import {
569
782
  isImportDeclaration,
570
783
  isJSXAttribute,
@@ -574,7 +787,7 @@ import {
574
787
  } from "@babel/types";
575
788
  function parseModule(source, file) {
576
789
  try {
577
- return parse(source, {
790
+ return parse2(source, {
578
791
  sourceType: "module",
579
792
  plugins: ["typescript", "jsx"]
580
793
  }).program;
@@ -611,24 +824,24 @@ function findJsx(root, name) {
611
824
  }
612
825
  function applyInsertions(source, insertions) {
613
826
  return [...insertions].sort((a, b) => b.at - a.at).reduce(
614
- (text2, { at, text: inserted }) => text2.slice(0, at) + inserted + text2.slice(at),
827
+ (text, { at, text: inserted }) => text.slice(0, at) + inserted + text.slice(at),
615
828
  source
616
829
  );
617
830
  }
618
- function importInsertion(source, program, lines) {
619
- const imports = program.body.filter((node) => isImportDeclaration(node));
831
+ function importInsertion(source, program2, lines2) {
832
+ const imports = program2.body.filter((node) => isImportDeclaration(node));
620
833
  const first = imports[0];
621
834
  const quote = first && source[startOf(first.source)];
622
835
  const semicolons = !first || source[endOf(first) - 1] === ";";
623
- const text2 = lines.map((line) => quote === '"' ? line.replaceAll("'", '"') : line).map((line) => semicolons ? line : line.replace(/;$/, "")).join("\n");
836
+ const text = lines2.map((line) => quote === '"' ? line.replaceAll("'", '"') : line).map((line) => semicolons ? line : line.replace(/;$/, "")).join("\n");
624
837
  const lastImport = imports.at(-1);
625
838
  if (lastImport) return { at: endOf(lastImport), text: `
626
- ${text2}` };
627
- const lastDirective = program.directives.at(-1);
839
+ ${text}` };
840
+ const lastDirective = program2.directives.at(-1);
628
841
  if (lastDirective) return { at: endOf(lastDirective), text: `
629
842
 
630
- ${text2}` };
631
- return { at: 0, text: `${text2}
843
+ ${text}` };
844
+ return { at: 0, text: `${text}
632
845
 
633
846
  ` };
634
847
  }
@@ -667,7 +880,7 @@ function tailwindPackages(setup, range) {
667
880
  }
668
881
  function appDir(project) {
669
882
  const dir = APP_DIRS.find(
670
- (candidate) => existsSync6(path6.join(project.root, candidate, "layout.tsx"))
883
+ (candidate) => existsSync7(path8.join(project.root, candidate, "layout.tsx"))
671
884
  );
672
885
  if (!dir)
673
886
  throw new CliError("No app/layout.tsx or src/app/layout.tsx found.");
@@ -683,7 +896,7 @@ function cssEntryPath(project) {
683
896
  return `${entryDir(project)}/nxds.css`;
684
897
  }
685
898
  function read(project, file) {
686
- return readFileSync3(path6.join(project.root, file), "utf8");
899
+ return readFileSync3(path8.join(project.root, file), "utf8");
687
900
  }
688
901
  function cssEntry(config, withPreflight) {
689
902
  const tokens = relativeImport(config.css, `${config.dir}/tailwind/nxds.css`);
@@ -722,8 +935,8 @@ function wireNextLayout(project, config, steps) {
722
935
  const layout = `${appDir(project)}/layout.tsx`;
723
936
  const source = read(project, layout);
724
937
  if (/NxdsAppearance(Provider|Script)/.test(source)) return NO_CHANGES;
725
- const program = parseModule(source, layout);
726
- const html = findJsx(program, "html");
938
+ const program2 = parseModule(source, layout);
939
+ const html = findJsx(program2, "html");
727
940
  const body = html && findJsx(html, "body");
728
941
  const head = html && findJsx(html, "head");
729
942
  if (!html || !body?.closingElement || head && !head.closingElement) {
@@ -785,15 +998,15 @@ ${htmlIndent}${step}</head>`
785
998
  imports.push("import { defaultAppearance } from './appearance-state';");
786
999
  }
787
1000
  if (imports.length === 0) return NO_CHANGES;
788
- insertions.push(importInsertion(source, program, imports));
1001
+ insertions.push(importInsertion(source, program2, imports));
789
1002
  return {
790
1003
  changes: [editedFile(layout, applyInsertions(source, insertions))],
791
1004
  manual: []
792
1005
  };
793
1006
  }
794
- function renderedElement(program) {
1007
+ function renderedElement(program2) {
795
1008
  const call = findNode(
796
- program,
1009
+ program2,
797
1010
  (node) => isCallExpression(node) && isMemberExpression(node.callee) && isIdentifier(node.callee.property, { name: "render" }) && isJSXElement2(node.arguments[0])
798
1011
  );
799
1012
  return call?.arguments[0];
@@ -803,12 +1016,12 @@ function wireViteEntry(project, config, steps) {
803
1016
  if (!entry) throw new CliError("No src/main.tsx found.");
804
1017
  const source = read(project, entry);
805
1018
  if (source.includes("NxdsAppearanceProvider")) return NO_CHANGES;
806
- const program = parseModule(source, entry);
1019
+ const program2 = parseModule(source, entry);
807
1020
  const imports = [];
808
1021
  const insertions = [];
809
1022
  if (steps.has("add-css-import")) imports.push("import './nxds.css';");
810
1023
  if (steps.has("wrap-root-with-provider")) {
811
- const app = renderedElement(program);
1024
+ const app = renderedElement(program2);
812
1025
  if (!app) {
813
1026
  return {
814
1027
  changes: [],
@@ -836,7 +1049,7 @@ ${indent}</NxdsAppearanceProvider>` }
836
1049
  );
837
1050
  }
838
1051
  if (imports.length === 0) return NO_CHANGES;
839
- insertions.push(importInsertion(source, program, imports));
1052
+ insertions.push(importInsertion(source, program2, imports));
840
1053
  return {
841
1054
  changes: [editedFile(entry, applyInsertions(source, insertions))],
842
1055
  manual: []
@@ -863,8 +1076,8 @@ export function nxdsAppearance(): Plugin {
863
1076
  };
864
1077
  }
865
1078
  `;
866
- function configObject(program) {
867
- const declaration = program.body.find(
1079
+ function configObject(program2) {
1080
+ const declaration = program2.body.find(
868
1081
  (node) => isExportDefaultDeclaration(node)
869
1082
  )?.declaration;
870
1083
  if (isObjectExpression(declaration)) return declaration;
@@ -894,11 +1107,11 @@ var NXDS_VITE_PLUGIN = {
894
1107
  var VITE_ALIAS = "{ '@': '/src' }";
895
1108
  function wireViteConfig(project, addTailwind, steps) {
896
1109
  const configFile = findFile(project, VITE_CONFIGS) ?? "vite.config.ts";
897
- const addScript = steps.has("add-head-script") && !existsSync6(path6.join(project.root, "nxds.vite.ts"));
1110
+ const addScript = steps.has("add-head-script") && !existsSync7(path8.join(project.root, "nxds.vite.ts"));
898
1111
  const changes = addScript ? [{ path: "nxds.vite.ts", content: VITE_PLUGIN, created: true }] : [];
899
- const source = existsSync6(path6.join(project.root, configFile)) ? read(project, configFile) : "";
900
- const program = source ? parseModule(source, configFile) : void 0;
901
- const object = program && configObject(program);
1112
+ const source = existsSync7(path8.join(project.root, configFile)) ? read(project, configFile) : "";
1113
+ const program2 = source ? parseModule(source, configFile) : void 0;
1114
+ const object = program2 && configObject(program2);
902
1115
  const plugins = object && pluginsArray(object);
903
1116
  const newPlugins = [
904
1117
  ...addTailwind ? [TAILWIND_VITE_PLUGIN] : [],
@@ -938,8 +1151,8 @@ function wireViteConfig(project, addTailwind, steps) {
938
1151
  step: `Add resolve.alias ${VITE_ALIAS}, so imports like @/components/nxds/... resolve.`
939
1152
  });
940
1153
  }
941
- if (program && insertions.length > 0) {
942
- insertions.push(importInsertion(source, program, imports));
1154
+ if (program2 && insertions.length > 0) {
1155
+ insertions.push(importInsertion(source, program2, imports));
943
1156
  changes.push(editedFile(configFile, applyInsertions(source, insertions)));
944
1157
  }
945
1158
  return { changes, manual };
@@ -984,8 +1197,8 @@ var ESLINT_CONFIGS = [
984
1197
  "eslint.config.mjs",
985
1198
  "eslint.config.ts"
986
1199
  ];
987
- function exportedConfigs(program) {
988
- const declaration = program.body.find(
1200
+ function exportedConfigs(program2) {
1201
+ const declaration = program2.body.find(
989
1202
  (node) => isExportDefaultDeclaration(node)
990
1203
  )?.declaration;
991
1204
  if (isArrayExpression(declaration)) return declaration.elements;
@@ -1057,7 +1270,7 @@ function wireNextTailwind(project) {
1057
1270
  };
1058
1271
  }
1059
1272
  function createdUnlessExists(project, file, content) {
1060
- if (existsSync6(path6.join(project.root, file))) return NO_CHANGES;
1273
+ if (existsSync7(path8.join(project.root, file))) return NO_CHANGES;
1061
1274
  return { changes: [{ path: file, content, created: true }], manual: [] };
1062
1275
  }
1063
1276
  function planWiring(project, config, addTailwind, setupSteps) {
@@ -1094,36 +1307,53 @@ function planWiring(project, config, addTailwind, setupSteps) {
1094
1307
  // src/commands/check.ts
1095
1308
  async function diff(cwd, registry, names) {
1096
1309
  const config = requireConfig(cwd);
1097
- const latest = await loadRelease(registry, "latest");
1310
+ const target = { ...config, react: reactMajor(readProject(cwd)) };
1311
+ const latest = await loadRelease(registry, "latest", target.react);
1098
1312
  const previous = await fetchRelease(registry, config.version);
1099
- const previousFiles = previous ? releaseFiles(previous, config.framework) : NO_PREVIOUS;
1313
+ const previousFiles = previous && releaseFiles(previous, config);
1100
1314
  const { known, removed } = splitInstalled(latest, config.items);
1101
1315
  const items = resolveItems(
1102
1316
  latest,
1103
1317
  config.framework,
1104
1318
  names.length > 0 ? names : known
1105
1319
  );
1320
+ const update2 = `\`${CLI_COMMAND} update\``;
1321
+ const overwrite = `\`${CLI_COMMAND} update --overwrite\``;
1322
+ function advice(state, file) {
1323
+ if (state === "outdated") return `${update2} replaces it`;
1324
+ if (state === "missing") return `${update2} restores it`;
1325
+ if (state === "conflicted") {
1326
+ return `has conflict markers; resolve them, or ${overwrite} replaces it`;
1327
+ }
1328
+ const base = baseOf(previousFiles, file.path);
1329
+ if (base === void 0) {
1330
+ return `has your edits; ${update2} keeps it, ${overwrite} replaces it`;
1331
+ }
1332
+ if (sameContent(base, file.content)) {
1333
+ return `has your edits; nxds hasn't changed it since ${config.version}`;
1334
+ }
1335
+ return `has your edits; ${update2} merges nxds's changes into it, ${overwrite} replaces it`;
1336
+ }
1106
1337
  const files = items.flatMap(
1107
- (item) => variantOf(item, config.framework).files.map((file) => ({
1108
- item: item.name,
1109
- path: file.path,
1110
- state: fileState(cwd, config, file, previousFiles)
1111
- }))
1338
+ (item) => variantFor(item, target).files.map((file) => {
1339
+ const state = fileState(cwd, config, file, previousFiles);
1340
+ return {
1341
+ item: item.name,
1342
+ path: file.path,
1343
+ state,
1344
+ advice: state === "current" ? null : advice(state, file)
1345
+ };
1346
+ })
1112
1347
  );
1113
1348
  const differing = files.filter((file) => file.state !== "current");
1114
- const advice = {
1115
- outdated: "`nxds update` replaces it",
1116
- changed: "differs from nxds's copy; `nxds update --overwrite` replaces it",
1117
- missing: "`nxds update` restores it"
1118
- };
1119
1349
  return {
1120
1350
  data: { version: config.version, latest: latest.version, files, removed },
1121
1351
  lines: [
1122
1352
  ...differing.map(
1123
- (file) => `${file.state.padEnd(8)} ${file.path} (${advice[file.state]})`
1353
+ (file) => `${file.state.padEnd(10)} ${file.path} (${file.advice})`
1124
1354
  ),
1125
1355
  ...removed.map(
1126
- (name) => `removed ${name} (no longer in nxds; \`nxds update\` drops it)`
1356
+ (name) => `${"removed".padEnd(10)} ${name} (no longer in nxds; \`${CLI_COMMAND} update\` drops it)`
1127
1357
  ),
1128
1358
  ...differing.length === 0 && removed.length === 0 ? [`Every file matches nxds ${latest.version}.`] : []
1129
1359
  ],
@@ -1133,7 +1363,7 @@ async function diff(cwd, registry, names) {
1133
1363
  function tailwindMessage(project) {
1134
1364
  const declared = project.dependencies.tailwindcss;
1135
1365
  if (declared === void 0) {
1136
- return "Tailwind 4 is not installed. Run `nxds init`.";
1366
+ return `Tailwind 4 is not installed. Run \`${CLI_COMMAND} init\`.`;
1137
1367
  }
1138
1368
  const tailwindMajor = majorVersion(project, "tailwindcss");
1139
1369
  if (tailwindMajor === 4) return `tailwindcss ${declared}`;
@@ -1143,14 +1373,14 @@ function tailwindMessage(project) {
1143
1373
  return `tailwindcss ${declared}. nxds needs Tailwind CSS 4.`;
1144
1374
  }
1145
1375
  function read2(root, file) {
1146
- if (!file || !existsSync7(path7.join(root, file))) return "";
1147
- return readFileSync4(path7.join(root, file), "utf8");
1376
+ if (!file || !existsSync8(path9.join(root, file))) return "";
1377
+ return readFileSync4(path9.join(root, file), "utf8");
1148
1378
  }
1149
1379
  function sourcesNxdsDir(config, css) {
1150
- const entryDir2 = path7.posix.dirname(config.css);
1380
+ const entryDir2 = path9.posix.dirname(config.css);
1151
1381
  return [...css.matchAll(/@source\s+(['"])(.*?)\1/g)].some(([, , source]) => {
1152
1382
  const base = (source ?? "").split(/[*{[]/)[0] ?? "";
1153
- const folder = path7.posix.join(entryDir2, base).replace(/\/+$/, "");
1383
+ const folder = path9.posix.join(entryDir2, base).replace(/\/+$/, "");
1154
1384
  return folder === "." || /^\.\.(\/\.\.)*$/.test(folder) || config.dir === folder || config.dir.startsWith(`${folder}/`);
1155
1385
  });
1156
1386
  }
@@ -1164,10 +1394,42 @@ function unscannedReason(cwd, config, css) {
1164
1394
  }
1165
1395
  return null;
1166
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
+ }
1167
1429
  function packageProblems(project, config, items) {
1168
1430
  const needed = Object.assign(
1169
1431
  {},
1170
- ...items.map((item) => variantOf(item, config.framework).packages.npm)
1432
+ ...items.map((item) => variantFor(item, config).packages.npm)
1171
1433
  );
1172
1434
  return Object.entries(needed).flatMap(([name, range]) => {
1173
1435
  if (!(name in project.dependencies))
@@ -1190,7 +1452,7 @@ async function doctor(cwd, registry) {
1190
1452
  check(
1191
1453
  "config",
1192
1454
  config !== null,
1193
- config ? "nxds.json found" : "No nxds.json. Run `nxds init`."
1455
+ config ? "nxds.json found" : `No nxds.json. Run \`${CLI_COMMAND} init\`.`
1194
1456
  );
1195
1457
  if (!project.setup) {
1196
1458
  check("app", false, "Not a Next.js App Router or Vite + React app.");
@@ -1200,19 +1462,20 @@ async function doctor(cwd, registry) {
1200
1462
  check(
1201
1463
  "release",
1202
1464
  catalog !== null,
1203
- catalog ? `Files come from nxds ${config.version}` : `${registry} has no nxds ${config.version}. Run \`nxds update\`.`
1465
+ catalog ? `Files come from nxds ${config.version}` : `${registry} has no nxds ${config.version}. Run \`${CLI_COMMAND} update\`.`
1204
1466
  );
1205
1467
  check(
1206
1468
  "tailwind",
1207
1469
  majorVersion(project, "tailwindcss") === 4,
1208
1470
  tailwindMessage(project)
1209
1471
  );
1472
+ checks.push(...reactChecks(project, config));
1210
1473
  const css = read2(cwd, config.css);
1211
1474
  const importsTokens = css.includes("tailwind/nxds.css");
1212
1475
  check(
1213
1476
  "css entry",
1214
1477
  importsTokens,
1215
- !css ? `${config.css} is missing. Run \`nxds init\`.` : importsTokens ? `${config.css} imports the nxds tokens` : `${config.css} does not import ${config.dir}/tailwind/nxds.css. Add the @import back.`
1478
+ !css ? `${config.css} is missing. Run \`${CLI_COMMAND} init\`.` : importsTokens ? `${config.css} imports the nxds tokens` : `${config.css} does not import ${config.dir}/tailwind/nxds.css. Add the @import back.`
1216
1479
  );
1217
1480
  const unscanned = unscannedReason(cwd, config, css);
1218
1481
  check(
@@ -1222,7 +1485,7 @@ async function doctor(cwd, registry) {
1222
1485
  );
1223
1486
  const entry = project.setup === "next-app" ? `${entryDir(project)}/layout.tsx` : findFile(project, VITE_ENTRIES);
1224
1487
  const entrySource = read2(cwd, entry);
1225
- const cssFile = path7.basename(config.css);
1488
+ const cssFile = path9.basename(config.css);
1226
1489
  const importsCss = entrySource.includes(cssFile);
1227
1490
  check(
1228
1491
  "css import",
@@ -1239,31 +1502,43 @@ async function doctor(cwd, registry) {
1239
1502
  check(
1240
1503
  "theme script",
1241
1504
  scriptSource.includes(scriptMarker),
1242
- scriptSource.includes(scriptMarker) ? "The theme script runs before first paint" : "The theme script is missing, so the page can flash the wrong theme. Run `nxds init`."
1505
+ scriptSource.includes(scriptMarker) ? "The theme script runs before first paint" : `The theme script is missing, so the page can flash the wrong theme. Run \`${CLI_COMMAND} init\`.`
1243
1506
  );
1244
1507
  if (!catalog) return report(checks);
1245
1508
  const { known, removed } = splitInstalled(catalog, config.items);
1246
1509
  check(
1247
1510
  "items",
1248
1511
  removed.length === 0,
1249
- removed.length === 0 ? `${known.length} installed items, all in this release` : `No longer in nxds: ${removed.join(", ")}. Run \`nxds update\`.`
1512
+ removed.length === 0 ? `${known.length} installed items, all in this release` : `No longer in nxds: ${removed.join(", ")}. Run \`${CLI_COMMAND} update\`.`
1250
1513
  );
1251
1514
  const items = resolveItems(catalog, config.framework, known);
1252
1515
  const missingFiles = items.flatMap(
1253
- (item) => variantOf(item, config.framework).files.filter(
1254
- (file) => !existsSync7(path7.join(cwd, config.dir, file.path))
1516
+ (item) => variantFor(item, config).files.filter(
1517
+ (file) => !existsSync8(path9.join(cwd, config.dir, file.path))
1255
1518
  ).map((file) => `${item.name}: ${file.path}`)
1256
1519
  );
1257
1520
  check(
1258
1521
  "files",
1259
1522
  missingFiles.length === 0,
1260
- missingFiles.length === 0 ? `All files for ${known.length} installed items are present` : `Missing ${missingFiles.join(", ")}. Run \`nxds add\` for those items.`
1523
+ missingFiles.length === 0 ? `All files for ${known.length} installed items are present` : `Missing ${missingFiles.join(", ")}. Run \`${CLI_COMMAND} add\` for those items.`
1524
+ );
1525
+ const conflicted = conflictedFiles(
1526
+ cwd,
1527
+ config,
1528
+ items.flatMap(
1529
+ (item) => variantFor(item, config).files.map((file) => file.path)
1530
+ )
1531
+ ).map((file) => `${config.dir}/${file}`);
1532
+ check(
1533
+ "conflicts",
1534
+ conflicted.length === 0,
1535
+ conflicted.length === 0 ? "No conflict markers left from an update" : `Resolve the <<<<<<< markers in ${conflicted.join(", ")}.`
1261
1536
  );
1262
1537
  const problems = packageProblems(project, config, items);
1263
1538
  check(
1264
1539
  "packages",
1265
1540
  problems.length === 0,
1266
- problems.length === 0 ? "Every package the installed items need is installed in range, @nxds/core included" : `${problems.join("; ")}. Run \`nxds update\`, then your package manager's install.`
1541
+ problems.length === 0 ? "Every package the installed items need is installed in range, @nxds/core included" : `${problems.join("; ")}. Run \`${CLI_COMMAND} update\`, then your package manager's install.`
1267
1542
  );
1268
1543
  return report(checks);
1269
1544
  }
@@ -1282,16 +1557,29 @@ function report(checks) {
1282
1557
  }
1283
1558
 
1284
1559
  // src/commands/install.ts
1285
- import { existsSync as existsSync9, mkdirSync as mkdirSync2, writeFileSync as writeFileSync4 } from "fs";
1286
- import path9 from "path";
1560
+ import { existsSync as existsSync10, mkdirSync as mkdirSync2, writeFileSync as writeFileSync4 } from "fs";
1561
+ import path11 from "path";
1287
1562
 
1288
1563
  // src/install.ts
1289
- import { existsSync as existsSync8, readFileSync as readFileSync5, writeFileSync as writeFileSync3 } from "fs";
1290
- import path8 from "path";
1564
+ import { existsSync as existsSync9, readFileSync as readFileSync5, writeFileSync as writeFileSync3 } from "fs";
1565
+ import path10 from "path";
1566
+ function emptyInstallResult() {
1567
+ return {
1568
+ items: [],
1569
+ written: [],
1570
+ unchanged: [],
1571
+ merged: [],
1572
+ conflicted: [],
1573
+ unresolved: [],
1574
+ skipped: [],
1575
+ packages: [],
1576
+ cssImports: []
1577
+ };
1578
+ }
1291
1579
  function addCssImports(project, config, files, dryRun) {
1292
1580
  if (files.length === 0) return [];
1293
- const entry = path8.join(project.root, config.css);
1294
- const current = existsSync8(entry) ? readFileSync5(entry, "utf8") : "";
1581
+ const entry = path10.join(project.root, config.css);
1582
+ const current = existsSync9(entry) ? readFileSync5(entry, "utf8") : "";
1295
1583
  const added = [...new Set(files)].map((file) => cssImportLine(config, file)).filter((line) => !current.includes(line));
1296
1584
  if (added.length > 0 && !dryRun) {
1297
1585
  writeFileSync3(entry, `${current.trimEnd()}
@@ -1302,7 +1590,7 @@ ${added.join("\n")}
1302
1590
  }
1303
1591
  function installItems(catalog, project, config, names, previous, options) {
1304
1592
  const items = resolveItems(catalog, config.framework, names);
1305
- const variants = items.map((item) => variantOf(item, config.framework));
1593
+ const variants = items.map((item) => variantFor(item, config));
1306
1594
  const files = new Map(
1307
1595
  variants.flatMap((variant) => variant.files).map((file) => [file.path, file])
1308
1596
  );
@@ -1311,7 +1599,7 @@ function installItems(catalog, project, config, names, previous, options) {
1311
1599
  config,
1312
1600
  [...files.values()],
1313
1601
  previous,
1314
- options
1602
+ { ...options, version: catalog.version }
1315
1603
  );
1316
1604
  const packages = packagesToInstall(
1317
1605
  project,
@@ -1347,6 +1635,15 @@ function installLines(config, result, options) {
1347
1635
  const verb = dryRun ? "Would write" : "Wrote";
1348
1636
  return [
1349
1637
  ...result.written.map((file) => `${verb} ${config.dir}/${file}`),
1638
+ ...result.merged.map(
1639
+ (file) => `${dryRun ? "Would merge" : "Merged"} nxds's changes into your edits in ${config.dir}/${file}`
1640
+ ),
1641
+ ...result.conflicted.map(
1642
+ (file) => `${dryRun ? "Would conflict" : "Conflict"} in ${config.dir}/${file}: nxds changed lines you edited. Resolve the <<<<<<< markers, then run \`${CLI_COMMAND} update\` again.`
1643
+ ),
1644
+ ...result.unresolved.map(
1645
+ (file) => `Still conflicted: ${config.dir}/${file}. Resolve the <<<<<<< markers, then run \`${CLI_COMMAND} update\` again.`
1646
+ ),
1350
1647
  ...result.skipped.map(
1351
1648
  (file) => `Kept ${config.dir}/${file}: it differs from nxds's copy, so it may hold your edits. Pass --overwrite to replace it.`
1352
1649
  ),
@@ -1356,10 +1653,19 @@ function installLines(config, result, options) {
1356
1653
  )
1357
1654
  ];
1358
1655
  }
1656
+ function settled(result) {
1657
+ return result.skipped.length === 0 && result.conflicted.length === 0 && result.unresolved.length === 0;
1658
+ }
1659
+ function assertFilesMatchReact(config, react) {
1660
+ if (config.react === react) return;
1661
+ throw new CliError(
1662
+ `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.`
1663
+ );
1664
+ }
1359
1665
  function nxdsDir(dir) {
1360
- const normalized = projectPath(dir);
1666
+ const normalized = relativePath(dir);
1361
1667
  if (normalized) return normalized;
1362
- throw new CliError(
1668
+ throw new UsageError(
1363
1669
  `--dir takes a folder inside your app, such as components/nxds, not "${dir}".`
1364
1670
  );
1365
1671
  }
@@ -1380,7 +1686,7 @@ async function init(cwd, registry, options) {
1380
1686
  const project = readProject(cwd);
1381
1687
  if (!project.setup) {
1382
1688
  throw new CliError(
1383
- "nxds init supports Next.js App Router and Vite + React apps. For other setups, follow the manual steps at https://nxds.dev/getting-started/install."
1689
+ `\`${CLI_COMMAND} init\` supports Next.js App Router and Vite + React apps. For other setups, follow the manual steps at https://nxds.dev/getting-started/install.`
1384
1690
  );
1385
1691
  }
1386
1692
  if (!project.typescript) {
@@ -1388,11 +1694,17 @@ async function init(cwd, registry, options) {
1388
1694
  "nxds components are TypeScript. Add a tsconfig.json to your app, then run init again."
1389
1695
  );
1390
1696
  }
1697
+ const react = reactMajor(project);
1391
1698
  const addTailwind = !("tailwindcss" in project.dependencies);
1392
1699
  if (!addTailwind) assertTailwind4(project);
1393
1700
  const dir = options.dir ? nxdsDir(options.dir) : void 0;
1394
1701
  const existing = readConfig(cwd);
1395
- const catalog = await loadRelease(registry, existing?.version ?? "latest");
1702
+ const catalog = await loadRelease(
1703
+ registry,
1704
+ existing?.version ?? "latest",
1705
+ react
1706
+ );
1707
+ if (existing) assertFilesMatchReact(existing, react);
1396
1708
  if (existing && dir && dir !== existing.dir) {
1397
1709
  throw new CliError(
1398
1710
  `nxds is already set up in ${existing.dir}. Run init without --dir, or move the folder and change "dir" in nxds.json.`
@@ -1400,12 +1712,13 @@ async function init(cwd, registry, options) {
1400
1712
  }
1401
1713
  const config = existing ?? {
1402
1714
  framework: "react",
1715
+ react,
1403
1716
  version: catalog.version,
1404
1717
  dir: dir ?? defaultDir(project),
1405
1718
  css: cssEntryPath(project),
1406
1719
  items: []
1407
1720
  };
1408
- const setup = setupVariant(catalog, config.framework);
1721
+ const setup = setupVariant(catalog, config);
1409
1722
  const wiring = planWiring(project, config, addTailwind, setup.steps);
1410
1723
  const tailwind = packagesToInstall(
1411
1724
  project,
@@ -1419,23 +1732,24 @@ async function init(cwd, registry, options) {
1419
1732
  project,
1420
1733
  config,
1421
1734
  [SETUP],
1422
- NO_PREVIOUS,
1735
+ null,
1423
1736
  options
1424
1737
  );
1425
1738
  if (!options.dryRun) {
1426
1739
  for (const change of wiring.changes) {
1427
- const target = path9.join(project.root, change.path);
1428
- mkdirSync2(path9.dirname(target), { recursive: true });
1740
+ const target = path11.join(project.root, change.path);
1741
+ mkdirSync2(path11.dirname(target), { recursive: true });
1429
1742
  writeFileSync4(target, change.content);
1430
1743
  }
1431
1744
  writeConfig(cwd, config);
1432
1745
  }
1433
1746
  const packages = [...tailwind, ...installed.packages];
1434
1747
  const [createVerb, editVerb] = options.dryRun ? ["Would create", "Would edit"] : ["Created", "Edited"];
1435
- const done = wiring.manual.length === 0;
1748
+ const done = wiring.manual.length === 0 && settled(installed);
1436
1749
  return {
1437
1750
  data: {
1438
1751
  setup: project.setup,
1752
+ react: config.react,
1439
1753
  version: config.version,
1440
1754
  dir: config.dir,
1441
1755
  css: config.css,
@@ -1447,13 +1761,15 @@ async function init(cwd, registry, options) {
1447
1761
  manual: wiring.manual
1448
1762
  },
1449
1763
  lines: [
1450
- `nxds ${config.version} in ${config.dir}`,
1764
+ `nxds ${config.version} in ${config.dir}, for React ${config.react}`,
1451
1765
  ...installLines(config, { ...installed, packages }, options),
1452
1766
  ...wiring.changes.map(
1453
1767
  (change) => `${change.created ? createVerb : editVerb} ${change.path}`
1454
1768
  ),
1455
1769
  ...manualLines(wiring.manual),
1456
- ...done && !options.dryRun ? ["nxds is set up. Add a component with `nxds add button`."] : []
1770
+ ...done && !options.dryRun ? [
1771
+ `nxds is set up. Add a component with \`${CLI_COMMAND} add button\`.`
1772
+ ] : []
1457
1773
  ],
1458
1774
  ok: done
1459
1775
  };
@@ -1461,22 +1777,21 @@ async function init(cwd, registry, options) {
1461
1777
  function manualLines(manual) {
1462
1778
  if (manual.length === 0) return [];
1463
1779
  return [
1464
- "Finish these steps by hand, then run `nxds doctor`:",
1780
+ `Finish these steps by hand, then run \`${CLI_COMMAND} doctor\`:`,
1465
1781
  ...manual.map(({ file, step }) => ` ${file}: ${step}`)
1466
1782
  ];
1467
1783
  }
1468
1784
  async function add(cwd, registry, names, options) {
1469
- if (names.length === 0) {
1470
- throw new CliError("Name at least one item, such as `nxds add button`.");
1471
- }
1472
1785
  const config = requireConfig(cwd);
1473
- const catalog = await loadRelease(registry, config.version);
1786
+ const project = readProject(cwd);
1787
+ assertFilesMatchReact(config, reactMajor(project));
1788
+ const catalog = await loadRelease(registry, config.version, config.react);
1474
1789
  const installed = installItems(
1475
1790
  catalog,
1476
- readProject(cwd),
1791
+ project,
1477
1792
  config,
1478
1793
  names,
1479
- NO_PREVIOUS,
1794
+ null,
1480
1795
  options
1481
1796
  );
1482
1797
  if (!options.dryRun) writeConfig(cwd, config);
@@ -1484,61 +1799,85 @@ async function add(cwd, registry, names, options) {
1484
1799
  data: { dryRun: options.dryRun, ...installed },
1485
1800
  lines: [
1486
1801
  ...installLines(config, installed, options),
1487
- ...installed.written.length === 0 && installed.skipped.length === 0 ? [
1802
+ ...installed.written.length === 0 && settled(installed) ? [
1488
1803
  `${names.join(", ")} ${names.length === 1 ? "is" : "are"} already up to date.`
1489
1804
  ] : []
1490
1805
  ],
1491
- ok: true
1806
+ ok: settled(installed)
1492
1807
  };
1493
1808
  }
1494
- function filesOf(catalog, framework, names) {
1809
+ function filesOf(catalog, target, names) {
1495
1810
  return new Set(
1496
- resolveItems(catalog, framework, names).flatMap(
1497
- (item) => variantOf(item, framework).files.map((file) => file.path)
1811
+ resolveItems(catalog, target.framework, names).flatMap(
1812
+ (item) => variantFor(item, target).files.map((file) => file.path)
1498
1813
  )
1499
1814
  );
1500
1815
  }
1501
- async function update(cwd, registry, names, options) {
1502
- if (names.length > 0) {
1503
- throw new CliError(
1504
- "`nxds update` updates every installed item, so one app never mixes two releases. Run it without item names."
1505
- );
1506
- }
1816
+ async function update(cwd, registry, options) {
1507
1817
  const config = requireConfig(cwd);
1508
- const latest = await loadRelease(registry, "latest");
1509
- const previous = await fetchRelease(registry, config.version);
1818
+ const project = readProject(cwd);
1819
+ const from = config.version;
1820
+ const fromReact = config.react;
1821
+ const react = reactMajor(project);
1822
+ const latest = await loadRelease(registry, "latest", react);
1510
1823
  const { known, removed } = splitInstalled(latest, config.items);
1511
- const current = filesOf(latest, config.framework, known);
1512
- const installedBefore = previous ? filesOf(
1513
- previous,
1514
- config.framework,
1515
- splitInstalled(previous, config.items).known
1516
- ) : /* @__PURE__ */ new Set();
1824
+ const current = filesOf(latest, { ...config, react }, known);
1825
+ const previous = await fetchRelease(registry, from);
1826
+ const installedBefore = previous ? filesOf(previous, config, splitInstalled(previous, config.items).known) : /* @__PURE__ */ new Set();
1827
+ const unchanged = from === latest.version && fromReact === react;
1828
+ const unresolved = unchanged || options.overwrite ? [] : conflictedFiles(cwd, config, /* @__PURE__ */ new Set([...current, ...installedBefore]));
1829
+ if (unresolved.length > 0) {
1830
+ const data2 = {
1831
+ ...emptyInstallResult(),
1832
+ dryRun: options.dryRun,
1833
+ from,
1834
+ version: from,
1835
+ fromReact,
1836
+ react: fromReact,
1837
+ unresolved,
1838
+ removed: [],
1839
+ orphaned: []
1840
+ };
1841
+ return {
1842
+ data: data2,
1843
+ lines: [
1844
+ ...unresolved.map((file) => `Still conflicted: ${config.dir}/${file}`),
1845
+ `Changed nothing, and nxds.json stays on ${from}. Resolve the <<<<<<< markers, then run \`${CLI_COMMAND} update\` again.`
1846
+ ],
1847
+ ok: false
1848
+ };
1849
+ }
1517
1850
  const orphaned = [...installedBefore].filter(
1518
- (file) => !current.has(file) && existsSync9(path9.join(cwd, config.dir, file))
1851
+ (file) => !current.has(file) && existsSync10(path11.join(cwd, config.dir, file))
1519
1852
  );
1853
+ const previousFiles = previous && releaseFiles(previous, config);
1520
1854
  config.items = known;
1521
1855
  const installed = installItems(
1522
1856
  latest,
1523
- readProject(cwd),
1524
- config,
1857
+ project,
1858
+ { ...config, react },
1525
1859
  known,
1526
- previous ? releaseFiles(previous, config.framework) : NO_PREVIOUS,
1860
+ previousFiles,
1527
1861
  options
1528
1862
  );
1529
- const from = config.version;
1530
- const stays = from !== latest.version && installed.skipped.length > 0;
1531
- if (!stays) config.version = latest.version;
1863
+ const stays = !unchanged && installed.skipped.length > 0;
1864
+ if (!stays) {
1865
+ config.version = latest.version;
1866
+ config.react = react;
1867
+ }
1532
1868
  if (!options.dryRun) writeConfig(cwd, config);
1869
+ const data = {
1870
+ dryRun: options.dryRun,
1871
+ from,
1872
+ version: config.version,
1873
+ fromReact,
1874
+ react: config.react,
1875
+ ...installed,
1876
+ removed,
1877
+ orphaned
1878
+ };
1533
1879
  return {
1534
- data: {
1535
- dryRun: options.dryRun,
1536
- from,
1537
- version: config.version,
1538
- ...installed,
1539
- removed,
1540
- orphaned
1541
- },
1880
+ data,
1542
1881
  lines: [
1543
1882
  ...previous ? [] : [
1544
1883
  `${registry} has no nxds ${from}, so every file that differs from ${latest.version} counts as changed.`
@@ -1550,121 +1889,187 @@ async function update(cwd, registry, names, options) {
1550
1889
  ...orphaned.map(
1551
1890
  (file) => `No installed item uses ${config.dir}/${file} any more. Delete it once nothing in your app imports it.`
1552
1891
  ),
1553
- stays ? `nxds.json stays on ${from} until every kept file is replaced.` : `nxds ${from === latest.version ? "is on" : `${options.dryRun ? "would move" : "moved"} from ${from} to`} ${latest.version}.`
1892
+ ...fromReact !== react && !stays ? [
1893
+ `${options.dryRun ? "Would switch" : "Switched"} the files from React ${fromReact} to React ${react}.`
1894
+ ] : [],
1895
+ 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}.`
1554
1896
  ],
1555
- ok: !stays
1897
+ ok: settled(installed)
1556
1898
  };
1557
1899
  }
1558
1900
 
1559
- // src/index.ts
1560
- var HELP = `nxds: add nxds to your app.
1561
-
1562
- Usage
1563
- nxds init Set up nxds: token CSS, provider, and the layout wiring
1564
- nxds add <item...> Add components or blocks, and everything they need
1565
- nxds list [--kind k] List components and blocks (k: component, block)
1566
- nxds search <term> Find items by name or description
1567
- nxds view <item> Show an item's files, packages and docs link
1568
- nxds info Show what the CLI detects in this app
1569
- nxds update Update every installed item to the latest release
1570
- nxds diff [item...] Compare your nxds files with the latest release
1571
- nxds doctor Check that nxds is wired up correctly
1572
-
1573
- Options
1574
- --json Print machine-readable JSON
1575
- --dry-run Show what would change, without changing anything
1576
- --overwrite Replace files that differ from nxds's copy (add, update)
1577
- --no-install Skip installing npm packages (init, add, update)
1578
- --dir <path> nxds folder for init (default: components/nxds, or src/components/nxds)
1579
- --cwd <path> Run in another folder
1580
- --registry <url|path> Where to get nxds releases (default: https://nxds.dev/r)
1581
- -v, --version Print the version
1582
-
1583
- Docs: https://nxds.dev/getting-started/install`;
1584
- var OPTIONS = {
1585
- json: { type: "boolean", default: false },
1586
- "dry-run": { type: "boolean", default: false },
1587
- overwrite: { type: "boolean", default: false },
1588
- "no-install": { type: "boolean", default: false },
1589
- dir: { type: "string" },
1590
- cwd: { type: "string" },
1591
- registry: { type: "string" },
1592
- kind: { type: "string" },
1593
- help: { type: "boolean", short: "h", default: false },
1594
- version: { type: "boolean", short: "v", default: false }
1595
- };
1596
- function parseCommandLine() {
1597
- try {
1598
- return parseArgs({ allowPositionals: true, options: OPTIONS });
1599
- } catch (error) {
1600
- throw new CliError(
1601
- `${error.message}
1602
- Run \`nxds --help\` for the options.`
1603
- );
1604
- }
1901
+ // src/cli.ts
1902
+ function context(command) {
1903
+ const { cwd, registry } = command.optsWithGlobals();
1904
+ return {
1905
+ cwd: path12.resolve(cwd ?? process.cwd()),
1906
+ registry: resolveRegistry(registry)
1907
+ };
1908
+ }
1909
+ function installOptions(command) {
1910
+ const flags = command.opts();
1911
+ return {
1912
+ overwrite: flags.overwrite ?? false,
1913
+ dryRun: flags.dryRun ?? false,
1914
+ install: flags.install
1915
+ };
1916
+ }
1917
+ function withInstallFlags(command) {
1918
+ return command.option("--dry-run", "show what would change, without changing anything").option("--overwrite", "replace files that differ from nxds's copy").option("--no-install", "skip installing npm packages");
1919
+ }
1920
+ function program(done, write) {
1921
+ const cli = new Command().configureOutput({
1922
+ writeOut: write,
1923
+ writeErr: write,
1924
+ // Errors are printed once, by `main`, as text or JSON.
1925
+ outputError: () => {
1926
+ }
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(
1928
+ new Option(
1929
+ "--registry <url|path>",
1930
+ "where to get nxds releases (default: https://nxds.dev/r)"
1931
+ ).env("NXDS_REGISTRY_URL")
1932
+ ).configureHelp({ showGlobalOptions: true }).addHelpText(
1933
+ "after",
1934
+ `
1935
+ Exit codes: 0 done, 1 error, 2 usage error, 3 done with something left for you (conflicts, kept files, manual steps, failed checks).
1936
+ Docs: https://nxds.dev/getting-started/install`
1937
+ );
1938
+ withInstallFlags(
1939
+ cli.command("init").description("set up nxds: token CSS, provider, and the layout wiring").option(
1940
+ "--dir <path>",
1941
+ "nxds folder (default: components/nxds, or src/components/nxds)"
1942
+ )
1943
+ ).action(async (_options, command) => {
1944
+ const { cwd, registry } = context(command);
1945
+ const { dir } = command.opts();
1946
+ done(await init(cwd, registry, { ...installOptions(command), dir }));
1947
+ });
1948
+ withInstallFlags(
1949
+ cli.command("add").description("add components or blocks, and everything they need").argument("<items...>", "item names, such as button")
1950
+ ).action(async (items, _options, command) => {
1951
+ const { cwd, registry } = context(command);
1952
+ done(await add(cwd, registry, items, installOptions(command)));
1953
+ });
1954
+ withInstallFlags(
1955
+ cli.command("update").description(
1956
+ "update every installed item to the latest release, merging nxds changes into your edits"
1957
+ )
1958
+ ).action(async (_options, command) => {
1959
+ const { cwd, registry } = context(command);
1960
+ done(await update(cwd, registry, installOptions(command)));
1961
+ });
1962
+ cli.command("list").description("list components and blocks").addOption(
1963
+ new Option("--kind <kind>", "only this kind").choices([
1964
+ "component",
1965
+ "block"
1966
+ ])
1967
+ ).action(async (_options, command) => {
1968
+ const { cwd, registry } = context(command);
1969
+ const { kind } = command.opts();
1970
+ done(await list(cwd, registry, kind));
1971
+ });
1972
+ cli.command("search").description("find items by name or description").argument("<term...>", "words to look for").action(async (term, _options, command) => {
1973
+ const { cwd, registry } = context(command);
1974
+ done(await search(cwd, registry, term.join(" ")));
1975
+ });
1976
+ cli.command("view").description("show an item's files, packages and docs link").argument("<item>", "item name").action(async (item, _options, command) => {
1977
+ const { cwd, registry } = context(command);
1978
+ done(await view(cwd, registry, item));
1979
+ });
1980
+ cli.command("info").description("show what the CLI detects in this app").action((_options, command) => {
1981
+ done(info(context(command).cwd));
1982
+ });
1983
+ cli.command("diff").description("compare your nxds files with the latest release").argument("[items...]", "item names (default: every installed item)").action(async (items, _options, command) => {
1984
+ const { cwd, registry } = context(command);
1985
+ done(await diff(cwd, registry, items));
1986
+ });
1987
+ cli.command("doctor").description("check that nxds is wired up correctly").action(async (_options, command) => {
1988
+ const { cwd, registry } = context(command);
1989
+ done(await doctor(cwd, registry));
1990
+ });
1991
+ return cli;
1605
1992
  }
1606
- function text(data, output) {
1607
- return { data, lines: [output], ok: true };
1608
- }
1609
- async function run(values, positionals) {
1610
- if (values.version) return text({ version: "0.0.0" }, "0.0.0");
1611
- if (values.help || positionals[0] === "help")
1612
- return text({ help: HELP }, HELP);
1613
- const [command, ...args] = positionals;
1614
- const cwd = path10.resolve(values.cwd ?? process.cwd());
1615
- const registry = resolveRegistry(values.registry);
1616
- const installOptions = {
1617
- overwrite: values.overwrite,
1618
- dryRun: values["dry-run"],
1619
- install: !values["no-install"]
1993
+ function printed(text, key) {
1994
+ const trimmed = text.trimEnd();
1995
+ return {
1996
+ exitCode: EXIT_CODES.ok,
1997
+ data: { [key]: trimmed },
1998
+ lines: [trimmed]
1620
1999
  };
1621
- switch (command) {
1622
- case "init":
1623
- return init(cwd, registry, { ...installOptions, dir: values.dir });
1624
- case "add":
1625
- return add(cwd, registry, args, installOptions);
1626
- case "update":
1627
- return update(cwd, registry, args, installOptions);
1628
- case "list":
1629
- return list(cwd, registry, values.kind);
1630
- case "search":
1631
- return search(cwd, registry, args.join(" "));
1632
- case "view":
1633
- return view(cwd, registry, args[0]);
1634
- case "info":
1635
- return info(cwd);
1636
- case "diff":
1637
- return diff(cwd, registry, args);
1638
- case "doctor":
1639
- return doctor(cwd, registry);
1640
- default:
1641
- throw new CliError(
1642
- command ? `Unknown command "${command}". Run \`nxds --help\`.` : HELP
1643
- );
2000
+ }
2001
+ function commanderOutcome(error, printedText) {
2002
+ if (error.code === "commander.version") {
2003
+ return printed(printedText, "version");
1644
2004
  }
2005
+ if (error.exitCode === 0) return printed(printedText, "help");
2006
+ if (error.code === "commander.help") {
2007
+ return { exitCode: EXIT_CODES.usage, error: printedText.trimEnd() };
2008
+ }
2009
+ return {
2010
+ exitCode: EXIT_CODES.usage,
2011
+ error: `${error.message.replace(/^error: /, "")}
2012
+ Run \`${CLI_COMMAND} --help\` for the commands and options.`
2013
+ };
1645
2014
  }
1646
- async function main() {
1647
- const json = process.argv.includes("--json");
2015
+ async function outcome(argv) {
2016
+ let result;
2017
+ let printedText = "";
2018
+ const cli = program(
2019
+ (done) => {
2020
+ result = done;
2021
+ },
2022
+ (text) => {
2023
+ printedText += text;
2024
+ }
2025
+ );
1648
2026
  try {
1649
- const { values, positionals } = parseCommandLine();
1650
- const result = await run(values, positionals);
1651
- const output = json ? JSON.stringify({ ok: result.ok, data: result.data }, null, 2) : result.lines.join("\n");
1652
- process.stdout.write(`${output}
1653
- `);
1654
- process.exitCode = result.ok ? 0 : 1;
2027
+ await cli.parseAsync(argv, { from: "user" });
2028
+ if (!result) throw new Error("The command stored no result.");
2029
+ return {
2030
+ exitCode: result.ok ? EXIT_CODES.ok : EXIT_CODES.attention,
2031
+ data: result.data,
2032
+ lines: result.lines
2033
+ };
1655
2034
  } catch (error) {
1656
- process.exitCode = 1;
1657
- const message = error instanceof Error ? error.message : String(error);
1658
- if (json) {
1659
- process.stdout.write(
1660
- `${JSON.stringify({ ok: false, error: message }, null, 2)}
1661
- `
1662
- );
1663
- } else if (error instanceof CliError) {
1664
- console.error(message);
1665
- } else {
1666
- console.error(error);
2035
+ if (error instanceof CommanderError) {
2036
+ return commanderOutcome(error, printedText);
2037
+ }
2038
+ if (error instanceof CliError) {
2039
+ return { exitCode: error.exitCode, error: error.message };
1667
2040
  }
2041
+ const message = error instanceof Error ? error.message : String(error);
2042
+ const stack = error instanceof Error ? error.stack : void 0;
2043
+ return {
2044
+ exitCode: EXIT_CODES.error,
2045
+ error: message,
2046
+ stack: stack ?? message
2047
+ };
1668
2048
  }
1669
2049
  }
1670
- await main();
2050
+ async function main(argv, output) {
2051
+ const json = argv.includes("--json");
2052
+ const result = await outcome(argv);
2053
+ const ok = result.exitCode === EXIT_CODES.ok;
2054
+ if (json) {
2055
+ const body = "error" in result ? { ok: false, exitCode: result.exitCode, error: result.error } : { ok, exitCode: result.exitCode, data: result.data };
2056
+ output.out(`${JSON.stringify(body, null, 2)}
2057
+ `);
2058
+ } else if ("lines" in result) {
2059
+ output.out(`${result.lines.join("\n")}
2060
+ `);
2061
+ } else if (result.stack !== void 0) {
2062
+ output.err(`${result.stack}
2063
+ `);
2064
+ } else {
2065
+ output.err(`${result.error}
2066
+ `);
2067
+ }
2068
+ return result.exitCode;
2069
+ }
2070
+
2071
+ // src/index.ts
2072
+ process.exitCode = await main(process.argv.slice(2), {
2073
+ out: (text) => process.stdout.write(text),
2074
+ err: (text) => process.stderr.write(text)
2075
+ });