@llmnative/react 2.1.0 → 2.2.1

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.
Files changed (39) hide show
  1. package/dist/index.css +1 -1
  2. package/dist/index.js +21 -21
  3. package/dist/index.js.map +1 -1
  4. package/dist/index.mjs +10322 -8474
  5. package/dist/index.mjs.map +1 -1
  6. package/dist/types/src/App.d.ts +10 -1
  7. package/dist/types/src/I18n.d.ts +93 -0
  8. package/dist/types/src/components/auth/AuthGate.d.ts +26 -0
  9. package/dist/types/src/components/auth/AuthLayout.d.ts +38 -0
  10. package/dist/types/src/components/auth/ForgotPassword.d.ts +19 -0
  11. package/dist/types/src/components/auth/OneTimeCodeInput.d.ts +27 -0
  12. package/dist/types/src/components/auth/ResetPassword.d.ts +31 -0
  13. package/dist/types/src/components/auth/SignIn.d.ts +42 -0
  14. package/dist/types/src/components/auth/SignUp.d.ts +43 -0
  15. package/dist/types/src/components/auth/VerifyCode.d.ts +32 -0
  16. package/dist/types/src/components/auth/VerifyEmail.d.ts +31 -0
  17. package/dist/types/src/components/auth/index.d.ts +12 -0
  18. package/dist/types/src/components/auth/providers.d.ts +22 -0
  19. package/dist/types/src/components/auth/shared.d.ts +124 -0
  20. package/dist/types/src/components/auth/useAuthFlows.d.ts +46 -0
  21. package/dist/types/src/index.d.ts +5 -1
  22. package/dist/types/src/providers/auth/AuthProvider.d.ts +51 -0
  23. package/dist/types/src/providers/auth/firebase/FirebaseAuthProvider.d.ts +8 -1
  24. package/dist/types/src/providers/auth/github/GitHubAuthProvider.d.ts +2 -0
  25. package/dist/types/src/providers/auth/mock/MockAuthProvider.d.ts +55 -0
  26. package/dist/types/src/providers/auth/supabase/SupabaseAuthProvider.d.ts +16 -1
  27. package/dist/types/src/providers/manifest.d.ts +4 -1
  28. package/llms-full.txt +66 -2
  29. package/package.json +1 -1
  30. package/scripts/cli/setup-project.js +678 -667
  31. package/templates/_shared/conf/access.tsx +23 -0
  32. package/templates/_shared/pages/access/ForgotPasswordPage.tsx +13 -0
  33. package/templates/_shared/pages/access/ResetPasswordPage.tsx +19 -0
  34. package/templates/_shared/pages/access/SignInPage.tsx +21 -0
  35. package/templates/_shared/pages/access/SignUpPage.tsx +23 -0
  36. package/templates/_shared/pages/access/VerifyEmailPage.tsx +20 -0
  37. package/templates/_shared/pages/access/paths.ts +12 -0
  38. package/templates/_shared/sections/Footer.tsx +9 -9
  39. package/templates/_shared/sections/Header.tsx +26 -24
