@geekmidas/cli 1.5.0 → 1.6.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.
Files changed (119) hide show
  1. package/CHANGELOG.md +17 -0
  2. package/dist/{HostingerProvider-B9N-TKbp.mjs → HostingerProvider-402UdK89.mjs} +34 -1
  3. package/dist/HostingerProvider-402UdK89.mjs.map +1 -0
  4. package/dist/{HostingerProvider-DUV9-Tzg.cjs → HostingerProvider-BiXdHjiq.cjs} +34 -1
  5. package/dist/HostingerProvider-BiXdHjiq.cjs.map +1 -0
  6. package/dist/{Route53Provider-C8mS0zY6.mjs → Route53Provider-DbBo7Uz5.mjs} +53 -1
  7. package/dist/Route53Provider-DbBo7Uz5.mjs.map +1 -0
  8. package/dist/{Route53Provider-Bs7Arms9.cjs → Route53Provider-kfJ77LmL.cjs} +53 -1
  9. package/dist/Route53Provider-kfJ77LmL.cjs.map +1 -0
  10. package/dist/backup-provisioner-B5e-F6zX.cjs +164 -0
  11. package/dist/backup-provisioner-B5e-F6zX.cjs.map +1 -0
  12. package/dist/backup-provisioner-BIArpmTr.mjs +163 -0
  13. package/dist/backup-provisioner-BIArpmTr.mjs.map +1 -0
  14. package/dist/{config-ZQM1vBoz.cjs → config-6JHOwLCx.cjs} +30 -2
  15. package/dist/{config-ZQM1vBoz.cjs.map → config-6JHOwLCx.cjs.map} +1 -1
  16. package/dist/{config-DfCJ29PQ.mjs → config-DxASSNjr.mjs} +25 -3
  17. package/dist/{config-DfCJ29PQ.mjs.map → config-DxASSNjr.mjs.map} +1 -1
  18. package/dist/config.cjs +3 -2
  19. package/dist/config.d.cts +14 -2
  20. package/dist/config.d.cts.map +1 -1
  21. package/dist/config.d.mts +15 -3
  22. package/dist/config.d.mts.map +1 -1
  23. package/dist/config.mjs +3 -3
  24. package/dist/{dokploy-api-z0833e7r.mjs → dokploy-api-2ldYoN3i.mjs} +131 -1
  25. package/dist/dokploy-api-2ldYoN3i.mjs.map +1 -0
  26. package/dist/dokploy-api-C93pveuy.mjs +3 -0
  27. package/dist/dokploy-api-CbDh4o93.cjs +3 -0
  28. package/dist/{dokploy-api-CQvhV6Hd.cjs → dokploy-api-DLgvEQlr.cjs} +131 -1
  29. package/dist/dokploy-api-DLgvEQlr.cjs.map +1 -0
  30. package/dist/{index-C0SpUT9Y.d.mts → index-C-KxSGGK.d.mts} +133 -31
  31. package/dist/index-C-KxSGGK.d.mts.map +1 -0
  32. package/dist/{index-B58qjyBd.d.cts → index-Cyk2rTyj.d.cts} +132 -30
  33. package/dist/index-Cyk2rTyj.d.cts.map +1 -0
  34. package/dist/index.cjs +662 -152
  35. package/dist/index.cjs.map +1 -1
  36. package/dist/index.mjs +626 -116
  37. package/dist/index.mjs.map +1 -1
  38. package/dist/{openapi-BcSjLfWq.mjs → openapi-BYlyAbH3.mjs} +6 -5
  39. package/dist/openapi-BYlyAbH3.mjs.map +1 -0
  40. package/dist/{openapi-D6Hcfov0.cjs → openapi-CnvwSRDU.cjs} +6 -5
  41. package/dist/openapi-CnvwSRDU.cjs.map +1 -0
  42. package/dist/openapi.cjs +3 -3
  43. package/dist/openapi.d.cts +1 -0
  44. package/dist/openapi.d.cts.map +1 -1
  45. package/dist/openapi.d.mts +2 -1
  46. package/dist/openapi.d.mts.map +1 -1
  47. package/dist/openapi.mjs +3 -3
  48. package/dist/{types-B9UZ7fOG.d.mts → types-CZg5iUgD.d.mts} +1 -1
  49. package/dist/{types-B9UZ7fOG.d.mts.map → types-CZg5iUgD.d.mts.map} +1 -1
  50. package/dist/workspace/index.cjs +1 -1
  51. package/dist/workspace/index.d.cts +1 -1
  52. package/dist/workspace/index.d.mts +2 -2
  53. package/dist/workspace/index.mjs +1 -1
  54. package/dist/{workspace-BW2iU37P.mjs → workspace-9IQIjwkQ.mjs} +20 -4
  55. package/dist/workspace-9IQIjwkQ.mjs.map +1 -0
  56. package/dist/{workspace-2Do2YcGZ.cjs → workspace-D2ocAlpl.cjs} +20 -4
  57. package/dist/workspace-D2ocAlpl.cjs.map +1 -0
  58. package/examples/cron-example.ts +6 -6
  59. package/examples/function-example.ts +1 -1
  60. package/package.json +6 -3
  61. package/src/config.ts +44 -0
  62. package/src/deploy/__tests__/backup-provisioner.spec.ts +428 -0
  63. package/src/deploy/__tests__/createDnsProvider.spec.ts +23 -0
  64. package/src/deploy/__tests__/env-resolver.spec.ts +1 -1
  65. package/src/deploy/__tests__/undeploy.spec.ts +758 -0
  66. package/src/deploy/backup-provisioner.ts +316 -0
  67. package/src/deploy/dns/DnsProvider.ts +39 -1
  68. package/src/deploy/dns/HostingerProvider.ts +74 -0
  69. package/src/deploy/dns/Route53Provider.ts +81 -0
  70. package/src/deploy/dns/index.ts +25 -0
  71. package/src/deploy/dokploy-api.ts +237 -0
  72. package/src/deploy/index.ts +71 -13
  73. package/src/deploy/state.ts +171 -0
  74. package/src/deploy/undeploy.ts +407 -0
  75. package/src/dev/__tests__/index.spec.ts +490 -0
  76. package/src/dev/index.ts +313 -18
  77. package/src/generators/FunctionGenerator.ts +1 -1
  78. package/src/generators/Generator.ts +4 -1
  79. package/src/init/__tests__/generators.spec.ts +167 -18
  80. package/src/init/__tests__/init.spec.ts +66 -3
  81. package/src/init/generators/auth.ts +6 -5
  82. package/src/init/generators/config.ts +49 -7
  83. package/src/init/generators/docker.ts +8 -8
  84. package/src/init/generators/index.ts +1 -0
  85. package/src/init/generators/models.ts +3 -5
  86. package/src/init/generators/package.ts +4 -0
  87. package/src/init/generators/test.ts +133 -0
  88. package/src/init/generators/ui.ts +13 -12
  89. package/src/init/generators/web.ts +9 -8
  90. package/src/init/index.ts +2 -0
  91. package/src/init/templates/api.ts +6 -6
  92. package/src/init/templates/minimal.ts +2 -2
  93. package/src/init/templates/worker.ts +2 -2
  94. package/src/init/versions.ts +3 -3
  95. package/src/openapi.ts +6 -2
  96. package/src/test/__tests__/__fixtures__/workspace.ts +104 -0
  97. package/src/test/__tests__/api.spec.ts +199 -0
  98. package/src/test/__tests__/auth.spec.ts +162 -0
  99. package/src/test/__tests__/index.spec.ts +323 -0
  100. package/src/test/__tests__/web.spec.ts +210 -0
  101. package/src/test/index.ts +165 -14
  102. package/src/workspace/__tests__/index.spec.ts +3 -0
  103. package/src/workspace/index.ts +4 -2
  104. package/src/workspace/schema.ts +26 -0
  105. package/src/workspace/types.ts +14 -37
  106. package/dist/HostingerProvider-B9N-TKbp.mjs.map +0 -1
  107. package/dist/HostingerProvider-DUV9-Tzg.cjs.map +0 -1
  108. package/dist/Route53Provider-Bs7Arms9.cjs.map +0 -1
  109. package/dist/Route53Provider-C8mS0zY6.mjs.map +0 -1
  110. package/dist/dokploy-api-CQvhV6Hd.cjs.map +0 -1
  111. package/dist/dokploy-api-CWc02yyg.cjs +0 -3
  112. package/dist/dokploy-api-DSJYNx88.mjs +0 -3
  113. package/dist/dokploy-api-z0833e7r.mjs.map +0 -1
  114. package/dist/index-B58qjyBd.d.cts.map +0 -1
  115. package/dist/index-C0SpUT9Y.d.mts.map +0 -1
  116. package/dist/openapi-BcSjLfWq.mjs.map +0 -1
  117. package/dist/openapi-D6Hcfov0.cjs.map +0 -1
  118. package/dist/workspace-2Do2YcGZ.cjs.map +0 -1
  119. package/dist/workspace-BW2iU37P.mjs.map +0 -1
