novaui-cli 1.0.4 → 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 +98 -18
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "novaui-cli",
3
- "version": "1.0.4",
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
@@ -3,12 +3,18 @@
3
3
  import fs from "node:fs"
4
4
  import path from "node:path"
5
5
  import readline from "node:readline"
6
- import { execSync } from "node:child_process"
6
+ import { execFileSync } from "node:child_process"
7
+ import { fileURLToPath } from "node:url"
7
8
 
8
9
  const BASE_URL =
9
10
  "https://raw.githubusercontent.com/KaloyanBehov/native-ui/main"
10
11
 
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")
12
18
 
13
19
  const DEFAULT_CONFIG = {
14
20
  globalCss: "src/global.css",
@@ -156,6 +162,7 @@ module.exports = {
156
162
  plugins: [],
157
163
  };
158
164
  `
165
+ }
159
166
 
160
167
  // ─── Utils ──────────────────────────────────────────────────────────────────
161
168
 
@@ -171,10 +178,21 @@ export function cn(...inputs: ClassValue[]) {
171
178
 
172
179
  function detectPackageManager() {
173
180
  const userAgent = process.env.npm_config_user_agent || ""
174
- if (userAgent.startsWith("yarn")) return "yarn add"
175
- if (userAgent.startsWith("pnpm")) return "pnpm add"
176
- if (userAgent.startsWith("bun")) return "bun add"
177
- 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(" ")}`
178
196
  }
179
197
 
180
198
  function ensureDir(dir) {
@@ -188,7 +206,7 @@ function writeIfNotExists(filePath, content, label) {
188
206
  console.log(style(c.dim, ` ℹ ${label} already exists, skipping.`))
189
207
  return false
190
208
  }
191
- fs.writeFileSync(filePath, content)
209
+ fs.writeFileSync(filePath, content, "utf8")
192
210
  console.log(style(c.green, ` ✓ Created ${label}`))
193
211
  return true
194
212
  }
