novaui-cli 1.1.1 → 1.1.3

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.
package/src/bin.js CHANGED
@@ -1,620 +1,133 @@
1
1
  #!/usr/bin/env node
2
2
 
3
- import fs from "node:fs"
4
- import path from "node:path"
5
- import readline from "node:readline"
6
- import { execFileSync } from "node:child_process"
7
- import { fileURLToPath } from "node:url"
3
+ import { Command } from 'commander';
4
+ import fs from 'node:fs';
5
+ import { fileURLToPath } from 'node:url';
6
+ import pc from 'picocolors';
8
7
 
9
- const BASE_URL =
10
- "https://raw.githubusercontent.com/KaloyanBehov/novaui/main"
8
+ // ─── Module imports ──────────────────────────────────────────────────────────
11
9
 
12
- const CONFIG_FILENAME = "components.json"
13
- const FETCH_TIMEOUT_MS = 15000
10
+ import { getThemeCssContent } from './themes/index.js';
11
+ import { add, pickComponentsInteractively } from './commands/add.js';
12
+ import { ASCII_BANNER, askTheme, init } from './commands/init.js';
13
+ import { CONFIG_FILENAME, DEFAULT_CONFIG, DEFAULT_THEME_CSS, UTILS_CONTENT } from './constants.js';
14
+ import { loadConfig, writeConfig } from './utils/config.js';
15
+ import { detectPackageManager, getInstallHint, getMissingDeps } from './utils/deps.js';
16
+ import { fetchWithTimeout, formatError } from './utils/fetch.js';
17
+ import { ensureDir, writeIfNotExists } from './utils/fs-helpers.js';
18
+ import { getTailwindConfigContent } from './utils/tailwind.js';
19
+ import { assertValidComponentConfig } from './utils/validate.js';
20
+ import { getCliVersion } from './utils/version.js';
21
+ import { checkForUpdates } from './utils/version-check.js';
14
22
 
15
- const __filename = fileURLToPath(import.meta.url)
16
- const __dirname = path.dirname(__filename)
17
- const CLI_PACKAGE_JSON_PATH = path.resolve(__dirname, "../package.json")
23
+ // ─── Derived constants ───────────────────────────────────────────────────────
18
24
 
