@geekmidas/cli 0.18.0 → 0.20.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 (118) hide show
  1. package/dist/{bundler-C74EKlNa.cjs → bundler-CyHg1v_T.cjs} +3 -3
  2. package/dist/{bundler-C74EKlNa.cjs.map → bundler-CyHg1v_T.cjs.map} +1 -1
  3. package/dist/{bundler-B6z6HEeh.mjs → bundler-DQIuE3Kn.mjs} +3 -3
  4. package/dist/{bundler-B6z6HEeh.mjs.map → bundler-DQIuE3Kn.mjs.map} +1 -1
  5. package/dist/{config-DYULeEv8.mjs → config-BaYqrF3n.mjs} +48 -10
  6. package/dist/config-BaYqrF3n.mjs.map +1 -0
  7. package/dist/{config-AmInkU7k.cjs → config-CxrLu8ia.cjs} +53 -9
  8. package/dist/config-CxrLu8ia.cjs.map +1 -0
  9. package/dist/config.cjs +4 -1
  10. package/dist/config.d.cts +27 -2
  11. package/dist/config.d.cts.map +1 -1
  12. package/dist/config.d.mts +27 -2
  13. package/dist/config.d.mts.map +1 -1
  14. package/dist/config.mjs +3 -2
  15. package/dist/dokploy-api-B0w17y4_.mjs +3 -0
  16. package/dist/{dokploy-api-CaETb2L6.mjs → dokploy-api-B9qR2Yn1.mjs} +1 -1
  17. package/dist/{dokploy-api-CaETb2L6.mjs.map → dokploy-api-B9qR2Yn1.mjs.map} +1 -1
  18. package/dist/dokploy-api-BnGeUqN4.cjs +3 -0
  19. package/dist/{dokploy-api-C7F9VykY.cjs → dokploy-api-C5czOZoc.cjs} +1 -1
  20. package/dist/{dokploy-api-C7F9VykY.cjs.map → dokploy-api-C5czOZoc.cjs.map} +1 -1
  21. package/dist/{encryption-D7Efcdi9.cjs → encryption-BAz0xQ1Q.cjs} +1 -1
  22. package/dist/{encryption-D7Efcdi9.cjs.map → encryption-BAz0xQ1Q.cjs.map} +1 -1
  23. package/dist/{encryption-h4Nb6W-M.mjs → encryption-JtMsiGNp.mjs} +2 -2
  24. package/dist/{encryption-h4Nb6W-M.mjs.map → encryption-JtMsiGNp.mjs.map} +1 -1
  25. package/dist/index-CWN-bgrO.d.mts +495 -0
  26. package/dist/index-CWN-bgrO.d.mts.map +1 -0
  27. package/dist/index-DEWYvYvg.d.cts +495 -0
  28. package/dist/index-DEWYvYvg.d.cts.map +1 -0
  29. package/dist/index.cjs +2640 -564
  30. package/dist/index.cjs.map +1 -1
  31. package/dist/index.mjs +2635 -564
  32. package/dist/index.mjs.map +1 -1
  33. package/dist/{openapi-CZVcfxk-.mjs → openapi-CgqR6Jkw.mjs} +3 -3
  34. package/dist/{openapi-CZVcfxk-.mjs.map → openapi-CgqR6Jkw.mjs.map} +1 -1
  35. package/dist/{openapi-C89hhkZC.cjs → openapi-DfpxS0xv.cjs} +8 -2
  36. package/dist/{openapi-C89hhkZC.cjs.map → openapi-DfpxS0xv.cjs.map} +1 -1
  37. package/dist/{openapi-react-query-CM2_qlW9.mjs → openapi-react-query-5rSortLH.mjs} +1 -1
  38. package/dist/{openapi-react-query-CM2_qlW9.mjs.map → openapi-react-query-5rSortLH.mjs.map} +1 -1
  39. package/dist/{openapi-react-query-iKjfLzff.cjs → openapi-react-query-DvNpdDpM.cjs} +1 -1
  40. package/dist/{openapi-react-query-iKjfLzff.cjs.map → openapi-react-query-DvNpdDpM.cjs.map} +1 -1
  41. package/dist/openapi-react-query.cjs +1 -1
  42. package/dist/openapi-react-query.mjs +1 -1
  43. package/dist/openapi.cjs +3 -2
  44. package/dist/openapi.d.cts +1 -1
  45. package/dist/openapi.d.mts +1 -1
  46. package/dist/openapi.mjs +3 -2
  47. package/dist/{storage-Bn3K9Ccu.cjs → storage-BPRgh3DU.cjs} +136 -5
  48. package/dist/storage-BPRgh3DU.cjs.map +1 -0
  49. package/dist/{storage-nkGIjeXt.mjs → storage-DNj_I11J.mjs} +1 -1
  50. package/dist/storage-Dhst7BhI.mjs +272 -0
  51. package/dist/storage-Dhst7BhI.mjs.map +1 -0
  52. package/dist/{storage-UfyTn7Zm.cjs → storage-fOR8dMu5.cjs} +1 -1
  53. package/dist/{types-iFk5ms7y.d.mts → types-K2uQJ-FO.d.mts} +2 -2
  54. package/dist/{types-BgaMXsUa.d.cts.map → types-K2uQJ-FO.d.mts.map} +1 -1
  55. package/dist/{types-BgaMXsUa.d.cts → types-l53qUmGt.d.cts} +2 -2
  56. package/dist/{types-iFk5ms7y.d.mts.map → types-l53qUmGt.d.cts.map} +1 -1
  57. package/dist/workspace/index.cjs +19 -0
  58. package/dist/workspace/index.d.cts +3 -0
  59. package/dist/workspace/index.d.mts +3 -0
  60. package/dist/workspace/index.mjs +3 -0
  61. package/dist/workspace-CPLEZDZf.mjs +3788 -0
  62. package/dist/workspace-CPLEZDZf.mjs.map +1 -0
  63. package/dist/workspace-iWgBlX6h.cjs +3885 -0
  64. package/dist/workspace-iWgBlX6h.cjs.map +1 -0
  65. package/package.json +9 -4
  66. package/src/build/__tests__/workspace-build.spec.ts +215 -0
  67. package/src/build/index.ts +189 -1
  68. package/src/config.ts +71 -14
  69. package/src/deploy/__tests__/docker.spec.ts +1 -1
  70. package/src/deploy/__tests__/index.spec.ts +305 -1
  71. package/src/deploy/index.ts +426 -4
  72. package/src/deploy/types.ts +32 -0
  73. package/src/dev/__tests__/index.spec.ts +572 -1
  74. package/src/dev/index.ts +582 -2
  75. package/src/docker/__tests__/compose.spec.ts +425 -0
  76. package/src/docker/__tests__/templates.spec.ts +145 -0
  77. package/src/docker/compose.ts +248 -0
  78. package/src/docker/index.ts +159 -3
  79. package/src/docker/templates.ts +219 -4
  80. package/src/index.ts +24 -0
  81. package/src/init/__tests__/generators.spec.ts +17 -24
  82. package/src/init/__tests__/init.spec.ts +157 -5
  83. package/src/init/generators/auth.ts +220 -0
  84. package/src/init/generators/config.ts +61 -4
  85. package/src/init/generators/docker.ts +115 -8
  86. package/src/init/generators/env.ts +7 -127
  87. package/src/init/generators/index.ts +1 -0
  88. package/src/init/generators/models.ts +3 -1
  89. package/src/init/generators/monorepo.ts +154 -10
  90. package/src/init/generators/package.ts +5 -3
  91. package/src/init/generators/web.ts +213 -0
  92. package/src/init/index.ts +290 -58
  93. package/src/init/templates/api.ts +38 -29
  94. package/src/init/templates/index.ts +132 -4
  95. package/src/init/templates/minimal.ts +33 -35
  96. package/src/init/templates/serverless.ts +16 -19
  97. package/src/init/templates/worker.ts +50 -25
  98. package/src/init/versions.ts +47 -0
  99. package/src/secrets/keystore.ts +144 -0
  100. package/src/secrets/storage.ts +109 -6
  101. package/src/test/index.ts +97 -0
  102. package/src/workspace/__tests__/client-generator.spec.ts +357 -0
  103. package/src/workspace/__tests__/index.spec.ts +543 -0
  104. package/src/workspace/__tests__/schema.spec.ts +519 -0
  105. package/src/workspace/__tests__/type-inference.spec.ts +251 -0
  106. package/src/workspace/client-generator.ts +307 -0
  107. package/src/workspace/index.ts +372 -0
  108. package/src/workspace/schema.ts +368 -0
  109. package/src/workspace/types.ts +336 -0
  110. package/tsconfig.tsbuildinfo +1 -1
  111. package/tsdown.config.ts +1 -0
  112. package/dist/config-AmInkU7k.cjs.map +0 -1
  113. package/dist/config-DYULeEv8.mjs.map +0 -1
  114. package/dist/dokploy-api-B7KxOQr3.cjs +0 -3
  115. package/dist/dokploy-api-DHvfmWbi.mjs +0 -3
  116. package/dist/storage-BaOP55oq.mjs +0 -147
  117. package/dist/storage-BaOP55oq.mjs.map +0 -1
  118. package/dist/storage-Bn3K9Ccu.cjs.map +0 -1
