@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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@geekmidas/cli",
3
- "version": "0.43.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 tsdown would normally create
58
- // (since we're mocking execSync, the file won't be created automatically)
59
- await writeFile(join(distDir, 'server.js'), 'console.log("bundled");');
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
  }
@@ -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, vi } from 'vitest';
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.json at workspace root', async () => {
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 to current directory when not in workspace', async () => {
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 secretsJsonPath = join(secretsDir, 'dev-secrets.json');
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 include zod as dependency', () => {
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
- expect(pkg.dependencies.zod).toBeDefined();
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
- expect(pkg.dependencies.zod).toBeDefined();
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',