novaui-cli 1.0.4 → 1.0.6

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 (3) hide show
  1. package/README.md +118 -0
  2. package/package.json +3 -2
  3. package/src/bin.js +98 -18
package/README.md ADDED
@@ -0,0 +1,118 @@
1
+ # novaui-cli
2
+
3
+ The CLI for **NovaUI** -- a React Native UI component library with 50+ components built on [NativeWind](https://www.nativewind.dev/) (Tailwind CSS for React Native).
4
+
5
+ Inspired by [shadcn/ui](https://ui.shadcn.com), adapted for mobile. Components are copied directly into your project -- you own the code, customize anything.
6
+
7
+ ## Getting Started
8
+
9
+ ### Initialize your project
10
+
11
+ ```bash
12
+ npx novaui-cli init
13
+ ```
14
+
15
+ The interactive setup walks you through configuring file paths:
16
+
17
+ ```
18
+ ? Path for global.css? (src/global.css)
19
+ ? Path for UI components? (src/components/ui)
20
+ ? Path for lib (utils)? (src/lib)
21
+ ```
22
+
23
+ This creates:
24
+
25
+ - **`components.json`** -- stores your configured paths
26
+ - **`tailwind.config.js`** -- NovaUI theme with colors, border radius, and NativeWind preset
27
+ - **`global.css`** -- light and dark theme CSS variables
28
+ - **`lib/utils.ts`** -- the `cn()` class merging utility
29
+
30
+ It also installs the required dependencies: `nativewind`, `tailwindcss`, `clsx`, `tailwind-merge`, `class-variance-authority`.
31
+
32
+ ### Add components
33
+
34
+ ```bash
35
+ npx novaui-cli add button
36
+ npx novaui-cli add card
37
+ npx novaui-cli add dialog
38
+ ```
39
+
40
+ Components are copied into your configured directory (default: `src/components/ui/`). Each component's dependencies are installed automatically.
41
+
42
+ ### Use them
43
+
44
+ ```tsx
45
+ import { Button } from "./src/components/ui/button"
46
+ import { Card, CardHeader, CardTitle, CardContent } from "./src/components/ui/card"
47
+
48
+ export default function App() {
49
+ return (
50
+ <Card>
51
+ <CardHeader>
52
+ <CardTitle>Welcome to NovaUI</CardTitle>
53
+ </CardHeader>
54
+ <CardContent>
55
+ <Button onPress={() => console.log("Pressed!")}>
56
+ Get Started
57
+ </Button>
58
+ </CardContent>
59
+ </Card>
60
+ )
61
+ }
62
+ ```
63
+
64
+ Don't forget to import your `global.css` in your root entry file (e.g. `App.tsx`):
65
+
66
+ ```tsx
67
+ import "./src/global.css"
68
+ ```
69
+
70
+ ## Commands
71
+
72
+ | Command | Description |
73
+ |---|---|
74
+ | `npx novaui-cli init` | Interactive setup -- config, Tailwind, global CSS, utils |
75
+ | `npx novaui-cli add <name>` | Add a component to your project |
76
+ | `npx novaui-cli --version` | Show CLI version |
77
+ | `npx novaui-cli --help` | Show help |
78
+
79
+ ## Available Components
80
+
81
+ Accordion, Alert, Alert Dialog, Aspect Ratio, Avatar, Badge, Breadcrumb, Button, Button Group, Calendar, Card, Carousel, Chart, Checkbox, Collapsible, Combobox, Command, Context Menu, Dialog, Drawer, Dropdown Menu, Empty, Field, Hover Card, Input, Input Group, Input OTP, Item, Label, Menubar, Navigation Menu, Pagination, Popover, Progress, Radio Group, Resizable, Scroll Area, Select, Separator, Sheet, Skeleton, Spinner, Switch, Table, Tabs, Text, Textarea, Toggle, Toggle Group, Typography
82
+
83
+ ```bash
84
+ npx novaui-cli add <name>
85
+ ```
86
+
87
+ Use the component filename without the extension: `button`, `card`, `alert-dialog`, `dropdown-menu`, etc.
88
+
89
+ ## Configuration
90
+
91
+ Running `init` creates a `components.json` in your project root:
92
+
93
+ ```json
94
+ {
95
+ "globalCss": "src/global.css",
96
+ "componentsUi": "src/components/ui",
97
+ "lib": "src/lib"
98
+ }
99
+ ```
100
+
101
+ Edit this file or run `npx novaui-cli init` again to change paths.
102
+
103
+ ## Requirements
104
+
105
+ | Package | Version |
106
+ |---|---|
107
+ | react | >= 18 |
108
+ | react-native | >= 0.72 |
109
+ | nativewind | >= 4 |
110
+ | tailwindcss | >= 3 |
111
+
112
+ ## Links
113
+
114
+ - [GitHub](https://github.com/KaloyanBehov/native-ui)
115
+
116
+ ## License
117
+
118
+ MIT
package/package.json CHANGED
@@ -1,13 +1,14 @@
1
1
  {
2
2
  "name": "novaui-cli",
3
- "version": "1.0.4",
3
+ "version": "1.0.6",
4
4
  "description": "CLI for NovaUI - React Native component library with NativeWind",
5
5
  "type": "module",
6
6
  "bin": {
7
7
  "novaui": "./src/bin.js"
8
8
  },
9
9
  "files": [
10
- "src"
10
+ "src",
11
+ "README.md"
11
12
  ],
12
13
  "keywords": [
13
14
  "novaui",
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
  }