19
- const DEFAULT_CONFIG = {
20
- globalCss: "src/global.css",
21
- componentsUi: "src/components/ui",
22
- lib: "src/lib",
23
- }
24
-
25
- // ─── ANSI styles (no dependency, TTY-safe) ──────────────────────────────────
26
-
27
- const isTty = process.stdout.isTTY === true
28
- const c = {
29
- reset: "\x1b[0m",
30
- bold: "\x1b[1m",
31
- dim: "\x1b[2m",
32
- cyan: "\x1b[36m",
33
- green: "\x1b[32m",
34
- yellow: "\x1b[33m",
35
- blue: "\x1b[34m",
36
- magenta: "\x1b[35m",
37
- }
38
- function style(use, text) {
39
- return isTty && use ? use + text + c.reset : text
40
- }
41
-
42
- const ASCII_BANNER = `
43
- ${style(c.cyan, " _ __ __ ____ ___ __")}
44
- ${style(c.cyan, " / | / /__ / / / _// | / /")}
45
- ${style(c.cyan, " / |/ / _ \\/ / / / / /| |/ / ")}
46
- ${style(c.cyan, "/_/|_/ \\___/_/ /___/_/ |_/_/ ")}
47
- ${style(c.dim, " React Native + NativeWind UI")}
48
- `
49
-
50
- // ─── CSS Variables (global.css) ─────────────────────────────────────────────
51
-
52
- const GLOBAL_CSS_CONTENT = `@tailwind base;
53
- @tailwind components;
54
- @tailwind utilities;
55
-
56
- @layer base {
57
- :root {
58
- --background: 0 0% 100%;
59
- --foreground: 240 10% 3.9%;
60
- --card: 0 0% 100%;
61
- --card-foreground: 240 10% 3.9%;
62
- --popover: 0 0% 100%;
63
- --popover-foreground: 240 10% 3.9%;
64
- --primary: 240 5.9% 10%;
65
- --primary-foreground: 0 0% 98%;
66
- --secondary: 240 4.8% 95.9%;
67
- --secondary-foreground: 240 5.9% 10%;
68
- --muted: 240 4.8% 95.9%;
69
- --muted-foreground: 240 3.8% 46.1%;
70
- --accent: 240 4.8% 95.9%;
71
- --accent-foreground: 240 5.9% 10%;
72
- --destructive: 0 84.2% 60.2%;
73
- --destructive-foreground: 0 0% 98%;
74
- --border: 240 5.9% 90%;
75
- --input: 240 5.9% 90%;
76
- --ring: 240 5.9% 10%;
77
- --radius: 0.5rem;
78
- }
79
-
80
- .dark {
81
- --background: 240 10% 3.9%;
82
- --foreground: 0 0% 98%;
83
- --card: 240 10% 3.9%;
84
- --card-foreground: 0 0% 98%;
85
- --popover: 240 10% 3.9%;
86
- --popover-foreground: 0 0% 98%;
87
- --primary: 0 0% 98%;
88
- --primary-foreground: 240 5.9% 10%;
89
- --secondary: 240 3.7% 15.9%;
90
- --secondary-foreground: 0 0% 98%;
91
- --muted: 240 3.7% 15.9%;
92
- --muted-foreground: 240 5% 64.9%;
93
- --accent: 240 3.7% 15.9%;
94
- --accent-foreground: 0 0% 98%;
95
- --destructive: 0 62.8% 30.6%;
96
- --destructive-foreground: 0 0% 98%;
97
- --border: 240 3.7% 15.9%;
98
- --input: 240 3.7% 15.9%;
99
- --ring: 240 4.9% 83.9%;
100
- }
101
- }
102
- `
103
-
104
- // ─── Tailwind Config ────────────────────────────────────────────────────────
105
-
106
- function getTailwindConfigContent(config) {
107
- const normalized = config.componentsUi.replace(/^\.\//, "")
108
- const contentPaths = [
109
- '"./App.{js,jsx,ts,tsx}"',
110
- '"./src/**/*.{js,jsx,ts,tsx}"',
111
- ]
112
- if (!normalized.startsWith("src/")) {
113
- contentPaths.push(`"./${normalized}/**/*.{js,jsx,ts,tsx}"`)
114
- }
115
- return `/** @type {import('tailwindcss').Config} */
116
- module.exports = {
117
- content: [
118
- ${contentPaths.join(",\n ")},
119
- ],
120
- presets: [require("nativewind/preset")],
121
- theme: {
122
- extend: {
123
- colors: {
124
- border: "hsl(var(--border))",
125
- input: "hsl(var(--input))",
126
- ring: "hsl(var(--ring))",
127
- background: "hsl(var(--background))",
128
- foreground: "hsl(var(--foreground))",
129
- primary: {
130
- DEFAULT: "hsl(var(--primary))",
131
- foreground: "hsl(var(--primary-foreground))",
132
- },
133
- secondary: {
134
- DEFAULT: "hsl(var(--secondary))",
135
- foreground: "hsl(var(--secondary-foreground))",
136
- },
137
- destructive: {
138
- DEFAULT: "hsl(var(--destructive))",
139
- foreground: "hsl(var(--destructive-foreground))",
140
- },
141
- muted: {
142
- DEFAULT: "hsl(var(--muted))",
143
- foreground: "hsl(var(--muted-foreground))",
144
- },
145
- accent: {
146
- DEFAULT: "hsl(var(--accent))",
147
- foreground: "hsl(var(--accent-foreground))",
148
- },
149
- popover: {
150
- DEFAULT: "hsl(var(--popover))",
151
- foreground: "hsl(var(--popover-foreground))",
152
- },
153
- card: {
154
- DEFAULT: "hsl(var(--card))",
155
- foreground: "hsl(var(--card-foreground))",
156
- },
157
- },
158
- borderRadius: {
159
- lg: "var(--radius)",
160
- md: "calc(var(--radius) - 2px)",
161
- sm: "calc(var(--radius) - 4px)",
162
- },
163
- },
164
- },
165
- plugins: [],
166
- };
167
- `
168
- }
169
-
170
- // ─── Utils ──────────────────────────────────────────────────────────────────
171
-
172
- const UTILS_CONTENT = `import { type ClassValue, clsx } from "clsx"
173
- import { twMerge } from "tailwind-merge"
174
-
175
- export function cn(...inputs: ClassValue[]) {
176
- return twMerge(clsx(inputs))
177
- }
178
- `
179
-
180
- // ─── Helpers ────────────────────────────────────────────────────────────────
181
-
182
- function detectPackageManager() {
183
- const userAgent = process.env.npm_config_user_agent || ""
184
- if (userAgent.startsWith("yarn")) return { command: "yarn", baseArgs: ["add"] }
185
- if (userAgent.startsWith("pnpm")) return { command: "pnpm", baseArgs: ["add"] }
186
- if (userAgent.startsWith("bun")) return { command: "bun", baseArgs: ["add"] }
187
- return { command: "npm", baseArgs: ["install"] }
188
- }
189
-
190
- function installPackages(packages) {
191
- if (!Array.isArray(packages) || packages.length === 0) return
192
- const { command, baseArgs } = detectPackageManager()
193
- execFileSync(command, [...baseArgs, ...packages], { stdio: "inherit" })
194
- }
195
-
196
- function getInstallHint(packages) {
197
- const { command, baseArgs } = detectPackageManager()
198
- return `${command} ${[...baseArgs, ...packages].join(" ")}`
199
- }
200
-
201
- function ensureDir(dir) {
202
- if (!fs.existsSync(dir)) {
203
- fs.mkdirSync(dir, { recursive: true })
204
- }
205
- }
206
-
207
- function writeIfNotExists(filePath, content, label) {
208
- if (fs.existsSync(filePath)) {
209
- console.log(style(c.dim, ` ℹ ${label} already exists, skipping.`))
210
- return false
211
- }
212
- fs.writeFileSync(filePath, content, "utf8")
213
- console.log(style(c.green, ` ✓ Created ${label}`))
214
- return true
215
- }
216
-
217
- /** Returns which of the requested deps are not listed in package.json (dependencies or devDependencies). */
218
- function getMissingDeps(cwd, deps) {
219
- const pkgPath = path.join(cwd, "package.json")
220
- if (!fs.existsSync(pkgPath)) {
221
- return [...deps]
222
- }
223
- const pkg = JSON.parse(fs.readFileSync(pkgPath, "utf8"))
224
- const installed = new Set([
225
- ...Object.keys(pkg.dependencies || {}),
226
- ...Object.keys(pkg.devDependencies || {}),
227
- ])
228
- return deps.filter((d) => !installed.has(d))
229
- }
230
-
231
- /** Ask user a question; returns trimmed answer or default. */
232
- function ask(question, defaultAnswer = "") {
233
- if (process.stdin.isTTY !== true) {
234
- return Promise.resolve(defaultAnswer)
235
- }
236
- const rl = readline.createInterface({ input: process.stdin, output: process.stdout })
237
- const defaultPart = defaultAnswer ? style(c.dim, ` (${defaultAnswer})`) : ""
238
- const prompt = style(c.cyan, " ? ") + question + defaultPart + style(c.dim, " ")
239
- return new Promise((resolve) => {
240
- rl.question(prompt, (answer) => {
241
- rl.close()
242
- const trimmed = answer.trim()
243
- resolve(trimmed !== "" ? trimmed : defaultAnswer)
244
- })
245
- })
246
- }
247
-
248
- /** Load components.json from cwd; returns null if missing or invalid. */
249
- function loadConfig(cwd) {
250
- const configPath = path.join(cwd, CONFIG_FILENAME)
251
- if (!fs.existsSync(configPath)) return null
252
- try {
253
- const raw = JSON.parse(fs.readFileSync(configPath, "utf8"))
254
- return { ...DEFAULT_CONFIG, ...raw }
255
- } catch {
256
- return null
257
- }
258
- }
259
-
260
- /** Write components.json to cwd. */
261
- function writeConfig(cwd, config) {
262
- const configPath = path.join(cwd, CONFIG_FILENAME)
263
- fs.writeFileSync(configPath, JSON.stringify(config, null, 2), "utf8")
264
- }
265
-
266
- function getCliVersion() {
267
- try {
268
- const pkg = JSON.parse(fs.readFileSync(CLI_PACKAGE_JSON_PATH, "utf8"))
269
- return pkg.version || "unknown"
270
- } catch {
271
- return "unknown"
272
- }
273
- }
274
-
275
- function assertValidComponentConfig(componentName, componentConfig) {
276
- if (!componentConfig || typeof componentConfig !== "object") {
277
- throw new Error(`Registry entry for "${componentName}" is invalid.`)
278
- }
279
-
280
- const { files, dependencies } = componentConfig
281
- if (!Array.isArray(files) || files.some((file) => typeof file !== "string" || file.trim() === "")) {
282
- throw new Error(`Registry entry for "${componentName}" must include a valid "files" array.`)
283
- }
284
-
285
- if (
286
- dependencies !== undefined &&
287
- (!Array.isArray(dependencies) ||
288
- dependencies.some((dep) => typeof dep !== "string" || dep.trim() === ""))
289
- ) {
290
- throw new Error(`Registry entry for "${componentName}" has an invalid "dependencies" array.`)
291
- }
292
- }
293
-
294
- async function fetchWithTimeout(url) {
295
- const controller = new AbortController()
296
- const timeout = setTimeout(() => controller.abort(), FETCH_TIMEOUT_MS)
297
- try {
298
- return await fetch(url, { signal: controller.signal })
299
- } catch (error) {
300
- if (error && error.name === "AbortError") {
301
- throw new Error(`Request timed out after ${FETCH_TIMEOUT_MS}ms: ${url}`)
302
- }
303
- throw error
304
- } finally {
305
- clearTimeout(timeout)
306
- }
307
- }
308
-
309
- function formatError(error) {
310
- if (error instanceof Error && error.message) return error.message
311
- return String(error)
312
- }
313
-
314
- // ─── Commands ───────────────────────────────────────────────────────────────
315
-
316
- async function init() {
317
- const cwd = process.cwd()
318
- const existingConfig = loadConfig(cwd)
319
-
320
- // ─── Banner ───────────────────────────────────────────────────────────────
321
- console.log(ASCII_BANNER)
322
- console.log(style(c.bold, " Welcome to NovaUI"))
323
- console.log(style(c.dim, " Let's set up your project in a few steps."))
324
- console.log("")
325
-
326
- let config
327
- if (existingConfig) {
328
- console.log(style(c.blue, " ⚙ Config"))
329
- console.log(style(c.dim, " components.json found in this directory."))
330
- console.log("")
331
- const reconfig = await ask("Re-configure paths? (y/N)", "n")
332
- if (reconfig.toLowerCase() === "y" || reconfig.toLowerCase() === "yes") {
333
- config = {
334
- globalCss: (await ask("Where should global.css be placed?", DEFAULT_CONFIG.globalCss)).replace(/\\/g, "/"),
335
- componentsUi: (await ask("Where should UI components be placed?", DEFAULT_CONFIG.componentsUi)).replace(/\\/g, "/"),
336
- lib: (await ask("Where should lib (e.g. utils) be placed?", DEFAULT_CONFIG.lib)).replace(/\\/g, "/"),
337
- }
338
- writeConfig(cwd, config)
339
- console.log("")
340
- console.log(style(c.green, " ✓ Updated components.json"))
341
- } else {
342
- config = existingConfig
343
- }
344
- } else {
345
- console.log(style(c.blue, " ⚙ Configure paths"))
346
- console.log(style(c.dim, " Where should NovaUI put its files? Press Enter for defaults."))
347
- console.log("")
348
- config = {
349
- globalCss: (await ask("Path for global.css?", DEFAULT_CONFIG.globalCss)).replace(/\\/g, "/"),
350
- componentsUi: (await ask("Path for UI components?", DEFAULT_CONFIG.componentsUi)).replace(/\\/g, "/"),
351
- lib: (await ask("Path for lib (utils)?", DEFAULT_CONFIG.lib)).replace(/\\/g, "/"),
352
- }
353
- writeConfig(cwd, config)
354
- console.log("")
355
- console.log(style(c.green, " ✓ Created components.json"))
356
- }
357
-
358
- // ─── Setup files ──────────────────────────────────────────────────────────
359
- console.log("")
360
- console.log(style(c.blue, " 📁 Setting up project"))
361
- console.log("")
25
+ // Export default theme CSS for backward compatibility (tests)
26
+ export const GLOBAL_CSS_CONTENT = DEFAULT_THEME_CSS;
362
27
 
363
- const utilsDir = path.join(cwd, config.lib)
364
- ensureDir(utilsDir)
365
- const utilsPath = path.join(utilsDir, "utils.ts")
366
- writeIfNotExists(utilsPath, UTILS_CONTENT, `${config.lib}/utils.ts`)
367
-
368
- const globalCssDir = path.dirname(path.join(cwd, config.globalCss))
369
- ensureDir(globalCssDir)
370
- writeIfNotExists(path.join(cwd, config.globalCss), GLOBAL_CSS_CONTENT, config.globalCss)
371
-
372
- const tailwindContent = getTailwindConfigContent(config)
373
- writeIfNotExists(
374
- path.join(cwd, "tailwind.config.js"),
375
- tailwindContent,
376
- "tailwind.config.js"
377
- )
378
-
379
- // ─── Dependencies ─────────────────────────────────────────────────────────
380
- const deps = [
381
- "nativewind",
382
- "tailwindcss",
383
- "clsx",
384
- "tailwind-merge",
385
- "class-variance-authority",
386
- ]
387
- const missingDeps = getMissingDeps(cwd, deps)
388
-
389
- console.log("")
390
- console.log(style(c.blue, " 📦 Dependencies"))
391
- console.log("")
392
-
393
- if (missingDeps.length === 0) {
394
- console.log(style(c.dim, " ✓ All required packages already in package.json, skipping install."))
395
- } else {
396
- console.log(style(c.dim, ` Installing: ${missingDeps.join(", ")}`))
397
- console.log("")
398
- try {
399
- installPackages(missingDeps)
400
- } catch {
401
- console.error("")
402
- console.error(style(c.yellow, " ✗ Install failed. Run manually:"))
403
- console.error(style(c.dim, ` ${getInstallHint(missingDeps)}`))
404
- }
405
- }
406
-
407
- // ─── Success ───────────────────────────────────────────────────────────────
408
- console.log("")
409
- console.log(style(c.green, " ┌─────────────────────────────────────────┐"))
410
- console.log(style(c.green, " │ ✓ NovaUI is ready! │"))
411
- console.log(style(c.green, " └─────────────────────────────────────────┘"))
412
- console.log("")
413
- console.log(style(c.bold, " Next steps:"))
414
- console.log("")
415
- console.log(style(c.dim, " 1. Import global CSS in your root entry (e.g. App.tsx):"))
416
- console.log(style(c.cyan, ` import "${config.globalCss}"`))
417
- console.log("")
418
- console.log(style(c.dim, " 2. Add components:"))
419
- console.log(style(c.cyan, " npx novaui add button"))
420
- console.log(style(c.cyan, " npx novaui add card"))
421
- console.log("")
422
- }
423
-
424
- async function add(componentName, options = {}) {
425
- const { force = false } = options
426
-
427
- if (!componentName) {
428
- throw new Error("Missing component name. Usage: novaui add <component-name>")
429
- }
430
-
431
- const cwd = process.cwd()
432
-
433
- console.log("")
434
- console.log(` ◆ NovaUI – Adding "${componentName}"...`)
435
- console.log("")
436
-
437
- // 1. Fetch registry
438
- console.log(" Fetching registry...")
439
- const registryResponse = await fetchWithTimeout(`${BASE_URL}/registry.json`)
440
-
441
- if (!registryResponse.ok) {
442
- throw new Error(`Failed to fetch registry: ${registryResponse.statusText}`)
443
- }
444
-
445
- const registry = await registryResponse.json()
446
- if (!registry || typeof registry !== "object" || Array.isArray(registry)) {
447
- throw new Error("Registry response is not a valid object.")
448
- }
449
-
450
- if (!registry[componentName]) {
451
- const available = Object.keys(registry).join(", ")
452
- throw new Error(
453
- `Component "${componentName}" not found in registry.\n\n Available components:\n ${available}`
454
- )
455
- }
456
-
457
- const componentConfig = registry[componentName]
458
- assertValidComponentConfig(componentName, componentConfig)
459
- const projectConfig = loadConfig(cwd) || DEFAULT_CONFIG
460
- const targetBaseDir = path.join(cwd, projectConfig.componentsUi)
461
- ensureDir(targetBaseDir)
462
-
463
- // 2. Ensure utils.ts exists (in configured lib dir)
464
- const utilsDir = path.join(cwd, projectConfig.lib)
465
- const utilsPath = path.join(utilsDir, "utils.ts")
466
-
467
- if (!fs.existsSync(utilsPath)) {
468
- ensureDir(utilsDir)
469
- fs.writeFileSync(utilsPath, UTILS_CONTENT)
470
- console.log(` ✓ Created ${projectConfig.lib}/utils.ts`)
471
- }
472
-
473
- if (!loadConfig(cwd)) {
474
- console.log("")
475
- console.log(" ℹ No components.json found. Using default paths. Run 'npx novaui init' to customize.")
476
- console.log("")
477
- }
478
-
479
- // 3. Fetch and write component files
480
- for (const file of componentConfig.files) {
481
- const fileUrl = `${BASE_URL}/${file}`
482
- const fileName = path.basename(file)
483
- const destPath = path.join(targetBaseDir, fileName)
484
-
485
- if (!force && fs.existsSync(destPath)) {
486
- console.log(style(c.dim, ` ℹ ${fileName} already exists, skipping. (use --force to overwrite)`))
487
- continue
488
- }
489
-
490
- console.log(` Downloading ${fileName}...`)
491
- const fileResponse = await fetchWithTimeout(fileUrl)
492
-
493
- if (!fileResponse.ok) {
494
- console.error(` ✗ Failed to download ${fileName}`)
495
- continue
496
- }
497
-
498
- const content = await fileResponse.text()
499
- fs.writeFileSync(destPath, content, "utf8")
500
- console.log(` ✓ Added ${fileName}`)
501
- }
502
-
503
- // 4. Install component dependencies (only those not already in package.json)
504
- if (componentConfig.dependencies && componentConfig.dependencies.length > 0) {
505
- const missingDeps = getMissingDeps(cwd, componentConfig.dependencies)
506
- if (missingDeps.length === 0) {
507
- console.log("")
508
- console.log(" ✓ Component dependencies already in package.json, skipping install.")
509
- } else {
510
- console.log("")
511
- console.log(` Installing dependencies: ${missingDeps.join(", ")}...`)
512
- try {
513
- installPackages(missingDeps)
514
- } catch {
515
- console.error(" ✗ Failed to install dependencies automatically.")
516
- }
517
- }
518
- }
519
-
520
- console.log("")
521
- console.log(` ✓ Successfully added "${componentName}"!`)
522
- console.log("")
523
- }
524
-
525
- function showHelp() {
526
- console.log(ASCII_BANNER)
527
- console.log(style(c.dim, ` Version: ${getCliVersion()}`))
528
- console.log("")
529
- console.log(style(c.bold, " Usage"))
530
- console.log(style(c.dim, " novaui init Set up NovaUI (config, Tailwind, global.css, utils)"))
531
- console.log(style(c.dim, " novaui add <component> Add a component (e.g. button, card)"))
532
- console.log(style(c.dim, " novaui add <component> --force Overwrite existing component files"))
533
- console.log(style(c.dim, " novaui --version Show CLI version"))
534
- console.log("")
535
- console.log(style(c.bold, " Examples"))
536
- console.log(style(c.cyan, " npx novaui init"))
537
- console.log(style(c.cyan, " npx novaui add button"))
538
- console.log(style(c.cyan, " npx novaui add card"))
539
- console.log("")
540
- }
541
-
542
- function showVersion() {
543
- console.log(getCliVersion())
544
- }
545
-
546
- // ─── Exports (for testing) ──────────────────────────────────────────────────
28
+ // ─── Re-exports (backward compatibility for tests) ───────────────────────────
547
29
 
548
30
  export {
549
- DEFAULT_CONFIG,
31
+ add,
32
+ askTheme,
33
+ assertValidComponentConfig,
550
34
  CONFIG_FILENAME,
551
- GLOBAL_CSS_CONTENT,
552
- UTILS_CONTENT,
553
- getTailwindConfigContent,
35
+ DEFAULT_CONFIG,
554
36
  detectPackageManager,
555
37
  ensureDir,
556
- writeIfNotExists,
557
- getMissingDeps,
558
- loadConfig,
559
- writeConfig,
560
- assertValidComponentConfig,
561
38
  fetchWithTimeout,
562
39
  formatError,
563
40
  getCliVersion,
564
41
  getInstallHint,
42
+ getMissingDeps,
43
+ getTailwindConfigContent,
44
+ getThemeCssContent,
565
45
  init,
566
- add,
567
- }
46
+ loadConfig,
47
+ UTILS_CONTENT,
48
+ writeConfig,
49
+ writeIfNotExists,
50
+ };
568
51
 
