xertica-ui 2.0.1 → 2.0.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/CHANGELOG.md CHANGED
@@ -7,6 +7,30 @@ Versioning follows [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
8
  ---
9
9
 
10
+ ## [2.0.3] — 2026-05-11
11
+
12
+ ### Added
13
+
14
+ - **CLI `update` — atualização de projeto** — novo modo no comando `update` permite atualizar os arquivos do projeto (app shell, shared, features, pages) para qualquer versão publicada do `xertica-ui`, com seleção granular de quais partes atualizar e confirmação antes de sobrescrever.
15
+
16
+ ---
17
+
18
+ ## [2.0.2] — 2026-05-11
19
+
20
+ ### Fixed
21
+
22
+ - **CLI rewritten for FSD/FDA structure** — `npx xertica-ui@latest init` agora copia corretamente a estrutura Feature-Sliced Design:
23
+ - Removida cópia de `src/app/routes.tsx` (arquivo não existe mais após refatoração)
24
+ - Adicionada cópia de `src/app/components/AppLayout.tsx`
25
+ - Adicionada cópia completa de `src/shared/` (`auth.ts`, `navigation.ts`, `types/auth.ts`)
26
+ - Adicionada cópia de `src/features/auth|home|template` conforme seleções do usuário
27
+ - Cópia de pages corrigida de `src/app/pages/` → `src/pages/`
28
+ - `AuthGuard.tsx` agora gerado dinamicamente com imports e rotas apenas das páginas selecionadas
29
+ - **`generateDemoResponse` exportado via `xertica-ui/assistant`** — estava ausente do barrel causando `SyntaxError: does not provide an export named 'generateDemoResponse'`
30
+ - **Build corrigido: UMD → CJS** — múltiplos entry points não são suportados com formato UMD no Vite; migrado para CJS (todos os `*.umd.js` → `*.cjs.js` no `package.json` exports)
31
+
32
+ ---
33
+
10
34
  ## [2.0.0] — 2026-05-11
11
35
 
12
36
  ### Added
package/README.md CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  > **Enterprise-grade React design system** built on Tailwind CSS v4, Radix UI, and Lucide Icons — with a robust AI-first documentation layer for precise LLM-driven composition and autonomous agent interaction.
4
4
 
5
- [![npm version](https://img.shields.io/npm/v/xertica-ui)](https://www.npmjs.com/package/xertica-ui)
5
+ [![npm version](https://img.shields.io/badge/npm-2.0.3-blue)](https://www.npmjs.com/package/xertica-ui)
6
6
  [![license](https://img.shields.io/badge/license-proprietary-red)](./LICENSE)
7
7
 
8
8
  ---
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.3');
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();
package/dist/cli.js CHANGED
@@ -528,7 +528,7 @@ var generateTokensCss = (theme) => {
528
528
  var __filename = fileURLToPath(import.meta.url);
529
529
  var __dirname = path.dirname(__filename);
530
530
  var program = new Command();
531
- program.name("xertica-ui").description("CLI to initialize Xertica UI projects").version("1.7.0");
531
+ program.name("xertica-ui").description("CLI to initialize Xertica UI projects").version("2.0.3");
532
532
  program.command("init").description("Initialize a new Xertica UI project").argument("[directory]", "Directory to initialize in", ".").action(async (directory) => {
533
533
  const targetDir = path.resolve(process.cwd(), directory);
534
534
  const templatesDir = path.resolve(__dirname, "../templates");
@@ -583,9 +583,8 @@ program.command("init").description("Initialize a new Xertica UI project").argum
583
583
  ];
584
584
  for (const file of rootFilesToCopy) {
585
585
  const srcPath = path.join(templatesDir, file);
586
- const destPath = path.join(targetDir, file);
587
586
  if (await fs.pathExists(srcPath)) {
588
- await fs.copy(srcPath, destPath);
587
+ await fs.copy(srcPath, path.join(targetDir, file));
589
588
  }
590
589
  }
591
590
  const pkgTemplatePath = path.join(templatesDir, "package.json");
@@ -595,86 +594,136 @@ program.command("init").description("Initialize a new Xertica UI project").argum
595
594
  pkgContent.name = projectName.toLowerCase().replace(/[^a-z0-9-]/g, "-");
596
595
  await fs.writeJson(path.join(targetDir, "package.json"), pkgContent, { spaces: 2 });
597
596
  }
597
+ await fs.copy(
598
+ path.join(templatesDir, "src", "main.tsx"),
599
+ path.join(targetDir, "src", "main.tsx")
600
+ );
598
601
  await fs.ensureDir(path.join(targetDir, "src", "app"));
599
602
  await fs.copy(
600
603
  path.join(templatesDir, "src", "app", "App.tsx"),
601
604
  path.join(targetDir, "src", "app", "App.tsx")
602
605
  );
606
+ await fs.ensureDir(path.join(targetDir, "src", "app", "components"));
603
607
  await fs.copy(
604
- path.join(templatesDir, "src", "main.tsx"),
605
- path.join(targetDir, "src", "main.tsx")
608
+ path.join(templatesDir, "src", "app", "components", "AppLayout.tsx"),
609
+ path.join(targetDir, "src", "app", "components", "AppLayout.tsx")
606
610
  );
607
611
  await fs.copy(
608
- path.join(templatesDir, "src", "app", "routes.tsx"),
609
- path.join(targetDir, "src", "app", "routes.tsx")
612
+ path.join(templatesDir, "src", "shared"),
613
+ path.join(targetDir, "src", "shared")
610
614
  );
611
- await fs.ensureDir(path.join(targetDir, "src", "app", "pages"));
612
- const optionalPages = {
613
- login: { file: "LoginPage.tsx", dir: "Login", shouldCopy: hasLogin },
614
- forgot: { file: "ForgotPasswordPage.tsx", dir: "ForgotPassword", shouldCopy: hasLogin },
615
- verify: { file: "VerifyEmailPage.tsx", dir: "VerifyEmail", shouldCopy: hasLogin },
616
- reset: { file: "ResetPasswordPage.tsx", dir: "ResetPassword", shouldCopy: hasLogin },
617
- home: { file: "HomePage.tsx", dir: "Home", shouldCopy: hasHome },
618
- template: { file: "TemplatePage.tsx", dir: "Template", shouldCopy: hasTemplate }
619
- };
620
- const srcPagesDir = path.join(templatesDir, "src", "app", "pages");
621
- for (const key in optionalPages) {
622
- const { file, dir, shouldCopy } = optionalPages[key];
623
- if (shouldCopy) {
624
- const srcFilePath = path.join(srcPagesDir, file);
625
- if (await fs.pathExists(srcFilePath)) {
626
- await fs.copy(srcFilePath, path.join(targetDir, "src", "app", "pages", file));
627
- }
628
- if (dir) {
629
- const srcDirPath = path.join(srcPagesDir, dir);
630
- if (await fs.pathExists(srcDirPath)) {
631
- await fs.copy(srcDirPath, path.join(targetDir, "src", "app", "pages", dir));
632
- }
633
- }
634
- }
615
+ if (hasLogin) {
616
+ await fs.copy(
617
+ path.join(templatesDir, "src", "features", "auth"),
618
+ path.join(targetDir, "src", "features", "auth")
619
+ );
635
620
  }
636
- if (!hasLogin || !hasHome || !hasTemplate) {
637
- const imports = [];
638
- const routes = [];
639
- if (hasLogin) {
640
- imports.push(`import { LoginPage } from './pages/LoginPage';`);
641
- imports.push(`import { ForgotPasswordPage } from './pages/ForgotPasswordPage';`);
642
- imports.push(`import { VerifyEmailPage } from './pages/VerifyEmailPage';`);
643
- imports.push(`import { ResetPasswordPage } from './pages/ResetPasswordPage';`);
644
- routes.push(` <Route path="/login" element={<LoginPage onLogin={handleLogin} />} />`);
645
- routes.push(` <Route path="/forgot-password" element={<ForgotPasswordPage />} />`);
646
- routes.push(` <Route path="/verify-email" element={<VerifyEmailPage />} />`);
647
- routes.push(` <Route path="/reset-password" element={<ResetPasswordPage />} />`);
648
- }
649
- if (hasHome) {
650
- imports.push(`import { HomePage } from './pages/HomePage';`);
651
- routes.push(` <Route path="/home" element={<HomePage user={user} onLogout={handleLogout} />} />`);
652
- }
653
- if (hasTemplate) {
654
- imports.push(`import { TemplatePage } from './pages/TemplatePage';`);
655
- routes.push(` <Route path="/template" element={<TemplatePage user={user} onLogout={handleLogout} />} />`);
621
+ if (hasHome) {
622
+ await fs.copy(
623
+ path.join(templatesDir, "src", "features", "home"),
624
+ path.join(targetDir, "src", "features", "home")
625
+ );
626
+ }
627
+ if (hasTemplate) {
628
+ await fs.copy(
629
+ path.join(templatesDir, "src", "features", "template"),
630
+ path.join(targetDir, "src", "features", "template")
631
+ );
632
+ }
633
+ await fs.ensureDir(path.join(targetDir, "src", "pages"));
634
+ const pagesToCopy = [];
635
+ if (hasLogin) pagesToCopy.push("LoginPage.tsx", "ForgotPasswordPage.tsx", "VerifyEmailPage.tsx", "ResetPasswordPage.tsx");
636
+ if (hasHome) pagesToCopy.push("HomePage.tsx");
637
+ if (hasTemplate) pagesToCopy.push("TemplatePage.tsx");
638
+ for (const pageFile of pagesToCopy) {
639
+ const src = path.join(templatesDir, "src", "pages", pageFile);
640
+ if (await fs.pathExists(src)) {
641
+ await fs.copy(src, path.join(targetDir, "src", "pages", pageFile));
656
642
  }
657
- const minimalApp = `import React, { useState, useEffect, useLayoutEffect } from 'react';
658
- import { BrowserRouter as Router, Routes, Route, Navigate, useNavigate, useLocation } from 'react-router-dom';
659
- import { XerticaProvider } from 'xertica-ui';
660
- ${imports.join("\n")}
643
+ }
644
+ const firstProtectedPath = hasHome ? "/home" : hasTemplate ? "/template" : "/login";
645
+ const authGuardImports = [
646
+ `import React, { useState, useEffect } from 'react';`,
647
+ `import { Routes, Route, Navigate, useNavigate, useLocation } from 'react-router-dom';`
648
+ ];
649
+ if (hasLogin) {
650
+ authGuardImports.push(`import { getStoredUser, storeUser, clearStoredUser } from '../../shared/lib/auth';`);
651
+ authGuardImports.push(`import type { User } from '../../shared/types/auth';`);
652
+ authGuardImports.push(`import { LoginPage } from '../../pages/LoginPage';`);
653
+ authGuardImports.push(`import { ForgotPasswordPage } from '../../pages/ForgotPasswordPage';`);
654
+ authGuardImports.push(`import { VerifyEmailPage } from '../../pages/VerifyEmailPage';`);
655
+ authGuardImports.push(`import { ResetPasswordPage } from '../../pages/ResetPasswordPage';`);
656
+ } else {
657
+ authGuardImports.push(`import { getStoredUser, storeUser, clearStoredUser } from '../../shared/lib/auth';`);
658
+ authGuardImports.push(`import type { User } from '../../shared/types/auth';`);
659
+ }
660
+ if (hasHome) authGuardImports.push(`import { HomePage } from '../../pages/HomePage';`);
661
+ if (hasTemplate) authGuardImports.push(`import { TemplatePage } from '../../pages/TemplatePage';`);
662
+ const protectedRoutes = [];
663
+ if (hasHome) protectedRoutes.push(` <Route path="/home" element={<ProtectedRoute user={user}><HomePage user={user} onLogout={handleLogout} /></ProtectedRoute>} />`);
664
+ if (hasTemplate) protectedRoutes.push(` <Route path="/template" element={<ProtectedRoute user={user}><TemplatePage user={user} onLogout={handleLogout} /></ProtectedRoute>} />`);
665
+ const authRoutes = [];
666
+ if (hasLogin) {
667
+ authRoutes.push(` <Route path="/login" element={user ? <Navigate to="${firstProtectedPath}" replace /> : <LoginPage onLogin={handleLogin} />} />`);
668
+ authRoutes.push(` <Route path="/forgot-password" element={user ? <Navigate to="${firstProtectedPath}" replace /> : <ForgotPasswordPage />} />`);
669
+ authRoutes.push(` <Route path="/verify-email" element={user ? <Navigate to="${firstProtectedPath}" replace /> : <VerifyEmailPage />} />`);
670
+ authRoutes.push(` <Route path="/reset-password" element={user ? <Navigate to="${firstProtectedPath}" replace /> : <ResetPasswordPage />} />`);
671
+ }
672
+ const handleLoginFn = hasLogin ? `
673
+ const handleLogin = (email: string, password: string): boolean => {
674
+ if (!email.trim() || !password.trim()) return false;
675
+ const userData: User = { email };
676
+ storeUser(userData);
677
+ setUser(userData);
678
+ navigate('${firstProtectedPath}');
679
+ return true;
680
+ };` : "";
681
+ const authPathsRedirect = hasLogin ? `
682
+ useEffect(() => {
683
+ const authPaths = ['/login', '/forgot-password', '/verify-email', '/reset-password'];
684
+ if (user && authPaths.includes(location.pathname)) {
685
+ navigate('${firstProtectedPath}', { replace: true });
686
+ }
687
+ }, [user, location.pathname, navigate]);` : "";
688
+ const authGuardContent = `${authGuardImports.join("\n")}
689
+
690
+ function ProtectedRoute({ children, user }: { children: React.ReactNode; user: User | null }) {
691
+ if (!user) return <Navigate to="${hasLogin ? "/login" : firstProtectedPath}" replace />;
692
+ return <>{children}</>;
693
+ }
694
+
695
+ export function AuthGuard() {
696
+ const [user, setUser] = useState<User | null>(null);
697
+ const navigate = useNavigate();${hasLogin ? `
698
+ const location = useLocation();` : ""}
699
+
700
+ useEffect(() => {
701
+ setUser(getStoredUser());
702
+ }, []);
703
+ ${authPathsRedirect}${handleLoginFn}
704
+
705
+ const handleLogout = () => {
706
+ clearStoredUser();
707
+ setUser(null);
708
+ navigate('${hasLogin ? "/login" : firstProtectedPath}');
709
+ };
661
710
 
662
- export default function App() {
663
711
  return (
664
- <XerticaProvider>
665
- <Router>
666
- <Routes>
667
- ${routes.join("\n")}
668
- <Route path="/" element={<Navigate to="${hasLogin ? "/login" : hasHome ? "/home" : "/template"}" replace />} />
669
- <Route path="*" element={<Navigate to="${hasLogin ? "/login" : hasHome ? "/home" : "/template"}" replace />} />
670
- </Routes>
671
- </Router>
672
- </XerticaProvider>
712
+ <div className="min-h-screen bg-muted overflow-x-hidden max-w-full">
713
+ <Routes>
714
+ ${authRoutes.join("\n")}
715
+ ${protectedRoutes.join("\n")}
716
+ <Route path="/" element={<Navigate to="${hasLogin ? firstProtectedPath === "/login" ? "/login" : firstProtectedPath : firstProtectedPath}" replace />} />
717
+ <Route path="*" element={<Navigate to="${hasLogin ? "/login" : firstProtectedPath}" replace />} />
718
+ </Routes>
719
+ </div>
673
720
  );
674
721
  }
675
722
  `;
676
- await fs.writeFile(path.join(targetDir, "src", "app", "App.tsx"), minimalApp);
677
- }
723
+ await fs.writeFile(
724
+ path.join(targetDir, "src", "app", "components", "AuthGuard.tsx"),
725
+ authGuardContent
726
+ );
678
727
  const selectedTheme = colorThemes.find((t) => t.id === response.theme) || colorThemes[0];
679
728
  const tokensDir = path.join(targetDir, "src", "styles", "xertica");
680
729
  await fs.ensureDir(tokensDir);
@@ -682,8 +731,7 @@ ${routes.join("\n")}
682
731
  path.join(templatesDir, "src", "styles", "index.css"),
683
732
  path.join(targetDir, "src", "styles", "index.css")
684
733
  );
685
- const newTokensCss = generateTokensCss(selectedTheme);
686
- await fs.writeFile(path.join(tokensDir, "tokens.css"), newTokensCss);
734
+ await fs.writeFile(path.join(tokensDir, "tokens.css"), generateTokensCss(selectedTheme));
687
735
  spinner.succeed("Project initialized successfully!");
688
736
  if (response.install) {
689
737
  const installSpinner = ora("Installing dependencies...").start();
@@ -705,34 +753,151 @@ ${routes.join("\n")}
705
753
  console.error(error);
706
754
  }
707
755
  });
708
- program.command("update").alias("update-theme").description("Update theme tokens in your project").action(async () => {
756
+ program.command("update").alias("update-theme").description("Update theme or project files to the latest version").action(async () => {
709
757
  const targetDir = process.cwd();
710
- const themeResponse = await prompts({
758
+ const { updateType } = await prompts({
759
+ type: "select",
760
+ name: "updateType",
761
+ message: "What do you want to update?",
762
+ choices: [
763
+ { title: "Theme only", description: "Change the color tokens (tokens.css)", value: "theme" },
764
+ { title: "Project files", description: "Update app shell, shared, features and pages to a specific version", value: "project" }
765
+ ]
766
+ });
767
+ if (!updateType) return;
768
+ if (updateType === "theme") {
769
+ const { theme } = await prompts({
770
+ type: "select",
771
+ name: "theme",
772
+ message: "Select the new color theme:",
773
+ choices: colorThemes.map((t) => ({
774
+ title: t.name,
775
+ description: t.description,
776
+ value: t.id
777
+ })),
778
+ initial: 0
779
+ });
780
+ if (!theme) return;
781
+ const spinner2 = ora("Updating theme...").start();
782
+ try {
783
+ const tokensPath = path.join(targetDir, "src", "styles", "xertica", "tokens.css");
784
+ const selectedTheme = colorThemes.find((t) => t.id === theme);
785
+ if (selectedTheme) {
786
+ await fs.ensureDir(path.dirname(tokensPath));
787
+ await fs.writeFile(tokensPath, generateTokensCss(selectedTheme));
788
+ spinner2.succeed(`Theme updated to "${selectedTheme.name}" successfully!`);
789
+ } else {
790
+ spinner2.fail("Theme not found.");
791
+ }
792
+ } catch (error) {
793
+ spinner2.fail("Failed to update theme");
794
+ console.error(error);
795
+ }
796
+ return;
797
+ }
798
+ const { versionType } = await prompts({
711
799
  type: "select",
712
- name: "theme",
713
- message: "Select the new color theme:",
714
- choices: colorThemes.map((t) => ({
715
- title: t.name,
716
- description: t.description,
717
- value: t.id
718
- })),
719
- initial: 0
800
+ name: "versionType",
801
+ message: "Which version do you want to update to?",
802
+ choices: [
803
+ { title: "Latest", description: "Install the latest published version", value: "latest" },
804
+ { title: "Specific version", description: "Enter a version number (e.g. 2.0.2)", value: "specific" }
805
+ ]
806
+ });
807
+ if (!versionType) return;
808
+ let targetVersion = "latest";
809
+ if (versionType === "specific") {
810
+ const { version } = await prompts({
811
+ type: "text",
812
+ name: "version",
813
+ message: "Enter the version (e.g. 2.0.2):",
814
+ validate: (v) => /^\d+\.\d+\.\d+/.test(v.trim()) ? true : "Enter a valid semver (e.g. 2.0.2)"
815
+ });
816
+ if (!version) return;
817
+ targetVersion = version.trim();
818
+ }
819
+ const { filesToUpdate } = await prompts({
820
+ type: "multiselect",
821
+ name: "filesToUpdate",
822
+ message: "Select which parts of the project to update:",
823
+ choices: [
824
+ { title: "App shell (src/app/)", description: "App.tsx, AppLayout.tsx", value: "app", selected: true },
825
+ { title: "Shared utilities (src/shared/)", description: "auth.ts, navigation.ts, types", value: "shared", selected: true },
826
+ { title: "Features (src/features/)", description: "auth, home, template UI components", value: "features", selected: true },
827
+ { title: "Pages (src/pages/)", description: "Thin page wrapper components", value: "pages", selected: true },
828
+ { title: "Root config files", description: "vite.config.ts, tsconfig.json, etc.", value: "config", selected: false }
829
+ ]
720
830
  });
721
- if (!themeResponse.theme) return;
722
- const spinner = ora("Updating theme...").start();
831
+ if (!filesToUpdate || filesToUpdate.length === 0) return;
832
+ const { confirmed } = await prompts({
833
+ type: "confirm",
834
+ name: "confirmed",
835
+ message: chalk.yellow(`\u26A0\uFE0F This will overwrite the selected files. Local changes will be lost. Continue?`),
836
+ initial: false
837
+ });
838
+ if (!confirmed) {
839
+ console.log(chalk.gray("Update cancelled."));
840
+ return;
841
+ }
842
+ const spinner = ora(`Installing xertica-ui@${targetVersion}...`).start();
723
843
  try {
724
- const tokensPath = path.join(targetDir, "src", "styles", "xertica", "tokens.css");
725
- const selectedTheme = colorThemes.find((t) => t.id === themeResponse.theme);
726
- if (selectedTheme) {
727
- await fs.ensureDir(path.dirname(tokensPath));
728
- const newTokensCss = generateTokensCss(selectedTheme);
729
- await fs.writeFile(tokensPath, newTokensCss);
730
- spinner.succeed(`Theme updated to "${selectedTheme.name}" successfully!`);
731
- } else {
732
- spinner.fail("Theme not found.");
844
+ await execa("npm", ["install", `xertica-ui@${targetVersion}`], { cwd: targetDir });
845
+ spinner.text = "Copying updated files...";
846
+ const updatedTemplatesDir = path.join(targetDir, "node_modules", "xertica-ui", "templates");
847
+ if (filesToUpdate.includes("app")) {
848
+ await fs.copy(
849
+ path.join(updatedTemplatesDir, "src", "app", "App.tsx"),
850
+ path.join(targetDir, "src", "app", "App.tsx"),
851
+ { overwrite: true }
852
+ );
853
+ await fs.copy(
854
+ path.join(updatedTemplatesDir, "src", "app", "components", "AppLayout.tsx"),
855
+ path.join(targetDir, "src", "app", "components", "AppLayout.tsx"),
856
+ { overwrite: true }
857
+ );
858
+ }
859
+ if (filesToUpdate.includes("shared")) {
860
+ await fs.copy(
861
+ path.join(updatedTemplatesDir, "src", "shared"),
862
+ path.join(targetDir, "src", "shared"),
863
+ { overwrite: true }
864
+ );
865
+ }
866
+ if (filesToUpdate.includes("features")) {
867
+ for (const feature of ["auth", "home", "template"]) {
868
+ const destFeature = path.join(targetDir, "src", "features", feature);
869
+ const srcFeature = path.join(updatedTemplatesDir, "src", "features", feature);
870
+ if (await fs.pathExists(destFeature) && await fs.pathExists(srcFeature)) {
871
+ await fs.copy(srcFeature, destFeature, { overwrite: true });
872
+ }
873
+ }
874
+ }
875
+ if (filesToUpdate.includes("pages")) {
876
+ const pagesDir = path.join(targetDir, "src", "pages");
877
+ const srcPagesDir = path.join(updatedTemplatesDir, "src", "pages");
878
+ if (await fs.pathExists(pagesDir) && await fs.pathExists(srcPagesDir)) {
879
+ const existingPages = await fs.readdir(pagesDir);
880
+ for (const pageFile of existingPages) {
881
+ const src = path.join(srcPagesDir, pageFile);
882
+ if (await fs.pathExists(src)) {
883
+ await fs.copy(src, path.join(pagesDir, pageFile), { overwrite: true });
884
+ }
885
+ }
886
+ }
887
+ }
888
+ if (filesToUpdate.includes("config")) {
889
+ const configFiles = ["vite.config.ts", "tsconfig.json", "tsconfig.node.json", "postcss.config.js"];
890
+ for (const file of configFiles) {
891
+ const src = path.join(updatedTemplatesDir, "..", file);
892
+ if (await fs.pathExists(src)) {
893
+ await fs.copy(src, path.join(targetDir, file), { overwrite: true });
894
+ }
895
+ }
733
896
  }
897
+ spinner.succeed(`Project updated to xertica-ui@${targetVersion} successfully!`);
898
+ console.log(chalk.gray("\n Run npm run dev to start the development server."));
734
899
  } catch (error) {
735
- spinner.fail("Failed to update theme");
900
+ spinner.fail("Failed to update project");
736
901
  console.error(error);
737
902
  }
738
903
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "xertica-ui",
3
- "version": "2.0.1",
3
+ "version": "2.0.3",
4
4
  "description": "Xertica UI — Enterprise-grade React design system with Tailwind CSS v4, Radix UI, and AI-first documentation.",
5
5
  "type": "module",
6
6
  "main": "./dist/index.cjs.js",
@@ -1,6 +1,6 @@
1
1
  {
2
- "name": "my-xertica-app",
3
- "version": "2.0.1",
2
+ "name": "xertica-ui-template",
3
+ "version": "2.0.3",
4
4
  "private": true,
5
5
  "type": "module",
6
6
  "scripts": {
@@ -17,7 +17,7 @@
17
17
  "react-dom": "^18.3.1",
18
18
  "react-router-dom": "^7.1.3",
19
19
  "sonner": "^1.7.3",
20
- "xertica-ui": "^2.0.1"
20
+ "xertica-ui": "^2.0.3"
21
21
  },
22
22
  "devDependencies": {
23
23
  "@eslint/js": "^9.18.0",