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/CHANGELOG.md +16 -0
- package/README.md +1 -1
- package/bin/cli.ts +472 -285
- package/components/assistant/index.ts +1 -0
- package/dist/assistant.cjs.js +136 -0
- package/dist/assistant.es.js +137 -1
- package/dist/cli.js +256 -91
- package/dist/components/assistant/index.d.ts +1 -0
- package/dist/utils/demo-responses.d.ts +3 -0
- package/package.json +1 -1
- package/templates/package.json +2 -2
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('
|
|
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
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
{ title: '
|
|
41
|
-
{ title: '
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
const
|
|
73
|
-
const
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
'
|
|
80
|
-
'
|
|
81
|
-
'
|
|
82
|
-
'
|
|
83
|
-
'
|
|
84
|
-
'
|
|
85
|
-
'
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
await fs.copy(
|
|
113
|
-
path.join(templatesDir, 'src', 'app', 'App.tsx'),
|
|
114
|
-
path.join(targetDir, 'src', 'app', 'App.tsx')
|
|
115
|
-
);
|
|
116
|
-
|
|
117
|
-
//
|
|
118
|
-
await fs.
|
|
119
|
-
|
|
120
|
-
path.join(
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
path.join(
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
}
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
import {
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
const
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
console.
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
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();
|