@@ -1,667 +1,678 @@
1
- const fs = require('fs');
2
- const path = require('path');
3
- const readline = require('readline');
4
-
5
- const root = process.cwd();
6
- const args = process.argv.slice(2);
7
- const shouldReset = args.includes('--reset');
8
-
9
- // The built-in themes (BUILT_IN_THEME_IDS of src/Theme.tsx), those of the IDEs first: a new app starts in the
10
- // colors of an IDE, frame included (CR-126).
11
- const THEMES = [
12
- 'vscode-light-modern', 'vscode-dark-modern', 'vscode-light-plus', 'vscode-dark-plus',
13
- 'tokyo-night-light', 'tokyo-night', 'default', 'flat', 'cyber',
14
- ];
15
- const DEFAULT_THEME = 'vscode-light-modern';
16
-
17
- function getArg(name, fallback = '') {
18
- const prefix = `--${name}=`;
19
- const arg = args.find((item) => item.startsWith(prefix));
20
- return arg ? arg.slice(prefix.length) : fallback;
21
- }
22
-
23
- function ensureDir(dirPath) {
24
- if (!fs.existsSync(dirPath)) {
25
- fs.mkdirSync(dirPath, { recursive: true });
26
- console.log(`Created directory: ${dirPath}`);
27
- }
28
- }
29
-
30
- function ensureFile(filePath, content) {
31
- ensureDir(path.dirname(filePath));
32
- if (!fs.existsSync(filePath)) {
33
- fs.writeFileSync(filePath, content.trimStart());
34
- console.log(`Created file: ${filePath}`);
35
- }
36
- }
37
-
38
- function resetProjectDirectory() {
39
- console.log('Resetting scaffold files...');
40
- [
41
- 'src',
42
- 'public',
43
- 'dist',
44
- '.env',
45
- '.env.example',
46
- '.firebaserc',
47
- 'database.rules.json',
48
- 'firestore.rules',
49
- 'firestore.indexes.json',
50
- 'firebase.json',
51
- 'storage.rules',
52
- 'index.html',
53
- 'vite.config.ts',
54
- 'tsconfig.json',
55
- 'tsconfig.node.json',
56
- 'postcss.config.js',
57
- ].forEach((entry) => {
58
- const entryPath = path.join(root, entry);
59
- if (fs.existsSync(entryPath)) {
60
- fs.rmSync(entryPath, { recursive: true, force: true });
61
- console.log(`Removed: ${entryPath}`);
62
- }
63
- });
64
- }
65
-
66
- async function askInteractive(callback) {
67
- const rl = readline.createInterface({
68
- input: process.stdin,
69
- output: process.stdout,
70
- });
71
-
72
- const question = (text) => new Promise((resolve) => rl.question(text, resolve));
73
- const defaultName = path.basename(root);
74
-
75
- try {
76
- const projectnameInput = await question(`What is your project name? (default: ${defaultName}) `);
77
- const projectname = projectnameInput.trim() || defaultName;
78
-
79
- const providerInput = await question('Which data provider? (firebase, supabase, mock, custom; default: mock) ');
80
- const provider = ['firebase', 'supabase', 'mock', 'custom'].includes(providerInput.trim())
81
- ? providerInput.trim()
82
- : 'mock';
83
-
84
- const aiProviderInput = await question('Which AI provider? (none, openai, openrouter, opencode, gemini, anthropic, deepseek, mistral; default: none) ');
85
- const aiProvider = ['none', 'openai', 'openrouter', 'opencode', 'gemini', 'anthropic', 'deepseek', 'mistral'].includes(aiProviderInput.trim())
86
- ? aiProviderInput.trim()
87
- : 'none';
88
-
89
- const proxyProviderInput = await question('Which proxy provider? (none, vite, express, nextjs-app, nextjs-pages, cloudflare; default: none) ');
90
- const proxyProvider = ['none', 'vite', 'express', 'nextjs-app', 'nextjs-pages', 'cloudflare'].includes(proxyProviderInput.trim())
91
- ? proxyProviderInput.trim()
92
- : 'none';
93
-
94
- const iconProviderInput = await question('Which icon provider? (lucide, phosphor; default: lucide) ');
95
- const iconProvider = ['lucide', 'phosphor'].includes(iconProviderInput.trim())
96
- ? iconProviderInput.trim()
97
- : 'lucide';
98
-
99
- const themeInput = await question(`Which theme? (${THEMES.join(', ')}; default: ${DEFAULT_THEME}) `);
100
- const theme = THEMES.includes(themeInput.trim())
101
- ? themeInput.trim()
102
- : DEFAULT_THEME;
103
-
104
- const localeInput = await question('Default locale? (en, it, fr, de, es; default: en) ');
105
- const locale = localeInput.trim() || 'en';
106
-
107
- const templateInput = await question('Which app template? (blank, crm, admin, inventory, project; default: blank) ');
108
- const template = ['blank', 'crm', 'admin', 'inventory', 'project'].includes(templateInput.trim())
109
- ? templateInput.trim()
110
- : 'blank';
111
-
112
- let hosting = 'n';
113
- const firebase = {};
114
- const supabase = {};
115
- const ai = {};
116
-
117
- if (provider === 'firebase') {
118
- const hostingInput = await question(`Firebase Hosting site? Leave blank to use "${defaultName}", type 'n' to disable: `);
119
- hosting = hostingInput.trim();
120
-
121
- firebase.apikey = (await question('Firebase API key: ')).trim();
122
- firebase.authDomain = (await question('Firebase Auth Domain: ')).trim();
123
- firebase.dbUrl = (await question('Firebase Database URL: ')).trim();
124
- firebase.projId = (await question('Firebase Project ID: ')).trim();
125
- firebase.storageBucket = (await question('Firebase Storage Bucket: ')).trim();
126
- firebase.messSenderId = (await question('Firebase Messaging Sender ID: ')).trim();
127
- firebase.appId = (await question('Firebase App ID: ')).trim();
128
- firebase.measurementId = (await question('Firebase Measurement ID: ')).trim();
129
- firebase.googleClientId = (await question('Google Client ID: ')).trim();
130
- }
131
-
132
- if (provider === 'supabase') {
133
- supabase.url = (await question('Supabase URL: ')).trim();
134
- supabase.anonKey = (await question('Supabase anon key: ')).trim();
135
- }
136
-
137
- if (aiProvider === 'openai') {
138
- ai.openaiApiKey = (await question('OpenAI API key: ')).trim();
139
- }
140
- if (aiProvider === 'openrouter') {
141
- ai.openRouterApiKey = (await question('OpenRouter API key: ')).trim();
142
- }
143
- if (aiProvider === 'opencode') {
144
- ai.openCodeApiKey = (await question('OpenCode API key: ')).trim();
145
- }
146
- if (aiProvider === 'gemini') {
147
- ai.geminiApiKey = (await question('Gemini API key: ')).trim();
148
- }
149
- if (aiProvider === 'anthropic') {
150
- ai.anthropicApiKey = (await question('Anthropic API key: ')).trim();
151
- }
152
- if (aiProvider === 'deepseek') {
153
- ai.deepSeekApiKey = (await question('DeepSeek API key: ')).trim();
154
- }
155
- if (aiProvider === 'mistral') {
156
- ai.mistralApiKey = (await question('Mistral API key: ')).trim();
157
- }
158
-
159
- rl.close();
160
- callback({ projectname, provider, aiProvider, proxyProvider, iconProvider, theme, locale, template, hosting, firebase, supabase, ai });
161
- } catch (error) {
162
- rl.close();
163
- console.error('Error during interactive prompt:', error);
164
- }
165
- }
166
-
167
- function normalizeFirebaseProjectId(projectName) {
168
- return String(projectName ?? '')
169
- .trim()
170
- .replace(/^[A-Z]/, (m) => m.toLowerCase())
171
- .replace(/[A-Z]/g, (m) => `-${m.toLowerCase()}`)
172
- .replace(/\s+/g, '-')
173
- .replace(/[^a-z0-9-]/g, '-')
174
- .replace(/-+/g, '-')
175
- .replace(/^-+|-+$/g, '');
176
- }
177
-
178
- function createPackageJson(params) {
179
- const packagePath = path.resolve(__dirname, '../../package.json');
180
- const frameworkPackage = JSON.parse(fs.readFileSync(packagePath, 'utf8'));
181
- const version = frameworkPackage.version;
182
-
183
- ensureFile(path.join(root, 'package.json'), JSON.stringify({
184
- name: normalizeFirebaseProjectId(params.projectname) || 'llmnative-app',
185
- version: '0.1.0',
186
- private: true,
187
- type: 'module',
188
- engines: {
189
- node: '>=24.18.0 <25',
190
- },
191
- packageManager: 'npm@11.11.0',
192
- scripts: {
193
- dev: 'vite',
194
- build: 'vite build',
195
- preview: 'vite preview',
196
- typecheck: 'tsc --noEmit',
197
- },
198
- dependencies: {
199
- '@phosphor-icons/react': '^2.1.10',
200
- firebase: '^12.16.0',
201
- 'lucide-react': '^1.26.0',
202
- react: '^19.2.8',
203
- 'react-dom': '^19.2.8',
204
- '@llmnative/react': `^${version}`,
205
- 'react-router-dom': '^7.18.1',
206
- },
207
- devDependencies: {
208
- '@tailwindcss/postcss': '^4.3.3',
209
- '@types/react': '^19.2.17',
210
- '@types/react-dom': '^19.2.3',
211
- '@vitejs/plugin-react': '^6.0.4',
212
- autoprefixer: '^10.5.4',
213
- postcss: '^8.5.23',
214
- // Same compiler the framework is built and type-checked with.
215
- typescript: frameworkPackage.devDependencies.typescript,
216
- vite: '^8.1.5',
217
- },
218
- }, null, 2));
219
- }
220
-
221
- function createIndexHtml(params) {
222
- ensureFile(path.join(root, 'index.html'), `
223
- <!doctype html>
224
- <html lang="en">
225
- <head>
226
- <meta charset="UTF-8" />
227
- <meta name="viewport" content="width=device-width, initial-scale=1.0" />
228
- <title>${params.projectname}</title>
229
- </head>
230
- <body>
231
- <div id="root"></div>
232
- <script type="module" src="/src/index.tsx"></script>
233
- </body>
234
- </html>
235
- `);
236
- }
237
-
238
- function createEnvFile(params) {
239
- const proxyEnabled = params.proxyProvider && params.proxyProvider !== 'none' ? 'true' : 'false';
240
- const env = [
241
- '# --- Core ---',
242
- `VITE_PROVIDER=${params.provider}`,
243
- `VITE_AI_PROVIDER=${params.aiProvider ?? 'none'}`,
244
- `VITE_ICON_PROVIDER=${params.iconProvider}`,
245
- `VITE_THEME=${params.theme}`,
246
- `VITE_LOCALE=${params.locale ?? 'en'}`,
247
- '',
248
- '# --- Proxy (required for AI calls from the browser) ---',
249
- `VITE_PROXY_PROVIDER=${params.proxyProvider ?? 'none'}`,
250
- `VITE_PROXY_ENABLED=${proxyEnabled}`,
251
- '',
252
- '# --- Firebase ---',
253
- `VITE_FIREBASE_APIKEY=${params.firebase.apikey ?? ''}`,
254
- `VITE_FIREBASE_AUTH_DOMAIN=${params.firebase.authDomain ?? ''}`,
255
- `VITE_FIREBASE_DATABASE_URL=${params.firebase.dbUrl ?? ''}`,
256
- `VITE_FIREBASE_PROJECT_ID=${params.firebase.projId ?? ''}`,
257
- `VITE_FIREBASE_STORAGE_BUCKET=${params.firebase.storageBucket ?? ''}`,
258
- `VITE_FIREBASE_MESSAGING_SENDER_ID=${params.firebase.messSenderId ?? ''}`,
259
- `VITE_FIREBASE_APP_ID=${params.firebase.appId ?? ''}`,
260
- `VITE_FIREBASE_MEASUREMENT_ID=${params.firebase.measurementId ?? ''}`,
261
- '',
262
- '# --- Google OAuth ---',
263
- `VITE_GOOGLE_CLIENT_ID=${params.firebase.googleClientId ?? ''}`,
264
- 'VITE_GOOGLE_SCOPE=',
265
- '',
266
- '# --- Supabase ---',
267
- `VITE_SUPABASE_URL=${params.supabase.url ?? ''}`,
268
- `VITE_SUPABASE_ANON_KEY=${params.supabase.anonKey ?? ''}`,
269
- '',
270
- '# --- Dropbox ---',
271
- 'VITE_DROPBOX_CLIENT_ID=',
272
- 'VITE_DROPBOX_ROOT_PATH=',
273
- '',
274
- '# --- AI providers ---',
275
- `VITE_OPENAI_API_KEY=${params.ai.openaiApiKey ?? ''}`,
276
- `VITE_OPENROUTER_API_KEY=${params.ai.openRouterApiKey ?? ''}`,
277
- `VITE_OPENCODE_API_KEY=${params.ai.openCodeApiKey ?? ''}`,
278
- `VITE_GEMINI_API_KEY=${params.ai.geminiApiKey ?? ''}`,
279
- `VITE_ANTHROPIC_API_KEY=${params.ai.anthropicApiKey ?? ''}`,
280
- `VITE_DEEPSEEK_API_KEY=${params.ai.deepSeekApiKey ?? ''}`,
281
- `VITE_MISTRAL_API_KEY=${params.ai.mistralApiKey ?? ''}`,
282
- '',
283
- '# --- OpenAI-compatible (leave BASE_URL empty to disable) ---',
284
- 'VITE_OPENAI_COMPATIBLE_BASE_URL=',
285
- 'VITE_OPENAI_COMPATIBLE_API_KEY=',
286
- ].join('\n');
287
-
288
- ensureFile(path.join(root, '.env'), `${env}\n`);
289
-
290
- const example = env
291
- .split('\n')
292
- .map(line => (line && !line.startsWith('#') && line.includes('=') ? line.split('=')[0] + '=' : line))
293
- .join('\n');
294
- ensureFile(path.join(root, '.env.example'), `${example}\n`);
295
- }
296
-
297
- function createFirebaseConfig(params) {
298
- if (params.provider !== 'firebase') return;
299
-
300
- ensureFile(path.join(root, '.firebaserc'), JSON.stringify({
301
- projects: { default: params.projectname },
302
- }, null, 2));
303
-
304
- const includeHosting = params.hosting.toLowerCase() !== 'n';
305
- const hostingSite = params.hosting === '' ? params.projectname : params.hosting;
306
-
307
- ensureFile(path.join(root, 'firebase.json'), JSON.stringify({
308
- firestore: { rules: 'firestore.rules', indexes: 'firestore.indexes.json' },
309
- storage: { rules: 'storage.rules' },
310
- ...(includeHosting && {
311
- hosting: {
312
- site: normalizeFirebaseProjectId(hostingSite),
313
- public: 'dist',
314
- ignore: ['firebase.json', '**/.*', '**/node_modules/**'],
315
- predeploy: ['npm run build'],
316
- rewrites: [{ source: '**', destination: '/index.html' }],
317
- },
318
- }),
319
- }, null, 2));
320
-
321
- ensureFile(path.join(root, 'firestore.rules'), `
322
- rules_version = '2';
323
-
324
- service cloud.firestore {
325
- match /databases/{database}/documents {
326
- match /{document=**} {
327
- allow read, write: if request.auth != null;
328
- }
329
- }
330
- }
331
- `);
332
-
333
- ensureFile(path.join(root, 'firestore.indexes.json'), JSON.stringify({
334
- indexes: [],
335
- fieldOverrides: [],
336
- }, null, 2));
337
-
338
- ensureFile(path.join(root, 'storage.rules'), `
339
- rules_version = '2';
340
-
341
- service firebase.storage {
342
- match /b/{bucket}/o {
343
- match /{allPaths=**} {
344
- allow read, write: if request.auth != null;
345
- }
346
- }
347
- }
348
- `);
349
- }
350
-
351
- function copyDir(src, dest) {
352
- if (!fs.existsSync(src)) return;
353
- fs.readdirSync(src, { withFileTypes: true }).forEach((entry) => {
354
- const srcPath = path.join(src, entry.name);
355
- const destPath = path.join(dest, entry.name);
356
- if (entry.isDirectory()) {
357
- copyDir(srcPath, destPath);
358
- } else {
359
- ensureDir(path.dirname(destPath));
360
- if (!fs.existsSync(destPath)) {
361
- fs.copyFileSync(srcPath, destPath);
362
- console.log(`Created file: ${destPath}`);
363
- }
364
- }
365
- });
366
- }
367
-
368
- function substituteProjectName(filePath, projectname) {
369
- const content = fs.readFileSync(filePath, 'utf8');
370
- const updated = content.replace(/\[projectname\]/g, projectname);
371
- if (updated !== content) fs.writeFileSync(filePath, updated);
372
- }
373
-
374
- function copyTemplateFiles(params) {
375
- const sharedSrc = path.resolve(__dirname, '../../templates/_shared');
376
- const templateSrc = path.resolve(__dirname, `../../templates/${params.template}`);
377
-
378
- if (!fs.existsSync(sharedSrc) || !fs.existsSync(templateSrc)) {
379
- throw new Error(`Template "${params.template}" not found in ${path.dirname(templateSrc)}`);
380
- }
381
-
382
- // Copy shared layouts and sections
383
- copyDir(path.join(sharedSrc, 'layouts'), path.join(root, 'src/layouts'));
384
- copyDir(path.join(sharedSrc, 'sections'), path.join(root, 'src/sections'));
385
-
386
- // Copy template-specific files (pages, data, conf/menu.ts)
387
- // layouts and sections from template override shared ones if present
388
- copyDir(templateSrc, path.join(root, 'src'));
389
-
390
- // Substitute [projectname] in copied files
391
- ['src/layouts', 'src/sections', 'src/pages'].forEach((dir) => {
392
- const absDir = path.join(root, dir);
393
- if (!fs.existsSync(absDir)) return;
394
- walkFiles(absDir, (file) => substituteProjectName(file, params.projectname));
395
- });
396
- }
397
-
398
- function walkFiles(dir, fn) {
399
- fs.readdirSync(dir, { withFileTypes: true }).forEach((entry) => {
400
- const full = path.join(dir, entry.name);
401
- if (entry.isDirectory()) walkFiles(full, fn);
402
- else fn(full);
403
- });
404
- }
405
-
406
- function createSourceFiles(params) {
407
- copyTemplateFiles(params);
408
-
409
- ensureFile(path.join(root, 'src/vite-env.d.ts'), `
410
- /// <reference types="vite/client" />
411
- `);
412
-
413
- ensureFile(path.join(root, 'src/styles/globals.css'), `
414
- @import "tailwindcss";
415
- @import "@llmnative/react/dist/index.css";
416
-
417
- @theme inline {
418
- --color-background: hsl(var(--rf-background));
419
- --color-foreground: hsl(var(--rf-foreground));
420
- --color-canvas: hsl(var(--rf-canvas, var(--rf-background)));
421
- --color-card: hsl(var(--rf-card));
422
- --color-card-foreground: hsl(var(--rf-card-foreground));
423
- --color-popover: hsl(var(--rf-popover));
424
- --color-popover-foreground: hsl(var(--rf-popover-foreground));
425
- --color-primary: hsl(var(--rf-primary));
426
- --color-primary-foreground: hsl(var(--rf-primary-foreground));
427
- --color-secondary: hsl(var(--rf-secondary));
428
- --color-secondary-foreground: hsl(var(--rf-secondary-foreground));
429
- --color-muted: hsl(var(--rf-muted));
430
- --color-muted-foreground: hsl(var(--rf-muted-foreground));
431
- --color-accent: hsl(var(--rf-accent));
432
- --color-accent-foreground: hsl(var(--rf-accent-foreground));
433
- --color-destructive: hsl(var(--rf-destructive));
434
- --color-destructive-foreground: hsl(var(--rf-destructive-foreground));
435
- --color-success: hsl(var(--rf-success));
436
- --color-success-foreground: hsl(var(--rf-success-foreground));
437
- --color-warning: hsl(var(--rf-warning));
438
- --color-warning-foreground: hsl(var(--rf-warning-foreground));
439
- --color-info: hsl(var(--rf-info));
440
- --color-info-foreground: hsl(var(--rf-info-foreground));
441
- --color-border: hsl(var(--rf-border));
442
- --color-input: hsl(var(--rf-input));
443
- --color-ring: hsl(var(--rf-ring));
444
- /* The frame of the app: the title bar, side bar and status bar of the theme. */
445
- --color-header: hsl(var(--rf-header, var(--rf-background)));
446
- --color-header-foreground: hsl(var(--rf-header-foreground, var(--rf-foreground)));
447
- --color-header-border: hsl(var(--rf-header-border, var(--rf-border)));
448
- --color-sidebar: hsl(var(--rf-sidebar, var(--rf-card)));
449
- --color-sidebar-foreground: hsl(var(--rf-sidebar-foreground, var(--rf-foreground)));
450
- --color-sidebar-border: hsl(var(--rf-sidebar-border, var(--rf-border)));
451
- --color-sidebar-accent: hsl(var(--rf-sidebar-accent, var(--rf-accent)));
452
- --color-sidebar-accent-foreground: hsl(var(--rf-sidebar-accent-foreground, var(--rf-accent-foreground)));
453
- --color-footer: hsl(var(--rf-footer, var(--rf-background)));
454
- --color-footer-foreground: hsl(var(--rf-footer-foreground, var(--rf-muted-foreground)));
455
- --color-footer-border: hsl(var(--rf-footer-border, var(--rf-border)));
456
- --radius: 0.5rem;
457
- }
458
-
459
- html, body, #root {
460
- min-height: 100%;
461
- }
462
-
463
- body {
464
- margin: 0;
465
- font-family: var(--font-sans);
466
- background-color: hsl(var(--rf-background));
467
- color: hsl(var(--rf-foreground));
468
- }
469
- `);
470
-
471
- ensureFile(path.join(root, 'src/conf/app.ts'), `
472
- import type { AIConfig, AppProvidersConfig } from '@llmnative/react';
473
- import { mockData } from '../data/mockData';
474
-
475
- const env = import.meta.env;
476
- const selectedProvider = env.VITE_PROVIDER ?? '${params.provider}';
477
- const selectedAIProvider = env.VITE_AI_PROVIDER ?? '${params.aiProvider ?? 'none'}';
478
-
479
- const dataDriver =
480
- selectedProvider === 'firebase' ? 'firestoreDb'
481
- : selectedProvider === 'supabase' ? 'supabaseDb'
482
- : 'mock';
483
-
484
- const storageDriver =
485
- selectedProvider === 'firebase' ? 'firestorage'
486
- : selectedProvider === 'supabase' ? 'supabaseStorage'
487
- : undefined;
488
-
489
- const authDriver =
490
- selectedProvider === 'firebase' ? 'firebaseAuth'
491
- : selectedProvider === 'supabase' ? 'supabaseAuth'
492
- : 'googleAuth';
493
-
494
- const aiDriver = selectedAIProvider !== 'none'
495
- ? selectedAIProvider
496
- : undefined;
497
-
498
- export const appConfig = {
499
- provider: selectedProvider,
500
- aiProvider: selectedAIProvider,
501
- iconProvider: env.VITE_ICON_PROVIDER ?? '${params.iconProvider}',
502
- theme: env.VITE_THEME ?? '${params.theme}',
503
- locale: env.VITE_LOCALE ?? '${params.locale}',
504
- };
505
-
506
- export const aiConfig: AIConfig = {
507
- openaiApiKey: env.VITE_OPENAI_API_KEY ?? '',
508
- openRouterApiKey: env.VITE_OPENROUTER_API_KEY ?? '',
509
- openCodeApiKey: env.VITE_OPENCODE_API_KEY ?? '',
510
- geminiApiKey: env.VITE_GEMINI_API_KEY ?? '',
511
- anthropicApiKey: env.VITE_ANTHROPIC_API_KEY ?? '',
512
- deepSeekApiKey: env.VITE_DEEPSEEK_API_KEY ?? '',
513
- mistralApiKey: env.VITE_MISTRAL_API_KEY ?? '',
514
- ...(env.VITE_OPENAI_COMPATIBLE_BASE_URL ? {
515
- openAICompatible: {
516
- apiKey: env.VITE_OPENAI_COMPATIBLE_API_KEY ?? '',
517
- baseUrl: env.VITE_OPENAI_COMPATIBLE_BASE_URL,
518
- },
519
- } : {}),
520
- };
521
-
522
- export const providers: AppProvidersConfig = {
523
- proxy: {
524
- enabled: env.VITE_PROXY_ENABLED === 'true',
525
- },
526
- mock: {
527
- data: mockData,
528
- },
529
- firebase: {
530
- apiKey: env.VITE_FIREBASE_APIKEY ?? '',
531
- authDomain: env.VITE_FIREBASE_AUTH_DOMAIN ?? '',
532
- databaseURL: env.VITE_FIREBASE_DATABASE_URL ?? '',
533
- projectId: env.VITE_FIREBASE_PROJECT_ID ?? '',
534
- storageBucket: env.VITE_FIREBASE_STORAGE_BUCKET ?? '',
535
- messagingSenderId: env.VITE_FIREBASE_MESSAGING_SENDER_ID ?? '',
536
- appId: env.VITE_FIREBASE_APP_ID ?? '',
537
- measurementId: env.VITE_FIREBASE_MEASUREMENT_ID ?? '',
538
- },
539
- supabase: {
540
- url: env.VITE_SUPABASE_URL ?? '',
541
- anonKey: env.VITE_SUPABASE_ANON_KEY ?? '',
542
- },
543
- google: {
544
- clientId: env.VITE_GOOGLE_CLIENT_ID ?? '',
545
- scope: env.VITE_GOOGLE_SCOPE ?? '',
546
- },
547
- dropbox: {
548
- clientId: env.VITE_DROPBOX_CLIENT_ID ?? '',
549
- rootPath: env.VITE_DROPBOX_ROOT_PATH ?? '',
550
- },
551
- ai: aiConfig,
552
- services: {
553
- data: dataDriver,
554
- ...(storageDriver ? { storage: storageDriver } : {}),
555
- auth: authDriver,
556
- ...(aiDriver ? { ai: aiDriver } : {}),
557
- },
558
- };
559
- `);
560
-
561
- ensureFile(path.join(root, 'src/index.tsx'), `
562
- import React from 'react';
563
- import { createRoot } from 'react-dom/client';
564
- import { App } from '@llmnative/react';
565
- import './styles/globals.css';
566
-
567
- import { appConfig, providers } from './conf/app';
568
- import { menu } from './conf/menu';
569
- import Default from './layouts/Default';
570
-
571
- createRoot(document.getElementById('root')!).render(
572
- <React.StrictMode>
573
- <App
574
- importPage={(pageSource) => import(/* @vite-ignore */ pageSource)}
575
- menuConfig={menu}
576
- LayoutDefault={Default}
577
- providers={providers}
578
- iconProvider={appConfig.iconProvider}
579
- themeProvider={{
580
- theme: appConfig.theme,
581
- // themeOverride: {
582
- // Modal: { size: 'xl' },
583
- // ActionButton: { className: 'btn-primary font-semibold' },
584
- // },
585
- }}
586
- i18n={{
587
- locale: appConfig.locale,
588
- // translations: {
589
- // it: { common: { save: 'Salva', cancel: 'Annulla' }, form: { buttonSave: 'Salva' } },
590
- // },
591
- }}
592
- />
593
- </React.StrictMode>
594
- );
595
- `);
596
- }
597
-
598
- function scaffoldProject() {
599
- const nonInteractive = args.includes('--yes')
600
- || args.some((arg) => arg.startsWith('--provider='))
601
- || args.some((arg) => arg.startsWith('--ai-provider='));
602
- if (nonInteractive) {
603
- const defaultName = path.basename(root);
604
- const params = {
605
- projectname: getArg('name', defaultName),
606
- provider: getArg('provider', 'mock'),
607
- aiProvider: getArg('ai-provider', 'none'),
608
- proxyProvider: getArg('proxy-provider', 'none'),
609
- iconProvider: getArg('icon-provider', 'lucide'),
610
- theme: getArg('theme', getArg('theme-provider', DEFAULT_THEME)),
611
- locale: getArg('locale', 'en'),
612
- template: getArg('template', 'blank'),
613
- hosting: getArg('hosting', 'n'),
614
- firebase: {
615
- apikey: getArg('firebase-api-key'),
616
- authDomain: getArg('firebase-auth-domain'),
617
- dbUrl: getArg('firebase-database-url'),
618
- projId: getArg('firebase-project-id'),
619
- storageBucket: getArg('firebase-storage-bucket'),
620
- messSenderId: getArg('firebase-messaging-sender-id'),
621
- appId: getArg('firebase-app-id'),
622
- measurementId: getArg('firebase-measurement-id'),
623
- googleClientId:getArg('google-client-id'),
624
- },
625
- supabase: {
626
- url: getArg('supabase-url'),
627
- anonKey: getArg('supabase-anon-key'),
628
- },
629
- ai: {
630
- openaiApiKey: getArg('openai-api-key'),
631
- openRouterApiKey: getArg('openrouter-api-key'),
632
- openCodeApiKey: getArg('opencode-api-key'),
633
- geminiApiKey: getArg('gemini-api-key'),
634
- anthropicApiKey: getArg('anthropic-api-key'),
635
- deepSeekApiKey: getArg('deepseek-api-key'),
636
- mistralApiKey: getArg('mistral-api-key'),
637
- },
638
- };
639
-
640
- if (shouldReset) resetProjectDirectory();
641
-
642
- console.log(`\nCreating project: ${params.projectname} | theme: ${params.theme} | template: ${params.template}\n`);
643
- createPackageJson(params);
644
- require('./setup-devtools').setupDevTools({ proxyProvider: params.proxyProvider });
645
- createIndexHtml(params);
646
- createEnvFile(params);
647
- createFirebaseConfig(params);
648
- createSourceFiles(params);
649
- console.log(`\nDone. Run: npm install && npm run dev\n`);
650
- return;
651
- }
652
-
653
- askInteractive((params) => {
654
- if (shouldReset) resetProjectDirectory();
655
-
656
- console.log(`\nCreating project: ${params.projectname} | theme: ${params.theme} | template: ${params.template}\n`);
657
- createPackageJson(params);
658
- require('./setup-devtools').setupDevTools({ proxyProvider: params.proxyProvider });
659
- createIndexHtml(params);
660
- createEnvFile(params);
661
- createFirebaseConfig(params);
662
- createSourceFiles(params);
663
- console.log(`\nDone. Run: npm install && npm run dev\n`);
664
- });
665
- }
666
-
667
- module.exports = { scaffoldProject };
1
+ const fs = require('fs');
2
+ const path = require('path');
3
+ const readline = require('readline');
4
+
5
+ const root = process.cwd();
6
+ const args = process.argv.slice(2);
7
+ const shouldReset = args.includes('--reset');
8
+
9
+ // The built-in themes (BUILT_IN_THEME_IDS of src/Theme.tsx), those of the IDEs first: a new app starts in the
10
+ // colors of an IDE, frame included (CR-126).
11
+ const THEMES = [
12
+ 'vscode-light-modern', 'vscode-dark-modern', 'vscode-light-plus', 'vscode-dark-plus',
13
+ 'tokyo-night-light', 'tokyo-night', 'default', 'flat', 'cyber',
14
+ ];
15
+ const DEFAULT_THEME = 'vscode-light-modern';
16
+
17
+ function getArg(name, fallback = '') {
18
+ const prefix = `--${name}=`;
19
+ const arg = args.find((item) => item.startsWith(prefix));
20
+ return arg ? arg.slice(prefix.length) : fallback;
21
+ }
22
+
23
+ function ensureDir(dirPath) {
24
+ if (!fs.existsSync(dirPath)) {
25
+ fs.mkdirSync(dirPath, { recursive: true });
26
+ console.log(`Created directory: ${dirPath}`);
27
+ }
28
+ }
29
+
30
+ function ensureFile(filePath, content) {
31
+ ensureDir(path.dirname(filePath));
32
+ if (!fs.existsSync(filePath)) {
33
+ fs.writeFileSync(filePath, content.trimStart());
34
+ console.log(`Created file: ${filePath}`);
35
+ }
36
+ }
37
+
38
+ function resetProjectDirectory() {
39
+ console.log('Resetting scaffold files...');
40
+ [
41
+ 'src',
42
+ 'public',
43
+ 'dist',
44
+ '.env',
45
+ '.env.example',
46
+ '.firebaserc',
47
+ 'database.rules.json',
48
+ 'firestore.rules',
49
+ 'firestore.indexes.json',
50
+ 'firebase.json',
51
+ 'storage.rules',
52
+ 'index.html',
53
+ 'vite.config.ts',
54
+ 'tsconfig.json',
55
+ 'tsconfig.node.json',
56
+ 'postcss.config.js',
57
+ ].forEach((entry) => {
58
+ const entryPath = path.join(root, entry);
59
+ if (fs.existsSync(entryPath)) {
60
+ fs.rmSync(entryPath, { recursive: true, force: true });
61
+ console.log(`Removed: ${entryPath}`);
62
+ }
63
+ });
64
+ }
65
+
66
+ async function askInteractive(callback) {
67
+ const rl = readline.createInterface({
68
+ input: process.stdin,
69
+ output: process.stdout,
70
+ });
71
+
72
+ const question = (text) => new Promise((resolve) => rl.question(text, resolve));
73
+ const defaultName = path.basename(root);
74
+
75
+ try {
76
+ const projectnameInput = await question(`What is your project name? (default: ${defaultName}) `);
77
+ const projectname = projectnameInput.trim() || defaultName;
78
+
79
+ const providerInput = await question('Which data provider? (firebase, supabase, mock, custom; default: mock) ');
80
+ const provider = ['firebase', 'supabase', 'mock', 'custom'].includes(providerInput.trim())
81
+ ? providerInput.trim()
82
+ : 'mock';
83
+
84
+ const aiProviderInput = await question('Which AI provider? (none, openai, openrouter, opencode, gemini, anthropic, deepseek, mistral; default: none) ');
85
+ const aiProvider = ['none', 'openai', 'openrouter', 'opencode', 'gemini', 'anthropic', 'deepseek', 'mistral'].includes(aiProviderInput.trim())
86
+ ? aiProviderInput.trim()
87
+ : 'none';
88
+
89
+ const proxyProviderInput = await question('Which proxy provider? (none, vite, express, nextjs-app, nextjs-pages, cloudflare; default: none) ');
90
+ const proxyProvider = ['none', 'vite', 'express', 'nextjs-app', 'nextjs-pages', 'cloudflare'].includes(proxyProviderInput.trim())
91
+ ? proxyProviderInput.trim()
92
+ : 'none';
93
+
94
+ const iconProviderInput = await question('Which icon provider? (lucide, phosphor; default: lucide) ');
95
+ const iconProvider = ['lucide', 'phosphor'].includes(iconProviderInput.trim())
96
+ ? iconProviderInput.trim()
97
+ : 'lucide';
98
+
99
+ const themeInput = await question(`Which theme? (${THEMES.join(', ')}; default: ${DEFAULT_THEME}) `);
100
+ const theme = THEMES.includes(themeInput.trim())
101
+ ? themeInput.trim()
102
+ : DEFAULT_THEME;
103
+
104
+ const localeInput = await question('Default locale? (en, it, fr, de, es; default: en) ');
105
+ const locale = localeInput.trim() || 'en';
106
+
107
+ const templateInput = await question('Which app template? (blank, crm, admin, inventory, project; default: blank) ');
108
+ const template = ['blank', 'crm', 'admin', 'inventory', 'project'].includes(templateInput.trim())
109
+ ? templateInput.trim()
110
+ : 'blank';
111
+
112
+ let hosting = 'n';
113
+ const firebase = {};
114
+ const supabase = {};
115
+ const ai = {};
116
+
117
+ if (provider === 'firebase') {
118
+ const hostingInput = await question(`Firebase Hosting site? Leave blank to use "${defaultName}", type 'n' to disable: `);
119
+ hosting = hostingInput.trim();
120
+
121
+ firebase.apikey = (await question('Firebase API key: ')).trim();
122
+ firebase.authDomain = (await question('Firebase Auth Domain: ')).trim();
123
+ firebase.dbUrl = (await question('Firebase Database URL: ')).trim();
124
+ firebase.projId = (await question('Firebase Project ID: ')).trim();
125
+ firebase.storageBucket = (await question('Firebase Storage Bucket: ')).trim();
126
+ firebase.messSenderId = (await question('Firebase Messaging Sender ID: ')).trim();
127
+ firebase.appId = (await question('Firebase App ID: ')).trim();
128
+ firebase.measurementId = (await question('Firebase Measurement ID: ')).trim();
129
+ firebase.googleClientId = (await question('Google Client ID: ')).trim();
130
+ }
131
+
132
+ if (provider === 'supabase') {
133
+ supabase.url = (await question('Supabase URL: ')).trim();
134
+ supabase.anonKey = (await question('Supabase anon key: ')).trim();
135
+ }
136
+
137
+ if (aiProvider === 'openai') {
138
+ ai.openaiApiKey = (await question('OpenAI API key: ')).trim();
139
+ }
140
+ if (aiProvider === 'openrouter') {
141
+ ai.openRouterApiKey = (await question('OpenRouter API key: ')).trim();
142
+ }
143
+ if (aiProvider === 'opencode') {
144
+ ai.openCodeApiKey = (await question('OpenCode API key: ')).trim();
145
+ }
146
+ if (aiProvider === 'gemini') {
147
+ ai.geminiApiKey = (await question('Gemini API key: ')).trim();
148
+ }
149
+ if (aiProvider === 'anthropic') {
150
+ ai.anthropicApiKey = (await question('Anthropic API key: ')).trim();
151
+ }
152
+ if (aiProvider === 'deepseek') {
153
+ ai.deepSeekApiKey = (await question('DeepSeek API key: ')).trim();
154
+ }
155
+ if (aiProvider === 'mistral') {
156
+ ai.mistralApiKey = (await question('Mistral API key: ')).trim();
157
+ }
158
+
159
+ rl.close();
160
+ callback({ projectname, provider, aiProvider, proxyProvider, iconProvider, theme, locale, template, hosting, firebase, supabase, ai });
161
+ } catch (error) {
162
+ rl.close();
163
+ console.error('Error during interactive prompt:', error);
164
+ }
165
+ }
166
+
167
+ function normalizeFirebaseProjectId(projectName) {
168
+ return String(projectName ?? '')
169
+ .trim()
170
+ .replace(/^[A-Z]/, (m) => m.toLowerCase())
171
+ .replace(/[A-Z]/g, (m) => `-${m.toLowerCase()}`)
172
+ .replace(/\s+/g, '-')
173
+ .replace(/[^a-z0-9-]/g, '-')
174
+ .replace(/-+/g, '-')
175
+ .replace(/^-+|-+$/g, '');
176
+ }
177
+
178
+ function createPackageJson(params) {
179
+ const packagePath = path.resolve(__dirname, '../../package.json');
180
+ const frameworkPackage = JSON.parse(fs.readFileSync(packagePath, 'utf8'));
181
+ const version = frameworkPackage.version;
182
+
183
+ ensureFile(path.join(root, 'package.json'), JSON.stringify({
184
+ name: normalizeFirebaseProjectId(params.projectname) || 'llmnative-app',
185
+ version: '0.1.0',
186
+ private: true,
187
+ type: 'module',
188
+ engines: {
189
+ node: '>=24.18.0 <25',
190
+ },
191
+ packageManager: 'npm@11.11.0',
192
+ scripts: {
193
+ dev: 'vite',
194
+ build: 'vite build',
195
+ preview: 'vite preview',
196
+ typecheck: 'tsc --noEmit',
197
+ },
198
+ dependencies: {
199
+ '@phosphor-icons/react': '^2.1.10',
200
+ firebase: '^12.16.0',
201
+ 'lucide-react': '^1.26.0',
202
+ react: '^19.2.8',
203
+ 'react-dom': '^19.2.8',
204
+ '@llmnative/react': `^${version}`,
205
+ 'react-router-dom': '^7.18.1',
206
+ },
207
+ devDependencies: {
208
+ '@tailwindcss/postcss': '^4.3.3',
209
+ '@types/react': '^19.2.17',
210
+ '@types/react-dom': '^19.2.3',
211
+ '@vitejs/plugin-react': '^6.0.4',
212
+ autoprefixer: '^10.5.4',
213
+ postcss: '^8.5.23',
214
+ // Same compiler the framework is built and type-checked with.
215
+ typescript: frameworkPackage.devDependencies.typescript,
216
+ vite: '^8.1.5',
217
+ },
218
+ }, null, 2));
219
+ }
220
+
221
+ function createIndexHtml(params) {
222
+ ensureFile(path.join(root, 'index.html'), `
223
+ <!doctype html>
224
+ <html lang="en">
225
+ <head>
226
+ <meta charset="UTF-8" />
227
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
228
+ <title>${params.projectname}</title>
229
+ </head>
230
+ <body>
231
+ <div id="root"></div>
232
+ <script type="module" src="/src/index.tsx"></script>
233
+ </body>
234
+ </html>
235
+ `);
236
+ }
237
+
238
+ function createEnvFile(params) {
239
+ const proxyEnabled = params.proxyProvider && params.proxyProvider !== 'none' ? 'true' : 'false';
240
+ const env = [
241
+ '# --- Core ---',
242
+ `VITE_PROVIDER=${params.provider}`,
243
+ `VITE_AI_PROVIDER=${params.aiProvider ?? 'none'}`,
244
+ `VITE_ICON_PROVIDER=${params.iconProvider}`,
245
+ `VITE_THEME=${params.theme}`,
246
+ `VITE_LOCALE=${params.locale ?? 'en'}`,
247
+ '',
248
+ '# --- Proxy (required for AI calls from the browser) ---',
249
+ `VITE_PROXY_PROVIDER=${params.proxyProvider ?? 'none'}`,
250
+ `VITE_PROXY_ENABLED=${proxyEnabled}`,
251
+ '',
252
+ '# --- Firebase ---',
253
+ `VITE_FIREBASE_APIKEY=${params.firebase.apikey ?? ''}`,
254
+ `VITE_FIREBASE_AUTH_DOMAIN=${params.firebase.authDomain ?? ''}`,
255
+ `VITE_FIREBASE_DATABASE_URL=${params.firebase.dbUrl ?? ''}`,
256
+ `VITE_FIREBASE_PROJECT_ID=${params.firebase.projId ?? ''}`,
257
+ `VITE_FIREBASE_STORAGE_BUCKET=${params.firebase.storageBucket ?? ''}`,
258
+ `VITE_FIREBASE_MESSAGING_SENDER_ID=${params.firebase.messSenderId ?? ''}`,
259
+ `VITE_FIREBASE_APP_ID=${params.firebase.appId ?? ''}`,
260
+ `VITE_FIREBASE_MEASUREMENT_ID=${params.firebase.measurementId ?? ''}`,
261
+ '',
262
+ '# --- Google OAuth ---',
263
+ `VITE_GOOGLE_CLIENT_ID=${params.firebase.googleClientId ?? ''}`,
264
+ 'VITE_GOOGLE_SCOPE=',
265
+ '',
266
+ '# --- Supabase ---',
267
+ `VITE_SUPABASE_URL=${params.supabase.url ?? ''}`,
268
+ `VITE_SUPABASE_ANON_KEY=${params.supabase.anonKey ?? ''}`,
269
+ '',
270
+ '# --- Dropbox ---',
271
+ 'VITE_DROPBOX_CLIENT_ID=',
272
+ 'VITE_DROPBOX_ROOT_PATH=',
273
+ '',
274
+ '# --- AI providers ---',
275
+ `VITE_OPENAI_API_KEY=${params.ai.openaiApiKey ?? ''}`,
276
+ `VITE_OPENROUTER_API_KEY=${params.ai.openRouterApiKey ?? ''}`,
277
+ `VITE_OPENCODE_API_KEY=${params.ai.openCodeApiKey ?? ''}`,
278
+ `VITE_GEMINI_API_KEY=${params.ai.geminiApiKey ?? ''}`,
279
+ `VITE_ANTHROPIC_API_KEY=${params.ai.anthropicApiKey ?? ''}`,
280
+ `VITE_DEEPSEEK_API_KEY=${params.ai.deepSeekApiKey ?? ''}`,
281
+ `VITE_MISTRAL_API_KEY=${params.ai.mistralApiKey ?? ''}`,
282
+ '',
283
+ '# --- OpenAI-compatible (leave BASE_URL empty to disable) ---',
284
+ 'VITE_OPENAI_COMPATIBLE_BASE_URL=',
285
+ 'VITE_OPENAI_COMPATIBLE_API_KEY=',
286
+ ].join('\n');
287
+
288
+ ensureFile(path.join(root, '.env'), `${env}\n`);
289
+
290
+ const example = env
291
+ .split('\n')
292
+ .map(line => (line && !line.startsWith('#') && line.includes('=') ? line.split('=')[0] + '=' : line))
293
+ .join('\n');
294
+ ensureFile(path.join(root, '.env.example'), `${example}\n`);
295
+ }
296
+
297
+ function createFirebaseConfig(params) {
298
+ if (params.provider !== 'firebase') return;
299
+
300
+ ensureFile(path.join(root, '.firebaserc'), JSON.stringify({
301
+ projects: { default: params.projectname },
302
+ }, null, 2));
303
+
304
+ const includeHosting = params.hosting.toLowerCase() !== 'n';
305
+ const hostingSite = params.hosting === '' ? params.projectname : params.hosting;
306
+
307
+ ensureFile(path.join(root, 'firebase.json'), JSON.stringify({
308
+ firestore: { rules: 'firestore.rules', indexes: 'firestore.indexes.json' },
309
+ storage: { rules: 'storage.rules' },
310
+ ...(includeHosting && {
311
+ hosting: {
312
+ site: normalizeFirebaseProjectId(hostingSite),
313
+ public: 'dist',
314
+ ignore: ['firebase.json', '**/.*', '**/node_modules/**'],
315
+ predeploy: ['npm run build'],
316
+ rewrites: [{ source: '**', destination: '/index.html' }],
317
+ },
318
+ }),
319
+ }, null, 2));
320
+
321
+ ensureFile(path.join(root, 'firestore.rules'), `
322
+ rules_version = '2';
323
+
324
+ service cloud.firestore {
325
+ match /databases/{database}/documents {
326
+ match /{document=**} {
327
+ allow read, write: if request.auth != null;
328
+ }
329
+ }
330
+ }
331
+ `);
332
+
333
+ ensureFile(path.join(root, 'firestore.indexes.json'), JSON.stringify({
334
+ indexes: [],
335
+ fieldOverrides: [],
336
+ }, null, 2));
337
+
338
+ ensureFile(path.join(root, 'storage.rules'), `
339
+ rules_version = '2';
340
+
341
+ service firebase.storage {
342
+ match /b/{bucket}/o {
343
+ match /{allPaths=**} {
344
+ allow read, write: if request.auth != null;
345
+ }
346
+ }
347
+ }
348
+ `);
349
+ }
350
+
351
+ function copyDir(src, dest) {
352
+ if (!fs.existsSync(src)) return;
353
+ fs.readdirSync(src, { withFileTypes: true }).forEach((entry) => {
354
+ const srcPath = path.join(src, entry.name);
355
+ const destPath = path.join(dest, entry.name);
356
+ if (entry.isDirectory()) {
357
+ copyDir(srcPath, destPath);
358
+ } else {
359
+ ensureDir(path.dirname(destPath));
360
+ if (!fs.existsSync(destPath)) {
361
+ fs.copyFileSync(srcPath, destPath);
362
+ console.log(`Created file: ${destPath}`);
363
+ }
364
+ }
365
+ });
366
+ }
367
+
368
+ function substituteProjectName(filePath, projectname) {
369
+ const content = fs.readFileSync(filePath, 'utf8');
370
+ const updated = content.replace(/\[projectname\]/g, projectname);
371
+ if (updated !== content) fs.writeFileSync(filePath, updated);
372
+ }
373
+
374
+ function copyTemplateFiles(params) {
375
+ const sharedSrc = path.resolve(__dirname, '../../templates/_shared');
376
+ const templateSrc = path.resolve(__dirname, `../../templates/${params.template}`);
377
+
378
+ if (!fs.existsSync(sharedSrc) || !fs.existsSync(templateSrc)) {
379
+ throw new Error(`Template "${params.template}" not found in ${path.dirname(templateSrc)}`);
380
+ }
381
+
382
+ // Copy shared layouts and sections
383
+ copyDir(path.join(sharedSrc, 'layouts'), path.join(root, 'src/layouts'));
384
+ copyDir(path.join(sharedSrc, 'sections'), path.join(root, 'src/sections'));
385
+ // The pages of access and their routes (CR-127), the same for every template
386
+ copyDir(path.join(sharedSrc, 'pages'), path.join(root, 'src/pages'));
387
+ copyDir(path.join(sharedSrc, 'conf'), path.join(root, 'src/conf'));
388
+
389
+ // Copy template-specific files (pages, data, conf/menu.ts)
390
+ // layouts and sections from template override shared ones if present
391
+ copyDir(templateSrc, path.join(root, 'src'));
392
+
393
+ // Substitute [projectname] in copied files
394
+ ['src/layouts', 'src/sections', 'src/pages'].forEach((dir) => {
395
+ const absDir = path.join(root, dir);
396
+ if (!fs.existsSync(absDir)) return;
397
+ walkFiles(absDir, (file) => substituteProjectName(file, params.projectname));
398
+ });
399
+ }
400
+
401
+ function walkFiles(dir, fn) {
402
+ fs.readdirSync(dir, { withFileTypes: true }).forEach((entry) => {
403
+ const full = path.join(dir, entry.name);
404
+ if (entry.isDirectory()) walkFiles(full, fn);
405
+ else fn(full);
406
+ });
407
+ }
408
+
409
+ function createSourceFiles(params) {
410
+ copyTemplateFiles(params);
411
+
412
+ ensureFile(path.join(root, 'src/vite-env.d.ts'), `
413
+ /// <reference types="vite/client" />
414
+ `);
415
+
416
+ ensureFile(path.join(root, 'src/styles/globals.css'), `
417
+ @import "tailwindcss";
418
+ @import "@llmnative/react/dist/index.css";
419
+
420
+ @theme inline {
421
+ --color-background: hsl(var(--rf-background));
422
+ --color-foreground: hsl(var(--rf-foreground));
423
+ --color-canvas: hsl(var(--rf-canvas, var(--rf-background)));
424
+ --color-card: hsl(var(--rf-card));
425
+ --color-card-foreground: hsl(var(--rf-card-foreground));
426
+ --color-popover: hsl(var(--rf-popover));
427
+ --color-popover-foreground: hsl(var(--rf-popover-foreground));
428
+ --color-primary: hsl(var(--rf-primary));
429
+ --color-primary-foreground: hsl(var(--rf-primary-foreground));
430
+ --color-secondary: hsl(var(--rf-secondary));
431
+ --color-secondary-foreground: hsl(var(--rf-secondary-foreground));
432
+ --color-muted: hsl(var(--rf-muted));
433
+ --color-muted-foreground: hsl(var(--rf-muted-foreground));
434
+ --color-accent: hsl(var(--rf-accent));
435
+ --color-accent-foreground: hsl(var(--rf-accent-foreground));
436
+ --color-destructive: hsl(var(--rf-destructive));
437
+ --color-destructive-foreground: hsl(var(--rf-destructive-foreground));
438
+ --color-success: hsl(var(--rf-success));
439
+ --color-success-foreground: hsl(var(--rf-success-foreground));
440
+ --color-warning: hsl(var(--rf-warning));
441
+ --color-warning-foreground: hsl(var(--rf-warning-foreground));
442
+ --color-info: hsl(var(--rf-info));
443
+ --color-info-foreground: hsl(var(--rf-info-foreground));
444
+ --color-border: hsl(var(--rf-border));
445
+ --color-input: hsl(var(--rf-input));
446
+ --color-ring: hsl(var(--rf-ring));
447
+ /* The frame of the app: the title bar, side bar and status bar of the theme. */
448
+ --color-header: hsl(var(--rf-header, var(--rf-background)));
449
+ --color-header-foreground: hsl(var(--rf-header-foreground, var(--rf-foreground)));
450
+ --color-header-border: hsl(var(--rf-header-border, var(--rf-border)));
451
+ --color-sidebar: hsl(var(--rf-sidebar, var(--rf-card)));
452
+ --color-sidebar-foreground: hsl(var(--rf-sidebar-foreground, var(--rf-foreground)));
453
+ --color-sidebar-border: hsl(var(--rf-sidebar-border, var(--rf-border)));
454
+ --color-sidebar-accent: hsl(var(--rf-sidebar-accent, var(--rf-accent)));
455
+ --color-sidebar-accent-foreground: hsl(var(--rf-sidebar-accent-foreground, var(--rf-accent-foreground)));
456
+ --color-footer: hsl(var(--rf-footer, var(--rf-background)));
457
+ --color-footer-foreground: hsl(var(--rf-footer-foreground, var(--rf-muted-foreground)));
458
+ --color-footer-border: hsl(var(--rf-footer-border, var(--rf-border)));
459
+ --radius: 0.5rem;
460
+ }
461
+
462
+ html, body, #root {
463
+ min-height: 100%;
464
+ }
465
+
466
+ body {
467
+ margin: 0;
468
+ font-family: var(--font-sans);
469
+ background-color: hsl(var(--rf-background));
470
+ color: hsl(var(--rf-foreground));
471
+ }
472
+ `);
473
+
474
+ ensureFile(path.join(root, 'src/conf/app.ts'), `
475
+ import type { AIConfig, AppProvidersConfig } from '@llmnative/react';
476
+ import { mockData } from '../data/mockData';
477
+
478
+ const env = import.meta.env;
479
+ const selectedProvider = env.VITE_PROVIDER ?? '${params.provider}';
480
+ const selectedAIProvider = env.VITE_AI_PROVIDER ?? '${params.aiProvider ?? 'none'}';
481
+
482
+ const dataDriver =
483
+ selectedProvider === 'firebase' ? 'firestoreDb'
484
+ : selectedProvider === 'supabase' ? 'supabaseDb'
485
+ : 'mock';
486
+
487
+ const storageDriver =
488
+ selectedProvider === 'firebase' ? 'firestorage'
489
+ : selectedProvider === 'supabase' ? 'supabaseStorage'
490
+ : undefined;
491
+
492
+ const authDriver =
493
+ selectedProvider === 'firebase' ? 'firebaseAuth'
494
+ : selectedProvider === 'supabase' ? 'supabaseAuth'
495
+ : selectedProvider === 'mock' ? 'mockAuth'
496
+ : 'googleAuth';
497
+
498
+ const aiDriver = selectedAIProvider !== 'none'
499
+ ? selectedAIProvider
500
+ : undefined;
501
+
502
+ export const appConfig = {
503
+ provider: selectedProvider,
504
+ aiProvider: selectedAIProvider,
505
+ iconProvider: env.VITE_ICON_PROVIDER ?? '${params.iconProvider}',
506
+ theme: env.VITE_THEME ?? '${params.theme}',
507
+ locale: env.VITE_LOCALE ?? '${params.locale}',
508
+ };
509
+
510
+ export const aiConfig: AIConfig = {
511
+ openaiApiKey: env.VITE_OPENAI_API_KEY ?? '',
512
+ openRouterApiKey: env.VITE_OPENROUTER_API_KEY ?? '',
513
+ openCodeApiKey: env.VITE_OPENCODE_API_KEY ?? '',
514
+ geminiApiKey: env.VITE_GEMINI_API_KEY ?? '',
515
+ anthropicApiKey: env.VITE_ANTHROPIC_API_KEY ?? '',
516
+ deepSeekApiKey: env.VITE_DEEPSEEK_API_KEY ?? '',
517
+ mistralApiKey: env.VITE_MISTRAL_API_KEY ?? '',
518
+ ...(env.VITE_OPENAI_COMPATIBLE_BASE_URL ? {
519
+ openAICompatible: {
520
+ apiKey: env.VITE_OPENAI_COMPATIBLE_API_KEY ?? '',
521
+ baseUrl: env.VITE_OPENAI_COMPATIBLE_BASE_URL,
522
+ },
523
+ } : {}),
524
+ };
525
+
526
+ export const providers: AppProvidersConfig = {
527
+ proxy: {
528
+ enabled: env.VITE_PROXY_ENABLED === 'true',
529
+ },
530
+ mock: {
531
+ data: mockData,
532
+ // A sign-in for the demo, with no service behind it: any email and password, code 123456.
533
+ auth: selectedProvider === 'mock',
534
+ },
535
+ firebase: {
536
+ apiKey: env.VITE_FIREBASE_APIKEY ?? '',
537
+ authDomain: env.VITE_FIREBASE_AUTH_DOMAIN ?? '',
538
+ databaseURL: env.VITE_FIREBASE_DATABASE_URL ?? '',
539
+ projectId: env.VITE_FIREBASE_PROJECT_ID ?? '',
540
+ storageBucket: env.VITE_FIREBASE_STORAGE_BUCKET ?? '',
541
+ messagingSenderId: env.VITE_FIREBASE_MESSAGING_SENDER_ID ?? '',
542
+ appId: env.VITE_FIREBASE_APP_ID ?? '',
543
+ measurementId: env.VITE_FIREBASE_MEASUREMENT_ID ?? '',
544
+ },
545
+ supabase: {
546
+ url: env.VITE_SUPABASE_URL ?? '',
547
+ anonKey: env.VITE_SUPABASE_ANON_KEY ?? '',
548
+ },
549
+ google: {
550
+ clientId: env.VITE_GOOGLE_CLIENT_ID ?? '',
551
+ scope: env.VITE_GOOGLE_SCOPE ?? '',
552
+ },
553
+ dropbox: {
554
+ clientId: env.VITE_DROPBOX_CLIENT_ID ?? '',
555
+ rootPath: env.VITE_DROPBOX_ROOT_PATH ?? '',
556
+ },
557
+ ai: aiConfig,
558
+ services: {
559
+ data: dataDriver,
560
+ ...(storageDriver ? { storage: storageDriver } : {}),
561
+ auth: authDriver,
562
+ ...(aiDriver ? { ai: aiDriver } : {}),
563
+ },
564
+ };
565
+ `);
566
+
567
+ ensureFile(path.join(root, 'src/index.tsx'), `
568
+ import React from 'react';
569
+ import { createRoot } from 'react-dom/client';
570
+ import { App, locales } from '@llmnative/react';
571
+ import './styles/globals.css';
572
+
573
+ import { appConfig, providers } from './conf/app';
574
+ import { menu } from './conf/menu';
575
+ import { accessMenu } from './conf/access';
576
+ import Default from './layouts/Default';
577
+ import SignInPage from './pages/access/SignInPage';
578
+
579
+ createRoot(document.getElementById('root')!).render(
580
+ <React.StrictMode>
581
+ <App
582
+ importPage={(pageSource) => import(/* @vite-ignore */ pageSource)}
583
+ menuConfig={{ ...menu, ...accessMenu }}
584
+ LayoutDefault={Default}
585
+ // Without a session every page shows the sign-in in its place, at the same address;
586
+ // the routes of accessMenu are public. Remove it for an app open to everyone.
587
+ authGate={{ fallback: <SignInPage /> }}
588
+ providers={providers}
589
+ iconProvider={appConfig.iconProvider}
590
+ themeProvider={{
591
+ theme: appConfig.theme,
592
+ // themeOverride: {
593
+ // Modal: { size: 'xl' },
594
+ // ActionButton: { className: 'btn-primary font-semibold' },
595
+ // },
596
+ }}
597
+ i18n={{
598
+ locale: appConfig.locale,
599
+ // The texts of the framework in its six languages; change one with a patch of yours:
600
+ // translations: { ...locales, it: { ...locales.it, common: { ...locales.it.common, save: 'Registra' } } },
601
+ translations: { ...locales },
602
+ }}
603
+ />
604
+ </React.StrictMode>
605
+ );
606
+ `);
607
+ }
608
+
609
+ function scaffoldProject() {
610
+ const nonInteractive = args.includes('--yes')
611
+ || args.some((arg) => arg.startsWith('--provider='))
612
+ || args.some((arg) => arg.startsWith('--ai-provider='));
613
+ if (nonInteractive) {
614
+ const defaultName = path.basename(root);
615
+ const params = {
616
+ projectname: getArg('name', defaultName),
617
+ provider: getArg('provider', 'mock'),
618
+ aiProvider: getArg('ai-provider', 'none'),
619
+ proxyProvider: getArg('proxy-provider', 'none'),
620
+ iconProvider: getArg('icon-provider', 'lucide'),
621
+ theme: getArg('theme', getArg('theme-provider', DEFAULT_THEME)),
622
+ locale: getArg('locale', 'en'),
623
+ template: getArg('template', 'blank'),
624
+ hosting: getArg('hosting', 'n'),
625
+ firebase: {
626
+ apikey: getArg('firebase-api-key'),
627
+ authDomain: getArg('firebase-auth-domain'),
628
+ dbUrl: getArg('firebase-database-url'),
629
+ projId: getArg('firebase-project-id'),
630
+ storageBucket: getArg('firebase-storage-bucket'),
631
+ messSenderId: getArg('firebase-messaging-sender-id'),
632
+ appId: getArg('firebase-app-id'),
633
+ measurementId: getArg('firebase-measurement-id'),
634
+ googleClientId:getArg('google-client-id'),
635
+ },
636
+ supabase: {
637
+ url: getArg('supabase-url'),
638
+ anonKey: getArg('supabase-anon-key'),
639
+ },
640
+ ai: {
641
+ openaiApiKey: getArg('openai-api-key'),
642
+ openRouterApiKey: getArg('openrouter-api-key'),
643
+ openCodeApiKey: getArg('opencode-api-key'),
644
+ geminiApiKey: getArg('gemini-api-key'),
645
+ anthropicApiKey: getArg('anthropic-api-key'),
646
+ deepSeekApiKey: getArg('deepseek-api-key'),
647
+ mistralApiKey: getArg('mistral-api-key'),
648
+ },
649
+ };
650
+
651
+ if (shouldReset) resetProjectDirectory();
652
+
653
+ console.log(`\nCreating project: ${params.projectname} | theme: ${params.theme} | template: ${params.template}\n`);
654
+ createPackageJson(params);
655
+ require('./setup-devtools').setupDevTools({ proxyProvider: params.proxyProvider });
656
+ createIndexHtml(params);
657
+ createEnvFile(params);
658
+ createFirebaseConfig(params);
659
+ createSourceFiles(params);
660
+ console.log(`\nDone. Run: npm install && npm run dev\n`);
661
+ return;
662
+ }
663
+
664
+ askInteractive((params) => {
665
+ if (shouldReset) resetProjectDirectory();
666
+
667
+ console.log(`\nCreating project: ${params.projectname} | theme: ${params.theme} | template: ${params.template}\n`);
668
+ createPackageJson(params);
669
+ require('./setup-devtools').setupDevTools({ proxyProvider: params.proxyProvider });
670
+ createIndexHtml(params);
671
+ createEnvFile(params);
672
+ createFirebaseConfig(params);
673
+ createSourceFiles(params);
674
+ console.log(`\nDone. Run: npm install && npm run dev\n`);
675
+ });
676
+ }
677
+
678
+ module.exports = { scaffoldProject };