xertica-ui 2.0.0 → 2.0.2

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/bin/cli.ts CHANGED
@@ -1,285 +1,472 @@
1
- #!/usr/bin/env node
2
- import { Command } from 'commander';
3
- import prompts from 'prompts';
4
- import chalk from 'chalk';
5
- import ora from 'ora';
6
- import fs from 'fs-extra';
7
- import path from 'path';
8
- import { fileURLToPath } from 'url';
9
- import { execa } from 'execa';
10
- import { colorThemes } from '../contexts/theme-data';
11
- import { generateTokensCss } from './generate-tokens';
12
-
13
- const __filename = fileURLToPath(import.meta.url);
14
- const __dirname = path.dirname(__filename);
15
-
16
- const program = new Command();
17
-
18
- program
19
- .name('xertica-ui')
20
- .description('CLI to initialize Xertica UI projects')
21
- .version('1.7.0');
22
-
23
- program
24
- .command('init')
25
- .description('Initialize a new Xertica UI project')
26
- .argument('[directory]', 'Directory to initialize in', '.')
27
- .action(async (directory) => {
28
- const targetDir = path.resolve(process.cwd(), directory);
29
- // The templates directory is bundled alongside the CLI
30
- const templatesDir = path.resolve(__dirname, '../templates');
31
-
32
- console.log(chalk.blue('🚀 Welcome to Xertica UI CLI!'));
33
-
34
- const response = await prompts([
35
- {
36
- type: 'multiselect',
37
- name: 'pages',
38
- message: 'Which pages/templates to include?',
39
- choices: [
40
- { title: 'Login Page (+ Forgot / Verify / Reset Password)', value: 'login', selected: true },
41
- { title: 'Home Page', value: 'home', selected: true },
42
- { title: 'Template Page (components showcase)', value: 'template', selected: true },
43
- ]
44
- },
45
- {
46
- type: 'select',
47
- name: 'theme',
48
- message: 'Select the default color theme for your project:',
49
- choices: colorThemes.map(t => ({
50
- title: t.name,
51
- description: t.description,
52
- value: t.id
53
- })),
54
- initial: 0
55
- },
56
- {
57
- type: 'confirm',
58
- name: 'install',
59
- message: 'Install dependencies automatically?',
60
- initial: true
61
- }
62
- ]);
63
-
64
- if (!response.pages) return;
65
-
66
- const spinner = ora('Initializing project...').start();
67
-
68
- try {
69
- // 1. Create Target Directory
70
- await fs.ensureDir(targetDir);
71
-
72
- const pages = response.pages || [];
73
- const hasLogin = pages.includes('login');
74
- const hasHome = pages.includes('home');
75
- const hasTemplate = pages.includes('template');
76
-
77
- // 2. Copy root config files from templates/
78
- const rootFilesToCopy = [
79
- 'index.html',
80
- 'vite.config.ts',
81
- 'tsconfig.json',
82
- 'tsconfig.node.json',
83
- 'postcss.config.js',
84
- 'vite-env.d.ts',
85
- 'eslint.config.js',
86
- '.env.example',
87
- 'guidelines',
88
- ];
89
-
90
- for (const file of rootFilesToCopy) {
91
- const srcPath = path.join(templatesDir, file);
92
- const destPath = path.join(targetDir, file);
93
- if (await fs.pathExists(srcPath)) {
94
- await fs.copy(srcPath, destPath);
95
- }
96
- }
97
-
98
- // 3. Copy package.json template and customize it
99
- const pkgTemplatePath = path.join(templatesDir, 'package.json');
100
- if (await fs.pathExists(pkgTemplatePath)) {
101
- const pkgContent = await fs.readJson(pkgTemplatePath);
102
- // Use directory name as project name
103
- const projectName = path.basename(targetDir) || 'my-xertica-app';
104
- pkgContent.name = projectName.toLowerCase().replace(/[^a-z0-9-]/g, '-');
105
- await fs.writeJson(path.join(targetDir, 'package.json'), pkgContent, { spaces: 2 });
106
- }
107
-
108
- // 4. Ensure src/app directory exists
109
- await fs.ensureDir(path.join(targetDir, 'src', 'app'));
110
-
111
- // 5. Copy src/app/App.tsx
112
- await fs.copy(
113
- path.join(templatesDir, 'src', 'app', 'App.tsx'),
114
- path.join(targetDir, 'src', 'app', 'App.tsx')
115
- );
116
-
117
- // 6. Copy src/main.tsx
118
- await fs.copy(
119
- path.join(templatesDir, 'src', 'main.tsx'),
120
- path.join(targetDir, 'src', 'main.tsx')
121
- );
122
-
123
- // 7. Copy src/app/routes.tsx
124
- await fs.copy(
125
- path.join(templatesDir, 'src', 'app', 'routes.tsx'),
126
- path.join(targetDir, 'src', 'app', 'routes.tsx')
127
- );
128
-
129
- // 8. Copy pages and contents
130
- await fs.ensureDir(path.join(targetDir, 'src', 'app', 'pages'));
131
-
132
- const optionalPages: Record<string, { file: string, dir?: string, shouldCopy: boolean }> = {
133
- login: { file: 'LoginPage.tsx', dir: 'Login', shouldCopy: hasLogin },
134
- forgot: { file: 'ForgotPasswordPage.tsx', dir: 'ForgotPassword', shouldCopy: hasLogin },
135
- verify: { file: 'VerifyEmailPage.tsx', dir: 'VerifyEmail', shouldCopy: hasLogin },
136
- reset: { file: 'ResetPasswordPage.tsx', dir: 'ResetPassword', shouldCopy: hasLogin },
137
- home: { file: 'HomePage.tsx', dir: 'Home', shouldCopy: hasHome },
138
- template: { file: 'TemplatePage.tsx', dir: 'Template', shouldCopy: hasTemplate },
139
- };
140
-
141
- const srcPagesDir = path.join(templatesDir, 'src', 'app', 'pages');
142
- for (const key in optionalPages) {
143
- const { file, dir, shouldCopy } = optionalPages[key];
144
- if (shouldCopy) {
145
- const srcFilePath = path.join(srcPagesDir, file);
146
- if (await fs.pathExists(srcFilePath)) {
147
- await fs.copy(srcFilePath, path.join(targetDir, 'src', 'app', 'pages', file));
148
- }
149
- if (dir) {
150
- const srcDirPath = path.join(srcPagesDir, dir);
151
- if (await fs.pathExists(srcDirPath)) {
152
- await fs.copy(srcDirPath, path.join(targetDir, 'src', 'app', 'pages', dir));
153
- }
154
- }
155
- }
156
- }
157
-
158
- // 10. If NOT all pages selected, create a minimal App.tsx
159
- if (!hasLogin || !hasHome || !hasTemplate) {
160
- const imports: string[] = [];
161
- const routes: string[] = [];
162
-
163
- if (hasLogin) {
164
- imports.push(`import { LoginPage } from './pages/LoginPage';`);
165
- imports.push(`import { ForgotPasswordPage } from './pages/ForgotPasswordPage';`);
166
- imports.push(`import { VerifyEmailPage } from './pages/VerifyEmailPage';`);
167
- imports.push(`import { ResetPasswordPage } from './pages/ResetPasswordPage';`);
168
- routes.push(` <Route path="/login" element={<LoginPage onLogin={handleLogin} />} />`);
169
- routes.push(` <Route path="/forgot-password" element={<ForgotPasswordPage />} />`);
170
- routes.push(` <Route path="/verify-email" element={<VerifyEmailPage />} />`);
171
- routes.push(` <Route path="/reset-password" element={<ResetPasswordPage />} />`);
172
- }
173
- if (hasHome) {
174
- imports.push(`import { HomePage } from './pages/HomePage';`);
175
- routes.push(` <Route path="/home" element={<HomePage user={user} onLogout={handleLogout} />} />`);
176
- }
177
- if (hasTemplate) {
178
- imports.push(`import { TemplatePage } from './pages/TemplatePage';`);
179
- routes.push(` <Route path="/template" element={<TemplatePage user={user} onLogout={handleLogout} />} />`);
180
- }
181
-
182
- const minimalApp = `import React, { useState, useEffect, useLayoutEffect } from 'react';
183
- import { BrowserRouter as Router, Routes, Route, Navigate, useNavigate, useLocation } from 'react-router-dom';
184
- import { XerticaProvider } from 'xertica-ui';
185
- ${imports.join('\n')}
186
-
187
- export default function App() {
188
- return (
189
- <XerticaProvider>
190
- <Router>
191
- <Routes>
192
- ${routes.join('\n')}
193
- <Route path="/" element={<Navigate to="${hasLogin ? '/login' : hasHome ? '/home' : '/template'}" replace />} />
194
- <Route path="*" element={<Navigate to="${hasLogin ? '/login' : hasHome ? '/home' : '/template'}" replace />} />
195
- </Routes>
196
- </Router>
197
- </XerticaProvider>
198
- );
199
- }
200
- `;
201
- await fs.writeFile(path.join(targetDir, 'src', 'app', 'App.tsx'), minimalApp);
202
- }
203
-
204
- // 11. Generate theme tokens
205
- const selectedTheme = colorThemes.find(t => t.id === response.theme) || colorThemes[0];
206
-
207
- // Create src/styles/xertica directory
208
- const tokensDir = path.join(targetDir, 'src', 'styles', 'xertica');
209
- await fs.ensureDir(tokensDir);
210
-
211
- // Copy src/styles/index.css
212
- await fs.copy(
213
- path.join(templatesDir, 'src', 'styles', 'index.css'),
214
- path.join(targetDir, 'src', 'styles', 'index.css')
215
- );
216
-
217
- const newTokensCss = generateTokensCss(selectedTheme);
218
- await fs.writeFile(path.join(tokensDir, 'tokens.css'), newTokensCss);
219
-
220
- spinner.succeed('Project initialized successfully!');
221
-
222
- if (response.install) {
223
- const installSpinner = ora('Installing dependencies...').start();
224
- await execa('npm', ['install'], { cwd: targetDir });
225
- installSpinner.succeed('Dependencies installed!');
226
- }
227
-
228
- console.log(chalk.green('\n✅ Done! Your Xertica UI project is ready.'));
229
- console.log(chalk.cyan(`\n cd ${directory}`));
230
- if (!response.install) {
231
- console.log(chalk.cyan(' npm install'));
232
- }
233
- console.log(chalk.cyan(' npm run dev'));
234
- console.log();
235
- console.log(chalk.gray(' Components are imported from the xertica-ui package.'));
236
- console.log(chalk.gray(' Customize the theme in src/styles/xertica/tokens.css'));
237
-
238
- } catch (error) {
239
- spinner.fail('Failed to initialize project');
240
- console.error(error);
241
- }
242
- });
243
-
244
- program
245
- .command('update')
246
- .alias('update-theme')
247
- .description('Update theme tokens in your project')
248
- .action(async () => {
249
- const targetDir = process.cwd();
250
-
251
- const themeResponse = await prompts({
252
- type: 'select',
253
- name: 'theme',
254
- message: 'Select the new color theme:',
255
- choices: colorThemes.map(t => ({
256
- title: t.name,
257
- description: t.description,
258
- value: t.id
259
- })),
260
- initial: 0
261
- });
262
-
263
- if (!themeResponse.theme) return;
264
-
265
- const spinner = ora('Updating theme...').start();
266
-
267
- try {
268
- const tokensPath = path.join(targetDir, 'src', 'styles', 'xertica', 'tokens.css');
269
- const selectedTheme = colorThemes.find(t => t.id === themeResponse.theme);
270
-
271
- if (selectedTheme) {
272
- await fs.ensureDir(path.dirname(tokensPath));
273
- const newTokensCss = generateTokensCss(selectedTheme);
274
- await fs.writeFile(tokensPath, newTokensCss);
275
- spinner.succeed(`Theme updated to "${selectedTheme.name}" successfully!`);
276
- } else {
277
- spinner.fail('Theme not found.');
278
- }
279
- } catch (error) {
280
- spinner.fail('Failed to update theme');
281
- console.error(error);
282
- }
283
- });
284
-
285
- program.parse();
1
+ #!/usr/bin/env node
2
+ import { Command } from 'commander';
3
+ import prompts from 'prompts';
4
+ import chalk from 'chalk';
5
+ import ora from 'ora';
6
+ import fs from 'fs-extra';
7
+ import path from 'path';
8
+ import { fileURLToPath } from 'url';
9
+ import { execa } from 'execa';
10
+ import { colorThemes } from '../contexts/theme-data';
11
+ import { generateTokensCss } from './generate-tokens';
12
+
13
+ const __filename = fileURLToPath(import.meta.url);
14
+ const __dirname = path.dirname(__filename);
15
+
16
+ const program = new Command();
17
+
18
+ program
19
+ .name('xertica-ui')
20
+ .description('CLI to initialize Xertica UI projects')
21
+ .version('2.0.2');
22
+
23
+ program
24
+ .command('init')
25
+ .description('Initialize a new Xertica UI project')
26
+ .argument('[directory]', 'Directory to initialize in', '.')
27
+ .action(async (directory) => {
28
+ const targetDir = path.resolve(process.cwd(), directory);
29
+ const templatesDir = path.resolve(__dirname, '../templates');
30
+
31
+ console.log(chalk.blue('🚀 Welcome to Xertica UI CLI!'));
32
+
33
+ const response = await prompts([
34
+ {
35
+ type: 'multiselect',
36
+ name: 'pages',
37
+ message: 'Which pages/templates to include?',
38
+ choices: [
39
+ { title: 'Login Page (+ Forgot / Verify / Reset Password)', value: 'login', selected: true },
40
+ { title: 'Home Page', value: 'home', selected: true },
41
+ { title: 'Template Page (components showcase)', value: 'template', selected: true },
42
+ ]
43
+ },
44
+ {
45
+ type: 'select',
46
+ name: 'theme',
47
+ message: 'Select the default color theme for your project:',
48
+ choices: colorThemes.map(t => ({
49
+ title: t.name,
50
+ description: t.description,
51
+ value: t.id
52
+ })),
53
+ initial: 0
54
+ },
55
+ {
56
+ type: 'confirm',
57
+ name: 'install',
58
+ message: 'Install dependencies automatically?',
59
+ initial: true
60
+ }
61
+ ]);
62
+
63
+ if (!response.pages) return;
64
+
65
+ const spinner = ora('Initializing project...').start();
66
+
67
+ try {
68
+ await fs.ensureDir(targetDir);
69
+
70
+ const pages = response.pages || [];
71
+ const hasLogin = pages.includes('login');
72
+ const hasHome = pages.includes('home');
73
+ const hasTemplate = pages.includes('template');
74
+
75
+ // 1. Copy root config files
76
+ const rootFilesToCopy = [
77
+ 'index.html',
78
+ 'vite.config.ts',
79
+ 'tsconfig.json',
80
+ 'tsconfig.node.json',
81
+ 'postcss.config.js',
82
+ 'vite-env.d.ts',
83
+ 'eslint.config.js',
84
+ '.env.example',
85
+ 'guidelines',
86
+ ];
87
+
88
+ for (const file of rootFilesToCopy) {
89
+ const srcPath = path.join(templatesDir, file);
90
+ if (await fs.pathExists(srcPath)) {
91
+ await fs.copy(srcPath, path.join(targetDir, file));
92
+ }
93
+ }
94
+
95
+ // 2. Copy package.json
96
+ const pkgTemplatePath = path.join(templatesDir, 'package.json');
97
+ if (await fs.pathExists(pkgTemplatePath)) {
98
+ const pkgContent = await fs.readJson(pkgTemplatePath);
99
+ const projectName = path.basename(targetDir) || 'my-xertica-app';
100
+ pkgContent.name = projectName.toLowerCase().replace(/[^a-z0-9-]/g, '-');
101
+ await fs.writeJson(path.join(targetDir, 'package.json'), pkgContent, { spaces: 2 });
102
+ }
103
+
104
+ // 3. Copy src/main.tsx
105
+ await fs.copy(
106
+ path.join(templatesDir, 'src', 'main.tsx'),
107
+ path.join(targetDir, 'src', 'main.tsx')
108
+ );
109
+
110
+ // 4. Copy src/app/App.tsx
111
+ await fs.ensureDir(path.join(targetDir, 'src', 'app'));
112
+ await fs.copy(
113
+ path.join(templatesDir, 'src', 'app', 'App.tsx'),
114
+ path.join(targetDir, 'src', 'app', 'App.tsx')
115
+ );
116
+
117
+ // 5. Copy src/app/components/AppLayout.tsx (always needed)
118
+ await fs.ensureDir(path.join(targetDir, 'src', 'app', 'components'));
119
+ await fs.copy(
120
+ path.join(templatesDir, 'src', 'app', 'components', 'AppLayout.tsx'),
121
+ path.join(targetDir, 'src', 'app', 'components', 'AppLayout.tsx')
122
+ );
123
+
124
+ // 6. Copy src/shared/ (always needed — auth helpers, navigation config, types)
125
+ await fs.copy(
126
+ path.join(templatesDir, 'src', 'shared'),
127
+ path.join(targetDir, 'src', 'shared')
128
+ );
129
+
130
+ // 7. Copy features based on selections
131
+ if (hasLogin) {
132
+ await fs.copy(
133
+ path.join(templatesDir, 'src', 'features', 'auth'),
134
+ path.join(targetDir, 'src', 'features', 'auth')
135
+ );
136
+ }
137
+ if (hasHome) {
138
+ await fs.copy(
139
+ path.join(templatesDir, 'src', 'features', 'home'),
140
+ path.join(targetDir, 'src', 'features', 'home')
141
+ );
142
+ }
143
+ if (hasTemplate) {
144
+ await fs.copy(
145
+ path.join(templatesDir, 'src', 'features', 'template'),
146
+ path.join(targetDir, 'src', 'features', 'template')
147
+ );
148
+ }
149
+
150
+ // 8. Copy pages based on selections
151
+ await fs.ensureDir(path.join(targetDir, 'src', 'pages'));
152
+
153
+ const pagesToCopy: string[] = [];
154
+ if (hasLogin) pagesToCopy.push('LoginPage.tsx', 'ForgotPasswordPage.tsx', 'VerifyEmailPage.tsx', 'ResetPasswordPage.tsx');
155
+ if (hasHome) pagesToCopy.push('HomePage.tsx');
156
+ if (hasTemplate) pagesToCopy.push('TemplatePage.tsx');
157
+
158
+ for (const pageFile of pagesToCopy) {
159
+ const src = path.join(templatesDir, 'src', 'pages', pageFile);
160
+ if (await fs.pathExists(src)) {
161
+ await fs.copy(src, path.join(targetDir, 'src', 'pages', pageFile));
162
+ }
163
+ }
164
+
165
+ // 9. Generate AuthGuard.tsx based on selected pages
166
+ const firstProtectedPath = hasHome ? '/home' : hasTemplate ? '/template' : '/login';
167
+
168
+ const authGuardImports: string[] = [
169
+ `import React, { useState, useEffect } from 'react';`,
170
+ `import { Routes, Route, Navigate, useNavigate, useLocation } from 'react-router-dom';`,
171
+ ];
172
+
173
+ if (hasLogin) {
174
+ authGuardImports.push(`import { getStoredUser, storeUser, clearStoredUser } from '../../shared/lib/auth';`);
175
+ authGuardImports.push(`import type { User } from '../../shared/types/auth';`);
176
+ authGuardImports.push(`import { LoginPage } from '../../pages/LoginPage';`);
177
+ authGuardImports.push(`import { ForgotPasswordPage } from '../../pages/ForgotPasswordPage';`);
178
+ authGuardImports.push(`import { VerifyEmailPage } from '../../pages/VerifyEmailPage';`);
179
+ authGuardImports.push(`import { ResetPasswordPage } from '../../pages/ResetPasswordPage';`);
180
+ } else {
181
+ authGuardImports.push(`import { getStoredUser, storeUser, clearStoredUser } from '../../shared/lib/auth';`);
182
+ authGuardImports.push(`import type { User } from '../../shared/types/auth';`);
183
+ }
184
+ if (hasHome) authGuardImports.push(`import { HomePage } from '../../pages/HomePage';`);
185
+ if (hasTemplate) authGuardImports.push(`import { TemplatePage } from '../../pages/TemplatePage';`);
186
+
187
+ const protectedRoutes: string[] = [];
188
+ if (hasHome) protectedRoutes.push(` <Route path="/home" element={<ProtectedRoute user={user}><HomePage user={user} onLogout={handleLogout} /></ProtectedRoute>} />`);
189
+ if (hasTemplate) protectedRoutes.push(` <Route path="/template" element={<ProtectedRoute user={user}><TemplatePage user={user} onLogout={handleLogout} /></ProtectedRoute>} />`);
190
+
191
+ const authRoutes: string[] = [];
192
+ if (hasLogin) {
193
+ authRoutes.push(` <Route path="/login" element={user ? <Navigate to="${firstProtectedPath}" replace /> : <LoginPage onLogin={handleLogin} />} />`);
194
+ authRoutes.push(` <Route path="/forgot-password" element={user ? <Navigate to="${firstProtectedPath}" replace /> : <ForgotPasswordPage />} />`);
195
+ authRoutes.push(` <Route path="/verify-email" element={user ? <Navigate to="${firstProtectedPath}" replace /> : <VerifyEmailPage />} />`);
196
+ authRoutes.push(` <Route path="/reset-password" element={user ? <Navigate to="${firstProtectedPath}" replace /> : <ResetPasswordPage />} />`);
197
+ }
198
+
199
+ const handleLoginFn = hasLogin ? `
200
+ const handleLogin = (email: string, password: string): boolean => {
201
+ if (!email.trim() || !password.trim()) return false;
202
+ const userData: User = { email };
203
+ storeUser(userData);
204
+ setUser(userData);
205
+ navigate('${firstProtectedPath}');
206
+ return true;
207
+ };` : '';
208
+
209
+ const authPathsRedirect = hasLogin ? `
210
+ useEffect(() => {
211
+ const authPaths = ['/login', '/forgot-password', '/verify-email', '/reset-password'];
212
+ if (user && authPaths.includes(location.pathname)) {
213
+ navigate('${firstProtectedPath}', { replace: true });
214
+ }
215
+ }, [user, location.pathname, navigate]);` : '';
216
+
217
+ const authGuardContent = `${authGuardImports.join('\n')}
218
+
219
+ function ProtectedRoute({ children, user }: { children: React.ReactNode; user: User | null }) {
220
+ if (!user) return <Navigate to="${hasLogin ? '/login' : firstProtectedPath}" replace />;
221
+ return <>{children}</>;
222
+ }
223
+
224
+ export function AuthGuard() {
225
+ const [user, setUser] = useState<User | null>(null);
226
+ const navigate = useNavigate();${hasLogin ? `\n const location = useLocation();` : ''}
227
+
228
+ useEffect(() => {
229
+ setUser(getStoredUser());
230
+ }, []);
231
+ ${authPathsRedirect}${handleLoginFn}
232
+
233
+ const handleLogout = () => {
234
+ clearStoredUser();
235
+ setUser(null);
236
+ navigate('${hasLogin ? '/login' : firstProtectedPath}');
237
+ };
238
+
239
+ return (
240
+ <div className="min-h-screen bg-muted overflow-x-hidden max-w-full">
241
+ <Routes>
242
+ ${authRoutes.join('\n')}
243
+ ${protectedRoutes.join('\n')}
244
+ <Route path="/" element={<Navigate to="${hasLogin ? (firstProtectedPath === '/login' ? '/login' : firstProtectedPath) : firstProtectedPath}" replace />} />
245
+ <Route path="*" element={<Navigate to="${hasLogin ? '/login' : firstProtectedPath}" replace />} />
246
+ </Routes>
247
+ </div>
248
+ );
249
+ }
250
+ `;
251
+
252
+ await fs.writeFile(
253
+ path.join(targetDir, 'src', 'app', 'components', 'AuthGuard.tsx'),
254
+ authGuardContent
255
+ );
256
+
257
+ // 10. Generate theme tokens
258
+ const selectedTheme = colorThemes.find(t => t.id === response.theme) || colorThemes[0];
259
+ const tokensDir = path.join(targetDir, 'src', 'styles', 'xertica');
260
+ await fs.ensureDir(tokensDir);
261
+ await fs.copy(
262
+ path.join(templatesDir, 'src', 'styles', 'index.css'),
263
+ path.join(targetDir, 'src', 'styles', 'index.css')
264
+ );
265
+ await fs.writeFile(path.join(tokensDir, 'tokens.css'), generateTokensCss(selectedTheme));
266
+
267
+ spinner.succeed('Project initialized successfully!');
268
+
269
+ if (response.install) {
270
+ const installSpinner = ora('Installing dependencies...').start();
271
+ await execa('npm', ['install'], { cwd: targetDir });
272
+ installSpinner.succeed('Dependencies installed!');
273
+ }
274
+
275
+ console.log(chalk.green('\n✅ Done! Your Xertica UI project is ready.'));
276
+ console.log(chalk.cyan(`\n cd ${directory}`));
277
+ if (!response.install) {
278
+ console.log(chalk.cyan(' npm install'));
279
+ }
280
+ console.log(chalk.cyan(' npm run dev'));
281
+ console.log();
282
+ console.log(chalk.gray(' Components are imported from the xertica-ui package.'));
283
+ console.log(chalk.gray(' Customize the theme in src/styles/xertica/tokens.css'));
284
+
285
+ } catch (error) {
286
+ spinner.fail('Failed to initialize project');
287
+ console.error(error);
288
+ }
289
+ });
290
+
291
+ program
292
+ .command('update')
293
+ .alias('update-theme')
294
+ .description('Update theme or project files to the latest version')
295
+ .action(async () => {
296
+ const targetDir = process.cwd();
297
+
298
+ const { updateType } = await prompts({
299
+ type: 'select',
300
+ name: 'updateType',
301
+ message: 'What do you want to update?',
302
+ choices: [
303
+ { title: 'Theme only', description: 'Change the color tokens (tokens.css)', value: 'theme' },
304
+ { title: 'Project files', description: 'Update app shell, shared, features and pages to a specific version', value: 'project' },
305
+ ],
306
+ });
307
+
308
+ if (!updateType) return;
309
+
310
+ // ── Theme update ─────────────────────────────────────────────────────────
311
+ if (updateType === 'theme') {
312
+ const { theme } = await prompts({
313
+ type: 'select',
314
+ name: 'theme',
315
+ message: 'Select the new color theme:',
316
+ choices: colorThemes.map(t => ({
317
+ title: t.name,
318
+ description: t.description,
319
+ value: t.id
320
+ })),
321
+ initial: 0
322
+ });
323
+
324
+ if (!theme) return;
325
+
326
+ const spinner = ora('Updating theme...').start();
327
+ try {
328
+ const tokensPath = path.join(targetDir, 'src', 'styles', 'xertica', 'tokens.css');
329
+ const selectedTheme = colorThemes.find(t => t.id === theme);
330
+ if (selectedTheme) {
331
+ await fs.ensureDir(path.dirname(tokensPath));
332
+ await fs.writeFile(tokensPath, generateTokensCss(selectedTheme));
333
+ spinner.succeed(`Theme updated to "${selectedTheme.name}" successfully!`);
334
+ } else {
335
+ spinner.fail('Theme not found.');
336
+ }
337
+ } catch (error) {
338
+ spinner.fail('Failed to update theme');
339
+ console.error(error);
340
+ }
341
+ return;
342
+ }
343
+
344
+ // ── Project files update ──────────────────────────────────────────────────
345
+ const { versionType } = await prompts({
346
+ type: 'select',
347
+ name: 'versionType',
348
+ message: 'Which version do you want to update to?',
349
+ choices: [
350
+ { title: 'Latest', description: 'Install the latest published version', value: 'latest' },
351
+ { title: 'Specific version', description: 'Enter a version number (e.g. 2.0.2)', value: 'specific' },
352
+ ],
353
+ });
354
+
355
+ if (!versionType) return;
356
+
357
+ let targetVersion = 'latest';
358
+ if (versionType === 'specific') {
359
+ const { version } = await prompts({
360
+ type: 'text',
361
+ name: 'version',
362
+ message: 'Enter the version (e.g. 2.0.2):',
363
+ validate: v => /^\d+\.\d+\.\d+/.test(v.trim()) ? true : 'Enter a valid semver (e.g. 2.0.2)',
364
+ });
365
+ if (!version) return;
366
+ targetVersion = version.trim();
367
+ }
368
+
369
+ const { filesToUpdate } = await prompts({
370
+ type: 'multiselect',
371
+ name: 'filesToUpdate',
372
+ message: 'Select which parts of the project to update:',
373
+ choices: [
374
+ { title: 'App shell (src/app/)', description: 'App.tsx, AppLayout.tsx', value: 'app', selected: true },
375
+ { title: 'Shared utilities (src/shared/)', description: 'auth.ts, navigation.ts, types', value: 'shared', selected: true },
376
+ { title: 'Features (src/features/)', description: 'auth, home, template UI components', value: 'features', selected: true },
377
+ { title: 'Pages (src/pages/)', description: 'Thin page wrapper components', value: 'pages', selected: true },
378
+ { title: 'Root config files', description: 'vite.config.ts, tsconfig.json, etc.', value: 'config', selected: false },
379
+ ],
380
+ });
381
+
382
+ if (!filesToUpdate || filesToUpdate.length === 0) return;
383
+
384
+ const { confirmed } = await prompts({
385
+ type: 'confirm',
386
+ name: 'confirmed',
387
+ message: chalk.yellow(`⚠️ This will overwrite the selected files. Local changes will be lost. Continue?`),
388
+ initial: false,
389
+ });
390
+
391
+ if (!confirmed) {
392
+ console.log(chalk.gray('Update cancelled.'));
393
+ return;
394
+ }
395
+
396
+ const spinner = ora(`Installing xertica-ui@${targetVersion}...`).start();
397
+
398
+ try {
399
+ // Install the target version in the consumer project
400
+ await execa('npm', ['install', `xertica-ui@${targetVersion}`], { cwd: targetDir });
401
+ spinner.text = 'Copying updated files...';
402
+
403
+ // Templates now come from the freshly installed version
404
+ const updatedTemplatesDir = path.join(targetDir, 'node_modules', 'xertica-ui', 'templates');
405
+
406
+ if (filesToUpdate.includes('app')) {
407
+ await fs.copy(
408
+ path.join(updatedTemplatesDir, 'src', 'app', 'App.tsx'),
409
+ path.join(targetDir, 'src', 'app', 'App.tsx'),
410
+ { overwrite: true }
411
+ );
412
+ await fs.copy(
413
+ path.join(updatedTemplatesDir, 'src', 'app', 'components', 'AppLayout.tsx'),
414
+ path.join(targetDir, 'src', 'app', 'components', 'AppLayout.tsx'),
415
+ { overwrite: true }
416
+ );
417
+ }
418
+
419
+ if (filesToUpdate.includes('shared')) {
420
+ await fs.copy(
421
+ path.join(updatedTemplatesDir, 'src', 'shared'),
422
+ path.join(targetDir, 'src', 'shared'),
423
+ { overwrite: true }
424
+ );
425
+ }
426
+
427
+ if (filesToUpdate.includes('features')) {
428
+ // Only update feature directories that already exist in the project
429
+ for (const feature of ['auth', 'home', 'template']) {
430
+ const destFeature = path.join(targetDir, 'src', 'features', feature);
431
+ const srcFeature = path.join(updatedTemplatesDir, 'src', 'features', feature);
432
+ if (await fs.pathExists(destFeature) && await fs.pathExists(srcFeature)) {
433
+ await fs.copy(srcFeature, destFeature, { overwrite: true });
434
+ }
435
+ }
436
+ }
437
+
438
+ if (filesToUpdate.includes('pages')) {
439
+ const pagesDir = path.join(targetDir, 'src', 'pages');
440
+ const srcPagesDir = path.join(updatedTemplatesDir, 'src', 'pages');
441
+ if (await fs.pathExists(pagesDir) && await fs.pathExists(srcPagesDir)) {
442
+ // Only overwrite pages that already exist in the project
443
+ const existingPages = await fs.readdir(pagesDir);
444
+ for (const pageFile of existingPages) {
445
+ const src = path.join(srcPagesDir, pageFile);
446
+ if (await fs.pathExists(src)) {
447
+ await fs.copy(src, path.join(pagesDir, pageFile), { overwrite: true });
448
+ }
449
+ }
450
+ }
451
+ }
452
+
453
+ if (filesToUpdate.includes('config')) {
454
+ const configFiles = ['vite.config.ts', 'tsconfig.json', 'tsconfig.node.json', 'postcss.config.js'];
455
+ for (const file of configFiles) {
456
+ const src = path.join(updatedTemplatesDir, '..', file);
457
+ if (await fs.pathExists(src)) {
458
+ await fs.copy(src, path.join(targetDir, file), { overwrite: true });
459
+ }
460
+ }
461
+ }
462
+
463
+ spinner.succeed(`Project updated to xertica-ui@${targetVersion} successfully!`);
464
+ console.log(chalk.gray('\n Run npm run dev to start the development server.'));
465
+
466
+ } catch (error) {
467
+ spinner.fail('Failed to update project');
468
+ console.error(error);
469
+ }
470
+ });
471
+
472
+ program.parse();