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/README.md +111 -0
- package/package.json +40 -13
- package/src/__tests__/commands.test.js +491 -0
- package/src/__tests__/fuzzy.test.js +150 -0
- package/src/__tests__/helpers.test.js +520 -0
- package/src/__tests__/preflight.test.js +379 -0
- package/src/__tests__/version-check.test.js +58 -0
- package/src/bin.js +103 -590
- package/src/commands/add.js +168 -0
- package/src/commands/init.js +330 -0
- package/src/constants.js +101 -0
- package/src/themes/default.js +51 -0
- package/src/themes/index.js +48 -0
- package/src/themes/ocean.js +51 -0
- package/src/themes/sunset.js +51 -0
- package/src/utils/config.js +22 -0
- package/src/utils/deps.js +40 -0
- package/src/utils/fetch.js +21 -0
- package/src/utils/fs-helpers.js +18 -0
- package/src/utils/fuzzy.js +99 -0
- package/src/utils/preflight.js +172 -0
- package/src/utils/tailwind.js +60 -0
- package/src/utils/validate.js +17 -0
- package/src/utils/version-check.js +136 -0
- package/src/utils/version.js +17 -0
package/src/bin.js
CHANGED
|
@@ -1,620 +1,133 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
|
-
import
|
|
4
|
-
import
|
|
5
|
-
import
|
|
6
|
-
import
|
|
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
|
-
|
|
10
|
-
"https://raw.githubusercontent.com/KaloyanBehov/novaui/main"
|
|
8
|
+
// ─── Module imports ──────────────────────────────────────────────────────────
|
|
11
9
|
|
|
12
|
-
|
|
13
|
-
|
|
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
|
-
|
|
16
|
-
const __dirname = path.dirname(__filename)
|
|
17
|
-
const CLI_PACKAGE_JSON_PATH = path.resolve(__dirname, "../package.json")
|
|
23
|
+
// ─── Derived constants ───────────────────────────────────────────────────────
|
|
18
24
|
|
|
19
|
-
|
|
20
|
-
|
|
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
|
-
|
|
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
|
-
|
|
31
|
+
add,
|
|
32
|
+
askTheme,
|
|
33
|
+
assertValidComponentConfig,
|
|
550
34
|
CONFIG_FILENAME,
|
|
551
|
-
|
|
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
|
-
|
|
567
|
-
|
|
46
|
+
loadConfig,
|
|
47
|
+
UTILS_CONTENT,
|
|
48
|
+
writeConfig,
|
|
49
|
+
writeIfNotExists,
|
|
50
|
+
};
|
|
568
51
|
|
|
569
|
-
// ───
|
|
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(
|
|
59
|
+
return process.argv[1].endsWith('/bin.js');
|
|
577
60
|
}
|
|
578
|
-
})()
|
|
61
|
+
})();
|
|
579
62
|
|
|
580
63
|
if (isDirectRun) {
|
|
581
|
-
const
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
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
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
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
|
-
|
|
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
|
-
}
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
// ─── Default: show help when no command given ─────────────────────────────
|
|
123
|
+
|
|
124
|
+
program.action(() => {
|
|
125
|
+
program.help();
|
|
126
|
+
});
|
|
618
127
|
|
|
619
|
-
|
|
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
|
}
|