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