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.
@@ -0,0 +1,51 @@
1
+ export const OCEAN_THEME_CSS = `@tailwind base;
2
+ @tailwind components;
3
+ @tailwind utilities;
4
+
5
+ @layer base {
6
+ :root {
7
+ --background: 210 40% 98%;
8
+ --foreground: 210 30% 16%;
9
+ --card: 0 0% 100%;
10
+ --card-foreground: 210 30% 16%;
11
+ --popover: 0 0% 100%;
12
+ --popover-foreground: 210 30% 16%;
13
+ --primary: 200 95% 46%;
14
+ --primary-foreground: 0 0% 100%;
15
+ --secondary: 197 46% 92%;
16
+ --secondary-foreground: 210 25% 24%;
17
+ --muted: 197 46% 92%;
18
+ --muted-foreground: 210 16% 42%;
19
+ --accent: 189 70% 88%;
20
+ --accent-foreground: 210 28% 20%;
21
+ --destructive: 0 75% 55%;
22
+ --destructive-foreground: 0 0% 100%;
23
+ --border: 197 30% 84%;
24
+ --input: 197 30% 84%;
25
+ --ring: 200 95% 46%;
26
+ --radius: 0.75rem;
27
+ }
28
+
29
+ .dark {
30
+ --background: 212 48% 10%;
31
+ --foreground: 210 20% 94%;
32
+ --card: 212 45% 13%;
33
+ --card-foreground: 210 20% 94%;
34
+ --popover: 212 45% 13%;
35
+ --popover-foreground: 210 20% 94%;
36
+ --primary: 196 100% 60%;
37
+ --primary-foreground: 210 50% 12%;
38
+ --secondary: 212 30% 20%;
39
+ --secondary-foreground: 210 20% 94%;
40
+ --muted: 212 30% 20%;
41
+ --muted-foreground: 209 15% 68%;
42
+ --accent: 202 45% 24%;
43
+ --accent-foreground: 210 20% 94%;
44
+ --destructive: 0 65% 45%;
45
+ --destructive-foreground: 0 0% 100%;
46
+ --border: 212 28% 24%;
47
+ --input: 212 28% 24%;
48
+ --ring: 196 100% 60%;
49
+ }
50
+ }
51
+ `
@@ -0,0 +1,51 @@
1
+ export const SUNSET_THEME_CSS = `@tailwind base;
2
+ @tailwind components;
3
+ @tailwind utilities;
4
+
5
+ @layer base {
6
+ :root {
7
+ --background: 24 100% 98%;
8
+ --foreground: 12 30% 18%;
9
+ --card: 0 0% 100%;
10
+ --card-foreground: 12 30% 18%;
11
+ --popover: 0 0% 100%;
12
+ --popover-foreground: 12 30% 18%;
13
+ --primary: 18 95% 53%;
14
+ --primary-foreground: 0 0% 100%;
15
+ --secondary: 22 78% 92%;
16
+ --secondary-foreground: 16 32% 26%;
17
+ --muted: 22 78% 92%;
18
+ --muted-foreground: 18 20% 46%;
19
+ --accent: 340 84% 93%;
20
+ --accent-foreground: 336 32% 28%;
21
+ --destructive: 0 73% 54%;
22
+ --destructive-foreground: 0 0% 100%;
23
+ --border: 20 58% 84%;
24
+ --input: 20 58% 84%;
25
+ --ring: 18 95% 53%;
26
+ --radius: 0.75rem;
27
+ }
28
+
29
+ .dark {
30
+ --background: 16 46% 12%;
31
+ --foreground: 24 60% 95%;
32
+ --card: 15 42% 16%;
33
+ --card-foreground: 24 60% 95%;
34
+ --popover: 15 42% 16%;
35
+ --popover-foreground: 24 60% 95%;
36
+ --primary: 24 100% 64%;
37
+ --primary-foreground: 16 46% 12%;
38
+ --secondary: 11 28% 24%;
39
+ --secondary-foreground: 24 60% 95%;
40
+ --muted: 11 28% 24%;
41
+ --muted-foreground: 20 24% 72%;
42
+ --accent: 332 42% 30%;
43
+ --accent-foreground: 24 60% 95%;
44
+ --destructive: 0 62% 45%;
45
+ --destructive-foreground: 0 0% 100%;
46
+ --border: 14 24% 28%;
47
+ --input: 14 24% 28%;
48
+ --ring: 24 100% 64%;
49
+ }
50
+ }
51
+ `
@@ -0,0 +1,22 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import { CONFIG_FILENAME, DEFAULT_CONFIG } from '../constants.js';
4
+
5
+ /** Load components.json from cwd; returns null if missing or invalid. */
6
+ export function loadConfig(cwd) {
7
+ const configPath = path.join(cwd, CONFIG_FILENAME);
8
+ if (!fs.existsSync(configPath)) return null;
9
+ try {
10
+ const raw = JSON.parse(fs.readFileSync(configPath, 'utf8'));
11
+ if (!raw || typeof raw !== 'object' || Array.isArray(raw)) return null;
12
+ return { ...DEFAULT_CONFIG, ...raw };
13
+ } catch {
14
+ return null;
15
+ }
16
+ }
17
+
18
+ /** Write components.json to cwd. */
19
+ export function writeConfig(cwd, config) {
20
+ const configPath = path.join(cwd, CONFIG_FILENAME);
21
+ fs.writeFileSync(configPath, JSON.stringify(config, null, 2), 'utf8');
22
+ }
@@ -0,0 +1,40 @@
1
+ import { execFileSync } from 'node:child_process';
2
+ import fs from 'node:fs';
3
+ import path from 'node:path';
4
+
5
+ export function detectPackageManager() {
6
+ const userAgent = process.env.npm_config_user_agent || '';
7
+ if (userAgent.startsWith('yarn')) return { command: 'yarn', baseArgs: ['add'], devFlag: '--dev' };
8
+ if (userAgent.startsWith('pnpm')) return { command: 'pnpm', baseArgs: ['add'], devFlag: '--save-dev' };
9
+ if (userAgent.startsWith('bun')) return { command: 'bun', baseArgs: ['add'], devFlag: '--dev' };
10
+ return { command: 'npm', baseArgs: ['install'], devFlag: '--save-dev' };
11
+ }
12
+
13
+ export function installPackages(packages, options = {}) {
14
+ if (!Array.isArray(packages) || packages.length === 0) return;
15
+ const { dev = false } = options;
16
+ const { command, baseArgs, devFlag } = detectPackageManager();
17
+ const args = dev ? [...baseArgs, devFlag, ...packages] : [...baseArgs, ...packages];
18
+ execFileSync(command, args, { stdio: 'inherit' });
19
+ }
20
+
21
+ export function getInstallHint(packages, options = {}) {
22
+ const { dev = false } = options;
23
+ const { command, baseArgs, devFlag } = detectPackageManager();
24
+ const args = dev ? [...baseArgs, devFlag, ...packages] : [...baseArgs, ...packages];
25
+ return `${command} ${args.join(' ')}`;
26
+ }
27
+
28
+ /** Returns which of the requested deps are not listed in package.json (dependencies or devDependencies). */
29
+ export function getMissingDeps(cwd, deps) {
30
+ const pkgPath = path.join(cwd, 'package.json');
31
+ if (!fs.existsSync(pkgPath)) {
32
+ return [...deps];
33
+ }
34
+ const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf8'));
35
+ const installed = new Set([
36
+ ...Object.keys(pkg.dependencies || {}),
37
+ ...Object.keys(pkg.devDependencies || {}),
38
+ ]);
39
+ return deps.filter((d) => !installed.has(d));
40
+ }
@@ -0,0 +1,21 @@
1
+ import { FETCH_TIMEOUT_MS } from '../constants.js';
2
+
3
+ export async function fetchWithTimeout(url) {
4
+ const controller = new AbortController();
5
+ const timeout = setTimeout(() => controller.abort(), FETCH_TIMEOUT_MS);
6
+ try {
7
+ return await fetch(url, { signal: controller.signal });
8
+ } catch (error) {
9
+ if (error && error.name === 'AbortError') {
10
+ throw new Error(`Request timed out after ${FETCH_TIMEOUT_MS}ms: ${url}`);
11
+ }
12
+ throw error;
13
+ } finally {
14
+ clearTimeout(timeout);
15
+ }
16
+ }
17
+
18
+ export function formatError(error) {
19
+ if (error instanceof Error && error.message) return error.message;
20
+ return String(error);
21
+ }
@@ -0,0 +1,18 @@
1
+ import fs from 'node:fs';
2
+ import pc from 'picocolors';
3
+
4
+ export function ensureDir(dir) {
5
+ if (!fs.existsSync(dir)) {
6
+ fs.mkdirSync(dir, { recursive: true });
7
+ }
8
+ }
9
+
10
+ export function writeIfNotExists(filePath, content, label) {
11
+ if (fs.existsSync(filePath)) {
12
+ console.log(pc.dim(` ℹ ${label} already exists, skipping.`));
13
+ return false;
14
+ }
15
+ fs.writeFileSync(filePath, content, 'utf8');
16
+ console.log(pc.green(` ✓ Created ${label}`));
17
+ return true;
18
+ }
@@ -0,0 +1,99 @@
1
+ /**
2
+ * Fuzzy matching utilities for better error messages
3
+ * Helps users find components when they mistype names
4
+ */
5
+
6
+ /**
7
+ * Calculate Levenshtein distance between two strings
8
+ * Used for finding similar component names
9
+ */
10
+ function levenshteinDistance(str1, str2) {
11
+ const len1 = str1.length
12
+ const len2 = str2.length
13
+ const matrix = []
14
+
15
+ for (let i = 0; i <= len1; i++) {
16
+ matrix[i] = [i]
17
+ }
18
+
19
+ for (let j = 0; j <= len2; j++) {
20
+ matrix[0][j] = j
21
+ }
22
+
23
+ for (let i = 1; i <= len1; i++) {
24
+ for (let j = 1; j <= len2; j++) {
25
+ const cost = str1[i - 1] === str2[j - 1] ? 0 : 1
26
+ matrix[i][j] = Math.min(
27
+ matrix[i - 1][j] + 1,
28
+ matrix[i][j - 1] + 1,
29
+ matrix[i - 1][j - 1] + cost
30
+ )
31
+ }
32
+ }
33
+
34
+ return matrix[len1][len2]
35
+ }
36
+
37
+ /**
38
+ * Find similar component names based on typo distance
39
+ * Returns up to maxResults similar names, sorted by similarity
40
+ */
41
+ export function findSimilarComponents(input, availableComponents, maxResults = 5) {
42
+ const normalizedInput = input.toLowerCase().trim()
43
+
44
+ const scored = availableComponents
45
+ .map(name => ({
46
+ name,
47
+ distance: levenshteinDistance(normalizedInput, name.toLowerCase()),
48
+ startsWithSame: name.toLowerCase().startsWith(normalizedInput[0]),
49
+ }))
50
+ .filter(item => item.distance <= 4) // Only suggest if reasonably close
51
+ .sort((a, b) => {
52
+ if (a.distance !== b.distance) {
53
+ return a.distance - b.distance
54
+ }
55
+ if (a.startsWithSame !== b.startsWithSame) {
56
+ return a.startsWithSame ? -1 : 1
57
+ }
58
+ return a.name.localeCompare(b.name)
59
+ })
60
+ .slice(0, maxResults)
61
+ .map(item => item.name)
62
+
63
+ return scored
64
+ }
65
+
66
+ /**
67
+ * Format a helpful error message for missing components
68
+ */
69
+ export function formatComponentNotFoundError(componentName, availableComponents) {
70
+ const similar = findSimilarComponents(componentName, availableComponents)
71
+
72
+ let message = `Component "${componentName}" not found in registry.`
73
+
74
+ if (similar.length > 0) {
75
+ message += '\n\n Did you mean one of these?'
76
+ similar.forEach(name => {
77
+ message += `\n • ${name}`
78
+ })
79
+ }
80
+
81
+ message += '\n\n Available components:'
82
+
83
+ const sorted = [...availableComponents].sort()
84
+ const columns = 3
85
+ const itemsPerColumn = Math.ceil(sorted.length / columns)
86
+
87
+ for (let i = 0; i < itemsPerColumn; i++) {
88
+ let row = ' '
89
+ for (let col = 0; col < columns; col++) {
90
+ const idx = i + col * itemsPerColumn
91
+ if (idx < sorted.length) {
92
+ row += sorted[idx].padEnd(20)
93
+ }
94
+ }
95
+ message += '\n' + row.trimEnd()
96
+ }
97
+
98
+ return message
99
+ }
@@ -0,0 +1,172 @@
1
+ import fs from 'node:fs'
2
+ import path from 'node:path'
3
+ import pc from 'picocolors'
4
+
5
+ /**
6
+ * Pre-flight checks to ensure the project environment is ready for NovaUI
7
+ */
8
+
9
+ export function checkPackageJson(cwd) {
10
+ const pkgPath = path.join(cwd, 'package.json')
11
+ if (!fs.existsSync(pkgPath)) {
12
+ throw new Error('package.json not found. Please run this command in a valid Node.js project.')
13
+ }
14
+
15
+ try {
16
+ const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf-8'))
17
+ return pkg
18
+ } catch (error) {
19
+ throw new Error('package.json is invalid or cannot be parsed.')
20
+ }
21
+ }
22
+
23
+ export function checkReactNativeProject(pkg) {
24
+ const deps = {
25
+ ...pkg.dependencies,
26
+ ...pkg.devDependencies,
27
+ }
28
+
29
+ if (!deps['react-native'] && !deps['expo']) {
30
+ console.log('')
31
+ console.log(pc.yellow(' ⚠ Warning: react-native or expo not found in dependencies.'))
32
+ console.log(pc.dim(' NovaUI is designed for React Native projects.'))
33
+ console.log('')
34
+ }
35
+ }
36
+
37
+ export function checkBabelConfig(cwd) {
38
+ const babelPaths = [
39
+ path.join(cwd, 'babel.config.js'),
40
+ path.join(cwd, 'babel.config.json'),
41
+ path.join(cwd, '.babelrc'),
42
+ path.join(cwd, '.babelrc.js'),
43
+ ]
44
+
45
+ for (const babelPath of babelPaths) {
46
+ if (fs.existsSync(babelPath)) {
47
+ return babelPath
48
+ }
49
+ }
50
+
51
+ return null
52
+ }
53
+
54
+ export function checkNativeWindInBabel(cwd) {
55
+ const babelPath = checkBabelConfig(cwd)
56
+
57
+ if (!babelPath) {
58
+ console.log('')
59
+ console.log(pc.yellow(' ⚠ Warning: babel.config.js not found.'))
60
+ console.log(pc.dim(' NativeWind requires Babel configuration.'))
61
+ console.log('')
62
+ return false
63
+ }
64
+
65
+ try {
66
+ const content = fs.readFileSync(babelPath, 'utf-8')
67
+ const hasNativeWind = content.includes('nativewind/babel') || content.includes('"nativewind/babel"')
68
+
69
+ if (!hasNativeWind) {
70
+ console.log('')
71
+ console.log(pc.yellow(' ⚠ Warning: NativeWind not detected in Babel config.'))
72
+ console.log(pc.dim(' Add "nativewind/babel" to your Babel plugins:'))
73
+ console.log('')
74
+ console.log(pc.cyan(' module.exports = {'))
75
+ console.log(pc.cyan(' plugins: ["nativewind/babel"],'))
76
+ console.log(pc.cyan(' }'))
77
+ console.log('')
78
+ console.log(pc.dim(' See: https://www.nativewind.dev/docs/getting-started/installation'))
79
+ console.log('')
80
+ return false
81
+ }
82
+
83
+ return true
84
+ } catch (error) {
85
+ console.log('')
86
+ console.log(pc.yellow(' ⚠ Warning: Could not read babel.config.js'))
87
+ console.log('')
88
+ return false
89
+ }
90
+ }
91
+
92
+ export function checkReactNativeVersion(pkg) {
93
+ const deps = {
94
+ ...pkg.dependencies,
95
+ ...pkg.devDependencies,
96
+ }
97
+
98
+ const rnVersion = deps['react-native']
99
+ if (rnVersion) {
100
+ const versionMatch = rnVersion.match(/(\d+)\.(\d+)/)
101
+ if (versionMatch) {
102
+ const [, major, minor] = versionMatch
103
+ const majorNum = parseInt(major, 10)
104
+ const minorNum = parseInt(minor, 10)
105
+
106
+ if (majorNum < 0 || (majorNum === 0 && minorNum < 72)) {
107
+ console.log('')
108
+ console.log(pc.yellow(' ⚠ Warning: React Native version is below 0.72'))
109
+ console.log(pc.dim(' NovaUI requires React Native >= 0.72'))
110
+ console.log('')
111
+ return false
112
+ }
113
+ }
114
+ }
115
+
116
+ return true
117
+ }
118
+
119
+ export function checkNativeWindVersion(pkg) {
120
+ const deps = {
121
+ ...pkg.dependencies,
122
+ ...pkg.devDependencies,
123
+ }
124
+
125
+ const nwVersion = deps['nativewind']
126
+ if (!nwVersion) {
127
+ console.log('')
128
+ console.log(pc.yellow(' ⚠ Warning: NativeWind not found in package.json'))
129
+ console.log(pc.dim(' NovaUI requires NativeWind >= 4.0'))
130
+ console.log(pc.dim(' Install: npm install nativewind'))
131
+ console.log(pc.dim(' See: https://www.nativewind.dev/docs/getting-started/installation'))
132
+ console.log('')
133
+ return false
134
+ }
135
+
136
+ // Check if version 4 or higher
137
+ const versionMatch = nwVersion.match(/(\d+)/)
138
+ if (versionMatch) {
139
+ const [, major] = versionMatch
140
+ const majorNum = parseInt(major, 10)
141
+
142
+ if (majorNum < 4) {
143
+ console.log('')
144
+ console.log(pc.yellow(' ⚠ Warning: NativeWind version is below 4.0'))
145
+ console.log(pc.dim(' NovaUI requires NativeWind >= 4.0'))
146
+ console.log(pc.dim(' Upgrade: npm install nativewind@latest'))
147
+ console.log('')
148
+ return false
149
+ }
150
+ }
151
+
152
+ return true
153
+ }
154
+
155
+ /**
156
+ * Run all pre-flight checks before init command
157
+ */
158
+ export function runInitPreflightChecks(cwd) {
159
+ const pkg = checkPackageJson(cwd)
160
+ checkReactNativeProject(pkg)
161
+ checkNativeWindVersion(pkg)
162
+ checkNativeWindInBabel(cwd)
163
+ checkReactNativeVersion(pkg)
164
+ }
165
+
166
+ /**
167
+ * Run minimal pre-flight checks before add command
168
+ */
169
+ export function runAddPreflightChecks(cwd) {
170
+ const pkg = checkPackageJson(cwd)
171
+ return pkg
172
+ }
@@ -0,0 +1,60 @@
1
+ export function getTailwindConfigContent(config) {
2
+ const normalized = config.componentsUi.replace(/^\.\//, '')
3
+ const contentPaths = ['"./App.{js,jsx,ts,tsx}"', '"./src/**/*.{js,jsx,ts,tsx}"']
4
+ if (!normalized.startsWith('src/')) {
5
+ contentPaths.push(`"./${normalized}/**/*.{js,jsx,ts,tsx}"`)
6
+ }
7
+ return `/** @type {import('tailwindcss').Config} */
8
+ module.exports = {
9
+ content: [
10
+ ${contentPaths.join(',\n ')},
11
+ ],
12
+ presets: [require("nativewind/preset")],
13
+ theme: {
14
+ extend: {
15
+ colors: {
16
+ border: "hsl(var(--border))",
17
+ input: "hsl(var(--input))",
18
+ ring: "hsl(var(--ring))",
19
+ background: "hsl(var(--background))",
20
+ foreground: "hsl(var(--foreground))",
21
+ primary: {
22
+ DEFAULT: "hsl(var(--primary))",
23
+ foreground: "hsl(var(--primary-foreground))",
24
+ },
25
+ secondary: {
26
+ DEFAULT: "hsl(var(--secondary))",
27
+ foreground: "hsl(var(--secondary-foreground))",
28
+ },
29
+ destructive: {
30
+ DEFAULT: "hsl(var(--destructive))",
31
+ foreground: "hsl(var(--destructive-foreground))",
32
+ },
33
+ muted: {
34
+ DEFAULT: "hsl(var(--muted))",
35
+ foreground: "hsl(var(--muted-foreground))",
36
+ },
37
+ accent: {
38
+ DEFAULT: "hsl(var(--accent))",
39
+ foreground: "hsl(var(--accent-foreground))",
40
+ },
41
+ popover: {
42
+ DEFAULT: "hsl(var(--popover))",
43
+ foreground: "hsl(var(--popover-foreground))",
44
+ },
45
+ card: {
46
+ DEFAULT: "hsl(var(--card))",
47
+ foreground: "hsl(var(--card-foreground))",
48
+ },
49
+ },
50
+ borderRadius: {
51
+ lg: "var(--radius)",
52
+ md: "calc(var(--radius) - 2px)",
53
+ sm: "calc(var(--radius) - 4px)",
54
+ },
55
+ },
56
+ },
57
+ plugins: [],
58
+ };
59
+ `
60
+ }
@@ -0,0 +1,17 @@
1
+ export function assertValidComponentConfig(componentName, componentConfig) {
2
+ if (!componentConfig || typeof componentConfig !== 'object') {
3
+ throw new Error(`Registry entry for "${componentName}" is invalid.`)
4
+ }
5
+
6
+ const { files, dependencies } = componentConfig
7
+ if (!Array.isArray(files) || files.some(file => typeof file !== 'string' || file.trim() === '')) {
8
+ throw new Error(`Registry entry for "${componentName}" must include a valid "files" array.`)
9
+ }
10
+
11
+ if (
12
+ dependencies !== undefined &&
13
+ (!Array.isArray(dependencies) || dependencies.some(dep => typeof dep !== 'string' || dep.trim() === ''))
14
+ ) {
15
+ throw new Error(`Registry entry for "${componentName}" has an invalid "dependencies" array.`)
16
+ }
17
+ }
@@ -0,0 +1,136 @@
1
+ import pc from 'picocolors'
2
+ import { getCliVersion } from './version.js'
3
+ import { fetchWithTimeout } from './fetch.js'
4
+
5
+ const NPM_REGISTRY_URL = 'https://registry.npmjs.org/novaui-cli/latest'
6
+ const CHECK_INTERVAL_MS = 24 * 60 * 60 * 1000 // 24 hours
7
+ const CACHE_FILE = '.novaui-version-check'
8
+
9
+ /**
10
+ * Compare two semantic versions
11
+ * Returns: 1 if v1 > v2, -1 if v1 < v2, 0 if equal
12
+ */
13
+ function compareVersions(v1, v2) {
14
+ const parts1 = v1.split('.').map(Number)
15
+ const parts2 = v2.split('.').map(Number)
16
+
17
+ for (let i = 0; i < Math.max(parts1.length, parts2.length); i++) {
18
+ const part1 = parts1[i] || 0
19
+ const part2 = parts2[i] || 0
20
+
21
+ if (part1 > part2) return 1
22
+ if (part1 < part2) return -1
23
+ }
24
+
25
+ return 0
26
+ }
27
+
28
+ /**
29
+ * Fetch the latest version from npm registry
30
+ */
31
+ async function getLatestVersion() {
32
+ try {
33
+ const response = await fetchWithTimeout(NPM_REGISTRY_URL)
34
+ if (!response.ok) return null
35
+
36
+ const data = await response.json()
37
+ return data.version || null
38
+ } catch {
39
+ return null
40
+ }
41
+ }
42
+
43
+ /**
44
+ * Check if we should run the version check based on cache
45
+ */
46
+ function shouldCheckVersion(cwd) {
47
+ const fs = require('node:fs')
48
+ const path = require('node:path')
49
+ const os = require('node:os')
50
+
51
+ const cacheDir = path.join(os.homedir(), '.novaui')
52
+ const cachePath = path.join(cacheDir, CACHE_FILE)
53
+
54
+ if (!fs.existsSync(cachePath)) {
55
+ return true
56
+ }
57
+
58
+ try {
59
+ const cache = JSON.parse(fs.readFileSync(cachePath, 'utf-8'))
60
+ const lastCheck = new Date(cache.lastCheck).getTime()
61
+ const now = Date.now()
62
+
63
+ return (now - lastCheck) > CHECK_INTERVAL_MS
64
+ } catch {
65
+ return true
66
+ }
67
+ }
68
+
69
+ /**
70
+ * Update the version check cache
71
+ */
72
+ function updateCache(latestVersion) {
73
+ const fs = require('node:fs')
74
+ const path = require('node:path')
75
+ const os = require('node:os')
76
+
77
+ const cacheDir = path.join(os.homedir(), '.novaui')
78
+ if (!fs.existsSync(cacheDir)) {
79
+ fs.mkdirSync(cacheDir, { recursive: true })
80
+ }
81
+
82
+ const cachePath = path.join(cacheDir, CACHE_FILE)
83
+ const cache = {
84
+ lastCheck: new Date().toISOString(),
85
+ latestVersion,
86
+ }
87
+
88
+ fs.writeFileSync(cachePath, JSON.stringify(cache, null, 2), 'utf-8')
89
+ }
90
+
91
+ /**
92
+ * Check for CLI updates and warn user if outdated
93
+ * Runs once per day to avoid slowing down commands
94
+ */
95
+ export async function checkForUpdates() {
96
+ const currentVersion = getCliVersion()
97
+
98
+ if (currentVersion === 'unknown') {
99
+ return
100
+ }
101
+
102
+ // Only check once per day
103
+ const cwd = process.cwd()
104
+ if (!shouldCheckVersion(cwd)) {
105
+ return
106
+ }
107
+
108
+ const latestVersion = await getLatestVersion()
109
+
110
+ if (!latestVersion) {
111
+ return
112
+ }
113
+
114
+ updateCache(latestVersion)
115
+
116
+ if (compareVersions(latestVersion, currentVersion) > 0) {
117
+ console.log('')
118
+ console.log(pc.yellow(` ⚠ A new version of novaui-cli is available!`))
119
+ console.log(pc.dim(` Current: ${currentVersion}`))
120
+ console.log(pc.dim(` Latest: ${latestVersion}`))
121
+ console.log('')
122
+ console.log(pc.cyan(` Update with: npm install -g novaui-cli@latest`))
123
+ console.log('')
124
+ }
125
+ }
126
+
127
+ /**
128
+ * Get version info for error reporting
129
+ */
130
+ export function getVersionInfo() {
131
+ const currentVersion = getCliVersion()
132
+ return {
133
+ cli: currentVersion,
134
+ node: process.version,
135
+ }
136
+ }