nxds-cli 0.0.0 → 0.2.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 +636 -378
  2. package/package.json +15 -11
package/dist/index.js CHANGED
@@ -1,27 +1,146 @@
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 path11 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
+ function variantWith(packages) {
53
+ return z.object({
54
+ /** Other items this one needs, installed first. */
55
+ items: z.array(z.string()),
56
+ packages,
57
+ files: z.array(catalogFileSchema),
58
+ /** Files in `files` the nxds CSS entry imports. */
59
+ css: z.array(relativePathSchema),
60
+ steps: z.array(setupStepSchema)
61
+ }).refine(
62
+ ({ css, files }) => css.every((entry) => files.some((file) => file.path === entry)),
63
+ { path: ["css"], message: "must name files in `files`" }
64
+ );
65
+ }
66
+ var variantSchema = variantWith(z.object({ npm: npmPackagesSchema }));
67
+ var setupVariantSchema = variantWith(
68
+ z.object({
69
+ npm: npmPackagesSchema,
70
+ /** The Tailwind range `init` installs, with the app's build plugin, when the app has no Tailwind. */
71
+ tailwind: rangeSchema
72
+ })
73
+ );
74
+ var itemInfo = {
75
+ name: z.string(),
76
+ title: z.string(),
77
+ description: z.string(),
78
+ docs: z.string()
79
+ };
80
+ var setupItemSchema = z.object({
81
+ ...itemInfo,
82
+ kind: z.literal("setup"),
83
+ variants: variantsSchema(setupVariantSchema)
84
+ });
85
+ var catalogItemSchema = z.discriminatedUnion("kind", [
86
+ setupItemSchema,
87
+ z.object({
88
+ ...itemInfo,
89
+ kind: z.enum(["component", "block"]),
90
+ variants: variantsSchema(variantSchema)
91
+ })
92
+ ]);
93
+ var CATALOG_SCHEMA = 1;
94
+ var catalogSchema = z.object({
95
+ schema: z.literal(CATALOG_SCHEMA),
96
+ /** The `@nxds/react` version this catalog was built from. */
97
+ version: versionSchema,
98
+ /** 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
+ frameworks: z.array(z.string()).transform((frameworks) => frameworks.filter(isFramework)),
100
+ items: z.array(catalogItemSchema)
101
+ });
102
+ function lines(issues, prefix) {
103
+ return issues.flatMap((issue) => {
104
+ const at = [...prefix, ...issue.path];
105
+ if (issue.code === "invalid_key") return lines(issue.issues, at);
106
+ return [
107
+ `${at.length > 0 ? at.map(String).join(".") : "(root)"}: ${issue.message}`
108
+ ];
109
+ });
110
+ }
111
+ function issueLines(error) {
112
+ return lines(error.issues, []);
113
+ }
6
114
 
7
115
  // src/catalog.ts
8
- import { existsSync as existsSync2 } from "fs";
116
+ import { existsSync } from "fs";
9
117
  import path2 from "path";
10
- import { valid as valid2, validRange } from "semver";
118
+ import { EnvHttpProxyAgent, fetch } from "undici";
11
119
 
12
- // src/config.ts
13
- import { existsSync, writeFileSync } from "fs";
14
- import path from "path";
15
- import { valid } from "semver";
120
+ // src/cli-command.ts
121
+ var CLI_COMMAND = "npx nxds-cli@latest";
16
122
 
17
123
  // src/errors.ts
18
124
  import { readFileSync } from "fs";
125
+ var EXIT_CODES = {
126
+ ok: 0,
127
+ /** A network or registry failure, an invalid file, or a bug in nxds. */
128
+ error: 1,
129
+ /** A bad flag, unknown command or item, or a missing argument. */
130
+ usage: 2,
131
+ /** The command ran, and left something for the person to do: conflicts, kept files, manual steps or failed checks. */
132
+ attention: 3
133
+ };
19
134
  var CliError = class extends Error {
135
+ exitCode = EXIT_CODES.error;
136
+ };
137
+ var UsageError = class extends CliError {
138
+ exitCode = EXIT_CODES.usage;
20
139
  };
21
140
  function readJson(file) {
22
- const text2 = readFileSync(file, "utf8");
141
+ const text = readFileSync(file, "utf8");
23
142
  try {
24
- return JSON.parse(text2);
143
+ return JSON.parse(text);
25
144
  } catch (error) {
26
145
  throw new CliError(
27
146
  `${file} is not valid JSON (${error.message}). Fix it, then run the command again.`
@@ -29,57 +148,11 @@ function readJson(file) {
29
148
  }
30
149
  }
31
150
 
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
151
  // src/catalog.ts
81
152
  var SETUP = "setup";
82
153
  var DEFAULT_REGISTRY = "https://nxds.dev/r";
154
+ var FETCH_TIMEOUT_MS = 3e4;
155
+ var dispatcher = new EnvHttpProxyAgent();
83
156
  function isUrl(registry) {
84
157
  return /^https?:\/\//.test(registry);
85
158
  }
@@ -87,13 +160,26 @@ function resolveRegistry(option) {
87
160
  if (!option) return DEFAULT_REGISTRY;
88
161
  return isUrl(option) ? option.replace(/\/+$/, "") : path2.resolve(option);
89
162
  }
163
+ function timeoutError(url, error) {
164
+ if (!(error instanceof Error) || error.name !== "TimeoutError") return null;
165
+ return new CliError(
166
+ `${url} did not answer within ${FETCH_TIMEOUT_MS / 1e3}s. Check your connection, then run the command again.`
167
+ );
168
+ }
90
169
  async function fetchJson(url) {
91
170
  let response;
92
171
  try {
93
- response = await fetch(url);
172
+ response = await fetch(url, {
173
+ dispatcher,
174
+ signal: AbortSignal.timeout(FETCH_TIMEOUT_MS)
175
+ });
94
176
  } catch (error) {
177
+ const timeout = timeoutError(url, error);
178
+ if (timeout) throw timeout;
179
+ const { message, cause } = error;
180
+ const reason = cause instanceof Error ? cause.message : message;
95
181
  throw new CliError(
96
- `Could not reach ${url} (${error.message}). Check your connection, then run the command again.`
182
+ `Could not reach ${url} (${reason}). Check your connection, or set HTTPS_PROXY if you are behind a proxy, then run the command again.`
97
183
  );
98
184
  }
99
185
  if (response.status === 404) return null;
@@ -105,7 +191,7 @@ async function fetchJson(url) {
105
191
  try {
106
192
  return await response.json();
107
193
  } catch (error) {
108
- throw new CliError(
194
+ throw timeoutError(url, error) ?? new CliError(
109
195
  `${url} is not valid JSON (${error.message}). Run the command again later.`
110
196
  );
111
197
  }
@@ -116,104 +202,45 @@ function registryLocation(registry, file) {
116
202
  async function readRegistryFile(registry, file) {
117
203
  const location = registryLocation(registry, file);
118
204
  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);
205
+ return existsSync(location) ? readJson(location) : null;
135
206
  }
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;
147
- }
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;
207
+ function isNewerSchema(value) {
208
+ return typeof value === "object" && value !== null && "schema" in value && typeof value.schema === "number" && value.schema > CATALOG_SCHEMA;
189
209
  }
190
210
  async function fetchRelease(registry, version) {
191
211
  const file = `${version}.json`;
192
212
  const value = await readRegistryFile(registry, file);
193
213
  if (value === null) return null;
194
- if (isRecord(value) && typeof value.schema === "number" && value.schema > 1) {
214
+ if (isNewerSchema(value)) {
215
+ const release = version === "latest" ? "The latest nxds release" : `nxds ${version}`;
195
216
  throw new CliError(
196
- `nxds ${version} needs a newer CLI. Run \`npx nxds@latest\` instead.`
217
+ `${release} needs a newer nxds-cli than ${"0.2.0"}. Run \`${CLI_COMMAND}\` to get the newest one.`
197
218
  );
198
219
  }
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.`
220
+ const invalid = (problems) => new CliError(
221
+ `${registryLocation(registry, file)} is not a valid nxds release:
222
+ ${problems.map((line) => ` ${line}`).join("\n")}
223
+ Check \`--registry\`, or run the command again later.`
203
224
  );
225
+ const parsed = catalogSchema.safeParse(value);
226
+ if (!parsed.success) throw invalid(issueLines(parsed.error));
227
+ if (version !== "latest" && parsed.data.version !== version) {
228
+ throw invalid([`version: it holds nxds ${parsed.data.version}`]);
229
+ }
230
+ return parsed.data;
204
231
  }
205
232
  async function loadRelease(registry, version) {
206
233
  const catalog = await fetchRelease(registry, version);
207
234
  if (catalog) return catalog;
208
235
  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.`
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.`
210
237
  );
211
238
  }
212
239
  function findItem(catalog, name) {
213
240
  const item = catalog.items.find((candidate) => candidate.name === name);
214
241
  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.`
242
+ throw new UsageError(
243
+ `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
244
  );
218
245
  }
219
246
  function variantOf(item, framework) {
@@ -258,15 +285,60 @@ function splitInstalled(catalog, installed) {
258
285
  };
259
286
  }
260
287
 
288
+ // src/config.ts
289
+ import { existsSync as existsSync2, writeFileSync } from "fs";
290
+ import path3 from "path";
291
+ import { z as z2 } from "zod";
292
+ var CONFIG_FILE = "nxds.json";
293
+ var configSchema = z2.looseObject({
294
+ /** The CLI installs React only. */
295
+ framework: z2.literal("react"),
296
+ /** The nxds release every installed file comes from. */
297
+ version: versionSchema,
298
+ /** The nxds folder, relative to the project root. */
299
+ dir: relativePathSchema,
300
+ /** The nxds CSS entry, relative to the project root. */
301
+ css: relativePathSchema,
302
+ /** Installed items, in install order. */
303
+ items: z2.array(z2.string())
304
+ });
305
+ function configPath(root) {
306
+ return path3.join(root, CONFIG_FILE);
307
+ }
308
+ function readConfig(root) {
309
+ const file = configPath(root);
310
+ if (!existsSync2(file)) return null;
311
+ const parsed = configSchema.safeParse(readJson(file));
312
+ if (parsed.success) return parsed.data;
313
+ throw new CliError(
314
+ `${CONFIG_FILE} is not valid:
315
+ ${issueLines(parsed.error).map((line) => ` ${line}`).join(
316
+ "\n"
317
+ )}
318
+ Fix it, or delete ${CONFIG_FILE} and run \`${CLI_COMMAND} init\` again.`
319
+ );
320
+ }
321
+ function requireConfig(root) {
322
+ const config = readConfig(root);
323
+ if (config) return config;
324
+ throw new CliError(
325
+ `nxds is not set up here. Run \`${CLI_COMMAND} init\` first.`
326
+ );
327
+ }
328
+ function writeConfig(root, config) {
329
+ writeFileSync(configPath(root), `${JSON.stringify(config, null, 2)}
330
+ `);
331
+ }
332
+
261
333
  // src/project.ts
262
334
  import { existsSync as existsSync4 } from "fs";
263
- import path4 from "path";
335
+ import path5 from "path";
264
336
  import { major, minVersion as minVersion2, validRange as validRange3 } from "semver";
265
337
 
266
338
  // src/packages.ts
267
339
  import { spawnSync } from "child_process";
268
340
  import { existsSync as existsSync3 } from "fs";
269
- import path3 from "path";
341
+ import path4 from "path";
270
342
  import { minVersion, satisfies, validRange as validRange2 } from "semver";
271
343
  var ADD_COMMAND = {
272
344
  npm: ["install"],
@@ -287,11 +359,11 @@ function packagesToInstall(project, packages) {
287
359
  function installedVersion(project, name) {
288
360
  let dir = project.root;
289
361
  for (; ; ) {
290
- const manifest = path3.join(dir, "node_modules", name, "package.json");
362
+ const manifest = path4.join(dir, "node_modules", name, "package.json");
291
363
  if (existsSync3(manifest)) {
292
364
  return readJson(manifest).version ?? null;
293
365
  }
294
- const parent = path3.dirname(dir);
366
+ const parent = path4.dirname(dir);
295
367
  if (parent === dir) return null;
296
368
  dir = parent;
297
369
  }
@@ -307,11 +379,11 @@ function installPackages(project, specs, { dev }) {
307
379
  cwd: project.root,
308
380
  stdio: ["ignore", 2, 2]
309
381
  };
310
- const run2 = process.platform === "win32" ? spawnSync(
382
+ const run = process.platform === "win32" ? spawnSync(
311
383
  [project.packageManager, ...args.map((arg) => `"${arg}"`)].join(" "),
312
384
  { ...options, shell: true }
313
385
  ) : spawnSync(project.packageManager, args, options);
314
- if (run2.status !== 0) {
386
+ if (run.status !== 0) {
315
387
  throw new CliError(
316
388
  `\`${project.packageManager} ${args.join(" ")}\` failed. Fix the error above, then run the same nxds command again.`
317
389
  );
@@ -338,7 +410,7 @@ var VITE_CONFIGS = [
338
410
  "vite.config.js"
339
411
  ];
340
412
  function firstExisting(root, candidates) {
341
- return candidates.find((file) => existsSync4(path4.join(root, file)));
413
+ return candidates.find((file) => existsSync4(path5.join(root, file)));
342
414
  }
343
415
  function detectSetup(root, dependencies) {
344
416
  if ("next" in dependencies && firstExisting(
@@ -353,10 +425,10 @@ function detectSetup(root, dependencies) {
353
425
  return null;
354
426
  }
355
427
  function readProject(root) {
356
- const manifestPath = path4.join(root, "package.json");
428
+ const manifestPath = path5.join(root, "package.json");
357
429
  if (!existsSync4(manifestPath)) {
358
430
  throw new CliError(
359
- `No package.json in ${root}. Run nxds from your app's root folder, or pass --cwd.`
431
+ `No package.json in ${root}. Run \`${CLI_COMMAND}\` from your app's root folder, or pass --cwd.`
360
432
  );
361
433
  }
362
434
  const manifest = readJson(manifestPath);
@@ -367,9 +439,9 @@ function readProject(root) {
367
439
  return {
368
440
  root,
369
441
  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")),
442
+ packageManager: LOCKFILES.find(([file]) => existsSync4(path5.join(root, file)))?.[1] ?? "npm",
443
+ usesSrc: existsSync4(path5.join(root, "src")),
444
+ typescript: existsSync4(path5.join(root, "tsconfig.json")),
373
445
  dependencies
374
446
  };
375
447
  }
@@ -385,7 +457,6 @@ function majorVersion(project, name) {
385
457
  }
386
458
 
387
459
  // src/commands/browse.ts
388
- var KINDS2 = ["component", "block"];
389
460
  function appRelease(cwd, registry) {
390
461
  return loadRelease(registry, readConfig(cwd)?.version ?? "latest");
391
462
  }
@@ -397,8 +468,8 @@ function summary(item) {
397
468
  description: item.description
398
469
  };
399
470
  }
400
- function firstSentence(text2) {
401
- return text2.split(/(?<=\.)\s/)[0]?.replaceAll("`", "") ?? text2;
471
+ function firstSentence(text) {
472
+ return text.split(/(?<=\.)\s/)[0]?.replaceAll("`", "") ?? text;
402
473
  }
403
474
  function summaryLines(items) {
404
475
  const width = Math.max(...items.map((item) => item.name.length));
@@ -407,17 +478,12 @@ function summaryLines(items) {
407
478
  );
408
479
  }
409
480
  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
481
  const items = (await appRelease(cwd, registry)).items.filter(
414
482
  (item) => item.name !== SETUP && (kind === void 0 || item.kind === kind)
415
483
  );
416
484
  return { data: items.map(summary), lines: summaryLines(items), ok: true };
417
485
  }
418
486
  async function search(cwd, registry, query) {
419
- if (!query)
420
- throw new CliError("Give a search term, such as `nxds search date`.");
421
487
  const needle = query.toLowerCase();
422
488
  const rank = (item) => {
423
489
  if (item.name.includes(needle)) return 0;
@@ -441,11 +507,10 @@ function projectFramework(cwd, catalog) {
441
507
  const [only, ...others] = catalog.frameworks;
442
508
  if (only && others.length === 0) return only;
443
509
  throw new CliError(
444
- "This release has several frameworks. Run `nxds init` first, so nxds knows which one your app uses."
510
+ `This release has several frameworks. Run \`${CLI_COMMAND} init\` first, so nxds knows which one your app uses.`
445
511
  );
446
512
  }
447
513
  async function view(cwd, registry, name) {
448
- if (!name) throw new CliError("Name an item, such as `nxds view button`.");
449
514
  const catalog = await appRelease(cwd, registry);
450
515
  const item = findItem(catalog, name);
451
516
  const variant = variantOf(item, projectFramework(cwd, catalog));
@@ -497,7 +562,7 @@ function info(cwd) {
497
562
  `Package manager: ${project.packageManager}`,
498
563
  `TypeScript: ${project.typescript ? "yes" : "no"}`,
499
564
  `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`.",
565
+ config ? `nxds: ${config.version} in ${config.dir}, CSS entry ${config.css}` : `nxds: not set up. Run \`${CLI_COMMAND} init\`.`,
501
566
  ...config && config.items.length > 0 ? [
502
567
  `Installed: ${config.items.filter((item) => item !== SETUP).join(", ") || "setup only"}`
503
568
  ] : []
@@ -509,42 +574,104 @@ function info(cwd) {
509
574
  // src/commands/check.ts
510
575
  import { spawnSync as spawnSync2 } from "child_process";
511
576
  import { existsSync as existsSync7, readFileSync as readFileSync4 } from "fs";
512
- import path7 from "path";
577
+ import path8 from "path";
513
578
  import { satisfies as satisfies2 } from "semver";
514
579
 
515
580
  // src/files.ts
516
581
  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();
582
+ import path6 from "path";
583
+ import { merge } from "node-diff3";
584
+ function baseOf(previous, file) {
585
+ if (!previous) return void 0;
586
+ return previous.get(file) ?? "";
587
+ }
588
+ function withLf(content) {
589
+ return `${content.replace(/\r\n?/g, "\n").replace(/\n+$/, "")}
590
+ `;
591
+ }
592
+ function normalize(content) {
593
+ const lines2 = withLf(content).split("\n").map((line) => line.trimEnd());
594
+ return `${lines2.join("\n").trimEnd()}
595
+ `;
596
+ }
597
+ function sameContent(a, b) {
598
+ return b !== void 0 && normalize(a) === normalize(b);
599
+ }
600
+ var OURS_LABEL = "your file";
601
+ var CONFLICT_START = `<<<<<<< ${OURS_LABEL}`;
602
+ function hasConflictMarkers(content) {
603
+ return content.split(/\r?\n/).includes(CONFLICT_START);
604
+ }
605
+ function conflictedFiles(root, config, files) {
606
+ return [...files].filter((file) => {
607
+ const target = path6.join(root, config.dir, file);
608
+ return existsSync5(target) && hasConflictMarkers(readFileSync2(target, "utf8"));
609
+ });
610
+ }
519
611
  function fileState(root, config, file, previous) {
520
- const target = path5.join(root, config.dir, file.path);
612
+ const target = path6.join(root, config.dir, file.path);
521
613
  if (!existsSync5(target)) return "missing";
522
614
  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: [] };
615
+ if (hasConflictMarkers(onDisk)) return "conflicted";
616
+ if (sameContent(onDisk, file.content)) return "current";
617
+ return sameContent(onDisk, previous?.get(file.path)) ? "outdated" : "changed";
618
+ }
619
+ function mergeFile(ours, base, theirs, version) {
620
+ const lines2 = (content) => withLf(content).split("\n");
621
+ const { conflict, result } = merge(lines2(ours), lines2(base), lines2(theirs), {
622
+ label: { a: OURS_LABEL, b: `nxds ${version}` }
623
+ });
624
+ return { content: result.join("\n"), conflict };
625
+ }
626
+ function writeFiles(root, config, files, previous, { overwrite, dryRun, version }) {
627
+ const result = {
628
+ written: [],
629
+ unchanged: [],
630
+ merged: [],
631
+ conflicted: [],
632
+ unresolved: [],
633
+ skipped: []
634
+ };
635
+ const write = (file, content) => {
636
+ if (dryRun) return;
637
+ const target = path6.join(root, config.dir, file);
638
+ mkdirSync(path6.dirname(target), { recursive: true });
639
+ writeFileSync2(target, content);
640
+ };
528
641
  for (const file of files) {
529
642
  const state = fileState(root, config, file, previous);
530
643
  if (state === "current") {
531
644
  result.unchanged.push(file.path);
532
645
  continue;
533
646
  }
534
- if (state === "changed" && !overwrite) {
647
+ if (overwrite || state === "missing" || state === "outdated") {
648
+ result.written.push(file.path);
649
+ write(file.path, file.content);
650
+ continue;
651
+ }
652
+ if (state === "conflicted") {
653
+ result.unresolved.push(file.path);
654
+ continue;
655
+ }
656
+ const base = baseOf(previous, file.path);
657
+ if (base === void 0) {
535
658
  result.skipped.push(file.path);
536
659
  continue;
537
660
  }
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);
661
+ const target = path6.join(root, config.dir, file.path);
662
+ const ours = readFileSync2(target, "utf8");
663
+ const merged = mergeFile(ours, base, file.content, version);
664
+ if (sameContent(ours, merged.content)) {
665
+ result.unchanged.push(file.path);
666
+ continue;
667
+ }
668
+ result[merged.conflict ? "conflicted" : "merged"].push(file.path);
669
+ write(file.path, merged.content);
543
670
  }
544
671
  return result;
545
672
  }
546
673
  function relativeImport(fromFile, toFile) {
547
- const relative = path5.relative(path5.dirname(fromFile), toFile).split(path5.sep).join("/");
674
+ const relative = path6.relative(path6.dirname(fromFile), toFile).split(path6.sep).join("/");
548
675
  return relative.startsWith(".") ? relative : `./${relative}`;
549
676
  }
550
677
 
@@ -561,7 +688,7 @@ import {
561
688
  isSpreadElement
562
689
  } from "@babel/types";
563
690
  import { existsSync as existsSync6, readFileSync as readFileSync3 } from "fs";
564
- import path6 from "path";
691
+ import path7 from "path";
565
692
 
566
693
  // src/setup/source-edit.ts
567
694
  import { parse } from "@babel/parser";
@@ -611,24 +738,24 @@ function findJsx(root, name) {
611
738
  }
612
739
  function applyInsertions(source, insertions) {
613
740
  return [...insertions].sort((a, b) => b.at - a.at).reduce(
614
- (text2, { at, text: inserted }) => text2.slice(0, at) + inserted + text2.slice(at),
741
+ (text, { at, text: inserted }) => text.slice(0, at) + inserted + text.slice(at),
615
742
  source
616
743
  );
617
744
  }
618
- function importInsertion(source, program, lines) {
619
- const imports = program.body.filter((node) => isImportDeclaration(node));
745
+ function importInsertion(source, program2, lines2) {
746
+ const imports = program2.body.filter((node) => isImportDeclaration(node));
620
747
  const first = imports[0];
621
748
  const quote = first && source[startOf(first.source)];
622
749
  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");
750
+ const text = lines2.map((line) => quote === '"' ? line.replaceAll("'", '"') : line).map((line) => semicolons ? line : line.replace(/;$/, "")).join("\n");
624
751
  const lastImport = imports.at(-1);
625
752
  if (lastImport) return { at: endOf(lastImport), text: `
626
- ${text2}` };
627
- const lastDirective = program.directives.at(-1);
753
+ ${text}` };
754
+ const lastDirective = program2.directives.at(-1);
628
755
  if (lastDirective) return { at: endOf(lastDirective), text: `
629
756
 
630
- ${text2}` };
631
- return { at: 0, text: `${text2}
757
+ ${text}` };
758
+ return { at: 0, text: `${text}
632
759
 
633
760
  ` };
634
761
  }
@@ -667,7 +794,7 @@ function tailwindPackages(setup, range) {
667
794
  }
668
795
  function appDir(project) {
669
796
  const dir = APP_DIRS.find(
670
- (candidate) => existsSync6(path6.join(project.root, candidate, "layout.tsx"))
797
+ (candidate) => existsSync6(path7.join(project.root, candidate, "layout.tsx"))
671
798
  );
672
799
  if (!dir)
673
800
  throw new CliError("No app/layout.tsx or src/app/layout.tsx found.");
@@ -683,7 +810,7 @@ function cssEntryPath(project) {
683
810
  return `${entryDir(project)}/nxds.css`;
684
811
  }
685
812
  function read(project, file) {
686
- return readFileSync3(path6.join(project.root, file), "utf8");
813
+ return readFileSync3(path7.join(project.root, file), "utf8");
687
814
  }
688
815
  function cssEntry(config, withPreflight) {
689
816
  const tokens = relativeImport(config.css, `${config.dir}/tailwind/nxds.css`);
@@ -722,8 +849,8 @@ function wireNextLayout(project, config, steps) {
722
849
  const layout = `${appDir(project)}/layout.tsx`;
723
850
  const source = read(project, layout);
724
851
  if (/NxdsAppearance(Provider|Script)/.test(source)) return NO_CHANGES;
725
- const program = parseModule(source, layout);
726
- const html = findJsx(program, "html");
852
+ const program2 = parseModule(source, layout);
853
+ const html = findJsx(program2, "html");
727
854
  const body = html && findJsx(html, "body");
728
855
  const head = html && findJsx(html, "head");
729
856
  if (!html || !body?.closingElement || head && !head.closingElement) {
@@ -785,15 +912,15 @@ ${htmlIndent}${step}</head>`
785
912
  imports.push("import { defaultAppearance } from './appearance-state';");
786
913
  }
787
914
  if (imports.length === 0) return NO_CHANGES;
788
- insertions.push(importInsertion(source, program, imports));
915
+ insertions.push(importInsertion(source, program2, imports));
789
916
  return {
790
917
  changes: [editedFile(layout, applyInsertions(source, insertions))],
791
918
  manual: []
792
919
  };
793
920
  }
794
- function renderedElement(program) {
921
+ function renderedElement(program2) {
795
922
  const call = findNode(
796
- program,
923
+ program2,
797
924
  (node) => isCallExpression(node) && isMemberExpression(node.callee) && isIdentifier(node.callee.property, { name: "render" }) && isJSXElement2(node.arguments[0])
798
925
  );
799
926
  return call?.arguments[0];
@@ -803,12 +930,12 @@ function wireViteEntry(project, config, steps) {
803
930
  if (!entry) throw new CliError("No src/main.tsx found.");
804
931
  const source = read(project, entry);
805
932
  if (source.includes("NxdsAppearanceProvider")) return NO_CHANGES;
806
- const program = parseModule(source, entry);
933
+ const program2 = parseModule(source, entry);
807
934
  const imports = [];
808
935
  const insertions = [];
809
936
  if (steps.has("add-css-import")) imports.push("import './nxds.css';");
810
937
  if (steps.has("wrap-root-with-provider")) {
811
- const app = renderedElement(program);
938
+ const app = renderedElement(program2);
812
939
  if (!app) {
813
940
  return {
814
941
  changes: [],
@@ -836,7 +963,7 @@ ${indent}</NxdsAppearanceProvider>` }
836
963
  );
837
964
  }
838
965
  if (imports.length === 0) return NO_CHANGES;
839
- insertions.push(importInsertion(source, program, imports));
966
+ insertions.push(importInsertion(source, program2, imports));
840
967
  return {
841
968
  changes: [editedFile(entry, applyInsertions(source, insertions))],
842
969
  manual: []
@@ -863,8 +990,8 @@ export function nxdsAppearance(): Plugin {
863
990
  };
864
991
  }
865
992
  `;
866
- function configObject(program) {
867
- const declaration = program.body.find(
993
+ function configObject(program2) {
994
+ const declaration = program2.body.find(
868
995
  (node) => isExportDefaultDeclaration(node)
869
996
  )?.declaration;
870
997
  if (isObjectExpression(declaration)) return declaration;
@@ -894,11 +1021,11 @@ var NXDS_VITE_PLUGIN = {
894
1021
  var VITE_ALIAS = "{ '@': '/src' }";
895
1022
  function wireViteConfig(project, addTailwind, steps) {
896
1023
  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"));
1024
+ const addScript = steps.has("add-head-script") && !existsSync6(path7.join(project.root, "nxds.vite.ts"));
898
1025
  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);
1026
+ const source = existsSync6(path7.join(project.root, configFile)) ? read(project, configFile) : "";
1027
+ const program2 = source ? parseModule(source, configFile) : void 0;
1028
+ const object = program2 && configObject(program2);
902
1029
  const plugins = object && pluginsArray(object);
903
1030
  const newPlugins = [
904
1031
  ...addTailwind ? [TAILWIND_VITE_PLUGIN] : [],
@@ -938,8 +1065,8 @@ function wireViteConfig(project, addTailwind, steps) {
938
1065
  step: `Add resolve.alias ${VITE_ALIAS}, so imports like @/components/nxds/... resolve.`
939
1066
  });
940
1067
  }
941
- if (program && insertions.length > 0) {
942
- insertions.push(importInsertion(source, program, imports));
1068
+ if (program2 && insertions.length > 0) {
1069
+ insertions.push(importInsertion(source, program2, imports));
943
1070
  changes.push(editedFile(configFile, applyInsertions(source, insertions)));
944
1071
  }
945
1072
  return { changes, manual };
@@ -984,8 +1111,8 @@ var ESLINT_CONFIGS = [
984
1111
  "eslint.config.mjs",
985
1112
  "eslint.config.ts"
986
1113
  ];
987
- function exportedConfigs(program) {
988
- const declaration = program.body.find(
1114
+ function exportedConfigs(program2) {
1115
+ const declaration = program2.body.find(
989
1116
  (node) => isExportDefaultDeclaration(node)
990
1117
  )?.declaration;
991
1118
  if (isArrayExpression(declaration)) return declaration.elements;
@@ -1057,7 +1184,7 @@ function wireNextTailwind(project) {
1057
1184
  };
1058
1185
  }
1059
1186
  function createdUnlessExists(project, file, content) {
1060
- if (existsSync6(path6.join(project.root, file))) return NO_CHANGES;
1187
+ if (existsSync6(path7.join(project.root, file))) return NO_CHANGES;
1061
1188
  return { changes: [{ path: file, content, created: true }], manual: [] };
1062
1189
  }
1063
1190
  function planWiring(project, config, addTailwind, setupSteps) {
@@ -1096,34 +1223,50 @@ async function diff(cwd, registry, names) {
1096
1223
  const config = requireConfig(cwd);
1097
1224
  const latest = await loadRelease(registry, "latest");
1098
1225
  const previous = await fetchRelease(registry, config.version);
1099
- const previousFiles = previous ? releaseFiles(previous, config.framework) : NO_PREVIOUS;
1226
+ const previousFiles = previous && releaseFiles(previous, config.framework);
1100
1227
  const { known, removed } = splitInstalled(latest, config.items);
1101
1228
  const items = resolveItems(
1102
1229
  latest,
1103
1230
  config.framework,
1104
1231
  names.length > 0 ? names : known
1105
1232
  );
1233
+ const update2 = `\`${CLI_COMMAND} update\``;
1234
+ const overwrite = `\`${CLI_COMMAND} update --overwrite\``;
1235
+ function advice(state, file) {
1236
+ if (state === "outdated") return `${update2} replaces it`;
1237
+ if (state === "missing") return `${update2} restores it`;
1238
+ if (state === "conflicted") {
1239
+ return `has conflict markers; resolve them, or ${overwrite} replaces it`;
1240
+ }
1241
+ const base = baseOf(previousFiles, file.path);
1242
+ if (base === void 0) {
1243
+ return `has your edits; ${update2} keeps it, ${overwrite} replaces it`;
1244
+ }
1245
+ if (sameContent(base, file.content)) {
1246
+ return `has your edits; nxds hasn't changed it since ${config.version}`;
1247
+ }
1248
+ return `has your edits; ${update2} merges nxds's changes into it, ${overwrite} replaces it`;
1249
+ }
1106
1250
  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
- }))
1251
+ (item) => variantOf(item, config.framework).files.map((file) => {
1252
+ const state = fileState(cwd, config, file, previousFiles);
1253
+ return {
1254
+ item: item.name,
1255
+ path: file.path,
1256
+ state,
1257
+ advice: state === "current" ? null : advice(state, file)
1258
+ };
1259
+ })
1112
1260
  );
1113
1261
  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
1262
  return {
1120
1263
  data: { version: config.version, latest: latest.version, files, removed },
1121
1264
  lines: [
1122
1265
  ...differing.map(
1123
- (file) => `${file.state.padEnd(8)} ${file.path} (${advice[file.state]})`
1266
+ (file) => `${file.state.padEnd(10)} ${file.path} (${file.advice})`
1124
1267
  ),
1125
1268
  ...removed.map(
1126
- (name) => `removed ${name} (no longer in nxds; \`nxds update\` drops it)`
1269
+ (name) => `${"removed".padEnd(10)} ${name} (no longer in nxds; \`${CLI_COMMAND} update\` drops it)`
1127
1270
  ),
1128
1271
  ...differing.length === 0 && removed.length === 0 ? [`Every file matches nxds ${latest.version}.`] : []
1129
1272
  ],
@@ -1133,7 +1276,7 @@ async function diff(cwd, registry, names) {
1133
1276
  function tailwindMessage(project) {
1134
1277
  const declared = project.dependencies.tailwindcss;
1135
1278
  if (declared === void 0) {
1136
- return "Tailwind 4 is not installed. Run `nxds init`.";
1279
+ return `Tailwind 4 is not installed. Run \`${CLI_COMMAND} init\`.`;
1137
1280
  }
1138
1281
  const tailwindMajor = majorVersion(project, "tailwindcss");
1139
1282
  if (tailwindMajor === 4) return `tailwindcss ${declared}`;
@@ -1143,14 +1286,14 @@ function tailwindMessage(project) {
1143
1286
  return `tailwindcss ${declared}. nxds needs Tailwind CSS 4.`;
1144
1287
  }
1145
1288
  function read2(root, file) {
1146
- if (!file || !existsSync7(path7.join(root, file))) return "";
1147
- return readFileSync4(path7.join(root, file), "utf8");
1289
+ if (!file || !existsSync7(path8.join(root, file))) return "";
1290
+ return readFileSync4(path8.join(root, file), "utf8");
1148
1291
  }
1149
1292
  function sourcesNxdsDir(config, css) {
1150
- const entryDir2 = path7.posix.dirname(config.css);
1293
+ const entryDir2 = path8.posix.dirname(config.css);
1151
1294
  return [...css.matchAll(/@source\s+(['"])(.*?)\1/g)].some(([, , source]) => {
1152
1295
  const base = (source ?? "").split(/[*{[]/)[0] ?? "";
1153
- const folder = path7.posix.join(entryDir2, base).replace(/\/+$/, "");
1296
+ const folder = path8.posix.join(entryDir2, base).replace(/\/+$/, "");
1154
1297
  return folder === "." || /^\.\.(\/\.\.)*$/.test(folder) || config.dir === folder || config.dir.startsWith(`${folder}/`);
1155
1298
  });
1156
1299
  }
@@ -1190,7 +1333,7 @@ async function doctor(cwd, registry) {
1190
1333
  check(
1191
1334
  "config",
1192
1335
  config !== null,
1193
- config ? "nxds.json found" : "No nxds.json. Run `nxds init`."
1336
+ config ? "nxds.json found" : `No nxds.json. Run \`${CLI_COMMAND} init\`.`
1194
1337
  );
1195
1338
  if (!project.setup) {
1196
1339
  check("app", false, "Not a Next.js App Router or Vite + React app.");
@@ -1200,7 +1343,7 @@ async function doctor(cwd, registry) {
1200
1343
  check(
1201
1344
  "release",
1202
1345
  catalog !== null,
1203
- catalog ? `Files come from nxds ${config.version}` : `${registry} has no nxds ${config.version}. Run \`nxds update\`.`
1346
+ catalog ? `Files come from nxds ${config.version}` : `${registry} has no nxds ${config.version}. Run \`${CLI_COMMAND} update\`.`
1204
1347
  );
1205
1348
  check(
1206
1349
  "tailwind",
@@ -1212,7 +1355,7 @@ async function doctor(cwd, registry) {
1212
1355
  check(
1213
1356
  "css entry",
1214
1357
  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.`
1358
+ !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
1359
  );
1217
1360
  const unscanned = unscannedReason(cwd, config, css);
1218
1361
  check(
@@ -1222,7 +1365,7 @@ async function doctor(cwd, registry) {
1222
1365
  );
1223
1366
  const entry = project.setup === "next-app" ? `${entryDir(project)}/layout.tsx` : findFile(project, VITE_ENTRIES);
1224
1367
  const entrySource = read2(cwd, entry);
1225
- const cssFile = path7.basename(config.css);
1368
+ const cssFile = path8.basename(config.css);
1226
1369
  const importsCss = entrySource.includes(cssFile);
1227
1370
  check(
1228
1371
  "css import",
@@ -1239,31 +1382,43 @@ async function doctor(cwd, registry) {
1239
1382
  check(
1240
1383
  "theme script",
1241
1384
  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`."
1385
+ 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
1386
  );
1244
1387
  if (!catalog) return report(checks);
1245
1388
  const { known, removed } = splitInstalled(catalog, config.items);
1246
1389
  check(
1247
1390
  "items",
1248
1391
  removed.length === 0,
1249
- removed.length === 0 ? `${known.length} installed items, all in this release` : `No longer in nxds: ${removed.join(", ")}. Run \`nxds update\`.`
1392
+ removed.length === 0 ? `${known.length} installed items, all in this release` : `No longer in nxds: ${removed.join(", ")}. Run \`${CLI_COMMAND} update\`.`
1250
1393
  );
1251
1394
  const items = resolveItems(catalog, config.framework, known);
1252
1395
  const missingFiles = items.flatMap(
1253
1396
  (item) => variantOf(item, config.framework).files.filter(
1254
- (file) => !existsSync7(path7.join(cwd, config.dir, file.path))
1397
+ (file) => !existsSync7(path8.join(cwd, config.dir, file.path))
1255
1398
  ).map((file) => `${item.name}: ${file.path}`)
1256
1399
  );
1257
1400
  check(
1258
1401
  "files",
1259
1402
  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.`
1403
+ missingFiles.length === 0 ? `All files for ${known.length} installed items are present` : `Missing ${missingFiles.join(", ")}. Run \`${CLI_COMMAND} add\` for those items.`
1404
+ );
1405
+ const conflicted = conflictedFiles(
1406
+ cwd,
1407
+ config,
1408
+ items.flatMap(
1409
+ (item) => variantOf(item, config.framework).files.map((file) => file.path)
1410
+ )
1411
+ ).map((file) => `${config.dir}/${file}`);
1412
+ check(
1413
+ "conflicts",
1414
+ conflicted.length === 0,
1415
+ conflicted.length === 0 ? "No conflict markers left from an update" : `Resolve the <<<<<<< markers in ${conflicted.join(", ")}.`
1261
1416
  );
1262
1417
  const problems = packageProblems(project, config, items);
1263
1418
  check(
1264
1419
  "packages",
1265
1420
  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.`
1421
+ 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
1422
  );
1268
1423
  return report(checks);
1269
1424
  }
@@ -1283,14 +1438,27 @@ function report(checks) {
1283
1438
 
1284
1439
  // src/commands/install.ts
1285
1440
  import { existsSync as existsSync9, mkdirSync as mkdirSync2, writeFileSync as writeFileSync4 } from "fs";
1286
- import path9 from "path";
1441
+ import path10 from "path";
1287
1442
 
1288
1443
  // src/install.ts
1289
1444
  import { existsSync as existsSync8, readFileSync as readFileSync5, writeFileSync as writeFileSync3 } from "fs";
1290
- import path8 from "path";
1445
+ import path9 from "path";
1446
+ function emptyInstallResult() {
1447
+ return {
1448
+ items: [],
1449
+ written: [],
1450
+ unchanged: [],
1451
+ merged: [],
1452
+ conflicted: [],
1453
+ unresolved: [],
1454
+ skipped: [],
1455
+ packages: [],
1456
+ cssImports: []
1457
+ };
1458
+ }
1291
1459
  function addCssImports(project, config, files, dryRun) {
1292
1460
  if (files.length === 0) return [];
1293
- const entry = path8.join(project.root, config.css);
1461
+ const entry = path9.join(project.root, config.css);
1294
1462
  const current = existsSync8(entry) ? readFileSync5(entry, "utf8") : "";
1295
1463
  const added = [...new Set(files)].map((file) => cssImportLine(config, file)).filter((line) => !current.includes(line));
1296
1464
  if (added.length > 0 && !dryRun) {
@@ -1311,7 +1479,7 @@ function installItems(catalog, project, config, names, previous, options) {
1311
1479
  config,
1312
1480
  [...files.values()],
1313
1481
  previous,
1314
- options
1482
+ { ...options, version: catalog.version }
1315
1483
  );
1316
1484
  const packages = packagesToInstall(
1317
1485
  project,
@@ -1347,6 +1515,15 @@ function installLines(config, result, options) {
1347
1515
  const verb = dryRun ? "Would write" : "Wrote";
1348
1516
  return [
1349
1517
  ...result.written.map((file) => `${verb} ${config.dir}/${file}`),
1518
+ ...result.merged.map(
1519
+ (file) => `${dryRun ? "Would merge" : "Merged"} nxds's changes into your edits in ${config.dir}/${file}`
1520
+ ),
1521
+ ...result.conflicted.map(
1522
+ (file) => `${dryRun ? "Would conflict" : "Conflict"} in ${config.dir}/${file}: nxds changed lines you edited. Resolve the <<<<<<< markers, then run \`${CLI_COMMAND} update\` again.`
1523
+ ),
1524
+ ...result.unresolved.map(
1525
+ (file) => `Still conflicted: ${config.dir}/${file}. Resolve the <<<<<<< markers, then run \`${CLI_COMMAND} update\` again.`
1526
+ ),
1350
1527
  ...result.skipped.map(
1351
1528
  (file) => `Kept ${config.dir}/${file}: it differs from nxds's copy, so it may hold your edits. Pass --overwrite to replace it.`
1352
1529
  ),
@@ -1356,10 +1533,13 @@ function installLines(config, result, options) {
1356
1533
  )
1357
1534
  ];
1358
1535
  }
1536
+ function settled(result) {
1537
+ return result.skipped.length === 0 && result.conflicted.length === 0 && result.unresolved.length === 0;
1538
+ }
1359
1539
  function nxdsDir(dir) {
1360
- const normalized = projectPath(dir);
1540
+ const normalized = relativePath(dir);
1361
1541
  if (normalized) return normalized;
1362
- throw new CliError(
1542
+ throw new UsageError(
1363
1543
  `--dir takes a folder inside your app, such as components/nxds, not "${dir}".`
1364
1544
  );
1365
1545
  }
@@ -1380,7 +1560,7 @@ async function init(cwd, registry, options) {
1380
1560
  const project = readProject(cwd);
1381
1561
  if (!project.setup) {
1382
1562
  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."
1563
+ `\`${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
1564
  );
1385
1565
  }
1386
1566
  if (!project.typescript) {
@@ -1419,20 +1599,20 @@ async function init(cwd, registry, options) {
1419
1599
  project,
1420
1600
  config,
1421
1601
  [SETUP],
1422
- NO_PREVIOUS,
1602
+ null,
1423
1603
  options
1424
1604
  );
1425
1605
  if (!options.dryRun) {
1426
1606
  for (const change of wiring.changes) {
1427
- const target = path9.join(project.root, change.path);
1428
- mkdirSync2(path9.dirname(target), { recursive: true });
1607
+ const target = path10.join(project.root, change.path);
1608
+ mkdirSync2(path10.dirname(target), { recursive: true });
1429
1609
  writeFileSync4(target, change.content);
1430
1610
  }
1431
1611
  writeConfig(cwd, config);
1432
1612
  }
1433
1613
  const packages = [...tailwind, ...installed.packages];
1434
1614
  const [createVerb, editVerb] = options.dryRun ? ["Would create", "Would edit"] : ["Created", "Edited"];
1435
- const done = wiring.manual.length === 0;
1615
+ const done = wiring.manual.length === 0 && settled(installed);
1436
1616
  return {
1437
1617
  data: {
1438
1618
  setup: project.setup,
@@ -1453,7 +1633,9 @@ async function init(cwd, registry, options) {
1453
1633
  (change) => `${change.created ? createVerb : editVerb} ${change.path}`
1454
1634
  ),
1455
1635
  ...manualLines(wiring.manual),
1456
- ...done && !options.dryRun ? ["nxds is set up. Add a component with `nxds add button`."] : []
1636
+ ...done && !options.dryRun ? [
1637
+ `nxds is set up. Add a component with \`${CLI_COMMAND} add button\`.`
1638
+ ] : []
1457
1639
  ],
1458
1640
  ok: done
1459
1641
  };
@@ -1461,14 +1643,11 @@ async function init(cwd, registry, options) {
1461
1643
  function manualLines(manual) {
1462
1644
  if (manual.length === 0) return [];
1463
1645
  return [
1464
- "Finish these steps by hand, then run `nxds doctor`:",
1646
+ `Finish these steps by hand, then run \`${CLI_COMMAND} doctor\`:`,
1465
1647
  ...manual.map(({ file, step }) => ` ${file}: ${step}`)
1466
1648
  ];
1467
1649
  }
1468
1650
  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
1651
  const config = requireConfig(cwd);
1473
1652
  const catalog = await loadRelease(registry, config.version);
1474
1653
  const installed = installItems(
@@ -1476,7 +1655,7 @@ async function add(cwd, registry, names, options) {
1476
1655
  readProject(cwd),
1477
1656
  config,
1478
1657
  names,
1479
- NO_PREVIOUS,
1658
+ null,
1480
1659
  options
1481
1660
  );
1482
1661
  if (!options.dryRun) writeConfig(cwd, config);
@@ -1484,11 +1663,11 @@ async function add(cwd, registry, names, options) {
1484
1663
  data: { dryRun: options.dryRun, ...installed },
1485
1664
  lines: [
1486
1665
  ...installLines(config, installed, options),
1487
- ...installed.written.length === 0 && installed.skipped.length === 0 ? [
1666
+ ...installed.written.length === 0 && settled(installed) ? [
1488
1667
  `${names.join(", ")} ${names.length === 1 ? "is" : "are"} already up to date.`
1489
1668
  ] : []
1490
1669
  ],
1491
- ok: true
1670
+ ok: settled(installed)
1492
1671
  };
1493
1672
  }
1494
1673
  function filesOf(catalog, framework, names) {
@@ -1498,24 +1677,40 @@ function filesOf(catalog, framework, names) {
1498
1677
  )
1499
1678
  );
1500
1679
  }
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
- }
1680
+ async function update(cwd, registry, options) {
1507
1681
  const config = requireConfig(cwd);
1682
+ const from = config.version;
1508
1683
  const latest = await loadRelease(registry, "latest");
1509
- const previous = await fetchRelease(registry, config.version);
1510
1684
  const { known, removed } = splitInstalled(latest, config.items);
1511
1685
  const current = filesOf(latest, config.framework, known);
1686
+ const previous = await fetchRelease(registry, from);
1512
1687
  const installedBefore = previous ? filesOf(
1513
1688
  previous,
1514
1689
  config.framework,
1515
1690
  splitInstalled(previous, config.items).known
1516
1691
  ) : /* @__PURE__ */ new Set();
1692
+ const unresolved = from === latest.version || options.overwrite ? [] : conflictedFiles(cwd, config, /* @__PURE__ */ new Set([...current, ...installedBefore]));
1693
+ if (unresolved.length > 0) {
1694
+ const data2 = {
1695
+ ...emptyInstallResult(),
1696
+ dryRun: options.dryRun,
1697
+ from,
1698
+ version: from,
1699
+ unresolved,
1700
+ removed: [],
1701
+ orphaned: []
1702
+ };
1703
+ return {
1704
+ data: data2,
1705
+ lines: [
1706
+ ...unresolved.map((file) => `Still conflicted: ${config.dir}/${file}`),
1707
+ `Changed nothing, and nxds.json stays on ${from}. Resolve the <<<<<<< markers, then run \`${CLI_COMMAND} update\` again.`
1708
+ ],
1709
+ ok: false
1710
+ };
1711
+ }
1517
1712
  const orphaned = [...installedBefore].filter(
1518
- (file) => !current.has(file) && existsSync9(path9.join(cwd, config.dir, file))
1713
+ (file) => !current.has(file) && existsSync9(path10.join(cwd, config.dir, file))
1519
1714
  );
1520
1715
  config.items = known;
1521
1716
  const installed = installItems(
@@ -1523,22 +1718,22 @@ async function update(cwd, registry, names, options) {
1523
1718
  readProject(cwd),
1524
1719
  config,
1525
1720
  known,
1526
- previous ? releaseFiles(previous, config.framework) : NO_PREVIOUS,
1721
+ previous && releaseFiles(previous, config.framework),
1527
1722
  options
1528
1723
  );
1529
- const from = config.version;
1530
1724
  const stays = from !== latest.version && installed.skipped.length > 0;
1531
1725
  if (!stays) config.version = latest.version;
1532
1726
  if (!options.dryRun) writeConfig(cwd, config);
1727
+ const data = {
1728
+ dryRun: options.dryRun,
1729
+ from,
1730
+ version: config.version,
1731
+ ...installed,
1732
+ removed,
1733
+ orphaned
1734
+ };
1533
1735
  return {
1534
- data: {
1535
- dryRun: options.dryRun,
1536
- from,
1537
- version: config.version,
1538
- ...installed,
1539
- removed,
1540
- orphaned
1541
- },
1736
+ data,
1542
1737
  lines: [
1543
1738
  ...previous ? [] : [
1544
1739
  `${registry} has no nxds ${from}, so every file that differs from ${latest.version} counts as changed.`
@@ -1550,121 +1745,184 @@ async function update(cwd, registry, names, options) {
1550
1745
  ...orphaned.map(
1551
1746
  (file) => `No installed item uses ${config.dir}/${file} any more. Delete it once nothing in your app imports it.`
1552
1747
  ),
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}.`
1748
+ 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
1749
  ],
1555
- ok: !stays
1750
+ ok: settled(installed)
1556
1751
  };
1557
1752
  }
1558
1753
 
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
- }
1754
+ // src/cli.ts
1755
+ function context(command) {
1756
+ const { cwd, registry } = command.optsWithGlobals();
1757
+ return {
1758
+ cwd: path11.resolve(cwd ?? process.cwd()),
1759
+ registry: resolveRegistry(registry)
1760
+ };
1761
+ }
1762
+ function installOptions(command) {
1763
+ const flags = command.opts();
1764
+ return {
1765
+ overwrite: flags.overwrite ?? false,
1766
+ dryRun: flags.dryRun ?? false,
1767
+ install: flags.install
1768
+ };
1769
+ }
1770
+ function withInstallFlags(command) {
1771
+ 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");
1772
+ }
1773
+ function program(done, write) {
1774
+ const cli = new Command().configureOutput({
1775
+ writeOut: write,
1776
+ writeErr: write,
1777
+ // Errors are printed once, by `main`, as text or JSON.
1778
+ outputError: () => {
1779
+ }
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(
1781
+ new Option(
1782
+ "--registry <url|path>",
1783
+ "where to get nxds releases (default: https://nxds.dev/r)"
1784
+ ).env("NXDS_REGISTRY_URL")
1785
+ ).configureHelp({ showGlobalOptions: true }).addHelpText(
1786
+ "after",
1787
+ `
1788
+ Exit codes: 0 done, 1 error, 2 usage error, 3 done with something left for you (conflicts, kept files, manual steps, failed checks).
1789
+ Docs: https://nxds.dev/getting-started/install`
1790
+ );
1791
+ withInstallFlags(
1792
+ cli.command("init").description("set up nxds: token CSS, provider, and the layout wiring").option(
1793
+ "--dir <path>",
1794
+ "nxds folder (default: components/nxds, or src/components/nxds)"
1795
+ )
1796
+ ).action(async (_options, command) => {
1797
+ const { cwd, registry } = context(command);
1798
+ const { dir } = command.opts();
1799
+ done(await init(cwd, registry, { ...installOptions(command), dir }));
1800
+ });
1801
+ withInstallFlags(
1802
+ cli.command("add").description("add components or blocks, and everything they need").argument("<items...>", "item names, such as button")
1803
+ ).action(async (items, _options, command) => {
1804
+ const { cwd, registry } = context(command);
1805
+ done(await add(cwd, registry, items, installOptions(command)));
1806
+ });
1807
+ withInstallFlags(
1808
+ cli.command("update").description(
1809
+ "update every installed item to the latest release, merging nxds changes into your edits"
1810
+ )
1811
+ ).action(async (_options, command) => {
1812
+ const { cwd, registry } = context(command);
1813
+ done(await update(cwd, registry, installOptions(command)));
1814
+ });
1815
+ cli.command("list").description("list components and blocks").addOption(
1816
+ new Option("--kind <kind>", "only this kind").choices([
1817
+ "component",
1818
+ "block"
1819
+ ])
1820
+ ).action(async (_options, command) => {
1821
+ const { cwd, registry } = context(command);
1822
+ const { kind } = command.opts();
1823
+ done(await list(cwd, registry, kind));
1824
+ });
1825
+ cli.command("search").description("find items by name or description").argument("<term...>", "words to look for").action(async (term, _options, command) => {
1826
+ const { cwd, registry } = context(command);
1827
+ done(await search(cwd, registry, term.join(" ")));
1828
+ });
1829
+ cli.command("view").description("show an item's files, packages and docs link").argument("<item>", "item name").action(async (item, _options, command) => {
1830
+ const { cwd, registry } = context(command);
1831
+ done(await view(cwd, registry, item));
1832
+ });
1833
+ cli.command("info").description("show what the CLI detects in this app").action((_options, command) => {
1834
+ done(info(context(command).cwd));
1835
+ });
1836
+ 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) => {
1837
+ const { cwd, registry } = context(command);
1838
+ done(await diff(cwd, registry, items));
1839
+ });
1840
+ cli.command("doctor").description("check that nxds is wired up correctly").action(async (_options, command) => {
1841
+ const { cwd, registry } = context(command);
1842
+ done(await doctor(cwd, registry));
1843
+ });
1844
+ return cli;
1605
1845
  }
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"]
1846
+ function printed(text, key) {
1847
+ const trimmed = text.trimEnd();
1848
+ return {
1849
+ exitCode: EXIT_CODES.ok,
1850
+ data: { [key]: trimmed },
1851
+ lines: [trimmed]
1620
1852
  };
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
- );
1853
+ }
1854
+ function commanderOutcome(error, printedText) {
1855
+ if (error.code === "commander.version") {
1856
+ return printed(printedText, "version");
1644
1857
  }
1858
+ if (error.exitCode === 0) return printed(printedText, "help");
1859
+ if (error.code === "commander.help") {
1860
+ return { exitCode: EXIT_CODES.usage, error: printedText.trimEnd() };
1861
+ }
1862
+ return {
1863
+ exitCode: EXIT_CODES.usage,
1864
+ error: `${error.message.replace(/^error: /, "")}
1865
+ Run \`${CLI_COMMAND} --help\` for the commands and options.`
1866
+ };
1645
1867
  }
1646
- async function main() {
1647
- const json = process.argv.includes("--json");
1868
+ async function outcome(argv) {
1869
+ let result;
1870
+ let printedText = "";
1871
+ const cli = program(
1872
+ (done) => {
1873
+ result = done;
1874
+ },
1875
+ (text) => {
1876
+ printedText += text;
1877
+ }
1878
+ );
1648
1879
  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;
1880
+ await cli.parseAsync(argv, { from: "user" });
1881
+ if (!result) throw new Error("The command stored no result.");
1882
+ return {
1883
+ exitCode: result.ok ? EXIT_CODES.ok : EXIT_CODES.attention,
1884
+ data: result.data,
1885
+ lines: result.lines
1886
+ };
1655
1887
  } 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);
1888
+ if (error instanceof CommanderError) {
1889
+ return commanderOutcome(error, printedText);
1890
+ }
1891
+ if (error instanceof CliError) {
1892
+ return { exitCode: error.exitCode, error: error.message };
1667
1893
  }
1894
+ const message = error instanceof Error ? error.message : String(error);
1895
+ const stack = error instanceof Error ? error.stack : void 0;
1896
+ return {
1897
+ exitCode: EXIT_CODES.error,
1898
+ error: message,
1899
+ stack: stack ?? message
1900
+ };
1901
+ }
1902
+ }
1903
+ async function main(argv, output) {
1904
+ const json = argv.includes("--json");
1905
+ const result = await outcome(argv);
1906
+ const ok = result.exitCode === EXIT_CODES.ok;
1907
+ if (json) {
1908
+ const body = "error" in result ? { ok: false, exitCode: result.exitCode, error: result.error } : { ok, exitCode: result.exitCode, data: result.data };
1909
+ output.out(`${JSON.stringify(body, null, 2)}
1910
+ `);
1911
+ } else if ("lines" in result) {
1912
+ output.out(`${result.lines.join("\n")}
1913
+ `);
1914
+ } else if (result.stack !== void 0) {
1915
+ output.err(`${result.stack}
1916
+ `);
1917
+ } else {
1918
+ output.err(`${result.error}
1919
+ `);
1668
1920
  }
1921
+ return result.exitCode;
1669
1922
  }
1670
- await main();
1923
+
1924
+ // src/index.ts
1925
+ process.exitCode = await main(process.argv.slice(2), {
1926
+ out: (text) => process.stdout.write(text),
1927
+ err: (text) => process.stderr.write(text)
1928
+ });