novaui-cli 1.0.3 → 1.0.5

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/package.json +1 -1
  2. package/src/bin.js +299 -70
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "novaui-cli",
3
- "version": "1.0.3",
3
+ "version": "1.0.5",
4
4
  "description": "CLI for NovaUI - React Native component library with NativeWind",
5
5
  "type": "module",
6
6
  "bin": {
package/src/bin.js CHANGED
@@ -2,11 +2,51 @@
2
2
 
3
3
  import fs from "node:fs"
4
4
  import path from "node:path"
5
- import { execSync } from "node:child_process"
5
+ import readline from "node:readline"
6
+ import { execFileSync } from "node:child_process"
7
+ import { fileURLToPath } from "node:url"
6
8
 
7
9
  const BASE_URL =
8
10
  "https://raw.githubusercontent.com/KaloyanBehov/native-ui/main"
9
11
 
12
+ const CONFIG_FILENAME = "components.json"
13
+ const FETCH_TIMEOUT_MS = 15000
14
+
15
+ const __filename = fileURLToPath(import.meta.url)
16
+ const __dirname = path.dirname(__filename)
17
+ const CLI_PACKAGE_JSON_PATH = path.resolve(__dirname, "../package.json")
18
+
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
+
10
50
  // ─── CSS Variables (global.css) ─────────────────────────────────────────────
11
51
 
12
52
  const GLOBAL_CSS_CONTENT = `@tailwind base;
@@ -63,11 +103,16 @@ const GLOBAL_CSS_CONTENT = `@tailwind base;
63
103
 
64
104
  // ─── Tailwind Config ────────────────────────────────────────────────────────
65
105
 
66
- const TAILWIND_CONFIG_CONTENT = `/** @type {import('tailwindcss').Config} */
106
+ function getTailwindConfigContent(config) {
107
+ const contentPaths = [
108
+ '"./App.{js,jsx,ts,tsx}"',
109
+ '"./src/**/*.{js,jsx,ts,tsx}"',
110
+ `"./${config.componentsUi}/**/*.{js,jsx,ts,tsx}"`,
111
+ ]
112
+ return `/** @type {import('tailwindcss').Config} */
67
113
  module.exports = {
68
114
  content: [
69
- "./App.{js,jsx,ts,tsx}",
70
- "./src/**/*.{js,jsx,ts,tsx}",
115
+ ${contentPaths.join(",\n ")},
71
116
  ],
72
117
  presets: [require("nativewind/preset")],
73
118
  theme: {
@@ -117,6 +162,7 @@ module.exports = {
117
162
  plugins: [],
118
163
  };
119
164
  `
165
+ }
120
166
 
121
167
  // ─── Utils ──────────────────────────────────────────────────────────────────
122
168
 
@@ -132,10 +178,21 @@ export function cn(...inputs: ClassValue[]) {
132
178
 
133
179
  function detectPackageManager() {
134
180
  const userAgent = process.env.npm_config_user_agent || ""
135
- if (userAgent.startsWith("yarn")) return "yarn add"
136
- if (userAgent.startsWith("pnpm")) return "pnpm add"
137
- if (userAgent.startsWith("bun")) return "bun add"
138
- return "npm install"
181
+ if (userAgent.startsWith("yarn")) return { command: "yarn", baseArgs: ["add"] }
182
+ if (userAgent.startsWith("pnpm")) return { command: "pnpm", baseArgs: ["add"] }
183
+ if (userAgent.startsWith("bun")) return { command: "bun", baseArgs: ["add"] }
184
+ return { command: "npm", baseArgs: ["install"] }
185
+ }
186
+
187
+ function installPackages(packages) {
188
+ if (!Array.isArray(packages) || packages.length === 0) return
189
+ const { command, baseArgs } = detectPackageManager()
190
+ execFileSync(command, [...baseArgs, ...packages], { stdio: "inherit" })
191
+ }
192
+
193
+ function getInstallHint(packages) {
194
+ const { command, baseArgs } = detectPackageManager()
195
+ return `${command} ${[...baseArgs, ...packages].join(" ")}`
139
196
  }
140
197
 
141
198
  function ensureDir(dir) {
@@ -146,46 +203,177 @@ function ensureDir(dir) {
146
203
 
147
204
  function writeIfNotExists(filePath, content, label) {
148
205
  if (fs.existsSync(filePath)) {
149
- console.log(`ℹ ${label} already exists, skipping.`)
206
+ console.log(style(c.dim, ` ℹ ${label} already exists, skipping.`))
150
207
  return false
151
208
  }
152
- fs.writeFileSync(filePath, content)
153
- console.log(`✓ Created ${label}`)
209
+ fs.writeFileSync(filePath, content, "utf8")
210
+ console.log(style(c.green, ` ✓ Created ${label}`))
154
211
  return true
155
212
  }
156
213
 
214
+ /** Returns which of the requested deps are not listed in package.json (dependencies or devDependencies). */
215
+ function getMissingDeps(cwd, deps) {
216
+ const pkgPath = path.join(cwd, "package.json")
217
+ if (!fs.existsSync(pkgPath)) {
218
+ return [...deps]
219
+ }
220
+ const pkg = JSON.parse(fs.readFileSync(pkgPath, "utf8"))
221
+ const installed = new Set([
222
+ ...Object.keys(pkg.dependencies || {}),
223
+ ...Object.keys(pkg.devDependencies || {}),
224
+ ])
225
+ return deps.filter((d) => !installed.has(d))
226
+ }
227
+
228
+ /** Ask user a question; returns trimmed answer or default. */
229
+ function ask(question, defaultAnswer = "") {
230
+ if (process.stdin.isTTY !== true) {
231
+ return Promise.resolve(defaultAnswer)
232
+ }
233
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout })
234
+ const defaultPart = defaultAnswer ? style(c.dim, ` (${defaultAnswer})`) : ""
235
+ const prompt = style(c.cyan, " ? ") + question + defaultPart + style(c.dim, " ")
236
+ return new Promise((resolve) => {
237
+ rl.question(prompt, (answer) => {
238
+ rl.close()
239
+ const trimmed = answer.trim()
240
+ resolve(trimmed !== "" ? trimmed : defaultAnswer)
241
+ })
242
+ })
243
+ }
244
+
245
+ /** Load components.json from cwd; returns null if missing or invalid. */
246
+ function loadConfig(cwd) {
247
+ const configPath = path.join(cwd, CONFIG_FILENAME)
248
+ if (!fs.existsSync(configPath)) return null
249
+ try {
250
+ const raw = JSON.parse(fs.readFileSync(configPath, "utf8"))
251
+ return { ...DEFAULT_CONFIG, ...raw }
252
+ } catch {
253
+ return null
254
+ }
255
+ }
256
+
257
+ /** Write components.json to cwd. */
258
+ function writeConfig(cwd, config) {
259
+ const configPath = path.join(cwd, CONFIG_FILENAME)
260
+ fs.writeFileSync(configPath, JSON.stringify(config, null, 2), "utf8")
261
+ }
262
+
263
+ function getCliVersion() {
264
+ try {
265
+ const pkg = JSON.parse(fs.readFileSync(CLI_PACKAGE_JSON_PATH, "utf8"))
266
+ return pkg.version || "unknown"
267
+ } catch {
268
+ return "unknown"
269
+ }
270
+ }
271
+
272
+ function assertValidComponentConfig(componentName, componentConfig) {
273
+ if (!componentConfig || typeof componentConfig !== "object") {
274
+ throw new Error(`Registry entry for "${componentName}" is invalid.`)
275
+ }
276
+
277
+ const { files, dependencies } = componentConfig
278
+ if (!Array.isArray(files) || files.some((file) => typeof file !== "string" || file.trim() === "")) {
279
+ throw new Error(`Registry entry for "${componentName}" must include a valid "files" array.`)
280
+ }
281
+
282
+ if (
283
+ dependencies !== undefined &&
284
+ (!Array.isArray(dependencies) ||
285
+ dependencies.some((dep) => typeof dep !== "string" || dep.trim() === ""))
286
+ ) {
287
+ throw new Error(`Registry entry for "${componentName}" has an invalid "dependencies" array.`)
288
+ }
289
+ }
290
+
291
+ async function fetchWithTimeout(url) {
292
+ const controller = new AbortController()
293
+ const timeout = setTimeout(() => controller.abort(), FETCH_TIMEOUT_MS)
294
+ try {
295
+ return await fetch(url, { signal: controller.signal })
296
+ } catch (error) {
297
+ if (error && error.name === "AbortError") {
298
+ throw new Error(`Request timed out after ${FETCH_TIMEOUT_MS}ms: ${url}`)
299
+ }
300
+ throw error
301
+ } finally {
302
+ clearTimeout(timeout)
303
+ }
304
+ }
305
+
306
+ function formatError(error) {
307
+ if (error instanceof Error && error.message) return error.message
308
+ return String(error)
309
+ }
310
+
157
311
  // ─── Commands ───────────────────────────────────────────────────────────────
158
312
 
159
313
  async function init() {
160
- console.log("")
161
- console.log(" ◆ NovaUI Initializing project...")
314
+ const cwd = process.cwd()
315
+ const existingConfig = loadConfig(cwd)
316
+
317
+ // ─── Banner ───────────────────────────────────────────────────────────────
318
+ console.log(ASCII_BANNER)
319
+ console.log(style(c.bold, " Welcome to NovaUI"))
320
+ console.log(style(c.dim, " Let's set up your project in a few steps."))
162
321
  console.log("")
163
322
 
164
- const cwd = process.cwd()
323
+ let config
324
+ if (existingConfig) {
325
+ console.log(style(c.blue, " ⚙ Config"))
326
+ console.log(style(c.dim, " components.json found in this directory."))
327
+ console.log("")
328
+ const reconfig = await ask("Re-configure paths? (y/N)", "n")
329
+ if (reconfig.toLowerCase() === "y" || reconfig.toLowerCase() === "yes") {
330
+ config = {
331
+ globalCss: (await ask("Where should global.css be placed?", DEFAULT_CONFIG.globalCss)).replace(/\\/g, "/"),
332
+ componentsUi: (await ask("Where should UI components be placed?", DEFAULT_CONFIG.componentsUi)).replace(/\\/g, "/"),
333
+ lib: (await ask("Where should lib (e.g. utils) be placed?", DEFAULT_CONFIG.lib)).replace(/\\/g, "/"),
334
+ }
335
+ writeConfig(cwd, config)
336
+ console.log("")
337
+ console.log(style(c.green, " ✓ Updated components.json"))
338
+ } else {
339
+ config = existingConfig
340
+ }
341
+ } else {
342
+ console.log(style(c.blue, " ⚙ Configure paths"))
343
+ console.log(style(c.dim, " Where should NovaUI put its files? Press Enter for defaults."))
344
+ console.log("")
345
+ config = {
346
+ globalCss: (await ask("Path for global.css?", DEFAULT_CONFIG.globalCss)).replace(/\\/g, "/"),
347
+ componentsUi: (await ask("Path for UI components?", DEFAULT_CONFIG.componentsUi)).replace(/\\/g, "/"),
348
+ lib: (await ask("Path for lib (utils)?", DEFAULT_CONFIG.lib)).replace(/\\/g, "/"),
349
+ }
350
+ writeConfig(cwd, config)
351
+ console.log("")
352
+ console.log(style(c.green, " ✓ Created components.json"))
353
+ }
354
+
355
+ // ─── Setup files ──────────────────────────────────────────────────────────
356
+ console.log("")
357
+ console.log(style(c.blue, " 📁 Setting up project"))
358
+ console.log("")
165
359
 
166
- // 1. Create src/lib/utils.ts
167
- const utilsDir = path.join(cwd, "src", "lib")
360
+ const utilsDir = path.join(cwd, config.lib)
168
361
  ensureDir(utilsDir)
169
- writeIfNotExists(path.join(utilsDir, "utils.ts"), UTILS_CONTENT, "src/lib/utils.ts")
362
+ const utilsPath = path.join(utilsDir, "utils.ts")
363
+ writeIfNotExists(utilsPath, UTILS_CONTENT, `${config.lib}/utils.ts`)
170
364
 
171
- // 2. Create src/global.css
172
- const srcDir = path.join(cwd, "src")
173
- ensureDir(srcDir)
174
- writeIfNotExists(path.join(srcDir, "global.css"), GLOBAL_CSS_CONTENT, "src/global.css")
365
+ const globalCssDir = path.dirname(path.join(cwd, config.globalCss))
366
+ ensureDir(globalCssDir)
367
+ writeIfNotExists(path.join(cwd, config.globalCss), GLOBAL_CSS_CONTENT, config.globalCss)
175
368
 
176
- // 3. Create tailwind.config.js
369
+ const tailwindContent = getTailwindConfigContent(config)
177
370
  writeIfNotExists(
178
371
  path.join(cwd, "tailwind.config.js"),
179
- TAILWIND_CONFIG_CONTENT,
372
+ tailwindContent,
180
373
  "tailwind.config.js"
181
374
  )
182
375
 
183
- // 4. Install core dependencies
184
- console.log("")
185
- console.log(" ◆ Installing dependencies...")
186
- console.log("")
187
-
188
- const installCmd = detectPackageManager()
376
+ // ─── Dependencies ─────────────────────────────────────────────────────────
189
377
  const deps = [
190
378
  "nativewind",
191
379
  "tailwindcss",
@@ -193,22 +381,40 @@ async function init() {
193
381
  "tailwind-merge",
194
382
  "class-variance-authority",
195
383
  ]
384
+ const missingDeps = getMissingDeps(cwd, deps)
196
385
 
197
- try {
198
- execSync(`${installCmd} ${deps.join(" ")}`, { stdio: "inherit" })
199
- } catch {
200
- console.error("")
201
- console.error(" ✗ Failed to install dependencies. Please install manually:")
202
- console.error(` ${installCmd} ${deps.join(" ")}`)
386
+ console.log("")
387
+ console.log(style(c.blue, " 📦 Dependencies"))
388
+ console.log("")
389
+
390
+ if (missingDeps.length === 0) {
391
+ console.log(style(c.dim, " ✓ All required packages already in package.json, skipping install."))
392
+ } else {
393
+ console.log(style(c.dim, ` Installing: ${missingDeps.join(", ")}`))
394
+ console.log("")
395
+ try {
396
+ installPackages(missingDeps)
397
+ } catch {
398
+ console.error("")
399
+ console.error(style(c.yellow, " ✗ Install failed. Run manually:"))
400
+ console.error(style(c.dim, ` ${getInstallHint(missingDeps)}`))
401
+ }
203
402
  }
204
403
 
404
+ // ─── Success ───────────────────────────────────────────────────────────────
405
+ console.log("")
406
+ console.log(style(c.green, " ┌─────────────────────────────────────────┐"))
407
+ console.log(style(c.green, " │ ✓ NovaUI is ready! │"))
408
+ console.log(style(c.green, " └─────────────────────────────────────────┘"))
205
409
  console.log("")
206
- console.log(" ✓ Project initialized!")
410
+ console.log(style(c.bold, " Next steps:"))
207
411
  console.log("")
208
- console.log(" Next steps:")
209
- console.log(' 1. Import "src/global.css" in your root layout/entry file')
210
- console.log(" 2. Start adding components:")
211
- console.log(" npx novaui add button")
412
+ console.log(style(c.dim, " 1. Import global CSS in your root entry (e.g. App.tsx):"))
413
+ console.log(style(c.cyan, ` import "${config.globalCss}"`))
414
+ console.log("")
415
+ console.log(style(c.dim, " 2. Add components:"))
416
+ console.log(style(c.cyan, " npx novaui add button"))
417
+ console.log(style(c.cyan, " npx novaui add card"))
212
418
  console.log("")
213
419
  }
214
420
 
@@ -226,13 +432,16 @@ async function add(componentName) {
226
432
 
227
433
  // 1. Fetch registry
228
434
  console.log(" Fetching registry...")
229
- const registryResponse = await fetch(`${BASE_URL}/registry.json`)
435
+ const registryResponse = await fetchWithTimeout(`${BASE_URL}/registry.json`)
230
436
 
231
437
  if (!registryResponse.ok) {
232
438
  throw new Error(`Failed to fetch registry: ${registryResponse.statusText}`)
233
439
  }
234
440
 
235
441
  const registry = await registryResponse.json()
442
+ if (!registry || typeof registry !== "object" || Array.isArray(registry)) {
443
+ throw new Error("Registry response is not a valid object.")
444
+ }
236
445
 
237
446
  if (!registry[componentName]) {
238
447
  console.error(` ✗ Component "${componentName}" not found in registry.`)
@@ -242,28 +451,36 @@ async function add(componentName) {
242
451
  process.exit(1)
243
452
  }
244
453
 
245
- const config = registry[componentName]
246
- const targetBaseDir = path.join(cwd, "src", "components", "ui")
454
+ const componentConfig = registry[componentName]
455
+ assertValidComponentConfig(componentName, componentConfig)
456
+ const projectConfig = loadConfig(cwd) || DEFAULT_CONFIG
457
+ const targetBaseDir = path.join(cwd, projectConfig.componentsUi)
247
458
  ensureDir(targetBaseDir)
248
459
 
249
- // 2. Ensure utils.ts exists
250
- const utilsDir = path.join(cwd, "src", "lib")
460
+ // 2. Ensure utils.ts exists (in configured lib dir)
461
+ const utilsDir = path.join(cwd, projectConfig.lib)
251
462
  const utilsPath = path.join(utilsDir, "utils.ts")
252
463
 
253
464
  if (!fs.existsSync(utilsPath)) {
254
465
  ensureDir(utilsDir)
255
466
  fs.writeFileSync(utilsPath, UTILS_CONTENT)
256
- console.log(" ✓ Created src/lib/utils.ts")
467
+ console.log(` ✓ Created ${projectConfig.lib}/utils.ts`)
468
+ }
469
+
470
+ if (!loadConfig(cwd)) {
471
+ console.log("")
472
+ console.log(" ℹ No components.json found. Using default paths. Run 'npx novaui init' to customize.")
473
+ console.log("")
257
474
  }
258
475
 
259
476
  // 3. Fetch and write component files
260
- for (const file of config.files) {
477
+ for (const file of componentConfig.files) {
261
478
  const fileUrl = `${BASE_URL}/${file}`
262
479
  const fileName = path.basename(file)
263
480
  const destPath = path.join(targetBaseDir, fileName)
264
481
 
265
482
  console.log(` Downloading ${fileName}...`)
266
- const fileResponse = await fetch(fileUrl)
483
+ const fileResponse = await fetchWithTimeout(fileUrl)
267
484
 
268
485
  if (!fileResponse.ok) {
269
486
  console.error(` ✗ Failed to download ${fileName}`)
@@ -271,21 +488,24 @@ async function add(componentName) {
271
488
  }
272
489
 
273
490
  const content = await fileResponse.text()
274
- fs.writeFileSync(destPath, content)
491
+ fs.writeFileSync(destPath, content, "utf8")
275
492
  console.log(` ✓ Added ${fileName}`)
276
493
  }
277
494
 
278
- // 4. Install component dependencies
279
- if (config.dependencies && config.dependencies.length > 0) {
280
- console.log("")
281
- console.log(` Installing dependencies: ${config.dependencies.join(", ")}...`)
282
- try {
283
- const installCmd = detectPackageManager()
284
- execSync(`${installCmd} ${config.dependencies.join(" ")}`, {
285
- stdio: "inherit",
286
- })
287
- } catch {
288
- console.error(" ✗ Failed to install dependencies automatically.")
495
+ // 4. Install component dependencies (only those not already in package.json)
496
+ if (componentConfig.dependencies && componentConfig.dependencies.length > 0) {
497
+ const missingDeps = getMissingDeps(cwd, componentConfig.dependencies)
498
+ if (missingDeps.length === 0) {
499
+ console.log("")
500
+ console.log(" ✓ Component dependencies already in package.json, skipping install.")
501
+ } else {
502
+ console.log("")
503
+ console.log(` Installing dependencies: ${missingDeps.join(", ")}...`)
504
+ try {
505
+ installPackages(missingDeps)
506
+ } catch {
507
+ console.error(" ✗ Failed to install dependencies automatically.")
508
+ }
289
509
  }
290
510
  }
291
511
 
@@ -295,20 +515,25 @@ async function add(componentName) {
295
515
  }
296
516
 
297
517
  function showHelp() {
518
+ console.log(ASCII_BANNER)
519
+ console.log(style(c.dim, ` Version: ${getCliVersion()}`))
298
520
  console.log("")
299
- console.log(" NovaUI CLI")
300
- console.log("")
301
- console.log(" Usage:")
302
- console.log(" novaui init Initialize project with Tailwind config, global.css, and utils")
303
- console.log(" novaui add <component> Add a component to your project")
521
+ console.log(style(c.bold, " Usage"))
522
+ console.log(style(c.dim, " novaui init Set up NovaUI (config, Tailwind, global.css, utils)"))
523
+ console.log(style(c.dim, " novaui add <component> Add a component (e.g. button, card)"))
524
+ console.log(style(c.dim, " novaui --version Show CLI version"))
304
525
  console.log("")
305
- console.log(" Examples:")
306
- console.log(" npx novaui-cli init")
307
- console.log(" npx novaui-cli add button")
308
- console.log(" npx novaui-cli add card")
526
+ console.log(style(c.bold, " Examples"))
527
+ console.log(style(c.cyan, " npx novaui init"))
528
+ console.log(style(c.cyan, " npx novaui add button"))
529
+ console.log(style(c.cyan, " npx novaui add card"))
309
530
  console.log("")
310
531
  }
311
532
 
533
+ function showVersion() {
534
+ console.log(getCliVersion())
535
+ }
536
+
312
537
  // ─── Main ───────────────────────────────────────────────────────────────────
313
538
 
314
539
  const command = process.argv[2]
@@ -327,6 +552,10 @@ async function main() {
327
552
  case "-h":
328
553
  showHelp()
329
554
  break
555
+ case "--version":
556
+ case "-v":
557
+ showVersion()
558
+ break
330
559
  default:
331
560
  if (command) {
332
561
  // Backwards compatible: treat unknown command as component name
@@ -338,7 +567,7 @@ async function main() {
338
567
  }
339
568
  } catch (error) {
340
569
  console.error("")
341
- console.error(` ✗ Error: ${error.message}`)
570
+ console.error(` ✗ Error: ${formatError(error)}`)
342
571
  process.exit(1)
343
572
  }
344
573
  }