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.
- package/dist/commands/create.d.ts +1 -0
- package/dist/commands/create.d.ts.map +1 -0
- package/dist/commands/create.js +78 -0
- package/dist/commands/create.js.map +1 -0
- package/dist/generators/api.d.ts +3 -0
- package/dist/generators/api.d.ts.map +1 -0
- package/dist/generators/api.js +346 -0
- package/dist/generators/api.js.map +1 -0
- package/dist/generators/auth.d.ts +2 -0
- package/dist/generators/auth.d.ts.map +1 -0
- package/dist/generators/auth.js +371 -0
- package/dist/generators/auth.js.map +1 -0
- package/dist/generators/backend.d.ts +2 -0
- package/dist/generators/backend.d.ts.map +1 -0
- package/dist/generators/backend.js +875 -0
- package/dist/generators/backend.js.map +1 -0
- package/dist/generators/common.d.ts +2 -0
- package/dist/generators/common.d.ts.map +1 -0
- package/dist/generators/common.js +354 -0
- package/dist/generators/common.js.map +1 -0
- package/dist/generators/database.d.ts +2 -0
- package/dist/generators/database.d.ts.map +1 -0
- package/dist/generators/database.js +157 -0
- package/dist/generators/database.js.map +1 -0
- package/dist/generators/docker.d.ts +2 -0
- package/dist/generators/docker.d.ts.map +1 -0
- package/dist/generators/docker.js +181 -0
- package/dist/generators/docker.js.map +1 -0
- package/dist/generators/frontend-helpers.d.ts +3 -0
- package/dist/generators/frontend-helpers.d.ts.map +1 -0
- package/dist/generators/frontend-helpers.js +23 -0
- package/dist/generators/frontend-helpers.js.map +1 -0
- package/dist/generators/frontend.d.ts +2 -0
- package/dist/generators/frontend.d.ts.map +1 -0
- package/dist/generators/frontend.js +735 -0
- package/dist/generators/frontend.js.map +1 -0
- package/dist/generators/index.d.ts +2 -0
- package/dist/generators/index.d.ts.map +1 -0
- package/dist/generators/index.js +59 -0
- package/dist/generators/index.js.map +1 -0
- package/dist/generators/nextjs-helpers.d.ts +6 -0
- package/dist/generators/nextjs-helpers.d.ts.map +1 -0
- package/dist/generators/nextjs-helpers.js +216 -0
- package/dist/generators/nextjs-helpers.js.map +1 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +24 -0
- package/dist/index.js.map +1 -0
- package/dist/types.d.ts +15 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +3 -0
- package/dist/types.js.map +1 -0
- package/dist/utils/logger.d.ts +9 -0
- package/dist/utils/logger.d.ts.map +1 -0
- package/dist/utils/logger.js +32 -0
- package/dist/utils/logger.js.map +1 -0
- package/dist/utils/prompts.d.ts +2 -0
- package/dist/utils/prompts.d.ts.map +1 -0
- package/dist/utils/prompts.js +107 -0
- package/dist/utils/prompts.js.map +1 -0
- package/dist/utils/validators.d.ts +2 -0
- package/dist/utils/validators.d.ts.map +1 -0
- package/dist/utils/validators.js +48 -0
- package/dist/utils/validators.js.map +1 -0
- package/package.json +49 -0
- package/src/commands/create.ts +82 -0
- package/src/generators/api.ts +353 -0
- package/src/generators/auth.ts +406 -0
- package/src/generators/backend.ts +927 -0
- package/src/generators/common.ts +377 -0
- package/src/generators/database.ts +165 -0
- package/src/generators/docker.ts +185 -0
- package/src/generators/frontend.ts +783 -0
- package/src/generators/index.ts +64 -0
- package/src/index.ts +27 -0
- package/src/types.ts +16 -0
- package/src/utils/logger.ts +31 -0
- package/src/utils/prompts.ts +105 -0
- package/src/utils/validators.ts +50 -0
- package/tests/validators.test.ts +69 -0
- package/tsc_output.txt +0 -0
- 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
|
+
}
|