@@ -13,11 +13,16 @@ import {
13
13
  findAvailablePort,
14
14
  generateAllDependencyEnvVars,
15
15
  isPortAvailable,
16
+ loadPortState,
16
17
  loadSecretsForApp,
17
18
  normalizeHooksConfig,
18
19
  normalizeProductionConfig,
19
20
  normalizeStudioConfig,
20
21
  normalizeTelescopeConfig,
22
+ parseComposePortMappings,
23
+ replacePortInUrl,
24
+ rewriteUrlsWithPorts,
25
+ savePortState,
21
26
  validateFrontendApp,
22
27
  validateFrontendApps,
23
28
  } from '../index';
@@ -582,7 +587,9 @@ describe('Workspace Dev Server', () => {
582
587
  const env = generateAllDependencyEnvVars(workspace);
583
588
  expect(env).toEqual({
584
589
  API_URL: 'http://localhost:3000',
590
+ NEXT_PUBLIC_API_URL: 'http://localhost:3000',
585
591
  AUTH_URL: 'http://localhost:3001',
592
+ NEXT_PUBLIC_AUTH_URL: 'http://localhost:3001',
586
593
  });
587
594
  });
588
595
 
@@ -611,6 +618,7 @@ describe('Workspace Dev Server', () => {
611
618
  const env = generateAllDependencyEnvVars(workspace);
612
619
  expect(env).toEqual({
613
620
  'USER-SERVICE_URL': 'http://localhost:3001',
621
+ 'NEXT_PUBLIC_USER-SERVICE_URL': 'http://localhost:3001',
614
622
  });
615
623
  });