package/src/index.ts CHANGED
@@ -18,6 +18,7 @@ import {
18
18
  secretsSetCommand,
19
19
  secretsShowCommand,
20
20
  } from './secrets';
21
+ import { type TestOptions, testCommand } from './test/index';
21
22
  import type { ComposeServiceName, LegacyProvider, MainProvider } from './types';
22
23
 
23
24
  const program = new Command();
@@ -40,6 +41,7 @@ program
40
41
  .option('-y, --yes', 'Skip prompts, use defaults', false)
41
42
  .option('--monorepo', 'Setup as monorepo with packages/models', false)
42
43
  .option('--api-path <path>', 'API app path in monorepo (default: apps/api)')
44
+ .option('--pm <manager>', 'Package manager (pnpm, npm, yarn, bun)')
43
45
  .action(async (name: string | undefined, options: InitOptions) => {
44
46
  try {
45
47
  const globalOptions = program.opts();
@@ -157,6 +159,28 @@ program
157
159
  }
158
160
  });
159
161
 
162
+ program
163
+ .command('test')
164
+ .description('Run tests with secrets loaded from environment')
165
+ .option('--stage <stage>', 'Stage to load secrets from', 'development')
166
+ .option('--run', 'Run tests once without watch mode')
167
+ .option('--watch', 'Enable watch mode')
168
+ .option('--coverage', 'Generate coverage report')
169
+ .option('--ui', 'Open Vitest UI')
170
+ .argument('[pattern]', 'Pattern to filter tests')
171
+ .action(async (pattern: string | undefined, options: TestOptions) => {
172
+ try {
173
+ const globalOptions = program.opts();
174
+ if (globalOptions.cwd) {
175
+ process.chdir(globalOptions.cwd);
176
+ }
177
+ await testCommand({ ...options, pattern });
178
+ } catch (error) {
179
+ console.error(error instanceof Error ? error.message : 'Command failed');
180
+ process.exit(1);
181
+ }
182
+ });
183
+
160
184
  program
