@geekmidas/cli 1.10.12 → 1.10.13

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": "1.10.12",
3
+ "version": "1.10.13",
4
4
  "description": "CLI tools for building Lambda handlers, server applications, and generating OpenAPI specs",
5
5
  "private": false,
6
6
  "type": "module",
@@ -57,8 +57,8 @@
57
57
  "tsx": "~4.20.3",
58
58
  "yaml": "~2.8.2",
59
59
  "@geekmidas/constructs": "~3.0.2",
60
- "@geekmidas/envkit": "~1.0.3",
61
60
  "@geekmidas/logger": "~1.0.0",
61
+ "@geekmidas/envkit": "~1.0.3",
62
62
  "@geekmidas/errors": "~1.0.0",
63
63
  "@geekmidas/schema": "~1.0.0"
64
64
  },
@@ -25,7 +25,7 @@ const baseOptions: TemplateOptions = {
25
25
  apiPath: '',
26
26
  packageManager: 'pnpm',
27
27
  deployTarget: 'dokploy',
28
- services: { db: true, cache: true, mail: false },
28
+ services: { db: true, cache: true, mail: false, storage: false },
29
29
  };
30
30
 
31
31
  describe('generatePackageJson', () => {
@@ -245,7 +245,7 @@ describe('generateDockerFiles', () => {
245
245
  it('should include mailpit with dynamic ports when mail is enabled', () => {
246
246
  const options = {
247
247
  ...baseOptions,
248
- services: { db: true, cache: true, mail: true },
248
+ services: { ...baseOptions.services, mail: true },
249
249
  };
250
250
  const files = generateDockerFiles(options, minimalTemplate);
251
251
  expect(files[0].content).toContain('mailpit');
@@ -255,6 +255,48 @@ describe('generateDockerFiles', () => {
255
255
  expect(files[0].content).toContain("'${MAILPIT_UI_HOST_PORT:-8025}:8025'");
256
256
  expect(files[0].content).toContain('MP_SMTP_AUTH:');
257
257
  });
258
+
259
+ it('should include minio with dynamic ports when storage is enabled', () => {
260
+ const options = {
261
+ ...baseOptions,
262
+ services: { ...baseOptions.services, storage: true },
263
+ };
264
+ const files = generateDockerFiles(options, minimalTemplate);
265
+ expect(files[0].content).toContain('minio');
266
+ expect(files[0].content).toContain('minio/minio:latest');
267
+ expect(files[0].content).toContain("'${MINIO_API_HOST_PORT:-9000}:9000'");
268
+ expect(files[0].content).toContain(
269
+ "'${MINIO_CONSOLE_HOST_PORT:-9001}:9001'",
270
+ );
271
+ expect(files[0].content).toContain('MINIO_ROOT_USER:');
272
+ expect(files[0].content).toContain('MINIO_ROOT_PASSWORD:');
273
+ expect(files[0].content).toContain('minio_data:');
274
+ });
275
+
276
+ it('should include all services when all are enabled', () => {
277
+ const options = {
278
+ ...baseOptions,
279
+ services: { db: true, cache: true, mail: true, storage: true },
280
+ };
281
+ const files = generateDockerFiles(options, minimalTemplate);
282
+ expect(files[0].content).toContain('postgres');
283
+ expect(files[0].content).toContain('redis');
284
+ expect(files[0].content).toContain('mailpit');
285
+ expect(files[0].content).toContain('minio');
286
+ });
287
+
288
+ it('should not include disabled services', () => {
289
+ const options = {
290
+ ...baseOptions,
291
+ database: false,
292
+ services: { db: false, cache: false, mail: false, storage: false },
293
+ };
294
+ const files = generateDockerFiles(options, minimalTemplate);
295
+ expect(files[0].content).not.toContain('postgres');
296
+ expect(files[0].content).toContain('redis'); // redis is always included
297
+ expect(files[0].content).not.toContain('mailpit');
298
+ expect(files[0].content).not.toContain('minio');
299
+ });
258
300
  });
259
301
 
260
302
  describe('generateMonorepoFiles', () => {
@@ -159,6 +159,30 @@ export function generateDockerFiles(
159
159
  MP_SMTP_AUTH: \${SMTP_USER:-${options.name}}:\${SMTP_PASS:-${options.name}}`);
160
160
  }
161
161
 
162
+ // MinIO for S3-compatible object storage
163
+ if (options.services?.storage) {
164
+ services.push(` minio:
165
+ image: minio/minio:latest
166
+ container_name: ${options.name}-minio
167
+ restart: unless-stopped
168
+ entrypoint: sh
169
+ command: -c 'mkdir -p /data/\${STORAGE_BUCKET:-${options.name}} && /usr/bin/docker-entrypoint.sh server --console-address ":9001" /data'
170
+ environment:
171
+ MINIO_ROOT_USER: \${STORAGE_ACCESS_KEY_ID:-${options.name}}
172
+ MINIO_ROOT_PASSWORD: \${STORAGE_SECRET_ACCESS_KEY:-${options.name}}
173
+ ports:
174
+ - '\${MINIO_API_HOST_PORT:-9000}:9000'
175
+ - '\${MINIO_CONSOLE_HOST_PORT:-9001}:9001'
176
+ volumes:
177
+ - minio_data:/data
178
+ healthcheck:
179
+ test: ['CMD', 'mc', 'ready', 'local']
180
+ interval: 10s
181
+ timeout: 5s
182
+ retries: 5`);
183
+ volumes.push(' minio_data:');
184
+ }
185
+
162
186
  // Build docker-compose.yml
163
187
  let dockerCompose = `# Use "gkm dev" or "gkm test" to start services.
164
188
  # Running "docker compose up" directly will not inject secrets or resolve ports.
package/src/init/index.ts CHANGED
@@ -181,12 +181,13 @@ export async function initCommand(
181
181
 
182
182
  // Parse services selection
183
183
  const servicesArray: string[] = options.yes
184
- ? ['db', 'cache', 'mail']
184
+ ? ['db', 'cache', 'mail', 'storage']
185
185
  : answers.services || [];
186
186
  const services: ServicesSelection = {
187
187
  db: servicesArray.includes('db'),
188
188
  cache: servicesArray.includes('cache'),
189
189
  mail: servicesArray.includes('mail'),
190
+ storage: servicesArray.includes('storage'),
190
191
  };
191
192
 
192
193
  const pkgManager: PackageManager = options.pm
@@ -38,6 +38,7 @@ export interface ServicesSelection {
38
38
  db: boolean;
39
39
  cache: boolean;
40
40
  mail: boolean;
41
+ storage: boolean;
41
42
  }
42
43
 
43
44
  /**
@@ -247,6 +248,11 @@ export const servicesChoices = [
247
248
  value: 'mail',
248
249
  description: 'Email testing service (dev only)',
249
250
  },
251
+ {
252
+ title: 'MinIO',
253
+ value: 'storage',
254
+ description: 'S3-compatible object storage (dev only)',
255
+ },
250
256
  ];
251
257
 
252
258
  /**
@@ -158,6 +158,41 @@ describe('reconcileSecrets', () => {
158
158
  expect(result!.services.postgres).toEqual(secrets.services.postgres);
159
159
  });
160
160
 
161
+ it('should add missing mailpit credentials when mail service is enabled', () => {
162
+ const workspace = createWorkspace({
163
+ services: { db: true, mail: true },
164
+ });
165
+ // Secrets only have postgres, not mailpit
166
+ const secrets = createSecrets({
167
+ NODE_ENV: 'development',
168
+ PORT: '3000',
169
+ API_DATABASE_URL: 'postgresql://api:pass@localhost:5432/test_dev',
170
+ API_DB_PASSWORD: 'pass',
171
+ AUTH_DATABASE_URL: 'postgresql://auth:pass@localhost:5432/test_dev',
172
+ AUTH_DB_PASSWORD: 'pass',
173
+ WEB_URL: 'http://localhost:3002',
174
+ BETTER_AUTH_SECRET: 'existing',
175
+ BETTER_AUTH_URL: 'http://localhost:3001',
176
+ BETTER_AUTH_TRUSTED_ORIGINS:
177
+ 'http://localhost:3000,http://localhost:3001,http://localhost:3002',
178
+ AUTH_PORT: '3001',
179
+ AUTH_URL: 'http://localhost:3001',
180
+ });
181
+
182
+ const result = reconcileSecrets(secrets, workspace);
183
+
184
+ expect(result).not.toBeNull();
185
+ expect(result!.services.mailpit).toBeDefined();
186
+ expect(result!.services.mailpit!.host).toBe('localhost');
187
+ expect(result!.services.mailpit!.port).toBe(1025);
188
+ expect(result!.services.mailpit!.username).toBeDefined();
189
+ expect(result!.services.mailpit!.password).toHaveLength(32);
190
+ expect(result!.urls.SMTP_HOST).toBe('localhost');
191
+ expect(result!.urls.SMTP_PORT).toBe('1025');
192
+ // Existing postgres should be preserved
193
+ expect(result!.services.postgres).toEqual(secrets.services.postgres);
194
+ });
195
+
161
196
  it('should not regenerate credentials for existing services', () => {
162
197
  const workspace = createWorkspace({
163
198
  services: { db: true, storage: true },
@@ -168,6 +168,7 @@ export function reconcileSecrets(
168
168
  { key: 'db', name: 'postgres' },
169
169
  { key: 'cache', name: 'redis' },
170
170
  { key: 'storage', name: 'minio' },
171
+ { key: 'mail', name: 'mailpit' },
171
172
  ];
172
173
 
173
174
  for (const { key, name } of serviceMap) {
@@ -235,6 +236,7 @@ async function generateFreshSecrets(
235
236
  if (workspace.services.db) serviceNames.push('postgres');
236
237
  if (workspace.services.cache) serviceNames.push('redis');
237
238
  if (workspace.services.storage) serviceNames.push('minio');
239
+ if (workspace.services.mail) serviceNames.push('mailpit');
238
240
 
239
241
  // Create base secrets with service credentials
240
242
  const secrets = createStageSecrets(stage, serviceNames, {
package/src/test/index.ts CHANGED
@@ -6,6 +6,7 @@ import { sniffAppEnvironment } from '../deploy/sniffer';
6
6
  import {
7
7
  createCredentialsPreload,
8
8
  loadEnvFiles,
9
+ loadPortState,
9
10
  parseComposePortMappings,
10
11
  resolveServicePorts,
11
12
  rewriteUrlsWithPorts,
@@ -133,6 +134,19 @@ export async function testCommand(options: TestOptions = {}): Promise<void> {
133
134
  console.log(
134
135
  ` 🔌 Applied ${Object.keys(resolvedPorts.ports).length} port mapping(s)`,
135
136
  );
137
+ } else {
138
+ // Fallback to saved port state from a previous gkm dev run
139
+ const ports = await loadPortState(cwd);
140
+ if (Object.keys(ports).length > 0) {
141
+ secretsEnv = rewriteUrlsWithPorts(secretsEnv, {
142
+ dockerEnv: {},
143
+ ports,
144
+ mappings,
145
+ });
146
+ console.log(
147
+ ` 🔌 Applied ${Object.keys(ports).length} port mapping(s)`,
148
+ );
149
+ }
136
150
  }
137
151
  }
138
152
  }