forgestack-os-cli 0.1.0

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 (82) hide show
  1. package/dist/commands/create.d.ts +1 -0
  2. package/dist/commands/create.d.ts.map +1 -0
  3. package/dist/commands/create.js +78 -0
  4. package/dist/commands/create.js.map +1 -0
  5. package/dist/generators/api.d.ts +3 -0
  6. package/dist/generators/api.d.ts.map +1 -0
  7. package/dist/generators/api.js +346 -0
  8. package/dist/generators/api.js.map +1 -0
  9. package/dist/generators/auth.d.ts +2 -0
  10. package/dist/generators/auth.d.ts.map +1 -0
  11. package/dist/generators/auth.js +371 -0
  12. package/dist/generators/auth.js.map +1 -0
  13. package/dist/generators/backend.d.ts +2 -0
  14. package/dist/generators/backend.d.ts.map +1 -0
  15. package/dist/generators/backend.js +875 -0
  16. package/dist/generators/backend.js.map +1 -0
  17. package/dist/generators/common.d.ts +2 -0
  18. package/dist/generators/common.d.ts.map +1 -0
  19. package/dist/generators/common.js +354 -0
  20. package/dist/generators/common.js.map +1 -0
  21. package/dist/generators/database.d.ts +2 -0
  22. package/dist/generators/database.d.ts.map +1 -0
  23. package/dist/generators/database.js +157 -0
  24. package/dist/generators/database.js.map +1 -0
  25. package/dist/generators/docker.d.ts +2 -0
  26. package/dist/generators/docker.d.ts.map +1 -0
  27. package/dist/generators/docker.js +181 -0
  28. package/dist/generators/docker.js.map +1 -0
  29. package/dist/generators/frontend-helpers.d.ts +3 -0
  30. package/dist/generators/frontend-helpers.d.ts.map +1 -0
  31. package/dist/generators/frontend-helpers.js +23 -0
  32. package/dist/generators/frontend-helpers.js.map +1 -0
  33. package/dist/generators/frontend.d.ts +2 -0
  34. package/dist/generators/frontend.d.ts.map +1 -0
  35. package/dist/generators/frontend.js +735 -0
  36. package/dist/generators/frontend.js.map +1 -0
  37. package/dist/generators/index.d.ts +2 -0
  38. package/dist/generators/index.d.ts.map +1 -0
  39. package/dist/generators/index.js +59 -0
  40. package/dist/generators/index.js.map +1 -0
  41. package/dist/generators/nextjs-helpers.d.ts +6 -0
  42. package/dist/generators/nextjs-helpers.d.ts.map +1 -0
  43. package/dist/generators/nextjs-helpers.js +216 -0
  44. package/dist/generators/nextjs-helpers.js.map +1 -0
  45. package/dist/index.d.ts +2 -0
  46. package/dist/index.d.ts.map +1 -0
  47. package/dist/index.js +24 -0
  48. package/dist/index.js.map +1 -0
  49. package/dist/types.d.ts +15 -0
  50. package/dist/types.d.ts.map +1 -0
  51. package/dist/types.js +3 -0
  52. package/dist/types.js.map +1 -0
  53. package/dist/utils/logger.d.ts +9 -0
  54. package/dist/utils/logger.d.ts.map +1 -0
  55. package/dist/utils/logger.js +32 -0
  56. package/dist/utils/logger.js.map +1 -0
  57. package/dist/utils/prompts.d.ts +2 -0
  58. package/dist/utils/prompts.d.ts.map +1 -0
  59. package/dist/utils/prompts.js +107 -0
  60. package/dist/utils/prompts.js.map +1 -0
  61. package/dist/utils/validators.d.ts +2 -0
  62. package/dist/utils/validators.d.ts.map +1 -0
  63. package/dist/utils/validators.js +48 -0
  64. package/dist/utils/validators.js.map +1 -0
  65. package/package.json +49 -0
  66. package/src/commands/create.ts +82 -0
  67. package/src/generators/api.ts +353 -0
  68. package/src/generators/auth.ts +406 -0
  69. package/src/generators/backend.ts +927 -0
  70. package/src/generators/common.ts +377 -0
  71. package/src/generators/database.ts +165 -0
  72. package/src/generators/docker.ts +185 -0
  73. package/src/generators/frontend.ts +783 -0
  74. package/src/generators/index.ts +64 -0
  75. package/src/index.ts +27 -0
  76. package/src/types.ts +16 -0
  77. package/src/utils/logger.ts +31 -0
  78. package/src/utils/prompts.ts +105 -0
  79. package/src/utils/validators.ts +50 -0
  80. package/tests/validators.test.ts +69 -0
  81. package/tsc_output.txt +0 -0
  82. package/tsconfig.json +21 -0
