@geekmidas/cli 0.43.0 ā 0.45.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/README.md +221 -0
- package/dist/index.cjs +11 -4
- package/dist/index.cjs.map +1 -1
- package/dist/index.mjs +11 -4
- package/dist/index.mjs.map +1 -1
- package/package.json +2 -2
- package/src/build/__tests__/bundler.spec.ts +3 -3
- package/src/deploy/index.ts +8 -0
- package/src/dev/__tests__/entry-integration.spec.ts +7 -6
- package/src/dev/index.ts +5 -1
- package/src/docker/__tests__/templates.spec.ts +123 -0
- package/src/init/__tests__/generators.spec.ts +3 -2
- package/src/init/__tests__/init.spec.ts +2 -1
- package/src/init/templates/minimal.ts +2 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@geekmidas/cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.45.0",
|
|
4
4
|
"description": "CLI tools for building Lambda handlers, server applications, and generating OpenAPI specs",
|
|
5
5
|
"private": false,
|
|
6
6
|
"type": "module",
|
|
@@ -48,8 +48,8 @@
|
|
|
48
48
|
"lodash.kebabcase": "^4.1.1",
|
|
49
49
|
"openapi-typescript": "^7.4.2",
|
|
50
50
|
"prompts": "~2.4.2",
|
|
51
|
-
"@geekmidas/envkit": "~0.6.0",
|
|
52
51
|
"@geekmidas/constructs": "~0.7.0",
|
|
52
|
+
"@geekmidas/envkit": "~0.6.0",
|
|
53
53
|
"@geekmidas/errors": "~0.1.0",
|
|
54
54
|
"@geekmidas/logger": "~0.4.0",
|
|
55
55
|
"@geekmidas/schema": "~0.1.0"
|
|
@@ -54,9 +54,9 @@ async function createEntryPoint(dir: string): Promise<string> {
|
|
|
54
54
|
const entryPoint = join(outputDir, 'server.ts');
|
|
55
55
|
await writeFile(entryPoint, 'console.log("hello");');
|
|
56
56
|
|
|
57
|
-
// Create the output file that
|
|
58
|
-
// (since we're mocking
|
|
59
|
-
await writeFile(join(distDir, 'server.
|
|
57
|
+
// Create the output file that esbuild would normally create
|
|
58
|
+
// (since we're mocking spawnSync, the file won't be created automatically)
|
|
59
|
+
await writeFile(join(distDir, 'server.mjs'), 'console.log("bundled");');
|
|
60
60
|
|
|
61
61
|
return entryPoint;
|
|
62
62
|
}
|
package/src/deploy/index.ts
CHANGED
|
@@ -808,12 +808,20 @@ export async function workspaceDeployCommand(
|
|
|
808
808
|
|
|
809
809
|
if (dockerServices.postgres || dockerServices.redis) {
|
|
810
810
|
logger.log('\nš§ Provisioning infrastructure services...');
|
|
811
|
+
// Pass existing URLs from secrets to skip re-creating services
|
|
812
|
+
const existingUrls = stageSecrets
|
|
813
|
+
? {
|
|
814
|
+
DATABASE_URL: stageSecrets.urls?.DATABASE_URL,
|
|
815
|
+
REDIS_URL: stageSecrets.urls?.REDIS_URL,
|
|
816
|
+
}
|
|
817
|
+
: undefined;
|
|
811
818
|
await provisionServices(
|
|
812
819
|
api,
|
|
813
820
|
project.projectId,
|
|
814
821
|
environmentId,
|
|
815
822
|
workspace.name,
|
|
816
823
|
dockerServices,
|
|
824
|
+
existingUrls,
|
|
817
825
|
);
|
|
818
826
|
}
|
|
819
827
|
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { mkdir, readFile, rm, writeFile } from 'node:fs/promises';
|
|
2
2
|
import { tmpdir } from 'node:os';
|
|
3
3
|
import { join } from 'node:path';
|
|
4
|
-
import { afterEach, beforeEach, describe, expect, it
|
|
4
|
+
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
|
|
5
5
|
import { prepareEntryCredentials } from '../index';
|
|
6
6
|
|
|
7
7
|
describe('prepareEntryCredentials', () => {
|
|
@@ -121,14 +121,14 @@ export default defineWorkspace({
|
|
|
121
121
|
expect(result.appName).toBe('auth');
|
|
122
122
|
});
|
|
123
123
|
|
|
124
|
-
it('should write credentials to dev-secrets
|
|
124
|
+
it('should write credentials to app-specific dev-secrets file at workspace root', async () => {
|
|
125
125
|
const apiDir = join(workspaceDir, 'apps', 'api');
|
|
126
126
|
|
|
127
127
|
const result = await prepareEntryCredentials({ cwd: apiDir });
|
|
128
128
|
|
|
129
|
-
// Verify the file was written at workspace root
|
|
129
|
+
// Verify the file was written at workspace root with app-specific name
|
|
130
130
|
expect(result.secretsJsonPath).toBe(
|
|
131
|
-
join(workspaceDir, '.gkm', 'dev-secrets.json'),
|
|
131
|
+
join(workspaceDir, '.gkm', 'dev-secrets-api.json'),
|
|
132
132
|
);
|
|
133
133
|
|
|
134
134
|
// Verify file contents
|
|
@@ -170,11 +170,12 @@ export default defineWorkspace({
|
|
|
170
170
|
expect(result.credentials.PORT).toBe('5000');
|
|
171
171
|
});
|
|
172
172
|
|
|
173
|
-
it('should write credentials
|
|
173
|
+
it('should write credentials with app name from package.json when not in workspace', async () => {
|
|
174
174
|
const result = await prepareEntryCredentials({ cwd: workspaceDir });
|
|
175
175
|
|
|
176
|
+
// App name extracted from package.json is used for app-specific filename
|
|
176
177
|
expect(result.secretsJsonPath).toBe(
|
|
177
|
-
join(workspaceDir, '.gkm', 'dev-secrets.json'),
|
|
178
|
+
join(workspaceDir, '.gkm', 'dev-secrets-standalone-app.json'),
|
|
178
179
|
);
|
|
179
180
|
});
|
|
180
181
|
});
|
package/src/dev/index.ts
CHANGED
|
@@ -1402,9 +1402,13 @@ export async function prepareEntryCredentials(options: {
|
|
|
1402
1402
|
credentials.PORT = String(resolvedPort);
|
|
1403
1403
|
|
|
1404
1404
|
// Write secrets to temp JSON file (always write since we have PORT)
|
|
1405
|
+
// Use app-specific filename to avoid race conditions when running multiple apps via turbo
|
|
1405
1406
|
const secretsDir = join(secretsRoot, '.gkm');
|
|
1406
1407
|
await mkdir(secretsDir, { recursive: true });
|
|
1407
|
-
const
|
|
1408
|
+
const secretsFileName = appName
|
|
1409
|
+
? `dev-secrets-${appName}.json`
|
|
1410
|
+
: 'dev-secrets.json';
|
|
1411
|
+
const secretsJsonPath = join(secretsDir, secretsFileName);
|
|
1408
1412
|
await writeFile(secretsJsonPath, JSON.stringify(credentials, null, 2));
|
|
1409
1413
|
|
|
1410
1414
|
return {
|
|
@@ -8,6 +8,7 @@ import {
|
|
|
8
8
|
generateBackendDockerfile,
|
|
9
9
|
generateDockerEntrypoint,
|
|
10
10
|
generateDockerignore,
|
|
11
|
+
generateEntryDockerfile,
|
|
11
12
|
generateMultiStageDockerfile,
|
|
12
13
|
generateNextjsDockerfile,
|
|
13
14
|
generateSlimDockerfile,
|
|
@@ -566,4 +567,126 @@ describe('docker templates', () => {
|
|
|
566
567
|
expect(dockerfile).toContain('ENV PORT=3000');
|
|
567
568
|
});
|
|
568
569
|
});
|
|
570
|
+
|
|
571
|
+
describe('generateEntryDockerfile', () => {
|
|
572
|
+
const baseOptions = {
|
|
573
|
+
baseImage: 'node:22-alpine',
|
|
574
|
+
port: 3002,
|
|
575
|
+
appPath: 'apps/auth',
|
|
576
|
+
entry: './src/index.ts',
|
|
577
|
+
turboPackage: '@myapp/auth',
|
|
578
|
+
packageManager: 'pnpm' as const,
|
|
579
|
+
};
|
|
580
|
+
|
|
581
|
+
it('should generate entry-based Dockerfile with esbuild bundling', () => {
|
|
582
|
+
const dockerfile = generateEntryDockerfile(baseOptions);
|
|
583
|
+
|
|
584
|
+
expect(dockerfile).toContain('# Entry-based Dockerfile');
|
|
585
|
+
});
|
|
586
|
+
|
|
587
|
+
it('should include four stages: pruner, deps, builder, runner', () => {
|
|
588
|
+
const dockerfile = generateEntryDockerfile(baseOptions);
|
|
589
|
+
|
|
590
|
+
expect(dockerfile).toContain('AS pruner');
|
|
591
|
+
expect(dockerfile).toContain('AS deps');
|
|
592
|
+
expect(dockerfile).toContain('AS builder');
|
|
593
|
+
expect(dockerfile).toContain('AS runner');
|
|
594
|
+
});
|
|
595
|
+
|
|
596
|
+
it('should use turbo prune for the package', () => {
|
|
597
|
+
const dockerfile = generateEntryDockerfile(baseOptions);
|
|
598
|
+
|
|
599
|
+
expect(dockerfile).toContain('turbo prune @myapp/auth --docker');
|
|
600
|
+
});
|
|
601
|
+
|
|
602
|
+
it('should bundle with esbuild and packages=bundle flag', () => {
|
|
603
|
+
const dockerfile = generateEntryDockerfile(baseOptions);
|
|
604
|
+
|
|
605
|
+
expect(dockerfile).toContain('npx esbuild ./src/index.ts');
|
|
606
|
+
expect(dockerfile).toContain('--packages=bundle');
|
|
607
|
+
expect(dockerfile).toContain('--bundle');
|
|
608
|
+
expect(dockerfile).toContain('--platform=node');
|
|
609
|
+
expect(dockerfile).toContain('--target=node22');
|
|
610
|
+
expect(dockerfile).toContain('--format=esm');
|
|
611
|
+
});
|
|
612
|
+
|
|
613
|
+
it('should include CJS compatibility banner for packages like pino', () => {
|
|
614
|
+
const dockerfile = generateEntryDockerfile(baseOptions);
|
|
615
|
+
|
|
616
|
+
expect(dockerfile).toContain('import { createRequire } from "module"');
|
|
617
|
+
expect(dockerfile).toContain('const require = createRequire(import.meta.url)');
|
|
618
|
+
});
|
|
619
|
+
|
|
620
|
+
it('should output to dist/index.mjs', () => {
|
|
621
|
+
const dockerfile = generateEntryDockerfile(baseOptions);
|
|
622
|
+
|
|
623
|
+
expect(dockerfile).toContain('--outfile=dist/index.mjs');
|
|
624
|
+
expect(dockerfile).toContain('CMD ["node", "index.mjs"]');
|
|
625
|
+
});
|
|
626
|
+
|
|
627
|
+
it('should copy bundled output only', () => {
|
|
628
|
+
const dockerfile = generateEntryDockerfile(baseOptions);
|
|
629
|
+
|
|
630
|
+
expect(dockerfile).toContain('COPY --from=builder');
|
|
631
|
+
expect(dockerfile).toContain('dist/index.mjs');
|
|
632
|
+
// Comment mentions no node_modules needed - fully bundled
|
|
633
|
+
expect(dockerfile).toContain('no node_modules needed');
|
|
634
|
+
});
|
|
635
|
+
|
|
636
|
+
it('should handle encrypted credentials injection', () => {
|
|
637
|
+
const dockerfile = generateEntryDockerfile(baseOptions);
|
|
638
|
+
|
|
639
|
+
expect(dockerfile).toContain('ARG GKM_ENCRYPTED_CREDENTIALS');
|
|
640
|
+
expect(dockerfile).toContain('ARG GKM_CREDENTIALS_IV');
|
|
641
|
+
expect(dockerfile).toContain('--define:__GKM_ENCRYPTED_CREDENTIALS__');
|
|
642
|
+
expect(dockerfile).toContain('--define:__GKM_CREDENTIALS_IV__');
|
|
643
|
+
});
|
|
644
|
+
|
|
645
|
+
it('should create app user instead of hono user', () => {
|
|
646
|
+
const dockerfile = generateEntryDockerfile(baseOptions);
|
|
647
|
+
|
|
648
|
+
expect(dockerfile).toContain('adduser --system --uid 1001 app');
|
|
649
|
+
expect(dockerfile).toContain('USER app');
|
|
650
|
+
});
|
|
651
|
+
|
|
652
|
+
it('should include health check with default path', () => {
|
|
653
|
+
const dockerfile = generateEntryDockerfile(baseOptions);
|
|
654
|
+
|
|
655
|
+
expect(dockerfile).toContain('HEALTHCHECK');
|
|
656
|
+
expect(dockerfile).toContain('/health');
|
|
657
|
+
});
|
|
658
|
+
|
|
659
|
+
it('should use custom health check path when provided', () => {
|
|
660
|
+
const dockerfile = generateEntryDockerfile({
|
|
661
|
+
...baseOptions,
|
|
662
|
+
healthCheckPath: '/api/auth/health',
|
|
663
|
+
});
|
|
664
|
+
|
|
665
|
+
expect(dockerfile).toContain('/api/auth/health');
|
|
666
|
+
});
|
|
667
|
+
|
|
668
|
+
it('should expose configured port', () => {
|
|
669
|
+
const dockerfile = generateEntryDockerfile(baseOptions);
|
|
670
|
+
|
|
671
|
+
expect(dockerfile).toContain('EXPOSE 3002');
|
|
672
|
+
expect(dockerfile).toContain('ENV PORT=3002');
|
|
673
|
+
});
|
|
674
|
+
|
|
675
|
+
it('should use npm when specified', () => {
|
|
676
|
+
const dockerfile = generateEntryDockerfile({
|
|
677
|
+
...baseOptions,
|
|
678
|
+
packageManager: 'npm',
|
|
679
|
+
});
|
|
680
|
+
|
|
681
|
+
expect(dockerfile).toContain('npx turbo');
|
|
682
|
+
expect(dockerfile).toContain('package-lock.json');
|
|
683
|
+
});
|
|
684
|
+
|
|
685
|
+
it('should install tini for signal handling', () => {
|
|
686
|
+
const dockerfile = generateEntryDockerfile(baseOptions);
|
|
687
|
+
|
|
688
|
+
expect(dockerfile).toContain('apk add --no-cache tini');
|
|
689
|
+
expect(dockerfile).toContain('ENTRYPOINT ["/sbin/tini", "--"]');
|
|
690
|
+
});
|
|
691
|
+
});
|
|
569
692
|
});
|
|
@@ -323,7 +323,7 @@ describe('generateModelsPackage', () => {
|
|
|
323
323
|
expect(pkg.name).toBe('@test-project/models');
|
|
324
324
|
});
|
|
325
325
|
|
|
326
|
-
it('should
|
|
326
|
+
it('should have empty dependencies (zod is at root level)', () => {
|
|
327
327
|
const options: TemplateOptions = {
|
|
328
328
|
...baseOptions,
|
|
329
329
|
monorepo: true,
|
|
@@ -335,7 +335,8 @@ describe('generateModelsPackage', () => {
|
|
|
335
335
|
);
|
|
336
336
|
expect(pkgJson).toBeDefined();
|
|
337
337
|
const pkg = JSON.parse(pkgJson!.content);
|
|
338
|
-
|
|
338
|
+
// zod is now at root level in monorepo, not in models package
|
|
339
|
+
expect(pkg.dependencies).toEqual({});
|
|
339
340
|
});
|
|
340
341
|
|
|
341
342
|
it('should include example schemas', () => {
|
|
@@ -266,7 +266,8 @@ describe('initCommand', () => {
|
|
|
266
266
|
const pkg = JSON.parse(content);
|
|
267
267
|
|
|
268
268
|
expect(pkg.name).toBe('@my-monorepo/models');
|
|
269
|
-
|
|
269
|
+
// zod is at root level in monorepo, not in models package
|
|
270
|
+
expect(pkg.dependencies).toEqual({});
|
|
270
271
|
|
|
271
272
|
const userPath = join(
|
|
272
273
|
tempDir,
|
|
@@ -19,12 +19,14 @@ export const minimalTemplate: TemplateConfig = {
|
|
|
19
19
|
'@hono/node-server': '~1.14.1',
|
|
20
20
|
hono: '~4.8.2',
|
|
21
21
|
pino: '~9.6.0',
|
|
22
|
+
zod: '~4.1.0',
|
|
22
23
|
},
|
|
23
24
|
|
|
24
25
|
devDependencies: {
|
|
25
26
|
'@biomejs/biome': '~2.3.0',
|
|
26
27
|
'@geekmidas/cli': GEEKMIDAS_VERSIONS['@geekmidas/cli'],
|
|
27
28
|
'@types/node': '~22.0.0',
|
|
29
|
+
esbuild: '~0.27.0',
|
|
28
30
|
tsx: '~4.20.0',
|
|
29
31
|
turbo: '~2.3.0',
|
|
30
32
|
typescript: '~5.8.2',
|