@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/dev/index.ts
DELETED
|
@@ -1,1649 +0,0 @@
|
|
|
1
|
-
import { type ChildProcess, execSync, spawn } from 'node:child_process';
|
|
2
|
-
import { existsSync } from 'node:fs';
|
|
3
|
-
import { mkdir, writeFile } from 'node:fs/promises';
|
|
4
|
-
import { dirname, join, resolve } from 'node:path';
|
|
5
|
-
import chokidar from 'chokidar';
|
|
6
|
-
import fg from 'fast-glob';
|
|
7
|
-
import { resolveProviders } from '../build/providerResolver';
|
|
8
|
-
import type {
|
|
9
|
-
BuildContext,
|
|
10
|
-
NormalizedHooksConfig,
|
|
11
|
-
NormalizedProductionConfig,
|
|
12
|
-
NormalizedStudioConfig,
|
|
13
|
-
NormalizedTelescopeConfig,
|
|
14
|
-
} from '../build/types';
|
|
15
|
-
import {
|
|
16
|
-
getAppNameFromCwd,
|
|
17
|
-
loadAppConfig,
|
|
18
|
-
loadWorkspaceConfig,
|
|
19
|
-
parseModuleConfig,
|
|
20
|
-
} from '../config';
|
|
21
|
-
import {
|
|
22
|
-
createEntryWrapper,
|
|
23
|
-
findAvailablePort,
|
|
24
|
-
isPortAvailable,
|
|
25
|
-
loadEnvFiles,
|
|
26
|
-
loadSecretsForApp,
|
|
27
|
-
prepareEntryCredentials,
|
|
28
|
-
resolveServicePorts,
|
|
29
|
-
rewriteUrlsWithPorts,
|
|
30
|
-
startWorkspaceServices,
|
|
31
|
-
} from '../credentials';
|
|
32
|
-
import {
|
|
33
|
-
CronGenerator,
|
|
34
|
-
EndpointGenerator,
|
|
35
|
-
FunctionGenerator,
|
|
36
|
-
QueueGenerator,
|
|
37
|
-
SubscriberGenerator,
|
|
38
|
-
TopicGenerator,
|
|
39
|
-
} from '../generators';
|
|
40
|
-
import {
|
|
41
|
-
generateOpenApi,
|
|
42
|
-
OPENAPI_OUTPUT_PATH,
|
|
43
|
-
resolveOpenApiConfig,
|
|
44
|
-
} from '../openapi';
|
|
45
|
-
import {
|
|
46
|
-
readStageSecrets,
|
|
47
|
-
secretsExist,
|
|
48
|
-
toEmbeddableSecrets,
|
|
49
|
-
} from '../secrets/storage.js';
|
|
50
|
-
import type {
|
|
51
|
-
GkmConfig,
|
|
52
|
-
LegacyProvider,
|
|
53
|
-
ProductionConfig,
|
|
54
|
-
Runtime,
|
|
55
|
-
ServerConfig,
|
|
56
|
-
StudioConfig,
|
|
57
|
-
TelescopeConfig,
|
|
58
|
-
} from '../types';
|
|
59
|
-
import {
|
|
60
|
-
type FrontendFramework,
|
|
61
|
-
getAppBuildOrder,
|
|
62
|
-
getDependencyEnvVars,
|
|
63
|
-
type NormalizedWorkspace,
|
|
64
|
-
} from '../workspace/index.js';
|
|
65
|
-
|
|
66
|
-
// Re-export shared utilities from credentials module so existing imports
|
|
67
|
-
// from '../dev' or '../dev/index' continue to work.
|
|
68
|
-
export {
|
|
69
|
-
buildDockerComposeEnv,
|
|
70
|
-
type ComposePortMapping,
|
|
71
|
-
createCredentialsPreload,
|
|
72
|
-
createEntryWrapper,
|
|
73
|
-
type EntryCredentialsResult,
|
|
74
|
-
findAvailablePort,
|
|
75
|
-
findSecretsRoot,
|
|
76
|
-
getContainerHostPort,
|
|
77
|
-
isPortAvailable,
|
|
78
|
-
loadEnvFiles,
|
|
79
|
-
loadPortState,
|
|
80
|
-
loadSecretsForApp,
|
|
81
|
-
type PortState,
|
|
82
|
-
parseComposePortMappings,
|
|
83
|
-
parseComposeServiceNames,
|
|
84
|
-
prepareEntryCredentials,
|
|
85
|
-
type ResolvedServicePorts,
|
|
86
|
-
replacePortInUrl,
|
|
87
|
-
resolveServicePorts,
|
|
88
|
-
rewriteUrlsWithPorts,
|
|
89
|
-
savePortState,
|
|
90
|
-
startComposeServices,
|
|
91
|
-
startWorkspaceServices,
|
|
92
|
-
} from '../credentials';
|
|
93
|
-
|
|
94
|
-
// Re-export execCommand from its own module
|
|
95
|
-
export { type ExecOptions, execCommand } from '../exec';
|
|
96
|
-
|
|
97
|
-
const logger = console;
|
|
98
|
-
|
|
99
|
-
/**
|
|
100
|
-
* Normalize telescope configuration
|
|
101
|
-
* @internal Exported for testing
|
|
102
|
-
*/
|
|
103
|
-
export function normalizeTelescopeConfig(
|
|
104
|
-
config: GkmConfig['telescope'],
|
|
105
|
-
): NormalizedTelescopeConfig | undefined {
|
|
106
|
-
if (config === false) {
|
|
107
|
-
return undefined;
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
// Handle string path (e.g., './src/config/telescope')
|
|
111
|
-
if (typeof config === 'string') {
|
|
112
|
-
const { path: telescopePath, importPattern: telescopeImportPattern } =
|
|
113
|
-
parseModuleConfig(config, 'telescope');
|
|
114
|
-
|
|
115
|
-
return {
|
|
116
|
-
enabled: true,
|
|
117
|
-
telescopePath,
|
|
118
|
-
telescopeImportPattern,
|
|
119
|
-
path: '/__telescope',
|
|
120
|
-
ignore: [],
|
|
121
|
-
recordBody: true,
|
|
122
|
-
maxEntries: 1000,
|
|
123
|
-
websocket: true,
|
|
124
|
-
};
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
// Default to enabled in development mode
|
|
128
|
-
const isEnabled =
|
|
129
|
-
config === true || config === undefined || config.enabled !== false;
|
|
130
|
-
|
|
131
|
-
if (!isEnabled) {
|
|
132
|
-
return undefined;
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
const telescopeConfig: TelescopeConfig =
|
|
136
|
-
typeof config === 'object' ? config : {};
|
|
137
|
-
|
|
138
|
-
return {
|
|
139
|
-
enabled: true,
|
|
140
|
-
path: telescopeConfig.path ?? '/__telescope',
|
|
141
|
-
ignore: telescopeConfig.ignore ?? [],
|
|
142
|
-
recordBody: telescopeConfig.recordBody ?? true,
|
|
143
|
-
maxEntries: telescopeConfig.maxEntries ?? 1000,
|
|
144
|
-
websocket: telescopeConfig.websocket ?? true,
|
|
145
|
-
};
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
/**
|
|
149
|
-
* Normalize studio configuration
|
|
150
|
-
* @internal Exported for testing
|
|
151
|
-
*/
|
|
152
|
-
export function normalizeStudioConfig(
|
|
153
|
-
config: GkmConfig['studio'],
|
|
154
|
-
): NormalizedStudioConfig | undefined {
|
|
155
|
-
if (config === false) {
|
|
156
|
-
return undefined;
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
// Handle string path (e.g., './src/config/studio')
|
|
160
|
-
if (typeof config === 'string') {
|
|
161
|
-
const { path: studioPath, importPattern: studioImportPattern } =
|
|
162
|
-
parseModuleConfig(config, 'studio');
|
|
163
|
-
|
|
164
|
-
return {
|
|
165
|
-
enabled: true,
|
|
166
|
-
studioPath,
|
|
167
|
-
studioImportPattern,
|
|
168
|
-
path: '/__studio',
|
|
169
|
-
schema: 'public',
|
|
170
|
-
};
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
// Default to enabled in development mode
|
|
174
|
-
const isEnabled =
|
|
175
|
-
config === true || config === undefined || config.enabled !== false;
|
|
176
|
-
|
|
177
|
-
if (!isEnabled) {
|
|
178
|
-
return undefined;
|
|
179
|
-
}
|
|
180
|
-
|
|
181
|
-
const studioConfig: StudioConfig = typeof config === 'object' ? config : {};
|
|
182
|
-
|
|
183
|
-
return {
|
|
184
|
-
enabled: true,
|
|
185
|
-
path: studioConfig.path ?? '/__studio',
|
|
186
|
-
schema: studioConfig.schema ?? 'public',
|
|
187
|
-
};
|
|
188
|
-
}
|
|
189
|
-
|
|
190
|
-
/**
|
|
191
|
-
* Normalize hooks configuration
|
|
192
|
-
* @internal Exported for testing
|
|
193
|
-
*/
|
|
194
|
-
export function normalizeHooksConfig(
|
|
195
|
-
config: GkmConfig['hooks'],
|
|
196
|
-
cwd: string = process.cwd(),
|
|
197
|
-
): NormalizedHooksConfig | undefined {
|
|
198
|
-
if (!config?.server) {
|
|
199
|
-
return undefined;
|
|
200
|
-
}
|
|
201
|
-
|
|
202
|
-
// Resolve the path (handle .ts extension)
|
|
203
|
-
const serverPath = config.server.endsWith('.ts')
|
|
204
|
-
? config.server
|
|
205
|
-
: `${config.server}.ts`;
|
|
206
|
-
|
|
207
|
-
const resolvedPath = resolve(cwd, serverPath);
|
|
208
|
-
|
|
209
|
-
return {
|
|
210
|
-
serverHooksPath: resolvedPath,
|
|
211
|
-
};
|
|
212
|
-
}
|
|
213
|
-
|
|
214
|
-
/**
|
|
215
|
-
* Normalize production configuration
|
|
216
|
-
* @internal Exported for testing
|
|
217
|
-
*/
|
|
218
|
-
export function normalizeProductionConfig(
|
|
219
|
-
cliProduction: boolean,
|
|
220
|
-
configProduction?: ProductionConfig,
|
|
221
|
-
): NormalizedProductionConfig | undefined {
|
|
222
|
-
// Production mode is only enabled if --production CLI flag is passed
|
|
223
|
-
if (!cliProduction) {
|
|
224
|
-
return undefined;
|
|
225
|
-
}
|
|
226
|
-
|
|
227
|
-
// Merge CLI flag with config options
|
|
228
|
-
const config = configProduction ?? {};
|
|
229
|
-
|
|
230
|
-
return {
|
|
231
|
-
enabled: true,
|
|
232
|
-
bundle: config.bundle ?? true,
|
|
233
|
-
minify: config.minify ?? true,
|
|
234
|
-
healthCheck: config.healthCheck ?? '/health',
|
|
235
|
-
gracefulShutdown: config.gracefulShutdown ?? true,
|
|
236
|
-
external: config.external ?? [],
|
|
237
|
-
subscribers: config.subscribers ?? 'exclude',
|
|
238
|
-
openapi: config.openapi ?? false,
|
|
239
|
-
optimizedHandlers: config.optimizedHandlers ?? true, // Default to optimized handlers in production
|
|
240
|
-
};
|
|
241
|
-
}
|
|
242
|
-
|
|
243
|
-
/**
|
|
244
|
-
* Get production config from GkmConfig
|
|
245
|
-
* @internal
|
|
246
|
-
*/
|
|
247
|
-
export function getProductionConfigFromGkm(
|
|
248
|
-
config: GkmConfig,
|
|
249
|
-
): ProductionConfig | undefined {
|
|
250
|
-
const serverConfig = config.providers?.server;
|
|
251
|
-
if (typeof serverConfig === 'object') {
|
|
252
|
-
return (serverConfig as ServerConfig).production;
|
|
253
|
-
}
|
|
254
|
-
return undefined;
|
|
255
|
-
}
|
|
256
|
-
|
|
257
|
-
export interface DevOptions {
|
|
258
|
-
port?: number;
|
|
259
|
-
portExplicit?: boolean;
|
|
260
|
-
enableOpenApi?: boolean;
|
|
261
|
-
/** Specific app to run in workspace mode (default: all apps) */
|
|
262
|
-
app?: string;
|
|
263
|
-
/** Filter apps by pattern (passed to turbo --filter) */
|
|
264
|
-
filter?: string;
|
|
265
|
-
/** Entry file to run (bypasses gkm config) */
|
|
266
|
-
entry?: string;
|
|
267
|
-
/** Watch for file changes (default: true with --entry) */
|
|
268
|
-
watch?: boolean;
|
|
269
|
-
}
|
|
270
|
-
|
|
271
|
-
export async function devCommand(options: DevOptions): Promise<void> {
|
|
272
|
-
// Handle --entry mode: run any file with secret injection
|
|
273
|
-
if (options.entry) {
|
|
274
|
-
return entryDevCommand(options);
|
|
275
|
-
}
|
|
276
|
-
|
|
277
|
-
// Load default .env file BEFORE loading config
|
|
278
|
-
// This ensures env vars are available when config and its dependencies are loaded
|
|
279
|
-
const defaultEnv = loadEnvFiles('.env');
|
|
280
|
-
if (defaultEnv.loaded.length > 0) {
|
|
281
|
-
logger.log(`📦 Loaded env: ${defaultEnv.loaded.join(', ')}`);
|
|
282
|
-
}
|
|
283
|
-
|
|
284
|
-
// Check if we're in an app subdirectory
|
|
285
|
-
const appName = getAppNameFromCwd();
|
|
286
|
-
let config: GkmConfig;
|
|
287
|
-
let appRoot: string = process.cwd();
|
|
288
|
-
let secretsRoot: string = process.cwd(); // Where .gkm/secrets/ lives
|
|
289
|
-
let workspaceAppName: string | undefined; // Set if in workspace mode
|
|
290
|
-
let workspaceAppPort: number | undefined; // Port from workspace config
|
|
291
|
-
let workspace: NormalizedWorkspace | undefined; // Set if in workspace mode
|
|
292
|
-
|
|
293
|
-
if (appName) {
|
|
294
|
-
// Try to load app-specific config from workspace
|
|
295
|
-
try {
|
|
296
|
-
const appConfig = await loadAppConfig();
|
|
297
|
-
config = appConfig.gkmConfig;
|
|
298
|
-
appRoot = appConfig.appRoot;
|
|
299
|
-
secretsRoot = appConfig.workspaceRoot;
|
|
300
|
-
workspaceAppName = appConfig.appName;
|
|
301
|
-
workspaceAppPort = appConfig.app.port;
|
|
302
|
-
workspace = appConfig.workspace;
|
|
303
|
-
logger.log(
|
|
304
|
-
`📦 Running app: ${appConfig.appName} on port ${workspaceAppPort}`,
|
|
305
|
-
);
|
|
306
|
-
|
|
307
|
-
// Check if app has an entry point (non-gkm app like better-auth)
|
|
308
|
-
if (appConfig.app.entry) {
|
|
309
|
-
logger.log(`📄 Using entry point: ${appConfig.app.entry}`);
|
|
310
|
-
return entryDevCommand({
|
|
311
|
-
...options,
|
|
312
|
-
entry: appConfig.app.entry,
|
|
313
|
-
port: workspaceAppPort,
|
|
314
|
-
portExplicit: true,
|
|
315
|
-
});
|
|
316
|
-
}
|
|
317
|
-
} catch {
|
|
318
|
-
// Not in a workspace or app not found in workspace - fall back to regular loading
|
|
319
|
-
const loadedConfig = await loadWorkspaceConfig();
|
|
320
|
-
|
|
321
|
-
// Route to workspace dev mode for multi-app workspaces
|
|
322
|
-
if (loadedConfig.type === 'workspace') {
|
|
323
|
-
logger.log('📦 Detected workspace configuration');
|
|
324
|
-
return workspaceDevCommand(loadedConfig.workspace, options);
|
|
325
|
-
}
|
|
326
|
-
|
|
327
|
-
config = loadedConfig.raw as GkmConfig;
|
|
328
|
-
}
|
|
329
|
-
} else {
|
|
330
|
-
// Try to load workspace config
|
|
331
|
-
const loadedConfig = await loadWorkspaceConfig();
|
|
332
|
-
|
|
333
|
-
// Route to workspace dev mode for multi-app workspaces
|
|
334
|
-
if (loadedConfig.type === 'workspace') {
|
|
335
|
-
logger.log('📦 Detected workspace configuration');
|
|
336
|
-
return workspaceDevCommand(loadedConfig.workspace, options);
|
|
337
|
-
}
|
|
338
|
-
|
|
339
|
-
// Single-app mode - use existing logic
|
|
340
|
-
config = loadedConfig.raw as GkmConfig;
|
|
341
|
-
}
|
|
342
|
-
|
|
343
|
-
// Load any additional env files specified in config
|
|
344
|
-
if (config.env) {
|
|
345
|
-
const { loaded, missing } = loadEnvFiles(config.env, appRoot);
|
|
346
|
-
if (loaded.length > 0) {
|
|
347
|
-
logger.log(`📦 Loaded env: ${loaded.join(', ')}`);
|
|
348
|
-
}
|
|
349
|
-
if (missing.length > 0) {
|
|
350
|
-
logger.warn(`⚠️ Missing env files: ${missing.join(', ')}`);
|
|
351
|
-
}
|
|
352
|
-
}
|
|
353
|
-
|
|
354
|
-
// Force server provider for dev mode
|
|
355
|
-
const resolved = resolveProviders(config, { provider: 'server' });
|
|
356
|
-
|
|
357
|
-
logger.log('🚀 Starting development server...');
|
|
358
|
-
logger.log(`Loading routes from: ${config.routes}`);
|
|
359
|
-
if (config.functions) {
|
|
360
|
-
logger.log(`Loading functions from: ${config.functions}`);
|
|
361
|
-
}
|
|
362
|
-
if (config.crons) {
|
|
363
|
-
logger.log(`Loading crons from: ${config.crons}`);
|
|
364
|
-
}
|
|
365
|
-
if (config.subscribers) {
|
|
366
|
-
logger.log(`Loading subscribers from: ${config.subscribers}`);
|
|
367
|
-
}
|
|
368
|
-
logger.log(`Using envParser: ${config.envParser}`);
|
|
369
|
-
|
|
370
|
-
// Parse envParser and logger configuration
|
|
371
|
-
const { path: envParserPath, importPattern: envParserImportPattern } =
|
|
372
|
-
parseModuleConfig(config.envParser, 'envParser');
|
|
373
|
-
const { path: loggerPath, importPattern: loggerImportPattern } =
|
|
374
|
-
parseModuleConfig(config.logger, 'logger');
|
|
375
|
-
|
|
376
|
-
// Normalize telescope configuration
|
|
377
|
-
const telescope = normalizeTelescopeConfig(config.telescope);
|
|
378
|
-
if (telescope) {
|
|
379
|
-
logger.log(`🔭 Telescope enabled at ${telescope.path}`);
|
|
380
|
-
}
|
|
381
|
-
|
|
382
|
-
// Normalize studio configuration
|
|
383
|
-
const studio = normalizeStudioConfig(config.studio);
|
|
384
|
-
if (studio) {
|
|
385
|
-
logger.log(`🗄️ Studio enabled at ${studio.path}`);
|
|
386
|
-
}
|
|
387
|
-
|
|
388
|
-
// Normalize hooks configuration
|
|
389
|
-
const hooks = normalizeHooksConfig(config.hooks, appRoot);
|
|
390
|
-
if (hooks) {
|
|
391
|
-
logger.log(`🪝 Server hooks enabled from ${config.hooks?.server}`);
|
|
392
|
-
}
|
|
393
|
-
|
|
394
|
-
// Resolve OpenAPI configuration
|
|
395
|
-
const openApiConfig = resolveOpenApiConfig(config);
|
|
396
|
-
// Enable OpenAPI docs endpoint if either root config or provider config enables it
|
|
397
|
-
const enableOpenApi = openApiConfig.enabled || resolved.enableOpenApi;
|
|
398
|
-
if (enableOpenApi) {
|
|
399
|
-
logger.log(`📄 OpenAPI output: ${OPENAPI_OUTPUT_PATH}`);
|
|
400
|
-
}
|
|
401
|
-
|
|
402
|
-
const buildContext: BuildContext = {
|
|
403
|
-
envParserPath,
|
|
404
|
-
envParserImportPattern,
|
|
405
|
-
loggerPath,
|
|
406
|
-
loggerImportPattern,
|
|
407
|
-
telescope,
|
|
408
|
-
studio,
|
|
409
|
-
hooks,
|
|
410
|
-
};
|
|
411
|
-
|
|
412
|
-
// Build initial version
|
|
413
|
-
await buildServer(
|
|
414
|
-
config,
|
|
415
|
-
buildContext,
|
|
416
|
-
resolved.providers[0] as LegacyProvider,
|
|
417
|
-
enableOpenApi,
|
|
418
|
-
appRoot,
|
|
419
|
-
);
|
|
420
|
-
|
|
421
|
-
// Generate OpenAPI spec on startup
|
|
422
|
-
if (enableOpenApi) {
|
|
423
|
-
await generateOpenApi(config);
|
|
424
|
-
}
|
|
425
|
-
|
|
426
|
-
// Determine runtime (default to node)
|
|
427
|
-
const runtime: Runtime = config.runtime ?? 'node';
|
|
428
|
-
|
|
429
|
-
// Load secrets for dev mode, resolve Docker ports, and write to JSON file
|
|
430
|
-
let secretsJsonPath: string | undefined;
|
|
431
|
-
const appSecrets = await loadSecretsForApp(secretsRoot, workspaceAppName);
|
|
432
|
-
|
|
433
|
-
// Resolve Docker service ports and rewrite connection URLs
|
|
434
|
-
const resolvedPorts = await resolveServicePorts(secretsRoot);
|
|
435
|
-
if (Object.keys(resolvedPorts.ports).length > 0) {
|
|
436
|
-
const rewritten = rewriteUrlsWithPorts(appSecrets, resolvedPorts);
|
|
437
|
-
Object.assign(appSecrets, rewritten);
|
|
438
|
-
logger.log(
|
|
439
|
-
`🔌 Applied ${Object.keys(resolvedPorts.ports).length} port mapping(s)`,
|
|
440
|
-
);
|
|
441
|
-
}
|
|
442
|
-
|
|
443
|
-
// Inject dependency URLs if in workspace mode
|
|
444
|
-
if (workspace && workspaceAppName) {
|
|
445
|
-
const depEnv = getDependencyEnvVars(workspace, workspaceAppName);
|
|
446
|
-
Object.assign(appSecrets, depEnv);
|
|
447
|
-
}
|
|
448
|
-
|
|
449
|
-
if (Object.keys(appSecrets).length > 0) {
|
|
450
|
-
const secretsDir = join(secretsRoot, '.gkm');
|
|
451
|
-
await mkdir(secretsDir, { recursive: true });
|
|
452
|
-
const secretsFileName = workspaceAppName
|
|
453
|
-
? `dev-secrets-${workspaceAppName}.json`
|
|
454
|
-
: 'dev-secrets.json';
|
|
455
|
-
secretsJsonPath = join(secretsDir, secretsFileName);
|
|
456
|
-
await writeFile(secretsJsonPath, JSON.stringify(appSecrets, null, 2));
|
|
457
|
-
logger.log(`🔐 Loaded ${Object.keys(appSecrets).length} secret(s)`);
|
|
458
|
-
}
|
|
459
|
-
|
|
460
|
-
// Start the dev server
|
|
461
|
-
// Priority: explicit --port option > workspace app port > default 3000
|
|
462
|
-
const devServer = new DevServer(
|
|
463
|
-
resolved.providers[0] as LegacyProvider,
|
|
464
|
-
options.port ?? workspaceAppPort ?? 3000,
|
|
465
|
-
options.portExplicit ?? false,
|
|
466
|
-
enableOpenApi,
|
|
467
|
-
telescope,
|
|
468
|
-
studio,
|
|
469
|
-
runtime,
|
|
470
|
-
appRoot,
|
|
471
|
-
secretsJsonPath,
|
|
472
|
-
);
|
|
473
|
-
|
|
474
|
-
await devServer.start();
|
|
475
|
-
|
|
476
|
-
// Watch for file changes
|
|
477
|
-
const envParserFile = config.envParser.split('#')[0] ?? config.envParser;
|
|
478
|
-
const loggerFile = config.logger.split('#')[0] ?? config.logger;
|
|
479
|
-
|
|
480
|
-
// Get hooks file path for watching
|
|
481
|
-
const hooksFileParts = config.hooks?.server?.split('#');
|
|
482
|
-
const hooksFile = hooksFileParts?.[0];
|
|
483
|
-
|
|
484
|
-
const watchPatterns = [
|
|
485
|
-
config.routes,
|
|
486
|
-
...(config.functions ? [config.functions] : []),
|
|
487
|
-
...(config.crons ? [config.crons] : []),
|
|
488
|
-
...(config.subscribers ? [config.subscribers] : []),
|
|
489
|
-
// Add .ts extension if not present for config files
|
|
490
|
-
envParserFile.endsWith('.ts') ? envParserFile : `${envParserFile}.ts`,
|
|
491
|
-
loggerFile.endsWith('.ts') ? loggerFile : `${loggerFile}.ts`,
|
|
492
|
-
// Add hooks file to watch list
|
|
493
|
-
...(hooksFile
|
|
494
|
-
? [hooksFile.endsWith('.ts') ? hooksFile : `${hooksFile}.ts`]
|
|
495
|
-
: []),
|
|
496
|
-
]
|
|
497
|
-
.flat()
|
|
498
|
-
.filter((p): p is string => typeof p === 'string');
|
|
499
|
-
|
|
500
|
-
// Normalize patterns - remove leading ./ when using cwd option
|
|
501
|
-
const normalizedPatterns = watchPatterns.map((p) =>
|
|
502
|
-
p.startsWith('./') ? p.slice(2) : p,
|
|
503
|
-
);
|
|
504
|
-
|
|
505
|
-
logger.log(`👀 Watching for changes in: ${normalizedPatterns.join(', ')}`);
|
|
506
|
-
|
|
507
|
-
// Resolve glob patterns to actual files (chokidar 4.x doesn't support globs)
|
|
508
|
-
const resolvedFiles = await fg(normalizedPatterns, {
|
|
509
|
-
cwd: appRoot,
|
|
510
|
-
absolute: false,
|
|
511
|
-
onlyFiles: true,
|
|
512
|
-
});
|
|
513
|
-
|
|
514
|
-
// Also watch the directories for new files
|
|
515
|
-
const dirsToWatch = [
|
|
516
|
-
...new Set(
|
|
517
|
-
resolvedFiles.map((f) => {
|
|
518
|
-
const parts = f.split('/');
|
|
519
|
-
return parts.slice(0, -1).join('/');
|
|
520
|
-
}),
|
|
521
|
-
),
|
|
522
|
-
];
|
|
523
|
-
|
|
524
|
-
logger.log(
|
|
525
|
-
`📁 Found ${resolvedFiles.length} files in ${dirsToWatch.length} directories`,
|
|
526
|
-
);
|
|
527
|
-
|
|
528
|
-
const watcher = chokidar.watch([...resolvedFiles, ...dirsToWatch], {
|
|
529
|
-
ignored: /(^|[/\\])\../, // ignore dotfiles
|
|
530
|
-
persistent: true,
|
|
531
|
-
ignoreInitial: true,
|
|
532
|
-
cwd: appRoot,
|
|
533
|
-
});
|
|
534
|
-
|
|
535
|
-
watcher.on('ready', () => {
|
|
536
|
-
logger.log('🔍 File watcher ready');
|
|
537
|
-
});
|
|
538
|
-
|
|
539
|
-
watcher.on('error', (error) => {
|
|
540
|
-
logger.error('❌ Watcher error:', error);
|
|
541
|
-
});
|
|
542
|
-
|
|
543
|
-
let rebuildTimeout: NodeJS.Timeout | null = null;
|
|
544
|
-
|
|
545
|
-
watcher.on('change', async (path) => {
|
|
546
|
-
logger.log(`📝 File changed: ${path}`);
|
|
547
|
-
|
|
548
|
-
// Debounce rebuilds
|
|
549
|
-
if (rebuildTimeout) {
|
|
550
|
-
clearTimeout(rebuildTimeout);
|
|
551
|
-
}
|
|
552
|
-
|
|
553
|
-
rebuildTimeout = setTimeout(async () => {
|
|
554
|
-
try {
|
|
555
|
-
logger.log('🔄 Rebuilding...');
|
|
556
|
-
await buildServer(
|
|
557
|
-
config,
|
|
558
|
-
buildContext,
|
|
559
|
-
resolved.providers[0] as LegacyProvider,
|
|
560
|
-
enableOpenApi,
|
|
561
|
-
appRoot,
|
|
562
|
-
true, // bust module cache on rebuild
|
|
563
|
-
);
|
|
564
|
-
|
|
565
|
-
// Regenerate OpenAPI if enabled
|
|
566
|
-
if (enableOpenApi) {
|
|
567
|
-
await generateOpenApi(config, { silent: true, bustCache: true });
|
|
568
|
-
}
|
|
569
|
-
|
|
570
|
-
logger.log('✅ Rebuild complete, restarting server...');
|
|
571
|
-
await devServer.restart();
|
|
572
|
-
} catch (error) {
|
|
573
|
-
const err = error as Error;
|
|
574
|
-
logger.error(`❌ Rebuild failed: ${err.message}`);
|
|
575
|
-
if (err.stack) {
|
|
576
|
-
logger.error(err.stack);
|
|
577
|
-
}
|
|
578
|
-
}
|
|
579
|
-
}, 300);
|
|
580
|
-
});
|
|
581
|
-
|
|
582
|
-
// Handle graceful shutdown
|
|
583
|
-
let isShuttingDown = false;
|
|
584
|
-
const shutdown = () => {
|
|
585
|
-
if (isShuttingDown) return;
|
|
586
|
-
isShuttingDown = true;
|
|
587
|
-
|
|
588
|
-
logger.log('\n🛑 Shutting down...');
|
|
589
|
-
|
|
590
|
-
// Use sync-style shutdown to ensure it completes before exit
|
|
591
|
-
Promise.all([watcher.close(), devServer.stop()])
|
|
592
|
-
.catch((err) => {
|
|
593
|
-
logger.error('Error during shutdown:', err);
|
|
594
|
-
})
|
|
595
|
-
.finally(() => {
|
|
596
|
-
process.exit(0);
|
|
597
|
-
});
|
|
598
|
-
};
|
|
599
|
-
|
|
600
|
-
process.on('SIGINT', shutdown);
|
|
601
|
-
process.on('SIGTERM', shutdown);
|
|
602
|
-
}
|
|
603
|
-
|
|
604
|
-
/**
|
|
605
|
-
* Generate all dependency environment variables for all apps.
|
|
606
|
-
* Returns a flat object with all {APP_NAME}_URL variables.
|
|
607
|
-
* @internal Exported for testing
|
|
608
|
-
*/
|
|
609
|
-
export function generateAllDependencyEnvVars(
|
|
610
|
-
workspace: NormalizedWorkspace,
|
|
611
|
-
urlPrefix = 'http://localhost',
|
|
612
|
-
): Record<string, string> {
|
|
613
|
-
const env: Record<string, string> = {};
|
|
614
|
-
|
|
615
|
-
for (const appName of Object.keys(workspace.apps)) {
|
|
616
|
-
const appEnv = getDependencyEnvVars(workspace, appName, urlPrefix);
|
|
617
|
-
Object.assign(env, appEnv);
|
|
618
|
-
}
|
|
619
|
-
|
|
620
|
-
return env;
|
|
621
|
-
}
|
|
622
|
-
|
|
623
|
-
/**
|
|
624
|
-
* Check for port conflicts across all apps.
|
|
625
|
-
* Returns list of conflicts if any ports are duplicated.
|
|
626
|
-
* @internal Exported for testing
|
|
627
|
-
*/
|
|
628
|
-
export function checkPortConflicts(
|
|
629
|
-
workspace: NormalizedWorkspace,
|
|
630
|
-
): { app1: string; app2: string; port: number }[] {
|
|
631
|
-
const conflicts: { app1: string; app2: string; port: number }[] = [];
|
|
632
|
-
const portToApp = new Map<number, string>();
|
|
633
|
-
|
|
634
|
-
for (const [appName, app] of Object.entries(workspace.apps)) {
|
|
635
|
-
const existingApp = portToApp.get(app.port);
|
|
636
|
-
if (existingApp) {
|
|
637
|
-
conflicts.push({ app1: existingApp, app2: appName, port: app.port });
|
|
638
|
-
} else {
|
|
639
|
-
portToApp.set(app.port, appName);
|
|
640
|
-
}
|
|
641
|
-
}
|
|
642
|
-
|
|
643
|
-
return conflicts;
|
|
644
|
-
}
|
|
645
|
-
|
|
646
|
-
/**
|
|
647
|
-
* Per-framework validation spec for supported frontend frameworks.
|
|
648
|
-
*/
|
|
649
|
-
interface FrontendFrameworkSpec {
|
|
650
|
-
displayName: string;
|
|
651
|
-
configFiles: string[];
|
|
652
|
-
dependency: string;
|
|
653
|
-
installCommand: string;
|
|
654
|
-
}
|
|
655
|
-
|
|
656
|
-
const FRONTEND_FRAMEWORKS: Record<FrontendFramework, FrontendFrameworkSpec> = {
|
|
657
|
-
nextjs: {
|
|
658
|
-
displayName: 'Next.js',
|
|
659
|
-
configFiles: [
|
|
660
|
-
'next.config.js',
|
|
661
|
-
'next.config.ts',
|
|
662
|
-
'next.config.mjs',
|
|
663
|
-
'next.config.cjs',
|
|
664
|
-
],
|
|
665
|
-
dependency: 'next',
|
|
666
|
-
installCommand: 'pnpm add next react react-dom',
|
|
667
|
-
},
|
|
668
|
-
remix: {
|
|
669
|
-
displayName: 'Remix',
|
|
670
|
-
configFiles: [
|
|
671
|
-
'remix.config.js',
|
|
672
|
-
'remix.config.mjs',
|
|
673
|
-
'remix.config.ts',
|
|
674
|
-
'vite.config.js',
|
|
675
|
-
'vite.config.ts',
|
|
676
|
-
'vite.config.mjs',
|
|
677
|
-
'vite.config.cjs',
|
|
678
|
-
],
|
|
679
|
-
dependency: '@remix-run/dev',
|
|
680
|
-
installCommand: 'pnpm add -D @remix-run/dev',
|
|
681
|
-
},
|
|
682
|
-
vite: {
|
|
683
|
-
displayName: 'Vite',
|
|
684
|
-
configFiles: [
|
|
685
|
-
'vite.config.js',
|
|
686
|
-
'vite.config.ts',
|
|
687
|
-
'vite.config.mjs',
|
|
688
|
-
'vite.config.cjs',
|
|
689
|
-
],
|
|
690
|
-
dependency: 'vite',
|
|
691
|
-
installCommand: 'pnpm add -D vite',
|
|
692
|
-
},
|
|
693
|
-
'tanstack-start': {
|
|
694
|
-
displayName: 'TanStack Start',
|
|
695
|
-
configFiles: [
|
|
696
|
-
'vite.config.js',
|
|
697
|
-
'vite.config.ts',
|
|
698
|
-
'vite.config.mjs',
|
|
699
|
-
'vite.config.cjs',
|
|
700
|
-
],
|
|
701
|
-
dependency: '@tanstack/react-start',
|
|
702
|
-
installCommand: 'pnpm add @tanstack/react-start @tanstack/react-router',
|
|
703
|
-
},
|
|
704
|
-
expo: {
|
|
705
|
-
displayName: 'Expo',
|
|
706
|
-
configFiles: ['app.config.ts', 'app.config.js', 'app.json'],
|
|
707
|
-
dependency: 'expo',
|
|
708
|
-
installCommand: 'pnpm add expo',
|
|
709
|
-
},
|
|
710
|
-
};
|
|
711
|
-
|
|
712
|
-
/**
|
|
713
|
-
* Auto-detect the frontend framework by scanning package.json deps and config
|
|
714
|
-
* files. Dependency match wins over config file match because deps are more
|
|
715
|
-
* distinctive (a Remix app has both vite.config.ts AND @remix-run/dev).
|
|
716
|
-
*/
|
|
717
|
-
function detectFrontendFramework(
|
|
718
|
-
fullPath: string,
|
|
719
|
-
deps: Record<string, string>,
|
|
720
|
-
): FrontendFramework | undefined {
|
|
721
|
-
// tanstack-start before vite/remix so its more specific dep wins; vite last
|
|
722
|
-
// because plain vite is the fallback when neither nextjs/remix/tanstack match.
|
|
723
|
-
// Order matters: more specific deps win over general ones.
|
|
724
|
-
// expo first (own dep), then tanstack-start (uses vite), remix (uses vite),
|
|
725
|
-
// nextjs (own dep), and plain vite last as the fallback.
|
|
726
|
-
const order: FrontendFramework[] = [
|
|
727
|
-
'expo',
|
|
728
|
-
'tanstack-start',
|
|
729
|
-
'nextjs',
|
|
730
|
-
'remix',
|
|
731
|
-
'vite',
|
|
732
|
-
];
|
|
733
|
-
for (const name of order) {
|
|
734
|
-
if (deps[FRONTEND_FRAMEWORKS[name].dependency]) {
|
|
735
|
-
return name;
|
|
736
|
-
}
|
|
737
|
-
}
|
|
738
|
-
for (const name of order) {
|
|
739
|
-
const hasConfig = FRONTEND_FRAMEWORKS[name].configFiles.some((file) =>
|
|
740
|
-
existsSync(join(fullPath, file)),
|
|
741
|
-
);
|
|
742
|
-
if (hasConfig) {
|
|
743
|
-
return name;
|
|
744
|
-
}
|
|
745
|
-
}
|
|
746
|
-
return undefined;
|
|
747
|
-
}
|
|
748
|
-
|
|
749
|
-
/**
|
|
750
|
-
* Validation result for a frontend app.
|
|
751
|
-
*/
|
|
752
|
-
export interface FrontendValidationResult {
|
|
753
|
-
appName: string;
|
|
754
|
-
valid: boolean;
|
|
755
|
-
errors: string[];
|
|
756
|
-
warnings: string[];
|
|
757
|
-
}
|
|
758
|
-
|
|
759
|
-
/**
|
|
760
|
-
* Validate a frontend app configuration.
|
|
761
|
-
*
|
|
762
|
-
* If `framework` is provided, validates strictly against that framework's
|
|
763
|
-
* expected config file and dependency. Otherwise auto-detects the framework
|
|
764
|
-
* from package.json and config files. If no recognized framework is found, the
|
|
765
|
-
* app is allowed through with a warning provided it has a `dev` script.
|
|
766
|
-
*
|
|
767
|
-
* @internal Exported for testing
|
|
768
|
-
*/
|
|
769
|
-
export async function validateFrontendApp(
|
|
770
|
-
appName: string,
|
|
771
|
-
appPath: string,
|
|
772
|
-
workspaceRoot: string,
|
|
773
|
-
framework?: FrontendFramework,
|
|
774
|
-
): Promise<FrontendValidationResult> {
|
|
775
|
-
const errors: string[] = [];
|
|
776
|
-
const warnings: string[] = [];
|
|
777
|
-
const fullPath = join(workspaceRoot, appPath);
|
|
778
|
-
|
|
779
|
-
const packageJsonPath = join(fullPath, 'package.json');
|
|
780
|
-
if (!existsSync(packageJsonPath)) {
|
|
781
|
-
errors.push(
|
|
782
|
-
`package.json not found at ${appPath}. Run: pnpm init in the app directory.`,
|
|
783
|
-
);
|
|
784
|
-
return { appName, valid: false, errors, warnings };
|
|
785
|
-
}
|
|
786
|
-
|
|
787
|
-
let pkg: {
|
|
788
|
-
dependencies?: Record<string, string>;
|
|
789
|
-
devDependencies?: Record<string, string>;
|
|
790
|
-
scripts?: Record<string, string>;
|
|
791
|
-
};
|
|
792
|
-
try {
|
|
793
|
-
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
794
|
-
pkg = require(packageJsonPath);
|
|
795
|
-
} catch {
|
|
796
|
-
errors.push(`Failed to read package.json at ${packageJsonPath}`);
|
|
797
|
-
return { appName, valid: false, errors, warnings };
|
|
798
|
-
}
|
|
799
|
-
|
|
800
|
-
const deps: Record<string, string> = {
|
|
801
|
-
...pkg.dependencies,
|
|
802
|
-
...pkg.devDependencies,
|
|
803
|
-
};
|
|
804
|
-
|
|
805
|
-
const resolvedFramework =
|
|
806
|
-
framework ?? detectFrontendFramework(fullPath, deps);
|
|
807
|
-
|
|
808
|
-
if (resolvedFramework) {
|
|
809
|
-
const spec = FRONTEND_FRAMEWORKS[resolvedFramework];
|
|
810
|
-
const hasConfig = spec.configFiles.some((file) =>
|
|
811
|
-
existsSync(join(fullPath, file)),
|
|
812
|
-
);
|
|
813
|
-
if (!hasConfig) {
|
|
814
|
-
errors.push(
|
|
815
|
-
`${spec.displayName} config file not found. Expected one of: ${spec.configFiles.join(', ')}`,
|
|
816
|
-
);
|
|
817
|
-
}
|
|
818
|
-
if (!deps[spec.dependency]) {
|
|
819
|
-
errors.push(
|
|
820
|
-
`${spec.displayName} not found in dependencies. Run: ${spec.installCommand}`,
|
|
821
|
-
);
|
|
822
|
-
}
|
|
823
|
-
} else {
|
|
824
|
-
warnings.push(
|
|
825
|
-
'No recognized frontend framework detected (Next.js, Vite, Remix). ' +
|
|
826
|
-
'The app will run via its "dev" script. Set `framework` in your app config to enable strict validation.',
|
|
827
|
-
);
|
|
828
|
-
}
|
|
829
|
-
|
|
830
|
-
if (!pkg.scripts?.dev) {
|
|
831
|
-
if (resolvedFramework) {
|
|
832
|
-
warnings.push(
|
|
833
|
-
'No "dev" script found in package.json. Turbo expects a "dev" script to run.',
|
|
834
|
-
);
|
|
835
|
-
} else {
|
|
836
|
-
errors.push(
|
|
837
|
-
'No "dev" script found in package.json. Without a recognized framework or a dev script, there is nothing to run.',
|
|
838
|
-
);
|
|
839
|
-
}
|
|
840
|
-
}
|
|
841
|
-
|
|
842
|
-
return {
|
|
843
|
-
appName,
|
|
844
|
-
valid: errors.length === 0,
|
|
845
|
-
errors,
|
|
846
|
-
warnings,
|
|
847
|
-
};
|
|
848
|
-
}
|
|
849
|
-
|
|
850
|
-
/**
|
|
851
|
-
* Validate all frontend apps in the workspace.
|
|
852
|
-
* Returns validation results for each frontend app.
|
|
853
|
-
* @internal Exported for testing
|
|
854
|
-
*/
|
|
855
|
-
export async function validateFrontendApps(
|
|
856
|
-
workspace: NormalizedWorkspace,
|
|
857
|
-
): Promise<FrontendValidationResult[]> {
|
|
858
|
-
const results: FrontendValidationResult[] = [];
|
|
859
|
-
|
|
860
|
-
for (const [appName, app] of Object.entries(workspace.apps)) {
|
|
861
|
-
if (app.type === 'web') {
|
|
862
|
-
const result = await validateFrontendApp(
|
|
863
|
-
appName,
|
|
864
|
-
app.path,
|
|
865
|
-
workspace.root,
|
|
866
|
-
app.framework as FrontendFramework | undefined,
|
|
867
|
-
);
|
|
868
|
-
results.push(result);
|
|
869
|
-
}
|
|
870
|
-
}
|
|
871
|
-
|
|
872
|
-
return results;
|
|
873
|
-
}
|
|
874
|
-
|
|
875
|
-
/**
|
|
876
|
-
* Load secrets for development stage.
|
|
877
|
-
* Returns env vars to inject, or empty object if secrets not configured/found.
|
|
878
|
-
* @internal Exported for testing
|
|
879
|
-
*/
|
|
880
|
-
export async function loadDevSecrets(
|
|
881
|
-
workspace: NormalizedWorkspace,
|
|
882
|
-
): Promise<Record<string, string>> {
|
|
883
|
-
// Check if secrets are enabled in workspace config
|
|
884
|
-
if (!workspace.secrets.enabled) {
|
|
885
|
-
return {};
|
|
886
|
-
}
|
|
887
|
-
|
|
888
|
-
// Try 'dev' stage first, then 'development'
|
|
889
|
-
const stages = ['dev', 'development'];
|
|
890
|
-
|
|
891
|
-
for (const stage of stages) {
|
|
892
|
-
if (secretsExist(stage, workspace.root)) {
|
|
893
|
-
const secrets = await readStageSecrets(stage, workspace.root);
|
|
894
|
-
if (secrets) {
|
|
895
|
-
logger.log(`🔐 Loading secrets from stage: ${stage}`);
|
|
896
|
-
return toEmbeddableSecrets(secrets);
|
|
897
|
-
}
|
|
898
|
-
}
|
|
899
|
-
}
|
|
900
|
-
|
|
901
|
-
logger.warn(
|
|
902
|
-
'⚠️ Secrets enabled but no dev/development secrets found. Run "gkm setup" to initialize your development environment',
|
|
903
|
-
);
|
|
904
|
-
return {};
|
|
905
|
-
}
|
|
906
|
-
|
|
907
|
-
/**
|
|
908
|
-
* Workspace dev command - orchestrates multi-app development using Turbo.
|
|
909
|
-
*
|
|
910
|
-
* Flow:
|
|
911
|
-
* 1. Check for port conflicts
|
|
912
|
-
* 2. Start docker-compose services (db, cache, mail)
|
|
913
|
-
* 3. Generate dependency URLs ({APP_NAME}_URL)
|
|
914
|
-
* 4. Spawn turbo run dev with injected env vars
|
|
915
|
-
*/
|
|
916
|
-
async function workspaceDevCommand(
|
|
917
|
-
workspace: NormalizedWorkspace,
|
|
918
|
-
options: DevOptions,
|
|
919
|
-
): Promise<void> {
|
|
920
|
-
const appCount = Object.keys(workspace.apps).length;
|
|
921
|
-
const backendApps = Object.entries(workspace.apps).filter(
|
|
922
|
-
([_, app]) => app.type === 'backend',
|
|
923
|
-
);
|
|
924
|
-
const frontendApps = Object.entries(workspace.apps).filter(
|
|
925
|
-
([_, app]) => app.type === 'web',
|
|
926
|
-
);
|
|
927
|
-
const mobileApps = Object.entries(workspace.apps).filter(
|
|
928
|
-
([_, app]) => app.type === 'mobile',
|
|
929
|
-
);
|
|
930
|
-
|
|
931
|
-
logger.log(`\n🚀 Starting workspace: ${workspace.name}`);
|
|
932
|
-
const counts = [
|
|
933
|
-
`${backendApps.length} backend`,
|
|
934
|
-
`${frontendApps.length} web`,
|
|
935
|
-
];
|
|
936
|
-
if (mobileApps.length > 0) counts.push(`${mobileApps.length} mobile`);
|
|
937
|
-
logger.log(` ${counts.join(', ')} app(s)`);
|
|
938
|
-
|
|
939
|
-
// Check for port conflicts
|
|
940
|
-
const conflicts = checkPortConflicts(workspace);
|
|
941
|
-
if (conflicts.length > 0) {
|
|
942
|
-
for (const conflict of conflicts) {
|
|
943
|
-
logger.error(
|
|
944
|
-
`❌ Port conflict: Apps "${conflict.app1}" and "${conflict.app2}" both use port ${conflict.port}`,
|
|
945
|
-
);
|
|
946
|
-
}
|
|
947
|
-
throw new Error(
|
|
948
|
-
'Port conflicts detected. Please assign unique ports to each app.',
|
|
949
|
-
);
|
|
950
|
-
}
|
|
951
|
-
|
|
952
|
-
// Validate frontend apps (Next.js setup)
|
|
953
|
-
if (frontendApps.length > 0) {
|
|
954
|
-
logger.log('\n🔍 Validating frontend apps...');
|
|
955
|
-
const validationResults = await validateFrontendApps(workspace);
|
|
956
|
-
|
|
957
|
-
let hasErrors = false;
|
|
958
|
-
for (const result of validationResults) {
|
|
959
|
-
if (!result.valid) {
|
|
960
|
-
hasErrors = true;
|
|
961
|
-
logger.error(
|
|
962
|
-
`\n❌ Frontend app "${result.appName}" validation failed:`,
|
|
963
|
-
);
|
|
964
|
-
for (const error of result.errors) {
|
|
965
|
-
logger.error(` • ${error}`);
|
|
966
|
-
}
|
|
967
|
-
}
|
|
968
|
-
for (const warning of result.warnings) {
|
|
969
|
-
logger.warn(` ⚠️ ${result.appName}: ${warning}`);
|
|
970
|
-
}
|
|
971
|
-
}
|
|
972
|
-
|
|
973
|
-
if (hasErrors) {
|
|
974
|
-
throw new Error(
|
|
975
|
-
'Frontend app validation failed. Fix the issues above and try again.',
|
|
976
|
-
);
|
|
977
|
-
}
|
|
978
|
-
logger.log('✅ Frontend apps validated');
|
|
979
|
-
}
|
|
980
|
-
|
|
981
|
-
// Frontend apps import API clients directly from backend packages
|
|
982
|
-
// (e.g. import { createApi } from '@myapp/api/client')
|
|
983
|
-
// No file copying needed — pnpm workspace resolution handles it.
|
|
984
|
-
|
|
985
|
-
// Resolve dynamic service ports from docker-compose.yml
|
|
986
|
-
const resolvedPorts = await resolveServicePorts(workspace.root);
|
|
987
|
-
|
|
988
|
-
// Load secrets BEFORE starting Docker so POSTGRES_USER, POSTGRES_PASSWORD,
|
|
989
|
-
// etc. are available for docker-compose variable interpolation
|
|
990
|
-
const rawSecrets = await loadDevSecrets(workspace);
|
|
991
|
-
|
|
992
|
-
// Start docker-compose services with resolved ports AND secrets
|
|
993
|
-
await startWorkspaceServices(workspace, resolvedPorts.dockerEnv, rawSecrets);
|
|
994
|
-
|
|
995
|
-
// Rewrite URLs with resolved ports (hostnames and port numbers)
|
|
996
|
-
const secretsEnv = rewriteUrlsWithPorts(rawSecrets, resolvedPorts);
|
|
997
|
-
if (Object.keys(secretsEnv).length > 0) {
|
|
998
|
-
logger.log(` Loaded ${Object.keys(secretsEnv).length} secret(s)`);
|
|
999
|
-
}
|
|
1000
|
-
|
|
1001
|
-
// Generate dependency URLs
|
|
1002
|
-
const dependencyEnv = generateAllDependencyEnvVars(workspace);
|
|
1003
|
-
if (Object.keys(dependencyEnv).length > 0) {
|
|
1004
|
-
logger.log('📡 Dependency URLs:');
|
|
1005
|
-
for (const [key, value] of Object.entries(dependencyEnv)) {
|
|
1006
|
-
logger.log(` ${key}=${value}`);
|
|
1007
|
-
}
|
|
1008
|
-
}
|
|
1009
|
-
|
|
1010
|
-
// Build turbo filter
|
|
1011
|
-
let turboFilter: string[] = [];
|
|
1012
|
-
if (options.app) {
|
|
1013
|
-
// Run specific app
|
|
1014
|
-
if (!workspace.apps[options.app]) {
|
|
1015
|
-
const appNames = Object.keys(workspace.apps).join(', ');
|
|
1016
|
-
throw new Error(
|
|
1017
|
-
`App "${options.app}" not found. Available apps: ${appNames}`,
|
|
1018
|
-
);
|
|
1019
|
-
}
|
|
1020
|
-
turboFilter = ['--filter', options.app];
|
|
1021
|
-
logger.log(`\n🎯 Running single app: ${options.app}`);
|
|
1022
|
-
} else if (options.filter) {
|
|
1023
|
-
// Use custom filter
|
|
1024
|
-
turboFilter = ['--filter', options.filter];
|
|
1025
|
-
logger.log(`\n🔍 Using filter: ${options.filter}`);
|
|
1026
|
-
} else {
|
|
1027
|
-
// Run all apps
|
|
1028
|
-
logger.log(`\n🎯 Running all ${appCount} apps`);
|
|
1029
|
-
}
|
|
1030
|
-
|
|
1031
|
-
// List apps and their ports
|
|
1032
|
-
const buildOrder = getAppBuildOrder(workspace);
|
|
1033
|
-
logger.log('\n📋 Apps (in dependency order):');
|
|
1034
|
-
for (const appName of buildOrder) {
|
|
1035
|
-
const app = workspace.apps[appName];
|
|
1036
|
-
if (!app) continue;
|
|
1037
|
-
const deps =
|
|
1038
|
-
app.dependencies.length > 0
|
|
1039
|
-
? ` (depends on: ${app.dependencies.join(', ')})`
|
|
1040
|
-
: '';
|
|
1041
|
-
const icon =
|
|
1042
|
-
app.type === 'backend' ? '🔧' : app.type === 'mobile' ? '📱' : '🌐';
|
|
1043
|
-
logger.log(` ${icon} ${appName} → http://localhost:${app.port}${deps}`);
|
|
1044
|
-
}
|
|
1045
|
-
|
|
1046
|
-
// Find the config file path for GKM_CONFIG_PATH
|
|
1047
|
-
const configFiles = ['gkm.config.ts', 'gkm.config.js', 'gkm.config.json'];
|
|
1048
|
-
let configPath = '';
|
|
1049
|
-
for (const file of configFiles) {
|
|
1050
|
-
const fullPath = join(workspace.root, file);
|
|
1051
|
-
if (existsSync(fullPath)) {
|
|
1052
|
-
configPath = fullPath;
|
|
1053
|
-
break;
|
|
1054
|
-
}
|
|
1055
|
-
}
|
|
1056
|
-
|
|
1057
|
-
// Prepare environment variables
|
|
1058
|
-
// Order matters: secrets first, then dependencies (dependencies can override)
|
|
1059
|
-
const turboEnv: Record<string, string> = {
|
|
1060
|
-
...process.env,
|
|
1061
|
-
...secretsEnv,
|
|
1062
|
-
...dependencyEnv,
|
|
1063
|
-
NODE_ENV: 'development',
|
|
1064
|
-
// Inject config path so child processes can find the workspace config
|
|
1065
|
-
...(configPath ? { GKM_CONFIG_PATH: configPath } : {}),
|
|
1066
|
-
};
|
|
1067
|
-
|
|
1068
|
-
// Spawn turbo run dev
|
|
1069
|
-
logger.log('\n🏃 Starting turbo run dev...\n');
|
|
1070
|
-
|
|
1071
|
-
const turboProcess = spawn('pnpm', ['turbo', 'run', 'dev', ...turboFilter], {
|
|
1072
|
-
cwd: workspace.root,
|
|
1073
|
-
stdio: 'inherit',
|
|
1074
|
-
env: turboEnv,
|
|
1075
|
-
detached: true,
|
|
1076
|
-
});
|
|
1077
|
-
|
|
1078
|
-
// No file watcher needed — frontend apps import API clients directly
|
|
1079
|
-
// from backend packages via workspace dependencies.
|
|
1080
|
-
|
|
1081
|
-
// Handle graceful shutdown
|
|
1082
|
-
let isShuttingDown = false;
|
|
1083
|
-
const shutdown = () => {
|
|
1084
|
-
if (isShuttingDown) return;
|
|
1085
|
-
isShuttingDown = true;
|
|
1086
|
-
|
|
1087
|
-
logger.log('\n🛑 Shutting down workspace...');
|
|
1088
|
-
|
|
1089
|
-
// Kill turbo process group
|
|
1090
|
-
const pid = turboProcess.pid;
|
|
1091
|
-
if (pid) {
|
|
1092
|
-
try {
|
|
1093
|
-
process.kill(-pid, 'SIGTERM');
|
|
1094
|
-
} catch {
|
|
1095
|
-
try {
|
|
1096
|
-
process.kill(pid, 'SIGTERM');
|
|
1097
|
-
} catch {
|
|
1098
|
-
// Process already dead
|
|
1099
|
-
}
|
|
1100
|
-
}
|
|
1101
|
-
}
|
|
1102
|
-
|
|
1103
|
-
// Force kill after timeout if processes are still alive
|
|
1104
|
-
setTimeout(() => {
|
|
1105
|
-
if (pid) {
|
|
1106
|
-
try {
|
|
1107
|
-
process.kill(-pid, 'SIGKILL');
|
|
1108
|
-
} catch {
|
|
1109
|
-
try {
|
|
1110
|
-
process.kill(pid, 'SIGKILL');
|
|
1111
|
-
} catch {
|
|
1112
|
-
// Process already dead
|
|
1113
|
-
}
|
|
1114
|
-
}
|
|
1115
|
-
}
|
|
1116
|
-
process.exit(0);
|
|
1117
|
-
}, 3000);
|
|
1118
|
-
};
|
|
1119
|
-
|
|
1120
|
-
process.on('SIGINT', shutdown);
|
|
1121
|
-
process.on('SIGTERM', shutdown);
|
|
1122
|
-
|
|
1123
|
-
// Wait for turbo to exit
|
|
1124
|
-
return new Promise((resolve, reject) => {
|
|
1125
|
-
turboProcess.on('error', (error) => {
|
|
1126
|
-
logger.error('❌ Turbo error:', error);
|
|
1127
|
-
reject(error);
|
|
1128
|
-
});
|
|
1129
|
-
|
|
1130
|
-
turboProcess.on('exit', (code) => {
|
|
1131
|
-
if (code !== null && code !== 0) {
|
|
1132
|
-
reject(new Error(`Turbo exited with code ${code}`));
|
|
1133
|
-
} else {
|
|
1134
|
-
resolve();
|
|
1135
|
-
}
|
|
1136
|
-
});
|
|
1137
|
-
});
|
|
1138
|
-
}
|
|
1139
|
-
|
|
1140
|
-
async function buildServer(
|
|
1141
|
-
config: any,
|
|
1142
|
-
context: BuildContext,
|
|
1143
|
-
provider: LegacyProvider,
|
|
1144
|
-
enableOpenApi: boolean,
|
|
1145
|
-
appRoot: string = process.cwd(),
|
|
1146
|
-
bustCache = false,
|
|
1147
|
-
): Promise<void> {
|
|
1148
|
-
// Initialize generators
|
|
1149
|
-
const endpointGenerator = new EndpointGenerator();
|
|
1150
|
-
const functionGenerator = new FunctionGenerator();
|
|
1151
|
-
const cronGenerator = new CronGenerator();
|
|
1152
|
-
const subscriberGenerator = new SubscriberGenerator();
|
|
1153
|
-
const queueGenerator = new QueueGenerator();
|
|
1154
|
-
const topicGenerator = new TopicGenerator();
|
|
1155
|
-
|
|
1156
|
-
// Load all constructs (resolve paths relative to appRoot)
|
|
1157
|
-
const [
|
|
1158
|
-
allEndpoints,
|
|
1159
|
-
allFunctions,
|
|
1160
|
-
allCrons,
|
|
1161
|
-
allSubscribers,
|
|
1162
|
-
allQueues,
|
|
1163
|
-
allTopics,
|
|
1164
|
-
] = await Promise.all([
|
|
1165
|
-
endpointGenerator.load(config.routes, appRoot, bustCache),
|
|
1166
|
-
config.functions
|
|
1167
|
-
? functionGenerator.load(config.functions, appRoot, bustCache)
|
|
1168
|
-
: [],
|
|
1169
|
-
config.crons ? cronGenerator.load(config.crons, appRoot, bustCache) : [],
|
|
1170
|
-
config.subscribers
|
|
1171
|
-
? subscriberGenerator.load(config.subscribers, appRoot, bustCache)
|
|
1172
|
-
: [],
|
|
1173
|
-
config.queues ? queueGenerator.load(config.queues, appRoot, bustCache) : [],
|
|
1174
|
-
config.topics ? topicGenerator.load(config.topics, appRoot, bustCache) : [],
|
|
1175
|
-
]);
|
|
1176
|
-
|
|
1177
|
-
// Ensure .gkm directory exists in app root
|
|
1178
|
-
const outputDir = join(appRoot, '.gkm', provider);
|
|
1179
|
-
await mkdir(outputDir, { recursive: true });
|
|
1180
|
-
|
|
1181
|
-
// Build for server provider
|
|
1182
|
-
await Promise.all([
|
|
1183
|
-
endpointGenerator.build(context, allEndpoints, outputDir, {
|
|
1184
|
-
provider,
|
|
1185
|
-
enableOpenApi,
|
|
1186
|
-
}),
|
|
1187
|
-
functionGenerator.build(context, allFunctions, outputDir, { provider }),
|
|
1188
|
-
cronGenerator.build(context, allCrons, outputDir, { provider }),
|
|
1189
|
-
subscriberGenerator.build(context, allSubscribers, outputDir, { provider }),
|
|
1190
|
-
queueGenerator.build(context, allQueues, outputDir, { provider }),
|
|
1191
|
-
topicGenerator.build(context, allTopics, outputDir, { provider }),
|
|
1192
|
-
]);
|
|
1193
|
-
}
|
|
1194
|
-
|
|
1195
|
-
/**
|
|
1196
|
-
* Run any TypeScript file with secret injection.
|
|
1197
|
-
* Does not require gkm.config.ts.
|
|
1198
|
-
*/
|
|
1199
|
-
async function entryDevCommand(options: DevOptions): Promise<void> {
|
|
1200
|
-
const { entry, watch = true } = options;
|
|
1201
|
-
|
|
1202
|
-
if (!entry) {
|
|
1203
|
-
throw new Error('--entry requires a file path');
|
|
1204
|
-
}
|
|
1205
|
-
|
|
1206
|
-
const entryPath = resolve(process.cwd(), entry);
|
|
1207
|
-
|
|
1208
|
-
if (!existsSync(entryPath)) {
|
|
1209
|
-
throw new Error(`Entry file not found: ${entryPath}`);
|
|
1210
|
-
}
|
|
1211
|
-
|
|
1212
|
-
// Load .env files
|
|
1213
|
-
const defaultEnv = loadEnvFiles('.env');
|
|
1214
|
-
if (defaultEnv.loaded.length > 0) {
|
|
1215
|
-
logger.log(`📦 Loaded env: ${defaultEnv.loaded.join(', ')}`);
|
|
1216
|
-
}
|
|
1217
|
-
|
|
1218
|
-
// Prepare credentials (loads workspace config, secrets, injects PORT)
|
|
1219
|
-
// Only pass explicitPort if --port was actually specified by the user
|
|
1220
|
-
const { credentials, resolvedPort, secretsJsonPath, appName } =
|
|
1221
|
-
await prepareEntryCredentials({
|
|
1222
|
-
explicitPort: options.portExplicit ? options.port : undefined,
|
|
1223
|
-
});
|
|
1224
|
-
|
|
1225
|
-
if (appName) {
|
|
1226
|
-
logger.log(`📦 App: ${appName} (port ${resolvedPort})`);
|
|
1227
|
-
}
|
|
1228
|
-
|
|
1229
|
-
logger.log(`🚀 Starting entry file: ${entry} on port ${resolvedPort}`);
|
|
1230
|
-
|
|
1231
|
-
if (Object.keys(credentials).length > 1) {
|
|
1232
|
-
logger.log(
|
|
1233
|
-
`🔐 Loaded ${Object.keys(credentials).length - 1} secret(s) + PORT`,
|
|
1234
|
-
);
|
|
1235
|
-
}
|
|
1236
|
-
|
|
1237
|
-
// Create wrapper entry that injects secrets before importing user's file
|
|
1238
|
-
const wrapperDir = join(process.cwd(), '.gkm');
|
|
1239
|
-
await mkdir(wrapperDir, { recursive: true });
|
|
1240
|
-
const wrapperPath = join(wrapperDir, 'entry-wrapper.ts');
|
|
1241
|
-
await createEntryWrapper(wrapperPath, entryPath, secretsJsonPath);
|
|
1242
|
-
|
|
1243
|
-
// Start with tsx
|
|
1244
|
-
const runner = new EntryRunner(wrapperPath, entryPath, watch, resolvedPort);
|
|
1245
|
-
await runner.start();
|
|
1246
|
-
|
|
1247
|
-
// Handle graceful shutdown
|
|
1248
|
-
let isShuttingDown = false;
|
|
1249
|
-
const shutdown = () => {
|
|
1250
|
-
if (isShuttingDown) return;
|
|
1251
|
-
isShuttingDown = true;
|
|
1252
|
-
|
|
1253
|
-
logger.log('\n🛑 Shutting down...');
|
|
1254
|
-
runner.stop();
|
|
1255
|
-
process.exit(0);
|
|
1256
|
-
};
|
|
1257
|
-
|
|
1258
|
-
process.on('SIGINT', shutdown);
|
|
1259
|
-
process.on('SIGTERM', shutdown);
|
|
1260
|
-
|
|
1261
|
-
// Keep the process alive
|
|
1262
|
-
await new Promise(() => {});
|
|
1263
|
-
}
|
|
1264
|
-
|
|
1265
|
-
/**
|
|
1266
|
-
* Runs and watches a TypeScript entry file using tsx.
|
|
1267
|
-
*/
|
|
1268
|
-
class EntryRunner {
|
|
1269
|
-
private childProcess: ChildProcess | null = null;
|
|
1270
|
-
private watcher: ReturnType<typeof chokidar.watch> | null = null;
|
|
1271
|
-
private isRunning = false;
|
|
1272
|
-
|
|
1273
|
-
constructor(
|
|
1274
|
-
private wrapperPath: string,
|
|
1275
|
-
private entryPath: string,
|
|
1276
|
-
private watch: boolean,
|
|
1277
|
-
private port: number,
|
|
1278
|
-
) {}
|
|
1279
|
-
|
|
1280
|
-
async start(): Promise<void> {
|
|
1281
|
-
await this.runProcess();
|
|
1282
|
-
|
|
1283
|
-
if (this.watch) {
|
|
1284
|
-
// Watch the entry file's directory for changes
|
|
1285
|
-
const watchDir = dirname(this.entryPath);
|
|
1286
|
-
|
|
1287
|
-
this.watcher = chokidar.watch(watchDir, {
|
|
1288
|
-
ignored: /(^|[/\\])\../,
|
|
1289
|
-
persistent: true,
|
|
1290
|
-
ignoreInitial: true,
|
|
1291
|
-
});
|
|
1292
|
-
|
|
1293
|
-
let restartTimeout: NodeJS.Timeout | null = null;
|
|
1294
|
-
|
|
1295
|
-
this.watcher.on('change', (path) => {
|
|
1296
|
-
logger.log(`📝 File changed: ${path}`);
|
|
1297
|
-
|
|
1298
|
-
// Debounce restarts
|
|
1299
|
-
if (restartTimeout) {
|
|
1300
|
-
clearTimeout(restartTimeout);
|
|
1301
|
-
}
|
|
1302
|
-
|
|
1303
|
-
restartTimeout = setTimeout(async () => {
|
|
1304
|
-
logger.log('🔄 Restarting...');
|
|
1305
|
-
await this.restart();
|
|
1306
|
-
}, 300);
|
|
1307
|
-
});
|
|
1308
|
-
|
|
1309
|
-
logger.log(`👀 Watching for changes in: ${watchDir}`);
|
|
1310
|
-
}
|
|
1311
|
-
}
|
|
1312
|
-
|
|
1313
|
-
private async runProcess(): Promise<void> {
|
|
1314
|
-
// Pass PORT as environment variable
|
|
1315
|
-
const env = { ...process.env, PORT: String(this.port) };
|
|
1316
|
-
|
|
1317
|
-
this.childProcess = spawn('npx', ['tsx', this.wrapperPath], {
|
|
1318
|
-
stdio: 'inherit',
|
|
1319
|
-
env,
|
|
1320
|
-
detached: true,
|
|
1321
|
-
});
|
|
1322
|
-
|
|
1323
|
-
this.isRunning = true;
|
|
1324
|
-
|
|
1325
|
-
this.childProcess.on('error', (error) => {
|
|
1326
|
-
logger.error('❌ Process error:', error);
|
|
1327
|
-
});
|
|
1328
|
-
|
|
1329
|
-
this.childProcess.on('exit', (code) => {
|
|
1330
|
-
if (code !== null && code !== 0 && code !== 143) {
|
|
1331
|
-
// 143 = SIGTERM
|
|
1332
|
-
logger.error(`❌ Process exited with code ${code}`);
|
|
1333
|
-
}
|
|
1334
|
-
this.isRunning = false;
|
|
1335
|
-
});
|
|
1336
|
-
|
|
1337
|
-
// Give the process a moment to start
|
|
1338
|
-
await new Promise((resolve) => setTimeout(resolve, 500));
|
|
1339
|
-
|
|
1340
|
-
if (this.isRunning) {
|
|
1341
|
-
logger.log('');
|
|
1342
|
-
logger.log(
|
|
1343
|
-
` \x1b[32m✓ Ready\x1b[0m at \x1b[36mhttp://localhost:${this.port}\x1b[0m`,
|
|
1344
|
-
);
|
|
1345
|
-
logger.log('');
|
|
1346
|
-
}
|
|
1347
|
-
}
|
|
1348
|
-
|
|
1349
|
-
async restart(): Promise<void> {
|
|
1350
|
-
this.stopProcess();
|
|
1351
|
-
await new Promise((resolve) => setTimeout(resolve, 500));
|
|
1352
|
-
await this.runProcess();
|
|
1353
|
-
}
|
|
1354
|
-
|
|
1355
|
-
stop(): void {
|
|
1356
|
-
this.watcher?.close();
|
|
1357
|
-
this.stopProcess();
|
|
1358
|
-
}
|
|
1359
|
-
|
|
1360
|
-
private stopProcess(): void {
|
|
1361
|
-
if (this.childProcess && this.isRunning) {
|
|
1362
|
-
const pid = this.childProcess.pid;
|
|
1363
|
-
if (pid) {
|
|
1364
|
-
try {
|
|
1365
|
-
process.kill(-pid, 'SIGTERM');
|
|
1366
|
-
} catch {
|
|
1367
|
-
try {
|
|
1368
|
-
process.kill(pid, 'SIGTERM');
|
|
1369
|
-
} catch {
|
|
1370
|
-
// Process already dead
|
|
1371
|
-
}
|
|
1372
|
-
}
|
|
1373
|
-
}
|
|
1374
|
-
this.childProcess = null;
|
|
1375
|
-
this.isRunning = false;
|
|
1376
|
-
}
|
|
1377
|
-
}
|
|
1378
|
-
}
|
|
1379
|
-
|
|
1380
|
-
/**
|
|
1381
|
-
* Generate the content of the dev server entry file (server.ts).
|
|
1382
|
-
* Uses dynamic import for createApp so Credentials are populated
|
|
1383
|
-
* before any app modules evaluate.
|
|
1384
|
-
* @internal Exported for testing
|
|
1385
|
-
*/
|
|
1386
|
-
export function generateServerEntryContent(options: {
|
|
1387
|
-
secretsJsonPath?: string;
|
|
1388
|
-
runtime?: Runtime;
|
|
1389
|
-
enableOpenApi?: boolean;
|
|
1390
|
-
appImportPath?: string;
|
|
1391
|
-
}): string {
|
|
1392
|
-
const {
|
|
1393
|
-
secretsJsonPath,
|
|
1394
|
-
runtime = 'node',
|
|
1395
|
-
enableOpenApi = false,
|
|
1396
|
-
appImportPath = './app.js',
|
|
1397
|
-
} = options;
|
|
1398
|
-
|
|
1399
|
-
const credentialsInjection = secretsJsonPath
|
|
1400
|
-
? `import { existsSync, readFileSync } from 'node:fs';
|
|
1401
|
-
|
|
1402
|
-
// Inject dev secrets via globalThis (must happen before app import)
|
|
1403
|
-
const secretsPath = '${secretsJsonPath}';
|
|
1404
|
-
if (existsSync(secretsPath)) {
|
|
1405
|
-
const __secrets = JSON.parse(readFileSync(secretsPath, 'utf-8'));
|
|
1406
|
-
globalThis.__gkm_credentials__ = __secrets;
|
|
1407
|
-
Object.assign(process.env, __secrets);
|
|
1408
|
-
}
|
|
1409
|
-
|
|
1410
|
-
`
|
|
1411
|
-
: '';
|
|
1412
|
-
|
|
1413
|
-
const serveCode =
|
|
1414
|
-
runtime === 'bun'
|
|
1415
|
-
? `Bun.serve({
|
|
1416
|
-
port,
|
|
1417
|
-
fetch: app.fetch,
|
|
1418
|
-
});`
|
|
1419
|
-
: `const { serve } = await import('@hono/node-server');
|
|
1420
|
-
const server = serve({
|
|
1421
|
-
fetch: app.fetch,
|
|
1422
|
-
port,
|
|
1423
|
-
});
|
|
1424
|
-
// Inject WebSocket support if available
|
|
1425
|
-
const injectWs = (app as any).__injectWebSocket;
|
|
1426
|
-
if (injectWs) {
|
|
1427
|
-
injectWs(server);
|
|
1428
|
-
console.log('🔌 Telescope real-time updates enabled');
|
|
1429
|
-
}`;
|
|
1430
|
-
|
|
1431
|
-
return `#!/usr/bin/env node
|
|
1432
|
-
/**
|
|
1433
|
-
* Development server entry point
|
|
1434
|
-
* This file is auto-generated by 'gkm dev'
|
|
1435
|
-
*/
|
|
1436
|
-
${credentialsInjection}
|
|
1437
|
-
const port = process.argv.includes('--port')
|
|
1438
|
-
? Number.parseInt(process.argv[process.argv.indexOf('--port') + 1])
|
|
1439
|
-
: 3000;
|
|
1440
|
-
|
|
1441
|
-
// Dynamic import so Credentials are populated before env.ts evaluates
|
|
1442
|
-
const { createApp } = await import('${appImportPath}');
|
|
1443
|
-
|
|
1444
|
-
// createApp is async to support optional WebSocket setup
|
|
1445
|
-
const { app, start } = await createApp(undefined, ${enableOpenApi});
|
|
1446
|
-
|
|
1447
|
-
// Start the server
|
|
1448
|
-
start({
|
|
1449
|
-
port,
|
|
1450
|
-
serve: async (app, port) => {
|
|
1451
|
-
${serveCode}
|
|
1452
|
-
},
|
|
1453
|
-
}).catch((error) => {
|
|
1454
|
-
console.error('Failed to start server:', error);
|
|
1455
|
-
process.exit(1);
|
|
1456
|
-
});
|
|
1457
|
-
`;
|
|
1458
|
-
}
|
|
1459
|
-
|
|
1460
|
-
class DevServer {
|
|
1461
|
-
private serverProcess: ChildProcess | null = null;
|
|
1462
|
-
private isRunning = false;
|
|
1463
|
-
private actualPort: number;
|
|
1464
|
-
private startTime = Date.now();
|
|
1465
|
-
|
|
1466
|
-
constructor(
|
|
1467
|
-
private provider: LegacyProvider,
|
|
1468
|
-
private requestedPort: number,
|
|
1469
|
-
private portExplicit: boolean,
|
|
1470
|
-
private enableOpenApi: boolean,
|
|
1471
|
-
private telescope: NormalizedTelescopeConfig | undefined,
|
|
1472
|
-
private studio: NormalizedStudioConfig | undefined,
|
|
1473
|
-
private runtime: Runtime = 'node',
|
|
1474
|
-
private appRoot: string = process.cwd(),
|
|
1475
|
-
private secretsJsonPath?: string,
|
|
1476
|
-
) {
|
|
1477
|
-
this.actualPort = requestedPort;
|
|
1478
|
-
}
|
|
1479
|
-
|
|
1480
|
-
async start(): Promise<void> {
|
|
1481
|
-
this.startTime = Date.now();
|
|
1482
|
-
if (this.isRunning) {
|
|
1483
|
-
await this.stop();
|
|
1484
|
-
}
|
|
1485
|
-
|
|
1486
|
-
// Check port availability
|
|
1487
|
-
if (this.portExplicit) {
|
|
1488
|
-
// Port was explicitly specified - throw if unavailable
|
|
1489
|
-
const available = await isPortAvailable(this.requestedPort);
|
|
1490
|
-
if (!available) {
|
|
1491
|
-
throw new Error(
|
|
1492
|
-
`Port ${this.requestedPort} is already in use. ` +
|
|
1493
|
-
`Either stop the process using that port or omit -p/--port to auto-select an available port.`,
|
|
1494
|
-
);
|
|
1495
|
-
}
|
|
1496
|
-
this.actualPort = this.requestedPort;
|
|
1497
|
-
} else {
|
|
1498
|
-
// Find an available port starting from the default
|
|
1499
|
-
this.actualPort = await findAvailablePort(this.requestedPort);
|
|
1500
|
-
|
|
1501
|
-
if (this.actualPort !== this.requestedPort) {
|
|
1502
|
-
logger.log(
|
|
1503
|
-
`ℹ️ Port ${this.requestedPort} was in use, using port ${this.actualPort} instead`,
|
|
1504
|
-
);
|
|
1505
|
-
}
|
|
1506
|
-
}
|
|
1507
|
-
|
|
1508
|
-
const serverEntryPath = join(
|
|
1509
|
-
this.appRoot,
|
|
1510
|
-
'.gkm',
|
|
1511
|
-
this.provider,
|
|
1512
|
-
'server.ts',
|
|
1513
|
-
);
|
|
1514
|
-
|
|
1515
|
-
// Create server entry file
|
|
1516
|
-
await this.createServerEntry();
|
|
1517
|
-
|
|
1518
|
-
logger.log(`\n⏳ Starting server...`);
|
|
1519
|
-
|
|
1520
|
-
// Start the server using tsx (TypeScript execution)
|
|
1521
|
-
// Use detached: true so we can kill the entire process tree
|
|
1522
|
-
this.serverProcess = spawn(
|
|
1523
|
-
'npx',
|
|
1524
|
-
['tsx', serverEntryPath, '--port', this.actualPort.toString()],
|
|
1525
|
-
{
|
|
1526
|
-
stdio: 'inherit',
|
|
1527
|
-
env: { ...process.env, NODE_ENV: 'development' },
|
|
1528
|
-
detached: true,
|
|
1529
|
-
},
|
|
1530
|
-
);
|
|
1531
|
-
|
|
1532
|
-
this.isRunning = true;
|
|
1533
|
-
|
|
1534
|
-
this.serverProcess.on('error', (error) => {
|
|
1535
|
-
logger.error('❌ Server error:', error);
|
|
1536
|
-
});
|
|
1537
|
-
|
|
1538
|
-
this.serverProcess.on('exit', (code, signal) => {
|
|
1539
|
-
if (code !== null && code !== 0 && signal !== 'SIGTERM') {
|
|
1540
|
-
logger.error(`❌ Server exited with code ${code}`);
|
|
1541
|
-
}
|
|
1542
|
-
this.isRunning = false;
|
|
1543
|
-
});
|
|
1544
|
-
|
|
1545
|
-
// Give the server a moment to start
|
|
1546
|
-
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
1547
|
-
|
|
1548
|
-
if (this.isRunning) {
|
|
1549
|
-
const base = `http://localhost:${this.actualPort}`;
|
|
1550
|
-
const lines: string[] = [` Local: ${base}`];
|
|
1551
|
-
if (this.enableOpenApi) {
|
|
1552
|
-
lines.push(` API Docs: ${base}/__docs`);
|
|
1553
|
-
}
|
|
1554
|
-
if (this.telescope) {
|
|
1555
|
-
lines.push(` Telescope: ${base}${this.telescope.path}`);
|
|
1556
|
-
}
|
|
1557
|
-
if (this.studio) {
|
|
1558
|
-
lines.push(` Studio: ${base}${this.studio.path}`);
|
|
1559
|
-
}
|
|
1560
|
-
|
|
1561
|
-
const maxLen = Math.max(...lines.map((l) => l.length));
|
|
1562
|
-
const pad = (s: string) => s.padEnd(maxLen);
|
|
1563
|
-
const border = '─'.repeat(maxLen + 2);
|
|
1564
|
-
|
|
1565
|
-
logger.log('');
|
|
1566
|
-
logger.log(
|
|
1567
|
-
` \x1b[32m✓ Ready\x1b[0m in ${((Date.now() - this.startTime) / 1000).toFixed(1)}s`,
|
|
1568
|
-
);
|
|
1569
|
-
logger.log('');
|
|
1570
|
-
logger.log(` ┌${border}┐`);
|
|
1571
|
-
for (const line of lines) {
|
|
1572
|
-
logger.log(` │ ${pad(line)} │`);
|
|
1573
|
-
}
|
|
1574
|
-
logger.log(` └${border}┘`);
|
|
1575
|
-
logger.log('');
|
|
1576
|
-
}
|
|
1577
|
-
}
|
|
1578
|
-
|
|
1579
|
-
async stop(): Promise<void> {
|
|
1580
|
-
const port = this.actualPort;
|
|
1581
|
-
|
|
1582
|
-
if (this.serverProcess && this.isRunning) {
|
|
1583
|
-
const pid = this.serverProcess.pid;
|
|
1584
|
-
|
|
1585
|
-
// Use SIGKILL directly since the server ignores SIGTERM
|
|
1586
|
-
if (pid) {
|
|
1587
|
-
try {
|
|
1588
|
-
process.kill(-pid, 'SIGKILL');
|
|
1589
|
-
} catch {
|
|
1590
|
-
try {
|
|
1591
|
-
process.kill(pid, 'SIGKILL');
|
|
1592
|
-
} catch {
|
|
1593
|
-
// Process might already be dead
|
|
1594
|
-
}
|
|
1595
|
-
}
|
|
1596
|
-
}
|
|
1597
|
-
|
|
1598
|
-
this.serverProcess = null;
|
|
1599
|
-
this.isRunning = false;
|
|
1600
|
-
}
|
|
1601
|
-
|
|
1602
|
-
// Also kill any processes still holding the port
|
|
1603
|
-
this.killProcessesOnPort(port);
|
|
1604
|
-
}
|
|
1605
|
-
|
|
1606
|
-
private killProcessesOnPort(port: number): void {
|
|
1607
|
-
try {
|
|
1608
|
-
// Use lsof to find PIDs on the port and kill them with -9
|
|
1609
|
-
execSync(`lsof -ti tcp:${port} | xargs kill -9 2>/dev/null || true`, {
|
|
1610
|
-
stdio: 'ignore',
|
|
1611
|
-
});
|
|
1612
|
-
} catch {
|
|
1613
|
-
// Ignore errors - port may already be free
|
|
1614
|
-
}
|
|
1615
|
-
}
|
|
1616
|
-
|
|
1617
|
-
async restart(): Promise<void> {
|
|
1618
|
-
const portToReuse = this.actualPort;
|
|
1619
|
-
await this.stop();
|
|
1620
|
-
|
|
1621
|
-
// Wait for port to be released (up to 3 seconds)
|
|
1622
|
-
let attempts = 0;
|
|
1623
|
-
while (attempts < 30) {
|
|
1624
|
-
if (await isPortAvailable(portToReuse)) {
|
|
1625
|
-
break;
|
|
1626
|
-
}
|
|
1627
|
-
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
1628
|
-
attempts++;
|
|
1629
|
-
}
|
|
1630
|
-
|
|
1631
|
-
// Force reuse the same port
|
|
1632
|
-
this.requestedPort = portToReuse;
|
|
1633
|
-
await this.start();
|
|
1634
|
-
}
|
|
1635
|
-
|
|
1636
|
-
private async createServerEntry(): Promise<void> {
|
|
1637
|
-
const { writeFile: fsWriteFile } = await import('node:fs/promises');
|
|
1638
|
-
|
|
1639
|
-
const serverPath = join(this.appRoot, '.gkm', this.provider, 'server.ts');
|
|
1640
|
-
|
|
1641
|
-
const content = generateServerEntryContent({
|
|
1642
|
-
secretsJsonPath: this.secretsJsonPath,
|
|
1643
|
-
runtime: this.runtime,
|
|
1644
|
-
enableOpenApi: this.enableOpenApi,
|
|
1645
|
-
});
|
|
1646
|
-
|
|
1647
|
-
await fsWriteFile(serverPath, content);
|
|
1648
|
-
}
|
|
1649
|
-
}
|