161
185
  .command('cron')
162
186
  .description('Manage cron jobs')
@@ -35,13 +35,13 @@ describe('generatePackageJson', () => {
35
35
  it('should include telescope when enabled', () => {
36
36
  const files = generatePackageJson(baseOptions, minimalTemplate);
37
37
  const pkg = JSON.parse(files[0].content);
38
- expect(pkg.dependencies['@geekmidas/telescope']).toBe('workspace:*');
38
+ expect(pkg.dependencies['@geekmidas/telescope']).toMatch(/^~/);
39
39
  });
40
40
 
41
41
  it('should include database dependencies when enabled', () => {
42
42
  const files = generatePackageJson(baseOptions, minimalTemplate);
43
43
  const pkg = JSON.parse(files[0].content);
44
- expect(pkg.dependencies['@geekmidas/db']).toBe('workspace:*');
44
+ expect(pkg.dependencies['@geekmidas/db']).toMatch(/^~/);
45
45
  expect(pkg.dependencies.kysely).toBeDefined();
46
46
  expect(pkg.dependencies.pg).toBeDefined();
47
47
  });
@@ -53,12 +53,12 @@ describe('generatePackageJson', () => {
53
53
  expect(pkg.dependencies['@geekmidas/telescope']).toBeUndefined();
54
54
  });
55
55
 
56
- it('should use workspace:* for @geekmidas packages', () => {
56
+ it('should use tilde versions for @geekmidas packages', () => {
57
57
  const files = generatePackageJson(baseOptions, minimalTemplate);
58
58
  const pkg = JSON.parse(files[0].content);
59
- expect(pkg.dependencies['@geekmidas/constructs']).toBe('workspace:*');
60
- expect(pkg.dependencies['@geekmidas/envkit']).toBe('workspace:*');
61
- expect(pkg.dependencies['@geekmidas/logger']).toBe('workspace:*');
59
+ expect(pkg.dependencies['@geekmidas/constructs']).toMatch(/^~/);
60
+ expect(pkg.dependencies['@geekmidas/envkit']).toMatch(/^~/);
61
+ expect(pkg.dependencies['@geekmidas/logger']).toMatch(/^~/);
62
62
  });
63
63
 
64
64
  it('should use tilde versions for external packages', () => {
@@ -169,38 +169,31 @@ describe('generateConfigFiles', () => {
169
169
  });
170
170
 
171
171
  describe('generateEnvFiles', () => {
172
- it('should generate all env files for non-monorepo', () => {
172
+ it('should only generate .gitignore for non-monorepo', () => {
173
+ // .env files are no longer generated - secrets are encrypted instead
173
174
  const files = generateEnvFiles(baseOptions, minimalTemplate);
174
175
  const paths = files.map((f) => f.path);
175
- expect(paths).toContain('.env');
176
- expect(paths).toContain('.env.example');
177
- expect(paths).toContain('.env.development');
178
- expect(paths).toContain('.env.test');
179
176
  expect(paths).toContain('.gitignore');
177
+ expect(paths).not.toContain('.env');
178
+ expect(paths).not.toContain('.env.example');
179
+ expect(paths).not.toContain('.env.development');
180
+ expect(paths).not.toContain('.env.test');
180
181
  });
181
182
 
182
- it('should not generate .gitignore for monorepo', () => {
183
+ it('should not generate any files for monorepo (gitignore at root)', () => {
183
184
  const options: TemplateOptions = {
184
185
  ...baseOptions,
185
186
  monorepo: true,
186
187
  apiPath: 'apps/api',
187
188
  };
188
189
  const files = generateEnvFiles(options, minimalTemplate);
189
- const paths = files.map((f) => f.path);
190
- expect(paths).not.toContain('.gitignore');
190
+ expect(files).toHaveLength(0);
191
191
  });
192
192
 
193
- it('should include DATABASE_URL when database is enabled', () => {
193
+ it('should include .gkm in gitignore', () => {
194
194
  const files = generateEnvFiles(baseOptions, minimalTemplate);
195
- const envFile = files.find((f) => f.path === '.env');
196
- expect(envFile?.content).toContain('DATABASE_URL');
197
- });
198
-
199
- it('should include RABBITMQ_URL for worker template', () => {
200
- const options = { ...baseOptions, template: 'worker' as const };
201
- const files = generateEnvFiles(options, workerTemplate);
202
- const envFile = files.find((f) => f.path === '.env');
203
- expect(envFile?.content).toContain('RABBITMQ_URL');
195
+ const gitignore = files.find((f) => f.path === '.gitignore');
196
+ expect(gitignore?.content).toContain('.gkm/');
204
197
  });
205
198
  });
206
199
 
@@ -37,9 +37,10 @@ describe('initCommand', () => {
37
37
  expect(existsSync(join(projectDir, 'biome.json'))).toBe(true);
38
38
  expect(existsSync(join(projectDir, 'turbo.json'))).toBe(true);
39
39
  expect(existsSync(join(projectDir, 'docker-compose.yml'))).toBe(true);
40
- expect(existsSync(join(projectDir, '.env'))).toBe(true);
41
- expect(existsSync(join(projectDir, '.env.development'))).toBe(true);
42
- expect(existsSync(join(projectDir, '.env.test'))).toBe(true);
40
+ // Secrets are now encrypted instead of .env files
41
+ expect(
42
+ existsSync(join(projectDir, '.gkm/secrets/development.json')),
43
+ ).toBe(true);
43
44
  expect(existsSync(join(projectDir, '.gitignore'))).toBe(true);
44
45
  expect(existsSync(join(projectDir, 'src/config/env.ts'))).toBe(true);
45
46
  expect(existsSync(join(projectDir, 'src/config/logger.ts'))).toBe(true);
@@ -61,8 +62,8 @@ describe('initCommand', () => {
61
62
 
62
63
  expect(pkg.name).toBe('my-api');
63
64
  expect(pkg.type).toBe('module');
64
- expect(pkg.dependencies['@geekmidas/constructs']).toBe('workspace:*');
65
- expect(pkg.dependencies['@geekmidas/telescope']).toBe('workspace:*');
65
+ expect(pkg.dependencies['@geekmidas/constructs']).toMatch(/^~/);
66
+ expect(pkg.dependencies['@geekmidas/telescope']).toMatch(/^~/);
66
67
  expect(pkg.dependencies.zod).toMatch(/^~/);
67
68
  expect(pkg.devDependencies['@biomejs/biome']).toBeDefined();
68
69
  expect(pkg.devDependencies.turbo).toBeDefined();
@@ -298,6 +299,157 @@ describe('initCommand', () => {
298
299
  });
299
300
  });
300
301
 
302
+ describe('fullstack template', () => {
303
+ it('should create monorepo with api and web apps', async () => {
304
+ await initCommand('my-fullstack', {
305
+ template: 'fullstack',
306
+ yes: true,
307
+ skipInstall: true,
308
+ });
309
+
310
+ const projectDir = join(tempDir, 'my-fullstack');
311
+
312
+ // Root files
313
+ expect(existsSync(join(projectDir, 'package.json'))).toBe(true);
314
+ expect(existsSync(join(projectDir, 'pnpm-workspace.yaml'))).toBe(true);
315
+ expect(existsSync(join(projectDir, 'tsconfig.json'))).toBe(true);
316
+ expect(existsSync(join(projectDir, 'biome.json'))).toBe(true);
317
+ expect(existsSync(join(projectDir, 'turbo.json'))).toBe(true);
318
+ expect(existsSync(join(projectDir, 'gkm.config.ts'))).toBe(true);
319
+
320
+ // API app files
321
+ expect(existsSync(join(projectDir, 'apps/api/package.json'))).toBe(true);
322
+ expect(existsSync(join(projectDir, 'apps/api/tsconfig.json'))).toBe(true);
323
+ expect(
324
+ existsSync(join(projectDir, 'apps/api/src/endpoints/health.ts')),
325
+ ).toBe(true);
326
+
327
+ // Web app files
328
+ expect(existsSync(join(projectDir, 'apps/web/package.json'))).toBe(true);
329
+ expect(existsSync(join(projectDir, 'apps/web/next.config.ts'))).toBe(
330
+ true,
331
+ );
332
+ expect(existsSync(join(projectDir, 'apps/web/tsconfig.json'))).toBe(true);
333
+ expect(existsSync(join(projectDir, 'apps/web/src/app/layout.tsx'))).toBe(
334
+ true,
335
+ );
336
+ expect(existsSync(join(projectDir, 'apps/web/src/app/page.tsx'))).toBe(
337
+ true,
338
+ );
339
+
340
+ // Models package
341
+ expect(existsSync(join(projectDir, 'packages/models/package.json'))).toBe(
342
+ true,
343
+ );
344
+ });
345
+
346
+ it('should create workspace config with defineWorkspace', async () => {
347
+ await initCommand('my-fullstack', {
348
+ template: 'fullstack',
349
+ yes: true,
350
+ skipInstall: true,
351
+ });
352
+
353
+ const configPath = join(tempDir, 'my-fullstack', 'gkm.config.ts');
354
+ const content = await readFile(configPath, 'utf-8');
355
+
356
+ expect(content).toContain('import { defineWorkspace }');
357
+ expect(content).toContain("name: 'my-fullstack'");
358
+ expect(content).toContain("type: 'backend'");
359
+ expect(content).toContain("path: 'apps/api'");
360
+ expect(content).toContain("type: 'frontend'");
361
+ expect(content).toContain("framework: 'nextjs'");
362
+ expect(content).toContain("path: 'apps/web'");
363
+ expect(content).toContain("packages: ['packages/*']");
364
+ });
365
+
366
+ it('should create root package.json with gkm commands', async () => {
367
+ await initCommand('my-fullstack', {
368
+ template: 'fullstack',
369
+ yes: true,
370
+ skipInstall: true,
371
+ });
372
+
373
+ const pkgPath = join(tempDir, 'my-fullstack', 'package.json');
374
+ const content = await readFile(pkgPath, 'utf-8');
375
+ const pkg = JSON.parse(content);
376
+
377
+ expect(pkg.scripts.dev).toBe('gkm dev');
378
+ expect(pkg.scripts.build).toBe('gkm build');
379
+ expect(pkg.devDependencies['@geekmidas/cli']).toBeDefined();
380
+ });
381
+
382
+ it('should create Next.js web app with models dependency', async () => {
383
+ await initCommand('my-fullstack', {
384
+ template: 'fullstack',
385
+ yes: true,
386
+ skipInstall: true,
387
+ });
388
+
389
+ const pkgPath = join(tempDir, 'my-fullstack', 'apps/web/package.json');
390
+ const content = await readFile(pkgPath, 'utf-8');
391
+ const pkg = JSON.parse(content);
392
+
393
+ expect(pkg.name).toBe('@my-fullstack/web');
394
+ expect(pkg.dependencies['@my-fullstack/models']).toBe('workspace:*');
395
+ expect(pkg.dependencies.next).toBeDefined();
396
+ expect(pkg.dependencies.react).toBeDefined();
397
+ expect(pkg.scripts.dev).toContain('next dev');
398
+ });
399
+
400
+ it('should include services config in workspace', async () => {
401
+ await initCommand('my-fullstack', {
402
+ template: 'fullstack',
403
+ yes: true,
404
+ skipInstall: true,
405
+ });
406
+
407
+ const configPath = join(tempDir, 'my-fullstack', 'gkm.config.ts');
408
+ const content = await readFile(configPath, 'utf-8');
409
+
410
+ expect(content).toContain('services:');
411
+ expect(content).toContain('db: true');
412
+ expect(content).toContain('cache: true');
413
+ expect(content).toContain('mail: true');
414
+ });
415
+
416
+ it('should include deploy config for dokploy', async () => {
417
+ await initCommand('my-fullstack', {
418
+ template: 'fullstack',
419
+ yes: true,
420
+ skipInstall: true,
421
+ });
422
+
423
+ const configPath = join(tempDir, 'my-fullstack', 'gkm.config.ts');
424
+ const content = await readFile(configPath, 'utf-8');
425
+
426
+ expect(content).toContain('deploy:');
427
+ expect(content).toContain("default: 'dokploy'");
428
+
429
+ const pkgPath = join(tempDir, 'my-fullstack', 'package.json');
430
+ const pkgContent = await readFile(pkgPath, 'utf-8');
431
+ const pkg = JSON.parse(pkgContent);
432
+
433
+ expect(pkg.scripts.deploy).toContain('gkm deploy');
434
+ });
435
+
436
+ it('should NOT create app-level gkm.config.ts for api', async () => {
437
+ await initCommand('my-fullstack', {
438
+ template: 'fullstack',
439
+ yes: true,
440
+ skipInstall: true,
441
+ });
442
+
443
+ // Config should be at root only, not in apps/api
444
+ expect(existsSync(join(tempDir, 'my-fullstack', 'gkm.config.ts'))).toBe(
445
+ true,
446
+ );
447
+ expect(
448
+ existsSync(join(tempDir, 'my-fullstack', 'apps/api/gkm.config.ts')),
449
+ ).toBe(false);
450
+ });
451
+ });
452
+
301
453
  describe('docker-compose', () => {
302
454
  it('should include postgres for database-enabled projects', async () => {
303
455
  await initCommand('my-api', {
@@ -0,0 +1,220 @@
1
+ import type { GeneratedFile, TemplateOptions } from '../templates/index.js';
2
+ import { GEEKMIDAS_VERSIONS } from '../versions.js';
3
+
4
+ /**
5
+ * Generate auth app files for fullstack template
6
+ * Uses better-auth with magic link authentication
7
+ */
8
+ export function generateAuthAppFiles(
9
+ options: TemplateOptions,
10
+ ): GeneratedFile[] {
11
+ if (!options.monorepo || options.template !== 'fullstack') {
12
+ return [];
13
+ }
14
+
15
+ const packageName = `@${options.name}/auth`;
16
+ const modelsPackage = `@${options.name}/models`;
17
+
18
+ // package.json for auth app
19
+ const packageJson = {
20
+ name: packageName,
21
+ version: '0.0.1',
22
+ private: true,
23
+ type: 'module',
24
+ scripts: {
25
+ dev: 'tsx watch src/index.ts',
26
+ build: 'tsc',
27
+ start: 'node dist/index.js',
28
+ typecheck: 'tsc --noEmit',
29
+ },
30
+ dependencies: {
31
+ [modelsPackage]: 'workspace:*',
32
+ '@geekmidas/envkit': GEEKMIDAS_VERSIONS['@geekmidas/envkit'],
33
+ '@geekmidas/logger': GEEKMIDAS_VERSIONS['@geekmidas/logger'],
34
+ '@hono/node-server': '~1.13.0',
35
+ 'better-auth': '~1.2.0',
36
+ hono: '~4.8.0',
37
+ kysely: '~0.27.0',
38
+ pg: '~8.13.0',
39
+ },
40
+ devDependencies: {
41
+ '@types/node': '~22.0.0',
42
+ '@types/pg': '~8.11.0',
43
+ tsx: '~4.20.0',
44
+ typescript: '~5.8.2',
45
+ },
46
+ };
47
+
48
+ // tsconfig.json for auth app
49
+ const tsConfig = {
50
+ extends: '../../tsconfig.json',
51
+ compilerOptions: {
52
+ noEmit: true,
53
+ baseUrl: '.',
54
+ paths: {
55
+ [`@${options.name}/*`]: ['../../packages/*/src'],
56
+ },
57
+ },
58
+ include: ['src/**/*.ts'],
59
+ exclude: ['node_modules', 'dist'],
60
+ };
61
+
62
+ // src/config/env.ts
63
+ const envTs = `import { Credentials } from '@geekmidas/envkit/credentials';
64
+ import { EnvironmentParser } from '@geekmidas/envkit';
65
+
66
+ export const envParser = new EnvironmentParser({ ...process.env, ...Credentials });
67
+
68
+ // Global config - only minimal shared values
69
+ // Service-specific config should be parsed where needed
70
+ export const config = envParser
71
+ .create((get) => ({
72
+ nodeEnv: get('NODE_ENV').enum(['development', 'test', 'production']).default('development'),
73
+ stage: get('STAGE').enum(['development', 'staging', 'production']).default('development'),
74
+ }))
75
+ .parse();
76
+ `;
77
+
78
+ // src/config/logger.ts
79
+ const loggerTs = `import { createLogger } from '@geekmidas/logger/${options.loggerType}';
80
+
81
+ export const logger = createLogger();
82
+ `;
83
+
84
+ // src/auth.ts - better-auth instance with magic link
85
+ const authTs = `import { betterAuth } from 'better-auth';
86
+ import { magicLink } from 'better-auth/plugins';
87
+ import { Pool } from 'pg';
88
+ import { envParser } from './config/env.js';
89
+ import { logger } from './config/logger.js';
90
+
91
+ // Parse auth-specific config (no defaults - values from secrets)
92
+ const authConfig = envParser
93
+ .create((get) => ({
94
+ databaseUrl: get('DATABASE_URL').string(),
95
+ baseUrl: get('BETTER_AUTH_URL').string(),
96
+ trustedOrigins: get('BETTER_AUTH_TRUSTED_ORIGINS').string(),
97
+ secret: get('BETTER_AUTH_SECRET').string(),
98
+ }))
99
+ .parse();
100
+
101
+ export const auth = betterAuth({
102
+ database: new Pool({
103
+ connectionString: authConfig.databaseUrl,
104
+ }),
105
+ baseURL: authConfig.baseUrl,
106
+ trustedOrigins: authConfig.trustedOrigins.split(','),
107
+ secret: authConfig.secret,
108
+ plugins: [
109
+ magicLink({
110
+ sendMagicLink: async ({ email, url }) => {
111
+ // TODO: Implement email sending using @geekmidas/emailkit
112
+ // For development, log the magic link
113
+ logger.info({ email, url }, 'Magic link generated');
114
+ console.log('\\n================================');
115
+ console.log('MAGIC LINK FOR:', email);
116
+ console.log(url);
117
+ console.log('================================\\n');
118
+ },
119
+ expiresIn: 300, // 5 minutes
120
+ }),
121
+ ],
122
+ emailAndPassword: {
123
+ enabled: false, // Only magic link for now
124
+ },
125
+ });
126
+
127
+ export type Auth = typeof auth;
128
+ `;
129
+
130
+ // src/index.ts - Hono app entry point
131
+ const indexTs = `import { Hono } from 'hono';
132
+ import { cors } from 'hono/cors';
133
+ import { serve } from '@hono/node-server';
134
+ import { auth } from './auth.js';
135
+ import { envParser } from './config/env.js';
136
+ import { logger } from './config/logger.js';
137
+
138
+ // Parse server config (no defaults - values from secrets)
139
+ const serverConfig = envParser
140
+ .create((get) => ({
141
+ port: get('PORT').string().transform(Number),
142
+ trustedOrigins: get('BETTER_AUTH_TRUSTED_ORIGINS').string(),
143
+ }))
144
+ .parse();
145
+
146
+ const app = new Hono();
147
+
148
+ // CORS must be registered before routes
149
+ app.use(
150
+ '/api/auth/*',
151
+ cors({
152
+ origin: serverConfig.trustedOrigins.split(','),
153
+ allowHeaders: ['Content-Type', 'Authorization'],
154
+ allowMethods: ['POST', 'GET', 'OPTIONS'],
155
+ credentials: true,
156
+ }),
157
+ );
158
+
159
+ // Health check endpoint
160
+ app.get('/health', (c) => {
161
+ return c.json({
162
+ status: 'ok',
163
+ service: 'auth',
164
+ timestamp: new Date().toISOString(),
165
+ });
166
+ });
167
+
168
+ // Mount better-auth handler
169
+ app.on(['POST', 'GET'], '/api/auth/*', (c) => {
170
+ return auth.handler(c.req.raw);
171
+ });
172
+
173
+ logger.info({ port: serverConfig.port }, 'Starting auth server');
174
+
175
+ serve({
176
+ fetch: app.fetch,
177
+ port: serverConfig.port,
178
+ }, (info) => {
179
+ logger.info({ port: info.port }, 'Auth server running');
180
+ });
181
+ `;
182
+
183
+ // .gitignore for auth app
184
+ const gitignore = `node_modules/
185
+ dist/
186
+ .env.local
187
+ *.log
188
+ `;
189
+
190
+ return [
191
+ {
192
+ path: 'apps/auth/package.json',
193
+ content: `${JSON.stringify(packageJson, null, 2)}\n`,
194
+ },
195
+ {
196
+ path: 'apps/auth/tsconfig.json',
197
+ content: `${JSON.stringify(tsConfig, null, 2)}\n`,
198
+ },
199
+ {
200
+ path: 'apps/auth/src/config/env.ts',
201
+ content: envTs,
202
+ },
203
+ {
204
+ path: 'apps/auth/src/config/logger.ts',
205
+ content: loggerTs,
206
+ },
207
+ {
208
+ path: 'apps/auth/src/auth.ts',
209
+ content: authTs,
210
+ },
211
+ {
212
+ path: 'apps/auth/src/index.ts',
213
+ content: indexTs,
214
+ },
215
+ {
216
+ path: 'apps/auth/.gitignore',
217
+ content: gitignore,
218
+ },
219
+ ];
220
+ }
@@ -14,6 +14,7 @@ export function generateConfigFiles(
14
14
  const { telescope, studio, routesStructure } = options;
15
15
  const isServerless = template.name === 'serverless';
16
16
  const hasWorker = template.name === 'worker';
17
+ const isFullstack = options.template === 'fullstack';
17
18
 
18
19
  // Get routes glob pattern based on structure
19
20
  const getRoutesGlob = () => {
@@ -27,7 +28,21 @@ export function generateConfigFiles(
27
28
  }
28
29
  };
29
30
 
30
- // Build gkm.config.ts
31
+ // For fullstack template, generate workspace config at root
32
+ // Single app config is still generated for non-fullstack monorepo setups
33
+ if (isFullstack) {
34
+ // Workspace config is generated in monorepo.ts for fullstack
35
+ return generateSingleAppConfigFiles(options, template, {
36
+ telescope,
37
+ studio,
38
+ routesStructure,
39
+ isServerless,
40
+ hasWorker,
41
+ getRoutesGlob,
42
+ });
43
+ }
44
+
45
+ // Build gkm.config.ts for single-app
31
46
  let gkmConfig = `import { defineConfig } from '@geekmidas/cli/config';
32
47
 
33
48
  export default defineConfig({
@@ -70,12 +85,12 @@ export default defineConfig({
70
85
  `;
71
86
 
72
87
  // Build tsconfig.json - extends root for monorepo, standalone for non-monorepo
88
+ // Using noEmit: true since typecheck is done via turbo
73
89
  const tsConfig = options.monorepo
74
90
  ? {
75
91
  extends: '../../tsconfig.json',
76
92
  compilerOptions: {
77
- outDir: './dist',
78
- rootDir: './src',
93
+ noEmit: true,
79
94
  baseUrl: '.',
80
95
  paths: {
81
96
  [`@${options.name}/*`]: ['../../packages/*/src'],
@@ -120,7 +135,7 @@ export default defineConfig({
120
135
 
121
136
  // Build biome.json
122
137
  const biomeConfig = {
123
- $schema: 'https://biomejs.dev/schemas/1.9.4/schema.json',
138
+ $schema: 'https://biomejs.dev/schemas/2.3.0/schema.json',
124
139
  vcs: {
125
140
  enabled: true,
126
141
  clientKind: 'git',
@@ -213,3 +228,45 @@ export default defineConfig({
213
228
  },
214
229
  ];
215
230
  }
231
+
232
+ /**
233
+ * Helper to generate config files for API app in fullstack template
234
+ * (workspace config is at root, so no gkm.config.ts for app)
235
+ */
236
+ interface ConfigHelperOptions {
237
+ telescope: boolean;
238
+ studio: boolean;
239
+ routesStructure: string;
240
+ isServerless: boolean;
241
+ hasWorker: boolean;
242
+ getRoutesGlob: () => string;
243
+ }
244
+
245
+ function generateSingleAppConfigFiles(
246
+ options: TemplateOptions,
247
+ _template: TemplateConfig,
248
+ _helpers: ConfigHelperOptions,
249
+ ): GeneratedFile[] {
250
+ // For fullstack, only generate tsconfig.json for the API app
251
+ // The workspace gkm.config.ts is generated in monorepo.ts
252
+ // Using noEmit: true since typecheck is done via turbo
253
+ const tsConfig = {
254
+ extends: '../../tsconfig.json',
255
+ compilerOptions: {
256
+ noEmit: true,
257
+ baseUrl: '.',
258
+ paths: {
259
+ [`@${options.name}/*`]: ['../../packages/*/src'],
260
+ },
261
+ },
262
+ include: ['src/**/*.ts'],
263
+ exclude: ['node_modules', 'dist'],
264
+ };
265
+
266
+ return [
267
+ {
268
+ path: 'tsconfig.json',
269
+ content: `${JSON.stringify(tsConfig, null, 2)}\n`,
270
+ },
271
+ ];
272
+ }