newtui 2.0.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 (97) hide show
  1. package/LICENSE +202 -0
  2. package/README.md +53 -0
  3. package/cli/index.js +137 -0
  4. package/dist/index.js +1408 -0
  5. package/package.json +70 -0
  6. package/registry/html/components/attachment.css +42 -0
  7. package/registry/html/components/attachment.html +8 -0
  8. package/registry/html/components/avatar.css +32 -0
  9. package/registry/html/components/avatar.html +3 -0
  10. package/registry/html/components/badge.css +39 -0
  11. package/registry/html/components/badge.html +3 -0
  12. package/registry/html/components/bot-command-card.css +67 -0
  13. package/registry/html/components/bot-command-card.html +17 -0
  14. package/registry/html/components/button.css +98 -0
  15. package/registry/html/components/button.html +5 -0
  16. package/registry/html/components/card.css +20 -0
  17. package/registry/html/components/card.html +4 -0
  18. package/registry/html/components/channel-topic.css +35 -0
  19. package/registry/html/components/channel-topic.html +8 -0
  20. package/registry/html/components/code-block.css +25 -0
  21. package/registry/html/components/code-block.html +2 -0
  22. package/registry/html/components/context-menu.css +46 -0
  23. package/registry/html/components/context-menu.html +14 -0
  24. package/registry/html/components/cooldown-bar.css +33 -0
  25. package/registry/html/components/cooldown-bar.html +9 -0
  26. package/registry/html/components/divider.css +18 -0
  27. package/registry/html/components/divider.html +1 -0
  28. package/registry/html/components/dropdown.css +50 -0
  29. package/registry/html/components/dropdown.html +17 -0
  30. package/registry/html/components/embed.css +65 -0
  31. package/registry/html/components/embed.html +14 -0
  32. package/registry/html/components/empty-state.css +26 -0
  33. package/registry/html/components/empty-state.html +5 -0
  34. package/registry/html/components/form-fields.css +85 -0
  35. package/registry/html/components/form-fields.html +20 -0
  36. package/registry/html/components/kbd-tag.css +18 -0
  37. package/registry/html/components/kbd-tag.html +1 -0
  38. package/registry/html/components/member-list.css +32 -0
  39. package/registry/html/components/member-list.html +13 -0
  40. package/registry/html/components/mention.css +36 -0
  41. package/registry/html/components/mention.html +3 -0
  42. package/registry/html/components/message-group.css +50 -0
  43. package/registry/html/components/message-group.html +20 -0
  44. package/registry/html/components/modal.css +37 -0
  45. package/registry/html/components/modal.html +17 -0
  46. package/registry/html/components/modal.js +10 -0
  47. package/registry/html/components/notif-badge.css +28 -0
  48. package/registry/html/components/notif-badge.html +13 -0
  49. package/registry/html/components/pagination.css +57 -0
  50. package/registry/html/components/pagination.html +16 -0
  51. package/registry/html/components/permission.css +64 -0
  52. package/registry/html/components/permission.html +33 -0
  53. package/registry/html/components/progress.css +29 -0
  54. package/registry/html/components/progress.html +6 -0
  55. package/registry/html/components/reaction.css +33 -0
  56. package/registry/html/components/reaction.html +14 -0
  57. package/registry/html/components/reply-preview.css +35 -0
  58. package/registry/html/components/reply-preview.html +4 -0
  59. package/registry/html/components/role-tag.css +19 -0
  60. package/registry/html/components/role-tag.html +3 -0
  61. package/registry/html/components/scrollbar.css +26 -0
  62. package/registry/html/components/scrollbar.html +7 -0
  63. package/registry/html/components/select-menu.css +72 -0
  64. package/registry/html/components/select-menu.html +19 -0
  65. package/registry/html/components/server-banner.css +58 -0
  66. package/registry/html/components/server-banner.html +21 -0
  67. package/registry/html/components/skeleton.css +40 -0
  68. package/registry/html/components/skeleton.html +7 -0
  69. package/registry/html/components/slash-command.css +16 -0
  70. package/registry/html/components/slash-command.html +3 -0
  71. package/registry/html/components/spoiler.css +22 -0
  72. package/registry/html/components/spoiler.html +8 -0
  73. package/registry/html/components/stage-banner.css +46 -0
  74. package/registry/html/components/stage-banner.html +22 -0
  75. package/registry/html/components/status-indicator.css +85 -0
  76. package/registry/html/components/status-indicator.html +4 -0
  77. package/registry/html/components/tabs.css +28 -0
  78. package/registry/html/components/tabs.html +7 -0
  79. package/registry/html/components/tabs.js +12 -0
  80. package/registry/html/components/timeline.css +63 -0
  81. package/registry/html/components/timeline.html +26 -0
  82. package/registry/html/components/toast.css +37 -0
  83. package/registry/html/components/toast.html +7 -0
  84. package/registry/html/components/toast.js +16 -0
  85. package/registry/html/components/token-field.css +10 -0
  86. package/registry/html/components/token-field.html +18 -0
  87. package/registry/html/components/token-field.js +17 -0
  88. package/registry/html/components/tooltip.css +26 -0
  89. package/registry/html/components/tooltip.html +8 -0
  90. package/registry/html/components/typing-indicator.css +45 -0
  91. package/registry/html/components/typing-indicator.html +4 -0
  92. package/registry/html/components/user-profile.css +67 -0
  93. package/registry/html/components/user-profile.html +30 -0
  94. package/registry/html/components/voice-channel.css +57 -0
  95. package/registry/html/components/voice-channel.html +13 -0
  96. package/registry/html/registry.json +279 -0
  97. package/registry/html/tokens.css +64 -0