@@ -0,0 +1,783 @@
1
+ import path from 'path';
2
+ import fs from 'fs-extra';
3
+ import { StackConfig } from '../types';
4
+
5
+ export async function generateFrontend(config: StackConfig, frontendDir: string) {
6
+ // For Phase 1 MVP, we'll create a basic React + Vite template
7
+ // In later phases, we'll add support for other frameworks
8
+
9
+ switch (config.frontend) {
10
+ case 'react-vite':
11
+ await generateReactVite(config, frontendDir);
12
+ break;
13
+ case 'nextjs':
14
+ await generateNextJS(config, frontendDir);
15
+ break;
16
+ case 'vue-vite':
17
+ await generateVueVite(config, frontendDir);
18
+ break;
19
+ case 'sveltekit':
20
+ await generateSvelteKit(config, frontendDir);
21
+ break;
22
+ default:
23
+ throw new Error(`Unsupported frontend: ${config.frontend}`);
24
+ }
25
+ }
26
+
27
+ async function generateReactVite(config: StackConfig, frontendDir: string) {
28
+ // Package.json
29
+ const packageJson = {
30
+ name: `${config.projectName}-frontend`,
31
+ version: '0.1.0',
32
+ type: 'module',
33
+ scripts: {
34
+ dev: 'vite',
35
+ build: 'tsc && vite build',
36
+ preview: 'vite preview',
37
+ lint: 'eslint . --ext ts,tsx',
38
+ },
39
+ dependencies: {
40
+ react: '^18.2.0',
41
+ 'react-dom': '^18.2.0',
42
+ 'react-router-dom': '^6.21.3',
43
+ axios: '^1.6.5',
44
+ ...(config.auth === 'clerk' && { '@clerk/clerk-react': '^4.30.7' }),
45
+ ...(config.auth === 'supabase' && { '@supabase/supabase-js': '^2.39.3' }),
46
+ ...(config.auth === 'firebase' && { firebase: '^10.7.2' }),
47
+ },
48
+ devDependencies: {
49
+ '@types/react': '^18.2.48',
50
+ '@types/react-dom': '^18.2.18',
51
+ '@vitejs/plugin-react': '^4.2.1',
52
+ typescript: '^5.3.3',
53
+ vite: '^5.0.11',
54
+ 'tailwindcss': '^3.4.1',
55
+ 'autoprefixer': '^10.4.17',
56
+ 'postcss': '^8.4.33',
57
+ 'eslint': '^8.56.0',
58
+ '@typescript-eslint/eslint-plugin': '^6.19.0',
59
+ '@typescript-eslint/parser': '^6.19.0',
60
+ },
61
+ };
62
+
63
+ await fs.writeJSON(path.join(frontendDir, 'package.json'), packageJson, { spaces: 2 });
64
+
65
+ // Vite config
66
+ const viteConfig = `import { defineConfig } from 'vite'
67
+ import react from '@vitejs/plugin-react'
68
+
69
+ export default defineConfig({
70
+ plugins: [react()],
71
+ server: {
72
+ port: 5173,
73
+ proxy: {
74
+ '/api': {
75
+ target: 'http://localhost:3000',
76
+ changeOrigin: true,
77
+ },
78
+ },
79
+ },
80
+ })
81
+ `;
82
+
83
+ await fs.writeFile(path.join(frontendDir, 'vite.config.ts'), viteConfig);
84
+
85
+ // TypeScript config
86
+ const tsConfig = {
87
+ compilerOptions: {
88
+ target: 'ES2020',
89
+ useDefineForClassFields: true,
90
+ lib: ['ES2020', 'DOM', 'DOM.Iterable'],
91
+ module: 'ESNext',
92
+ skipLibCheck: true,
93
+ moduleResolution: 'bundler',
94
+ allowImportingTsExtensions: true,
95
+ resolveJsonModule: true,
96
+ isolatedModules: true,
97
+ noEmit: true,
98
+ jsx: 'react-jsx',
99
+ strict: true,
100
+ noUnusedLocals: true,
101
+ noUnusedParameters: true,
102
+ noFallthroughCasesInSwitch: true,
103
+ },
104
+ include: ['src'],
105
+ references: [{ path: './tsconfig.node.json' }],
106
+ };
107
+
108
+ await fs.writeJSON(path.join(frontendDir, 'tsconfig.json'), tsConfig, { spaces: 2 });
109
+
110
+ // Tailwind config
111
+ const tailwindConfig = `/** @type {import('tailwindcss').Config} */
112
+ export default {
113
+ content: [
114
+ "./index.html",
115
+ "./src/**/*.{js,ts,jsx,tsx}",
116
+ ],
117
+ theme: {
118
+ extend: {},
119
+ },
120
+ plugins: [],
121
+ }
122
+ `;
123
+
124
+ await fs.writeFile(path.join(frontendDir, 'tailwind.config.js'), tailwindConfig);
125
+
126
+ // PostCSS config
127
+ const postcssConfig = `export default {
128
+ plugins: {
129
+ tailwindcss: {},
130
+ autoprefixer: {},
131
+ },
132
+ }
133
+ `;
134
+
135
+ await fs.writeFile(path.join(frontendDir, 'postcss.config.js'), postcssConfig);
136
+
137
+ // Create src directory structure
138
+ const srcDir = path.join(frontendDir, 'src');
139
+ await fs.ensureDir(srcDir);
140
+ await fs.ensureDir(path.join(srcDir, 'components'));
141
+ await fs.ensureDir(path.join(srcDir, 'pages'));
142
+ await fs.ensureDir(path.join(srcDir, 'lib'));
143
+ await fs.ensureDir(path.join(srcDir, 'hooks'));
144
+
145
+ // index.html
146
+ const indexHtml = `<!doctype html>
147
+ <html lang="en">
148
+ <head>
149
+ <meta charset="UTF-8" />
150
+ <link rel="icon" type="image/svg+xml" href="/vite.svg" />
151
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
152
+ <title>${config.projectName}</title>
153
+ </head>
154
+ <body>
155
+ <div id="root"></div>
156
+ <script type="module" src="/src/main.tsx"></script>
157
+ </body>
158
+ </html>
159
+ `;
160
+
161
+ await fs.writeFile(path.join(frontendDir, 'index.html'), indexHtml);
162
+
163
+ // Main entry point
164
+ const mainTsx = `import React from 'react'
165
+ import ReactDOM from 'react-dom/client'
166
+ import App from './App.tsx'
167
+ import './index.css'
168
+
169
+ ReactDOM.createRoot(document.getElementById('root')!).render(
170
+ <React.StrictMode>
171
+ <App />
172
+ </React.StrictMode>,
173
+ )
174
+ `;
175
+
176
+ await fs.writeFile(path.join(srcDir, 'main.tsx'), mainTsx);
177
+
178
+ // Main CSS
179
+ const indexCss = `@tailwind base;
180
+ @tailwind components;
181
+ @tailwind utilities;
182
+
183
+ :root {
184
+ font-family: Inter, system-ui, Avenir, Helvetica, Arial, sans-serif;
185
+ line-height: 1.5;
186
+ font-weight: 400;
187
+
188
+ color-scheme: light dark;
189
+ color: rgba(255, 255, 255, 0.87);
190
+ background-color: #242424;
191
+
192
+ font-synthesis: none;
193
+ text-rendering: optimizeLegibility;
194
+ -webkit-font-smoothing: antialiased;
195
+ -moz-osx-font-smoothing: grayscale;
196
+ }
197
+
198
+ body {
199
+ margin: 0;
200
+ min-height: 100vh;
201
+ }
202
+ `;
203
+
204
+ await fs.writeFile(path.join(srcDir, 'index.css'), indexCss);
205
+
206
+ // App component
207
+ const appTsx = getAppComponent(config);
208
+ await fs.writeFile(path.join(srcDir, 'App.tsx'), appTsx);
209
+
210
+ // API client
211
+ const apiClient = getApiClient(config);
212
+ await fs.writeFile(path.join(srcDir, 'lib', 'api.ts'), apiClient);
213
+ }
214
+
215
+ function getAppComponent(config: StackConfig): string {
216
+ return `import { BrowserRouter, Routes, Route } from 'react-router-dom'
217
+ import HomePage from './pages/HomePage'
218
+ import DashboardPage from './pages/DashboardPage'
219
+ import LoginPage from './pages/LoginPage'
220
+ import { AuthProvider } from './lib/auth'
221
+ ${config.auth === 'clerk' ? "import { ClerkProvider } from '@clerk/clerk-react'" : ''}
222
+
223
+ function App() {
224
+ ${config.auth === 'clerk' ? `const clerkPubKey = import.meta.env.VITE_CLERK_PUBLISHABLE_KEY;` : ''}
225
+
226
+ return (
227
+ ${config.auth === 'clerk' ? '<ClerkProvider publishableKey={clerkPubKey}>' : ''}
228
+ ${config.auth === 'jwt' ? '<AuthProvider>' : ''}
229
+ <BrowserRouter>
230
+ <Routes>
231
+ <Route path="/" element={<HomePage />} />
232
+ <Route path="/login" element={<LoginPage />} />
233
+ <Route path="/dashboard" element={<DashboardPage />} />
234
+ </Routes>
235
+ </BrowserRouter>
236
+ ${config.auth === 'jwt' ? '</AuthProvider>' : ''}
237
+ ${config.auth === 'clerk' ? '</ClerkProvider>' : ''}
238
+ )
239
+ }
240
+
241
+ export default App
242
+ `;
243
+ }
244
+
245
+ function getApiClient(config: StackConfig): string {
246
+ return `import axios from 'axios';
247
+
248
+ const api = axios.create({
249
+ baseURL: import.meta.env.VITE_API_URL || 'http://localhost:3000/api',
250
+ headers: {
251
+ 'Content-Type': 'application/json',
252
+ },
253
+ });
254
+
255
+ // Request interceptor to add auth token
256
+ api.interceptors.request.use((config) => {
257
+ const token = localStorage.getItem('token');
258
+ if (token) {
259
+ config.headers.Authorization = \`Bearer \${token}\`;
260
+ }
261
+ return config;
262
+ });
263
+
264
+ // Response interceptor for error handling
265
+ api.interceptors.response.use(
266
+ (response) => response,
267
+ (error) => {
268
+ if (error.response?.status === 401) {
269
+ localStorage.removeItem('token');
270
+ window.location.href = '/login';
271
+ }
272
+ return Promise.reject(error);
273
+ }
274
+ );
275
+
276
+ export default api;
277
+ `;
278
+ }
279
+
280
+ async function generateNextJS(config: StackConfig, frontendDir: string) {
281
+ // Package.json
282
+ const packageJson = {
283
+ name: `${config.projectName}-frontend`,
284
+ version: '0.1.0',
285
+ scripts: {
286
+ dev: 'next dev',
287
+ build: 'next build',
288
+ start: 'next start',
289
+ lint: 'next lint',
290
+ },
291
+ dependencies: {
292
+ react: '^18.2.0',
293
+ 'react-dom': '^18.2.0',
294
+ next: '^14.1.0',
295
+ axios: '^1.6.5',
296
+ ...(config.auth === 'clerk' && { '@clerk/nextjs': '^4.29.3' }),
297
+ ...(config.auth === 'supabase' && { '@supabase/supabase-js': '^2.39.3', '@supabase/ssr': '^0.0.10' }),
298
+ ...(config.auth === 'firebase' && { firebase: '^10.7.2' }),
299
+ ...(config.auth === 'authjs' && { 'next-auth': '^4.24.5' }),
300
+ },
301
+ devDependencies: {
302
+ '@types/node': '^20.11.5',
303
+ '@types/react': '^18.2.48',
304
+ '@types/react-dom': '^18.2.18',
305
+ typescript: '^5.3.3',
306
+ tailwindcss: '^3.4.1',
307
+ autoprefixer: '^10.4.17',
308
+ postcss: '^8.4.33',
309
+ eslint: '^8.56.0',
310
+ 'eslint-config-next': '^14.1.0',
311
+ },
312
+ };
313
+
314
+ await fs.writeJSON(path.join(frontendDir, 'package.json'), packageJson, { spaces: 2 });
315
+
316
+ // Next.js config
317
+ const nextConfig = `/** @type {import('next').NextConfig} */
318
+ const nextConfig = {
319
+ reactStrictMode: true,
320
+ }
321
+
322
+ module.exports = nextConfig
323
+ `;
324
+
325
+ await fs.writeFile(path.join(frontendDir, 'next.config.js'), nextConfig);
326
+
327
+ // TypeScript config
328
+ const tsConfig = {
329
+ compilerOptions: {
330
+ target: 'ES2017',
331
+ lib: ['dom', 'dom.iterable', 'esnext'],
332
+ allowJs: true,
333
+ skipLibCheck: true,
334
+ strict: true,
335
+ noEmit: true,
336
+ esModuleInterop: true,
337
+ module: 'esnext',
338
+ moduleResolution: 'bundler',
339
+ resolveJsonModule: true,
340
+ isolatedModules: true,
341
+ jsx: 'preserve',
342
+ incremental: true,
343
+ plugins: [{ name: 'next' }],
344
+ paths: {
345
+ '@/*': ['./*'],
346
+ },
347
+ },
348
+ include: ['next-env.d.ts', '**/*.ts', '**/*.tsx', '.next/types/**/*.ts'],
349
+ exclude: ['node_modules'],
350
+ };
351
+
352
+ await fs.writeJSON(path.join(frontendDir, 'tsconfig.json'), tsConfig, { spaces: 2 });
353
+
354
+ // Tailwind config
355
+ const tailwindConfig = `import type { Config } from 'tailwindcss'
356
+
357
+ const config: Config = {
358
+ content: [
359
+ './pages/**/*.{js,ts,jsx,tsx,mdx}',
360
+ './components/**/*.{js,ts,jsx,tsx,mdx}',
361
+ './app/**/*.{js,ts,jsx,tsx,mdx}',
362
+ ],
363
+ theme: {
364
+ extend: {},
365
+ },
366
+ plugins: [],
367
+ }
368
+ export default config
369
+ `;
370
+
371
+ await fs.writeFile(path.join(frontendDir, 'tailwind.config.ts'), tailwindConfig);
372
+
373
+ // PostCSS config
374
+ const postcssConfig = `module.exports = {
375
+ plugins: {
376
+ tailwindcss: {},
377
+ autoprefixer: {},
378
+ },
379
+ }
380
+ `;
381
+
382
+ await fs.writeFile(path.join(frontendDir, 'postcss.config.js'), postcssConfig);
383
+
384
+ // Create app directory structure
385
+ const appDir = path.join(frontendDir, 'app');
386
+ await fs.ensureDir(appDir);
387
+ await fs.ensureDir(path.join(appDir, 'login'));
388
+ await fs.ensureDir(path.join(appDir, 'dashboard'));
389
+ await fs.ensureDir(path.join(frontendDir, 'components'));
390
+ await fs.ensureDir(path.join(frontendDir, 'lib'));
391
+
392
+ // Root layout
393
+ const layout = getNextJSLayout(config);
394
+ await fs.writeFile(path.join(appDir, 'layout.tsx'), layout);
395
+
396
+ // Home page
397
+ const homePage = `export default function HomePage() {
398
+ return (
399
+ <div className="min-h-screen bg-gray-900 text-white">
400
+ <div className="container mx-auto px-4 py-16">
401
+ <h1 className="text-5xl font-bold mb-4">${config.projectName}</h1>
402
+ <p className="text-xl text-gray-400 mb-8">
403
+ Generated by ForgeStack OS
404
+ </p>
405
+ <div className="space-x-4">
406
+ <a
407
+ href="/login"
408
+ className="inline-block bg-blue-600 hover:bg-blue-700 px-6 py-3 rounded font-semibold"
409
+ >
410
+ Get Started
411
+ </a>
412
+ </div>
413
+ </div>
414
+ </div>
415
+ )
416
+ }
417
+ `;
418
+
419
+ await fs.writeFile(path.join(appDir, 'page.tsx'), homePage);
420
+
421
+ // Login page
422
+ const loginPage = getNextJSLoginPage(config);
423
+ await fs.writeFile(path.join(appDir, 'login', 'page.tsx'), loginPage);
424
+
425
+ // Dashboard page
426
+ const dashboardPage = getNextJSDashboardPage(config);
427
+ await fs.writeFile(path.join(appDir, 'dashboard', 'page.tsx'), dashboardPage);
428
+
429
+ // Global CSS
430
+ const globalsCss = `@tailwind base;
431
+ @tailwind components;
432
+ @tailwind utilities;
433
+
434
+ :root {
435
+ --foreground-rgb: 255, 255, 255;
436
+ --background-start-rgb: 17, 24, 39;
437
+ --background-end-rgb: 0, 0, 0;
438
+ }
439
+
440
+ body {
441
+ color: rgb(var(--foreground-rgb));
442
+ background: linear-gradient(
443
+ to bottom,
444
+ transparent,
445
+ rgb(var(--background-end-rgb))
446
+ )
447
+ rgb(var(--background-start-rgb));
448
+ }
449
+ `;
450
+
451
+ await fs.writeFile(path.join(appDir, 'globals.css'), globalsCss);
452
+
453
+ // API client
454
+ const apiClient = `import axios from 'axios';
455
+
456
+ const api = axios.create({
457
+ baseURL: process.env.NEXT_PUBLIC_API_URL || 'http://localhost:3000/api',
458
+ headers: {
459
+ 'Content-Type': 'application/json',
460
+ },
461
+ });
462
+
463
+ // Request interceptor
464
+ api.interceptors.request.use((config) => {
465
+ if (typeof window !== 'undefined') {
466
+ const token = localStorage.getItem('token');
467
+ if (token) {
468
+ config.headers.Authorization = \`Bearer \${token}\`;
469
+ }
470
+ }
471
+ return config;
472
+ });
473
+
474
+ // Response interceptor
475
+ api.interceptors.response.use(
476
+ (response) => response,
477
+ (error) => {
478
+ if (error.response?.status === 401) {
479
+ if (typeof window !== 'undefined') {
480
+ localStorage.removeItem('token');
481
+ window.location.href = '/login';
482
+ }
483
+ }
484
+ return Promise.reject(error);
485
+ }
486
+ );
487
+
488
+ export default api;
489
+ `;
490
+
491
+ await fs.writeFile(path.join(frontendDir, 'lib', 'api.ts'), apiClient);
492
+
493
+ // Middleware for auth (if using Clerk or custom JWT)
494
+ if (config.auth === 'clerk') {
495
+ const middleware = `import { authMiddleware } from '@clerk/nextjs';
496
+
497
+ export default authMiddleware({
498
+ publicRoutes: ['/', '/login'],
499
+ });
500
+
501
+ export const config = {
502
+ matcher: ['/((?!.+\\\\.[\\\\w]+$|_next).*)', '/', '/(api|trpc)(.*)'],
503
+ };
504
+ `;
505
+ await fs.writeFile(path.join(frontendDir, 'middleware.ts'), middleware);
506
+ } else if (config.auth === 'jwt') {
507
+ const middleware = `import { NextResponse } from 'next/server';
508
+ import type { NextRequest } from 'next/server';
509
+
510
+ export function middleware(request: NextRequest) {
511
+ const token = request.cookies.get('token')?.value;
512
+ const isAuthPage = request.nextUrl.pathname.startsWith('/login');
513
+ const isProtectedPage = request.nextUrl.pathname.startsWith('/dashboard');
514
+
515
+ if (isProtectedPage && !token) {
516
+ return NextResponse.redirect(new URL('/login', request.url));
517
+ }
518
+
519
+ if (isAuthPage && token) {
520
+ return NextResponse.redirect(new URL('/dashboard', request.url));
521
+ }
522
+
523
+ return NextResponse.next();
524
+ }
525
+
526
+ export const config = {
527
+ matcher: ['/dashboard/:path*', '/login'],
528
+ };
529
+ `;
530
+ await fs.writeFile(path.join(frontendDir, 'middleware.ts'), middleware);
531
+ }
532
+
533
+ // .env.local.example
534
+ const envExample = `NEXT_PUBLIC_API_URL=http://localhost:3000/api
535
+ ${config.auth === 'clerk' ? 'NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY=pk_test_xxxxx\nCLERK_SECRET_KEY=sk_test_xxxxx' : ''}
536
+ ${config.auth === 'supabase' ? 'NEXT_PUBLIC_SUPABASE_URL=https://xxxxx.supabase.co\nNEXT_PUBLIC_SUPABASE_ANON_KEY=xxxxx' : ''}
537
+ `;
538
+
539
+ await fs.writeFile(path.join(frontendDir, '.env.local.example'), envExample);
540
+
541
+ // .gitignore
542
+ const gitignore = `# dependencies
543
+ /node_modules
544
+ /.pnp
545
+ .pnp.js
546
+
547
+ # testing
548
+ /coverage
549
+
550
+ # next.js
551
+ /.next/
552
+ /out/
553
+
554
+ # production
555
+ /build
556
+
557
+ # misc
558
+ .DS_Store
559
+ *.pem
560
+
561
+ # debug
562
+ npm-debug.log*
563
+ yarn-debug.log*
564
+ yarn-error.log*
565
+
566
+ # local env files
567
+ .env*.local
568
+
569
+ # vercel
570
+ .vercel
571
+
572
+ # typescript
573
+ *.tsbuildinfo
574
+ next-env.d.ts
575
+ `;
576
+
577
+ await fs.writeFile(path.join(frontendDir, '.gitignore'), gitignore);
578
+ }
579
+
580
+ async function generateVueVite(_config: StackConfig, _frontendDir: string) {
581
+ // Placeholder for Vue generation
582
+ throw new Error('Vue support coming in Phase 2');
583
+ }
584
+
585
+ async function generateSvelteKit(_config: StackConfig, _frontendDir: string) {
586
+ // Placeholder for SvelteKit generation
587
+ throw new Error('SvelteKit support coming in Phase 2');
588
+ }
589
+ // Helper functions for Next.js at the end of frontend.ts
590
+
591
+ function getNextJSLayout(config: StackConfig): string {
592
+ return `import type { Metadata } from 'next'
593
+ import './globals.css'
594
+
595
+ export const metadata: Metadata = {
596
+ title: '${config.projectName}',
597
+ description: 'Generated by ForgeStack OS',
598
+ }
599
+
600
+ export default function RootLayout({
601
+ children,
602
+ }: {
603
+ children: React.ReactNode
604
+ }) {
605
+ return (
606
+ <html lang="en">
607
+ <body>{children}</body>
608
+ </html>
609
+ )
610
+ }
611
+ `;
612
+ }
613
+
614
+ function getNextJSLoginPage(config: StackConfig): string {
615
+ if (config.auth === 'clerk') {
616
+ return `import { SignIn } from '@clerk/nextjs'
617
+
618
+ export default function LoginPage() {
619
+ return (
620
+ <div className="min-h-screen flex items-center justify-center bg-gray-900">
621
+ <SignIn />
622
+ </div>
623
+ )
624
+ }
625
+ `;
626
+ }
627
+
628
+ return `'use client'
629
+
630
+ import { useState } from 'react'
631
+ import { useRouter } from 'next/navigation'
632
+ import api from '@/lib/api'
633
+
634
+ export default function LoginPage() {
635
+ const [email, setEmail] = useState('')
636
+ const [password, setPassword] = useState('')
637
+ const [error, setError] = useState('')
638
+ const router = useRouter()
639
+
640
+ const handleSubmit = async (e: React.FormEvent) => {
641
+ e.preventDefault()
642
+ try {
643
+ const res = await api.post('/auth/login', { email, password })
644
+ localStorage.setItem('token', res.data.token)
645
+ router.push('/dashboard')
646
+ } catch (err: any) {
647
+ setError(err.response?.data?.error || 'Login failed')
648
+ }
649
+ }
650
+
651
+ return (
652
+ <div className="min-h-screen flex items-center justify-center bg-gray-900">
653
+ <div className="bg-gray-800 p-8 rounded-lg shadow-lg w-full max-w-md">
654
+ <h1 className="text-3xl font-bold text-white mb-6">Login</h1>
655
+ {error && <div className="bg-red-500 text-white p-3 rounded mb-4">{error}</div>}
656
+ <form onSubmit={handleSubmit}>
657
+ <div className="mb-4">
658
+ <label className="block text-gray-300 mb-2">Email</label>
659
+ <input
660
+ type="email"
661
+ value={email}
662
+ onChange={(e) => setEmail(e.target.value)}
663
+ className="w-full p-3 rounded bg-gray-700 text-white"
664
+ required
665
+ />
666
+ </div>
667
+ <div className="mb-6">
668
+ <label className="block text-gray-300 mb-2">Password</label>
669
+ <input
670
+ type="password"
671
+ value={password}
672
+ onChange={(e) => setPassword(e.target.value)}
673
+ className="w-full p-3 rounded bg-gray-700 text-white"
674
+ required
675
+ />
676
+ </div>
677
+ <button
678
+ type="submit"
679
+ className="w-full bg-blue-600 hover:bg-blue-700 text-white font-bold py-3 rounded"
680
+ >
681
+ Login
682
+ </button>
683
+ </form>
684
+ </div>
685
+ </div>
686
+ )
687
+ }
688
+ `;
689
+ }
690
+
691
+ function getNextJSDashboardPage(config: StackConfig): string {
692
+ if (config.auth === 'clerk') {
693
+ return `import { UserButton } from '@clerk/nextjs'
694
+ import { currentUser } from '@clerk/nextjs/server'
695
+ import { redirect } from 'next/navigation'
696
+
697
+ export default async function DashboardPage() {
698
+ const user = await currentUser()
699
+
700
+ if (!user) {
701
+ redirect('/login')
702
+ }
703
+
704
+ return (
705
+ <div className="min-h-screen bg-gray-900 text-white">
706
+ <nav className="bg-gray-800 p-4">
707
+ <div className="container mx-auto flex justify-between items-center">
708
+ <h1 className="text-2xl font-bold">${config.projectName}</h1>
709
+ <UserButton afterSignOutUrl="/" />
710
+ </div>
711
+ </nav>
712
+ <div className="container mx-auto px-4 py-16">
713
+ <h2 className="text-3xl font-bold mb-4">Welcome, {user.firstName || user.emailAddresses[0].emailAddress}!</h2>
714
+ <p className="text-gray-400">You're logged in to your dashboard.</p>
715
+ </div>
716
+ </div>
717
+ )
718
+ }
719
+ `;
720
+ }
721
+
722
+ return `'use client'
723
+
724
+ import { useEffect, useState } from 'react'
725
+ import { useRouter } from 'next/navigation'
726
+
727
+ export default function DashboardPage() {
728
+ const [user, setUser] = useState<any>(null)
729
+ const [loading, setLoading] = useState(true)
730
+ const router = useRouter()
731
+
732
+ useEffect(() => {
733
+ const token = localStorage.getItem('token')
734
+ if (!token) {
735
+ router.push('/login')
736
+ return
737
+ }
738
+
739
+ try {
740
+ const payload = JSON.parse(atob(token.split('.')[1]))
741
+ setUser(payload)
742
+ } catch (err) {
743
+ router.push('/login')
744
+ } finally {
745
+ setLoading(false)
746
+ }
747
+ }, [router])
748
+
749
+ const handleLogout = () => {
750
+ localStorage.removeItem('token')
751
+ router.push('/login')
752
+ }
753
+
754
+ if (loading) {
755
+ return (
756
+ <div className="min-h-screen bg-gray-900 flex items-center justify-center">
757
+ <div className="text-white">Loading...</div>
758
+ </div>
759
+ )
760
+ }
761
+
762
+ return (
763
+ <div className="min-h-screen bg-gray-900 text-white">
764
+ <nav className="bg-gray-800 p-4">
765
+ <div className="container mx-auto flex justify-between items-center">
766
+ <h1 className="text-2xl font-bold">${config.projectName}</h1>
767
+ <button
768
+ onClick={handleLogout}
769
+ className="bg-red-600 hover:bg-red-700 px-4 py-2 rounded"
770
+ >
771
+ Logout
772
+ </button>
773
+ </div>
774
+ </nav>
775
+ <div className="container mx-auto px-4 py-16">
776
+ <h2 className="text-3xl font-bold mb-4">Welcome, {user?.email}!</h2>
777
+ <p className="text-gray-400">You're logged in to your dashboard.</p>
778
+ </div>
779
+ </div>
780
+ )
781
+ }
782
+ `;
783
+ }