@@ -209,6 +227,9 @@ function getMissingDeps(cwd, deps) {
209
227
 
210
228
  /** Ask user a question; returns trimmed answer or default. */
211
229
  function ask(question, defaultAnswer = "") {
230
+ if (process.stdin.isTTY !== true) {
231
+ return Promise.resolve(defaultAnswer)
232
+ }
212
233
  const rl = readline.createInterface({ input: process.stdin, output: process.stdout })
213
234
  const defaultPart = defaultAnswer ? style(c.dim, ` (${defaultAnswer})`) : ""
214
235
  const prompt = style(c.cyan, " ? ") + question + defaultPart + style(c.dim, " ")
@@ -236,7 +257,55 @@ function loadConfig(cwd) {
236
257
  /** Write components.json to cwd. */
237
258
  function writeConfig(cwd, config) {
238
259
  const configPath = path.join(cwd, CONFIG_FILENAME)
239
- fs.writeFileSync(configPath, JSON.stringify(config, null, 2))
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)
240
309
  }
241
310
 
242
311
  // ─── Commands ───────────────────────────────────────────────────────────────
@@ -323,13 +392,12 @@ async function init() {
323
392
  } else {
324
393
  console.log(style(c.dim, ` Installing: ${missingDeps.join(", ")}`))
325
394
  console.log("")
326
- const installCmd = detectPackageManager()
327
395
  try {
328
- execSync(`${installCmd} ${missingDeps.join(" ")}`, { stdio: "inherit" })
396
+ installPackages(missingDeps)
329
397
  } catch {
330
398
  console.error("")
331
399
  console.error(style(c.yellow, " ✗ Install failed. Run manually:"))
332
- console.error(style(c.dim, ` ${installCmd} ${missingDeps.join(" ")}`))
400
+ console.error(style(c.dim, ` ${getInstallHint(missingDeps)}`))
333
401
  }
334
402
  }
335
403
 
@@ -364,13 +432,16 @@ async function add(componentName) {
364
432
 
365
433
  // 1. Fetch registry
366
434
  console.log(" Fetching registry...")
367
- const registryResponse = await fetch(`${BASE_URL}/registry.json`)
435
+ const registryResponse = await fetchWithTimeout(`${BASE_URL}/registry.json`)
368
436
 
369
437
  if (!registryResponse.ok) {
370
438
  throw new Error(`Failed to fetch registry: ${registryResponse.statusText}`)
371
439
  }
372
440
 
373
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
+ }
374
445
 
375
446
  if (!registry[componentName]) {
376
447
  console.error(` ✗ Component "${componentName}" not found in registry.`)
@@ -381,6 +452,7 @@ async function add(componentName) {
381
452
  }
382
453
 
383
454
  const componentConfig = registry[componentName]
455
+ assertValidComponentConfig(componentName, componentConfig)
384
456
  const projectConfig = loadConfig(cwd) || DEFAULT_CONFIG
385
457
  const targetBaseDir = path.join(cwd, projectConfig.componentsUi)
386
458
  ensureDir(targetBaseDir)
@@ -408,7 +480,7 @@ async function add(componentName) {
408
480
  const destPath = path.join(targetBaseDir, fileName)
409
481
 
410
482
  console.log(` Downloading ${fileName}...`)
411
- const fileResponse = await fetch(fileUrl)
483
+ const fileResponse = await fetchWithTimeout(fileUrl)
412
484
 
413
485
  if (!fileResponse.ok) {
414
486
  console.error(` ✗ Failed to download ${fileName}`)
@@ -416,7 +488,7 @@ async function add(componentName) {
416
488
  }
417
489
 
418
490
  const content = await fileResponse.text()
419
- fs.writeFileSync(destPath, content)
491
+ fs.writeFileSync(destPath, content, "utf8")
420
492
  console.log(` ✓ Added ${fileName}`)
421
493
  }
422
494
 
@@ -430,10 +502,7 @@ async function add(componentName) {
430
502
  console.log("")
431
503
  console.log(` Installing dependencies: ${missingDeps.join(", ")}...`)
432
504
  try {
433
- const installCmd = detectPackageManager()
434
- execSync(`${installCmd} ${missingDeps.join(" ")}`, {
435
- stdio: "inherit",
436
- })
505
+ installPackages(missingDeps)
437
506
  } catch {
438
507
  console.error(" ✗ Failed to install dependencies automatically.")
439
508
  }
@@ -447,9 +516,12 @@ async function add(componentName) {
447
516
 
448
517
  function showHelp() {
449
518
  console.log(ASCII_BANNER)
519
+ console.log(style(c.dim, ` Version: ${getCliVersion()}`))
520
+ console.log("")
450
521
  console.log(style(c.bold, " Usage"))
451
522
  console.log(style(c.dim, " novaui init Set up NovaUI (config, Tailwind, global.css, utils)"))
452
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"))
453
525
  console.log("")
454
526
  console.log(style(c.bold, " Examples"))
455
527
  console.log(style(c.cyan, " npx novaui init"))
@@ -458,6 +530,10 @@ function showHelp() {
458
530
  console.log("")
459
531
  }
460
532
 
533
+ function showVersion() {
534
+ console.log(getCliVersion())
535
+ }
536
+
461
537
  // ─── Main ───────────────────────────────────────────────────────────────────
462
538
 
463
539
  const command = process.argv[2]
@@ -476,6 +552,10 @@ async function main() {
476
552
  case "-h":
477
553
  showHelp()
478
554
  break
555
+ case "--version":
556
+ case "-v":
557
+ showVersion()
558
+ break
479
559
  default:
480
560
  if (command) {
481
561
  // Backwards compatible: treat unknown command as component name
@@ -487,7 +567,7 @@ async function main() {
487
567
  }
488
568
  } catch (error) {
489
569
  console.error("")
490
- console.error(` ✗ Error: ${error.message}`)
570
+ console.error(` ✗ Error: ${formatError(error)}`)
491
571
  process.exit(1)
492
572
  }
493
573
  }