nexu-app 2.0.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 (106) hide show
  1. package/README.md +149 -0
  2. package/dist/index.d.ts +2 -0
  3. package/dist/index.js +1192 -0
  4. package/package.json +43 -0
  5. package/templates/default/.changeset/config.json +11 -0
  6. package/templates/default/.eslintignore +16 -0
  7. package/templates/default/.eslintrc.js +67 -0
  8. package/templates/default/.github/actions/build/action.yml +35 -0
  9. package/templates/default/.github/actions/quality/action.yml +53 -0
  10. package/templates/default/.github/dependabot.yml +51 -0
  11. package/templates/default/.github/workflows/deploy-dev.yml +83 -0
  12. package/templates/default/.github/workflows/deploy-prod.yml +83 -0
  13. package/templates/default/.github/workflows/deploy-rec.yml +83 -0
  14. package/templates/default/.husky/commit-msg +1 -0
  15. package/templates/default/.husky/pre-commit +1 -0
  16. package/templates/default/.nexu-version +1 -0
  17. package/templates/default/.prettierignore +7 -0
  18. package/templates/default/.prettierrc +19 -0
  19. package/templates/default/.vscode/extensions.json +14 -0
  20. package/templates/default/.vscode/settings.json +36 -0
  21. package/templates/default/apps/gitkeep +0 -0
  22. package/templates/default/commitlint.config.js +26 -0
  23. package/templates/default/docker/docker-compose.dev.yml +49 -0
  24. package/templates/default/docker/docker-compose.prod.yml +64 -0
  25. package/templates/default/docker/docker-compose.yml +6 -0
  26. package/templates/default/docs/architecture.md +452 -0
  27. package/templates/default/docs/cli.md +330 -0
  28. package/templates/default/docs/contributing.md +462 -0
  29. package/templates/default/docs/scripts.md +460 -0
  30. package/templates/default/gitignore +44 -0
  31. package/templates/default/lintstagedrc.cjs +4 -0
  32. package/templates/default/package.json +51 -0
  33. package/templates/default/packages/auth/package.json +61 -0
  34. package/templates/default/packages/auth/src/components/ProtectedRoute.tsx +75 -0
  35. package/templates/default/packages/auth/src/components/SignInForm.tsx +153 -0
  36. package/templates/default/packages/auth/src/components/SignUpForm.tsx +179 -0
  37. package/templates/default/packages/auth/src/components/SocialButtons.tsx +147 -0
  38. package/templates/default/packages/auth/src/components/index.ts +4 -0
  39. package/templates/default/packages/auth/src/hooks/index.ts +4 -0
  40. package/templates/default/packages/auth/src/hooks/useAuth.ts +51 -0
  41. package/templates/default/packages/auth/src/hooks/useRequireAuth.ts +54 -0
  42. package/templates/default/packages/auth/src/hooks/useSession.ts +48 -0
  43. package/templates/default/packages/auth/src/hooks/useUser.ts +48 -0
  44. package/templates/default/packages/auth/src/index.ts +45 -0
  45. package/templates/default/packages/auth/src/next/index.ts +18 -0
  46. package/templates/default/packages/auth/src/next/middleware.ts +183 -0
  47. package/templates/default/packages/auth/src/next/server.ts +219 -0
  48. package/templates/default/packages/auth/src/providers/AuthContext.tsx +435 -0
  49. package/templates/default/packages/auth/src/providers/index.ts +1 -0
  50. package/templates/default/packages/auth/src/types/index.ts +284 -0
  51. package/templates/default/packages/auth/src/utils/api.ts +228 -0
  52. package/templates/default/packages/auth/src/utils/index.ts +3 -0
  53. package/templates/default/packages/auth/src/utils/oauth.ts +230 -0
  54. package/templates/default/packages/auth/src/utils/token.ts +204 -0
  55. package/templates/default/packages/auth/tsconfig.json +14 -0
  56. package/templates/default/packages/auth/tsup.config.ts +18 -0
  57. package/templates/default/packages/cache/package.json +26 -0
  58. package/templates/default/packages/cache/src/index.ts +137 -0
  59. package/templates/default/packages/cache/tsconfig.json +9 -0
  60. package/templates/default/packages/cache/tsup.config.ts +9 -0
  61. package/templates/default/packages/config/eslint/index.js +20 -0
  62. package/templates/default/packages/config/package.json +9 -0
  63. package/templates/default/packages/config/typescript/base.json +26 -0
  64. package/templates/default/packages/constants/package.json +26 -0
  65. package/templates/default/packages/constants/src/index.ts +121 -0
  66. package/templates/default/packages/constants/tsconfig.json +9 -0
  67. package/templates/default/packages/constants/tsup.config.ts +9 -0
  68. package/templates/default/packages/logger/package.json +27 -0
  69. package/templates/default/packages/logger/src/index.ts +197 -0
  70. package/templates/default/packages/logger/tsconfig.json +11 -0
  71. package/templates/default/packages/logger/tsup.config.ts +9 -0
  72. package/templates/default/packages/result/package.json +26 -0
  73. package/templates/default/packages/result/src/index.ts +142 -0
  74. package/templates/default/packages/result/tsconfig.json +9 -0
  75. package/templates/default/packages/result/tsup.config.ts +9 -0
  76. package/templates/default/packages/types/package.json +26 -0
  77. package/templates/default/packages/types/src/index.ts +78 -0
  78. package/templates/default/packages/types/tsconfig.json +9 -0
  79. package/templates/default/packages/types/tsup.config.ts +10 -0
  80. package/templates/default/packages/ui/package.json +38 -0
  81. package/templates/default/packages/ui/src/components/Button.tsx +58 -0
  82. package/templates/default/packages/ui/src/components/Card.tsx +85 -0
  83. package/templates/default/packages/ui/src/components/Input.tsx +45 -0
  84. package/templates/default/packages/ui/src/index.ts +15 -0
  85. package/templates/default/packages/ui/tsconfig.json +11 -0
  86. package/templates/default/packages/ui/tsup.config.ts +11 -0
  87. package/templates/default/packages/utils/package.json +30 -0
  88. package/templates/default/packages/utils/src/index.test.ts +130 -0
  89. package/templates/default/packages/utils/src/index.ts +154 -0
  90. package/templates/default/packages/utils/tsconfig.json +10 -0
  91. package/templates/default/packages/utils/tsup.config.ts +10 -0
  92. package/templates/default/pnpm-workspace.yaml +3 -0
  93. package/templates/default/scripts/audit.mjs +700 -0
  94. package/templates/default/scripts/deploy.mjs +40 -0
  95. package/templates/default/scripts/generate-app.mjs +808 -0
  96. package/templates/default/scripts/lib/package-manager.mjs +186 -0
  97. package/templates/default/scripts/setup.mjs +102 -0
  98. package/templates/default/services/.env.example +16 -0
  99. package/templates/default/services/docker-compose.yml +207 -0
  100. package/templates/default/services/grafana/provisioning/dashboards/dashboards.yml +11 -0
  101. package/templates/default/services/grafana/provisioning/datasources/datasources.yml +9 -0
  102. package/templates/default/services/postgres/init/gitkeep +2 -0
  103. package/templates/default/services/prometheus/prometheus.yml +13 -0
  104. package/templates/default/tsconfig.json +27 -0
  105. package/templates/default/turbo.json +40 -0
  106. package/templates/default/vitest.config.ts +15 -0
