@geekmidas/cli 1.10.4 → 1.10.5

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.4",
3
+ "version": "1.10.5",
4
4
  "description": "CLI tools for building Lambda handlers, server applications, and generating OpenAPI specs",
5
5
  "private": false,
6
6
  "type": "module",
@@ -57,10 +57,10 @@
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
- "@geekmidas/schema": "~1.0.0",
60
+ "@geekmidas/errors": "~1.0.0",
62
61
  "@geekmidas/logger": "~1.0.0",
63
- "@geekmidas/errors": "~1.0.0"
62
+ "@geekmidas/schema": "~1.0.0",
63
+ "@geekmidas/envkit": "~1.0.3"
64
64
  },
65
65
  "devDependencies": {
66
66
  "@types/lodash.kebabcase": "^4.1.9",
@@ -1257,10 +1257,11 @@ describe('rewriteUrlsWithPorts', () => {
1257
1257
  containerPort: 5672,
1258
1258
  };
1259
1259
 
1260
- it('should rewrite DATABASE_URL with resolved postgres port', () => {
1260
+ it('should rewrite DATABASE_URL with resolved postgres port and hostname', () => {
1261
1261
  const secrets = {
1262
1262
  DATABASE_URL: 'postgresql://app:pass@postgres:5432/mydb',
1263
1263
  POSTGRES_PORT: '5432',
1264
+ POSTGRES_HOST: 'postgres',
1264
1265
  SOME_OTHER: 'value',
1265
1266
  };
1266
1267
  const result = rewriteUrlsWithPorts(secrets, {
@@ -1269,9 +1270,10 @@ describe('rewriteUrlsWithPorts', () => {
1269
1270
  mappings: [pgMapping],
1270
1271
  });
1271
1272
  expect(result.DATABASE_URL).toBe(
1272
- 'postgresql://app:pass@postgres:5433/mydb',
1273
+ 'postgresql://app:pass@localhost:5433/mydb',
1273
1274
  );
1274
1275
  expect(result.POSTGRES_PORT).toBe('5433');
1276
+ expect(result.POSTGRES_HOST).toBe('localhost');
1275
1277
  expect(result.SOME_OTHER).toBe('value');
1276
1278
  });
1277
1279
 
@@ -1293,18 +1295,20 @@ describe('rewriteUrlsWithPorts', () => {
1293
1295
  );
1294
1296
  });
1295
1297
 
1296
- it('should rewrite REDIS_URL and REDIS_PORT', () => {
1298
+ it('should rewrite REDIS_URL, REDIS_PORT, and REDIS_HOST', () => {
1297
1299
  const secrets = {
1298
1300
  REDIS_URL: 'redis://:pass@redis:6379',
1299
1301
  REDIS_PORT: '6379',
1302
+ REDIS_HOST: 'redis',
1300
1303
  };
1301
1304
  const result = rewriteUrlsWithPorts(secrets, {
1302
1305
  dockerEnv: { REDIS_HOST_PORT: '6380' },
1303
1306
  ports: { REDIS_HOST_PORT: 6380 },
1304
1307
  mappings: [redisMapping],
1305
1308
  });
1306
- expect(result.REDIS_URL).toBe('redis://:pass@redis:6380');
1309
+ expect(result.REDIS_URL).toBe('redis://:pass@localhost:6380');
1307
1310
  expect(result.REDIS_PORT).toBe('6380');
1311
+ expect(result.REDIS_HOST).toBe('localhost');
1308
1312
  });
1309
1313
 
1310
1314
  it('should rewrite RABBITMQ_URL and RABBITMQ_PORT', () => {
@@ -1317,7 +1321,7 @@ describe('rewriteUrlsWithPorts', () => {
1317
1321
  ports: { RABBITMQ_HOST_PORT: 5673 },
1318
1322
  mappings: [rmqMapping],
1319
1323
  });
1320
- expect(result.RABBITMQ_URL).toBe('amqp://app:pass@rabbitmq:5673/%2F');
1324
+ expect(result.RABBITMQ_URL).toBe('amqp://app:pass@localhost:5673/%2F');
1321
1325
  expect(result.RABBITMQ_PORT).toBe('5673');
1322
1326
  });
1323
1327
 
@@ -1336,26 +1340,48 @@ describe('rewriteUrlsWithPorts', () => {
1336
1340
  ports: { POSTGRES_HOST_PORT: 5433, REDIS_HOST_PORT: 6380 },
1337
1341
  mappings: [pgMapping, redisMapping],
1338
1342
  });
1339
- expect(result.DATABASE_URL).toContain(':5433/');
1343
+ expect(result.DATABASE_URL).toBe(
1344
+ 'postgresql://app:pass@localhost:5433/mydb',
1345
+ );
1340
1346
  expect(result.POSTGRES_PORT).toBe('5433');
1341
- expect(result.REDIS_URL).toContain(':6380');
1347
+ expect(result.REDIS_URL).toBe('redis://:pass@localhost:6380');
1342
1348
  expect(result.REDIS_PORT).toBe('6380');
1343
1349
  });
1344
1350
 
1345
- it('should not modify secrets when ports are defaults', () => {
1351
+ it('should rewrite hostnames even when ports are defaults', () => {
1346
1352
  const secrets = {
1347
1353
  DATABASE_URL: 'postgresql://app:pass@postgres:5432/mydb',
1348
1354
  POSTGRES_PORT: '5432',
1355
+ POSTGRES_HOST: 'postgres',
1349
1356
  };
1350
1357
  const result = rewriteUrlsWithPorts(secrets, {
1351
1358
  dockerEnv: { POSTGRES_HOST_PORT: '5432' },
1352
1359
  ports: { POSTGRES_HOST_PORT: 5432 },
1353
1360
  mappings: [pgMapping],
1354
1361
  });
1355
- expect(result.DATABASE_URL).toBe(secrets.DATABASE_URL);
1362
+ expect(result.DATABASE_URL).toBe(
1363
+ 'postgresql://app:pass@localhost:5432/mydb',
1364
+ );
1365
+ expect(result.POSTGRES_HOST).toBe('localhost');
1356
1366
  expect(result.POSTGRES_PORT).toBe('5432');
1357
1367
  });
1358
1368
 
1369
+ it('should not rewrite _HOST vars that are already localhost', () => {
1370
+ const secrets = {
1371
+ DATABASE_URL: 'postgresql://app:pass@localhost:5432/mydb',
1372
+ POSTGRES_HOST: 'localhost',
1373
+ };
1374
+ const result = rewriteUrlsWithPorts(secrets, {
1375
+ dockerEnv: { POSTGRES_HOST_PORT: '5432' },
1376
+ ports: { POSTGRES_HOST_PORT: 5432 },
1377
+ mappings: [pgMapping],
1378
+ });
1379
+ expect(result.DATABASE_URL).toBe(
1380
+ 'postgresql://app:pass@localhost:5432/mydb',
1381
+ );
1382
+ expect(result.POSTGRES_HOST).toBe('localhost');
1383
+ });
1384
+
1359
1385
  it('should return empty for no mappings', () => {
1360
1386
  const result = rewriteUrlsWithPorts(
1361
1387
  {},
package/src/dev/index.ts CHANGED
@@ -354,7 +354,10 @@ export function rewriteUrlsWithPorts(
354
354
 
355
355
  // Build a map of defaultPort → resolvedPort for all changed ports
356
356
  const portReplacements: { defaultPort: number; resolvedPort: number }[] = [];
357
+ // Collect Docker service names for hostname rewriting
358
+ const serviceNames = new Set<string>();
357
359
  for (const mapping of mappings) {
360
+ serviceNames.add(mapping.service);
358
361
  const resolved = ports[mapping.envVar];
359
362
  if (resolved !== undefined) {
360
363
  portReplacements.push({
@@ -364,6 +367,14 @@ export function rewriteUrlsWithPorts(
364
367
  }
365
368
  }
366
369
 
370
+ // Rewrite _HOST env vars that use Docker service names
371
+ for (const [key, value] of Object.entries(result)) {
372
+ if (!key.endsWith('_HOST')) continue;
373
+ if (serviceNames.has(value)) {
374
+ result[key] = 'localhost';
375
+ }
376
+ }
377
+
367
378
  // Rewrite _PORT env vars whose values match a default port
368
379
  for (const [key, value] of Object.entries(result)) {
369
380
  if (!key.endsWith('_PORT')) continue;
@@ -374,11 +385,17 @@ export function rewriteUrlsWithPorts(
374
385
  }
375
386
  }
376
387
 
377
- // Rewrite URLs containing default ports
388
+ // Rewrite URLs: replace Docker service hostnames with localhost and fix ports
378
389
  for (const [key, value] of Object.entries(result)) {
379
390
  if (!key.endsWith('_URL') && key !== 'DATABASE_URL') continue;
380
391
 
381
392
  let rewritten = value;
393
+ for (const name of serviceNames) {
394
+ rewritten = rewritten.replace(
395
+ new RegExp(`@${name}:`, 'g'),
396
+ '@localhost:',
397
+ );
398
+ }
382
399
  for (const { defaultPort, resolvedPort } of portReplacements) {
383
400
  rewritten = replacePortInUrl(rewritten, defaultPort, resolvedPort);
384
401
  }
@@ -122,14 +122,14 @@ describe('generateDockerCompose', () => {
122
122
  });
123
123
 
124
124
  describe('postgres service', () => {
125
- it('should add DATABASE_URL environment variable', () => {
125
+ it('should add DATABASE_URL environment variable with credential interpolation', () => {
126
126
  const yaml = generateDockerCompose({
127
127
  ...baseOptions,
128
128
  services: { postgres: true },
129
129
  });
130
130
 
131
131
  expect(yaml).toContain(
132
- '- DATABASE_URL=${DATABASE_URL:-postgresql://postgres:postgres@postgres:5432/app}',
132
+ '- DATABASE_URL=${DATABASE_URL:-postgresql://${POSTGRES_USER:-postgres}:${POSTGRES_PASSWORD:-postgres}@postgres:5432/${POSTGRES_DB:-app}}',
133
133
  );
134
134
  });
135
135
 
@@ -185,13 +185,15 @@ describe('generateDockerCompose', () => {
185
185
  expect(yaml).toContain('postgres_data:');
186
186
  });
187
187
 
188
- it('should include postgres healthcheck', () => {
188
+ it('should include postgres healthcheck using POSTGRES_USER', () => {
189
189
  const yaml = generateDockerCompose({
190
190
  ...baseOptions,
191
191
  services: { postgres: true },
192
192
  });
193
193
 
194
- expect(yaml).toContain('test: ["CMD-SHELL", "pg_isready -U postgres"]');
194
+ expect(yaml).toContain(
195
+ 'test: ["CMD-SHELL", "pg_isready -U $$POSTGRES_USER"]',
196
+ );
195
197
  });
196
198
 
197
199
  it('should add depends_on for postgres', () => {
@@ -801,7 +803,7 @@ describe('generateWorkspaceCompose', () => {
801
803
  const yaml = generateWorkspaceCompose(workspace);
802
804
 
803
805
  expect(yaml).toContain(
804
- 'DATABASE_URL=${DATABASE_URL:-postgresql://postgres:postgres@postgres:5432/app}',
806
+ 'DATABASE_URL=${DATABASE_URL:-postgresql://${POSTGRES_USER:-postgres}:${POSTGRES_PASSWORD:-postgres}@postgres:5432/${POSTGRES_DB:-app}}',
805
807
  );
806
808
  });
807
809
 
@@ -105,7 +105,7 @@ services:
105
105
 
106
106
  // Add environment variables based on services
107
107
  if (serviceMap.has('postgres')) {
108
- yaml += ` - DATABASE_URL=\${DATABASE_URL:-postgresql://postgres:postgres@postgres:5432/app}
108
+ yaml += ` - DATABASE_URL=\${DATABASE_URL:-postgresql://\${POSTGRES_USER:-postgres}:\${POSTGRES_PASSWORD:-postgres}@postgres:5432/\${POSTGRES_DB:-app}}
109
109
  `;
110
110
  }
111
111
 
@@ -156,7 +156,7 @@ services:
156
156
  volumes:
157
157
  - postgres_data:/var/lib/postgresql/data
158
158
  healthcheck:
159
- test: ["CMD-SHELL", "pg_isready -U postgres"]
159
+ test: ["CMD-SHELL", "pg_isready -U $$POSTGRES_USER"]
160
160
  interval: 5s
161
161
  timeout: 5s
162
162
  retries: 5
@@ -335,7 +335,7 @@ services:
335
335
  volumes:
336
336
  - postgres_data:/var/lib/postgresql/data
337
337
  healthcheck:
338
- test: ["CMD-SHELL", "pg_isready -U postgres"]
338
+ test: ["CMD-SHELL", "pg_isready -U $$POSTGRES_USER"]
339
339
  interval: 5s
340
340
  timeout: 5s
341
341
  retries: 5
@@ -480,7 +480,7 @@ function generateAppService(
480
480
  // Add infrastructure service URLs for backend apps
481
481
  if (app.type === 'backend') {
482
482
  if (hasPostgres) {
483
- yaml += ` - DATABASE_URL=\${DATABASE_URL:-postgresql://postgres:postgres@postgres:5432/app}
483
+ yaml += ` - DATABASE_URL=\${DATABASE_URL:-postgresql://\${POSTGRES_USER:-postgres}:\${POSTGRES_PASSWORD:-postgres}@postgres:5432/\${POSTGRES_DB:-app}}
484
484
  `;
485
485
  }
486
486
  if (hasRedis) {
@@ -46,15 +46,15 @@ export function generateDockerFiles(
46
46
  container_name: ${options.name}-postgres
47
47
  restart: unless-stopped${envFile}
48
48
  environment:
49
- POSTGRES_USER: postgres
50
- POSTGRES_PASSWORD: postgres
51
- POSTGRES_DB: ${options.name.replace(/-/g, '_')}_dev
49
+ POSTGRES_USER: \${POSTGRES_USER:-postgres}
50
+ POSTGRES_PASSWORD: \${POSTGRES_PASSWORD:-postgres}
51
+ POSTGRES_DB: \${POSTGRES_DB:-${options.name.replace(/-/g, '_')}_dev}
52
52
  ports:
53
53
  - '\${POSTGRES_HOST_PORT:-5432}:5432'
54
54
  volumes:
55
55
  - postgres_data:/var/lib/postgresql/data${initVolume}
56
56
  healthcheck:
57
- test: ['CMD-SHELL', 'pg_isready -U postgres']
57
+ test: ['CMD-SHELL', 'pg_isready -U $$POSTGRES_USER']
58
58
  interval: 5s
59
59
  timeout: 5s
60
60
  retries: 5`);
@@ -24,7 +24,7 @@ import {
24
24
  rewriteUrlsWithPorts,
25
25
  savePortState,
26
26
  } from '../../dev/index';
27
- import { ensureTestDatabase, rewriteDatabaseUrlForTests } from '../index';
27
+ import { rewriteDatabaseUrlForTests } from '../index';
28
28
 
29
29
  describe('rewriteDatabaseUrlForTests', () => {
30
30
  beforeAll(() => {
@@ -128,39 +128,6 @@ describe('rewriteDatabaseUrlForTests', () => {
128
128
  });
129
129
  });
130
130
 
131
- describe('ensureTestDatabase', () => {
132
- beforeAll(() => {
133
- vi.spyOn(console, 'log').mockImplementation(() => {});
134
- });
135
-
136
- afterAll(() => {
137
- vi.restoreAllMocks();
138
- });
139
-
140
- it('should do nothing when DATABASE_URL is missing', async () => {
141
- // Should resolve without error
142
- await ensureTestDatabase({});
143
- await ensureTestDatabase({ REDIS_URL: 'redis://localhost:6379' });
144
- });
145
-
146
- it('should do nothing when database name is empty', async () => {
147
- await ensureTestDatabase({
148
- DATABASE_URL: 'postgresql://app:secret@localhost:5432/',
149
- });
150
- });
151
-
152
- it('should not throw when postgres is unreachable', async () => {
153
- // Use a port that's almost certainly not running postgres
154
- await ensureTestDatabase({
155
- DATABASE_URL: 'postgresql://app:secret@localhost:59999/test_db',
156
- });
157
- // Should log a warning but not throw
158
- expect(console.log).toHaveBeenCalledWith(
159
- expect.stringContaining('Could not ensure test database'),
160
- );
161
- });
162
- });
163
-
164
131
  describe('port rewriting + test database pipeline', () => {
165
132
  let testDir: string;
166
133
 
@@ -359,4 +326,65 @@ services:
359
326
  // RabbitMQ port rewritten
360
327
  expect(secrets.RABBITMQ_URL).toBe('amqp://app:secret@localhost:5673');
361
328
  });
329
+
330
+ it('should preserve root postgres credentials through the pipeline', async () => {
331
+ writeFileSync(
332
+ join(testDir, 'docker-compose.yml'),
333
+ `
334
+ services:
335
+ postgres:
336
+ image: postgres:17
337
+ ports:
338
+ - '\${POSTGRES_HOST_PORT:-5432}:5432'
339
+ `,
340
+ );
341
+
342
+ await savePortState(testDir, {
343
+ POSTGRES_HOST_PORT: 5434,
344
+ });
345
+
346
+ // Simulate toEmbeddableSecrets output for a workspace with app-specific users
347
+ let secrets: Record<string, string> = {
348
+ DATABASE_URL: 'postgresql://app:rootpass@postgres:5432/app',
349
+ API_DATABASE_URL: 'postgresql://api:apipass@localhost:5432/myproject_dev',
350
+ AUTH_DATABASE_URL:
351
+ 'postgresql://auth:authpass@localhost:5432/myproject_dev',
352
+ POSTGRES_USER: 'app',
353
+ POSTGRES_PASSWORD: 'rootpass',
354
+ POSTGRES_DB: 'app',
355
+ POSTGRES_HOST: 'postgres',
356
+ POSTGRES_PORT: '5432',
357
+ };
358
+
359
+ // Apply port + host rewriting
360
+ const mappings = parseComposePortMappings(
361
+ join(testDir, 'docker-compose.yml'),
362
+ );
363
+ const ports = await loadPortState(testDir);
364
+ secrets = rewriteUrlsWithPorts(secrets, {
365
+ dockerEnv: {},
366
+ ports,
367
+ mappings,
368
+ });
369
+
370
+ // Apply test database suffix
371
+ secrets = rewriteDatabaseUrlForTests(secrets);
372
+
373
+ // Root credentials should be present and rewritten to localhost
374
+ expect(secrets.POSTGRES_USER).toBe('app');
375
+ expect(secrets.POSTGRES_PASSWORD).toBe('rootpass');
376
+ expect(secrets.POSTGRES_HOST).toBe('localhost');
377
+ expect(secrets.POSTGRES_PORT).toBe('5434');
378
+
379
+ // All DATABASE_URLs should have localhost, resolved port, and _test suffix
380
+ expect(secrets.DATABASE_URL).toBe(
381
+ 'postgresql://app:rootpass@localhost:5434/app_test',
382
+ );
383
+ expect(secrets.API_DATABASE_URL).toBe(
384
+ 'postgresql://api:apipass@localhost:5434/myproject_dev_test',
385
+ );
386
+ expect(secrets.AUTH_DATABASE_URL).toBe(
387
+ 'postgresql://auth:authpass@localhost:5434/myproject_dev_test',
388
+ );
389
+ });
362
390
  });
package/src/test/index.ts CHANGED
@@ -81,7 +81,6 @@ export async function testCommand(options: TestOptions = {}): Promise<void> {
81
81
 
82
82
  // 4. Use a separate test database (append _test suffix)
83
83
  secretsEnv = rewriteDatabaseUrlForTests(secretsEnv);
84
- await ensureTestDatabase(secretsEnv);
85
84
 
86
85
  // 5. Load workspace config + dependency URLs + sniff env vars
87
86
  let dependencyEnv: Record<string, string> = {};
@@ -225,43 +224,3 @@ export function rewriteDatabaseUrlForTests(
225
224
 
226
225
  return result;
227
226
  }
228
-
229
- /**
230
- * Ensure the test database exists by connecting to the default database
231
- * and running CREATE DATABASE IF NOT EXISTS.
232
- * @internal Exported for testing
233
- */
234
- export async function ensureTestDatabase(
235
- env: Record<string, string>,
236
- ): Promise<void> {
237
- const databaseUrl = env.DATABASE_URL;
238
- if (!databaseUrl) return;
239
-
240
- try {
241
- const url = new URL(databaseUrl);
242
- const testDbName = url.pathname.slice(1);
243
- if (!testDbName) return;
244
-
245
- // Connect to the default 'postgres' database to create the test database
246
- url.pathname = '/postgres';
247
- const { default: pg } = await import('pg');
248
- const client = new pg.Client({ connectionString: url.toString() });
249
- await client.connect();
250
-
251
- try {
252
- await client.query(`CREATE DATABASE "${testDbName}"`);
253
- console.log(` 📦 Created test database "${testDbName}"`);
254
- } catch (err: unknown) {
255
- // 42P04 = database already exists — that's fine
256
- if ((err as { code?: string }).code !== '42P04') throw err;
257
- } finally {
258
- await client.end();
259
- }
260
- } catch (err) {
261
- // Don't fail test startup if we can't create the database
262
- // (e.g., postgres not running yet, will fail later with a clear error)
263
- console.log(
264
- ` ⚠️ Could not ensure test database: ${(err as Error).message}`,
265
- );
266
- }
267
- }