@geekmidas/cli 9.0.2 → 10.0.0-alpha.1
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 +66 -122
- package/dist/{CachedStateProvider-BdX7klRc.cjs → CachedStateProvider-BLo7zv7S.cjs} +1 -1
- package/dist/CachedStateProvider-BxoAm-B2.mjs +3 -0
- package/dist/{CachedStateProvider-klLWZXWF.cjs → CachedStateProvider-CqPdW7CB.cjs} +1 -1
- package/dist/{CachedStateProvider-klLWZXWF.cjs.map → CachedStateProvider-CqPdW7CB.cjs.map} +1 -1
- package/dist/{CachedStateProvider-CzyFct42.mjs → CachedStateProvider-DTniehMP.mjs} +1 -1
- package/dist/{CachedStateProvider-CzyFct42.mjs.map → CachedStateProvider-DTniehMP.mjs.map} +1 -1
- package/dist/{HostingerProvider-B2ZYz7Yc.mjs → HostingerProvider-Cft7lEYr.mjs} +4 -3
- package/dist/HostingerProvider-Cft7lEYr.mjs.map +1 -0
- package/dist/{HostingerProvider-CrHfsgz9.cjs → HostingerProvider-h3EbEp-Z.cjs} +4 -3
- package/dist/HostingerProvider-h3EbEp-Z.cjs.map +1 -0
- package/dist/{LocalStateProvider-CP49rjzt.mjs → LocalStateProvider-Bn6tS-oL.mjs} +1 -1
- package/dist/{LocalStateProvider-CP49rjzt.mjs.map → LocalStateProvider-Bn6tS-oL.mjs.map} +1 -1
- package/dist/{LocalStateProvider-CalzNfNA.cjs → LocalStateProvider-Db9SEjyH.cjs} +1 -1
- package/dist/{LocalStateProvider-CalzNfNA.cjs.map → LocalStateProvider-Db9SEjyH.cjs.map} +1 -1
- package/dist/{Route53Provider-tlgBzrRQ.mjs → Route53Provider-RdpUDJQk.mjs} +1 -1
- package/dist/{Route53Provider-tlgBzrRQ.mjs.map → Route53Provider-RdpUDJQk.mjs.map} +1 -1
- package/dist/{Route53Provider-DwgXiLxt.cjs → Route53Provider-di2fMM5N.cjs} +1 -1
- package/dist/{Route53Provider-DwgXiLxt.cjs.map → Route53Provider-di2fMM5N.cjs.map} +1 -1
- package/dist/{SSMStateProvider-BlrTiV5m.mjs → SSMStateProvider-BRt2py-A.mjs} +1 -1
- package/dist/{SSMStateProvider-BlrTiV5m.mjs.map → SSMStateProvider-BRt2py-A.mjs.map} +1 -1
- package/dist/{SSMStateProvider-Bt6r8CTL.cjs → SSMStateProvider-DyX8s9HI.cjs} +1 -1
- package/dist/{SSMStateProvider-Bt6r8CTL.cjs.map → SSMStateProvider-DyX8s9HI.cjs.map} +1 -1
- package/dist/{backup-provisioner-Xunq-p_8.mjs → backup-provisioner-Bgj5Vi_L.mjs} +2 -2
- package/dist/{backup-provisioner-Xunq-p_8.mjs.map → backup-provisioner-Bgj5Vi_L.mjs.map} +1 -1
- package/dist/{backup-provisioner-B3HBN-2k.cjs → backup-provisioner-BrsHwYG0.cjs} +2 -2
- package/dist/{backup-provisioner-B3HBN-2k.cjs.map → backup-provisioner-BrsHwYG0.cjs.map} +1 -1
- package/dist/{bundler-Dufq3uvI.cjs → bundler-BY1OmV1e.cjs} +44 -14
- package/dist/bundler-BY1OmV1e.cjs.map +1 -0
- package/dist/{bundler-DkA4qzUD.mjs → bundler-Be8Zg4Wd.mjs} +45 -15
- package/dist/bundler-Be8Zg4Wd.mjs.map +1 -0
- package/dist/config-D8o6I_7Z.mjs +469 -0
- package/dist/config-D8o6I_7Z.mjs.map +1 -0
- package/dist/config-DbPE1KXF.cjs +535 -0
- package/dist/config-DbPE1KXF.cjs.map +1 -0
- package/dist/config.cjs +3 -2
- package/dist/config.d.cts +2 -2
- package/dist/config.d.cts.map +1 -1
- package/dist/config.d.mts +2 -2
- package/dist/config.d.mts.map +1 -1
- package/dist/config.mjs +3 -2
- package/dist/{credentials-s1kLcIzK.mjs → credentials-B5zRHdAl.mjs} +21 -6
- package/dist/credentials-B5zRHdAl.mjs.map +1 -0
- package/dist/{credentials-C8DWtnMY.cjs → credentials-BapzjyV5.cjs} +26 -5
- package/dist/credentials-BapzjyV5.cjs.map +1 -0
- package/dist/credentials-Dh0Q6HzY.mjs +2548 -0
- package/dist/credentials-Dh0Q6HzY.mjs.map +1 -0
- package/dist/credentials-K961ThG3.cjs +2680 -0
- package/dist/credentials-K961ThG3.cjs.map +1 -0
- package/dist/deploy/sniffer-routes-worker.cjs +6 -2
- package/dist/deploy/sniffer-routes-worker.cjs.map +1 -1
- package/dist/deploy/sniffer-routes-worker.mjs +6 -2
- package/dist/deploy/sniffer-routes-worker.mjs.map +1 -1
- package/dist/discover-CAlFgYec.mjs +229 -0
- package/dist/discover-CAlFgYec.mjs.map +1 -0
- package/dist/discover-DTmFeloL.cjs +271 -0
- package/dist/discover-DTmFeloL.cjs.map +1 -0
- package/dist/dokploy-api-BS2xnJLZ.mjs +3 -0
- package/dist/{dokploy-api-CqOIKHme.cjs → dokploy-api-BsVH0y_s.cjs} +141 -32
- package/dist/dokploy-api-BsVH0y_s.cjs.map +1 -0
- package/dist/dokploy-api-BuxDRo3l.cjs +3 -0
- package/dist/{dokploy-api-sfZFlWuE.mjs → dokploy-api-aTFPz7_4.mjs} +141 -32
- package/dist/dokploy-api-aTFPz7_4.mjs.map +1 -0
- package/dist/emit-CbcRolsa.cjs +236 -0
- package/dist/emit-CbcRolsa.cjs.map +1 -0
- package/dist/emit-CiZS_5Ub.mjs +206 -0
- package/dist/emit-CiZS_5Ub.mjs.map +1 -0
- package/dist/{encryption-MfXAiFTm.cjs → encryption-C6jaU0hE.cjs} +1 -1
- package/dist/{encryption-MfXAiFTm.cjs.map → encryption-C6jaU0hE.cjs.map} +1 -1
- package/dist/{encryption-BVS7Vmzs.cjs → encryption-CHzzeR8a.cjs} +1 -1
- package/dist/encryption-CQUncu7v.mjs +3 -0
- package/dist/{encryption-CO5Q4Bhf.mjs → encryption-D-abyhZx.mjs} +1 -1
- package/dist/{encryption-CO5Q4Bhf.mjs.map → encryption-D-abyhZx.mjs.map} +1 -1
- package/dist/fullstack-secrets-C-GGohke.cjs +111 -0
- package/dist/fullstack-secrets-C-GGohke.cjs.map +1 -0
- package/dist/fullstack-secrets-D1o9-G5l.mjs +87 -0
- package/dist/fullstack-secrets-D1o9-G5l.mjs.map +1 -0
- package/dist/{fullstack-secrets-BYzi6Hwl.cjs → generator-CNzBw7g5.cjs} +1 -114
- package/dist/generator-CNzBw7g5.cjs.map +1 -0
- package/dist/{fullstack-secrets-l8s3II_H.mjs → generator-VE2JrFlY.mjs} +2 -85
- package/dist/generator-VE2JrFlY.mjs.map +1 -0
- package/dist/index-BThUzEIp.d.mts +175 -0
- package/dist/index-BThUzEIp.d.mts.map +1 -0
- package/dist/index-CHB4tSzX.d.cts +175 -0
- package/dist/index-CHB4tSzX.d.cts.map +1 -0
- package/dist/index.cjs +2530 -1810
- package/dist/index.cjs.map +1 -1
- package/dist/index.mjs +2506 -1786
- package/dist/index.mjs.map +1 -1
- package/dist/{openapi-Cfwi0fqF.mjs → openapi-CgpMWMmM.mjs} +174 -95
- package/dist/openapi-CgpMWMmM.mjs.map +1 -0
- package/dist/{openapi-BzNOC7_i.cjs → openapi-CljONd_M.cjs} +175 -108
- package/dist/openapi-CljONd_M.cjs.map +1 -0
- package/dist/{openapi-react-query-C4iIvpPf.mjs → openapi-react-query-Byltvm_7.mjs} +2 -2
- package/dist/openapi-react-query-Byltvm_7.mjs.map +1 -0
- package/dist/{openapi-react-query-CYWik72n.cjs → openapi-react-query-CmJ1xDbn.cjs} +2 -2
- package/dist/openapi-react-query-CmJ1xDbn.cjs.map +1 -0
- package/dist/openapi-react-query.cjs +1 -1
- package/dist/openapi-react-query.mjs +1 -1
- package/dist/openapi.cjs +4 -3
- package/dist/openapi.d.cts +1 -1
- package/dist/openapi.d.mts +1 -1
- package/dist/openapi.mjs +4 -3
- package/dist/reconcile/public.cjs +9 -0
- package/dist/reconcile/public.d.cts +125 -0
- package/dist/reconcile/public.d.cts.map +1 -0
- package/dist/reconcile/public.d.mts +125 -0
- package/dist/reconcile/public.d.mts.map +1 -0
- package/dist/reconcile/public.mjs +4 -0
- package/dist/{reconcile-B2FoJE0a.mjs → reconcile-CFs8C_1a.mjs} +5 -4
- package/dist/reconcile-CFs8C_1a.mjs.map +1 -0
- package/dist/{reconcile-BzqKJ2Rt.cjs → reconcile-D0pimNiy.cjs} +5 -4
- package/dist/reconcile-D0pimNiy.cjs.map +1 -0
- package/dist/setup-CJiQM2pX.cjs +473 -0
- package/dist/setup-CJiQM2pX.cjs.map +1 -0
- package/dist/setup-DMtU3p41.cjs +11 -0
- package/dist/setup-DOsYueeQ.mjs +443 -0
- package/dist/setup-DOsYueeQ.mjs.map +1 -0
- package/dist/setup-DSutDAlt.mjs +11 -0
- package/dist/{storage-B1wvztiJ.cjs → storage-BJmzYxt8.cjs} +1 -1
- package/dist/{storage-B1wvztiJ.cjs.map → storage-BJmzYxt8.cjs.map} +1 -1
- package/dist/{storage-DOEtT2Hr.cjs → storage-BM9hGpgv.cjs} +1 -1
- package/dist/{storage-dbb9RyBl.mjs → storage-Bi24WfoW.mjs} +1 -1
- package/dist/{storage-dbb9RyBl.mjs.map → storage-Bi24WfoW.mjs.map} +1 -1
- package/dist/{storage-Cs4WBsc4.mjs → storage-CYlGwWTj.mjs} +1 -1
- package/dist/{sync-CSooqRQ8.mjs → sync-CmEuNZsB.mjs} +2 -2
- package/dist/{sync-CSooqRQ8.mjs.map → sync-CmEuNZsB.mjs.map} +1 -1
- package/dist/sync-D306yOWu.cjs +4 -0
- package/dist/{sync-o_CtEbvR.cjs → sync-SizaChmC.cjs} +2 -2
- package/dist/{sync-o_CtEbvR.cjs.map → sync-SizaChmC.cjs.map} +1 -1
- package/dist/{sync-B5UHXwRM.mjs → sync-hOqAWHR6.mjs} +1 -1
- package/dist/{index-Bw548Bta.d.mts → types-C-arnQ1r.d.mts} +590 -143
- package/dist/types-C-arnQ1r.d.mts.map +1 -0
- package/dist/{index-DTpkeFb8.d.cts → types-abJQuk7m.d.cts} +590 -143
- package/dist/types-abJQuk7m.d.cts.map +1 -0
- package/dist/workspace/index.cjs +5 -1
- package/dist/workspace/index.d.cts +3 -3
- package/dist/workspace/index.d.mts +3 -3
- package/dist/workspace/index.mjs +2 -2
- package/dist/{workspace-D-37Ylqo.mjs → workspace-7FqprxuJ.mjs} +117 -30
- package/dist/workspace-7FqprxuJ.mjs.map +1 -0
- package/dist/workspace-8acupeaI.cjs +9 -0
- package/dist/{workspace-CQTT7HJQ.cjs → workspace-XvqO9jIv.cjs} +139 -28
- package/dist/workspace-XvqO9jIv.cjs.map +1 -0
- package/dist/workspace-yP5AHnrn.mjs +8 -0
- package/package.json +26 -9
- package/CHANGELOG.md +0 -650
- package/FUNCTION_CRON_SUPPORT.md +0 -266
- package/dist/CachedStateProvider-GcGziXh1.mjs +0 -3
- package/dist/HostingerProvider-B2ZYz7Yc.mjs.map +0 -1
- package/dist/HostingerProvider-CrHfsgz9.cjs.map +0 -1
- package/dist/bundler-DkA4qzUD.mjs.map +0 -1
- package/dist/bundler-Dufq3uvI.cjs.map +0 -1
- package/dist/config-Bbu1JKvf.mjs +0 -211
- package/dist/config-Bbu1JKvf.mjs.map +0 -1
- package/dist/config-DujV8DbR.cjs +0 -253
- package/dist/config-DujV8DbR.cjs.map +0 -1
- package/dist/credentials-C8DWtnMY.cjs.map +0 -1
- package/dist/credentials-s1kLcIzK.mjs.map +0 -1
- package/dist/dokploy-api-BPYkG-ZZ.cjs +0 -3
- package/dist/dokploy-api-BiSzWM4v.mjs +0 -3
- package/dist/dokploy-api-CqOIKHme.cjs.map +0 -1
- package/dist/dokploy-api-sfZFlWuE.mjs.map +0 -1
- package/dist/encryption-Bd_m4q3g.mjs +0 -3
- package/dist/fullstack-secrets-BYzi6Hwl.cjs.map +0 -1
- package/dist/fullstack-secrets-l8s3II_H.mjs.map +0 -1
- package/dist/index-Bw548Bta.d.mts.map +0 -1
- package/dist/index-DTpkeFb8.d.cts.map +0 -1
- package/dist/openapi-BzNOC7_i.cjs.map +0 -1
- package/dist/openapi-Cfwi0fqF.mjs.map +0 -1
- package/dist/openapi-react-query-C4iIvpPf.mjs.map +0 -1
- package/dist/openapi-react-query-CYWik72n.cjs.map +0 -1
- package/dist/reconcile-B2FoJE0a.mjs.map +0 -1
- package/dist/reconcile-BzqKJ2Rt.cjs.map +0 -1
- package/dist/setup-DDXUXzzl.mjs +0 -765
- package/dist/setup-DDXUXzzl.mjs.map +0 -1
- package/dist/setup-DLwhfyt-.cjs +0 -8
- package/dist/setup-Dh-B7YTX.mjs +0 -8
- package/dist/setup-WZ-xqAQO.cjs +0 -849
- package/dist/setup-WZ-xqAQO.cjs.map +0 -1
- package/dist/sync-BLA94rgC.cjs +0 -4
- package/dist/types-B3GQUg3k.d.mts +0 -280
- package/dist/types-B3GQUg3k.d.mts.map +0 -1
- package/dist/types-BWQDiooe.d.cts +0 -280
- package/dist/types-BWQDiooe.d.cts.map +0 -1
- package/dist/workspace-CQTT7HJQ.cjs.map +0 -1
- package/dist/workspace-D-37Ylqo.mjs.map +0 -1
- package/docs/OPENAPI_TYPESCRIPT_DESIGN.md +0 -408
- package/docs/dev-server-resilience-design.md +0 -189
- package/docs/manifest-refactor-design.md +0 -287
- package/examples/cron-example.ts +0 -45
- package/examples/env.ts +0 -33
- package/examples/function-example.ts +0 -40
- package/examples/gkm.config.json +0 -22
- package/examples/gkm.config.ts +0 -14
- package/examples/gkm.minimal.config.json +0 -7
- package/examples/gkm.production.config.json +0 -27
- package/examples/logger.ts +0 -10
- package/scripts/sync-versions.ts +0 -108
- package/src/__tests__/EndpointGenerator.hooks.spec.ts +0 -204
- package/src/__tests__/config.spec.ts +0 -422
- package/src/__tests__/loadEnvFiles.spec.ts +0 -131
- package/src/__tests__/normalizeHooksConfig.spec.ts +0 -63
- package/src/__tests__/openapi-react-query.spec.ts +0 -506
- package/src/__tests__/openapi.spec.ts +0 -625
- package/src/__tests__/test-helpers.ts +0 -181
- package/src/auth/__tests__/credentials.spec.ts +0 -331
- package/src/auth/__tests__/index.spec.ts +0 -237
- package/src/auth/credentials.ts +0 -286
- package/src/auth/index.ts +0 -233
- package/src/build/__tests__/bundler.spec.ts +0 -446
- package/src/build/__tests__/endpoint-analyzer.spec.ts +0 -623
- package/src/build/__tests__/handler-templates.spec.ts +0 -1287
- package/src/build/__tests__/index-new.spec.ts +0 -550
- package/src/build/__tests__/manifests.spec.ts +0 -583
- package/src/build/__tests__/partitions.spec.ts +0 -110
- package/src/build/__tests__/workspace-build.spec.ts +0 -215
- package/src/build/bundler.ts +0 -256
- package/src/build/endpoint-analyzer.ts +0 -236
- package/src/build/handler-templates.ts +0 -1253
- package/src/build/index.ts +0 -616
- package/src/build/manifests.ts +0 -285
- package/src/build/partitions.ts +0 -58
- package/src/build/providerResolver.ts +0 -184
- package/src/build/types.ts +0 -104
- package/src/config.ts +0 -330
- package/src/credentials/__tests__/fullDockerPorts.spec.ts +0 -144
- package/src/credentials/__tests__/helpers.ts +0 -112
- package/src/credentials/__tests__/prepareEntryCredentials.spec.ts +0 -125
- package/src/credentials/__tests__/readonlyDockerPorts.spec.ts +0 -190
- package/src/credentials/__tests__/workspaceCredentials.spec.ts +0 -209
- package/src/credentials/index.ts +0 -843
- package/src/debug.ts +0 -68
- package/src/deploy/CachedStateProvider.ts +0 -86
- package/src/deploy/LocalStateProvider.ts +0 -57
- package/src/deploy/SSMStateProvider.ts +0 -117
- package/src/deploy/StateProvider.ts +0 -175
- package/src/deploy/__tests__/CachedStateProvider.spec.ts +0 -235
- package/src/deploy/__tests__/HostingerProvider.spec.ts +0 -347
- package/src/deploy/__tests__/LocalStateProvider.spec.ts +0 -130
- package/src/deploy/__tests__/Route53Provider.spec.ts +0 -425
- package/src/deploy/__tests__/SSMStateProvider.spec.ts +0 -201
- package/src/deploy/__tests__/__fixtures__/entry-apps/async-entry.ts +0 -24
- package/src/deploy/__tests__/__fixtures__/entry-apps/nested-config-entry.ts +0 -24
- package/src/deploy/__tests__/__fixtures__/entry-apps/no-env-entry.ts +0 -12
- package/src/deploy/__tests__/__fixtures__/entry-apps/simple-entry.ts +0 -14
- package/src/deploy/__tests__/__fixtures__/entry-apps/throwing-entry.ts +0 -16
- package/src/deploy/__tests__/__fixtures__/env-parsers/non-function-export.ts +0 -10
- package/src/deploy/__tests__/__fixtures__/env-parsers/optional-env-parser.ts +0 -21
- package/src/deploy/__tests__/__fixtures__/env-parsers/parseable-env-parser.ts +0 -18
- package/src/deploy/__tests__/__fixtures__/env-parsers/throwing-env-parser.ts +0 -16
- package/src/deploy/__tests__/__fixtures__/env-parsers/valid-env-parser.ts +0 -16
- package/src/deploy/__tests__/__fixtures__/route-apps/endpoints/auth.ts +0 -16
- package/src/deploy/__tests__/__fixtures__/route-apps/endpoints/health.ts +0 -13
- package/src/deploy/__tests__/__fixtures__/route-apps/endpoints/users.ts +0 -15
- package/src/deploy/__tests__/__fixtures__/route-apps/services.ts +0 -55
- package/src/deploy/__tests__/backup-provisioner.spec.ts +0 -428
- package/src/deploy/__tests__/createDnsProvider.spec.ts +0 -195
- package/src/deploy/__tests__/createStateProvider.spec.ts +0 -126
- package/src/deploy/__tests__/dns-orchestration.spec.ts +0 -192
- package/src/deploy/__tests__/dns-verification.spec.ts +0 -229
- package/src/deploy/__tests__/docker.spec.ts +0 -149
- package/src/deploy/__tests__/dokploy-api.spec.ts +0 -697
- package/src/deploy/__tests__/dokploy.spec.ts +0 -435
- package/src/deploy/__tests__/domain.spec.ts +0 -235
- package/src/deploy/__tests__/env-resolver.spec.ts +0 -903
- package/src/deploy/__tests__/index.spec.ts +0 -1091
- package/src/deploy/__tests__/init.spec.ts +0 -793
- package/src/deploy/__tests__/secrets.spec.ts +0 -303
- package/src/deploy/__tests__/sniffer.spec.ts +0 -838
- package/src/deploy/__tests__/state-e2e.spec.ts +0 -387
- package/src/deploy/__tests__/state.spec.ts +0 -876
- package/src/deploy/__tests__/undeploy.spec.ts +0 -758
- package/src/deploy/backup-provisioner.ts +0 -316
- package/src/deploy/dns/DnsProvider.ts +0 -201
- package/src/deploy/dns/HostingerProvider.ts +0 -174
- package/src/deploy/dns/Route53Provider.ts +0 -340
- package/src/deploy/dns/hostinger-api.ts +0 -261
- package/src/deploy/dns/index.ts +0 -627
- package/src/deploy/docker.ts +0 -260
- package/src/deploy/dokploy-api.ts +0 -1101
- package/src/deploy/dokploy.ts +0 -177
- package/src/deploy/domain.ts +0 -134
- package/src/deploy/env-resolver.ts +0 -305
- package/src/deploy/index.ts +0 -2094
- package/src/deploy/init.ts +0 -427
- package/src/deploy/secrets.ts +0 -187
- package/src/deploy/sniffer-envkit-patch.ts +0 -61
- package/src/deploy/sniffer-hooks.ts +0 -57
- package/src/deploy/sniffer-loader.ts +0 -28
- package/src/deploy/sniffer-routes-worker.ts +0 -104
- package/src/deploy/sniffer-worker.ts +0 -75
- package/src/deploy/sniffer.ts +0 -578
- package/src/deploy/state-commands.ts +0 -274
- package/src/deploy/state.ts +0 -482
- package/src/deploy/types.ts +0 -102
- package/src/deploy/undeploy.ts +0 -407
- package/src/dev/__tests__/entry-integration.spec.ts +0 -347
- package/src/dev/__tests__/entry.spec.ts +0 -144
- package/src/dev/__tests__/index.spec.ts +0 -2084
- package/src/dev/index.ts +0 -1649
- package/src/docker/__tests__/compose.spec.ts +0 -1142
- package/src/docker/__tests__/templates.spec.ts +0 -694
- package/src/docker/compose.ts +0 -808
- package/src/docker/index.ts +0 -523
- package/src/docker/templates.ts +0 -1067
- package/src/exec/index.ts +0 -118
- package/src/generators/CronGenerator.ts +0 -105
- package/src/generators/EndpointGenerator.ts +0 -954
- package/src/generators/FunctionGenerator.ts +0 -117
- package/src/generators/Generator.ts +0 -147
- package/src/generators/OpenApiTsGenerator.ts +0 -821
- package/src/generators/QueueGenerator.ts +0 -245
- package/src/generators/SubscriberGenerator.ts +0 -282
- package/src/generators/TopicGenerator.ts +0 -45
- package/src/generators/__tests__/CronGenerator.spec.ts +0 -445
- package/src/generators/__tests__/EndpointGenerator.spec.ts +0 -544
- package/src/generators/__tests__/FunctionGenerator.spec.ts +0 -256
- package/src/generators/__tests__/Generator.bustCache.spec.ts +0 -72
- package/src/generators/__tests__/OpenApiTsGenerator.registry.spec.ts +0 -98
- package/src/generators/__tests__/OpenApiTsGenerator.responseType.spec.ts +0 -56
- package/src/generators/__tests__/QueueGenerator.spec.ts +0 -141
- package/src/generators/__tests__/SubscriberGenerator.spec.ts +0 -410
- package/src/generators/index.ts +0 -11
- package/src/index.ts +0 -972
- package/src/init/__tests__/generators.spec.ts +0 -1070
- package/src/init/__tests__/init.spec.ts +0 -577
- package/src/init/__tests__/utils.spec.ts +0 -104
- package/src/init/generators/auth.ts +0 -225
- package/src/init/generators/config.ts +0 -316
- package/src/init/generators/docker.ts +0 -388
- package/src/init/generators/env.ts +0 -62
- package/src/init/generators/index.ts +0 -6
- package/src/init/generators/mobile-expo.ts +0 -576
- package/src/init/generators/models.ts +0 -153
- package/src/init/generators/monorepo.ts +0 -475
- package/src/init/generators/package.ts +0 -95
- package/src/init/generators/source.ts +0 -15
- package/src/init/generators/test.ts +0 -178
- package/src/init/generators/ui.ts +0 -1473
- package/src/init/generators/web-tanstack.ts +0 -348
- package/src/init/generators/web.ts +0 -363
- package/src/init/index.ts +0 -542
- package/src/init/templates/api.ts +0 -404
- package/src/init/templates/index.ts +0 -342
- package/src/init/templates/minimal.ts +0 -207
- package/src/init/templates/serverless.ts +0 -148
- package/src/init/templates/worker.ts +0 -216
- package/src/init/utils.ts +0 -197
- package/src/init/versions.ts +0 -52
- package/src/openapi-react-query.ts +0 -288
- package/src/openapi.ts +0 -259
- package/src/secrets/__tests__/encryption.spec.ts +0 -226
- package/src/secrets/__tests__/generator.spec.ts +0 -575
- package/src/secrets/__tests__/index.spec.ts +0 -91
- package/src/secrets/__tests__/reconcile.spec.ts +0 -123
- package/src/secrets/__tests__/storage.spec.ts +0 -645
- package/src/secrets/encryption.ts +0 -91
- package/src/secrets/generator.ts +0 -329
- package/src/secrets/index.ts +0 -417
- package/src/secrets/keystore.ts +0 -144
- package/src/secrets/reconcile.ts +0 -53
- package/src/secrets/storage.ts +0 -337
- package/src/secrets/sync.ts +0 -136
- package/src/secrets/types.ts +0 -70
- package/src/setup/__tests__/auto-setup.spec.ts +0 -147
- package/src/setup/__tests__/reconcile-secrets.spec.ts +0 -439
- package/src/setup/fullstack-secrets.ts +0 -140
- package/src/setup/index.ts +0 -411
- package/src/test/__tests__/__fixtures__/workspace.ts +0 -104
- package/src/test/__tests__/api.spec.ts +0 -199
- package/src/test/__tests__/auth.spec.ts +0 -162
- package/src/test/__tests__/index.spec.ts +0 -502
- package/src/test/__tests__/web.spec.ts +0 -210
- package/src/test/index.ts +0 -198
- package/src/types.ts +0 -363
- package/src/upgrade/__tests__/index.spec.ts +0 -354
- package/src/upgrade/index.ts +0 -253
- package/src/workspace/__tests__/client-generator.spec.ts +0 -326
- package/src/workspace/__tests__/index.spec.ts +0 -766
- package/src/workspace/__tests__/publicEnv.spec.ts +0 -64
- package/src/workspace/__tests__/schema.spec.ts +0 -737
- package/src/workspace/__tests__/type-inference.spec.ts +0 -251
- package/src/workspace/client-generator.ts +0 -109
- package/src/workspace/index.ts +0 -433
- package/src/workspace/publicEnv.ts +0 -57
- package/src/workspace/schema.ts +0 -861
- package/src/workspace/types.ts +0 -943
- package/tsconfig.json +0 -9
- package/tsdown.config.ts +0 -22
package/src/deploy/index.ts
DELETED
|
@@ -1,2094 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Deploy Module
|
|
3
|
-
*
|
|
4
|
-
* Handles deployment of GKM workspaces to various providers (Docker, Dokploy).
|
|
5
|
-
*
|
|
6
|
-
* ## Per-App Database Credentials
|
|
7
|
-
*
|
|
8
|
-
* When deploying to Dokploy with Postgres, this module creates per-app database
|
|
9
|
-
* users with isolated schemas. This follows the same pattern as local dev mode
|
|
10
|
-
* (docker/postgres/init.sh).
|
|
11
|
-
*
|
|
12
|
-
* ### How It Works
|
|
13
|
-
*
|
|
14
|
-
* 1. **Provisioning**: Creates Postgres service with master credentials
|
|
15
|
-
* 2. **User Creation**: For each backend app that needs DATABASE_URL:
|
|
16
|
-
* - Generates a unique password (stored in deploy state)
|
|
17
|
-
* - Creates a database user with that password
|
|
18
|
-
* - Assigns schema permissions based on app name
|
|
19
|
-
* 3. **Schema Assignment**:
|
|
20
|
-
* - `api` app: Uses `public` schema (shared tables)
|
|
21
|
-
* - Other apps (e.g., `auth`): Get their own schema with `search_path` set
|
|
22
|
-
* 4. **Environment Injection**: Each app receives its own DATABASE_URL
|
|
23
|
-
*
|
|
24
|
-
* ### Security
|
|
25
|
-
*
|
|
26
|
-
* - External Postgres port is enabled only during user creation, then disabled
|
|
27
|
-
* - Each app can only access its own schema
|
|
28
|
-
* - Credentials are stored in `.gkm/deploy-{stage}.json` (gitignored)
|
|
29
|
-
* - Subsequent deploys reuse existing credentials from state
|
|
30
|
-
*
|
|
31
|
-
* ### Example Flow
|
|
32
|
-
*
|
|
33
|
-
* ```
|
|
34
|
-
* gkm deploy --stage production
|
|
35
|
-
* ├─ Create Postgres (user: postgres, db: myproject)
|
|
36
|
-
* ├─ Enable external port temporarily
|
|
37
|
-
* ├─ Create user "api" → public schema
|
|
38
|
-
* ├─ Create user "auth" → auth schema (search_path=auth)
|
|
39
|
-
* ├─ Disable external port
|
|
40
|
-
* ├─ Deploy "api" with DATABASE_URL=postgresql://api:xxx@postgres:5432/myproject
|
|
41
|
-
* └─ Deploy "auth" with DATABASE_URL=postgresql://auth:yyy@postgres:5432/myproject
|
|
42
|
-
* ```
|
|
43
|
-
*
|
|
44
|
-
* @module deploy
|
|
45
|
-
*/
|
|
46
|
-
|
|
47
|
-
import { randomBytes } from 'node:crypto';
|
|
48
|
-
import { stdin as input, stdout as output } from 'node:process';
|
|
49
|
-
import * as readline from 'node:readline/promises';
|
|
50
|
-
import { Client as PgClient } from 'pg';
|
|
51
|
-
import {
|
|
52
|
-
getDokployCredentials,
|
|
53
|
-
getDokployRegistryId,
|
|
54
|
-
storeDokployCredentials,
|
|
55
|
-
validateDokployToken,
|
|
56
|
-
} from '../auth';
|
|
57
|
-
import { storeDokployRegistryId } from '../auth/credentials';
|
|
58
|
-
import { buildCommand } from '../build/index';
|
|
59
|
-
import { type GkmConfig, loadConfig, loadWorkspaceConfig } from '../config';
|
|
60
|
-
import { readStageSecrets } from '../secrets/storage.js';
|
|
61
|
-
import {
|
|
62
|
-
getAppBuildOrder,
|
|
63
|
-
getDeployTargetError,
|
|
64
|
-
getPublicEnvPrefix,
|
|
65
|
-
isDeployTargetSupported,
|
|
66
|
-
} from '../workspace/index.js';
|
|
67
|
-
import type { NormalizedWorkspace } from '../workspace/types.js';
|
|
68
|
-
import { orchestrateDns, verifyDnsRecords } from './dns/index.js';
|
|
69
|
-
import { deployDocker, resolveDockerConfig } from './docker';
|
|
70
|
-
import { deployDokploy } from './dokploy';
|
|
71
|
-
import {
|
|
72
|
-
DokployApi,
|
|
73
|
-
type DokployApplication,
|
|
74
|
-
type DokployPostgres,
|
|
75
|
-
type DokployRedis,
|
|
76
|
-
} from './dokploy-api';
|
|
77
|
-
import { isMainFrontendApp, resolveHost } from './domain.js';
|
|
78
|
-
import {
|
|
79
|
-
type EnvResolverContext,
|
|
80
|
-
formatMissingVarsError,
|
|
81
|
-
validateEnvVars,
|
|
82
|
-
} from './env-resolver.js';
|
|
83
|
-
import { updateConfig } from './init';
|
|
84
|
-
import { createStateProvider } from './StateProvider.js';
|
|
85
|
-
import { generateSecretsReport, prepareSecretsForAllApps } from './secrets.js';
|
|
86
|
-
import { sniffAllApps } from './sniffer.js';
|
|
87
|
-
import {
|
|
88
|
-
type AppDbCredentials,
|
|
89
|
-
createEmptyState,
|
|
90
|
-
getAllAppCredentials,
|
|
91
|
-
getApplicationId,
|
|
92
|
-
getBackupState,
|
|
93
|
-
getPostgresId,
|
|
94
|
-
getRedisId,
|
|
95
|
-
setAppCredentials,
|
|
96
|
-
setApplicationId,
|
|
97
|
-
setBackupState,
|
|
98
|
-
setPostgresBackupId,
|
|
99
|
-
setPostgresId,
|
|
100
|
-
setRedisId,
|
|
101
|
-
} from './state.js';
|
|
102
|
-
import type {
|
|
103
|
-
AppDeployResult,
|
|
104
|
-
DeployOptions,
|
|
105
|
-
DeployProvider,
|
|
106
|
-
DeployResult,
|
|
107
|
-
DockerDeployConfig,
|
|
108
|
-
DokployDeployConfig,
|
|
109
|
-
WorkspaceDeployResult,
|
|
110
|
-
} from './types';
|
|
111
|
-
|
|
112
|
-
const logger = console;
|
|
113
|
-
|
|
114
|
-
/**
|
|
115
|
-
* Prompt for input
|
|
116
|
-
*/
|
|
117
|
-
async function prompt(message: string, hidden = false): Promise<string> {
|
|
118
|
-
if (!process.stdin.isTTY) {
|
|
119
|
-
throw new Error('Interactive input required. Please configure manually.');
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
if (hidden) {
|
|
123
|
-
process.stdout.write(message);
|
|
124
|
-
return new Promise((resolve) => {
|
|
125
|
-
let value = '';
|
|
126
|
-
const onData = (char: Buffer) => {
|
|
127
|
-
const c = char.toString();
|
|
128
|
-
if (c === '\n' || c === '\r') {
|
|
129
|
-
process.stdin.setRawMode(false);
|
|
130
|
-
process.stdin.pause();
|
|
131
|
-
process.stdin.removeListener('data', onData);
|
|
132
|
-
process.stdout.write('\n');
|
|
133
|
-
resolve(value);
|
|
134
|
-
} else if (c === '\u0003') {
|
|
135
|
-
process.stdin.setRawMode(false);
|
|
136
|
-
process.stdin.pause();
|
|
137
|
-
process.stdout.write('\n');
|
|
138
|
-
process.exit(1);
|
|
139
|
-
} else if (c === '\u007F' || c === '\b') {
|
|
140
|
-
if (value.length > 0) value = value.slice(0, -1);
|
|
141
|
-
} else {
|
|
142
|
-
value += c;
|
|
143
|
-
}
|
|
144
|
-
};
|
|
145
|
-
process.stdin.setRawMode(true);
|
|
146
|
-
process.stdin.resume();
|
|
147
|
-
process.stdin.on('data', onData);
|
|
148
|
-
});
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
const rl = readline.createInterface({ input, output });
|
|
152
|
-
try {
|
|
153
|
-
return await rl.question(message);
|
|
154
|
-
} finally {
|
|
155
|
-
rl.close();
|
|
156
|
-
}
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
/**
|
|
160
|
-
* Docker compose services that can be provisioned
|
|
161
|
-
*/
|
|
162
|
-
interface DockerComposeServices {
|
|
163
|
-
postgres?: boolean;
|
|
164
|
-
redis?: boolean;
|
|
165
|
-
rabbitmq?: boolean;
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
/**
|
|
169
|
-
* Service URLs including both connection URLs and individual parameters
|
|
170
|
-
*/
|
|
171
|
-
interface ServiceUrls {
|
|
172
|
-
DATABASE_URL?: string;
|
|
173
|
-
DATABASE_HOST?: string;
|
|
174
|
-
DATABASE_PORT?: string;
|
|
175
|
-
DATABASE_NAME?: string;
|
|
176
|
-
DATABASE_USER?: string;
|
|
177
|
-
DATABASE_PASSWORD?: string;
|
|
178
|
-
REDIS_URL?: string;
|
|
179
|
-
REDIS_HOST?: string;
|
|
180
|
-
REDIS_PORT?: string;
|
|
181
|
-
REDIS_PASSWORD?: string;
|
|
182
|
-
}
|
|
183
|
-
|
|
184
|
-
/**
|
|
185
|
-
* Result of Dokploy setup including provisioned service URLs
|
|
186
|
-
*/
|
|
187
|
-
interface DokploySetupResult {
|
|
188
|
-
config: DokployDeployConfig;
|
|
189
|
-
serviceUrls?: ServiceUrls;
|
|
190
|
-
}
|
|
191
|
-
|
|
192
|
-
/**
|
|
193
|
-
* Result from provisioning services
|
|
194
|
-
*/
|
|
195
|
-
export interface ProvisionServicesResult {
|
|
196
|
-
serviceUrls: ServiceUrls;
|
|
197
|
-
serviceIds: {
|
|
198
|
-
postgresId?: string;
|
|
199
|
-
redisId?: string;
|
|
200
|
-
};
|
|
201
|
-
}
|
|
202
|
-
|
|
203
|
-
/**
|
|
204
|
-
* Configuration for a database user to create during Dokploy deployment.
|
|
205
|
-
*
|
|
206
|
-
* @property name - The database username (typically matches the app name)
|
|
207
|
-
* @property password - The generated password for this user
|
|
208
|
-
* @property usePublicSchema - If true, user gets access to public schema (for api app).
|
|
209
|
-
* If false, user gets their own schema with search_path set.
|
|
210
|
-
*/
|
|
211
|
-
interface DbUserConfig {
|
|
212
|
-
name: string;
|
|
213
|
-
password: string;
|
|
214
|
-
usePublicSchema: boolean;
|
|
215
|
-
}
|
|
216
|
-
|
|
217
|
-
/**
|
|
218
|
-
* Wait for Postgres to be ready to accept connections.
|
|
219
|
-
*
|
|
220
|
-
* Polls the Postgres server until it accepts a connection or max retries reached.
|
|
221
|
-
* Used after enabling the external port to ensure the database is accessible
|
|
222
|
-
* before creating users.
|
|
223
|
-
*
|
|
224
|
-
* @param host - The Postgres server hostname
|
|
225
|
-
* @param port - The external port (typically 5432)
|
|
226
|
-
* @param user - Master database user (postgres)
|
|
227
|
-
* @param password - Master database password
|
|
228
|
-
* @param database - Database name to connect to
|
|
229
|
-
* @param maxRetries - Maximum number of connection attempts (default: 30)
|
|
230
|
-
* @param retryIntervalMs - Milliseconds between retries (default: 2000)
|
|
231
|
-
* @throws Error if Postgres is not ready after maxRetries
|
|
232
|
-
*/
|
|
233
|
-
async function waitForPostgres(
|
|
234
|
-
host: string,
|
|
235
|
-
port: number,
|
|
236
|
-
user: string,
|
|
237
|
-
password: string,
|
|
238
|
-
database: string,
|
|
239
|
-
maxRetries = 30,
|
|
240
|
-
retryIntervalMs = 2000,
|
|
241
|
-
): Promise<void> {
|
|
242
|
-
for (let i = 0; i < maxRetries; i++) {
|
|
243
|
-
try {
|
|
244
|
-
const client = new PgClient({ host, port, user, password, database });
|
|
245
|
-
await client.connect();
|
|
246
|
-
await client.end();
|
|
247
|
-
return;
|
|
248
|
-
} catch {
|
|
249
|
-
if (i < maxRetries - 1) {
|
|
250
|
-
logger.log(` Waiting for Postgres... (${i + 1}/${maxRetries})`);
|
|
251
|
-
await new Promise((r) => setTimeout(r, retryIntervalMs));
|
|
252
|
-
}
|
|
253
|
-
}
|
|
254
|
-
}
|
|
255
|
-
throw new Error(`Postgres not ready after ${maxRetries} retries`);
|
|
256
|
-
}
|
|
257
|
-
|
|
258
|
-
/**
|
|
259
|
-
* Initialize Postgres with per-app users and schemas.
|
|
260
|
-
*
|
|
261
|
-
* This function implements the same user/schema isolation pattern used in local
|
|
262
|
-
* dev mode (see docker/postgres/init.sh). It:
|
|
263
|
-
*
|
|
264
|
-
* 1. Temporarily enables the external Postgres port
|
|
265
|
-
* 2. Connects using master credentials
|
|
266
|
-
* 3. Creates each user with appropriate schema permissions
|
|
267
|
-
* 4. Disables the external port for security
|
|
268
|
-
*
|
|
269
|
-
* Schema assignment follows this pattern:
|
|
270
|
-
* - `api` app: Uses `public` schema (shared tables, migrations run here)
|
|
271
|
-
* - Other apps: Get their own schema with `search_path` configured
|
|
272
|
-
*
|
|
273
|
-
* @param api - The Dokploy API client
|
|
274
|
-
* @param postgres - The provisioned Postgres service details
|
|
275
|
-
* @param serverHostname - The Dokploy server hostname (for external connection)
|
|
276
|
-
* @param users - Array of users to create with their schema configuration
|
|
277
|
-
*
|
|
278
|
-
* @example
|
|
279
|
-
* ```ts
|
|
280
|
-
* await initializePostgresUsers(api, postgres, 'dokploy.example.com', [
|
|
281
|
-
* { name: 'api', password: 'xxx', usePublicSchema: true },
|
|
282
|
-
* { name: 'auth', password: 'yyy', usePublicSchema: false },
|
|
283
|
-
* ]);
|
|
284
|
-
* ```
|
|
285
|
-
*/
|
|
286
|
-
async function initializePostgresUsers(
|
|
287
|
-
api: DokployApi,
|
|
288
|
-
postgres: DokployPostgres,
|
|
289
|
-
serverHostname: string,
|
|
290
|
-
users: DbUserConfig[],
|
|
291
|
-
): Promise<void> {
|
|
292
|
-
logger.log('\n🔧 Initializing database users...');
|
|
293
|
-
|
|
294
|
-
// Enable external port temporarily
|
|
295
|
-
const externalPort = 5432;
|
|
296
|
-
logger.log(` Enabling external port ${externalPort}...`);
|
|
297
|
-
await api.savePostgresExternalPort(postgres.postgresId, externalPort);
|
|
298
|
-
|
|
299
|
-
// Redeploy to apply external port change
|
|
300
|
-
await api.deployPostgres(postgres.postgresId);
|
|
301
|
-
|
|
302
|
-
// Wait for Postgres to be ready with external port
|
|
303
|
-
logger.log(
|
|
304
|
-
` Waiting for Postgres to be accessible at ${serverHostname}:${externalPort}...`,
|
|
305
|
-
);
|
|
306
|
-
await waitForPostgres(
|
|
307
|
-
serverHostname,
|
|
308
|
-
externalPort,
|
|
309
|
-
postgres.databaseUser,
|
|
310
|
-
postgres.databasePassword,
|
|
311
|
-
postgres.databaseName,
|
|
312
|
-
);
|
|
313
|
-
|
|
314
|
-
// Connect and create users
|
|
315
|
-
const client = new PgClient({
|
|
316
|
-
host: serverHostname,
|
|
317
|
-
port: externalPort,
|
|
318
|
-
user: postgres.databaseUser,
|
|
319
|
-
password: postgres.databasePassword,
|
|
320
|
-
database: postgres.databaseName,
|
|
321
|
-
});
|
|
322
|
-
|
|
323
|
-
try {
|
|
324
|
-
await client.connect();
|
|
325
|
-
|
|
326
|
-
for (const user of users) {
|
|
327
|
-
const schemaName = user.usePublicSchema ? 'public' : user.name;
|
|
328
|
-
logger.log(
|
|
329
|
-
` Creating user "${user.name}" with schema "${schemaName}"...`,
|
|
330
|
-
);
|
|
331
|
-
|
|
332
|
-
// Create or update user with all settings in one DO block
|
|
333
|
-
// This avoids "tuple already updated by self" errors from multiple ALTER USER calls
|
|
334
|
-
if (user.usePublicSchema) {
|
|
335
|
-
// API uses public schema
|
|
336
|
-
await client.query(`
|
|
337
|
-
DO $$ BEGIN
|
|
338
|
-
IF NOT EXISTS (SELECT FROM pg_roles WHERE rolname = '${user.name}') THEN
|
|
339
|
-
CREATE USER "${user.name}" WITH PASSWORD '${user.password}';
|
|
340
|
-
ELSE
|
|
341
|
-
ALTER USER "${user.name}" WITH PASSWORD '${user.password}';
|
|
342
|
-
END IF;
|
|
343
|
-
END $$;
|
|
344
|
-
`);
|
|
345
|
-
await client.query(`
|
|
346
|
-
GRANT ALL ON SCHEMA public TO "${user.name}";
|
|
347
|
-
ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT ALL ON TABLES TO "${user.name}";
|
|
348
|
-
ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT ALL ON SEQUENCES TO "${user.name}";
|
|
349
|
-
`);
|
|
350
|
-
} else {
|
|
351
|
-
// Other apps get their own schema - combine user creation and search_path in one block
|
|
352
|
-
await client.query(`
|
|
353
|
-
DO $$ BEGIN
|
|
354
|
-
IF NOT EXISTS (SELECT FROM pg_roles WHERE rolname = '${user.name}') THEN
|
|
355
|
-
CREATE USER "${user.name}" WITH PASSWORD '${user.password}';
|
|
356
|
-
ELSE
|
|
357
|
-
ALTER USER "${user.name}" WITH PASSWORD '${user.password}';
|
|
358
|
-
END IF;
|
|
359
|
-
-- Set search_path in same transaction to avoid tuple conflict
|
|
360
|
-
ALTER USER "${user.name}" SET search_path TO "${schemaName}";
|
|
361
|
-
END $$;
|
|
362
|
-
`);
|
|
363
|
-
await client.query(`
|
|
364
|
-
CREATE SCHEMA IF NOT EXISTS "${schemaName}" AUTHORIZATION "${user.name}";
|
|
365
|
-
GRANT USAGE ON SCHEMA "${schemaName}" TO "${user.name}";
|
|
366
|
-
GRANT ALL ON ALL TABLES IN SCHEMA "${schemaName}" TO "${user.name}";
|
|
367
|
-
ALTER DEFAULT PRIVILEGES IN SCHEMA "${schemaName}" GRANT ALL ON TABLES TO "${user.name}";
|
|
368
|
-
`);
|
|
369
|
-
}
|
|
370
|
-
|
|
371
|
-
logger.log(` ✓ User "${user.name}" configured`);
|
|
372
|
-
}
|
|
373
|
-
} finally {
|
|
374
|
-
await client.end();
|
|
375
|
-
}
|
|
376
|
-
|
|
377
|
-
// Disable external port for security
|
|
378
|
-
logger.log(' Disabling external port...');
|
|
379
|
-
await api.savePostgresExternalPort(postgres.postgresId, null);
|
|
380
|
-
await api.deployPostgres(postgres.postgresId);
|
|
381
|
-
|
|
382
|
-
logger.log(' ✓ Database users initialized');
|
|
383
|
-
}
|
|
384
|
-
|
|
385
|
-
/**
|
|
386
|
-
* Get the server hostname from the Dokploy endpoint URL
|
|
387
|
-
*/
|
|
388
|
-
function getServerHostname(endpoint: string): string {
|
|
389
|
-
const url = new URL(endpoint);
|
|
390
|
-
return url.hostname;
|
|
391
|
-
}
|
|
392
|
-
|
|
393
|
-
/**
|
|
394
|
-
* Build per-app DATABASE_URL for internal Docker network communication.
|
|
395
|
-
*
|
|
396
|
-
* The URL uses the Postgres container name (postgresAppName) as the host,
|
|
397
|
-
* which resolves via Docker's internal DNS when apps are in the same network.
|
|
398
|
-
*
|
|
399
|
-
* @param appName - The database username (matches the app name)
|
|
400
|
-
* @param appPassword - The app's database password
|
|
401
|
-
* @param postgresAppName - The Postgres container/service name in Dokploy
|
|
402
|
-
* @param databaseName - The database name (typically the project name)
|
|
403
|
-
* @returns A properly encoded PostgreSQL connection URL
|
|
404
|
-
*
|
|
405
|
-
* @example
|
|
406
|
-
* ```ts
|
|
407
|
-
* const url = buildPerAppDatabaseUrl('api', 'secret123', 'postgres-abc', 'myproject');
|
|
408
|
-
* // Returns: postgresql://api:secret123@postgres-abc:5432/myproject
|
|
409
|
-
* ```
|
|
410
|
-
*/
|
|
411
|
-
function _buildPerAppDatabaseUrl(
|
|
412
|
-
appName: string,
|
|
413
|
-
appPassword: string,
|
|
414
|
-
postgresAppName: string,
|
|
415
|
-
databaseName: string,
|
|
416
|
-
): string {
|
|
417
|
-
return `postgresql://${appName}:${encodeURIComponent(appPassword)}@${postgresAppName}:5432/${databaseName}`;
|
|
418
|
-
}
|
|
419
|
-
|
|
420
|
-
/**
|
|
421
|
-
* Provision docker compose services in Dokploy
|
|
422
|
-
* @internal Exported for testing
|
|
423
|
-
*/
|
|
424
|
-
export async function provisionServices(
|
|
425
|
-
api: DokployApi,
|
|
426
|
-
projectId: string,
|
|
427
|
-
environmentId: string | undefined,
|
|
428
|
-
projectName: string,
|
|
429
|
-
services?: DockerComposeServices,
|
|
430
|
-
existingServiceIds?: { postgresId?: string; redisId?: string },
|
|
431
|
-
): Promise<ProvisionServicesResult | undefined> {
|
|
432
|
-
logger.log(
|
|
433
|
-
`\n🔍 provisionServices called: services=${JSON.stringify(services)}, envId=${environmentId}`,
|
|
434
|
-
);
|
|
435
|
-
if (!services || !environmentId) {
|
|
436
|
-
logger.log(' Skipping: no services or no environmentId');
|
|
437
|
-
return undefined;
|
|
438
|
-
}
|
|
439
|
-
|
|
440
|
-
const serviceUrls: ServiceUrls = {};
|
|
441
|
-
const serviceIds: { postgresId?: string; redisId?: string } = {};
|
|
442
|
-
|
|
443
|
-
if (services.postgres) {
|
|
444
|
-
logger.log('\n🐘 Checking PostgreSQL...');
|
|
445
|
-
const postgresName = 'db';
|
|
446
|
-
|
|
447
|
-
try {
|
|
448
|
-
let postgres: DokployPostgres | null = null;
|
|
449
|
-
let created = false;
|
|
450
|
-
|
|
451
|
-
// Check if we have an existing ID from state
|
|
452
|
-
if (existingServiceIds?.postgresId) {
|
|
453
|
-
logger.log(` Using cached ID: ${existingServiceIds.postgresId}`);
|
|
454
|
-
postgres = await api.getPostgres(existingServiceIds.postgresId);
|
|
455
|
-
if (postgres) {
|
|
456
|
-
logger.log(` ✓ PostgreSQL found: ${postgres.postgresId}`);
|
|
457
|
-
} else {
|
|
458
|
-
logger.log(` ⚠ Cached ID invalid, will create new`);
|
|
459
|
-
}
|
|
460
|
-
}
|
|
461
|
-
|
|
462
|
-
// If not found by ID, use findOrCreate
|
|
463
|
-
if (!postgres) {
|
|
464
|
-
const databasePassword = randomBytes(16).toString('hex');
|
|
465
|
-
// Use project name as database name (replace hyphens with underscores for PostgreSQL)
|
|
466
|
-
const databaseName = projectName.replace(/-/g, '_');
|
|
467
|
-
|
|
468
|
-
const result = await api.findOrCreatePostgres(
|
|
469
|
-
postgresName,
|
|
470
|
-
projectId,
|
|
471
|
-
environmentId,
|
|
472
|
-
{ databaseName, databasePassword },
|
|
473
|
-
);
|
|
474
|
-
postgres = result.postgres;
|
|
475
|
-
created = result.created;
|
|
476
|
-
|
|
477
|
-
if (created) {
|
|
478
|
-
logger.log(` ✓ Created PostgreSQL: ${postgres.postgresId}`);
|
|
479
|
-
|
|
480
|
-
// Deploy the database (only for new instances)
|
|
481
|
-
await api.deployPostgres(postgres.postgresId);
|
|
482
|
-
logger.log(' ✓ PostgreSQL deployed');
|
|
483
|
-
} else {
|
|
484
|
-
logger.log(` ✓ PostgreSQL already exists: ${postgres.postgresId}`);
|
|
485
|
-
}
|
|
486
|
-
}
|
|
487
|
-
|
|
488
|
-
// Store the ID for state
|
|
489
|
-
serviceIds.postgresId = postgres.postgresId;
|
|
490
|
-
|
|
491
|
-
// Store individual connection parameters
|
|
492
|
-
serviceUrls.DATABASE_HOST = postgres.appName;
|
|
493
|
-
serviceUrls.DATABASE_PORT = '5432';
|
|
494
|
-
serviceUrls.DATABASE_NAME = postgres.databaseName;
|
|
495
|
-
serviceUrls.DATABASE_USER = postgres.databaseUser;
|
|
496
|
-
serviceUrls.DATABASE_PASSWORD = postgres.databasePassword;
|
|
497
|
-
|
|
498
|
-
// Construct connection URL using internal docker network hostname
|
|
499
|
-
serviceUrls.DATABASE_URL = `postgresql://${postgres.databaseUser}:${postgres.databasePassword}@${postgres.appName}:5432/${postgres.databaseName}`;
|
|
500
|
-
logger.log(` ✓ Database credentials configured`);
|
|
501
|
-
} catch (error) {
|
|
502
|
-
const message = error instanceof Error ? error.message : 'Unknown error';
|
|
503
|
-
logger.log(` ⚠ Failed to provision PostgreSQL: ${message}`);
|
|
504
|
-
}
|
|
505
|
-
}
|
|
506
|
-
|
|
507
|
-
if (services.redis) {
|
|
508
|
-
logger.log('\n🔴 Checking Redis...');
|
|
509
|
-
const redisName = 'cache';
|
|
510
|
-
|
|
511
|
-
try {
|
|
512
|
-
let redis: DokployRedis | null = null;
|
|
513
|
-
let created = false;
|
|
514
|
-
|
|
515
|
-
// Check if we have an existing ID from state
|
|
516
|
-
if (existingServiceIds?.redisId) {
|
|
517
|
-
logger.log(` Using cached ID: ${existingServiceIds.redisId}`);
|
|
518
|
-
redis = await api.getRedis(existingServiceIds.redisId);
|
|
519
|
-
if (redis) {
|
|
520
|
-
logger.log(` ✓ Redis found: ${redis.redisId}`);
|
|
521
|
-
} else {
|
|
522
|
-
logger.log(` ⚠ Cached ID invalid, will create new`);
|
|
523
|
-
}
|
|
524
|
-
}
|
|
525
|
-
|
|
526
|
-
// If not found by ID, use findOrCreate
|
|
527
|
-
if (!redis) {
|
|
528
|
-
const { randomBytes } = await import('node:crypto');
|
|
529
|
-
const databasePassword = randomBytes(16).toString('hex');
|
|
530
|
-
|
|
531
|
-
const result = await api.findOrCreateRedis(
|
|
532
|
-
redisName,
|
|
533
|
-
projectId,
|
|
534
|
-
environmentId,
|
|
535
|
-
{ databasePassword },
|
|
536
|
-
);
|
|
537
|
-
redis = result.redis;
|
|
538
|
-
created = result.created;
|
|
539
|
-
|
|
540
|
-
if (created) {
|
|
541
|
-
logger.log(` ✓ Created Redis: ${redis.redisId}`);
|
|
542
|
-
|
|
543
|
-
// Deploy the redis instance (only for new instances)
|
|
544
|
-
await api.deployRedis(redis.redisId);
|
|
545
|
-
logger.log(' ✓ Redis deployed');
|
|
546
|
-
} else {
|
|
547
|
-
logger.log(` ✓ Redis already exists: ${redis.redisId}`);
|
|
548
|
-
}
|
|
549
|
-
}
|
|
550
|
-
|
|
551
|
-
// Store the ID for state
|
|
552
|
-
serviceIds.redisId = redis.redisId;
|
|
553
|
-
|
|
554
|
-
// Store individual connection parameters
|
|
555
|
-
serviceUrls.REDIS_HOST = redis.appName;
|
|
556
|
-
serviceUrls.REDIS_PORT = '6379';
|
|
557
|
-
if (redis.databasePassword) {
|
|
558
|
-
serviceUrls.REDIS_PASSWORD = redis.databasePassword;
|
|
559
|
-
}
|
|
560
|
-
|
|
561
|
-
// Construct connection URL
|
|
562
|
-
const password = redis.databasePassword
|
|
563
|
-
? `:${redis.databasePassword}@`
|
|
564
|
-
: '';
|
|
565
|
-
serviceUrls.REDIS_URL = `redis://${password}${redis.appName}:6379`;
|
|
566
|
-
logger.log(` ✓ Redis credentials configured`);
|
|
567
|
-
} catch (error) {
|
|
568
|
-
const message = error instanceof Error ? error.message : 'Unknown error';
|
|
569
|
-
logger.log(` ⚠ Failed to provision Redis: ${message}`);
|
|
570
|
-
}
|
|
571
|
-
}
|
|
572
|
-
|
|
573
|
-
return Object.keys(serviceUrls).length > 0
|
|
574
|
-
? { serviceUrls, serviceIds }
|
|
575
|
-
: undefined;
|
|
576
|
-
}
|
|
577
|
-
|
|
578
|
-
/**
|
|
579
|
-
* Ensure Dokploy is fully configured, recovering/creating resources as needed
|
|
580
|
-
*/
|
|
581
|
-
async function ensureDokploySetup(
|
|
582
|
-
config: GkmConfig,
|
|
583
|
-
dockerConfig: DockerDeployConfig,
|
|
584
|
-
stage: string,
|
|
585
|
-
services?: DockerComposeServices,
|
|
586
|
-
): Promise<DokploySetupResult> {
|
|
587
|
-
logger.log('\n🔧 Checking Dokploy setup...');
|
|
588
|
-
|
|
589
|
-
// Step 1: Ensure we have Dokploy credentials
|
|
590
|
-
let creds = await getDokployCredentials();
|
|
591
|
-
|
|
592
|
-
if (!creds) {
|
|
593
|
-
logger.log("\n📋 Dokploy credentials not found. Let's set them up.");
|
|
594
|
-
const endpoint = await prompt(
|
|
595
|
-
'Dokploy URL (e.g., https://dokploy.example.com): ',
|
|
596
|
-
);
|
|
597
|
-
const normalizedEndpoint = endpoint.replace(/\/$/, '');
|
|
598
|
-
|
|
599
|
-
try {
|
|
600
|
-
new URL(normalizedEndpoint);
|
|
601
|
-
} catch {
|
|
602
|
-
throw new Error('Invalid URL format');
|
|
603
|
-
}
|
|
604
|
-
|
|
605
|
-
logger.log(
|
|
606
|
-
`\nGenerate a token at: ${normalizedEndpoint}/settings/profile\n`,
|
|
607
|
-
);
|
|
608
|
-
const token = await prompt('API Token: ', true);
|
|
609
|
-
|
|
610
|
-
logger.log('\nValidating credentials...');
|
|
611
|
-
const isValid = await validateDokployToken(normalizedEndpoint, token);
|
|
612
|
-
if (!isValid) {
|
|
613
|
-
throw new Error('Invalid credentials. Please check your token.');
|
|
614
|
-
}
|
|
615
|
-
|
|
616
|
-
await storeDokployCredentials(token, normalizedEndpoint);
|
|
617
|
-
creds = { token, endpoint: normalizedEndpoint };
|
|
618
|
-
logger.log('✓ Credentials saved');
|
|
619
|
-
}
|
|
620
|
-
|
|
621
|
-
const api = new DokployApi({ baseUrl: creds.endpoint, token: creds.token });
|
|
622
|
-
|
|
623
|
-
// Step 2: Check if we have config in gkm.config.ts
|
|
624
|
-
const existingConfig = config.providers?.dokploy;
|
|
625
|
-
if (
|
|
626
|
-
existingConfig &&
|
|
627
|
-
typeof existingConfig !== 'boolean' &&
|
|
628
|
-
existingConfig.applicationId &&
|
|
629
|
-
existingConfig.projectId
|
|
630
|
-
) {
|
|
631
|
-
logger.log('✓ Dokploy config found in gkm.config.ts');
|
|
632
|
-
|
|
633
|
-
// Verify the application still exists
|
|
634
|
-
try {
|
|
635
|
-
const projectDetails = await api.getProject(existingConfig.projectId);
|
|
636
|
-
logger.log('✓ Project verified');
|
|
637
|
-
|
|
638
|
-
// Get registry ID from config first, then from local storage
|
|
639
|
-
const storedRegistryId =
|
|
640
|
-
existingConfig.registryId ?? (await getDokployRegistryId());
|
|
641
|
-
|
|
642
|
-
// Get environment ID for service provisioning (match by stage name)
|
|
643
|
-
const environments = projectDetails.environments ?? [];
|
|
644
|
-
let environment = environments.find(
|
|
645
|
-
(e) => e.name.toLowerCase() === stage.toLowerCase(),
|
|
646
|
-
);
|
|
647
|
-
|
|
648
|
-
// Create environment if it doesn't exist for this stage
|
|
649
|
-
if (!environment) {
|
|
650
|
-
logger.log(` Creating "${stage}" environment...`);
|
|
651
|
-
environment = await api.createEnvironment(
|
|
652
|
-
existingConfig.projectId,
|
|
653
|
-
stage,
|
|
654
|
-
);
|
|
655
|
-
logger.log(` ✓ Created environment: ${environment.environmentId}`);
|
|
656
|
-
}
|
|
657
|
-
|
|
658
|
-
const environmentId = environment.environmentId;
|
|
659
|
-
|
|
660
|
-
// Provision services if configured
|
|
661
|
-
logger.log(
|
|
662
|
-
` Services config: ${JSON.stringify(services)}, envId: ${environmentId}`,
|
|
663
|
-
);
|
|
664
|
-
// For single-app mode, we don't have state persistence yet, so pass undefined
|
|
665
|
-
const provisionResult = await provisionServices(
|
|
666
|
-
api,
|
|
667
|
-
existingConfig.projectId,
|
|
668
|
-
environmentId,
|
|
669
|
-
dockerConfig.appName!,
|
|
670
|
-
services,
|
|
671
|
-
undefined, // No state in single-app mode
|
|
672
|
-
);
|
|
673
|
-
|
|
674
|
-
return {
|
|
675
|
-
config: {
|
|
676
|
-
endpoint: existingConfig.endpoint,
|
|
677
|
-
projectId: existingConfig.projectId,
|
|
678
|
-
applicationId: existingConfig.applicationId,
|
|
679
|
-
registry: existingConfig.registry,
|
|
680
|
-
registryId: storedRegistryId ?? undefined,
|
|
681
|
-
},
|
|
682
|
-
serviceUrls: provisionResult?.serviceUrls,
|
|
683
|
-
};
|
|
684
|
-
} catch {
|
|
685
|
-
logger.log('⚠ Project not found, will recover...');
|
|
686
|
-
}
|
|
687
|
-
}
|
|
688
|
-
|
|
689
|
-
// Step 3: Find or create project
|
|
690
|
-
logger.log('\n📁 Looking for project...');
|
|
691
|
-
const projectName = dockerConfig.projectName!;
|
|
692
|
-
const projects = await api.listProjects();
|
|
693
|
-
let project = projects.find(
|
|
694
|
-
(p) => p.name.toLowerCase() === projectName.toLowerCase(),
|
|
695
|
-
);
|
|
696
|
-
|
|
697
|
-
let environmentId: string;
|
|
698
|
-
|
|
699
|
-
if (project) {
|
|
700
|
-
logger.log(
|
|
701
|
-
` Found existing project: ${project.name} (${project.projectId})`,
|
|
702
|
-
);
|
|
703
|
-
|
|
704
|
-
// Step 4: Get or create environment for existing project (match by stage)
|
|
705
|
-
const projectDetails = await api.getProject(project.projectId);
|
|
706
|
-
const environments = projectDetails.environments ?? [];
|
|
707
|
-
const matchingEnv = environments.find(
|
|
708
|
-
(e) => e.name.toLowerCase() === stage.toLowerCase(),
|
|
709
|
-
);
|
|
710
|
-
if (matchingEnv) {
|
|
711
|
-
environmentId = matchingEnv.environmentId;
|
|
712
|
-
logger.log(` Using environment: ${matchingEnv.name}`);
|
|
713
|
-
} else {
|
|
714
|
-
logger.log(` Creating "${stage}" environment...`);
|
|
715
|
-
const env = await api.createEnvironment(project.projectId, stage);
|
|
716
|
-
environmentId = env.environmentId;
|
|
717
|
-
logger.log(` ✓ Created environment: ${stage}`);
|
|
718
|
-
}
|
|
719
|
-
} else {
|
|
720
|
-
logger.log(` Creating project: ${projectName}`);
|
|
721
|
-
const result = await api.createProject(projectName);
|
|
722
|
-
project = result.project;
|
|
723
|
-
// Rename the default environment to match stage if different
|
|
724
|
-
if (result.environment.name.toLowerCase() !== stage.toLowerCase()) {
|
|
725
|
-
logger.log(` Creating "${stage}" environment...`);
|
|
726
|
-
const env = await api.createEnvironment(project.projectId, stage);
|
|
727
|
-
environmentId = env.environmentId;
|
|
728
|
-
} else {
|
|
729
|
-
environmentId = result.environment.environmentId;
|
|
730
|
-
}
|
|
731
|
-
logger.log(` ✓ Created project: ${project.projectId}`);
|
|
732
|
-
logger.log(` ✓ Using environment: ${stage}`);
|
|
733
|
-
}
|
|
734
|
-
|
|
735
|
-
// Step 5: Find or create application
|
|
736
|
-
logger.log('\n📦 Looking for application...');
|
|
737
|
-
const appName = dockerConfig.appName!;
|
|
738
|
-
|
|
739
|
-
let applicationId: string;
|
|
740
|
-
|
|
741
|
-
// Try to find existing app from config
|
|
742
|
-
if (
|
|
743
|
-
existingConfig &&
|
|
744
|
-
typeof existingConfig !== 'boolean' &&
|
|
745
|
-
existingConfig.applicationId
|
|
746
|
-
) {
|
|
747
|
-
applicationId = existingConfig.applicationId;
|
|
748
|
-
logger.log(` Using application from config: ${applicationId}`);
|
|
749
|
-
} else {
|
|
750
|
-
// Create new application
|
|
751
|
-
logger.log(` Creating application: ${appName}`);
|
|
752
|
-
const app = await api.createApplication(
|
|
753
|
-
appName,
|
|
754
|
-
project.projectId,
|
|
755
|
-
environmentId,
|
|
756
|
-
);
|
|
757
|
-
applicationId = app.applicationId;
|
|
758
|
-
logger.log(` ✓ Created application: ${applicationId}`);
|
|
759
|
-
}
|
|
760
|
-
|
|
761
|
-
// Step 6: Ensure registry is set up
|
|
762
|
-
logger.log('\n🐳 Checking registry...');
|
|
763
|
-
let registryId = await getDokployRegistryId();
|
|
764
|
-
|
|
765
|
-
if (registryId) {
|
|
766
|
-
// Verify stored registry still exists
|
|
767
|
-
try {
|
|
768
|
-
const registry = await api.getRegistry(registryId);
|
|
769
|
-
logger.log(` Using registry: ${registry.registryName}`);
|
|
770
|
-
} catch {
|
|
771
|
-
logger.log(' ⚠ Stored registry not found, clearing...');
|
|
772
|
-
registryId = undefined;
|
|
773
|
-
await storeDokployRegistryId('');
|
|
774
|
-
}
|
|
775
|
-
}
|
|
776
|
-
|
|
777
|
-
if (!registryId) {
|
|
778
|
-
const registries = await api.listRegistries();
|
|
779
|
-
|
|
780
|
-
if (registries.length === 0) {
|
|
781
|
-
// No registries exist
|
|
782
|
-
if (dockerConfig.registry) {
|
|
783
|
-
logger.log(" No registries found in Dokploy. Let's create one.");
|
|
784
|
-
logger.log(` Registry URL: ${dockerConfig.registry}`);
|
|
785
|
-
|
|
786
|
-
const username = await prompt('Registry username: ');
|
|
787
|
-
const password = await prompt('Registry password/token: ', true);
|
|
788
|
-
|
|
789
|
-
const registry = await api.createRegistry(
|
|
790
|
-
'Default Registry',
|
|
791
|
-
dockerConfig.registry,
|
|
792
|
-
username,
|
|
793
|
-
password,
|
|
794
|
-
);
|
|
795
|
-
registryId = registry.registryId;
|
|
796
|
-
await storeDokployRegistryId(registryId);
|
|
797
|
-
logger.log(` ✓ Registry created: ${registryId}`);
|
|
798
|
-
} else {
|
|
799
|
-
logger.log(
|
|
800
|
-
' ⚠ No registry configured. Set docker.registry in gkm.config.ts',
|
|
801
|
-
);
|
|
802
|
-
}
|
|
803
|
-
} else {
|
|
804
|
-
// Show available registries and let user select or create new
|
|
805
|
-
logger.log(' Available registries:');
|
|
806
|
-
registries.forEach((reg, i) => {
|
|
807
|
-
logger.log(` ${i + 1}. ${reg.registryName} (${reg.registryUrl})`);
|
|
808
|
-
});
|
|
809
|
-
if (dockerConfig.registry) {
|
|
810
|
-
logger.log(` ${registries.length + 1}. Create new registry`);
|
|
811
|
-
}
|
|
812
|
-
|
|
813
|
-
const maxOption = dockerConfig.registry
|
|
814
|
-
? registries.length + 1
|
|
815
|
-
: registries.length;
|
|
816
|
-
const selection = await prompt(` Select registry (1-${maxOption}): `);
|
|
817
|
-
const index = parseInt(selection, 10) - 1;
|
|
818
|
-
|
|
819
|
-
if (index >= 0 && index < registries.length) {
|
|
820
|
-
// Selected existing registry
|
|
821
|
-
registryId = registries[index]!.registryId;
|
|
822
|
-
await storeDokployRegistryId(registryId);
|
|
823
|
-
logger.log(` ✓ Selected: ${registries[index]!.registryName}`);
|
|
824
|
-
} else if (dockerConfig.registry && index === registries.length) {
|
|
825
|
-
// Create new registry
|
|
826
|
-
logger.log(`\n Creating new registry...`);
|
|
827
|
-
logger.log(` Registry URL: ${dockerConfig.registry}`);
|
|
828
|
-
|
|
829
|
-
const username = await prompt(' Registry username: ');
|
|
830
|
-
const password = await prompt(' Registry password/token: ', true);
|
|
831
|
-
|
|
832
|
-
const registry = await api.createRegistry(
|
|
833
|
-
dockerConfig.registry.replace(/^https?:\/\//, ''),
|
|
834
|
-
dockerConfig.registry,
|
|
835
|
-
username,
|
|
836
|
-
password,
|
|
837
|
-
);
|
|
838
|
-
registryId = registry.registryId;
|
|
839
|
-
await storeDokployRegistryId(registryId);
|
|
840
|
-
logger.log(` ✓ Registry created: ${registryId}`);
|
|
841
|
-
} else {
|
|
842
|
-
logger.log(' ⚠ Invalid selection, skipping registry setup');
|
|
843
|
-
}
|
|
844
|
-
}
|
|
845
|
-
}
|
|
846
|
-
|
|
847
|
-
// Step 7: Build and save config
|
|
848
|
-
const dokployConfig: DokployDeployConfig = {
|
|
849
|
-
endpoint: creds.endpoint,
|
|
850
|
-
projectId: project.projectId,
|
|
851
|
-
applicationId,
|
|
852
|
-
registryId: registryId ?? undefined,
|
|
853
|
-
};
|
|
854
|
-
|
|
855
|
-
// Update gkm.config.ts
|
|
856
|
-
await updateConfig(dokployConfig);
|
|
857
|
-
|
|
858
|
-
logger.log('\n✅ Dokploy setup complete!');
|
|
859
|
-
logger.log(` Project: ${project.projectId}`);
|
|
860
|
-
logger.log(` Application: ${applicationId}`);
|
|
861
|
-
if (registryId) {
|
|
862
|
-
logger.log(` Registry: ${registryId}`);
|
|
863
|
-
}
|
|
864
|
-
|
|
865
|
-
// Step 8: Provision docker compose services if configured
|
|
866
|
-
// For single-app mode, we don't have state persistence yet, so pass undefined
|
|
867
|
-
const provisionResult = await provisionServices(
|
|
868
|
-
api,
|
|
869
|
-
project.projectId,
|
|
870
|
-
environmentId,
|
|
871
|
-
dockerConfig.appName!,
|
|
872
|
-
services,
|
|
873
|
-
undefined, // No state in single-app mode
|
|
874
|
-
);
|
|
875
|
-
|
|
876
|
-
return {
|
|
877
|
-
config: dokployConfig,
|
|
878
|
-
serviceUrls: provisionResult?.serviceUrls,
|
|
879
|
-
};
|
|
880
|
-
}
|
|
881
|
-
|
|
882
|
-
/**
|
|
883
|
-
* Generate image tag from stage and timestamp
|
|
884
|
-
*/
|
|
885
|
-
export function generateTag(stage: string): string {
|
|
886
|
-
const timestamp = new Date().toISOString().replace(/[:.]/g, '-').slice(0, 19);
|
|
887
|
-
return `${stage}-${timestamp}`;
|
|
888
|
-
}
|
|
889
|
-
|
|
890
|
-
/**
|
|
891
|
-
* Deploy all apps in a workspace to Dokploy.
|
|
892
|
-
*
|
|
893
|
-
* Two-phase orchestration:
|
|
894
|
-
* - PHASE 1: Deploy backend apps (with encrypted secrets)
|
|
895
|
-
* - PHASE 2: Deploy frontend apps (with public URLs from backends)
|
|
896
|
-
*
|
|
897
|
-
* Security model:
|
|
898
|
-
* - Backend apps get encrypted secrets embedded at build time
|
|
899
|
-
* - Only GKM_MASTER_KEY is injected as Dokploy env var
|
|
900
|
-
* - Frontend apps get public URLs baked in at build time (no secrets)
|
|
901
|
-
*
|
|
902
|
-
* @internal Exported for testing
|
|
903
|
-
*/
|
|
904
|
-
export async function workspaceDeployCommand(
|
|
905
|
-
workspace: NormalizedWorkspace,
|
|
906
|
-
options: DeployOptions,
|
|
907
|
-
): Promise<WorkspaceDeployResult> {
|
|
908
|
-
const { provider, stage, tag, apps: selectedApps } = options;
|
|
909
|
-
|
|
910
|
-
if (provider !== 'dokploy') {
|
|
911
|
-
throw new Error(
|
|
912
|
-
`Workspace deployment only supports Dokploy. Got: ${provider}`,
|
|
913
|
-
);
|
|
914
|
-
}
|
|
915
|
-
|
|
916
|
-
logger.log(`\n🚀 Deploying workspace "${workspace.name}" to Dokploy...`);
|
|
917
|
-
logger.log(` Stage: ${stage}`);
|
|
918
|
-
|
|
919
|
-
// Generate tag if not provided
|
|
920
|
-
const imageTag = tag ?? generateTag(stage);
|
|
921
|
-
logger.log(` Tag: ${imageTag}`);
|
|
922
|
-
|
|
923
|
-
// Get apps to deploy in dependency order
|
|
924
|
-
const buildOrder = getAppBuildOrder(workspace);
|
|
925
|
-
|
|
926
|
-
// Filter to selected apps if specified
|
|
927
|
-
let appsToDeployNames = buildOrder;
|
|
928
|
-
if (selectedApps && selectedApps.length > 0) {
|
|
929
|
-
// Validate selected apps exist
|
|
930
|
-
const invalidApps = selectedApps.filter((name) => !workspace.apps[name]);
|
|
931
|
-
if (invalidApps.length > 0) {
|
|
932
|
-
throw new Error(
|
|
933
|
-
`Unknown apps: ${invalidApps.join(', ')}\n` +
|
|
934
|
-
`Available apps: ${Object.keys(workspace.apps).join(', ')}`,
|
|
935
|
-
);
|
|
936
|
-
}
|
|
937
|
-
// Keep only selected apps, but maintain dependency order
|
|
938
|
-
appsToDeployNames = buildOrder.filter((name) =>
|
|
939
|
-
selectedApps.includes(name),
|
|
940
|
-
);
|
|
941
|
-
logger.log(` Deploying apps: ${appsToDeployNames.join(', ')}`);
|
|
942
|
-
} else {
|
|
943
|
-
logger.log(` Deploying all apps: ${appsToDeployNames.join(', ')}`);
|
|
944
|
-
}
|
|
945
|
-
|
|
946
|
-
// Filter apps by deploy target
|
|
947
|
-
const dokployApps = appsToDeployNames.filter((name) => {
|
|
948
|
-
const app = workspace.apps[name]!;
|
|
949
|
-
const target = app.resolvedDeployTarget;
|
|
950
|
-
if (!isDeployTargetSupported(target)) {
|
|
951
|
-
logger.log(
|
|
952
|
-
` ⚠️ Skipping ${name}: ${getDeployTargetError(target, name)}`,
|
|
953
|
-
);
|
|
954
|
-
return false;
|
|
955
|
-
}
|
|
956
|
-
return true;
|
|
957
|
-
});
|
|
958
|
-
|
|
959
|
-
if (dokployApps.length === 0) {
|
|
960
|
-
throw new Error(
|
|
961
|
-
'No apps to deploy. All selected apps have unsupported deploy targets.',
|
|
962
|
-
);
|
|
963
|
-
}
|
|
964
|
-
|
|
965
|
-
appsToDeployNames = dokployApps;
|
|
966
|
-
|
|
967
|
-
// ==================================================================
|
|
968
|
-
// PREFLIGHT: Load secrets and sniff environment requirements
|
|
969
|
-
// ==================================================================
|
|
970
|
-
logger.log('\n🔐 Loading secrets and analyzing environment requirements...');
|
|
971
|
-
|
|
972
|
-
// Load secrets for this stage
|
|
973
|
-
const stageSecrets = await readStageSecrets(stage, workspace.root);
|
|
974
|
-
if (!stageSecrets) {
|
|
975
|
-
logger.log(` ⚠️ No secrets found for stage "${stage}"`);
|
|
976
|
-
logger.log(
|
|
977
|
-
` Run "gkm secrets:init --stage ${stage}" to create secrets`,
|
|
978
|
-
);
|
|
979
|
-
}
|
|
980
|
-
|
|
981
|
-
// Sniff environment variables for all apps
|
|
982
|
-
const sniffedApps = await sniffAllApps(workspace.apps, workspace.root);
|
|
983
|
-
|
|
984
|
-
// Prepare encrypted secrets for backend apps
|
|
985
|
-
const encryptedSecrets = stageSecrets
|
|
986
|
-
? prepareSecretsForAllApps(stageSecrets, sniffedApps)
|
|
987
|
-
: new Map();
|
|
988
|
-
|
|
989
|
-
// Report on secrets preparation
|
|
990
|
-
if (stageSecrets) {
|
|
991
|
-
const report = generateSecretsReport(encryptedSecrets, sniffedApps);
|
|
992
|
-
if (report.appsWithSecrets.length > 0) {
|
|
993
|
-
logger.log(
|
|
994
|
-
` ✓ Encrypted secrets for: ${report.appsWithSecrets.join(', ')}`,
|
|
995
|
-
);
|
|
996
|
-
}
|
|
997
|
-
if (report.appsWithMissingSecrets.length > 0) {
|
|
998
|
-
for (const { appName, missing } of report.appsWithMissingSecrets) {
|
|
999
|
-
logger.log(` ⚠️ ${appName}: Missing secrets: ${missing.join(', ')}`);
|
|
1000
|
-
}
|
|
1001
|
-
}
|
|
1002
|
-
}
|
|
1003
|
-
|
|
1004
|
-
// ==================================================================
|
|
1005
|
-
// SETUP: Credentials, Project, Registry
|
|
1006
|
-
// ==================================================================
|
|
1007
|
-
let creds = await getDokployCredentials();
|
|
1008
|
-
if (!creds) {
|
|
1009
|
-
logger.log("\n📋 Dokploy credentials not found. Let's set them up.");
|
|
1010
|
-
const endpoint = await prompt(
|
|
1011
|
-
'Dokploy URL (e.g., https://dokploy.example.com): ',
|
|
1012
|
-
);
|
|
1013
|
-
const normalizedEndpoint = endpoint.replace(/\/$/, '');
|
|
1014
|
-
|
|
1015
|
-
try {
|
|
1016
|
-
new URL(normalizedEndpoint);
|
|
1017
|
-
} catch {
|
|
1018
|
-
throw new Error('Invalid URL format');
|
|
1019
|
-
}
|
|
1020
|
-
|
|
1021
|
-
logger.log(
|
|
1022
|
-
`\nGenerate a token at: ${normalizedEndpoint}/settings/profile\n`,
|
|
1023
|
-
);
|
|
1024
|
-
const token = await prompt('API Token: ', true);
|
|
1025
|
-
|
|
1026
|
-
logger.log('\nValidating credentials...');
|
|
1027
|
-
const isValid = await validateDokployToken(normalizedEndpoint, token);
|
|
1028
|
-
if (!isValid) {
|
|
1029
|
-
throw new Error('Invalid credentials. Please check your token.');
|
|
1030
|
-
}
|
|
1031
|
-
|
|
1032
|
-
await storeDokployCredentials(token, normalizedEndpoint);
|
|
1033
|
-
creds = { token, endpoint: normalizedEndpoint };
|
|
1034
|
-
logger.log('✓ Credentials saved');
|
|
1035
|
-
}
|
|
1036
|
-
|
|
1037
|
-
const api = new DokployApi({ baseUrl: creds.endpoint, token: creds.token });
|
|
1038
|
-
|
|
1039
|
-
// Find or create project for the workspace
|
|
1040
|
-
logger.log('\n📁 Setting up Dokploy project...');
|
|
1041
|
-
const projectName = workspace.name;
|
|
1042
|
-
const projects = await api.listProjects();
|
|
1043
|
-
let project = projects.find(
|
|
1044
|
-
(p) => p.name.toLowerCase() === projectName.toLowerCase(),
|
|
1045
|
-
);
|
|
1046
|
-
|
|
1047
|
-
let environmentId: string;
|
|
1048
|
-
|
|
1049
|
-
if (project) {
|
|
1050
|
-
logger.log(` Found existing project: ${project.name}`);
|
|
1051
|
-
const projectDetails = await api.getProject(project.projectId);
|
|
1052
|
-
const environments = projectDetails.environments ?? [];
|
|
1053
|
-
const matchingEnv = environments.find(
|
|
1054
|
-
(e) => e.name.toLowerCase() === stage.toLowerCase(),
|
|
1055
|
-
);
|
|
1056
|
-
if (matchingEnv) {
|
|
1057
|
-
environmentId = matchingEnv.environmentId;
|
|
1058
|
-
logger.log(` Using environment: ${matchingEnv.name}`);
|
|
1059
|
-
} else {
|
|
1060
|
-
logger.log(` Creating "${stage}" environment...`);
|
|
1061
|
-
const env = await api.createEnvironment(project.projectId, stage);
|
|
1062
|
-
environmentId = env.environmentId;
|
|
1063
|
-
logger.log(` ✓ Created environment: ${stage}`);
|
|
1064
|
-
}
|
|
1065
|
-
} else {
|
|
1066
|
-
logger.log(` Creating project: ${projectName}`);
|
|
1067
|
-
const result = await api.createProject(projectName);
|
|
1068
|
-
project = result.project;
|
|
1069
|
-
if (result.environment.name.toLowerCase() !== stage.toLowerCase()) {
|
|
1070
|
-
logger.log(` Creating "${stage}" environment...`);
|
|
1071
|
-
const env = await api.createEnvironment(project.projectId, stage);
|
|
1072
|
-
environmentId = env.environmentId;
|
|
1073
|
-
} else {
|
|
1074
|
-
environmentId = result.environment.environmentId;
|
|
1075
|
-
}
|
|
1076
|
-
logger.log(` ✓ Created project: ${project.projectId}`);
|
|
1077
|
-
}
|
|
1078
|
-
|
|
1079
|
-
// ==================================================================
|
|
1080
|
-
// STATE: Create state provider and load deploy state
|
|
1081
|
-
// ==================================================================
|
|
1082
|
-
logger.log('\n📋 Loading deploy state...');
|
|
1083
|
-
|
|
1084
|
-
// Create state provider based on workspace config
|
|
1085
|
-
const stateProvider = await createStateProvider({
|
|
1086
|
-
config: workspace.state,
|
|
1087
|
-
workspaceRoot: workspace.root,
|
|
1088
|
-
workspaceName: workspace.name,
|
|
1089
|
-
});
|
|
1090
|
-
|
|
1091
|
-
let state = await stateProvider.read(stage);
|
|
1092
|
-
|
|
1093
|
-
if (state) {
|
|
1094
|
-
logger.log(` Found existing state for stage "${stage}"`);
|
|
1095
|
-
// Verify project ID matches (in case of recreation)
|
|
1096
|
-
if (state.projectId !== project.projectId) {
|
|
1097
|
-
logger.log(` ⚠ Project ID changed, updating state`);
|
|
1098
|
-
state.projectId = project.projectId;
|
|
1099
|
-
}
|
|
1100
|
-
// Verify environment ID matches (in case of recreation)
|
|
1101
|
-
if (state.environmentId !== environmentId) {
|
|
1102
|
-
logger.log(` ⚠ Environment ID changed, updating state`);
|
|
1103
|
-
state.environmentId = environmentId;
|
|
1104
|
-
}
|
|
1105
|
-
} else {
|
|
1106
|
-
logger.log(` Creating new state for stage "${stage}"`);
|
|
1107
|
-
state = createEmptyState(stage, project.projectId, environmentId);
|
|
1108
|
-
}
|
|
1109
|
-
|
|
1110
|
-
// Get or set up registry
|
|
1111
|
-
logger.log('\n🐳 Checking registry...');
|
|
1112
|
-
let registryId = await getDokployRegistryId();
|
|
1113
|
-
const registry = workspace.deploy.dokploy?.registry;
|
|
1114
|
-
|
|
1115
|
-
if (registryId) {
|
|
1116
|
-
try {
|
|
1117
|
-
const reg = await api.getRegistry(registryId);
|
|
1118
|
-
logger.log(` Using registry: ${reg.registryName}`);
|
|
1119
|
-
} catch {
|
|
1120
|
-
logger.log(' ⚠ Stored registry not found, clearing...');
|
|
1121
|
-
registryId = undefined;
|
|
1122
|
-
await storeDokployRegistryId('');
|
|
1123
|
-
}
|
|
1124
|
-
}
|
|
1125
|
-
|
|
1126
|
-
if (!registryId) {
|
|
1127
|
-
const registries = await api.listRegistries();
|
|
1128
|
-
if (registries.length > 0) {
|
|
1129
|
-
registryId = registries[0]!.registryId;
|
|
1130
|
-
await storeDokployRegistryId(registryId);
|
|
1131
|
-
logger.log(` Using registry: ${registries[0]!.registryName}`);
|
|
1132
|
-
} else if (registry) {
|
|
1133
|
-
logger.log(" No registries found in Dokploy. Let's create one.");
|
|
1134
|
-
logger.log(` Registry URL: ${registry}`);
|
|
1135
|
-
|
|
1136
|
-
const username = await prompt('Registry username: ');
|
|
1137
|
-
const password = await prompt('Registry password/token: ', true);
|
|
1138
|
-
|
|
1139
|
-
const reg = await api.createRegistry(
|
|
1140
|
-
'Default Registry',
|
|
1141
|
-
registry,
|
|
1142
|
-
username,
|
|
1143
|
-
password,
|
|
1144
|
-
);
|
|
1145
|
-
registryId = reg.registryId;
|
|
1146
|
-
await storeDokployRegistryId(registryId);
|
|
1147
|
-
logger.log(` ✓ Registry created: ${registryId}`);
|
|
1148
|
-
} else {
|
|
1149
|
-
logger.log(
|
|
1150
|
-
' ⚠ No registry configured. Set deploy.dokploy.registry in workspace config',
|
|
1151
|
-
);
|
|
1152
|
-
}
|
|
1153
|
-
}
|
|
1154
|
-
|
|
1155
|
-
// Provision infrastructure services if configured
|
|
1156
|
-
const services = workspace.services;
|
|
1157
|
-
const dockerServices = {
|
|
1158
|
-
postgres: services.db !== undefined && services.db !== false,
|
|
1159
|
-
redis: services.cache !== undefined && services.cache !== false,
|
|
1160
|
-
};
|
|
1161
|
-
|
|
1162
|
-
// Track provisioned postgres info for per-app DATABASE_URL
|
|
1163
|
-
let provisionedPostgres: DokployPostgres | null = null;
|
|
1164
|
-
let provisionedRedis: DokployRedis | null = null;
|
|
1165
|
-
|
|
1166
|
-
if (dockerServices.postgres || dockerServices.redis) {
|
|
1167
|
-
logger.log('\n🔧 Provisioning infrastructure services...');
|
|
1168
|
-
// Pass existing service IDs from state (prefer state over URL sniffing)
|
|
1169
|
-
const existingServiceIds = {
|
|
1170
|
-
postgresId: getPostgresId(state),
|
|
1171
|
-
redisId: getRedisId(state),
|
|
1172
|
-
};
|
|
1173
|
-
|
|
1174
|
-
const provisionResult = await provisionServices(
|
|
1175
|
-
api,
|
|
1176
|
-
project.projectId,
|
|
1177
|
-
environmentId,
|
|
1178
|
-
workspace.name,
|
|
1179
|
-
dockerServices,
|
|
1180
|
-
existingServiceIds,
|
|
1181
|
-
);
|
|
1182
|
-
|
|
1183
|
-
// Update state with returned service IDs
|
|
1184
|
-
if (provisionResult?.serviceIds) {
|
|
1185
|
-
if (provisionResult.serviceIds.postgresId) {
|
|
1186
|
-
setPostgresId(state, provisionResult.serviceIds.postgresId);
|
|
1187
|
-
// Fetch full postgres info for later use
|
|
1188
|
-
provisionedPostgres = await api.getPostgres(
|
|
1189
|
-
provisionResult.serviceIds.postgresId,
|
|
1190
|
-
);
|
|
1191
|
-
}
|
|
1192
|
-
if (provisionResult.serviceIds.redisId) {
|
|
1193
|
-
setRedisId(state, provisionResult.serviceIds.redisId);
|
|
1194
|
-
// Fetch full redis info for later use
|
|
1195
|
-
provisionedRedis = await api.getRedis(
|
|
1196
|
-
provisionResult.serviceIds.redisId,
|
|
1197
|
-
);
|
|
1198
|
-
}
|
|
1199
|
-
}
|
|
1200
|
-
}
|
|
1201
|
-
|
|
1202
|
-
// ==================================================================
|
|
1203
|
-
// Separate apps by type for two-phase deployment
|
|
1204
|
-
// ==================================================================
|
|
1205
|
-
// Mobile apps deploy via their own toolchain (e.g. EAS Build for Expo)
|
|
1206
|
-
// — skip them in the Dokploy deploy phases.
|
|
1207
|
-
const skippedMobileApps = appsToDeployNames.filter(
|
|
1208
|
-
(name) => workspace.apps[name]!.type === 'mobile',
|
|
1209
|
-
);
|
|
1210
|
-
if (skippedMobileApps.length > 0) {
|
|
1211
|
-
logger.log(
|
|
1212
|
-
`\n📱 Skipping ${skippedMobileApps.length} mobile app(s) — deploy via framework toolchain: ${skippedMobileApps.join(', ')}`,
|
|
1213
|
-
);
|
|
1214
|
-
}
|
|
1215
|
-
|
|
1216
|
-
const backendApps = appsToDeployNames.filter(
|
|
1217
|
-
(name) => workspace.apps[name]!.type === 'backend',
|
|
1218
|
-
);
|
|
1219
|
-
const frontendApps = appsToDeployNames.filter(
|
|
1220
|
-
(name) => workspace.apps[name]!.type === 'web',
|
|
1221
|
-
);
|
|
1222
|
-
|
|
1223
|
-
// ==================================================================
|
|
1224
|
-
// Initialize per-app database users if Postgres is provisioned
|
|
1225
|
-
// ==================================================================
|
|
1226
|
-
const perAppDbCredentials = new Map<string, AppDbCredentials>();
|
|
1227
|
-
|
|
1228
|
-
if (provisionedPostgres && backendApps.length > 0) {
|
|
1229
|
-
// Determine which backend apps need DATABASE_URL
|
|
1230
|
-
const appsNeedingDb = backendApps.filter((appName) => {
|
|
1231
|
-
const requirements = sniffedApps.get(appName);
|
|
1232
|
-
return requirements?.requiredEnvVars.includes('DATABASE_URL');
|
|
1233
|
-
});
|
|
1234
|
-
|
|
1235
|
-
if (appsNeedingDb.length > 0) {
|
|
1236
|
-
logger.log(`\n🔐 Setting up per-app database credentials...`);
|
|
1237
|
-
logger.log(` Apps needing DATABASE_URL: ${appsNeedingDb.join(', ')}`);
|
|
1238
|
-
|
|
1239
|
-
// Get or generate credentials for each app
|
|
1240
|
-
const existingCredentials = getAllAppCredentials(state);
|
|
1241
|
-
const usersToCreate: DbUserConfig[] = [];
|
|
1242
|
-
|
|
1243
|
-
for (const appName of appsNeedingDb) {
|
|
1244
|
-
let credentials = existingCredentials[appName];
|
|
1245
|
-
|
|
1246
|
-
if (credentials) {
|
|
1247
|
-
logger.log(` ${appName}: Using existing credentials from state`);
|
|
1248
|
-
} else {
|
|
1249
|
-
// Generate new credentials
|
|
1250
|
-
const password = randomBytes(16).toString('hex');
|
|
1251
|
-
credentials = { dbUser: appName, dbPassword: password };
|
|
1252
|
-
setAppCredentials(state, appName, credentials);
|
|
1253
|
-
logger.log(` ${appName}: Generated new credentials`);
|
|
1254
|
-
}
|
|
1255
|
-
|
|
1256
|
-
perAppDbCredentials.set(appName, credentials);
|
|
1257
|
-
|
|
1258
|
-
// Always add to users to create (idempotent - will update if exists)
|
|
1259
|
-
usersToCreate.push({
|
|
1260
|
-
name: appName,
|
|
1261
|
-
password: credentials.dbPassword,
|
|
1262
|
-
usePublicSchema: appName === 'api', // API uses public schema, others get their own
|
|
1263
|
-
});
|
|
1264
|
-
}
|
|
1265
|
-
|
|
1266
|
-
// Initialize database users
|
|
1267
|
-
const serverHostname = getServerHostname(creds.endpoint);
|
|
1268
|
-
await initializePostgresUsers(
|
|
1269
|
-
api,
|
|
1270
|
-
provisionedPostgres,
|
|
1271
|
-
serverHostname,
|
|
1272
|
-
usersToCreate,
|
|
1273
|
-
);
|
|
1274
|
-
}
|
|
1275
|
-
}
|
|
1276
|
-
|
|
1277
|
-
// ==================================================================
|
|
1278
|
-
// Provision backup destination if configured
|
|
1279
|
-
// ==================================================================
|
|
1280
|
-
if (workspace.deploy?.backups && provisionedPostgres) {
|
|
1281
|
-
logger.log('\n💾 Provisioning backup destination...');
|
|
1282
|
-
|
|
1283
|
-
const { provisionBackupDestination } = await import(
|
|
1284
|
-
'./backup-provisioner.js'
|
|
1285
|
-
);
|
|
1286
|
-
|
|
1287
|
-
const backupState = await provisionBackupDestination({
|
|
1288
|
-
api,
|
|
1289
|
-
projectId: project.projectId,
|
|
1290
|
-
projectName: workspace.name,
|
|
1291
|
-
stage,
|
|
1292
|
-
config: workspace.deploy.backups,
|
|
1293
|
-
existingState: getBackupState(state),
|
|
1294
|
-
logger,
|
|
1295
|
-
});
|
|
1296
|
-
|
|
1297
|
-
// Save backup state
|
|
1298
|
-
setBackupState(state, backupState);
|
|
1299
|
-
|
|
1300
|
-
// Create backup schedule for postgres if not already configured
|
|
1301
|
-
if (!backupState.postgresBackupId) {
|
|
1302
|
-
const backupSchedule = workspace.deploy.backups.schedule ?? '0 2 * * *';
|
|
1303
|
-
const backupRetention = workspace.deploy.backups.retention ?? 30;
|
|
1304
|
-
|
|
1305
|
-
logger.log(' Creating postgres backup schedule...');
|
|
1306
|
-
const backup = await api.createPostgresBackup({
|
|
1307
|
-
schedule: backupSchedule,
|
|
1308
|
-
prefix: `${stage}/postgres`,
|
|
1309
|
-
destinationId: backupState.destinationId,
|
|
1310
|
-
database: provisionedPostgres.databaseName,
|
|
1311
|
-
postgresId: provisionedPostgres.postgresId,
|
|
1312
|
-
enabled: true,
|
|
1313
|
-
keepLatestCount: backupRetention,
|
|
1314
|
-
});
|
|
1315
|
-
setPostgresBackupId(state, backup.backupId);
|
|
1316
|
-
logger.log(` ✓ Postgres backup schedule created (${backupSchedule})`);
|
|
1317
|
-
} else {
|
|
1318
|
-
logger.log(' ✓ Using existing postgres backup schedule');
|
|
1319
|
-
}
|
|
1320
|
-
}
|
|
1321
|
-
|
|
1322
|
-
// Track deployed app public URLs for frontend builds
|
|
1323
|
-
const publicUrls: Record<string, string> = {};
|
|
1324
|
-
const results: AppDeployResult[] = [];
|
|
1325
|
-
const dokployConfig = workspace.deploy.dokploy;
|
|
1326
|
-
|
|
1327
|
-
// Track domain IDs and hostnames for DNS orchestration
|
|
1328
|
-
const appHostnames = new Map<string, string>(); // appName -> hostname
|
|
1329
|
-
const appDomainIds = new Map<string, string>(); // appName -> domainId
|
|
1330
|
-
|
|
1331
|
-
// ==================================================================
|
|
1332
|
-
// PRE-COMPUTE: Frontend URLs for BETTER_AUTH_TRUSTED_ORIGINS
|
|
1333
|
-
// ==================================================================
|
|
1334
|
-
const frontendUrls: string[] = [];
|
|
1335
|
-
for (const appName of frontendApps) {
|
|
1336
|
-
const app = workspace.apps[appName]!;
|
|
1337
|
-
const isMainFrontend = isMainFrontendApp(appName, app, workspace.apps);
|
|
1338
|
-
const hostname = resolveHost(
|
|
1339
|
-
appName,
|
|
1340
|
-
app,
|
|
1341
|
-
stage,
|
|
1342
|
-
dokployConfig,
|
|
1343
|
-
isMainFrontend,
|
|
1344
|
-
);
|
|
1345
|
-
frontendUrls.push(`https://${hostname}`);
|
|
1346
|
-
}
|
|
1347
|
-
|
|
1348
|
-
// ==================================================================
|
|
1349
|
-
// PHASE 1: Deploy backend apps (with encrypted secrets)
|
|
1350
|
-
// ==================================================================
|
|
1351
|
-
if (backendApps.length > 0) {
|
|
1352
|
-
logger.log('\n📦 PHASE 1: Deploying backend applications...');
|
|
1353
|
-
|
|
1354
|
-
for (const appName of backendApps) {
|
|
1355
|
-
const app = workspace.apps[appName]!;
|
|
1356
|
-
|
|
1357
|
-
logger.log(`\n ⚙️ Deploying ${appName}...`);
|
|
1358
|
-
|
|
1359
|
-
try {
|
|
1360
|
-
// Use simple app name - project already provides namespace
|
|
1361
|
-
const dokployAppName = appName;
|
|
1362
|
-
|
|
1363
|
-
// Check state for cached application ID
|
|
1364
|
-
let application: DokployApplication | null = null;
|
|
1365
|
-
const cachedAppId = getApplicationId(state, appName);
|
|
1366
|
-
|
|
1367
|
-
if (cachedAppId) {
|
|
1368
|
-
logger.log(` Using cached ID: ${cachedAppId}`);
|
|
1369
|
-
application = await api.getApplication(cachedAppId);
|
|
1370
|
-
if (application) {
|
|
1371
|
-
logger.log(
|
|
1372
|
-
` ✓ Application found: ${application.applicationId}`,
|
|
1373
|
-
);
|
|
1374
|
-
} else {
|
|
1375
|
-
logger.log(` ⚠ Cached ID invalid, will create new`);
|
|
1376
|
-
}
|
|
1377
|
-
}
|
|
1378
|
-
|
|
1379
|
-
// If not found by ID, use findOrCreate
|
|
1380
|
-
if (!application) {
|
|
1381
|
-
const result = await api.findOrCreateApplication(
|
|
1382
|
-
dokployAppName,
|
|
1383
|
-
project.projectId,
|
|
1384
|
-
environmentId,
|
|
1385
|
-
);
|
|
1386
|
-
application = result.application;
|
|
1387
|
-
|
|
1388
|
-
if (result.created) {
|
|
1389
|
-
logger.log(
|
|
1390
|
-
` Created application: ${application.applicationId}`,
|
|
1391
|
-
);
|
|
1392
|
-
} else {
|
|
1393
|
-
logger.log(
|
|
1394
|
-
` Found existing application: ${application.applicationId}`,
|
|
1395
|
-
);
|
|
1396
|
-
}
|
|
1397
|
-
}
|
|
1398
|
-
|
|
1399
|
-
// Store application ID in state
|
|
1400
|
-
setApplicationId(state, appName, application.applicationId);
|
|
1401
|
-
|
|
1402
|
-
// Get encrypted secrets for this app
|
|
1403
|
-
const appSecrets = encryptedSecrets.get(appName);
|
|
1404
|
-
const buildArgs: string[] = [];
|
|
1405
|
-
|
|
1406
|
-
if (appSecrets && appSecrets.secretCount > 0) {
|
|
1407
|
-
buildArgs.push(
|
|
1408
|
-
`GKM_ENCRYPTED_CREDENTIALS=${appSecrets.payload.encrypted}`,
|
|
1409
|
-
);
|
|
1410
|
-
buildArgs.push(`GKM_CREDENTIALS_IV=${appSecrets.payload.iv}`);
|
|
1411
|
-
logger.log(` Encrypted ${appSecrets.secretCount} secrets`);
|
|
1412
|
-
}
|
|
1413
|
-
|
|
1414
|
-
// Build Docker image with encrypted secrets
|
|
1415
|
-
const imageName = `${workspace.name}-${appName}`;
|
|
1416
|
-
const imageRef = registry
|
|
1417
|
-
? `${registry}/${imageName}:${imageTag}`
|
|
1418
|
-
: `${imageName}:${imageTag}`;
|
|
1419
|
-
|
|
1420
|
-
logger.log(` Building Docker image: ${imageRef}`);
|
|
1421
|
-
|
|
1422
|
-
await deployDocker({
|
|
1423
|
-
stage,
|
|
1424
|
-
tag: imageTag,
|
|
1425
|
-
skipPush: false,
|
|
1426
|
-
config: {
|
|
1427
|
-
registry,
|
|
1428
|
-
imageName,
|
|
1429
|
-
appName,
|
|
1430
|
-
},
|
|
1431
|
-
buildArgs,
|
|
1432
|
-
});
|
|
1433
|
-
|
|
1434
|
-
// Compute hostname first (needed for BETTER_AUTH_URL)
|
|
1435
|
-
const backendHost = resolveHost(
|
|
1436
|
-
appName,
|
|
1437
|
-
app,
|
|
1438
|
-
stage,
|
|
1439
|
-
dokployConfig,
|
|
1440
|
-
false, // Backend apps are not main frontend
|
|
1441
|
-
);
|
|
1442
|
-
|
|
1443
|
-
// Build dependency URLs from already-deployed apps
|
|
1444
|
-
const dependencyUrls: Record<string, string> = {};
|
|
1445
|
-
if (app.dependencies) {
|
|
1446
|
-
for (const dep of app.dependencies) {
|
|
1447
|
-
if (publicUrls[dep]) {
|
|
1448
|
-
dependencyUrls[dep] = publicUrls[dep];
|
|
1449
|
-
}
|
|
1450
|
-
}
|
|
1451
|
-
}
|
|
1452
|
-
|
|
1453
|
-
// Build env resolver context
|
|
1454
|
-
const envContext: EnvResolverContext = {
|
|
1455
|
-
app,
|
|
1456
|
-
appName,
|
|
1457
|
-
stage,
|
|
1458
|
-
state,
|
|
1459
|
-
appCredentials: perAppDbCredentials.get(appName),
|
|
1460
|
-
postgres: provisionedPostgres
|
|
1461
|
-
? {
|
|
1462
|
-
host: provisionedPostgres.appName,
|
|
1463
|
-
port: 5432,
|
|
1464
|
-
database: provisionedPostgres.databaseName,
|
|
1465
|
-
}
|
|
1466
|
-
: undefined,
|
|
1467
|
-
redis: provisionedRedis
|
|
1468
|
-
? {
|
|
1469
|
-
host: provisionedRedis.appName,
|
|
1470
|
-
port: 6379,
|
|
1471
|
-
password: provisionedRedis.databasePassword,
|
|
1472
|
-
}
|
|
1473
|
-
: undefined,
|
|
1474
|
-
appHostname: backendHost,
|
|
1475
|
-
frontendUrls,
|
|
1476
|
-
userSecrets: stageSecrets ?? undefined,
|
|
1477
|
-
masterKey: appSecrets?.masterKey,
|
|
1478
|
-
dependencyUrls,
|
|
1479
|
-
};
|
|
1480
|
-
|
|
1481
|
-
// Resolve all required environment variables
|
|
1482
|
-
// Always include PORT, NODE_ENV, STAGE even if not explicitly required
|
|
1483
|
-
const appRequirements = sniffedApps.get(appName);
|
|
1484
|
-
const sniffedVars = appRequirements?.requiredEnvVars ?? [];
|
|
1485
|
-
const requiredVars = [
|
|
1486
|
-
...new Set(['PORT', 'NODE_ENV', 'STAGE', ...sniffedVars]),
|
|
1487
|
-
];
|
|
1488
|
-
const { valid, missing, resolved } = validateEnvVars(
|
|
1489
|
-
requiredVars,
|
|
1490
|
-
envContext,
|
|
1491
|
-
);
|
|
1492
|
-
|
|
1493
|
-
if (!valid) {
|
|
1494
|
-
throw new Error(formatMissingVarsError(appName, missing, stage));
|
|
1495
|
-
}
|
|
1496
|
-
|
|
1497
|
-
// Build env vars string for Dokploy
|
|
1498
|
-
const envVars: string[] = Object.entries(resolved).map(
|
|
1499
|
-
([key, value]) => `${key}=${value}`,
|
|
1500
|
-
);
|
|
1501
|
-
|
|
1502
|
-
if (Object.keys(resolved).length > 0) {
|
|
1503
|
-
logger.log(
|
|
1504
|
-
` Resolved ${Object.keys(resolved).length} env vars: ${Object.keys(resolved).join(', ')}`,
|
|
1505
|
-
);
|
|
1506
|
-
}
|
|
1507
|
-
|
|
1508
|
-
// Configure and deploy application in Dokploy
|
|
1509
|
-
await api.saveDockerProvider(application.applicationId, imageRef, {
|
|
1510
|
-
registryId,
|
|
1511
|
-
});
|
|
1512
|
-
|
|
1513
|
-
await api.saveApplicationEnv(
|
|
1514
|
-
application.applicationId,
|
|
1515
|
-
envVars.join('\n'),
|
|
1516
|
-
);
|
|
1517
|
-
|
|
1518
|
-
logger.log(` Deploying to Dokploy...`);
|
|
1519
|
-
await api.deployApplication(application.applicationId);
|
|
1520
|
-
|
|
1521
|
-
// Check if domain already exists (backendHost computed above)
|
|
1522
|
-
const existingDomains = await api.getDomainsByApplicationId(
|
|
1523
|
-
application.applicationId,
|
|
1524
|
-
);
|
|
1525
|
-
const existingDomain = existingDomains.find(
|
|
1526
|
-
(d) => d.host === backendHost,
|
|
1527
|
-
);
|
|
1528
|
-
|
|
1529
|
-
if (existingDomain) {
|
|
1530
|
-
// Domain already exists
|
|
1531
|
-
appHostnames.set(appName, backendHost);
|
|
1532
|
-
appDomainIds.set(appName, existingDomain.domainId);
|
|
1533
|
-
publicUrls[appName] = `https://${backendHost}`;
|
|
1534
|
-
logger.log(` ✓ Domain: https://${backendHost} (existing)`);
|
|
1535
|
-
} else {
|
|
1536
|
-
// Create new domain
|
|
1537
|
-
try {
|
|
1538
|
-
const domain = await api.createDomain({
|
|
1539
|
-
host: backendHost,
|
|
1540
|
-
port: app.port,
|
|
1541
|
-
https: true,
|
|
1542
|
-
certificateType: 'letsencrypt',
|
|
1543
|
-
applicationId: application.applicationId,
|
|
1544
|
-
});
|
|
1545
|
-
|
|
1546
|
-
appHostnames.set(appName, backendHost);
|
|
1547
|
-
appDomainIds.set(appName, domain.domainId);
|
|
1548
|
-
publicUrls[appName] = `https://${backendHost}`;
|
|
1549
|
-
logger.log(` ✓ Domain: https://${backendHost} (created)`);
|
|
1550
|
-
} catch (domainError) {
|
|
1551
|
-
const message =
|
|
1552
|
-
domainError instanceof Error
|
|
1553
|
-
? domainError.message
|
|
1554
|
-
: 'Unknown error';
|
|
1555
|
-
logger.log(` ⚠ Domain creation failed: ${message}`);
|
|
1556
|
-
appHostnames.set(appName, backendHost);
|
|
1557
|
-
publicUrls[appName] = `https://${backendHost}`;
|
|
1558
|
-
}
|
|
1559
|
-
}
|
|
1560
|
-
|
|
1561
|
-
results.push({
|
|
1562
|
-
appName,
|
|
1563
|
-
type: app.type,
|
|
1564
|
-
success: true,
|
|
1565
|
-
applicationId: application.applicationId,
|
|
1566
|
-
imageRef,
|
|
1567
|
-
});
|
|
1568
|
-
|
|
1569
|
-
logger.log(` ✓ ${appName} deployed successfully`);
|
|
1570
|
-
} catch (error) {
|
|
1571
|
-
const message =
|
|
1572
|
-
error instanceof Error ? error.message : 'Unknown error';
|
|
1573
|
-
logger.log(` ✗ Failed to deploy ${appName}: ${message}`);
|
|
1574
|
-
|
|
1575
|
-
results.push({
|
|
1576
|
-
appName,
|
|
1577
|
-
type: app.type,
|
|
1578
|
-
success: false,
|
|
1579
|
-
error: message,
|
|
1580
|
-
});
|
|
1581
|
-
|
|
1582
|
-
// Abort on backend failure to prevent incomplete deployment
|
|
1583
|
-
throw new Error(
|
|
1584
|
-
`Backend deployment failed for ${appName}. Aborting to prevent partial deployment.`,
|
|
1585
|
-
);
|
|
1586
|
-
}
|
|
1587
|
-
}
|
|
1588
|
-
}
|
|
1589
|
-
|
|
1590
|
-
// ==================================================================
|
|
1591
|
-
// PHASE 2: Deploy frontend apps (with public URLs from backends)
|
|
1592
|
-
// ==================================================================
|
|
1593
|
-
if (frontendApps.length > 0) {
|
|
1594
|
-
logger.log('\n🌐 PHASE 2: Deploying frontend applications...');
|
|
1595
|
-
|
|
1596
|
-
for (const appName of frontendApps) {
|
|
1597
|
-
const app = workspace.apps[appName]!;
|
|
1598
|
-
|
|
1599
|
-
logger.log(`\n 🌐 Deploying ${appName}...`);
|
|
1600
|
-
|
|
1601
|
-
try {
|
|
1602
|
-
// Use simple app name - project already provides namespace
|
|
1603
|
-
const dokployAppName = appName;
|
|
1604
|
-
|
|
1605
|
-
// Check state for cached application ID
|
|
1606
|
-
let application: DokployApplication | null = null;
|
|
1607
|
-
const cachedAppId = getApplicationId(state, appName);
|
|
1608
|
-
|
|
1609
|
-
if (cachedAppId) {
|
|
1610
|
-
logger.log(` Using cached ID: ${cachedAppId}`);
|
|
1611
|
-
application = await api.getApplication(cachedAppId);
|
|
1612
|
-
if (application) {
|
|
1613
|
-
logger.log(
|
|
1614
|
-
` ✓ Application found: ${application.applicationId}`,
|
|
1615
|
-
);
|
|
1616
|
-
} else {
|
|
1617
|
-
logger.log(` ⚠ Cached ID invalid, will create new`);
|
|
1618
|
-
}
|
|
1619
|
-
}
|
|
1620
|
-
|
|
1621
|
-
// If not found by ID, use findOrCreate
|
|
1622
|
-
if (!application) {
|
|
1623
|
-
const result = await api.findOrCreateApplication(
|
|
1624
|
-
dokployAppName,
|
|
1625
|
-
project.projectId,
|
|
1626
|
-
environmentId,
|
|
1627
|
-
);
|
|
1628
|
-
application = result.application;
|
|
1629
|
-
|
|
1630
|
-
if (result.created) {
|
|
1631
|
-
logger.log(
|
|
1632
|
-
` Created application: ${application.applicationId}`,
|
|
1633
|
-
);
|
|
1634
|
-
} else {
|
|
1635
|
-
logger.log(
|
|
1636
|
-
` Found existing application: ${application.applicationId}`,
|
|
1637
|
-
);
|
|
1638
|
-
}
|
|
1639
|
-
}
|
|
1640
|
-
|
|
1641
|
-
// Store application ID in state
|
|
1642
|
-
setApplicationId(state, appName, application.applicationId);
|
|
1643
|
-
|
|
1644
|
-
// Build dependency URLs for frontend (same pattern as backend)
|
|
1645
|
-
const dependencyUrls: Record<string, string> = {};
|
|
1646
|
-
if (app.dependencies) {
|
|
1647
|
-
for (const dep of app.dependencies) {
|
|
1648
|
-
if (publicUrls[dep]) {
|
|
1649
|
-
dependencyUrls[dep] = publicUrls[dep];
|
|
1650
|
-
}
|
|
1651
|
-
}
|
|
1652
|
-
}
|
|
1653
|
-
|
|
1654
|
-
// Compute hostname for this frontend app
|
|
1655
|
-
const isMainFrontend = isMainFrontendApp(appName, app, workspace.apps);
|
|
1656
|
-
const frontendHost = resolveHost(
|
|
1657
|
-
appName,
|
|
1658
|
-
app,
|
|
1659
|
-
stage,
|
|
1660
|
-
dokployConfig,
|
|
1661
|
-
isMainFrontend,
|
|
1662
|
-
);
|
|
1663
|
-
|
|
1664
|
-
// Build env context for frontend
|
|
1665
|
-
const envContext: EnvResolverContext = {
|
|
1666
|
-
app,
|
|
1667
|
-
appName,
|
|
1668
|
-
stage,
|
|
1669
|
-
state,
|
|
1670
|
-
appHostname: frontendHost,
|
|
1671
|
-
frontendUrls: [],
|
|
1672
|
-
userSecrets: stageSecrets ?? undefined,
|
|
1673
|
-
dependencyUrls,
|
|
1674
|
-
};
|
|
1675
|
-
|
|
1676
|
-
// Resolve all env vars BEFORE Docker build (public-prefixed vars
|
|
1677
|
-
// must be present at bundler build time so they get inlined).
|
|
1678
|
-
const sniffedVars = sniffedApps.get(appName)?.requiredEnvVars ?? [];
|
|
1679
|
-
const { valid, missing, resolved } = validateEnvVars(
|
|
1680
|
-
sniffedVars,
|
|
1681
|
-
envContext,
|
|
1682
|
-
);
|
|
1683
|
-
|
|
1684
|
-
if (!valid) {
|
|
1685
|
-
throw new Error(formatMissingVarsError(appName, missing, stage));
|
|
1686
|
-
}
|
|
1687
|
-
|
|
1688
|
-
if (Object.keys(resolved).length > 0) {
|
|
1689
|
-
logger.log(
|
|
1690
|
-
` Resolved ${Object.keys(resolved).length} env vars: ${Object.keys(resolved).join(', ')}`,
|
|
1691
|
-
);
|
|
1692
|
-
}
|
|
1693
|
-
|
|
1694
|
-
// Build args: only the framework's public-prefixed vars get baked
|
|
1695
|
-
// into the bundle. Server-only vars stay as runtime env.
|
|
1696
|
-
const publicPrefix = getPublicEnvPrefix(app.framework);
|
|
1697
|
-
const buildArgs: string[] = [];
|
|
1698
|
-
const publicUrlArgNames: string[] = [];
|
|
1699
|
-
|
|
1700
|
-
if (publicPrefix) {
|
|
1701
|
-
for (const [key, value] of Object.entries(resolved)) {
|
|
1702
|
-
if (key.startsWith(publicPrefix)) {
|
|
1703
|
-
buildArgs.push(`${key}=${value}`);
|
|
1704
|
-
publicUrlArgNames.push(key);
|
|
1705
|
-
}
|
|
1706
|
-
}
|
|
1707
|
-
}
|
|
1708
|
-
|
|
1709
|
-
if (buildArgs.length > 0) {
|
|
1710
|
-
logger.log(` Build args: ${publicUrlArgNames.join(', ')}`);
|
|
1711
|
-
}
|
|
1712
|
-
|
|
1713
|
-
// Build Docker image with public-prefixed vars as build args
|
|
1714
|
-
const imageName = `${workspace.name}-${appName}`;
|
|
1715
|
-
const imageRef = registry
|
|
1716
|
-
? `${registry}/${imageName}:${imageTag}`
|
|
1717
|
-
: `${imageName}:${imageTag}`;
|
|
1718
|
-
|
|
1719
|
-
logger.log(` Building Docker image: ${imageRef}`);
|
|
1720
|
-
|
|
1721
|
-
await deployDocker({
|
|
1722
|
-
stage,
|
|
1723
|
-
tag: imageTag,
|
|
1724
|
-
skipPush: false,
|
|
1725
|
-
config: {
|
|
1726
|
-
registry,
|
|
1727
|
-
imageName,
|
|
1728
|
-
appName,
|
|
1729
|
-
},
|
|
1730
|
-
buildArgs,
|
|
1731
|
-
// Pass arg names for Dockerfile ARG generation
|
|
1732
|
-
publicUrlArgs: publicUrlArgNames,
|
|
1733
|
-
});
|
|
1734
|
-
|
|
1735
|
-
// Prepare runtime environment variables
|
|
1736
|
-
const envVars: string[] = [
|
|
1737
|
-
`NODE_ENV=production`,
|
|
1738
|
-
`PORT=${app.port}`,
|
|
1739
|
-
`STAGE=${stage}`,
|
|
1740
|
-
];
|
|
1741
|
-
|
|
1742
|
-
// Add all resolved vars as runtime env (for SSR and server components)
|
|
1743
|
-
for (const [key, value] of Object.entries(resolved)) {
|
|
1744
|
-
envVars.push(`${key}=${value}`);
|
|
1745
|
-
}
|
|
1746
|
-
|
|
1747
|
-
// Configure and deploy application in Dokploy
|
|
1748
|
-
await api.saveDockerProvider(application.applicationId, imageRef, {
|
|
1749
|
-
registryId,
|
|
1750
|
-
});
|
|
1751
|
-
|
|
1752
|
-
await api.saveApplicationEnv(
|
|
1753
|
-
application.applicationId,
|
|
1754
|
-
envVars.join('\n'),
|
|
1755
|
-
);
|
|
1756
|
-
|
|
1757
|
-
logger.log(` Deploying to Dokploy...`);
|
|
1758
|
-
await api.deployApplication(application.applicationId);
|
|
1759
|
-
|
|
1760
|
-
// Check if domain already exists (frontendHost computed earlier for env context)
|
|
1761
|
-
const existingFrontendDomains = await api.getDomainsByApplicationId(
|
|
1762
|
-
application.applicationId,
|
|
1763
|
-
);
|
|
1764
|
-
const existingFrontendDomain = existingFrontendDomains.find(
|
|
1765
|
-
(d) => d.host === frontendHost,
|
|
1766
|
-
);
|
|
1767
|
-
|
|
1768
|
-
if (existingFrontendDomain) {
|
|
1769
|
-
// Domain already exists
|
|
1770
|
-
appHostnames.set(appName, frontendHost);
|
|
1771
|
-
appDomainIds.set(appName, existingFrontendDomain.domainId);
|
|
1772
|
-
publicUrls[appName] = `https://${frontendHost}`;
|
|
1773
|
-
logger.log(` ✓ Domain: https://${frontendHost} (existing)`);
|
|
1774
|
-
} else {
|
|
1775
|
-
// Create new domain
|
|
1776
|
-
try {
|
|
1777
|
-
const domain = await api.createDomain({
|
|
1778
|
-
host: frontendHost,
|
|
1779
|
-
port: app.port,
|
|
1780
|
-
https: true,
|
|
1781
|
-
certificateType: 'letsencrypt',
|
|
1782
|
-
applicationId: application.applicationId,
|
|
1783
|
-
});
|
|
1784
|
-
|
|
1785
|
-
appHostnames.set(appName, frontendHost);
|
|
1786
|
-
appDomainIds.set(appName, domain.domainId);
|
|
1787
|
-
publicUrls[appName] = `https://${frontendHost}`;
|
|
1788
|
-
logger.log(` ✓ Domain: https://${frontendHost} (created)`);
|
|
1789
|
-
} catch (domainError) {
|
|
1790
|
-
const message =
|
|
1791
|
-
domainError instanceof Error
|
|
1792
|
-
? domainError.message
|
|
1793
|
-
: 'Unknown error';
|
|
1794
|
-
logger.log(` ⚠ Domain creation failed: ${message}`);
|
|
1795
|
-
appHostnames.set(appName, frontendHost);
|
|
1796
|
-
publicUrls[appName] = `https://${frontendHost}`;
|
|
1797
|
-
}
|
|
1798
|
-
}
|
|
1799
|
-
|
|
1800
|
-
results.push({
|
|
1801
|
-
appName,
|
|
1802
|
-
type: app.type,
|
|
1803
|
-
success: true,
|
|
1804
|
-
applicationId: application.applicationId,
|
|
1805
|
-
imageRef,
|
|
1806
|
-
});
|
|
1807
|
-
|
|
1808
|
-
logger.log(` ✓ ${appName} deployed successfully`);
|
|
1809
|
-
} catch (error) {
|
|
1810
|
-
const message =
|
|
1811
|
-
error instanceof Error ? error.message : 'Unknown error';
|
|
1812
|
-
logger.log(` ✗ Failed to deploy ${appName}: ${message}`);
|
|
1813
|
-
|
|
1814
|
-
results.push({
|
|
1815
|
-
appName,
|
|
1816
|
-
type: app.type,
|
|
1817
|
-
success: false,
|
|
1818
|
-
error: message,
|
|
1819
|
-
});
|
|
1820
|
-
// Don't abort on frontend failures - continue with other frontends
|
|
1821
|
-
}
|
|
1822
|
-
}
|
|
1823
|
-
}
|
|
1824
|
-
|
|
1825
|
-
// ==================================================================
|
|
1826
|
-
// STATE: Save deploy state
|
|
1827
|
-
// ==================================================================
|
|
1828
|
-
logger.log('\n📋 Saving deploy state...');
|
|
1829
|
-
await stateProvider.write(stage, state);
|
|
1830
|
-
logger.log(' ✓ State saved');
|
|
1831
|
-
|
|
1832
|
-
// ==================================================================
|
|
1833
|
-
// DNS: Create DNS records, verify propagation, and validate for SSL
|
|
1834
|
-
// ==================================================================
|
|
1835
|
-
const dnsConfig = workspace.deploy.dns;
|
|
1836
|
-
if (dnsConfig && appHostnames.size > 0) {
|
|
1837
|
-
const dnsResult = await orchestrateDns(
|
|
1838
|
-
appHostnames,
|
|
1839
|
-
dnsConfig,
|
|
1840
|
-
creds.endpoint,
|
|
1841
|
-
);
|
|
1842
|
-
|
|
1843
|
-
// Verify DNS records resolve correctly (with state caching)
|
|
1844
|
-
if (dnsResult?.serverIp && appHostnames.size > 0) {
|
|
1845
|
-
await verifyDnsRecords(appHostnames, dnsResult.serverIp, state);
|
|
1846
|
-
|
|
1847
|
-
// Save state again to persist DNS verification results
|
|
1848
|
-
await stateProvider.write(stage, state);
|
|
1849
|
-
}
|
|
1850
|
-
|
|
1851
|
-
// Validate domains to trigger SSL certificate generation
|
|
1852
|
-
if (dnsResult?.success && appHostnames.size > 0) {
|
|
1853
|
-
logger.log('\n🔒 Validating domains for SSL certificates...');
|
|
1854
|
-
for (const [appName, hostname] of appHostnames) {
|
|
1855
|
-
try {
|
|
1856
|
-
const result = await api.validateDomain(hostname);
|
|
1857
|
-
if (result.isValid) {
|
|
1858
|
-
logger.log(` ✓ ${appName}: ${hostname} → ${result.resolvedIp}`);
|
|
1859
|
-
} else {
|
|
1860
|
-
logger.log(` ⚠ ${appName}: ${hostname} not valid`);
|
|
1861
|
-
}
|
|
1862
|
-
} catch (validationError) {
|
|
1863
|
-
const message =
|
|
1864
|
-
validationError instanceof Error
|
|
1865
|
-
? validationError.message
|
|
1866
|
-
: 'Unknown error';
|
|
1867
|
-
logger.log(` ⚠ ${appName}: validation failed - ${message}`);
|
|
1868
|
-
}
|
|
1869
|
-
}
|
|
1870
|
-
}
|
|
1871
|
-
}
|
|
1872
|
-
|
|
1873
|
-
// ==================================================================
|
|
1874
|
-
// Summary
|
|
1875
|
-
// ==================================================================
|
|
1876
|
-
const successCount = results.filter((r) => r.success).length;
|
|
1877
|
-
const failedCount = results.filter((r) => !r.success).length;
|
|
1878
|
-
|
|
1879
|
-
logger.log(`\n${'─'.repeat(50)}`);
|
|
1880
|
-
logger.log(`\n✅ Workspace deployment complete!`);
|
|
1881
|
-
logger.log(` Project: ${project.projectId}`);
|
|
1882
|
-
logger.log(` Successful: ${successCount}`);
|
|
1883
|
-
if (failedCount > 0) {
|
|
1884
|
-
logger.log(` Failed: ${failedCount}`);
|
|
1885
|
-
}
|
|
1886
|
-
|
|
1887
|
-
// Print deployed URLs
|
|
1888
|
-
if (Object.keys(publicUrls).length > 0) {
|
|
1889
|
-
logger.log('\n 📡 Deployed URLs:');
|
|
1890
|
-
for (const [name, url] of Object.entries(publicUrls)) {
|
|
1891
|
-
logger.log(` ${name}: ${url}`);
|
|
1892
|
-
}
|
|
1893
|
-
}
|
|
1894
|
-
|
|
1895
|
-
return {
|
|
1896
|
-
apps: results,
|
|
1897
|
-
projectId: project.projectId,
|
|
1898
|
-
successCount,
|
|
1899
|
-
failedCount,
|
|
1900
|
-
};
|
|
1901
|
-
}
|
|
1902
|
-
|
|
1903
|
-
/**
|
|
1904
|
-
* Main deploy command
|
|
1905
|
-
*/
|
|
1906
|
-
export async function deployCommand(
|
|
1907
|
-
options: DeployOptions,
|
|
1908
|
-
): Promise<DeployResult | WorkspaceDeployResult> {
|
|
1909
|
-
const { provider, stage, tag, skipPush, skipBuild } = options;
|
|
1910
|
-
|
|
1911
|
-
// Load config with workspace detection
|
|
1912
|
-
const loadedConfig = await loadWorkspaceConfig();
|
|
1913
|
-
|
|
1914
|
-
// Route to workspace deploy mode for multi-app workspaces
|
|
1915
|
-
if (loadedConfig.type === 'workspace') {
|
|
1916
|
-
logger.log('📦 Detected workspace configuration');
|
|
1917
|
-
return workspaceDeployCommand(loadedConfig.workspace, options);
|
|
1918
|
-
}
|
|
1919
|
-
|
|
1920
|
-
logger.log(`\n🚀 Deploying to ${provider}...`);
|
|
1921
|
-
logger.log(` Stage: ${stage}`);
|
|
1922
|
-
|
|
1923
|
-
// Single-app mode - use existing logic
|
|
1924
|
-
const config = await loadConfig();
|
|
1925
|
-
|
|
1926
|
-
// Generate tag if not provided
|
|
1927
|
-
const imageTag = tag ?? generateTag(stage);
|
|
1928
|
-
logger.log(` Tag: ${imageTag}`);
|
|
1929
|
-
|
|
1930
|
-
// Resolve docker config for image reference
|
|
1931
|
-
const dockerConfig = resolveDockerConfig(config);
|
|
1932
|
-
const imageName = dockerConfig.imageName!;
|
|
1933
|
-
const registry = dockerConfig.registry;
|
|
1934
|
-
const imageRef = registry
|
|
1935
|
-
? `${registry}/${imageName}:${imageTag}`
|
|
1936
|
-
: `${imageName}:${imageTag}`;
|
|
1937
|
-
|
|
1938
|
-
// For Dokploy, set up services BEFORE build so URLs are available
|
|
1939
|
-
let dokployConfig: DokployDeployConfig | undefined;
|
|
1940
|
-
let finalRegistry = registry;
|
|
1941
|
-
|
|
1942
|
-
if (provider === 'dokploy') {
|
|
1943
|
-
// Extract docker compose services config
|
|
1944
|
-
const composeServices = config.docker?.compose?.services;
|
|
1945
|
-
logger.log(
|
|
1946
|
-
`\n🔍 Docker compose config: ${JSON.stringify(config.docker?.compose)}`,
|
|
1947
|
-
);
|
|
1948
|
-
const dockerServices: DockerComposeServices | undefined = composeServices
|
|
1949
|
-
? Array.isArray(composeServices)
|
|
1950
|
-
? {
|
|
1951
|
-
postgres: composeServices.includes('postgres'),
|
|
1952
|
-
redis: composeServices.includes('redis'),
|
|
1953
|
-
rabbitmq: composeServices.includes('rabbitmq'),
|
|
1954
|
-
}
|
|
1955
|
-
: {
|
|
1956
|
-
postgres: Boolean(composeServices.postgres),
|
|
1957
|
-
redis: Boolean(composeServices.redis),
|
|
1958
|
-
rabbitmq: Boolean(composeServices.rabbitmq),
|
|
1959
|
-
}
|
|
1960
|
-
: undefined;
|
|
1961
|
-
|
|
1962
|
-
// Ensure Dokploy is fully set up (credentials, project, app, registry, services)
|
|
1963
|
-
const setupResult = await ensureDokploySetup(
|
|
1964
|
-
config,
|
|
1965
|
-
dockerConfig,
|
|
1966
|
-
stage,
|
|
1967
|
-
dockerServices,
|
|
1968
|
-
);
|
|
1969
|
-
dokployConfig = setupResult.config;
|
|
1970
|
-
finalRegistry = dokployConfig.registry ?? dockerConfig.registry;
|
|
1971
|
-
|
|
1972
|
-
// Save provisioned service URLs to secrets before build
|
|
1973
|
-
if (setupResult.serviceUrls) {
|
|
1974
|
-
const { readStageSecrets, writeStageSecrets, initStageSecrets } =
|
|
1975
|
-
await import('../secrets/storage');
|
|
1976
|
-
let secrets = await readStageSecrets(stage);
|
|
1977
|
-
|
|
1978
|
-
// Create secrets file if it doesn't exist
|
|
1979
|
-
if (!secrets) {
|
|
1980
|
-
logger.log(` Creating secrets file for stage "${stage}"...`);
|
|
1981
|
-
secrets = initStageSecrets(stage);
|
|
1982
|
-
}
|
|
1983
|
-
|
|
1984
|
-
let updated = false;
|
|
1985
|
-
// URL fields go to secrets.urls, individual params go to secrets.custom
|
|
1986
|
-
const urlFields = ['DATABASE_URL', 'REDIS_URL', 'RABBITMQ_URL'] as const;
|
|
1987
|
-
|
|
1988
|
-
for (const [key, value] of Object.entries(setupResult.serviceUrls)) {
|
|
1989
|
-
if (!value) continue;
|
|
1990
|
-
|
|
1991
|
-
if (urlFields.includes(key as (typeof urlFields)[number])) {
|
|
1992
|
-
// URL fields
|
|
1993
|
-
const urlKey = key as keyof typeof secrets.urls;
|
|
1994
|
-
if (!secrets.urls[urlKey]) {
|
|
1995
|
-
secrets.urls[urlKey] = value;
|
|
1996
|
-
logger.log(` Saved ${key} to secrets.urls`);
|
|
1997
|
-
updated = true;
|
|
1998
|
-
}
|
|
1999
|
-
} else {
|
|
2000
|
-
// Individual parameters (HOST, PORT, NAME, USER, PASSWORD)
|
|
2001
|
-
if (!secrets.custom[key]) {
|
|
2002
|
-
secrets.custom[key] = value;
|
|
2003
|
-
logger.log(` Saved ${key} to secrets.custom`);
|
|
2004
|
-
updated = true;
|
|
2005
|
-
}
|
|
2006
|
-
}
|
|
2007
|
-
}
|
|
2008
|
-
if (updated) {
|
|
2009
|
-
await writeStageSecrets(secrets);
|
|
2010
|
-
}
|
|
2011
|
-
}
|
|
2012
|
-
}
|
|
2013
|
-
|
|
2014
|
-
// Build for production with secrets injection (unless skipped)
|
|
2015
|
-
let masterKey: string | undefined;
|
|
2016
|
-
if (!skipBuild) {
|
|
2017
|
-
logger.log(`\n📦 Building for production...`);
|
|
2018
|
-
const buildResult = await buildCommand({
|
|
2019
|
-
provider: 'server',
|
|
2020
|
-
production: true,
|
|
2021
|
-
stage,
|
|
2022
|
-
});
|
|
2023
|
-
masterKey = buildResult.masterKey;
|
|
2024
|
-
} else {
|
|
2025
|
-
logger.log(`\n⏭️ Skipping build (--skip-build)`);
|
|
2026
|
-
}
|
|
2027
|
-
|
|
2028
|
-
// Deploy based on provider
|
|
2029
|
-
let result: DeployResult;
|
|
2030
|
-
|
|
2031
|
-
switch (provider) {
|
|
2032
|
-
case 'docker': {
|
|
2033
|
-
result = await deployDocker({
|
|
2034
|
-
stage,
|
|
2035
|
-
tag: imageTag,
|
|
2036
|
-
skipPush,
|
|
2037
|
-
masterKey,
|
|
2038
|
-
config: dockerConfig,
|
|
2039
|
-
});
|
|
2040
|
-
break;
|
|
2041
|
-
}
|
|
2042
|
-
|
|
2043
|
-
case 'dokploy': {
|
|
2044
|
-
if (!dokployConfig) {
|
|
2045
|
-
throw new Error('Dokploy config not initialized');
|
|
2046
|
-
}
|
|
2047
|
-
const finalImageRef = finalRegistry
|
|
2048
|
-
? `${finalRegistry}/${imageName}:${imageTag}`
|
|
2049
|
-
: `${imageName}:${imageTag}`;
|
|
2050
|
-
|
|
2051
|
-
// First build and push the Docker image
|
|
2052
|
-
await deployDocker({
|
|
2053
|
-
stage,
|
|
2054
|
-
tag: imageTag,
|
|
2055
|
-
skipPush: false, // Dokploy needs the image in registry
|
|
2056
|
-
masterKey,
|
|
2057
|
-
config: {
|
|
2058
|
-
registry: finalRegistry,
|
|
2059
|
-
imageName: dockerConfig.imageName,
|
|
2060
|
-
},
|
|
2061
|
-
});
|
|
2062
|
-
|
|
2063
|
-
// Then trigger Dokploy deployment
|
|
2064
|
-
result = await deployDokploy({
|
|
2065
|
-
stage,
|
|
2066
|
-
tag: imageTag,
|
|
2067
|
-
imageRef: finalImageRef,
|
|
2068
|
-
masterKey,
|
|
2069
|
-
config: dokployConfig,
|
|
2070
|
-
});
|
|
2071
|
-
break;
|
|
2072
|
-
}
|
|
2073
|
-
|
|
2074
|
-
case 'aws-lambda': {
|
|
2075
|
-
logger.log('\n⚠️ AWS Lambda deployment is not yet implemented.');
|
|
2076
|
-
logger.log(' Use SST or AWS CDK for Lambda deployments.');
|
|
2077
|
-
result = { imageRef, masterKey };
|
|
2078
|
-
break;
|
|
2079
|
-
}
|
|
2080
|
-
|
|
2081
|
-
default: {
|
|
2082
|
-
throw new Error(
|
|
2083
|
-
`Unknown deploy provider: ${provider}\n` +
|
|
2084
|
-
'Supported providers: docker, dokploy, aws-lambda',
|
|
2085
|
-
);
|
|
2086
|
-
}
|
|
2087
|
-
}
|
|
2088
|
-
|
|
2089
|
-
logger.log('\n✅ Deployment complete!');
|
|
2090
|
-
|
|
2091
|
-
return result;
|
|
2092
|
-
}
|
|
2093
|
-
|
|
2094
|
-
export type { DeployOptions, DeployProvider, DeployResult };
|