@@ -0,0 +1,808 @@
1
+ #!/usr/bin/env node
2
+
3
+ import fs from 'fs';
4
+ import path from 'path';
5
+ import { fileURLToPath } from 'url';
6
+ import { execSync } from 'child_process';
7
+ import readline from 'readline';
8
+ import { detectPackageManager, getRunCommand, getInstallCommand } from './lib/package-manager.mjs';
9
+
10
+ const __filename = fileURLToPath(import.meta.url);
11
+ const __dirname = path.dirname(__filename);
12
+
13
+ // Colors for console output
14
+ const colors = {
15
+ red: '\x1b[31m',
16
+ green: '\x1b[32m',
17
+ blue: '\x1b[34m',
18
+ yellow: '\x1b[33m',
19
+ cyan: '\x1b[36m',
20
+ bold: '\x1b[1m',
21
+ reset: '\x1b[0m',
22
+ };
23
+
24
+ const log = {
25
+ info: (msg) => console.log(`${colors.blue}ℹ${colors.reset} ${msg}`),
26
+ success: (msg) => console.log(`${colors.green}✓${colors.reset} ${msg}`),
27
+ warn: (msg) => console.log(`${colors.yellow}!${colors.reset} ${msg}`),
28
+ error: (msg) => console.log(`${colors.red}✗${colors.reset} ${msg}`),
29
+ title: (msg) => console.log(`\n${colors.bold}${colors.cyan}${msg}${colors.reset}\n`),
30
+ };
31
+
32
+ // Get directories
33
+ const ROOT_DIR = path.resolve(__dirname, '..');
34
+ const APPS_DIR = path.join(ROOT_DIR, 'apps');
35
+ const DOCKER_DIR = path.join(ROOT_DIR, 'docker');
36
+
37
+ // Detect package manager
38
+ const pm = detectPackageManager(ROOT_DIR);
39
+ const runCmd = getRunCommand(pm);
40
+
41
+ // Docker-specific helpers based on package manager
42
+ function getDockerSetup(pm) {
43
+ switch (pm) {
44
+ case 'pnpm':
45
+ return {
46
+ enableCmd: 'RUN corepack enable && corepack prepare pnpm@9 --activate',
47
+ lockFile: 'pnpm-lock.yaml pnpm-workspace.yaml',
48
+ installCmd: 'RUN pnpm install --frozen-lockfile',
49
+ installProdCmd: 'RUN pnpm install --prod --frozen-lockfile',
50
+ runPrefix: 'pnpm',
51
+ buildCmd: 'pnpm turbo build',
52
+ };
53
+ case 'yarn':
54
+ return {
55
+ enableCmd: 'RUN corepack enable && corepack prepare yarn@4 --activate',
56
+ lockFile: 'yarn.lock',
57
+ installCmd: 'RUN yarn install --frozen-lockfile',
58
+ installProdCmd: 'RUN yarn install --production --frozen-lockfile',
59
+ runPrefix: 'yarn',
60
+ buildCmd: 'yarn turbo build',
61
+ };
62
+ case 'npm':
63
+ default:
64
+ return {
65
+ enableCmd: '# Using npm (default)',
66
+ lockFile: 'package-lock.json',
67
+ installCmd: 'RUN npm ci',
68
+ installProdCmd: 'RUN npm ci --omit=dev',
69
+ runPrefix: 'npm run',
70
+ buildCmd: 'npm run turbo build',
71
+ };
72
+ }
73
+ }
74
+
75
+ const docker = getDockerSetup(pm);
76
+
77
+ // Framework configurations
78
+ const FRAMEWORKS = {
79
+ // Frontend frameworks
80
+ 'next': {
81
+ name: 'Next.js',
82
+ type: 'frontend',
83
+ defaultPort: 3000,
84
+ createCommand: (name) => `npx create-next-app@latest ${name} --typescript --tailwind --eslint --app --src-dir --import-alias "@/*" --use-${pm}`,
85
+ devCommand: 'dev',
86
+ buildCommand: 'build',
87
+ startCommand: 'start',
88
+ outputDir: '.next',
89
+ dockerfile: 'nextjs',
90
+ },
91
+ 'vite-react': {
92
+ name: 'Vite + React',
93
+ type: 'frontend',
94
+ defaultPort: 5173,
95
+ createCommand: (name) => `npm create vite@latest ${name} -- --template react-ts`,
96
+ devCommand: 'dev --host',
97
+ buildCommand: 'build',
98
+ startCommand: 'preview --host',
99
+ outputDir: 'dist',
100
+ dockerfile: 'vite',
101
+ },
102
+ 'vite-vue': {
103
+ name: 'Vite + Vue',
104
+ type: 'frontend',
105
+ defaultPort: 5173,
106
+ createCommand: (name) => `npm create vite@latest ${name} -- --template vue-ts`,
107
+ devCommand: 'dev --host',
108
+ buildCommand: 'build',
109
+ startCommand: 'preview --host',
110
+ outputDir: 'dist',
111
+ dockerfile: 'vite',
112
+ },
113
+ 'vite-svelte': {
114
+ name: 'Vite + Svelte',
115
+ type: 'frontend',
116
+ defaultPort: 5173,
117
+ createCommand: (name) => `npm create vite@latest ${name} -- --template svelte-ts`,
118
+ devCommand: 'dev --host',
119
+ buildCommand: 'build',
120
+ startCommand: 'preview --host',
121
+ outputDir: 'dist',
122
+ dockerfile: 'vite',
123
+ },
124
+ 'nuxt': {
125
+ name: 'Nuxt',
126
+ type: 'frontend',
127
+ defaultPort: 3000,
128
+ createCommand: (name) => `npx nuxi@latest init ${name}`,
129
+ devCommand: 'dev',
130
+ buildCommand: 'build',
131
+ startCommand: 'preview',
132
+ outputDir: '.output',
133
+ dockerfile: 'nuxt',
134
+ },
135
+ // Backend frameworks
136
+ 'express': {
137
+ name: 'Express.js',
138
+ type: 'backend',
139
+ defaultPort: 4000,
140
+ createCommand: null, // Manual setup
141
+ devCommand: 'dev',
142
+ buildCommand: 'build',
143
+ startCommand: 'start',
144
+ outputDir: 'dist',
145
+ dockerfile: 'node',
146
+ dependencies: ['express', 'cors', 'helmet'],
147
+ devDependencies: ['@types/express', '@types/cors', 'tsx', 'typescript'],
148
+ },
149
+ 'fastify': {
150
+ name: 'Fastify',
151
+ type: 'backend',
152
+ defaultPort: 4000,
153
+ createCommand: null,
154
+ devCommand: 'dev',
155
+ buildCommand: 'build',
156
+ startCommand: 'start',
157
+ outputDir: 'dist',
158
+ dockerfile: 'node',
159
+ dependencies: ['fastify', '@fastify/cors'],
160
+ devDependencies: ['tsx', 'typescript'],
161
+ },
162
+ 'hono': {
163
+ name: 'Hono',
164
+ type: 'backend',
165
+ defaultPort: 4000,
166
+ createCommand: null,
167
+ devCommand: 'dev',
168
+ buildCommand: 'build',
169
+ startCommand: 'start',
170
+ outputDir: 'dist',
171
+ dockerfile: 'node',
172
+ dependencies: ['hono', '@hono/node-server'],
173
+ devDependencies: ['tsx', 'typescript'],
174
+ },
175
+ 'nestjs': {
176
+ name: 'NestJS',
177
+ type: 'backend',
178
+ defaultPort: 4000,
179
+ createCommand: (name) => `npx @nestjs/cli@latest new ${name} --package-manager ${pm} --skip-git`,
180
+ devCommand: 'start:dev',
181
+ buildCommand: 'build',
182
+ startCommand: 'start:prod',
183
+ outputDir: 'dist',
184
+ dockerfile: 'node',
185
+ },
186
+ 'empty': {
187
+ name: 'Empty (Node.js)',
188
+ type: 'backend',
189
+ defaultPort: 3000,
190
+ createCommand: null,
191
+ devCommand: 'dev',
192
+ buildCommand: 'build',
193
+ startCommand: 'start',
194
+ outputDir: 'dist',
195
+ dockerfile: 'node',
196
+ dependencies: [],
197
+ devDependencies: ['tsx', 'typescript'],
198
+ },
199
+ };
200
+
201
+ // Dockerfile templates
202
+ const DOCKERFILES = {
203
+ nextjs: (appName, port) => `# ====== Base ======
204
+ FROM node:20-alpine AS base
205
+ ${docker.enableCmd}
206
+ WORKDIR /app
207
+
208
+ # ====== Dependencies ======
209
+ FROM base AS deps
210
+ COPY ${docker.lockFile} package.json ./
211
+ COPY apps/${appName}/package.json ./apps/${appName}/
212
+ COPY packages/*/package.json ./packages/
213
+ ${docker.installCmd}
214
+
215
+ # ====== Development ======
216
+ FROM base AS development
217
+ COPY --from=deps /app/node_modules ./node_modules
218
+ COPY --from=deps /app/apps/${appName}/node_modules ./apps/${appName}/node_modules
219
+ COPY . .
220
+ WORKDIR /app/apps/${appName}
221
+ ENV PORT=${port}
222
+ EXPOSE ${port}
223
+ CMD ["${docker.runPrefix}", "dev"]
224
+
225
+ # ====== Builder ======
226
+ FROM base AS builder
227
+ COPY --from=deps /app/node_modules ./node_modules
228
+ COPY --from=deps /app/apps/${appName}/node_modules ./apps/${appName}/node_modules
229
+ COPY . .
230
+ ENV NEXT_TELEMETRY_DISABLED=1
231
+ RUN ${docker.buildCmd} --filter=@repo/${appName}
232
+
233
+ # ====== Production ======
234
+ FROM node:20-alpine AS production
235
+ WORKDIR /app
236
+ ENV NODE_ENV=production
237
+ ENV NEXT_TELEMETRY_DISABLED=1
238
+ ENV PORT=${port}
239
+
240
+ RUN addgroup --system --gid 1001 nodejs
241
+ RUN adduser --system --uid 1001 nextjs
242
+
243
+ COPY --from=builder /app/apps/${appName}/public ./public
244
+ COPY --from=builder --chown=nextjs:nodejs /app/apps/${appName}/.next/standalone ./
245
+ COPY --from=builder --chown=nextjs:nodejs /app/apps/${appName}/.next/static ./.next/static
246
+
247
+ USER nextjs
248
+ EXPOSE ${port}
249
+ CMD ["node", "server.js"]
250
+ `,
251
+
252
+ vite: (appName, port) => `# ====== Base ======
253
+ FROM node:20-alpine AS base
254
+ ${docker.enableCmd}
255
+ WORKDIR /app
256
+
257
+ # ====== Dependencies ======
258
+ FROM base AS deps
259
+ COPY ${docker.lockFile} package.json ./
260
+ COPY apps/${appName}/package.json ./apps/${appName}/
261
+ COPY packages/*/package.json ./packages/
262
+ ${docker.installCmd}
263
+
264
+ # ====== Development ======
265
+ FROM base AS development
266
+ COPY --from=deps /app/node_modules ./node_modules
267
+ COPY --from=deps /app/apps/${appName}/node_modules ./apps/${appName}/node_modules
268
+ COPY . .
269
+ WORKDIR /app/apps/${appName}
270
+ EXPOSE ${port}
271
+ CMD ["${docker.runPrefix}", "dev", "--host"]
272
+
273
+ # ====== Builder ======
274
+ FROM base AS builder
275
+ COPY --from=deps /app/node_modules ./node_modules
276
+ COPY --from=deps /app/apps/${appName}/node_modules ./apps/${appName}/node_modules
277
+ COPY . .
278
+ RUN ${docker.buildCmd} --filter=@repo/${appName}
279
+
280
+ # ====== Production ======
281
+ FROM nginx:alpine AS production
282
+ COPY --from=builder /app/apps/${appName}/dist /usr/share/nginx/html
283
+ COPY apps/${appName}/docker/nginx.conf /etc/nginx/conf.d/default.conf
284
+ EXPOSE ${port}
285
+ CMD ["nginx", "-g", "daemon off;"]
286
+ `,
287
+
288
+ nuxt: (appName, port) => `# ====== Base ======
289
+ FROM node:20-alpine AS base
290
+ ${docker.enableCmd}
291
+ WORKDIR /app
292
+
293
+ # ====== Dependencies ======
294
+ FROM base AS deps
295
+ COPY ${docker.lockFile} package.json ./
296
+ COPY apps/${appName}/package.json ./apps/${appName}/
297
+ COPY packages/*/package.json ./packages/
298
+ ${docker.installCmd}
299
+
300
+ # ====== Development ======
301
+ FROM base AS development
302
+ COPY --from=deps /app/node_modules ./node_modules
303
+ COPY --from=deps /app/apps/${appName}/node_modules ./apps/${appName}/node_modules
304
+ COPY . .
305
+ WORKDIR /app/apps/${appName}
306
+ ENV PORT=${port}
307
+ EXPOSE ${port}
308
+ CMD ["${docker.runPrefix}", "dev"]
309
+
310
+ # ====== Builder ======
311
+ FROM base AS builder
312
+ COPY --from=deps /app/node_modules ./node_modules
313
+ COPY --from=deps /app/apps/${appName}/node_modules ./apps/${appName}/node_modules
314
+ COPY . .
315
+ RUN ${docker.buildCmd} --filter=@repo/${appName}
316
+
317
+ # ====== Production ======
318
+ FROM node:20-alpine AS production
319
+ WORKDIR /app
320
+ ENV NODE_ENV=production
321
+ ENV PORT=${port}
322
+
323
+ COPY --from=builder /app/apps/${appName}/.output ./
324
+
325
+ EXPOSE ${port}
326
+ CMD ["node", "server/index.mjs"]
327
+ `,
328
+
329
+ node: (appName, port) => `# ====== Base ======
330
+ FROM node:20-alpine AS base
331
+ ${docker.enableCmd}
332
+ WORKDIR /app
333
+
334
+ # ====== Dependencies ======
335
+ FROM base AS deps
336
+ COPY ${docker.lockFile} package.json ./
337
+ COPY apps/${appName}/package.json ./apps/${appName}/
338
+ COPY packages/*/package.json ./packages/
339
+ ${docker.installCmd}
340
+
341
+ # ====== Development ======
342
+ FROM base AS development
343
+ COPY --from=deps /app/node_modules ./node_modules
344
+ COPY --from=deps /app/apps/${appName}/node_modules ./apps/${appName}/node_modules
345
+ COPY . .
346
+ WORKDIR /app/apps/${appName}
347
+ ENV PORT=${port}
348
+ EXPOSE ${port}
349
+ CMD ["${docker.runPrefix}", "dev"]
350
+
351
+ # ====== Builder ======
352
+ FROM base AS builder
353
+ COPY --from=deps /app/node_modules ./node_modules
354
+ COPY --from=deps /app/apps/${appName}/node_modules ./apps/${appName}/node_modules
355
+ COPY . .
356
+ RUN ${docker.buildCmd} --filter=@repo/${appName}
357
+
358
+ # ====== Production ======
359
+ FROM node:20-alpine AS production
360
+ ${docker.enableCmd}
361
+ WORKDIR /app
362
+ ENV NODE_ENV=production
363
+ ENV PORT=${port}
364
+
365
+ COPY --from=builder /app/apps/${appName}/dist ./dist
366
+ COPY --from=builder /app/apps/${appName}/package.json ./
367
+
368
+ ${docker.installProdCmd}
369
+
370
+ EXPOSE ${port}
371
+ CMD ["node", "dist/index.js"]
372
+ `,
373
+ };
374
+
375
+ // Helper to ask questions
376
+ function question(prompt, defaultValue = '') {
377
+ const rl = readline.createInterface({
378
+ input: process.stdin,
379
+ output: process.stdout,
380
+ });
381
+
382
+ return new Promise((resolve) => {
383
+ const defaultText = defaultValue ? ` (${defaultValue})` : '';
384
+ rl.question(`${colors.cyan}?${colors.reset} ${prompt}${defaultText}: `, (answer) => {
385
+ rl.close();
386
+ resolve(answer || defaultValue);
387
+ });
388
+ });
389
+ }
390
+
391
+ // Helper to select from list
392
+ async function select(prompt, choices) {
393
+ console.log(`\n${colors.cyan}?${colors.reset} ${prompt}\n`);
394
+
395
+ choices.forEach((choice, index) => {
396
+ console.log(` ${colors.cyan}${index + 1}${colors.reset}) ${choice.name}`);
397
+ });
398
+
399
+ console.log('');
400
+ const answer = await question('Enter number', '1');
401
+ const index = parseInt(answer, 10) - 1;
402
+
403
+ if (index >= 0 && index < choices.length) {
404
+ return choices[index].value;
405
+ }
406
+
407
+ return choices[0].value;
408
+ }
409
+
410
+ // Create backend app manually
411
+ function createBackendApp(appDir, appName, framework, port) {
412
+ const config = FRAMEWORKS[framework];
413
+
414
+ // Create directory structure
415
+ fs.mkdirSync(path.join(appDir, 'src'), { recursive: true });
416
+
417
+ // Create package.json
418
+ const packageJson = {
419
+ name: `@repo/${appName}`,
420
+ version: '0.0.1',
421
+ private: true,
422
+ type: 'module',
423
+ scripts: {
424
+ dev: 'tsx watch src/index.ts',
425
+ build: 'tsc',
426
+ start: 'node dist/index.js',
427
+ lint: 'eslint src/',
428
+ typecheck: 'tsc --noEmit',
429
+ },
430
+ dependencies: {},
431
+ devDependencies: {
432
+ '@repo/config': 'workspace:*',
433
+ },
434
+ };
435
+
436
+ // Add framework-specific dependencies
437
+ if (config.dependencies) {
438
+ for (const dep of config.dependencies) {
439
+ packageJson.dependencies[dep] = 'latest';
440
+ }
441
+ }
442
+ if (config.devDependencies) {
443
+ for (const dep of config.devDependencies) {
444
+ packageJson.devDependencies[dep] = 'latest';
445
+ }
446
+ }
447
+
448
+ fs.writeFileSync(
449
+ path.join(appDir, 'package.json'),
450
+ JSON.stringify(packageJson, null, 2)
451
+ );
452
+
453
+ // Create tsconfig.json
454
+ const tsconfig = {
455
+ extends: '@repo/config/typescript/base',
456
+ compilerOptions: {
457
+ outDir: 'dist',
458
+ rootDir: 'src',
459
+ },
460
+ include: ['src/**/*'],
461
+ exclude: ['node_modules', 'dist'],
462
+ };
463
+
464
+ fs.writeFileSync(
465
+ path.join(appDir, 'tsconfig.json'),
466
+ JSON.stringify(tsconfig, null, 2)
467
+ );
468
+
469
+ // Create main file based on framework
470
+ let mainFile = '';
471
+
472
+ if (framework === 'express') {
473
+ mainFile = `import express from 'express';
474
+ import cors from 'cors';
475
+ import helmet from 'helmet';
476
+
477
+ const app = express();
478
+ const port = process.env.PORT || ${port};
479
+
480
+ app.use(helmet());
481
+ app.use(cors());
482
+ app.use(express.json());
483
+
484
+ app.get('/', (req, res) => {
485
+ res.json({ message: 'Hello from ${appName}!' });
486
+ });
487
+
488
+ app.get('/health', (req, res) => {
489
+ res.json({ status: 'ok' });
490
+ });
491
+
492
+ app.listen(port, () => {
493
+ console.log(\`🚀 Server running on http://localhost:\${port}\`);
494
+ });
495
+ `;
496
+ } else if (framework === 'fastify') {
497
+ mainFile = `import Fastify from 'fastify';
498
+ import cors from '@fastify/cors';
499
+
500
+ const fastify = Fastify({ logger: true });
501
+ const port = parseInt(process.env.PORT || '${port}', 10);
502
+
503
+ await fastify.register(cors);
504
+
505
+ fastify.get('/', async () => {
506
+ return { message: 'Hello from ${appName}!' };
507
+ });
508
+
509
+ fastify.get('/health', async () => {
510
+ return { status: 'ok' };
511
+ });
512
+
513
+ try {
514
+ await fastify.listen({ port, host: '0.0.0.0' });
515
+ } catch (err) {
516
+ fastify.log.error(err);
517
+ process.exit(1);
518
+ }
519
+ `;
520
+ } else if (framework === 'hono') {
521
+ mainFile = `import { serve } from '@hono/node-server';
522
+ import { Hono } from 'hono';
523
+ import { cors } from 'hono/cors';
524
+
525
+ const app = new Hono();
526
+ const port = parseInt(process.env.PORT || '${port}', 10);
527
+
528
+ app.use('*', cors());
529
+
530
+ app.get('/', (c) => {
531
+ return c.json({ message: 'Hello from ${appName}!' });
532
+ });
533
+
534
+ app.get('/health', (c) => {
535
+ return c.json({ status: 'ok' });
536
+ });
537
+
538
+ console.log(\`🚀 Server running on http://localhost:\${port}\`);
539
+
540
+ serve({
541
+ fetch: app.fetch,
542
+ port,
543
+ });
544
+ `;
545
+ } else {
546
+ // Empty template
547
+ mainFile = `const port = process.env.PORT || ${port};
548
+
549
+ console.log(\`🚀 App ${appName} starting on port \${port}\`);
550
+
551
+ // Add your code here
552
+ `;
553
+ }
554
+
555
+ fs.writeFileSync(path.join(appDir, 'src', 'index.ts'), mainFile);
556
+ }
557
+
558
+ // Create nginx config for Vite apps
559
+ function createNginxConfig(appDir, port) {
560
+ const nginxConfig = `server {
561
+ listen ${port};
562
+ server_name localhost;
563
+ root /usr/share/nginx/html;
564
+ index index.html;
565
+
566
+ location / {
567
+ try_files $uri $uri/ /index.html;
568
+ }
569
+
570
+ location /assets {
571
+ expires 1y;
572
+ add_header Cache-Control "public, immutable";
573
+ }
574
+
575
+ gzip on;
576
+ gzip_types text/plain text/css application/json application/javascript text/xml application/xml text/javascript;
577
+ }
578
+ `;
579
+
580
+ fs.mkdirSync(path.join(appDir, 'docker'), { recursive: true });
581
+ fs.writeFileSync(path.join(appDir, 'docker', 'nginx.conf'), nginxConfig);
582
+ }
583
+
584
+ // Create docker-compose files
585
+ function createDockerComposeFiles(appDir, appName, port, framework) {
586
+ const config = FRAMEWORKS[framework];
587
+
588
+ // docker-compose.yml (dev)
589
+ const dockerCompose = `services:
590
+ ${appName}:
591
+ build:
592
+ context: ../..
593
+ dockerfile: apps/${appName}/docker/Dockerfile
594
+ target: development
595
+ ports:
596
+ - "${port}:${port}"
597
+ environment:
598
+ - NODE_ENV=development
599
+ - PORT=${port}
600
+ volumes:
601
+ - ../../apps/${appName}:/app/apps/${appName}
602
+ - ../../packages:/app/packages
603
+ - /app/node_modules
604
+ - /app/apps/${appName}/node_modules
605
+ command: pnpm ${config.devCommand}
606
+ `;
607
+
608
+ fs.writeFileSync(path.join(appDir, 'docker-compose.yml'), dockerCompose);
609
+
610
+ // docker-compose.prod.yml
611
+ const dockerComposeProd = `services:
612
+ ${appName}:
613
+ build:
614
+ context: ../..
615
+ dockerfile: apps/${appName}/docker/Dockerfile
616
+ target: production
617
+ ports:
618
+ - "${port}:${port}"
619
+ environment:
620
+ - NODE_ENV=production
621
+ - PORT=${port}
622
+ restart: unless-stopped
623
+ `;
624
+
625
+ fs.writeFileSync(path.join(appDir, 'docker-compose.prod.yml'), dockerComposeProd);
626
+ }
627
+
628
+ // Update main docker-compose.yml
629
+ function updateMainCompose(appName) {
630
+ const mainComposePath = path.join(DOCKER_DIR, 'docker-compose.yml');
631
+ const includePath = `../apps/${appName}/docker-compose.yml`;
632
+
633
+ if (!fs.existsSync(DOCKER_DIR)) {
634
+ fs.mkdirSync(DOCKER_DIR, { recursive: true });
635
+ }
636
+
637
+ if (!fs.existsSync(mainComposePath)) {
638
+ const content = `# Main docker-compose - includes all apps
639
+ # Each app has its own docker-compose.yml in apps/<app-name>/
640
+
641
+ include:
642
+ - path: ${includePath}
643
+ `;
644
+ fs.writeFileSync(mainComposePath, content);
645
+ return;
646
+ }
647
+
648
+ let content = fs.readFileSync(mainComposePath, 'utf-8');
649
+
650
+ if (content.includes(`apps/${appName}/docker-compose.yml`)) {
651
+ return;
652
+ }
653
+
654
+ if (content.includes('include: []')) {
655
+ content = content.replace('include: []', `include:\n - path: ${includePath}`);
656
+ fs.writeFileSync(mainComposePath, content);
657
+ } else {
658
+ content = content.trimEnd() + `\n - path: ${includePath}\n`;
659
+ fs.writeFileSync(mainComposePath, content);
660
+ }
661
+ }
662
+
663
+ // Run command
664
+ function run(cmd, cwd) {
665
+ console.log(`${colors.blue}>${colors.reset} ${cmd}`);
666
+ execSync(cmd, { stdio: 'inherit', cwd });
667
+ }
668
+
669
+ // Main function
670
+ async function main() {
671
+ log.title('🚀 Generate New App');
672
+
673
+ // Get arguments
674
+ const args = process.argv.slice(2);
675
+ let appName = args[0];
676
+ let framework = args[1];
677
+ let port = args[2];
678
+
679
+ // If no app name provided, ask for it
680
+ if (!appName) {
681
+ appName = await question('App name');
682
+ if (!appName) {
683
+ log.error('App name is required');
684
+ process.exit(1);
685
+ }
686
+ }
687
+
688
+ const APP_DIR = path.join(APPS_DIR, appName);
689
+
690
+ // Check if app already exists
691
+ if (fs.existsSync(APP_DIR)) {
692
+ log.error(`App '${appName}' already exists in apps/`);
693
+ process.exit(1);
694
+ }
695
+
696
+ // If no framework provided, show selection
697
+ if (!framework) {
698
+ const choices = Object.entries(FRAMEWORKS).map(([key, value]) => ({
699
+ name: `${value.name} (${value.type})`,
700
+ value: key,
701
+ }));
702
+
703
+ framework = await select('Select framework', choices);
704
+ }
705
+
706
+ // Validate framework
707
+ if (!FRAMEWORKS[framework]) {
708
+ log.error(`Unknown framework: ${framework}`);
709
+ log.info('Available frameworks: ' + Object.keys(FRAMEWORKS).join(', '));
710
+ process.exit(1);
711
+ }
712
+
713
+ const config = FRAMEWORKS[framework];
714
+
715
+ // Get port
716
+ if (!port) {
717
+ port = await question('Port', config.defaultPort.toString());
718
+ }
719
+ port = parseInt(port, 10);
720
+
721
+ log.info(`Creating ${config.name} app: ${appName} (port: ${port})`);
722
+ console.log('');
723
+
724
+ // Create app
725
+ if (config.createCommand) {
726
+ // Use framework's CLI
727
+ log.info(`Running ${config.name} installer...`);
728
+
729
+ // Create apps directory if needed
730
+ if (!fs.existsSync(APPS_DIR)) {
731
+ fs.mkdirSync(APPS_DIR, { recursive: true });
732
+ }
733
+
734
+ try {
735
+ run(config.createCommand(appName), APPS_DIR);
736
+ } catch (error) {
737
+ log.error(`Failed to create app with ${config.name} CLI`);
738
+ process.exit(1);
739
+ }
740
+
741
+ // Update package.json name to use @repo/ prefix
742
+ const pkgPath = path.join(APP_DIR, 'package.json');
743
+ if (fs.existsSync(pkgPath)) {
744
+ const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf-8'));
745
+ pkg.name = `@repo/${appName}`;
746
+ fs.writeFileSync(pkgPath, JSON.stringify(pkg, null, 2));
747
+ }
748
+ } else {
749
+ // Manual setup for backend frameworks
750
+ log.info(`Setting up ${config.name} app manually...`);
751
+ createBackendApp(APP_DIR, appName, framework, port);
752
+ }
753
+
754
+ // Create docker directory
755
+ fs.mkdirSync(path.join(APP_DIR, 'docker'), { recursive: true });
756
+
757
+ // Create Dockerfile
758
+ const dockerfileContent = DOCKERFILES[config.dockerfile](appName, port);
759
+ fs.writeFileSync(path.join(APP_DIR, 'docker', 'Dockerfile'), dockerfileContent);
760
+
761
+ // Create nginx config for Vite apps
762
+ if (config.dockerfile === 'vite') {
763
+ createNginxConfig(APP_DIR, port);
764
+ }
765
+
766
+ // Create docker-compose files
767
+ createDockerComposeFiles(APP_DIR, appName, port, framework);
768
+
769
+ // Update main docker-compose
770
+ updateMainCompose(appName);
771
+
772
+ // Install dependencies if it was a manual setup
773
+ if (!config.createCommand) {
774
+ log.info('Installing dependencies...');
775
+ try {
776
+ const installCmd = getInstallCommand(pm);
777
+ log.info(`> ${installCmd}`);
778
+ run(installCmd, ROOT_DIR);
779
+ } catch (error) {
780
+ log.warn(`Failed to install dependencies. Run "${getInstallCommand(pm)}" manually.`);
781
+ }
782
+ }
783
+
784
+ // Success message
785
+ console.log('');
786
+ log.success(`Created app: apps/${appName}`);
787
+ console.log('');
788
+ console.log(`${colors.cyan}Files created:${colors.reset}`);
789
+ console.log(` - apps/${appName}/`);
790
+ console.log(` - apps/${appName}/docker/Dockerfile`);
791
+ console.log(` - apps/${appName}/docker-compose.yml`);
792
+ console.log(` - apps/${appName}/docker-compose.prod.yml`);
793
+ if (config.dockerfile === 'vite') {
794
+ console.log(` - apps/${appName}/docker/nginx.conf`);
795
+ }
796
+ console.log('');
797
+ console.log(`${colors.cyan}Commands:${colors.reset}`);
798
+ console.log(` Dev (local): cd apps/${appName} && ${runCmd} dev`);
799
+ console.log(` Dev (docker): cd apps/${appName} && docker compose up`);
800
+ console.log(` Dev (all apps): ${runCmd} docker:dev`);
801
+ console.log(` Build: ${runCmd} build --filter=@repo/${appName}`);
802
+ console.log('');
803
+ }
804
+
805
+ main().catch((error) => {
806
+ log.error(error.message);
807
+ process.exit(1);
808
+ });