569
- // ─── Main ───────────────────────────────────────────────────────────────────
52
+ // ─── CLI entry point ─────────────────────────────────────────────────────────
570
53
 
571
54
  const isDirectRun = (() => {
572
- if (!process.argv[1]) return false
55
+ if (!process.argv[1]) return false;
573
56
  try {
574
- return fs.realpathSync(process.argv[1]) === fileURLToPath(import.meta.url)
57
+ return fs.realpathSync(process.argv[1]) === fileURLToPath(import.meta.url);
575
58
  } catch {
576
- return process.argv[1].endsWith("/bin.js")
59
+ return process.argv[1].endsWith('/bin.js');
577
60
  }
578
- })()
61
+ })();
579
62
 
580
63
  if (isDirectRun) {
581
- const command = process.argv[2]
582
- const restArgs = process.argv.slice(3)
583
- const flags = new Set(restArgs.filter((a) => a.startsWith("--")))
584
- const positional = restArgs.filter((a) => !a.startsWith("--"))
585
- const force = flags.has("--force")
64
+ const program = new Command();
65
+
66
+ program
67
+ .name('novaui')
68
+ .description('NovaUI React Native + NativeWind UI component library')
69
+ .version(getCliVersion(), '-v, --version', 'Show CLI version')
70
+ .addHelpText('beforeAll', ASCII_BANNER)
71
+ .hook('preAction', async () => {
72
+ await checkForUpdates().catch(() => {});
73
+ });
74
+
75
+ // ─── init ─────────────────────────────────────────────────────────────────
76
+
77
+ program
78
+ .command('init')
79
+ .description('Set up NovaUI (config, Tailwind, global.css, utils)')
80
+ .option('-y, --yes', 'Skip prompts and use default configuration')
81
+ .action(async (options) => {
82
+ try {
83
+ await init({ yes: options.yes });
84
+ } catch (error) {
85
+ console.error('');
86
+ console.error(pc.red(` ✗ Error: ${formatError(error)}`));
87
+ process.exit(1);
88
+ }
89
+ });
90
+
91
+ // ─── add ──────────────────────────────────────────────────────────────────
586
92
 
587
- async function main() {
588
- try {
589
- switch (command) {
590
- case "init":
591
- await init()
592
- break
593
- case "add":
594
- await add(positional[0], { force })
595
- break
596
- case "--help":
597
- case "-h":
598
- showHelp()
599
- break
600
- case "--version":
601
- case "-v":
602
- showVersion()
603
- break
604
- default:
605
- if (command) {
606
- await add(command, { force })
607
- } else {
608
- showHelp()
93
+ program
94
+ .command('add [components...]')
95
+ .description('Add one or more components (e.g. button card input)')
96
+ .option('--force', 'Overwrite existing component files')
97
+ .action(async (components, options) => {
98
+ const force = options.force || false;
99
+ try {
100
+ if (components.length === 0) {
101
+ if (process.stdin.isTTY !== true) {
102
+ throw new Error('Missing component name. Usage: novaui add <component-name>');
103
+ }
104
+ // Interactive multi-select when no component names given
105
+ const selected = await pickComponentsInteractively();
106
+ for (const name of selected) {
107
+ await add(name, { force });
609
108
  }
610
- break
109
+ } else {
110
+ // Support batch: novaui add button card input
111
+ for (const name of components) {
112
+ await add(name, { force });
113
+ }
114
+ }
115
+ } catch (error) {
116
+ console.error('');
117
+ console.error(pc.red(` ✗ Error: ${formatError(error)}`));
118
+ process.exit(1);
611
119
  }
612
- } catch (error) {
613
- console.error("")
614
- console.error(` ✗ Error: ${formatError(error)}`)
615
- process.exit(1)
616
- }
617
- }
120
+ });
121
+
122
+ // ─── Default: show help when no command given ─────────────────────────────
123
+
124
+ program.action(() => {
125
+ program.help();
126
+ });
618
127
 
619
- main()
128
+ program.parseAsync(process.argv).catch((error) => {
129
+ console.error('');
130
+ console.error(pc.red(` ✗ Error: ${formatError(error)}`));
131
+ process.exit(1);
132
+ });
620
133
  }