616
624
 
@@ -633,6 +641,7 @@ describe('Workspace Dev Server', () => {
633
641
  const env = generateAllDependencyEnvVars(workspace, 'http://127.0.0.1');
634
642
  expect(env).toEqual({
635
643
  API_URL: 'http://127.0.0.1:3000',
644
+ NEXT_PUBLIC_API_URL: 'http://127.0.0.1:3000',
636
645
  });
637
646
  });
638
647
 
@@ -673,8 +682,11 @@ describe('Workspace Dev Server', () => {
673
682
  const env = generateAllDependencyEnvVars(workspace);
674
683
  expect(env).toEqual({
675
684
  AUTH_URL: 'http://localhost:3001',
685
+ NEXT_PUBLIC_AUTH_URL: 'http://localhost:3001',
676
686
  API_URL: 'http://localhost:3000',
687
+ NEXT_PUBLIC_API_URL: 'http://localhost:3000',
677
688
  PAYMENTS_URL: 'http://localhost:3002',
689
+ NEXT_PUBLIC_PAYMENTS_URL: 'http://localhost:3002',
678
690
  });
679
691
  });
680
692
  });
@@ -1188,3 +1200,481 @@ describe('loadSecretsForApp', () => {
1188
1200
  });
1189
1201
  });
1190
1202
  });
