@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.
- package/dist/{bundler-C74EKlNa.cjs → bundler-CyHg1v_T.cjs} +3 -3
- package/dist/{bundler-C74EKlNa.cjs.map → bundler-CyHg1v_T.cjs.map} +1 -1
- package/dist/{bundler-B6z6HEeh.mjs → bundler-DQIuE3Kn.mjs} +3 -3
- package/dist/{bundler-B6z6HEeh.mjs.map → bundler-DQIuE3Kn.mjs.map} +1 -1
- package/dist/{config-DYULeEv8.mjs → config-BaYqrF3n.mjs} +48 -10
- package/dist/config-BaYqrF3n.mjs.map +1 -0
- package/dist/{config-AmInkU7k.cjs → config-CxrLu8ia.cjs} +53 -9
- package/dist/config-CxrLu8ia.cjs.map +1 -0
- package/dist/config.cjs +4 -1
- package/dist/config.d.cts +27 -2
- package/dist/config.d.cts.map +1 -1
- package/dist/config.d.mts +27 -2
- package/dist/config.d.mts.map +1 -1
- package/dist/config.mjs +3 -2
- package/dist/dokploy-api-B0w17y4_.mjs +3 -0
- package/dist/{dokploy-api-CaETb2L6.mjs → dokploy-api-B9qR2Yn1.mjs} +1 -1
- package/dist/{dokploy-api-CaETb2L6.mjs.map → dokploy-api-B9qR2Yn1.mjs.map} +1 -1
- package/dist/dokploy-api-BnGeUqN4.cjs +3 -0
- package/dist/{dokploy-api-C7F9VykY.cjs → dokploy-api-C5czOZoc.cjs} +1 -1
- package/dist/{dokploy-api-C7F9VykY.cjs.map → dokploy-api-C5czOZoc.cjs.map} +1 -1
- package/dist/{encryption-D7Efcdi9.cjs → encryption-BAz0xQ1Q.cjs} +1 -1
- package/dist/{encryption-D7Efcdi9.cjs.map → encryption-BAz0xQ1Q.cjs.map} +1 -1
- package/dist/{encryption-h4Nb6W-M.mjs → encryption-JtMsiGNp.mjs} +2 -2
- package/dist/{encryption-h4Nb6W-M.mjs.map → encryption-JtMsiGNp.mjs.map} +1 -1
- package/dist/index-CWN-bgrO.d.mts +495 -0
- package/dist/index-CWN-bgrO.d.mts.map +1 -0
- package/dist/index-DEWYvYvg.d.cts +495 -0
- package/dist/index-DEWYvYvg.d.cts.map +1 -0
- package/dist/index.cjs +2640 -564
- package/dist/index.cjs.map +1 -1
- package/dist/index.mjs +2635 -564
- package/dist/index.mjs.map +1 -1
- package/dist/{openapi-CZVcfxk-.mjs → openapi-CgqR6Jkw.mjs} +3 -3
- package/dist/{openapi-CZVcfxk-.mjs.map → openapi-CgqR6Jkw.mjs.map} +1 -1
- package/dist/{openapi-C89hhkZC.cjs → openapi-DfpxS0xv.cjs} +8 -2
- package/dist/{openapi-C89hhkZC.cjs.map → openapi-DfpxS0xv.cjs.map} +1 -1
- package/dist/{openapi-react-query-CM2_qlW9.mjs → openapi-react-query-5rSortLH.mjs} +1 -1
- package/dist/{openapi-react-query-CM2_qlW9.mjs.map → openapi-react-query-5rSortLH.mjs.map} +1 -1
- package/dist/{openapi-react-query-iKjfLzff.cjs → openapi-react-query-DvNpdDpM.cjs} +1 -1
- package/dist/{openapi-react-query-iKjfLzff.cjs.map → openapi-react-query-DvNpdDpM.cjs.map} +1 -1
- package/dist/openapi-react-query.cjs +1 -1
- package/dist/openapi-react-query.mjs +1 -1
- package/dist/openapi.cjs +3 -2
- package/dist/openapi.d.cts +1 -1
- package/dist/openapi.d.mts +1 -1
- package/dist/openapi.mjs +3 -2
- package/dist/{storage-Bn3K9Ccu.cjs → storage-BPRgh3DU.cjs} +136 -5
- package/dist/storage-BPRgh3DU.cjs.map +1 -0
- package/dist/{storage-nkGIjeXt.mjs → storage-DNj_I11J.mjs} +1 -1
- package/dist/storage-Dhst7BhI.mjs +272 -0
- package/dist/storage-Dhst7BhI.mjs.map +1 -0
- package/dist/{storage-UfyTn7Zm.cjs → storage-fOR8dMu5.cjs} +1 -1
- package/dist/{types-iFk5ms7y.d.mts → types-K2uQJ-FO.d.mts} +2 -2
- package/dist/{types-BgaMXsUa.d.cts.map → types-K2uQJ-FO.d.mts.map} +1 -1
- package/dist/{types-BgaMXsUa.d.cts → types-l53qUmGt.d.cts} +2 -2
- package/dist/{types-iFk5ms7y.d.mts.map → types-l53qUmGt.d.cts.map} +1 -1
- package/dist/workspace/index.cjs +19 -0
- package/dist/workspace/index.d.cts +3 -0
- package/dist/workspace/index.d.mts +3 -0
- package/dist/workspace/index.mjs +3 -0
- package/dist/workspace-CPLEZDZf.mjs +3788 -0
- package/dist/workspace-CPLEZDZf.mjs.map +1 -0
- package/dist/workspace-iWgBlX6h.cjs +3885 -0
- package/dist/workspace-iWgBlX6h.cjs.map +1 -0
- package/package.json +9 -4
- package/src/build/__tests__/workspace-build.spec.ts +215 -0
- package/src/build/index.ts +189 -1
- package/src/config.ts +71 -14
- package/src/deploy/__tests__/docker.spec.ts +1 -1
- package/src/deploy/__tests__/index.spec.ts +305 -1
- package/src/deploy/index.ts +426 -4
- package/src/deploy/types.ts +32 -0
- package/src/dev/__tests__/index.spec.ts +572 -1
- package/src/dev/index.ts +582 -2
- package/src/docker/__tests__/compose.spec.ts +425 -0
- package/src/docker/__tests__/templates.spec.ts +145 -0
- package/src/docker/compose.ts +248 -0
- package/src/docker/index.ts +159 -3
- package/src/docker/templates.ts +219 -4
- package/src/index.ts +24 -0
- package/src/init/__tests__/generators.spec.ts +17 -24
- package/src/init/__tests__/init.spec.ts +157 -5
- package/src/init/generators/auth.ts +220 -0
- package/src/init/generators/config.ts +61 -4
- package/src/init/generators/docker.ts +115 -8
- package/src/init/generators/env.ts +7 -127
- package/src/init/generators/index.ts +1 -0
- package/src/init/generators/models.ts +3 -1
- package/src/init/generators/monorepo.ts +154 -10
- package/src/init/generators/package.ts +5 -3
- package/src/init/generators/web.ts +213 -0
- package/src/init/index.ts +290 -58
- package/src/init/templates/api.ts +38 -29
- package/src/init/templates/index.ts +132 -4
- package/src/init/templates/minimal.ts +33 -35
- package/src/init/templates/serverless.ts +16 -19
- package/src/init/templates/worker.ts +50 -25
- package/src/init/versions.ts +47 -0
- package/src/secrets/keystore.ts +144 -0
- package/src/secrets/storage.ts +109 -6
- package/src/test/index.ts +97 -0
- package/src/workspace/__tests__/client-generator.spec.ts +357 -0
- package/src/workspace/__tests__/index.spec.ts +543 -0
- package/src/workspace/__tests__/schema.spec.ts +519 -0
- package/src/workspace/__tests__/type-inference.spec.ts +251 -0
- package/src/workspace/client-generator.ts +307 -0
- package/src/workspace/index.ts +372 -0
- package/src/workspace/schema.ts +368 -0
- package/src/workspace/types.ts +336 -0
- package/tsconfig.tsbuildinfo +1 -1
- package/tsdown.config.ts +1 -0
- package/dist/config-AmInkU7k.cjs.map +0 -1
- package/dist/config-DYULeEv8.mjs.map +0 -1
- package/dist/dokploy-api-B7KxOQr3.cjs +0 -3
- package/dist/dokploy-api-DHvfmWbi.mjs +0 -3
- package/dist/storage-BaOP55oq.mjs +0 -147
- package/dist/storage-BaOP55oq.mjs.map +0 -1
- 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']).
|
|
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']).
|
|
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
|
|
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']).
|
|
60
|
-
expect(pkg.dependencies['@geekmidas/envkit']).
|
|
61
|
-
expect(pkg.dependencies['@geekmidas/logger']).
|
|
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
|
|
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
|
|
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
|
-
|
|
190
|
-
expect(paths).not.toContain('.gitignore');
|
|
190
|
+
expect(files).toHaveLength(0);
|
|
191
191
|
});
|
|
192
192
|
|
|
193
|
-
it('should include
|
|
193
|
+
it('should include .gkm in gitignore', () => {
|
|
194
194
|
const files = generateEnvFiles(baseOptions, minimalTemplate);
|
|
195
|
-
const
|
|
196
|
-
expect(
|
|
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
|
-
|
|
41
|
-
expect(
|
|
42
|
-
|
|
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']).
|
|
65
|
-
expect(pkg.dependencies['@geekmidas/telescope']).
|
|
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
|
-
//
|
|
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
|
-
|
|
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/
|
|
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
|
+
}
|