package/dist/index.js ADDED
@@ -0,0 +1,1408 @@
1
+ #!/usr/bin/env node
2
+ import { createRequire } from "node:module";
3
+ import { spawn } from "node:child_process";
4
+ import path from "node:path";
5
+ import { fileURLToPath } from "node:url";
6
+ import mri from "mri";
7
+ import { cancel, confirm, intro, isCancel, log, multiselect, outro, select, spinner, text } from "@clack/prompts";
8
+ import { z } from "zod";
9
+ import { existsSync } from "node:fs";
10
+ import { mkdir, readFile, writeFile } from "node:fs/promises";
11
+ import color from "picocolors";
12
+ import spawn$1 from "cross-spawn";
13
+ import { resolveCommand } from "package-manager-detector/commands";
14
+ import { detect } from "package-manager-detector/detect";
15
+
16
+ //#region src/tools/fileSystem.ts
17
+ function pathExists(target) {
18
+ return existsSync(target);
19
+ }
20
+ /** Resolve a `--cwd` value, failing early when the directory is missing. */
21
+ function resolveCwd(cwd) {
22
+ const resolved = path.resolve(cwd);
23
+ if (!existsSync(resolved)) throw new Error(`The path ${resolved} does not exist. Please try again.`);
24
+ return resolved;
25
+ }
26
+ /** First candidate (relative to `cwd`) that exists on disk. */
27
+ function findFirstExisting(cwd, candidates) {
28
+ return candidates.find((candidate) => existsSync(path.resolve(cwd, candidate)));
29
+ }
30
+ async function readFileIfExists(target) {
31
+ if (!existsSync(target)) return null;
32
+ return readFile(target, "utf8");
33
+ }
34
+ async function readTextFile(target) {
35
+ return readFile(target, "utf8");
36
+ }
37
+ /** Write a file, creating any missing parent directories first. */
38
+ async function writeFileAt(target, content) {
39
+ await mkdir(path.dirname(target), { recursive: true });
40
+ await writeFile(target, content, "utf8");
41
+ }
42
+ function relativePath(cwd, target) {
43
+ return path.relative(cwd, target);
44
+ }
45
+
46
+ //#endregion
47
+ //#region src/tools/options.ts
48
+ /**
49
+ * Static option tables for the `newtui` CLI.
50
+ *
51
+ * Everything here is declared `as const` so the derived union types stay in
52
+ * sync with the values the argv parser and the commands actually accept.
53
+ */
54
+ const COMMANDS = [
55
+ "init",
56
+ "add",
57
+ "list",
58
+ "diff"
59
+ ];
60
+ const PACKAGE_MANAGERS = [
61
+ "npm",
62
+ "yarn",
63
+ "pnpm",
64
+ "bun"
65
+ ];
66
+ /** The UI framework a project targets, and the key registry items carry. */
67
+ const FRAMEWORKS = ["react", "vue"];
68
+ /**
69
+ * The build tool a Vue project uses. Only Vue projects need this — it decides
70
+ * which stylesheet `init` looks for and which setup instructions it prints.
71
+ * Earlier releases of the Vue CLI stored these values under `framework`;
72
+ * `config.ts` migrates such a `components.json` on read.
73
+ */
74
+ const BUNDLERS = ["nuxt", "vite"];
75
+ const BOOLEAN_FLAGS = [
76
+ "all",
77
+ "defaults",
78
+ "help",
79
+ "json",
80
+ "legacy",
81
+ "overwrite",
82
+ "skip-install",
83
+ "version",
84
+ "yes"
85
+ ];
86
+ const STRING_FLAGS = [
87
+ "bundler",
88
+ "css",
89
+ "cwd",
90
+ "framework",
91
+ "path",
92
+ "registry",
93
+ "type"
94
+ ];
95
+ const FLAG_ALIASES = {
96
+ a: "all",
97
+ b: "bundler",
98
+ c: "cwd",
99
+ d: "defaults",
100
+ f: "framework",
101
+ h: "help",
102
+ o: "overwrite",
103
+ p: "path",
104
+ r: "registry",
105
+ t: "type",
106
+ v: "version",
107
+ y: "yes"
108
+ };
109
+ const SHARED_DEPENDENCIES = ["class-variance-authority", "tailwindcss-animate"];
110
+ const PROJECT_DEPENDENCIES_V3 = [
111
+ "clsx",
112
+ "tailwind-merge",
113
+ ...SHARED_DEPENDENCIES
114
+ ];
115
+ const PROJECT_DEPENDENCIES_V4 = ["cn", ...SHARED_DEPENDENCIES];
116
+ /** Nuxt config files `init` looks for when detecting a Vue project's bundler. */
117
+ const NUXT_CONFIG_CANDIDATES = [
118
+ "nuxt.config.ts",
119
+ "nuxt.config.js",
120
+ "nuxt.config.mjs"
121
+ ];
122
+ /** Vite config files `init` looks for when detecting a Vue project's bundler. */
123
+ const VITE_CONFIG_CANDIDATES = [
124
+ "vite.config.ts",
125
+ "vite.config.js",
126
+ "vite.config.mjs"
127
+ ];
128
+ /** Next.js config files `init` looks for when detecting a React project. */
129
+ const NEXT_CONFIG_CANDIDATES = [
130
+ "next.config.ts",
131
+ "next.config.js",
132
+ "next.config.mjs"
133
+ ];
134
+ /** Global stylesheets `init` looks for, per framework (Vue splits by bundler). */
135
+ const CSS_CANDIDATES = {
136
+ react: [
137
+ "app/globals.css",
138
+ "src/app/globals.css",
139
+ "styles/globals.css",
140
+ "src/styles/globals.css",
141
+ "src/index.css",
142
+ "src/app.css"
143
+ ],
144
+ nuxt: [
145
+ "src/assets/css/tailwind.css",
146
+ "assets/css/tailwind.css",
147
+ "src/assets/css/main.css",
148
+ "assets/css/main.css"
149
+ ],
150
+ vite: [
151
+ "src/assets/index.css",
152
+ "src/assets/main.css",
153
+ "src/style.css",
154
+ "src/index.css"
155
+ ]
156
+ };
157
+ /** Tailwind config files `init` looks for, in order of preference. */
158
+ const TAILWIND_CONFIG_CANDIDATES = [
159
+ "tailwind.config.ts",
160
+ "tailwind.config.js",
161
+ "tailwind.config.mjs",
162
+ "tailwind.config.cjs"
163
+ ];
164
+ function isCommandName(value) {
165
+ return COMMANDS.some((command) => command === value);
166
+ }
167
+ function isFrameworkName(value) {
168
+ return FRAMEWORKS.some((framework) => framework === value);
169
+ }
170
+ function isBundlerName(value) {
171
+ return BUNDLERS.some((bundler) => bundler === value);
172
+ }
173
+ /** Read a flag that expects a value, ignoring `--flag` used without one. */
174
+ function flagString(value) {
175
+ if (value === void 0 || value === true || value === false) return void 0;
176
+ return value.length > 0 ? value : void 0;
177
+ }
178
+ /** Read a boolean flag, tolerating `--flag=true` style input. */
179
+ function flagBoolean(value) {
180
+ return value === true || value === "true";
181
+ }
182
+
183
+ //#endregion
184
+ //#region src/tools/config.ts
185
+ const DEFAULT_STYLE = "default";
186
+ const DEFAULT_COMPONENTS = "@/components";
187
+ const DEFAULT_UTILS = "@/lib/utils";
188
+ const DEFAULT_UI = "@/components/ui";
189
+ const DEFAULT_LIB = "@/lib";
190
+ const DEFAULT_HOOKS = "@/hooks";
191
+ const DEFAULT_COMPOSABLES = "@/composables";
192
+ const DEFAULT_TAILWIND_CONFIG = "tailwind.config.ts";
193
+ const DEFAULT_TAILWIND_BASE_COLOR = "neutral";
194
+ const DEFAULT_FRAMEWORK = "react";
195
+ const DEFAULT_BUNDLER = "nuxt";
196
+ /** Where `init` writes the tokens when it finds no existing stylesheet. */
197
+ const DEFAULT_TAILWIND_CSS = {
198
+ react: "app/globals.css",
199
+ vue: "src/assets/css/tailwind.css"
200
+ };
201
+ const CONFIG_FILE_NAME = "components.json";
202
+ const frameworkSchema = z.enum(FRAMEWORKS);
203
+ const bundlerSchema = z.enum(BUNDLERS);
204
+ const rawConfigSchema = z.object({
205
+ $schema: z.string().optional(),
206
+ style: z.string().default(DEFAULT_STYLE),
207
+ framework: frameworkSchema.default(DEFAULT_FRAMEWORK),
208
+ /** Vue only — which build tool the project uses. */
209
+ bundler: bundlerSchema.optional(),
210
+ typescript: z.boolean().default(true),
211
+ /**
212
+ * React only, and left optional so a Vue `components.json` does not grow a
213
+ * field that means nothing there. Kept separate from `typescript` because
214
+ * it controls whether `"use client"` survives, which TypeScript alone does
215
+ * not imply. Absent means enabled — see `RSC_DEFAULT`.
216
+ */
217
+ rsc: z.boolean().optional(),
218
+ /**
219
+ * Legacy alias of `typescript` written by the React CLI before the two
220
+ * CLIs merged. Read by `migrateRawConfig` and preserved so an existing
221
+ * `components.json` keeps validating.
222
+ */
223
+ tsx: z.boolean().optional(),
224
+ tailwind: z.object({
225
+ config: z.string().default(DEFAULT_TAILWIND_CONFIG),
226
+ css: z.string(),
227
+ baseColor: z.string().default(DEFAULT_TAILWIND_BASE_COLOR),
228
+ cssVariables: z.boolean().default(true),
229
+ prefix: z.string().default("").optional()
230
+ }),
231
+ aliases: z.object({
232
+ components: z.string().default(DEFAULT_COMPONENTS),
233
+ utils: z.string().default(DEFAULT_UTILS),
234
+ ui: z.string().optional(),
235
+ lib: z.string().optional(),
236
+ hooks: z.string().optional(),
237
+ composables: z.string().optional()
238
+ }),
239
+ iconLibrary: z.string().optional(),
240
+ registry: z.string().optional()
241
+ }).strict();
242
+ const configSchema = rawConfigSchema.extend({ resolvedPaths: z.object({
243
+ cwd: z.string(),
244
+ tailwindConfig: z.string(),
245
+ tailwindCss: z.string(),
246
+ utils: z.string(),
247
+ components: z.string(),
248
+ ui: z.string(),
249
+ lib: z.string(),
250
+ hooks: z.string(),
251
+ composables: z.string()
252
+ }) });
253
+ /**
254
+ * The wire shape of `components.json`: the fields above plus the ones the two
255
+ * CLIs wrote before they merged. `framework` used to name the Vue build tool,
256
+ * and the React CLI spelled "is this project TypeScript" as `tsx`.
257
+ */
258
+ const componentsFileSchema = z.object({
259
+ framework: z.enum([...FRAMEWORKS, ...BUNDLERS]).optional(),
260
+ bundler: bundlerSchema.optional(),
261
+ typescript: z.boolean().optional(),
262
+ tsx: z.boolean().optional(),
263
+ aliases: z.object({ composables: z.string().optional() }).passthrough().optional()
264
+ }).passthrough();
265
+ /**
266
+ * Bring a `components.json` written by either of the old CLIs up to the merged
267
+ * shape, so an existing project keeps working without being re-initialized.
268
+ */
269
+ function migrateComponentsFile(file) {
270
+ const migrated = { ...file };
271
+ if (file.framework === "nuxt" || file.framework === "vite") {
272
+ migrated.bundler = file.bundler ?? file.framework;
273
+ migrated.framework = "vue";
274
+ } else if (file.framework === void 0) migrated.framework = file.typescript !== void 0 || file.aliases?.composables !== void 0 ? "vue" : "react";
275
+ if (file.typescript === void 0 && file.tsx !== void 0) migrated.typescript = file.tsx;
276
+ return migrated;
277
+ }
278
+ async function readTsConfigPaths(cwd) {
279
+ for (const candidate of ["tsconfig.json", "jsconfig.json"]) {
280
+ const filePath = path.resolve(cwd, candidate);
281
+ const raw = await readFileIfExists(filePath);
282
+ if (raw === null) continue;
283
+ try {
284
+ const cleaned = raw.replace(/\/\*[\s\S]*?\*\//g, "").replace(/^\s*\/\/.*$/gm, "").replace(/,(\s*[}\]])/g, "$1");
285
+ const parsed = JSON.parse(cleaned);
286
+ return {
287
+ baseUrl: parsed.compilerOptions?.baseUrl ?? ".",
288
+ paths: parsed.compilerOptions?.paths ?? {}
289
+ };
290
+ } catch {}
291
+ }
292
+ return {
293
+ baseUrl: ".",
294
+ paths: {}
295
+ };
296
+ }
297
+ /**
298
+ * Resolve an import alias such as `@/components/ui` to an absolute directory.
299
+ * Uses tsconfig `paths` when available, otherwise falls back to `src/` (the
300
+ * layout used by the newt/ui Vite and Nuxt templates) or the project root.
301
+ */
302
+ async function resolveImport(alias, cwd, preferSrcDir) {
303
+ const { baseUrl, paths } = await readTsConfigPaths(cwd);
304
+ const base = path.resolve(cwd, baseUrl);
305
+ for (const [pattern, targets] of Object.entries(paths)) {
306
+ const target = targets[0];
307
+ if (!target) continue;
308
+ const prefix = pattern.replace(/\*$/, "");
309
+ if (alias === prefix.replace(/\/$/, "") || alias.startsWith(prefix)) {
310
+ const rest = alias.slice(prefix.length);
311
+ const targetPrefix = target.replace(/\*$/, "");
312
+ return path.resolve(base, targetPrefix + rest);
313
+ }
314
+ }
315
+ const stripped = alias.replace(/^[@~]\//, "");
316
+ const srcDir = path.resolve(cwd, "src");
317
+ if (preferSrcDir || pathExists(srcDir)) return path.resolve(srcDir, stripped);
318
+ return path.resolve(cwd, stripped);
319
+ }
320
+ async function resolveConfigPaths(cwd, config) {
321
+ const preferSrcDir = config.tailwind.css.startsWith("src/");
322
+ const utils = await resolveImport(config.aliases.utils, cwd, preferSrcDir);
323
+ const components = await resolveImport(config.aliases.components, cwd, preferSrcDir);
324
+ const ui = config.aliases.ui ? await resolveImport(config.aliases.ui, cwd, preferSrcDir) : path.resolve(components, "ui");
325
+ const lib = config.aliases.lib ? await resolveImport(config.aliases.lib, cwd, preferSrcDir) : path.dirname(utils);
326
+ const hooks = config.aliases.hooks ? await resolveImport(config.aliases.hooks, cwd, preferSrcDir) : path.resolve(path.dirname(components), "hooks");
327
+ const composables = config.aliases.composables ? await resolveImport(config.aliases.composables, cwd, preferSrcDir) : path.resolve(path.dirname(components), "composables");
328
+ return configSchema.parse({
329
+ ...config,
330
+ resolvedPaths: {
331
+ cwd,
332
+ tailwindConfig: path.resolve(cwd, config.tailwind.config),
333
+ tailwindCss: path.resolve(cwd, config.tailwind.css),
334
+ utils,
335
+ components,
336
+ ui,
337
+ lib,
338
+ hooks,
339
+ composables
340
+ }
341
+ });
342
+ }
343
+ async function getRawConfig(cwd) {
344
+ const configPath = path.resolve(cwd, CONFIG_FILE_NAME);
345
+ const raw = await readFileIfExists(configPath);
346
+ if (raw === null) return null;
347
+ try {
348
+ return rawConfigSchema.parse(migrateComponentsFile(componentsFileSchema.parse(JSON.parse(raw))));
349
+ } catch (error) {
350
+ const detail = error instanceof Error ? error.message : String(error);
351
+ throw new Error(`Invalid configuration found in ${configPath}.\n${detail}`, { cause: error });
352
+ }
353
+ }
354
+ async function getConfig(cwd) {
355
+ const raw = await getRawConfig(cwd);
356
+ if (!raw) return null;
357
+ return resolveConfigPaths(cwd, raw);
358
+ }
359
+
360
+ //#endregion
361
+ //#region src/tools/logger.ts
362
+ const highlighter = {
363
+ error: (value) => color.red(value),
364
+ warn: (value) => color.yellow(value),
365
+ info: (value) => color.cyan(value),
366
+ success: (value) => color.green(value),
367
+ dim: (value) => color.dim(value),
368
+ bold: (value) => color.bold(value)
369
+ };
370
+ const logger = {
371
+ error(message) {
372
+ log.error(message);
373
+ },
374
+ warn(message) {
375
+ log.warn(message);
376
+ },
377
+ info(message) {
378
+ log.info(message);
379
+ },
380
+ success(message) {
381
+ log.success(message);
382
+ },
383
+ log(message) {
384
+ log.message(message);
385
+ },
386
+ break() {
387
+ console.log("");
388
+ }
389
+ };
390
+ function handleError(cause) {
391
+ const asString = z.string().safeParse(cause);
392
+ if (asString.success) logger.error(asString.data);
393
+ else if (cause instanceof Error) logger.error(cause.message);
394
+ else logger.error("Something went wrong. Please try again.");
395
+ process.exit(1);
396
+ }
397
+
398
+ //#endregion
399
+ //#region src/tools/packageManager.ts
400
+ const DEFAULT_PACKAGE_MANAGER = "npm";
401
+ function toPackageManager(name) {
402
+ return PACKAGE_MANAGERS.find((candidate) => candidate === name) ?? DEFAULT_PACKAGE_MANAGER;
403
+ }
404
+ /** Detect the package manager used by the project at `cwd` (lockfile first). */
405
+ async function detectPackageManager(cwd) {
406
+ return toPackageManager((await detect({ cwd }))?.name);
407
+ }
408
+ function run(command, args, cwd) {
409
+ return new Promise((resolve, reject) => {
410
+ const child = spawn$1(command, args, {
411
+ cwd,
412
+ stdio: "inherit"
413
+ });
414
+ child.on("error", reject);
415
+ child.on("close", (code) => {
416
+ if (code === 0) {
417
+ resolve();
418
+ return;
419
+ }
420
+ reject(/* @__PURE__ */ new Error(`\`${command} ${args.join(" ")}\` failed with exit code ${code ?? 1}.`));
421
+ });
422
+ });
423
+ }
424
+ async function installDependencies(cwd, dependencies, options = {}) {
425
+ if (dependencies.length === 0) return;
426
+ const packageManager = await detectPackageManager(cwd);
427
+ const args = options.dev ? [...dependencies, "-D"] : dependencies;
428
+ const command = resolveCommand(packageManager, "add", args);
429
+ if (!command) throw new Error(`Could not resolve an install command for ${packageManager}.`);
430
+ await run(command.command, command.args, cwd);
431
+ }
432
+
433
+ //#endregion
434
+ //#region src/tools/prompts.ts
435
+ /** Exit cleanly when the user cancels a prompt (ctrl+c / esc). */
436
+ function unwrap(value) {
437
+ if (isCancel(value)) {
438
+ cancel("Operation cancelled.");
439
+ process.exit(1);
440
+ }
441
+ return value;
442
+ }
443
+ async function promptConfirm(message, initialValue = true) {
444
+ return unwrap(await confirm({
445
+ message,
446
+ initialValue
447
+ }));
448
+ }
449
+ async function promptText(message, initialValue) {
450
+ return unwrap(await text({
451
+ message,
452
+ placeholder: initialValue,
453
+ defaultValue: initialValue
454
+ }));
455
+ }
456
+ async function promptSelect(message, options, initialValue) {
457
+ return unwrap(await select({
458
+ message,
459
+ options,
460
+ initialValue
461
+ }));
462
+ }
463
+ async function promptMultiselect(message, options) {
464
+ return unwrap(await multiselect({
465
+ message,
466
+ options,
467
+ required: false
468
+ }));
469
+ }
470
+
471
+ //#endregion
472
+ //#region src/tools/schema.ts
473
+ const registryItemTypeSchema = z.enum([
474
+ "registry:style",
475
+ "registry:lib",
476
+ "registry:example",
477
+ "registry:block",
478
+ "registry:component",
479
+ "registry:ui",
480
+ "registry:hook",
481
+ "registry:theme",
482
+ "registry:page",
483
+ "registry:file"
484
+ ]);
485
+ /**
486
+ * Which UI framework an item's files target. Items are served from
487
+ * per-framework registries, so this is informational for consumers that mix
488
+ * both (the docs site, external tooling) rather than a routing key.
489
+ */
490
+ const registryItemFrameworkSchema = z.enum(["react", "vue"]);
491
+ const registryItemFileSchema = z.union([z.string(), z.object({
492
+ path: z.string(),
493
+ content: z.string().optional(),
494
+ type: registryItemTypeSchema,
495
+ target: z.string().optional()
496
+ })]);
497
+ /**
498
+ * Tailwind v3 only. Consumers on v4 get the same values through
499
+ * `cssVars.theme`, which maps to an `@theme` block instead of a JS config.
500
+ */
501
+ const registryItemTailwindSchema = z.object({ config: z.object({
502
+ content: z.array(z.string()).optional(),
503
+ theme: z.record(z.string(), z.any()).optional(),
504
+ plugins: z.array(z.string()).optional()
505
+ }).optional() });
506
+ /**
507
+ * `theme` holds Tailwind v4 `@theme` entries (`--color-*`, `--radius-*`, …);
508
+ * `light`/`dark` hold plain custom properties written into the matching
509
+ * selector. Both are emitted for every item so a single registry serves
510
+ * Tailwind v3 and v4 projects.
511
+ */
512
+ const registryItemCssVarsSchema = z.object({
513
+ theme: z.record(z.string(), z.string()).optional(),
514
+ light: z.record(z.string(), z.string()).optional(),
515
+ dark: z.record(z.string(), z.string()).optional()
516
+ });
517
+ /**
518
+ * Raw CSS keyed by at-rule or selector — `@layer base`, `@layer components`,
519
+ * `@utility …`, `@keyframes …`. Tailwind v4 consumers get these appended to
520
+ * their stylesheet; v3 consumers get the same rules wrapped by the CLI.
521
+ */
522
+ const registryItemCssSchema = z.record(z.string(), z.union([z.string(), z.record(z.string(), z.any())]));
523
+ const registryItemSchema = z.object({
524
+ name: z.string(),
525
+ type: registryItemTypeSchema,
526
+ framework: registryItemFrameworkSchema.optional(),
527
+ title: z.string().optional(),
528
+ description: z.string().optional(),
529
+ dependencies: z.array(z.string()).optional(),
530
+ devDependencies: z.array(z.string()).optional(),
531
+ registryDependencies: z.array(z.string()).optional(),
532
+ files: z.array(registryItemFileSchema).optional(),
533
+ tailwind: registryItemTailwindSchema.optional(),
534
+ cssVars: registryItemCssVarsSchema.optional(),
535
+ css: registryItemCssSchema.optional(),
536
+ meta: z.record(z.string(), z.any()).optional(),
537
+ docs: z.string().optional(),
538
+ categories: z.array(z.string()).optional()
539
+ });
540
+ const registrySchema = z.array(registryItemSchema);
541
+ const registryIndexSchema = z.array(registryItemSchema.extend({ files: z.array(z.string()).optional() }));
542
+
543
+ //#endregion
544
+ //#region src/tools/registry.ts
545
+ /**
546
+ * React and Vue components are served from their own registry, each item
547
+ * carrying a `framework` field naming which one it came from. The project's
548
+ * `components.json` picks the base url; `--registry` and `NEWT_REGISTRY_URL`
549
+ * override it for custom registries.
550
+ */
551
+ const DEFAULT_REGISTRY_URLS = {
552
+ react: "https://newtui.dev/r",
553
+ vue: "https://newtui.dev/vue/r"
554
+ };
555
+ function getRegistryUrl(framework, override) {
556
+ return (override ?? process.env.NEWT_REGISTRY_URL ?? DEFAULT_REGISTRY_URLS[framework]).replace(/\/+$/, "");
557
+ }
558
+ /** Fetch `url` as JSON and parse it against `schema`, the I/O boundary for all registry reads. */
559
+ async function fetchJson(url, schema) {
560
+ const response = await fetch(url);
561
+ if (!response.ok) throw new Error(`Failed to fetch ${url} (${response.status} ${response.statusText}).`);
562
+ const data = await response.json();
563
+ return schema.parse(data);
564
+ }
565
+ async function getRegistryIndex(registryUrl) {
566
+ return fetchJson(`${registryUrl}/index.json`, registryIndexSchema);
567
+ }
568
+ async function getRegistryItem(registryUrl, style, name) {
569
+ const url = /^https?:\/\//.test(name) ? name : `${registryUrl}/styles/${style}/${name}.json`;
570
+ try {
571
+ return await fetchJson(url, registryItemSchema);
572
+ } catch (error) {
573
+ if (error instanceof z.ZodError) throw new Error(`Invalid registry item "${name}": ${error.message}`, { cause: error });
574
+ const detail = error instanceof Error ? error.message : String(error);
575
+ throw new Error(`Component "${name}" not found in registry.\n${detail}`, { cause: error });
576
+ }
577
+ }
578
+ /**
579
+ * Resolve the full dependency tree for the given names (including transitive
580
+ * `registryDependencies`) and return items in install order (deps first).
581
+ */
582
+ async function resolveTree(registryUrl, style, names) {
583
+ const resolved = /* @__PURE__ */ new Map();
584
+ const visiting = /* @__PURE__ */ new Set();
585
+ async function visit(name) {
586
+ if (resolved.has(name) || visiting.has(name)) return;
587
+ visiting.add(name);
588
+ const item = await getRegistryItem(registryUrl, style, name);
589
+ for (const dep of item.registryDependencies ?? []) await visit(dep);
590
+ visiting.delete(name);
591
+ resolved.set(name, item);
592
+ }
593
+ for (const name of names) await visit(name);
594
+ return Array.from(resolved.values());
595
+ }
596
+
597
+ //#endregion
598
+ //#region src/tools/tailwind.ts
599
+ const TAILWIND_MAJORS = [3, 4];
600
+ /**
601
+ * Which Tailwind major the target project is on.
602
+ *
603
+ * The declared dependency wins, because it is the only unambiguous signal.
604
+ * Otherwise the stylesheet decides: v4 is CSS-first (`@import "tailwindcss"`)
605
+ * while v3 uses the `@tailwind` directives and a JS config. A project with a
606
+ * `tailwind.config.*` and nothing else is treated as v3.
607
+ */
608
+ async function detectTailwindMajor(cwd, cssPath) {
609
+ const pkg = await readFileIfExists(path.join(cwd, "package.json"));
610
+ if (pkg) {
611
+ const range = readTailwindRange(pkg);
612
+ const major = range ? majorFromRange(range) : void 0;
613
+ if (major) return major;
614
+ }
615
+ const css = await readFileIfExists(cssPath);
616
+ if (css) {
617
+ if (/@import\s+["']tailwindcss["']/.test(css)) return 4;
618
+ if (/@tailwind\s+(base|components|utilities)/.test(css)) return 3;
619
+ }
620
+ return findFirstExisting(cwd, TAILWIND_CONFIG_CANDIDATES) ? 3 : 4;
621
+ }
622
+ /**
623
+ * The `dependencies`/`devDependencies` maps we need out of `package.json`.
624
+ * Each key is left unvalidated (`unknown`) since we only ever look up a
625
+ * single known key (`tailwindcss`) afterwards; a malformed neighbouring
626
+ * dependency entry should not stop that lookup from succeeding.
627
+ */
628
+ const packageJsonSchema = z.object({
629
+ dependencies: z.record(z.string(), z.unknown()).optional(),
630
+ devDependencies: z.record(z.string(), z.unknown()).optional()
631
+ });
632
+ function readTailwindRange(packageJson) {
633
+ let raw;
634
+ try {
635
+ raw = JSON.parse(packageJson);
636
+ } catch {
637
+ return;
638
+ }
639
+ const parsed = packageJsonSchema.safeParse(raw);
640
+ if (!parsed.success) return void 0;
641
+ for (const field of ["dependencies", "devDependencies"]) {
642
+ const range = z.string().safeParse(parsed.data[field]?.tailwindcss);
643
+ if (range.success) return range.data;
644
+ }
645
+ }
646
+ function majorFromRange(range) {
647
+ const match = /(\d+)/.exec(range);
648
+ if (!match?.[1]) return void 0;
649
+ const major = Number.parseInt(match[1], 10);
650
+ return TAILWIND_MAJORS.find((candidate) => candidate === major);
651
+ }
652
+ /**
653
+ * The stylesheet preamble a fresh project needs, per major.
654
+ */
655
+ function tailwindPreamble(major) {
656
+ return major === 4 ? "@import \"tailwindcss\";\n" : "@tailwind base;\n@tailwind components;\n@tailwind utilities;\n";
657
+ }
658
+ /**
659
+ * Render an item's `cssVars` and `css` into a stylesheet block.
660
+ *
661
+ * On v4 the theme entries become an `@theme` block, which is what generates
662
+ * the `bg-newt-*` / `rounded-*` / `shadow-*` utilities — there is no JS config
663
+ * to extend. On v3 those same utilities come from the preset in
664
+ * `tailwind.config`, so only the raw custom properties are written here.
665
+ */
666
+ function renderItemCss(item, major) {
667
+ const parts = [];
668
+ const vars = item.cssVars;
669
+ if (vars?.theme && major === 4) parts.push(block("@theme", vars.theme));
670
+ if (vars?.light) parts.push(block(":root", vars.light));
671
+ if (vars?.dark) {
672
+ parts.push(block(":root", vars.dark));
673
+ parts.push(block(".dark", vars.dark));
674
+ }
675
+ if (item.css) for (const [selector, body] of Object.entries(item.css)) if (body instanceof Object) parts.push(block(selector, body));
676
+ else parts.push(`${selector} {\n${indent(body)}\n}`);
677
+ return parts.length > 0 ? `${parts.join("\n\n")}\n` : "";
678
+ }
679
+ function block(selector, entries) {
680
+ return `${selector} {\n${Object.entries(entries).map(([key, value]) => ` ${cssProperty(key)}: ${String(value)};`).join("\n")}\n}`;
681
+ }
682
+ /** `newt-brand` and `--newt-brand` both mean the same custom property. */
683
+ function cssProperty(key) {
684
+ return key.startsWith("--") ? key : `--${key}`;
685
+ }
686
+ function indent(value) {
687
+ return value.split("\n").map((line) => line.length > 0 ? ` ${line}` : line).join("\n");
688
+ }
689
+
690
+ //#endregion
691
+ //#region src/tools/transformers.ts
692
+ function normalizeFile(file, fallbackType) {
693
+ if (!(file instanceof Object)) return null;
694
+ if (file.content === void 0) return null;
695
+ return {
696
+ path: file.path,
697
+ content: file.content,
698
+ type: file.type ?? fallbackType,
699
+ target: file.target
700
+ };
701
+ }
702
+ /**
703
+ * Rewrite registry-internal import paths to the user's configured aliases.
704
+ * The two registries root their components differently:
705
+ * React: `@/registry/default/ui/button` -> `@/components/ui/button`
706
+ * Vue: `@/lib/registry/default/ui/button` -> `@/components/ui/button`
707
+ * `@/lib/utils` maps to `<aliases.utils>` in both.
708
+ */
709
+ function transformImports(content, config) {
710
+ const uiAlias = config.aliases.ui ?? `${config.aliases.components}/ui`;
711
+ const libAlias = config.aliases.lib ?? "@/lib";
712
+ const hooksAlias = config.framework === "vue" ? config.aliases.composables ?? "@/composables" : config.aliases.hooks ?? "@/hooks";
713
+ const hooksSegment = config.framework === "vue" ? "composables" : "hooks";
714
+ const escapedRoot = (config.framework === "vue" ? "@/lib/registry" : "@/registry").replace(/[/\\]/g, "\\$&");
715
+ let out = content;
716
+ out = out.replace(new RegExp(`${escapedRoot}/[^/"']+/ui/`, "g"), `${uiAlias}/`);
717
+ out = out.replace(new RegExp(`${escapedRoot}/[^/"']+/${hooksSegment}/`, "g"), `${hooksAlias}/`);
718
+ out = out.replace(new RegExp(`${escapedRoot}/[^/"']+/lib/`, "g"), `${libAlias}/`);
719
+ out = out.replace(new RegExp(`${escapedRoot}/[^/"']+/`, "g"), `${config.aliases.components}/`);
720
+ out = out.replace(/(["'])@\/lib\/utils\1/g, `$1${config.aliases.utils}$1`);
721
+ return out;
722
+ }
723
+ /**
724
+ * Strip `"use client"` directives when a React project is not using React
725
+ * Server Components.
726
+ */
727
+ function transformRsc(content, config) {
728
+ if (config.framework !== "react" || (config.rsc ?? true)) return content;
729
+ return content.replace(/^["']use client["'];?\s*\n/m, "");
730
+ }
731
+ /**
732
+ * Drop `lang="ts"` from Vue SFC blocks when the project is not using
733
+ * TypeScript. Type annotations are not stripped (a full TS transform is out of
734
+ * scope).
735
+ */
736
+ function transformTypeScript(content, config) {
737
+ if (config.framework !== "vue" || config.typescript) return content;
738
+ return content.replace(/<script([^>]*)\slang="ts"/g, "<script$1");
739
+ }
740
+ /**
741
+ * Convert `.tsx`/`.ts` sources to `.jsx`/`.js` file names when the project is
742
+ * not using TypeScript. `.vue` single-file components keep their extension.
743
+ */
744
+ function transformFileName(filePath, config) {
745
+ if (config.typescript) return filePath;
746
+ return filePath.replace(/\.tsx$/, ".jsx").replace(/\.ts$/, ".js");
747
+ }
748
+ function transformContent(content, config) {
749
+ return transformTypeScript(transformRsc(transformImports(content, config), config), config);
750
+ }
751
+ /**
752
+ * Vue registry items are directories: `ui/button/Button.vue`, `ui/button/index.ts`.
753
+ * Keep everything below the registry root segment (`ui/`, `lib/`, ...) so the
754
+ * component directory structure is preserved inside the user's project. React
755
+ * items are single files, so they flatten to their base name.
756
+ */
757
+ function relativeToRoot(filePath, root, config) {
758
+ if (config.framework !== "vue") return path.basename(filePath);
759
+ const normalized = filePath.replace(/^\.?\//, "");
760
+ const prefix = `${root}/`;
761
+ const index = normalized.indexOf(prefix);
762
+ const rest = index === -1 ? normalized : normalized.slice(index + prefix.length);
763
+ return rest.length > 0 ? rest : path.basename(normalized);
764
+ }
765
+ /**
766
+ * Resolve where a registry file should be written based on its type and the user's aliases.
767
+ */
768
+ function resolveTargetPath(file, item, config) {
769
+ if (file.target) return path.resolve(config.resolvedPaths.cwd, file.target);
770
+ switch (file.type ?? item.type) {
771
+ case "registry:ui": return path.resolve(config.resolvedPaths.ui, transformFileName(relativeToRoot(file.path, "ui", config), config));
772
+ case "registry:lib": return path.resolve(config.resolvedPaths.lib, transformFileName(relativeToRoot(file.path, "lib", config), config));
773
+ case "registry:hook": return config.framework === "vue" ? path.resolve(config.resolvedPaths.composables, transformFileName(relativeToRoot(file.path, "composables", config), config)) : path.resolve(config.resolvedPaths.hooks, transformFileName(path.basename(file.path), config));
774
+ case "registry:block":
775
+ case "registry:component":
776
+ case "registry:example": return path.resolve(config.resolvedPaths.components, transformFileName(path.basename(file.path), config));
777
+ case "registry:page":
778
+ case "registry:file": return path.resolve(config.resolvedPaths.cwd, transformFileName(file.path, config));
779
+ default: return path.resolve(config.resolvedPaths.components, transformFileName(path.basename(file.path), config));
780
+ }
781
+ }
782
+
783
+ //#endregion
784
+ //#region src/commands/add.ts
785
+ async function add(options) {
786
+ const cwd = resolveCwd(options.cwd);
787
+ const config = await getConfig(cwd);
788
+ if (!config) throw new Error(`Configuration is missing. Please run ${highlighter.info("npx newtui init")} to create a components.json file.`);
789
+ intro(highlighter.bold("newt/ui — add"));
790
+ const registryUrl = getRegistryUrl(config.framework, options.registry ?? config.registry);
791
+ let selected = options.components;
792
+ if (options.all || selected.length === 0) {
793
+ const uiItems = (await getRegistryIndex(registryUrl)).filter((item) => item.type === "registry:ui");
794
+ selected = options.all ? uiItems.map((item) => item.name) : await promptMultiselect("Which components would you like to add?", uiItems.map((item) => ({
795
+ value: item.name,
796
+ label: item.title ?? item.name
797
+ })));
798
+ }
799
+ if (selected.length === 0) {
800
+ logger.warn("No components selected. Exiting.");
801
+ process.exit(0);
802
+ }
803
+ await runAdd(config, registryUrl, selected, options);
804
+ }
805
+ async function runAdd(config, registryUrl, names, options) {
806
+ const resolveSpinner = spinner();
807
+ resolveSpinner.start("Resolving registry dependencies...");
808
+ let tree;
809
+ try {
810
+ tree = await resolveTree(registryUrl, config.style, names);
811
+ } catch (error) {
812
+ resolveSpinner.stop("Failed to resolve components.");
813
+ throw error;
814
+ }
815
+ resolveSpinner.stop(`Resolved ${tree.length} item(s): ${tree.map((item) => item.name).join(", ")}`);
816
+ if (!options.yes) {
817
+ if (!await promptConfirm("Ready to install components and dependencies. Proceed?")) process.exit(0);
818
+ }
819
+ const dependencies = /* @__PURE__ */ new Set();
820
+ const devDependencies = /* @__PURE__ */ new Set();
821
+ const written = [];
822
+ const skipped = [];
823
+ for (const item of tree) {
824
+ for (const dep of item.dependencies ?? []) dependencies.add(dep);
825
+ for (const dep of item.devDependencies ?? []) devDependencies.add(dep);
826
+ for (const rawFile of item.files ?? []) {
827
+ const file = normalizeFile(rawFile, item.type);
828
+ if (!file) continue;
829
+ let target = resolveTargetPath(file, item, config);
830
+ if (options.path && !file.target) {
831
+ const relative = relativePath(config.resolvedPaths.ui, target);
832
+ const inUiDir = !relative.startsWith("..") && !path.isAbsolute(relative);
833
+ target = path.resolve(config.resolvedPaths.cwd, options.path, inUiDir ? relative : path.basename(target));
834
+ }
835
+ if (pathExists(target) && !options.overwrite) {
836
+ const relative = relativePath(config.resolvedPaths.cwd, target);
837
+ if (options.yes) {
838
+ skipped.push(relative);
839
+ continue;
840
+ }
841
+ if (!await promptConfirm(`File ${highlighter.info(relative)} already exists. Overwrite?`, false)) {
842
+ skipped.push(relative);
843
+ continue;
844
+ }
845
+ }
846
+ await writeFileAt(target, transformContent(file.content, config));
847
+ written.push(relativePath(config.resolvedPaths.cwd, target));
848
+ }
849
+ }
850
+ await applyItemStyles(tree, config);
851
+ for (const file of written) logger.success(`+ ${file}`);
852
+ for (const file of skipped) logger.warn(`~ ${file} (skipped, use --overwrite to replace)`);
853
+ if (!options.skipInstall) {
854
+ if (dependencies.size > 0) {
855
+ logger.info(`Installing dependencies: ${Array.from(dependencies).join(", ")}`);
856
+ await installDependencies(config.resolvedPaths.cwd, Array.from(dependencies));
857
+ }
858
+ if (devDependencies.size > 0) {
859
+ logger.info(`Installing devDependencies: ${Array.from(devDependencies).join(", ")}`);
860
+ await installDependencies(config.resolvedPaths.cwd, Array.from(devDependencies), { dev: true });
861
+ }
862
+ } else if (dependencies.size > 0 || devDependencies.size > 0) logger.warn(`Skipped install. Dependencies: ${[...dependencies, ...devDependencies].join(", ")}`);
863
+ const uiAlias = config.aliases.ui ?? `${config.aliases.components}/ui`;
864
+ logger.log(tree.map((item) => highlighter.dim(`import { ... } from "${uiAlias}/${item.name}"`)).join("\n"));
865
+ outro("Done.");
866
+ }
867
+ /**
868
+ * Append any `cssVars` / `css` an item carries to the project stylesheet,
869
+ * rendered for whichever Tailwind major the project is on. Most components
870
+ * need nothing here — they are styled by the tokens `init` wrote.
871
+ */
872
+ async function applyItemStyles(items, config) {
873
+ const styled = items.filter((item) => item.cssVars ?? item.css);
874
+ if (styled.length === 0) return;
875
+ const cssPath = config.resolvedPaths.tailwindCss;
876
+ const existing = await readFileIfExists(cssPath) ?? "";
877
+ const major = await detectTailwindMajor(config.resolvedPaths.cwd, cssPath);
878
+ const additions = [];
879
+ for (const item of styled) {
880
+ const rendered = renderItemCss(item, major);
881
+ if (rendered.length === 0) continue;
882
+ if (existing.includes(rendered.trim())) continue;
883
+ additions.push(`/* newt/ui — ${item.name} */\n${rendered}`);
884
+ }
885
+ if (additions.length === 0) return;
886
+ await writeFileAt(cssPath, `${existing.replace(/\s*$/, "\n")}\n${additions.join("\n")}`);
887
+ logger.success(`+ ${relativePath(config.resolvedPaths.cwd, cssPath)} (styles)`);
888
+ }
889
+
890
+ //#endregion
891
+ //#region src/commands/diff.ts
892
+ async function diff(options) {
893
+ const cwd = resolveCwd(options.cwd);
894
+ const config = await getConfig(cwd);
895
+ if (!config) throw new Error(`Configuration is missing. Please run ${highlighter.info("npx newtui init")} first.`);
896
+ if (!options.component) throw new Error("Please specify a component name, e.g. `newtui diff button`.");
897
+ intro(highlighter.bold("newt/ui — diff"));
898
+ const registryUrl = getRegistryUrl(config.framework, options.registry ?? config.registry);
899
+ const item = await getRegistryItem(registryUrl, config.style, options.component);
900
+ let changed = 0;
901
+ for (const rawFile of item.files ?? []) {
902
+ const file = normalizeFile(rawFile, item.type);
903
+ if (!file) continue;
904
+ const target = resolveTargetPath(file, item, config);
905
+ const relative = relativePath(cwd, target);
906
+ if (!pathExists(target)) {
907
+ logger.warn(`${relative}: not installed`);
908
+ changed++;
909
+ continue;
910
+ }
911
+ const local = await readTextFile(target);
912
+ const remote = transformContent(file.content, config);
913
+ if (local === remote) {
914
+ logger.success(`${relative}: up to date`);
915
+ continue;
916
+ }
917
+ changed++;
918
+ logger.info(`${relative}: differs`);
919
+ printLineDiff(local, remote);
920
+ }
921
+ if (changed === 0) {
922
+ outro(highlighter.success("No updates found."));
923
+ return;
924
+ }
925
+ outro(highlighter.dim(`Run ${highlighter.info(`npx newtui add ${item.name} --overwrite`)} to update.`));
926
+ }
927
+ function printLineDiff(local, remote) {
928
+ const a = local.split("\n");
929
+ const b = remote.split("\n");
930
+ const max = Math.max(a.length, b.length);
931
+ const lines = [];
932
+ let shown = 0;
933
+ for (let i = 0; i < max && shown < 40; i++) {
934
+ const left = a[i];
935
+ const right = b[i];
936
+ if (left === right) continue;
937
+ if (left !== void 0) lines.push(highlighter.error(`- ${left}`));
938
+ if (right !== void 0) lines.push(highlighter.success(`+ ${right}`));
939
+ shown++;
940
+ }
941
+ if (shown >= 40) lines.push(highlighter.dim("... (truncated)"));
942
+ if (lines.length > 0) logger.log(lines.join("\n"));
943
+ }
944
+
945
+ //#endregion
946
+ //#region src/tools/tokens.ts
947
+ /**
948
+ * Locate `registry/html/tokens.css` shipped with this package.
949
+ * Works both from `dist/index.js` (published) and `src/tools/tokens.ts` (dev).
950
+ */
951
+ function getTokensPath() {
952
+ const here = path.dirname(fileURLToPath(import.meta.url));
953
+ const candidates = [path.resolve(here, "../registry/html/tokens.css"), path.resolve(here, "../../registry/html/tokens.css")];
954
+ for (const candidate of candidates) if (pathExists(candidate)) return candidate;
955
+ throw new Error("Could not locate registry/html/tokens.css in the newt-ui package.");
956
+ }
957
+ /**
958
+ * Extract the `:root { --newt-* }` block from tokens.css, wrapped for Tailwind `@layer base`.
959
+ */
960
+ async function getTokensCssBlock() {
961
+ const raw = await readTextFile(getTokensPath());
962
+ const match = raw.match(/:root\s*\{[\s\S]*?\n\}/);
963
+ return [
964
+ "",
965
+ "/* newt/ui design tokens — source of truth: newt-ui/registry/html/tokens.css */",
966
+ "@layer base {",
967
+ (match ? match[0] : raw).split("\n").map((line) => line.length > 0 ? ` ${line}` : line).join("\n"),
968
+ "",
969
+ " .newt-root {",
970
+ " font-family: var(--newt-font-sans);",
971
+ " color: var(--newt-text-primary);",
972
+ " -webkit-font-smoothing: antialiased;",
973
+ " }",
974
+ "}",
975
+ ""
976
+ ].join("\n");
977
+ }
978
+ const TOKENS_MARKER = "--newt-bg-base";
979
+
980
+ //#endregion
981
+ //#region src/commands/init.ts
982
+ /** The registry item that carries the design tokens. */
983
+ const THEME_ITEM = "theme-newt";
984
+ const UTILS_SOURCE_V4 = `export { cn } from "cn"
985
+ `;
986
+ const UTILS_SOURCE_V3 = `import { type ClassValue, clsx } from "clsx"
987
+ import { twMerge } from "tailwind-merge"
988
+
989
+ export function cn(...inputs: ClassValue[]) {
990
+ return twMerge(clsx(inputs))
991
+ }
992
+ `;
993
+ async function init(options) {
994
+ const cwd = resolveCwd(options.cwd);
995
+ intro(highlighter.bold("newt/ui — init"));
996
+ await runInit(cwd, await promptForConfig(cwd, options), options);
997
+ outro(`Success! Project initialization completed. You may now add components: ${highlighter.info("npx newtui add button")}`);
998
+ }
999
+ /**
1000
+ * Decide whether this is a React or a Vue project from the config files it
1001
+ * has. A Nuxt or Vite config alone is not enough (Vite builds React apps too),
1002
+ * so a Next.js config wins first and the bundler check only runs afterwards.
1003
+ */
1004
+ async function detectFramework(cwd) {
1005
+ if (findFirstExisting(cwd, NEXT_CONFIG_CANDIDATES)) return "react";
1006
+ if (findFirstExisting(cwd, NUXT_CONFIG_CANDIDATES)) return "vue";
1007
+ const packageJson = await readFileIfExists(path.resolve(cwd, "package.json"));
1008
+ if (packageJson !== null) {
1009
+ if (/"(vue|nuxt)"\s*:/.test(packageJson)) return "vue";
1010
+ if (/"(react|next)"\s*:/.test(packageJson)) return "react";
1011
+ }
1012
+ if (findFirstExisting(cwd, VITE_CONFIG_CANDIDATES)) return "vue";
1013
+ return DEFAULT_FRAMEWORK;
1014
+ }
1015
+ /** Vue only — Nuxt and Vite need different stylesheet paths and setup steps. */
1016
+ function detectBundler(cwd) {
1017
+ if (findFirstExisting(cwd, NUXT_CONFIG_CANDIDATES)) return "nuxt";
1018
+ if (findFirstExisting(cwd, VITE_CONFIG_CANDIDATES)) return "vite";
1019
+ return DEFAULT_BUNDLER;
1020
+ }
1021
+ function detectCssPath(cwd, framework, bundler, override) {
1022
+ const candidates = framework === "vue" ? CSS_CANDIDATES[bundler ?? "nuxt"] : CSS_CANDIDATES.react;
1023
+ return override ?? findFirstExisting(cwd, candidates) ?? DEFAULT_TAILWIND_CSS[framework];
1024
+ }
1025
+ function detectTailwindConfig(cwd) {
1026
+ return findFirstExisting(cwd, TAILWIND_CONFIG_CANDIDATES) ?? "tailwind.config.ts";
1027
+ }
1028
+ /**
1029
+ * Build the `components.json` defaults for a framework. Each framework only
1030
+ * carries the aliases its registry actually references.
1031
+ */
1032
+ function buildDefaults(cwd, framework, bundler, options) {
1033
+ const isVue = framework === "vue";
1034
+ return rawConfigSchema.parse({
1035
+ $schema: "https://newtui.dev/schema.json",
1036
+ style: "default",
1037
+ framework,
1038
+ bundler: isVue ? bundler ?? "nuxt" : void 0,
1039
+ typescript: true,
1040
+ rsc: isVue ? void 0 : true,
1041
+ tailwind: {
1042
+ config: detectTailwindConfig(cwd),
1043
+ css: detectCssPath(cwd, framework, bundler, options.css),
1044
+ baseColor: "neutral",
1045
+ cssVariables: true,
1046
+ prefix: ""
1047
+ },
1048
+ aliases: {
1049
+ components: DEFAULT_COMPONENTS,
1050
+ utils: DEFAULT_UTILS,
1051
+ ui: DEFAULT_UI,
1052
+ lib: DEFAULT_LIB,
1053
+ hooks: isVue ? void 0 : DEFAULT_HOOKS,
1054
+ composables: isVue ? DEFAULT_COMPOSABLES : void 0
1055
+ },
1056
+ iconLibrary: "lucide"
1057
+ });
1058
+ }
1059
+ async function promptForConfig(cwd, options) {
1060
+ const detectedFramework = options.framework ?? await detectFramework(cwd);
1061
+ const detectedBundler = detectedFramework === "vue" ? options.bundler ?? detectBundler(cwd) : void 0;
1062
+ const defaults = buildDefaults(cwd, detectedFramework, detectedBundler, options);
1063
+ if (options.defaults || options.yes) return defaults;
1064
+ const framework = frameworkSchema.parse(await promptSelect(`Which ${highlighter.info("framework")} are you using?`, [{
1065
+ value: "react",
1066
+ label: "React"
1067
+ }, {
1068
+ value: "vue",
1069
+ label: "Vue"
1070
+ }], defaults.framework));
1071
+ const bundler = framework === "vue" ? bundlerSchema.parse(await promptSelect(`Which ${highlighter.info("build tool")} are you using?`, [{
1072
+ value: "nuxt",
1073
+ label: "Nuxt"
1074
+ }, {
1075
+ value: "vite",
1076
+ label: "Vite"
1077
+ }], detectedBundler ?? "nuxt")) : void 0;
1078
+ const frameworkDefaults = framework === defaults.framework ? defaults : buildDefaults(cwd, framework, bundler, options);
1079
+ const typescript = await promptConfirm(`Would you like to use ${highlighter.info("TypeScript")} (recommended)?`);
1080
+ const rsc = framework === "react" ? await promptConfirm(`Are you using ${highlighter.info("React Server Components")}?`) : void 0;
1081
+ const tailwindConfig = await promptText(`Where is your ${highlighter.info("tailwind.config")} located?`, frameworkDefaults.tailwind.config);
1082
+ const tailwindCss = await promptText(`Where is your ${highlighter.info("global CSS")} file?`, frameworkDefaults.tailwind.css);
1083
+ const components = await promptText(`Configure the import alias for ${highlighter.info("components")}:`, frameworkDefaults.aliases.components);
1084
+ const utils = await promptText(`Configure the import alias for ${highlighter.info("utils")}:`, frameworkDefaults.aliases.utils);
1085
+ const config = rawConfigSchema.parse({
1086
+ ...frameworkDefaults,
1087
+ framework,
1088
+ bundler,
1089
+ typescript,
1090
+ rsc,
1091
+ tailwind: {
1092
+ ...frameworkDefaults.tailwind,
1093
+ config: tailwindConfig,
1094
+ css: tailwindCss
1095
+ },
1096
+ aliases: {
1097
+ ...frameworkDefaults.aliases,
1098
+ components,
1099
+ utils,
1100
+ ui: `${components}/ui`
1101
+ }
1102
+ });
1103
+ if (!await promptConfirm(`Write configuration to ${highlighter.info("components.json")}. Proceed?`)) process.exit(0);
1104
+ return config;
1105
+ }
1106
+ async function runInit(cwd, rawConfig, options) {
1107
+ const configSpinner = spinner();
1108
+ configSpinner.start("Writing components.json...");
1109
+ const configPath = path.resolve(cwd, CONFIG_FILE_NAME);
1110
+ await writeFileAt(configPath, JSON.stringify(rawConfig, null, 2) + "\n");
1111
+ configSpinner.stop(`Wrote ${highlighter.info(CONFIG_FILE_NAME)}.`);
1112
+ const config = await resolveConfigPaths(cwd, rawConfig);
1113
+ const cssPath = config.resolvedPaths.tailwindCss;
1114
+ const major = await detectTailwindMajor(cwd, cssPath);
1115
+ const utilsSpinner = spinner();
1116
+ utilsSpinner.start("Writing lib/utils...");
1117
+ const utilsExt = config.typescript ? "ts" : "js";
1118
+ const utilsPath = `${config.resolvedPaths.utils}.${utilsExt}`;
1119
+ if (pathExists(utilsPath)) utilsSpinner.stop(`${relativePath(cwd, utilsPath)} already exists, skipped.`);
1120
+ else {
1121
+ const source = major === 4 ? UTILS_SOURCE_V4 : config.typescript ? UTILS_SOURCE_V3 : UTILS_SOURCE_V3.replace("type ClassValue, ", "").replace(": ClassValue[]", "");
1122
+ await writeFileAt(utilsPath, source);
1123
+ utilsSpinner.stop(`Wrote ${highlighter.info(relativePath(cwd, utilsPath))}.`);
1124
+ }
1125
+ const cssSpinner = spinner();
1126
+ cssSpinner.start("Adding newt/ui design tokens to CSS...");
1127
+ const existingCss = await readFileIfExists(cssPath) ?? "";
1128
+ const theme = await fetchTheme(config.framework, options.registry, config.style);
1129
+ if (existingCss.includes("--newt-bg-base")) cssSpinner.stop(`${relativePath(cwd, cssPath)} already contains newt tokens, skipped.`);
1130
+ else {
1131
+ const header = existingCss.length === 0 ? tailwindPreamble(major) : existingCss;
1132
+ await writeFileAt(cssPath, header.replace(/\s*$/, "\n") + await tokensCssBlock(theme, major));
1133
+ cssSpinner.stop(`Added tokens to ${highlighter.info(relativePath(cwd, cssPath))} (Tailwind v${major}).`);
1134
+ }
1135
+ if (!options.skipInstall) {
1136
+ await installDependencies(cwd, major === 4 ? [...PROJECT_DEPENDENCIES_V4] : [...PROJECT_DEPENDENCIES_V3]);
1137
+ logger.success("Installed dependencies.");
1138
+ }
1139
+ printTailwindInstructions(config.framework, config.tailwind.config, major, theme);
1140
+ if (config.framework === "vue") {
1141
+ if ((config.bundler ?? "nuxt") === "nuxt") printNuxtInstructions(config.tailwind.css);
1142
+ else printViteInstructions(config.tailwind.css);
1143
+ }
1144
+ }
1145
+ /**
1146
+ * The theme item carries the tokens in every shape the two Tailwind majors
1147
+ * need. It is fetched rather than derived so the values stay defined once, in
1148
+ * the registry. An unreachable registry is not fatal: the tokens bundled with
1149
+ * this package still cover the raw `--newt-*` variables, which is what a v3
1150
+ * project needs, and a v4 project is told what it is missing.
1151
+ */
1152
+ async function fetchTheme(framework, registry, style) {
1153
+ try {
1154
+ return await getRegistryItem(getRegistryUrl(framework, registry), style, THEME_ITEM);
1155
+ } catch {
1156
+ return null;
1157
+ }
1158
+ }
1159
+ async function tokensCssBlock(theme, major) {
1160
+ const rendered = theme ? renderItemCss(theme, major) : "";
1161
+ if (rendered.length > 0) return [
1162
+ "",
1163
+ "/* newt/ui design tokens — source: the newt/ui registry theme item */",
1164
+ rendered
1165
+ ].join("\n");
1166
+ return getTokensCssBlock();
1167
+ }
1168
+ function printTailwindInstructions(framework, tailwindConfig, major, theme) {
1169
+ logger.log(highlighter.bold("Tailwind setup"));
1170
+ if (major === 4) {
1171
+ logger.log(`Nothing else to configure — Tailwind v4 reads the ${highlighter.info("@theme")} block just written to your stylesheet, which is what generates ${highlighter.info("bg-newt-bg-elevated")}, ${highlighter.info("rounded-md")} and the rest.`);
1172
+ if (!theme) logger.warn(`The registry was unreachable, so only the raw ${highlighter.info("--newt-*")} variables were written. Re-run ${highlighter.info("newtui init")} when you are online to add the @theme block.`);
1173
+ return;
1174
+ }
1175
+ const extend = theme?.tailwind?.config?.theme?.extend;
1176
+ if (!extend) {
1177
+ logger.warn(`The registry was unreachable, so the v3 preset could not be printed. Re-run ${highlighter.info("newtui init")} when you are online.`);
1178
+ return;
1179
+ }
1180
+ const content = framework === "vue" ? `["./src/**/*.{vue,ts}"]` : `["./app/**/*.{ts,tsx}", "./components/**/*.{ts,tsx}"]`;
1181
+ logger.log(`Add the newt/ui preset to ${highlighter.info(tailwindConfig)} so utilities like ${highlighter.info("bg-newt-bg-elevated")} and ${highlighter.info("text-newt-brand")} map to the --newt-* tokens:`);
1182
+ logger.log(highlighter.dim(` import type { Config } from "tailwindcss"
1183
+
1184
+ const newtPreset = {
1185
+ darkMode: ["class"],
1186
+ theme: { extend: ${JSON.stringify(extend, null, 2).split("\n").join("\n ")} },
1187
+ } satisfies Omit<Config, "content">
1188
+
1189
+ export default {
1190
+ presets: [newtPreset],
1191
+ content: ${content},
1192
+ plugins: [require("tailwindcss-animate")],
1193
+ } satisfies Config`));
1194
+ }
1195
+ function printNuxtInstructions(css) {
1196
+ logger.log(highlighter.bold("Nuxt setup"));
1197
+ logger.log(`Make sure ${highlighter.info("@nuxtjs/tailwindcss")} is registered and your CSS entry is loaded in ${highlighter.info("nuxt.config.ts")}:`);
1198
+ logger.log(highlighter.dim(` export default defineNuxtConfig({
1199
+ modules: ["@nuxtjs/tailwindcss"],
1200
+ css: ["~/${css.replace(/^src\//, "")}"],
1201
+ })`));
1202
+ logger.log(highlighter.dim(" Tip: @newtui/nuxt auto-imports every component in your ui directory — see https://newtui.dev"));
1203
+ }
1204
+ function printViteInstructions(css) {
1205
+ logger.log(highlighter.bold("Vite setup"));
1206
+ logger.log(`Import ${highlighter.info(css)} from your app entry (usually ${highlighter.info("src/main.ts")}).`);
1207
+ }
1208
+
1209
+ //#endregion
1210
+ //#region src/commands/list.ts
1211
+ async function list(options) {
1212
+ const cwd = resolveCwd(options.cwd);
1213
+ const config = await getRawConfig(cwd).catch(() => null);
1214
+ const framework = options.framework ?? config?.framework ?? "react";
1215
+ const registryUrl = getRegistryUrl(framework, options.registry ?? config?.registry);
1216
+ const index = await getRegistryIndex(registryUrl);
1217
+ const filter = options.type ?? "registry:ui";
1218
+ const items = index.filter((item) => filter === "all" || item.type === filter);
1219
+ if (options.json) {
1220
+ console.log(JSON.stringify(items, null, 2));
1221
+ return;
1222
+ }
1223
+ intro(highlighter.bold(`Registry: ${registryUrl} (${framework})`));
1224
+ for (const item of items) {
1225
+ const title = item.title ?? item.name;
1226
+ const description = item.description ? highlighter.dim(` — ${item.description}`) : "";
1227
+ logger.log(`${highlighter.info(item.name.padEnd(22))} ${title}${description}`);
1228
+ }
1229
+ outro(highlighter.dim(`${items.length} item(s). Add with: npx newtui add <name>`));
1230
+ }
1231
+
1232
+ //#endregion
1233
+ //#region src/index.ts
1234
+ process.on("SIGINT", () => process.exit(0));
1235
+ process.on("SIGTERM", () => process.exit(0));
1236
+ function getPackageInfo() {
1237
+ const require = createRequire(import.meta.url);
1238
+ const here = path.dirname(fileURLToPath(import.meta.url));
1239
+ for (const candidate of ["../package.json", "../../package.json"]) try {
1240
+ return require(path.resolve(here, candidate));
1241
+ } catch {}
1242
+ return {
1243
+ name: "newtui",
1244
+ version: "0.0.0"
1245
+ };
1246
+ }
1247
+ /**
1248
+ * `newtui --legacy <args>` delegates to the HTML/CSS CLI (cli/index.js),
1249
+ * which is also exposed as the `newtui-html` bin.
1250
+ */
1251
+ function runLegacy(args) {
1252
+ const here = path.dirname(fileURLToPath(import.meta.url));
1253
+ const legacy = [path.resolve(here, "../cli/index.js"), path.resolve(here, "../../cli/index.js")].find((candidate) => {
1254
+ try {
1255
+ createRequire(import.meta.url).resolve(candidate);
1256
+ return true;
1257
+ } catch {
1258
+ return false;
1259
+ }
1260
+ });
1261
+ if (!legacy) {
1262
+ console.error("Legacy HTML/CSS CLI (cli/index.js) not found in package.");
1263
+ process.exit(1);
1264
+ }
1265
+ spawn(process.execPath, [legacy, ...args], { stdio: "inherit" }).on("exit", (code) => process.exit(code ?? 0));
1266
+ }
1267
+ function printHelp() {
1268
+ const { version } = getPackageInfo();
1269
+ console.log(`
1270
+ ${highlighter.bold("newtui")} ${highlighter.dim(`v${version}`)}
1271
+ Add Discord-inspired newt/ui components to your React, Vue or Nuxt project.
1272
+
1273
+ ${highlighter.bold("Usage")}
1274
+ $ newtui <command> [options]
1275
+
1276
+ ${highlighter.bold("Commands")}
1277
+ init initialize your project and install dependencies
1278
+ add [components...] add components to your project
1279
+ list list available components in the registry
1280
+ diff <component> check an installed component against the registry
1281
+
1282
+ ${highlighter.bold("Options")}
1283
+ -c, --cwd <dir> working directory (default: current directory)
1284
+ -r, --registry <url> registry base url (overrides NEWT_REGISTRY_URL)
1285
+ -y, --yes skip confirmation prompts
1286
+ -d, --defaults use the default configuration (init)
1287
+ -f, --framework <name> the framework to use: react or vue (init, list)
1288
+ -b, --bundler <name> the Vue build tool: nuxt or vite (init)
1289
+ --css <path> path to your global css file (init)
1290
+ --skip-install skip installing dependencies (init, add)
1291
+ -o, --overwrite overwrite existing files (add)
1292
+ -a, --all add every available component (add)
1293
+ -p, --path <path> the path to add the component to (add)
1294
+ -t, --type <type> filter by registry item type (list)
1295
+ --json output as JSON (list)
1296
+ --legacy use the legacy HTML/CSS CLI (same as newtui-html)
1297
+ -h, --help display this message
1298
+ -v, --version display the version number
1299
+
1300
+ ${highlighter.bold("Examples")}
1301
+ $ newtui init --defaults
1302
+ $ newtui add button avatar
1303
+ $ newtui list --framework vue
1304
+ $ newtui diff button
1305
+ `);
1306
+ }
1307
+ /**
1308
+ * `--framework` used to name the Vue build tool (`nuxt`/`vite`). It now names
1309
+ * the UI framework, so those two values are forwarded to `--bundler` instead
1310
+ * of being rejected.
1311
+ */
1312
+ function readFrameworkFlags(args) {
1313
+ const rawFramework = flagString(args.framework);
1314
+ const rawBundler = flagString(args.bundler);
1315
+ if (rawBundler !== void 0 && !isBundlerName(rawBundler)) throw new Error(`Unknown bundler "${rawBundler}". Expected "nuxt" or "vite".`);
1316
+ if (rawFramework === void 0) return { bundler: rawBundler };
1317
+ if (isFrameworkName(rawFramework)) return {
1318
+ framework: rawFramework,
1319
+ bundler: rawBundler
1320
+ };
1321
+ if (isBundlerName(rawFramework)) {
1322
+ logger.warn(`--framework ${rawFramework} now means the Vue build tool; use --bundler ${rawFramework} instead.`);
1323
+ return {
1324
+ framework: "vue",
1325
+ bundler: rawBundler ?? rawFramework
1326
+ };
1327
+ }
1328
+ throw new Error(`Unknown framework "${rawFramework}". Expected "react" or "vue".`);
1329
+ }
1330
+ async function main() {
1331
+ const argv = process.argv.slice(2);
1332
+ const legacyIndex = argv.indexOf("--legacy");
1333
+ if (legacyIndex !== -1) {
1334
+ runLegacy(argv.filter((_, index) => index !== legacyIndex));
1335
+ return;
1336
+ }
1337
+ const args = mri(argv, {
1338
+ alias: { ...FLAG_ALIASES },
1339
+ boolean: [...BOOLEAN_FLAGS],
1340
+ string: [...STRING_FLAGS]
1341
+ });
1342
+ const [command, ...rest] = args._;
1343
+ if (flagBoolean(args.version)) {
1344
+ console.log(getPackageInfo().version);
1345
+ return;
1346
+ }
1347
+ if (!command || flagBoolean(args.help)) {
1348
+ printHelp();
1349
+ return;
1350
+ }
1351
+ if (!isCommandName(command)) {
1352
+ logger.error(`Unknown command "${command}".`);
1353
+ printHelp();
1354
+ process.exit(1);
1355
+ }
1356
+ const cwd = flagString(args.cwd) ?? process.cwd();
1357
+ const registry = flagString(args.registry);
1358
+ const yes = flagBoolean(args.yes);
1359
+ const skipInstall = flagBoolean(args["skip-install"]);
1360
+ switch (command) {
1361
+ case "init": {
1362
+ const { framework, bundler } = readFrameworkFlags(args);
1363
+ await init({
1364
+ cwd,
1365
+ yes,
1366
+ defaults: flagBoolean(args.defaults),
1367
+ skipInstall,
1368
+ css: flagString(args.css),
1369
+ framework,
1370
+ bundler,
1371
+ registry
1372
+ });
1373
+ break;
1374
+ }
1375
+ case "add":
1376
+ await add({
1377
+ components: rest,
1378
+ cwd,
1379
+ registry,
1380
+ yes,
1381
+ overwrite: flagBoolean(args.overwrite),
1382
+ all: flagBoolean(args.all),
1383
+ path: flagString(args.path),
1384
+ skipInstall
1385
+ });
1386
+ break;
1387
+ case "list": {
1388
+ const { framework } = readFrameworkFlags(args);
1389
+ await list({
1390
+ cwd,
1391
+ registry,
1392
+ framework,
1393
+ type: flagString(args.type),
1394
+ json: flagBoolean(args.json)
1395
+ });
1396
+ break;
1397
+ }
1398
+ case "diff": await diff({
1399
+ component: rest[0],
1400
+ cwd,
1401
+ registry
1402
+ });
1403
+ }
1404
+ }
1405
+ main().catch(handleError);
1406
+
1407
+ //#endregion
1408
+ export { };