1203
+
1204
+ describe('replacePortInUrl', () => {
1205
+ it('should replace port in a postgresql URL', () => {
1206
+ const url = 'postgresql://app:pass@localhost:5432/mydb';
1207
+ expect(replacePortInUrl(url, 5432, 5433)).toBe(
1208
+ 'postgresql://app:pass@localhost:5433/mydb',
1209
+ );
1210
+ });
1211
+
1212
+ it('should replace port in a redis URL', () => {
1213
+ const url = 'redis://:pass@localhost:6379';
1214
+ expect(replacePortInUrl(url, 6379, 6380)).toBe(
1215
+ 'redis://:pass@localhost:6380',
1216
+ );
1217
+ });
1218
+
1219
+ it('should replace port with Docker hostname', () => {
1220
+ const url = 'postgresql://app:pass@postgres:5432/mydb';
1221
+ expect(replacePortInUrl(url, 5432, 5433)).toBe(
1222
+ 'postgresql://app:pass@postgres:5433/mydb',
1223
+ );
1224
+ });
1225
+
1226
+ it('should return url unchanged when ports are equal', () => {
1227
+ const url = 'postgresql://app:pass@localhost:5432/mydb';
1228
+ expect(replacePortInUrl(url, 5432, 5432)).toBe(url);
1229
+ });
1230
+
1231
+ it('should not replace port that appears in path or password', () => {
1232
+ const url = 'postgresql://app:pass5432@localhost:5432/db5432';
1233
+ const result = replacePortInUrl(url, 5432, 5433);
1234
+ // Only the :5432 before / should be replaced
1235
+ expect(result).toBe('postgresql://app:pass5432@localhost:5433/db5432');
1236
+ });
1237
+ });
1238
+
1239
+ describe('rewriteUrlsWithPorts', () => {
1240
+ const pgMapping = {
1241
+ service: 'postgres',
1242
+ envVar: 'POSTGRES_HOST_PORT',
1243
+ defaultPort: 5432,
1244
+ containerPort: 5432,
1245
+ };
1246
+ const redisMapping = {
1247
+ service: 'redis',
1248
+ envVar: 'REDIS_HOST_PORT',
1249
+ defaultPort: 6379,
1250
+ containerPort: 6379,
1251
+ };
1252
+ const rmqMapping = {
1253
+ service: 'rabbitmq',
1254
+ envVar: 'RABBITMQ_HOST_PORT',
1255
+ defaultPort: 5672,
1256
+ containerPort: 5672,
1257
+ };
1258
+
1259
+ it('should rewrite DATABASE_URL with resolved postgres port', () => {
1260
+ const secrets = {
1261
+ DATABASE_URL: 'postgresql://app:pass@postgres:5432/mydb',
1262
+ POSTGRES_PORT: '5432',
1263
+ SOME_OTHER: 'value',
1264
+ };
1265
+ const result = rewriteUrlsWithPorts(secrets, {
1266
+ dockerEnv: { POSTGRES_HOST_PORT: '5433' },
1267
+ ports: { POSTGRES_HOST_PORT: 5433 },
1268
+ mappings: [pgMapping],
1269
+ });
1270
+ expect(result.DATABASE_URL).toBe(
1271
+ 'postgresql://app:pass@postgres:5433/mydb',
1272
+ );
1273
+ expect(result.POSTGRES_PORT).toBe('5433');
1274
+ expect(result.SOME_OTHER).toBe('value');
1275
+ });
1276
+
1277
+ it('should rewrite fullstack APP_DATABASE_URL', () => {
1278
+ const secrets = {
1279
+ API_DATABASE_URL: 'postgresql://api:pass@localhost:5432/mydb',
1280
+ AUTH_DATABASE_URL: 'postgresql://auth:pass@localhost:5432/mydb',
1281
+ };
1282
+ const result = rewriteUrlsWithPorts(secrets, {
1283
+ dockerEnv: { POSTGRES_HOST_PORT: '5433' },
1284
+ ports: { POSTGRES_HOST_PORT: 5433 },
1285
+ mappings: [pgMapping],
1286
+ });
1287
+ expect(result.API_DATABASE_URL).toBe(
1288
+ 'postgresql://api:pass@localhost:5433/mydb',
1289
+ );
1290
+ expect(result.AUTH_DATABASE_URL).toBe(
1291
+ 'postgresql://auth:pass@localhost:5433/mydb',
1292
+ );
1293
+ });
1294
+
1295
+ it('should rewrite REDIS_URL and REDIS_PORT', () => {
1296
+ const secrets = {
1297
+ REDIS_URL: 'redis://:pass@redis:6379',
1298
+ REDIS_PORT: '6379',
1299
+ };
1300
+ const result = rewriteUrlsWithPorts(secrets, {
1301
+ dockerEnv: { REDIS_HOST_PORT: '6380' },
1302
+ ports: { REDIS_HOST_PORT: 6380 },
1303
+ mappings: [redisMapping],
1304
+ });
1305
+ expect(result.REDIS_URL).toBe('redis://:pass@redis:6380');
1306
+ expect(result.REDIS_PORT).toBe('6380');
1307
+ });
1308
+
1309
+ it('should rewrite RABBITMQ_URL and RABBITMQ_PORT', () => {
1310
+ const secrets = {
1311
+ RABBITMQ_URL: 'amqp://app:pass@rabbitmq:5672/%2F',
1312
+ RABBITMQ_PORT: '5672',
1313
+ };
1314
+ const result = rewriteUrlsWithPorts(secrets, {
1315
+ dockerEnv: { RABBITMQ_HOST_PORT: '5673' },
1316
+ ports: { RABBITMQ_HOST_PORT: 5673 },
1317
+ mappings: [rmqMapping],
1318
+ });
1319
+ expect(result.RABBITMQ_URL).toBe('amqp://app:pass@rabbitmq:5673/%2F');
1320
+ expect(result.RABBITMQ_PORT).toBe('5673');
1321
+ });
1322
+
1323
+ it('should handle multiple services at once', () => {
1324
+ const secrets = {
1325
+ DATABASE_URL: 'postgresql://app:pass@postgres:5432/mydb',
1326
+ POSTGRES_PORT: '5432',
1327
+ REDIS_URL: 'redis://:pass@redis:6379',
1328
+ REDIS_PORT: '6379',
1329
+ };
1330
+ const result = rewriteUrlsWithPorts(secrets, {
1331
+ dockerEnv: {
1332
+ POSTGRES_HOST_PORT: '5433',
1333
+ REDIS_HOST_PORT: '6380',
1334
+ },
1335
+ ports: { POSTGRES_HOST_PORT: 5433, REDIS_HOST_PORT: 6380 },
1336
+ mappings: [pgMapping, redisMapping],
1337
+ });
1338
+ expect(result.DATABASE_URL).toContain(':5433/');
1339
+ expect(result.POSTGRES_PORT).toBe('5433');
1340
+ expect(result.REDIS_URL).toContain(':6380');
1341
+ expect(result.REDIS_PORT).toBe('6380');
1342
+ });
1343
+
1344
+ it('should not modify secrets when ports are defaults', () => {
1345
+ const secrets = {
1346
+ DATABASE_URL: 'postgresql://app:pass@postgres:5432/mydb',
1347
+ POSTGRES_PORT: '5432',
1348
+ };
1349
+ const result = rewriteUrlsWithPorts(secrets, {
1350
+ dockerEnv: { POSTGRES_HOST_PORT: '5432' },
1351
+ ports: { POSTGRES_HOST_PORT: 5432 },
1352
+ mappings: [pgMapping],
1353
+ });
1354
+ expect(result.DATABASE_URL).toBe(secrets.DATABASE_URL);
1355
+ expect(result.POSTGRES_PORT).toBe('5432');
1356
+ });
1357
+
1358
+ it('should return empty for no mappings', () => {
1359
+ const result = rewriteUrlsWithPorts(
1360
+ {},
1361
+ { dockerEnv: {}, ports: {}, mappings: [] },
1362
+ );
1363
+ expect(result).toEqual({});
1364
+ });
1365
+ });
1366
+
1367
+ describe('port state persistence', () => {
1368
+ let testDir: string;
1369
+
1370
+ beforeEach(() => {
1371
+ testDir = join(tmpdir(), `gkm-port-state-test-${Date.now()}`);
1372
+ mkdirSync(testDir, { recursive: true });
1373
+ });
1374
+
1375
+ afterEach(() => {
1376
+ rmSync(testDir, { recursive: true, force: true });
1377
+ });
1378
+
1379
+ it('should return empty object when no state file exists', async () => {
1380
+ const state = await loadPortState(testDir);
1381
+ expect(state).toEqual({});
1382
+ });
1383
+
1384
+ it('should save and load port state', async () => {
1385
+ const ports = { POSTGRES_HOST_PORT: 5433, REDIS_HOST_PORT: 6380 };
1386
+ await savePortState(testDir, ports);
1387
+ const loaded = await loadPortState(testDir);
1388
+ expect(loaded).toEqual(ports);
1389
+ });
1390
+
1391
+ it('should create .gkm directory if missing', async () => {
1392
+ const ports = { POSTGRES_HOST_PORT: 5433 };
1393
+ await savePortState(testDir, ports);
1394
+ expect(existsSync(join(testDir, '.gkm'))).toBe(true);
1395
+ expect(existsSync(join(testDir, '.gkm', 'ports.json'))).toBe(true);
1396
+ });
1397
+
1398
+ it('should overwrite existing state', async () => {
1399
+ await savePortState(testDir, { POSTGRES_HOST_PORT: 5433 });
1400
+ await savePortState(testDir, {
1401
+ POSTGRES_HOST_PORT: 5434,
1402
+ REDIS_HOST_PORT: 6380,
1403
+ });
1404
+ const loaded = await loadPortState(testDir);
1405
+ expect(loaded).toEqual({
1406
+ POSTGRES_HOST_PORT: 5434,
1407
+ REDIS_HOST_PORT: 6380,
1408
+ });
1409
+ });
1410
+ });
1411
+
1412
+ describe('parseComposePortMappings', () => {
1413
+ let testDir: string;
1414
+
1415
+ beforeEach(() => {
1416
+ testDir = join(tmpdir(), `gkm-compose-parse-test-${Date.now()}`);
1417
+ mkdirSync(testDir, { recursive: true });
1418
+ });
1419
+
1420
+ afterEach(() => {
1421
+ rmSync(testDir, { recursive: true, force: true });
1422
+ });
1423
+
1424
+ it('should parse standard api template ports', () => {
1425
+ const composePath = join(testDir, 'docker-compose.yml');
1426
+ writeFileSync(
1427
+ composePath,
1428
+ `
1429
+ services:
1430
+ postgres:
1431
+ image: postgres:17
1432
+ ports:
1433
+ - '\${POSTGRES_HOST_PORT:-5432}:5432'
1434
+ redis:
1435
+ image: redis:7
1436
+ ports:
1437
+ - '\${REDIS_HOST_PORT:-6379}:6379'
1438
+ mailpit:
1439
+ image: axllent/mailpit
1440
+ ports:
1441
+ - '\${MAILPIT_SMTP_PORT:-1025}:1025'
1442
+ - '\${MAILPIT_UI_PORT:-8025}:8025'
1443
+ `,
1444
+ );
1445
+
1446
+ const mappings = parseComposePortMappings(composePath);
1447
+ expect(mappings).toEqual([
1448
+ {
1449
+ service: 'postgres',
1450
+ envVar: 'POSTGRES_HOST_PORT',
1451
+ defaultPort: 5432,
1452
+ containerPort: 5432,
1453
+ },
1454
+ {
1455
+ service: 'redis',
1456
+ envVar: 'REDIS_HOST_PORT',
1457
+ defaultPort: 6379,
1458
+ containerPort: 6379,
1459
+ },
1460
+ {
1461
+ service: 'mailpit',
1462
+ envVar: 'MAILPIT_SMTP_PORT',
1463
+ defaultPort: 1025,
1464
+ containerPort: 1025,
1465
+ },
1466
+ {
1467
+ service: 'mailpit',
1468
+ envVar: 'MAILPIT_UI_PORT',
1469
+ defaultPort: 8025,
1470
+ containerPort: 8025,
1471
+ },
1472
+ ]);
1473
+ });
1474
+
1475
+ it('should parse worker template with rabbitmq', () => {
1476
+ const composePath = join(testDir, 'docker-compose.yml');
1477
+ writeFileSync(
1478
+ composePath,
1479
+ `
1480
+ services:
1481
+ postgres:
1482
+ image: postgres:17
1483
+ ports:
1484
+ - '\${POSTGRES_HOST_PORT:-5432}:5432'
1485
+ rabbitmq:
1486
+ image: rabbitmq:3-management
1487
+ ports:
1488
+ - '\${RABBITMQ_HOST_PORT:-5672}:5672'
1489
+ - '\${RABBITMQ_MGMT_PORT:-15672}:15672'
1490
+ `,
1491
+ );
1492
+
1493
+ const mappings = parseComposePortMappings(composePath);
1494
+ expect(mappings).toEqual([
1495
+ {
1496
+ service: 'postgres',
1497
+ envVar: 'POSTGRES_HOST_PORT',
1498
+ defaultPort: 5432,
1499
+ containerPort: 5432,
1500
+ },
1501
+ {
1502
+ service: 'rabbitmq',
1503
+ envVar: 'RABBITMQ_HOST_PORT',
1504
+ defaultPort: 5672,
1505
+ containerPort: 5672,
1506
+ },
1507
+ {
1508
+ service: 'rabbitmq',
1509
+ envVar: 'RABBITMQ_MGMT_PORT',
1510
+ defaultPort: 15672,
1511
+ containerPort: 15672,
1512
+ },
1513
+ ]);
1514
+ });
1515
+
1516
+ it('should parse serverless template with redis variants', () => {
1517
+ const composePath = join(testDir, 'docker-compose.yml');
1518
+ writeFileSync(
1519
+ composePath,
1520
+ `
1521
+ services:
1522
+ postgres:
1523
+ image: postgres:17
1524
+ ports:
1525
+ - '\${POSTGRES_HOST_PORT:-5432}:5432'
1526
+ redis:
1527
+ image: redis:7
1528
+ ports:
1529
+ - '\${REDIS_HOST_PORT:-6379}:6379'
1530
+ serverless-redis-http:
1531
+ image: hiett/serverless-redis-http:latest
1532
+ ports:
1533
+ - '\${SRH_PORT:-8079}:80'
1534
+ `,
1535
+ );
1536
+
1537
+ const mappings = parseComposePortMappings(composePath);
1538
+ expect(mappings).toEqual([
1539
+ {
1540
+ service: 'postgres',
1541
+ envVar: 'POSTGRES_HOST_PORT',
1542
+ defaultPort: 5432,
1543
+ containerPort: 5432,
1544
+ },
1545
+ {
1546
+ service: 'redis',
1547
+ envVar: 'REDIS_HOST_PORT',
1548
+ defaultPort: 6379,
1549
+ containerPort: 6379,
1550
+ },
1551
+ {
1552
+ service: 'serverless-redis-http',
1553
+ envVar: 'SRH_PORT',
1554
+ defaultPort: 8079,
1555
+ containerPort: 80,
1556
+ },
1557
+ ]);
1558
+ });
1559
+
1560
+ it('should handle custom user-added services', () => {
1561
+ const composePath = join(testDir, 'docker-compose.yml');
1562
+ writeFileSync(
1563
+ composePath,
1564
+ `
1565
+ services:
1566
+ postgres:
1567
+ image: postgres:17
1568
+ ports:
1569
+ - '\${POSTGRES_HOST_PORT:-5432}:5432'
1570
+ pgadmin:
1571
+ image: dpage/pgadmin4
1572
+ ports:
1573
+ - '\${PGADMIN_HOST_PORT:-5050}:80'
1574
+ minio:
1575
+ image: minio/minio
1576
+ ports:
1577
+ - '\${MINIO_API_PORT:-9000}:9000'
1578
+ - '\${MINIO_CONSOLE_PORT:-9001}:9001'
1579
+ `,
1580
+ );
1581
+
1582
+ const mappings = parseComposePortMappings(composePath);
1583
+ expect(mappings).toEqual([
1584
+ {
1585
+ service: 'postgres',
1586
+ envVar: 'POSTGRES_HOST_PORT',
1587
+ defaultPort: 5432,
1588
+ containerPort: 5432,
1589
+ },
1590
+ {
1591
+ service: 'pgadmin',
1592
+ envVar: 'PGADMIN_HOST_PORT',
1593
+ defaultPort: 5050,
1594
+ containerPort: 80,
1595
+ },
1596
+ {
1597
+ service: 'minio',
1598
+ envVar: 'MINIO_API_PORT',
1599
+ defaultPort: 9000,
1600
+ containerPort: 9000,
1601
+ },
1602
+ {
1603
+ service: 'minio',
1604
+ envVar: 'MINIO_CONSOLE_PORT',
1605
+ defaultPort: 9001,
1606
+ containerPort: 9001,
1607
+ },
1608
+ ]);
1609
+ });
1610
+
1611
+ it('should skip fixed port mappings without env vars', () => {
1612
+ const composePath = join(testDir, 'docker-compose.yml');
1613
+ writeFileSync(
1614
+ composePath,
1615
+ `
1616
+ services:
1617
+ postgres:
1618
+ image: postgres:17
1619
+ ports:
1620
+ - '\${POSTGRES_HOST_PORT:-5432}:5432'
1621
+ nginx:
1622
+ image: nginx
1623
+ ports:
1624
+ - '8080:80'
1625
+ - '443:443'
1626
+ `,
1627
+ );
1628
+
1629
+ const mappings = parseComposePortMappings(composePath);
1630
+ expect(mappings).toEqual([
1631
+ {
1632
+ service: 'postgres',
1633
+ envVar: 'POSTGRES_HOST_PORT',
1634
+ defaultPort: 5432,
1635
+ containerPort: 5432,
1636
+ },
1637
+ ]);
1638
+ });
1639
+
1640
+ it('should skip services without ports', () => {
1641
+ const composePath = join(testDir, 'docker-compose.yml');
1642
+ writeFileSync(
1643
+ composePath,
1644
+ `
1645
+ services:
1646
+ postgres:
1647
+ image: postgres:17
1648
+ ports:
1649
+ - '\${POSTGRES_HOST_PORT:-5432}:5432'
1650
+ worker:
1651
+ image: node:22
1652
+ command: npm start
1653
+ `,
1654
+ );
1655
+
1656
+ const mappings = parseComposePortMappings(composePath);
1657
+ expect(mappings).toEqual([
1658
+ {
1659
+ service: 'postgres',
1660
+ envVar: 'POSTGRES_HOST_PORT',
1661
+ defaultPort: 5432,
1662
+ containerPort: 5432,
1663
+ },
1664
+ ]);
1665
+ });
1666
+
1667
+ it('should return empty array when file does not exist', () => {
1668
+ const composePath = join(testDir, 'nonexistent.yml');
1669
+ const mappings = parseComposePortMappings(composePath);
1670
+ expect(mappings).toEqual([]);
1671
+ });
1672
+
1673
+ it('should return empty array when no services defined', () => {
1674
+ const composePath = join(testDir, 'docker-compose.yml');
1675
+ writeFileSync(composePath, 'version: "3.8"\n');
1676
+
1677
+ const mappings = parseComposePortMappings(composePath);
1678
+ expect(mappings).toEqual([]);
1679
+ });
1680
+ });