@geekmidas/cli 10.0.0-alpha.64 → 10.0.0-alpha.66
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 +15 -1
- package/dist/{HostingerProvider-Bx2lHHvu.cjs → HostingerProvider-BznqRC2S.cjs} +5 -5
- package/dist/HostingerProvider-BznqRC2S.cjs.map +1 -0
- package/dist/{HostingerProvider-Cd4aS5Kj.mjs → HostingerProvider-CY_VpVud.mjs} +4 -4
- package/dist/HostingerProvider-CY_VpVud.mjs.map +1 -0
- package/dist/{LegacyStateStore-C2abV49N.cjs → LegacyStateStore-BFcTT2QL.cjs} +2 -2
- package/dist/{LegacyStateStore-C2abV49N.cjs.map → LegacyStateStore-BFcTT2QL.cjs.map} +1 -1
- package/dist/{LegacyStateStore-DIgkGaUP.mjs → LegacyStateStore-CFheFo62.mjs} +2 -2
- package/dist/{LegacyStateStore-DIgkGaUP.mjs.map → LegacyStateStore-CFheFo62.mjs.map} +1 -1
- package/dist/{LocalStateStore-DiJqKyPy.cjs → LocalStateStore-BVUexSvu.cjs} +2 -2
- package/dist/{LocalStateStore-DiJqKyPy.cjs.map → LocalStateStore-BVUexSvu.cjs.map} +1 -1
- package/dist/{LocalStateStore-Cy1ZBW7U.mjs → LocalStateStore-Bm3x9oOg.mjs} +2 -2
- package/dist/{LocalStateStore-Cy1ZBW7U.mjs.map → LocalStateStore-Bm3x9oOg.mjs.map} +1 -1
- package/dist/{OpenApiTsGenerator-BWuDRHe9.cjs → OpenApiTsGenerator-B2zrdyP4.cjs} +2 -2
- package/dist/{OpenApiTsGenerator-BWuDRHe9.cjs.map → OpenApiTsGenerator-B2zrdyP4.cjs.map} +1 -1
- package/dist/{OpenApiTsGenerator-C6v-IeHt.mjs → OpenApiTsGenerator-BmUAcR7d.mjs} +2 -2
- package/dist/{OpenApiTsGenerator-C6v-IeHt.mjs.map → OpenApiTsGenerator-BmUAcR7d.mjs.map} +1 -1
- package/dist/{Route53Provider-DVILBsvV.cjs → Route53Provider-Bllus2NR.cjs} +2 -2
- package/dist/Route53Provider-Bllus2NR.cjs.map +1 -0
- package/dist/{Route53Provider-Cvj40A8I.mjs → Route53Provider-QXxjkZO1.mjs} +2 -2
- package/dist/Route53Provider-QXxjkZO1.mjs.map +1 -0
- package/dist/{S3StateStore-D3x6ivBL.mjs → S3StateStore-BFHuctWM.mjs} +2 -2
- package/dist/{S3StateStore-D3x6ivBL.mjs.map → S3StateStore-BFHuctWM.mjs.map} +1 -1
- package/dist/{S3StateStore-Ds0V1Lgm.cjs → S3StateStore-CoXBhILd.cjs} +2 -2
- package/dist/{S3StateStore-Ds0V1Lgm.cjs.map → S3StateStore-CoXBhILd.cjs.map} +1 -1
- package/dist/{SSMStateStore-CLipvx47.cjs → SSMStateStore-BqCA9TP2.cjs} +2 -2
- package/dist/{SSMStateStore-CLipvx47.cjs.map → SSMStateStore-BqCA9TP2.cjs.map} +1 -1
- package/dist/{SSMStateStore-DfF174xQ.mjs → SSMStateStore-C1kvHScY.mjs} +2 -2
- package/dist/{SSMStateStore-DfF174xQ.mjs.map → SSMStateStore-C1kvHScY.mjs.map} +1 -1
- package/dist/{backup-provisioner-DIVCbfee.mjs → backup-provisioner-C8DnCmxK.mjs} +2 -2
- package/dist/backup-provisioner-C8DnCmxK.mjs.map +1 -0
- package/dist/{backup-provisioner-BmhIO_Yr.cjs → backup-provisioner-oDEsPj02.cjs} +2 -2
- package/dist/backup-provisioner-oDEsPj02.cjs.map +1 -0
- package/dist/{bundler-DYgm9xsd.mjs → bundler-Dvaiud0h.mjs} +4 -4
- package/dist/{bundler-DYgm9xsd.mjs.map → bundler-Dvaiud0h.mjs.map} +1 -1
- package/dist/{bundler-C8uJf324.cjs → bundler-orRqUJNZ.cjs} +4 -4
- package/dist/{bundler-C8uJf324.cjs.map → bundler-orRqUJNZ.cjs.map} +1 -1
- package/dist/{cli-Bogz-zEj.mjs → cli-CfHm0AO7.mjs} +7 -6
- package/dist/cli-CfHm0AO7.mjs.map +1 -0
- package/dist/{cli-D3jofJ6j.cjs → cli-D1JkkOwg.cjs} +8 -7
- package/dist/cli-D1JkkOwg.cjs.map +1 -0
- package/dist/compose-CNQL8BnO.mjs +1398 -0
- package/dist/compose-CNQL8BnO.mjs.map +1 -0
- package/dist/compose-jrLy7KGO.cjs +1427 -0
- package/dist/compose-jrLy7KGO.cjs.map +1 -0
- package/dist/{config-Be-4_I6H.d.cts → config-4XFxX1al.d.cts} +3 -3
- package/dist/{config-Be-4_I6H.d.cts.map → config-4XFxX1al.d.cts.map} +1 -1
- package/dist/{config-DqNRiQOp.d.mts → config-UtKzhpFb.d.mts} +3 -3
- package/dist/{config-DqNRiQOp.d.mts.map → config-UtKzhpFb.d.mts.map} +1 -1
- package/dist/config.cjs +3 -3
- package/dist/config.d.cts +3 -3
- package/dist/config.d.mts +3 -3
- package/dist/config.mjs +3 -3
- package/dist/{credentials-B70vmLcm.cjs → credentials-Bzbg23n8.cjs} +176 -3
- package/dist/credentials-Bzbg23n8.cjs.map +1 -0
- package/dist/{credentials-s0wMeZBU.mjs → credentials-Dq1MnWQf.mjs} +128 -3
- package/dist/credentials-Dq1MnWQf.mjs.map +1 -0
- package/dist/databases-C2p5LGAp.cjs +419 -0
- package/dist/databases-C2p5LGAp.cjs.map +1 -0
- package/dist/databases-hUHTuwhx.mjs +346 -0
- package/dist/databases-hUHTuwhx.mjs.map +1 -0
- package/dist/{define-CYs0PEfZ.mjs → define-DR8xGYfL.mjs} +5 -1
- package/dist/define-DR8xGYfL.mjs.map +1 -0
- package/dist/{define-zUsP72ym.cjs → define-DzRz8FgH.cjs} +5 -1
- package/dist/define-DzRz8FgH.cjs.map +1 -0
- package/dist/deploy/public.cjs +35 -26
- package/dist/deploy/public.cjs.map +1 -1
- package/dist/deploy/public.d.cts +140 -20
- package/dist/deploy/public.d.cts.map +1 -1
- package/dist/deploy/public.d.mts +140 -20
- package/dist/deploy/public.d.mts.map +1 -1
- package/dist/deploy/public.mjs +13 -13
- package/dist/{terminal-Bdv--jhY.cjs → deploy-B7aJi_Ah.cjs} +146 -116
- package/dist/deploy-B7aJi_Ah.cjs.map +1 -0
- package/dist/{terminal-D7Ntz6WE.mjs → deploy-DWn7YfIc.mjs} +137 -89
- package/dist/deploy-DWn7YfIc.mjs.map +1 -0
- package/dist/{discover-LMrqsWSU.mjs → discover-BFoJWKGD.mjs} +4 -2
- package/dist/discover-BFoJWKGD.mjs.map +1 -0
- package/dist/{discover-DHdECueV.cjs → discover-OEfZ9OkA.cjs} +4 -2
- package/dist/discover-OEfZ9OkA.cjs.map +1 -0
- package/dist/dokploy-B4LcYG1E.cjs +38 -0
- package/dist/dokploy-B4LcYG1E.cjs.map +1 -0
- package/dist/dokploy-DalwLML5.mjs +38 -0
- package/dist/dokploy-DalwLML5.mjs.map +1 -0
- package/dist/{encryption-C9X98u2F.cjs → encryption-3KlGS1sl.cjs} +6 -8
- package/dist/{encryption-C9X98u2F.cjs.map → encryption-3KlGS1sl.cjs.map} +1 -1
- package/dist/{encryption-CKHjMpCr.mjs → encryption-CPfENwWO.mjs} +3 -5
- package/dist/{encryption-CKHjMpCr.mjs.map → encryption-CPfENwWO.mjs.map} +1 -1
- package/dist/{deploy-ZRXIPp1c.mjs → engine-CunvOG3s.mjs} +2116 -1855
- package/dist/engine-CunvOG3s.mjs.map +1 -0
- package/dist/{deploy-55rJkdKz.cjs → engine-DR6KurWX.cjs} +2172 -1869
- package/dist/engine-DR6KurWX.cjs.map +1 -0
- package/dist/{github-Dd3tMa1u.cjs → github-BJ5gh9bw.cjs} +7 -7
- package/dist/{github-Dd3tMa1u.cjs.map → github-BJ5gh9bw.cjs.map} +1 -1
- package/dist/{github-D-byNI7T.mjs → github-MXA9v7Ge.mjs} +3 -3
- package/dist/{github-D-byNI7T.mjs.map → github-MXA9v7Ge.mjs.map} +1 -1
- package/dist/{index-CfE9Y885.d.cts → index-CyApaHVe.d.cts} +16 -3
- package/dist/index-CyApaHVe.d.cts.map +1 -0
- package/dist/{index-CJ2qsSpp.d.mts → index-DD1FfUHh.d.mts} +16 -3
- package/dist/index-DD1FfUHh.d.mts.map +1 -0
- package/dist/index.cjs +465 -1691
- package/dist/index.cjs.map +1 -1
- package/dist/index.mjs +405 -1619
- package/dist/index.mjs.map +1 -1
- package/dist/{local-C7wnrcYK.cjs → local-BdH4XN0E.cjs} +1 -178
- package/dist/local-BdH4XN0E.cjs.map +1 -0
- package/dist/{local-Cd8ZYQG3.mjs → local-CWR4Y_kW.mjs} +3 -126
- package/dist/local-CWR4Y_kW.mjs.map +1 -0
- package/dist/{migrate-B4gngTLe.mjs → migrate-BJBcm4sG.mjs} +7 -6
- package/dist/{migrate-B4gngTLe.mjs.map → migrate-BJBcm4sG.mjs.map} +1 -1
- package/dist/{migrate-tx9-P9uz.cjs → migrate-CF4J4GQS.cjs} +20 -19
- package/dist/{migrate-tx9-P9uz.cjs.map → migrate-CF4J4GQS.cjs.map} +1 -1
- package/dist/openapi.cjs +1 -1
- package/dist/openapi.d.cts +1 -1
- package/dist/openapi.d.mts +1 -1
- package/dist/openapi.mjs +1 -1
- package/dist/provider-B3Sv_a74.cjs +173 -0
- package/dist/provider-B3Sv_a74.cjs.map +1 -0
- package/dist/provider-C9_-45d5.cjs +56 -0
- package/dist/provider-C9_-45d5.cjs.map +1 -0
- package/dist/provider-D7rQzzzf.mjs +138 -0
- package/dist/provider-D7rQzzzf.mjs.map +1 -0
- package/dist/provider-q4lsYzYG.mjs +45 -0
- package/dist/provider-q4lsYzYG.mjs.map +1 -0
- package/dist/{ready-bQfu9GES.mjs → ready-2s3-CndS.mjs} +3 -3
- package/dist/{ready-bQfu9GES.mjs.map → ready-2s3-CndS.mjs.map} +1 -1
- package/dist/{ready-brEhh68f.cjs → ready-B6uHr4sm.cjs} +3 -3
- package/dist/{ready-brEhh68f.cjs.map → ready-B6uHr4sm.cjs.map} +1 -1
- package/dist/reconcile/public.cjs +1 -1
- package/dist/reconcile/public.d.cts +1 -1
- package/dist/reconcile/public.d.mts +1 -1
- package/dist/reconcile/public.mjs +1 -1
- package/dist/resolve-CVmch0wI.cjs +315 -0
- package/dist/resolve-CVmch0wI.cjs.map +1 -0
- package/dist/resolve-Db0tNlrW.mjs +262 -0
- package/dist/resolve-Db0tNlrW.mjs.map +1 -0
- package/dist/rollback-DvAo4OKS.mjs +133 -0
- package/dist/rollback-DvAo4OKS.mjs.map +1 -0
- package/dist/rollback-DxJK84hy.cjs +162 -0
- package/dist/rollback-DxJK84hy.cjs.map +1 -0
- package/dist/{runtime-CJEYK4Gz.d.mts → runtime-BRqFOvPM.d.cts} +2 -2
- package/dist/{runtime-CJEYK4Gz.d.mts.map → runtime-BRqFOvPM.d.cts.map} +1 -1
- package/dist/{runtime-Cjq1jlVX.d.cts → runtime-CEk5n4VH.d.mts} +2 -2
- package/dist/{runtime-Cjq1jlVX.d.cts.map → runtime-CEk5n4VH.d.mts.map} +1 -1
- package/dist/sandbox/discover-worker.cjs +1 -1
- package/dist/sandbox/discover-worker.mjs +1 -1
- package/dist/sandbox/migrate-worker.cjs +61 -0
- package/dist/sandbox/migrate-worker.cjs.map +1 -0
- package/dist/sandbox/migrate-worker.d.cts +1 -0
- package/dist/sandbox/migrate-worker.d.mts +1 -0
- package/dist/sandbox/migrate-worker.mjs +62 -0
- package/dist/sandbox/migrate-worker.mjs.map +1 -0
- package/dist/{schemas-D2LCF689.mjs → schemas-Cfhtpdo0.mjs} +10 -2
- package/dist/{schemas-Ckm_aBnF.cjs.map → schemas-Cfhtpdo0.mjs.map} +1 -1
- package/dist/{schemas-Ckm_aBnF.cjs → schemas-RUlKHu27.cjs} +15 -1
- package/dist/{schemas-D2LCF689.mjs.map → schemas-RUlKHu27.cjs.map} +1 -1
- package/dist/sniffer-5xmrkh2a.cjs +1742 -0
- package/dist/sniffer-5xmrkh2a.cjs.map +1 -0
- package/dist/sniffer-BAQhLEsI.mjs +1617 -0
- package/dist/sniffer-BAQhLEsI.mjs.map +1 -0
- package/dist/{sst-Cper040I.mjs → sst-B2HbrDR3.mjs} +4 -4
- package/dist/{sst-Cper040I.mjs.map → sst-B2HbrDR3.mjs.map} +1 -1
- package/dist/{sst-BdALKEas.cjs → sst-DcfsVH6x.cjs} +4 -4
- package/dist/{sst-BdALKEas.cjs.map → sst-DcfsVH6x.cjs.map} +1 -1
- package/dist/{sniffer-ByhbEW5A.cjs → state-Y6ATakMX.cjs} +1769 -2759
- package/dist/state-Y6ATakMX.cjs.map +1 -0
- package/dist/{sniffer-49KPbDgq.mjs → state-uuYqnRVO.mjs} +1763 -2764
- package/dist/state-uuYqnRVO.mjs.map +1 -0
- package/dist/target/public.cjs +6 -6
- package/dist/target/public.d.cts +2 -2
- package/dist/target/public.d.mts +2 -2
- package/dist/target/public.mjs +3 -3
- package/dist/terminal-CHy_MdjG.cjs +84 -0
- package/dist/terminal-CHy_MdjG.cjs.map +1 -0
- package/dist/terminal-DUhcZ1ND.mjs +84 -0
- package/dist/terminal-DUhcZ1ND.mjs.map +1 -0
- package/dist/{types-DkrDOMxw.d.cts → types-CSw2w69a.d.mts} +110 -20
- package/dist/types-CSw2w69a.d.mts.map +1 -0
- package/dist/{types-B_bEQvb9.d.mts → types-D5WIxRDP.d.cts} +111 -19
- package/dist/types-D5WIxRDP.d.cts.map +1 -0
- package/dist/vitest.cjs +1 -1
- package/dist/vitest.mjs +1 -1
- package/dist/workspace/index.cjs +3 -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-Bwit4q9O.mjs → workspace-Be6uBhlF.mjs} +36 -6
- package/dist/workspace-Be6uBhlF.mjs.map +1 -0
- package/dist/{workspace-BwgWJK7h.cjs → workspace-ON-bqiJz.cjs} +47 -5
- package/dist/workspace-ON-bqiJz.cjs.map +1 -0
- package/package.json +13 -13
- package/dist/HostingerProvider-Bx2lHHvu.cjs.map +0 -1
- package/dist/HostingerProvider-Cd4aS5Kj.mjs.map +0 -1
- package/dist/Route53Provider-Cvj40A8I.mjs.map +0 -1
- package/dist/Route53Provider-DVILBsvV.cjs.map +0 -1
- package/dist/backup-provisioner-BmhIO_Yr.cjs.map +0 -1
- package/dist/backup-provisioner-DIVCbfee.mjs.map +0 -1
- package/dist/cli-Bogz-zEj.mjs.map +0 -1
- package/dist/cli-D3jofJ6j.cjs.map +0 -1
- package/dist/credentials-B70vmLcm.cjs.map +0 -1
- package/dist/credentials-s0wMeZBU.mjs.map +0 -1
- package/dist/define-CYs0PEfZ.mjs.map +0 -1
- package/dist/define-zUsP72ym.cjs.map +0 -1
- package/dist/deploy-55rJkdKz.cjs.map +0 -1
- package/dist/deploy-Dc2LBJaK.mjs +0 -136
- package/dist/deploy-Dc2LBJaK.mjs.map +0 -1
- package/dist/deploy-ZRXIPp1c.mjs.map +0 -1
- package/dist/deploy-yUcR2OlW.cjs +0 -141
- package/dist/deploy-yUcR2OlW.cjs.map +0 -1
- package/dist/discover-DHdECueV.cjs.map +0 -1
- package/dist/discover-LMrqsWSU.mjs.map +0 -1
- package/dist/dokploy-CepdXuMl.mjs +0 -32
- package/dist/dokploy-CepdXuMl.mjs.map +0 -1
- package/dist/dokploy-D1d37rvM.cjs +0 -32
- package/dist/dokploy-D1d37rvM.cjs.map +0 -1
- package/dist/index-CJ2qsSpp.d.mts.map +0 -1
- package/dist/index-CfE9Y885.d.cts.map +0 -1
- package/dist/local-C7wnrcYK.cjs.map +0 -1
- package/dist/local-Cd8ZYQG3.mjs.map +0 -1
- package/dist/resolve-BXsNQjMR.cjs +0 -220
- package/dist/resolve-BXsNQjMR.cjs.map +0 -1
- package/dist/resolve-DZ2IJ0qY.mjs +0 -173
- package/dist/resolve-DZ2IJ0qY.mjs.map +0 -1
- package/dist/runtime-BGNDrHzh.cjs +0 -163
- package/dist/runtime-BGNDrHzh.cjs.map +0 -1
- package/dist/runtime-Cmn-FeYO.mjs +0 -134
- package/dist/runtime-Cmn-FeYO.mjs.map +0 -1
- package/dist/sniffer-49KPbDgq.mjs.map +0 -1
- package/dist/sniffer-ByhbEW5A.cjs.map +0 -1
- package/dist/storage-BzaUxmqe.mjs +0 -124
- package/dist/storage-BzaUxmqe.mjs.map +0 -1
- package/dist/storage-CSGoC3bK.cjs +0 -152
- package/dist/storage-CSGoC3bK.cjs.map +0 -1
- package/dist/store-BaaonhkJ.mjs +0 -415
- package/dist/store-BaaonhkJ.mjs.map +0 -1
- package/dist/store-D_2ByRtW.cjs +0 -510
- package/dist/store-D_2ByRtW.cjs.map +0 -1
- package/dist/terminal-Bdv--jhY.cjs.map +0 -1
- package/dist/terminal-D7Ntz6WE.mjs.map +0 -1
- package/dist/types-B_bEQvb9.d.mts.map +0 -1
- package/dist/types-DkrDOMxw.d.cts.map +0 -1
- package/dist/workspace-BwgWJK7h.cjs.map +0 -1
- package/dist/workspace-Bwit4q9O.mjs.map +0 -1
package/dist/index.mjs
CHANGED
|
@@ -1,37 +1,39 @@
|
|
|
1
1
|
#!/usr/bin/env -S npx tsx
|
|
2
2
|
import { n as __require, t as __exportAll } from "./rolldown-runtime-BhDjJH2R.mjs";
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
import { l as withOwningTsconfigJsx, r as discover, u as ConstructGenerator } from "./discover-
|
|
3
|
+
import { i as CommandFailed, o as DEFAULT_TIMEOUT_MS, t as LocalSandbox } from "./local-CWR4Y_kW.mjs";
|
|
4
|
+
import { a as getDokployCredentials, d as storeHostingerToken, i as getCredentialsPath, l as removeDokployCredentials, o as getDokployRegistryId, s as getDokployToken, u as storeDokployCredentials } from "./credentials-Dq1MnWQf.mjs";
|
|
5
|
+
import { l as withOwningTsconfigJsx, r as discover, u as ConstructGenerator } from "./discover-BFoJWKGD.mjs";
|
|
6
6
|
import { t as appKey } from "./derive-B4vDpzgP.mjs";
|
|
7
|
-
import {
|
|
7
|
+
import { A as deployedProblems, D as InvalidStages, O as TEST_STAGE, c as getAppGkmConfig, j as stageProblems, n as allConstructGlobs, s as getAppBuildOrder } from "./workspace-Be6uBhlF.mjs";
|
|
8
8
|
import { ConfigNotFound, NotABackendApp, NotInAnApp, loadAppConfig, loadConfig, loadWorkspaceConfig, loadWorkspaceSettings, parseModuleConfig } from "./config.mjs";
|
|
9
|
-
import {
|
|
10
|
-
import {
|
|
11
|
-
import {
|
|
9
|
+
import { $ as describeServices, A as reconcileWorkspace, At as withCustomSecret, D as backendsOf$1, Dt as maskPassword, E as FAKE_ENV, F as postgresDatabaseNames, J as cacheBackendFor, L as quoteIdentifier, M as LOCAL_CA_PATH, N as forgetState, O as constructGlobs, X as providerOf, at as findAvailablePort, ct as loadSecretsForApp, dt as secretsStoreFor, et as fakeKey, gt as projectKey, it as createEntryWrapper, k as derivedContainers, kt as toEmbeddableSecrets, lt as prepareEntryCredentials, mt as getKeyPath, nt as primaryPortKey, ot as isPortAvailable, pt as FileSecretsStore, rt as createCredentialsPreload, st as loadEnvFiles, v as dockerCommand, yt as deployIdentity, z as pgClient } from "./state-uuYqnRVO.mjs";
|
|
10
|
+
import { i as telemetryFor, n as EndpointGenerator, r as runtimeFor } from "./OpenApiTsGenerator-BmUAcR7d.mjs";
|
|
11
|
+
import { g as createStateStore, n as sniffAppEnvironment, o as DokployApi, x as keyFingerprint } from "./sniffer-BAQhLEsI.mjs";
|
|
12
12
|
import { generateOpenApi, openapiCommand, resolveOpenApiConfig } from "./openapi.mjs";
|
|
13
|
-
import { n as DEFAULT_CACHE, r as DEFAULT_EVENTS } from "./fakes-C9RiT6XG.mjs";
|
|
14
13
|
import { a as describeHolder, i as assignAppPorts, o as holderOf, r as appTag, s as withAppPorts, t as APP_TAG_ENV } from "./appPorts-C9z9Pzn9.mjs";
|
|
15
|
-
import { a as writeTestHarness, c as driversFor, i as TEST_MANIFEST_ENV, n as TEST_READY_FILE, o as SubscriberGenerator, s as cacheBackendsIn, t as TEST_READY_ENV } from "./ready-
|
|
14
|
+
import { a as writeTestHarness, c as driversFor, i as TEST_MANIFEST_ENV, n as TEST_READY_FILE, o as SubscriberGenerator, s as cacheBackendsIn, t as TEST_READY_ENV } from "./ready-2s3-CndS.mjs";
|
|
16
15
|
import { n as withCompute, r as withRoutes, t as manifestModule } from "./emit-C1e8qD08.mjs";
|
|
17
|
-
import { t as
|
|
18
|
-
import { a as
|
|
16
|
+
import { t as deploy$1 } from "./deploy-DWn7YfIc.mjs";
|
|
17
|
+
import { a as dockerCompose, i as stackDir, r as composeProject, t as composeTarget } from "./compose-CNQL8BnO.mjs";
|
|
18
|
+
import { t as LocalStateStore } from "./LocalStateStore-Bm3x9oOg.mjs";
|
|
19
|
+
import { a as getExecCommand, c as validateProjectName, i as findWorkspaceRoot, n as detectPackageManager$1, o as getInstallCommand, r as findWorkspacePackages, s as getRunCommand, t as checkDirectoryExists } from "./utils-D_fxUqvU.mjs";
|
|
19
20
|
import { createRequire } from "node:module";
|
|
20
|
-
import { basename, dirname,
|
|
21
|
+
import { basename, dirname, isAbsolute, join, relative, resolve } from "node:path";
|
|
21
22
|
import { Command } from "commander";
|
|
23
|
+
import { stdin, stdout } from "node:process";
|
|
24
|
+
import * as readline from "node:readline/promises";
|
|
22
25
|
import { existsSync, linkSync, mkdirSync, readFileSync, readdirSync, renameSync, unlinkSync, writeFileSync } from "node:fs";
|
|
23
|
-
import {
|
|
26
|
+
import { chmod, mkdir, readFile, rm, writeFile } from "node:fs/promises";
|
|
24
27
|
import { homedir } from "node:os";
|
|
25
28
|
import { promisify } from "node:util";
|
|
26
|
-
import {
|
|
29
|
+
import { canonicalId, kebabCase, migrationFolder, provideKey, schemeBase, serviceKey } from "@geekmidas/manifest";
|
|
27
30
|
import fg from "fast-glob";
|
|
28
31
|
import { pathToFileURL } from "node:url";
|
|
29
32
|
import { execFile, execSync, spawn } from "node:child_process";
|
|
30
33
|
import chokidar from "chokidar";
|
|
31
34
|
import { connect } from "node:net";
|
|
32
35
|
import { createHash, randomBytes, timingSafeEqual } from "node:crypto";
|
|
33
|
-
import
|
|
34
|
-
import { parseDocument, stringify } from "yaml";
|
|
36
|
+
import { parseDocument } from "yaml";
|
|
35
37
|
import prompts from "prompts";
|
|
36
38
|
import { createServer as createServer$1 } from "node:http";
|
|
37
39
|
import { Readable } from "node:stream";
|
|
@@ -44,7 +46,192 @@ import { Function } from "@geekmidas/constructs/functions";
|
|
|
44
46
|
import { Queue } from "@geekmidas/constructs/queue";
|
|
45
47
|
import { Topic } from "@geekmidas/constructs/topic";
|
|
46
48
|
//#region package.json
|
|
47
|
-
var version = "10.0.0-alpha.
|
|
49
|
+
var version = "10.0.0-alpha.66";
|
|
50
|
+
//#endregion
|
|
51
|
+
//#region src/prompt.ts
|
|
52
|
+
/**
|
|
53
|
+
* Asking the person at the terminal.
|
|
54
|
+
*
|
|
55
|
+
* CLI layer only: a command's action may prompt, nothing it calls may. A
|
|
56
|
+
* library call (`deploy()`) that needs something it was not given raises a
|
|
57
|
+
* named error instead, and the command decides whether to ask.
|
|
58
|
+
*/
|
|
59
|
+
/** A prompt was needed with no terminal to ask at. */
|
|
60
|
+
var PromptNeedsTerminal = class extends Error {
|
|
61
|
+
question;
|
|
62
|
+
instead;
|
|
63
|
+
constructor(question, instead) {
|
|
64
|
+
super(`Interactive input required. ${instead}`);
|
|
65
|
+
this.question = question;
|
|
66
|
+
this.instead = instead;
|
|
67
|
+
this.name = "PromptNeedsTerminal";
|
|
68
|
+
}
|
|
69
|
+
};
|
|
70
|
+
/** Whether there is a person at a terminal to ask. */
|
|
71
|
+
function canPrompt() {
|
|
72
|
+
return process.stdin.isTTY === true;
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* Ask `message` and resolve with the line typed — without echoing it when
|
|
76
|
+
* `hidden`. Ctrl+C at a hidden prompt exits, as it would anywhere else in a
|
|
77
|
+
* terminal program: raw mode has taken the signal away from the shell.
|
|
78
|
+
*/
|
|
79
|
+
async function prompt(message, options) {
|
|
80
|
+
if (!canPrompt()) throw new PromptNeedsTerminal(message, options.instead);
|
|
81
|
+
if (options.hidden) {
|
|
82
|
+
process.stdout.write(message);
|
|
83
|
+
return new Promise((resolve, reject) => {
|
|
84
|
+
let value = "";
|
|
85
|
+
const cleanup = () => {
|
|
86
|
+
process.stdin.setRawMode(false);
|
|
87
|
+
process.stdin.pause();
|
|
88
|
+
process.stdin.removeListener("data", onData);
|
|
89
|
+
process.stdin.removeListener("error", onError);
|
|
90
|
+
};
|
|
91
|
+
const onError = (err) => {
|
|
92
|
+
cleanup();
|
|
93
|
+
reject(err);
|
|
94
|
+
};
|
|
95
|
+
const onData = (char) => {
|
|
96
|
+
const c = char.toString();
|
|
97
|
+
if (c === "\n" || c === "\r") {
|
|
98
|
+
cleanup();
|
|
99
|
+
process.stdout.write("\n");
|
|
100
|
+
resolve(value);
|
|
101
|
+
} else if (c === "") {
|
|
102
|
+
cleanup();
|
|
103
|
+
process.stdout.write("\n");
|
|
104
|
+
process.exit(1);
|
|
105
|
+
} else if (c === "" || c === "\b") {
|
|
106
|
+
if (value.length > 0) value = value.slice(0, -1);
|
|
107
|
+
} else value += c;
|
|
108
|
+
};
|
|
109
|
+
process.stdin.setRawMode(true);
|
|
110
|
+
process.stdin.resume();
|
|
111
|
+
process.stdin.on("data", onData);
|
|
112
|
+
process.stdin.on("error", onError);
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
const rl = readline.createInterface({
|
|
116
|
+
input: stdin,
|
|
117
|
+
output: stdout
|
|
118
|
+
});
|
|
119
|
+
try {
|
|
120
|
+
return await rl.question(message);
|
|
121
|
+
} finally {
|
|
122
|
+
rl.close();
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
//#endregion
|
|
126
|
+
//#region src/auth/index.ts
|
|
127
|
+
const logger$8 = console;
|
|
128
|
+
/**
|
|
129
|
+
* Validate Dokploy token by making a test API call
|
|
130
|
+
*/
|
|
131
|
+
async function validateDokployToken(endpoint, token) {
|
|
132
|
+
const { DokployApi } = await import("./sniffer-BAQhLEsI.mjs").then((n) => n.c);
|
|
133
|
+
return new DokployApi({
|
|
134
|
+
baseUrl: endpoint,
|
|
135
|
+
token
|
|
136
|
+
}).validateToken();
|
|
137
|
+
}
|
|
138
|
+
/** What answers a login prompt without a terminal. */
|
|
139
|
+
const LOGIN_INSTEAD = "Please provide --token option.";
|
|
140
|
+
/**
|
|
141
|
+
* Login to a service
|
|
142
|
+
*/
|
|
143
|
+
async function loginCommand(options) {
|
|
144
|
+
const { provider, token: providedToken, endpoint: providedEndpoint } = options;
|
|
145
|
+
if (provider === "dokploy") {
|
|
146
|
+
logger$8.log("\n🔐 Logging in to Dokploy...\n");
|
|
147
|
+
let endpoint = providedEndpoint;
|
|
148
|
+
if (!endpoint) endpoint = await prompt("Dokploy URL (e.g., https://dokploy.example.com): ", { instead: LOGIN_INSTEAD });
|
|
149
|
+
endpoint = endpoint.replace(/\/$/, "");
|
|
150
|
+
try {
|
|
151
|
+
new URL(endpoint);
|
|
152
|
+
} catch {
|
|
153
|
+
logger$8.error("Invalid URL format");
|
|
154
|
+
process.exit(1);
|
|
155
|
+
}
|
|
156
|
+
let token = providedToken;
|
|
157
|
+
if (!token) {
|
|
158
|
+
logger$8.log(`\nGenerate a token at: ${endpoint}/settings/profile\n`);
|
|
159
|
+
token = await prompt("API Token: ", {
|
|
160
|
+
hidden: true,
|
|
161
|
+
instead: LOGIN_INSTEAD
|
|
162
|
+
});
|
|
163
|
+
}
|
|
164
|
+
if (!token) {
|
|
165
|
+
logger$8.error("Token is required");
|
|
166
|
+
process.exit(1);
|
|
167
|
+
}
|
|
168
|
+
logger$8.log("\nValidating credentials...");
|
|
169
|
+
if (!await validateDokployToken(endpoint, token)) {
|
|
170
|
+
logger$8.error("\n✗ Invalid credentials. Please check your token and try again.");
|
|
171
|
+
process.exit(1);
|
|
172
|
+
}
|
|
173
|
+
await storeDokployCredentials(token, endpoint);
|
|
174
|
+
logger$8.log("\n✓ Successfully logged in to Dokploy!");
|
|
175
|
+
logger$8.log(` Endpoint: ${endpoint}`);
|
|
176
|
+
logger$8.log(` Credentials stored in: ${getCredentialsPath()}`);
|
|
177
|
+
logger$8.log("\nYou can now use deploy commands without setting DOKPLOY_API_TOKEN.");
|
|
178
|
+
}
|
|
179
|
+
if (provider === "hostinger") {
|
|
180
|
+
logger$8.log("\n🔐 Logging in to Hostinger...\n");
|
|
181
|
+
let token = providedToken;
|
|
182
|
+
if (!token) {
|
|
183
|
+
logger$8.log("\nGenerate a token at: https://hpanel.hostinger.com/profile/api\n");
|
|
184
|
+
token = await prompt("API Token: ", {
|
|
185
|
+
hidden: true,
|
|
186
|
+
instead: LOGIN_INSTEAD
|
|
187
|
+
});
|
|
188
|
+
}
|
|
189
|
+
if (!token) {
|
|
190
|
+
logger$8.error("Token is required");
|
|
191
|
+
process.exit(1);
|
|
192
|
+
}
|
|
193
|
+
await storeHostingerToken(token);
|
|
194
|
+
logger$8.log("\n✓ Hostinger token stored.");
|
|
195
|
+
logger$8.log(` Credentials stored in: ${getCredentialsPath()}`);
|
|
196
|
+
logger$8.log(" Not validated here: every DNS endpoint is scoped to a domain, so the\n first deploy that touches DNS is where a bad token reports itself.");
|
|
197
|
+
logger$8.log("\nYou can now use it as a DNS provider without setting HOSTINGER_API_TOKEN.");
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
/**
|
|
201
|
+
* Logout from a service
|
|
202
|
+
*/
|
|
203
|
+
async function logoutCommand(options) {
|
|
204
|
+
const { provider = "dokploy" } = options;
|
|
205
|
+
if (provider === "all") {
|
|
206
|
+
if (await removeDokployCredentials()) logger$8.log("\n✓ Logged out from all services");
|
|
207
|
+
else logger$8.log("\nNo stored credentials found");
|
|
208
|
+
return;
|
|
209
|
+
}
|
|
210
|
+
if (provider === "dokploy") {
|
|
211
|
+
if (await removeDokployCredentials()) logger$8.log("\n✓ Logged out from Dokploy");
|
|
212
|
+
else logger$8.log("\nNo Dokploy credentials found");
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
/**
|
|
216
|
+
* Show current login status
|
|
217
|
+
*/
|
|
218
|
+
async function whoamiCommand() {
|
|
219
|
+
logger$8.log("\n📋 Current credentials:\n");
|
|
220
|
+
const dokploy = await getDokployCredentials();
|
|
221
|
+
if (dokploy) {
|
|
222
|
+
logger$8.log(" Dokploy:");
|
|
223
|
+
logger$8.log(` Endpoint: ${dokploy.endpoint}`);
|
|
224
|
+
logger$8.log(` Token: ${maskToken(dokploy.token)}`);
|
|
225
|
+
} else logger$8.log(" Dokploy: Not logged in");
|
|
226
|
+
logger$8.log(`\n Credentials file: ${getCredentialsPath()}`);
|
|
227
|
+
}
|
|
228
|
+
/**
|
|
229
|
+
* Mask a token for display
|
|
230
|
+
*/
|
|
231
|
+
function maskToken(token) {
|
|
232
|
+
if (token.length <= 8) return "****";
|
|
233
|
+
return `${token.slice(0, 4)}...${token.slice(-4)}`;
|
|
234
|
+
}
|
|
48
235
|
//#endregion
|
|
49
236
|
//#region src/trust/index.ts
|
|
50
237
|
/**
|
|
@@ -61,7 +248,7 @@ var version = "10.0.0-alpha.64";
|
|
|
61
248
|
* worse trade than one that tells you what to type. Caddy's own `caddy trust`
|
|
62
249
|
* draws the line in the same place.
|
|
63
250
|
*/
|
|
64
|
-
const logger$
|
|
251
|
+
const logger$7 = console;
|
|
65
252
|
const run = promisify(execFile);
|
|
66
253
|
/**
|
|
67
254
|
* How each platform is told about a new authority.
|
|
@@ -113,15 +300,15 @@ async function trustCommand(options = {}) {
|
|
|
113
300
|
const certificate = join(workspace.root, LOCAL_CA_PATH);
|
|
114
301
|
if (!existsSync(certificate)) throw new NoLocalAuthority(certificate);
|
|
115
302
|
const { describe, asks, steps } = installation(process.platform, certificate);
|
|
116
|
-
logger$
|
|
117
|
-
logger$
|
|
303
|
+
logger$7.log(`\n🔐 Trusting the local authority in ${describe}`);
|
|
304
|
+
logger$7.log(` ${certificate}\n`);
|
|
118
305
|
if (options.dryRun) {
|
|
119
|
-
for (const [command, args] of steps) logger$
|
|
306
|
+
for (const [command, args] of steps) logger$7.log(` ${command} ${args.join(" ")}`);
|
|
120
307
|
return;
|
|
121
308
|
}
|
|
122
|
-
logger$
|
|
309
|
+
logger$7.log(` ${asks}\n`);
|
|
123
310
|
for (const [command, args] of steps) await run(command, args, { stdio: "inherit" });
|
|
124
|
-
logger$
|
|
311
|
+
logger$7.log("\n✅ Local https addresses are trusted. Restart your browser.\n");
|
|
125
312
|
}
|
|
126
313
|
/**
|
|
127
314
|
* Trust the authority if this machine lacks it and its owner agrees.
|
|
@@ -138,7 +325,7 @@ async function ensureTrusted(root, url, options = {}) {
|
|
|
138
325
|
if (await isTrusted(url)) return;
|
|
139
326
|
if (!options.assumeYes) {
|
|
140
327
|
if (!process.stdin.isTTY) {
|
|
141
|
-
logger$
|
|
328
|
+
logger$7.log(" Local https addresses are not trusted here. Run \"gkm trust\", or \"gkm setup --yes\".");
|
|
142
329
|
return;
|
|
143
330
|
}
|
|
144
331
|
if (await declined(root)) return;
|
|
@@ -150,7 +337,7 @@ async function ensureTrusted(root, url, options = {}) {
|
|
|
150
337
|
});
|
|
151
338
|
if (!install) {
|
|
152
339
|
await recordDeclined(root);
|
|
153
|
-
logger$
|
|
340
|
+
logger$7.log(" Skipped. Run \"gkm trust\" later.");
|
|
154
341
|
return;
|
|
155
342
|
}
|
|
156
343
|
}
|
|
@@ -1315,7 +1502,7 @@ function closeFakes(served) {
|
|
|
1315
1502
|
}
|
|
1316
1503
|
//#endregion
|
|
1317
1504
|
//#region src/exec/index.ts
|
|
1318
|
-
const logger$
|
|
1505
|
+
const logger$6 = console;
|
|
1319
1506
|
/** `gkm exec` was given nothing to run. */
|
|
1320
1507
|
var NoCommandSpecified = class extends Error {
|
|
1321
1508
|
constructor() {
|
|
@@ -1338,17 +1525,17 @@ async function execCommand(commandArgs, options = {}) {
|
|
|
1338
1525
|
const cwd = options.cwd ?? process.cwd();
|
|
1339
1526
|
if (!commandArgs[0]) throw new NoCommandSpecified();
|
|
1340
1527
|
const defaultEnv = loadEnvFiles(".env");
|
|
1341
|
-
if (defaultEnv.loaded.length > 0) logger$
|
|
1528
|
+
if (defaultEnv.loaded.length > 0) logger$6.log(`📦 Loaded env: ${defaultEnv.loaded.join(", ")}`);
|
|
1342
1529
|
const { credentials, secretsJsonPath, appName } = await prepareEntryCredentials({ cwd });
|
|
1343
|
-
if (appName) logger$
|
|
1530
|
+
if (appName) logger$6.log(`📦 App: ${appName}`);
|
|
1344
1531
|
const secretCount = Object.keys(credentials).filter((k) => k !== "PORT").length;
|
|
1345
|
-
if (secretCount > 0) logger$
|
|
1532
|
+
if (secretCount > 0) logger$6.log(`🔐 Loaded ${secretCount} secret(s)`);
|
|
1346
1533
|
const preloadDir = join(cwd, ".gkm");
|
|
1347
1534
|
await mkdir(preloadDir, { recursive: true });
|
|
1348
1535
|
const preloadPath = join(preloadDir, "credentials-preload.mjs");
|
|
1349
1536
|
await createCredentialsPreload(preloadPath, secretsJsonPath);
|
|
1350
1537
|
const [cmd, ...args] = commandArgs;
|
|
1351
|
-
logger$
|
|
1538
|
+
logger$6.log(`🚀 Running: ${[cmd, ...args].join(" ")}`);
|
|
1352
1539
|
const nodeOptions = [(process.env.NODE_OPTIONS ?? "").replace(/--import[= ]\S+/g, "").trim(), `--import=${preloadPath}`].filter(Boolean).join(" ");
|
|
1353
1540
|
const child = spawn(cmd, args, {
|
|
1354
1541
|
cwd,
|
|
@@ -1362,7 +1549,7 @@ async function execCommand(commandArgs, options = {}) {
|
|
|
1362
1549
|
const exitCode = await new Promise((resolve) => {
|
|
1363
1550
|
child.on("close", (code) => resolve(code ?? 0));
|
|
1364
1551
|
child.on("error", (error) => {
|
|
1365
|
-
logger$
|
|
1552
|
+
logger$6.error(`Failed to run command: ${error.message}`);
|
|
1366
1553
|
resolve(1);
|
|
1367
1554
|
});
|
|
1368
1555
|
});
|
|
@@ -1370,7 +1557,7 @@ async function execCommand(commandArgs, options = {}) {
|
|
|
1370
1557
|
}
|
|
1371
1558
|
//#endregion
|
|
1372
1559
|
//#region src/dev/index.ts
|
|
1373
|
-
const logger$
|
|
1560
|
+
const logger$5 = console;
|
|
1374
1561
|
/**
|
|
1375
1562
|
* Normalize telescope configuration
|
|
1376
1563
|
* @internal Exported for testing
|
|
@@ -1440,13 +1627,13 @@ async function devCommand(options) {
|
|
|
1440
1627
|
if (options.subscribers === false) process.env[SUBSCRIBERS_ENV] = "off";
|
|
1441
1628
|
if (options.entry) return entryDevCommand(options);
|
|
1442
1629
|
const defaultEnv = loadEnvFiles(".env");
|
|
1443
|
-
if (defaultEnv.loaded.length > 0) logger$
|
|
1630
|
+
if (defaultEnv.loaded.length > 0) logger$5.log(`📦 Loaded env: ${defaultEnv.loaded.join(", ")}`);
|
|
1444
1631
|
const appConfig = await loadAppConfig().catch((error) => {
|
|
1445
1632
|
if (error instanceof NotInAnApp || error instanceof NotABackendApp) return;
|
|
1446
1633
|
throw error;
|
|
1447
1634
|
});
|
|
1448
1635
|
if (!appConfig) {
|
|
1449
|
-
logger$
|
|
1636
|
+
logger$5.log("🔎 Reading gkm.config.ts and the constructs");
|
|
1450
1637
|
const { workspace: everything } = await loadWorkspaceConfig();
|
|
1451
1638
|
return workspaceDevCommand(everything, options);
|
|
1452
1639
|
}
|
|
@@ -1459,12 +1646,12 @@ async function devCommand(options) {
|
|
|
1459
1646
|
holder: holderOf
|
|
1460
1647
|
});
|
|
1461
1648
|
if (assigned.running.length > 0) throw new WorkspacePortsInUse(assigned.running);
|
|
1462
|
-
for (const { app, from, to, holder } of assigned.moved) logger$
|
|
1649
|
+
for (const { app, from, to, holder } of assigned.moved) logger$5.log(`↪️ ${app}: ${from} is held by ${describeHolder(holder)}, which is not this workspace's — using ${to}`);
|
|
1463
1650
|
const workspace = withAppPorts(appConfig.workspace, assigned.ports);
|
|
1464
1651
|
const workspaceAppPort = assigned.ports[workspaceAppName] ?? appConfig.app.port;
|
|
1465
1652
|
process.env[APP_TAG_ENV] = appTag(appConfig.workspaceRoot, workspaceAppName);
|
|
1466
1653
|
if (appConfig.app.entry) {
|
|
1467
|
-
logger$
|
|
1654
|
+
logger$5.log(`📄 Using entry point: ${appConfig.app.entry}`);
|
|
1468
1655
|
return entryDevCommand({
|
|
1469
1656
|
...options,
|
|
1470
1657
|
entry: appConfig.app.entry,
|
|
@@ -1474,8 +1661,8 @@ async function devCommand(options) {
|
|
|
1474
1661
|
}
|
|
1475
1662
|
if (config.env) {
|
|
1476
1663
|
const { loaded, missing } = loadEnvFiles(config.env, appRoot);
|
|
1477
|
-
if (loaded.length > 0) logger$
|
|
1478
|
-
if (missing.length > 0) logger$
|
|
1664
|
+
if (loaded.length > 0) logger$5.log(`📦 Loaded env: ${loaded.join(", ")}`);
|
|
1665
|
+
if (missing.length > 0) logger$5.warn(`⚠️ Missing env files: ${missing.join(", ")}`);
|
|
1479
1666
|
}
|
|
1480
1667
|
const telescope = normalizeTelescopeConfig(config.telescope);
|
|
1481
1668
|
const hooks = normalizeHooksConfig(config.hooks, appRoot);
|
|
@@ -1494,7 +1681,7 @@ async function devCommand(options) {
|
|
|
1494
1681
|
bustCache,
|
|
1495
1682
|
serveEmpty: true
|
|
1496
1683
|
}));
|
|
1497
|
-
logger$
|
|
1684
|
+
logger$5.log(`🔨 Building ${workspaceAppName ?? "the app"}`);
|
|
1498
1685
|
const initial = await build();
|
|
1499
1686
|
const runtime = config.runtime ?? "node";
|
|
1500
1687
|
let secretsJsonPath;
|
|
@@ -1505,11 +1692,11 @@ async function devCommand(options) {
|
|
|
1505
1692
|
const reconciled = await reconcileWorkspace(workspace, {
|
|
1506
1693
|
stage: workspace.stages.local,
|
|
1507
1694
|
...options.fake ? { fake: true } : {},
|
|
1508
|
-
progress: (message) => logger$
|
|
1695
|
+
progress: (message) => logger$5.log(message)
|
|
1509
1696
|
});
|
|
1510
1697
|
if (reconciled.services.length > 0) {
|
|
1511
|
-
logger$
|
|
1512
|
-
for (const line of describeServices(reconciled.services)) logger$
|
|
1698
|
+
logger$5.log("🐳 Services");
|
|
1699
|
+
for (const line of describeServices(reconciled.services)) logger$5.log(line);
|
|
1513
1700
|
}
|
|
1514
1701
|
Object.assign(appSecrets, reconciled.env);
|
|
1515
1702
|
const own = reconciled.plan.resources.find((r) => (r.kind === "rest-api" || r.kind === "site") && appKey(r.id) === workspaceAppName);
|
|
@@ -1576,11 +1763,11 @@ async function devCommand(options) {
|
|
|
1576
1763
|
cwd: appRoot
|
|
1577
1764
|
});
|
|
1578
1765
|
watcher.on("error", (error) => {
|
|
1579
|
-
logger$
|
|
1766
|
+
logger$5.error("❌ Watcher error:", error);
|
|
1580
1767
|
});
|
|
1581
1768
|
let rebuildTimeout = null;
|
|
1582
1769
|
watcher.on("change", async (path) => {
|
|
1583
|
-
logger$
|
|
1770
|
+
logger$5.log(`🔄 ${path} changed — rebuilding`);
|
|
1584
1771
|
if (rebuildTimeout) clearTimeout(rebuildTimeout);
|
|
1585
1772
|
rebuildTimeout = setTimeout(async () => {
|
|
1586
1773
|
try {
|
|
@@ -1589,8 +1776,8 @@ async function devCommand(options) {
|
|
|
1589
1776
|
await devServer.restart();
|
|
1590
1777
|
} catch (error) {
|
|
1591
1778
|
const err = error;
|
|
1592
|
-
logger$
|
|
1593
|
-
if (err.stack) logger$
|
|
1779
|
+
logger$5.error(`❌ Rebuild failed: ${err.message}`);
|
|
1780
|
+
if (err.stack) logger$5.error(err.stack);
|
|
1594
1781
|
}
|
|
1595
1782
|
}, 300);
|
|
1596
1783
|
});
|
|
@@ -1598,13 +1785,13 @@ async function devCommand(options) {
|
|
|
1598
1785
|
const shutdown = () => {
|
|
1599
1786
|
if (isShuttingDown) return;
|
|
1600
1787
|
isShuttingDown = true;
|
|
1601
|
-
logger$
|
|
1788
|
+
logger$5.log("\n🛑 Shutting down...");
|
|
1602
1789
|
Promise.all([
|
|
1603
1790
|
watcher.close(),
|
|
1604
1791
|
devServer.stop(),
|
|
1605
1792
|
discovery.leave()
|
|
1606
1793
|
]).catch((err) => {
|
|
1607
|
-
logger$
|
|
1794
|
+
logger$5.error("Error during shutdown:", err);
|
|
1608
1795
|
}).finally(() => {
|
|
1609
1796
|
process.exit(0);
|
|
1610
1797
|
});
|
|
@@ -1808,13 +1995,13 @@ async function workspaceDevCommand(configured, options) {
|
|
|
1808
1995
|
holder: holderOf
|
|
1809
1996
|
});
|
|
1810
1997
|
if (assigned.running.length > 0) throw new WorkspacePortsInUse(assigned.running);
|
|
1811
|
-
for (const { app, from, to, holder } of assigned.moved) logger$
|
|
1998
|
+
for (const { app, from, to, holder } of assigned.moved) logger$5.log(`↪️ ${app}: ${from} is held by ${describeHolder(holder)}, which is not this workspace's — using ${to}`);
|
|
1812
1999
|
const workspace = withAppPorts(configured, assigned.ports);
|
|
1813
2000
|
const appCount = Object.keys(workspace.apps).length;
|
|
1814
2001
|
const frontendApps = Object.entries(workspace.apps).filter(([_, app]) => app.type === "web");
|
|
1815
2002
|
const conflicts = checkPortConflicts(workspace);
|
|
1816
2003
|
if (conflicts.length > 0) {
|
|
1817
|
-
for (const conflict of conflicts) logger$
|
|
2004
|
+
for (const conflict of conflicts) logger$5.error(`❌ Port conflict: Apps "${conflict.app1}" and "${conflict.app2}" both use port ${conflict.port}`);
|
|
1818
2005
|
throw new Error("Port conflicts detected. Please assign unique ports to each app.");
|
|
1819
2006
|
}
|
|
1820
2007
|
if (frontendApps.length > 0) {
|
|
@@ -1823,10 +2010,10 @@ async function workspaceDevCommand(configured, options) {
|
|
|
1823
2010
|
for (const result of validationResults) {
|
|
1824
2011
|
if (!result.valid) {
|
|
1825
2012
|
hasErrors = true;
|
|
1826
|
-
logger$
|
|
1827
|
-
for (const error of result.errors) logger$
|
|
2013
|
+
logger$5.error(`\n❌ Frontend app "${result.appName}" validation failed:`);
|
|
2014
|
+
for (const error of result.errors) logger$5.error(` • ${error}`);
|
|
1828
2015
|
}
|
|
1829
|
-
for (const warning of result.warnings) logger$
|
|
2016
|
+
for (const warning of result.warnings) logger$5.warn(` ⚠️ ${result.appName}: ${warning}`);
|
|
1830
2017
|
}
|
|
1831
2018
|
if (hasErrors) throw new Error("Frontend app validation failed. Fix the issues above and try again.");
|
|
1832
2019
|
}
|
|
@@ -1834,24 +2021,24 @@ async function workspaceDevCommand(configured, options) {
|
|
|
1834
2021
|
const reconciled = await reconcileWorkspace(workspace, {
|
|
1835
2022
|
stage: workspace.stages.local,
|
|
1836
2023
|
...options.fake ? { fake: true } : {},
|
|
1837
|
-
progress: (message) => logger$
|
|
2024
|
+
progress: (message) => logger$5.log(message)
|
|
1838
2025
|
});
|
|
1839
2026
|
if (reconciled.services.length > 0) {
|
|
1840
|
-
logger$
|
|
1841
|
-
for (const line of describeServices(reconciled.services)) logger$
|
|
2027
|
+
logger$5.log("🐳 Services");
|
|
2028
|
+
for (const line of describeServices(reconciled.services)) logger$5.log(line);
|
|
1842
2029
|
}
|
|
1843
2030
|
const secretsEnv = {
|
|
1844
2031
|
...rawSecrets,
|
|
1845
2032
|
...reconciled.env
|
|
1846
2033
|
};
|
|
1847
2034
|
const fakes = options.fake ? await serveFakes(reconciled.fakes, reconciled.ports) : [];
|
|
1848
|
-
for (const { id, port } of fakes) logger$
|
|
1849
|
-
logger$
|
|
2035
|
+
for (const { id, port } of fakes) logger$5.log(`🎭 ${id} fake: http://localhost:${port}`);
|
|
2036
|
+
logger$5.log(options.migrate || options.seed ? "🗄️ Migrating the databases" : "🗄️ Checking for pending migrations");
|
|
1850
2037
|
if (options.migrate || options.seed) {
|
|
1851
|
-
const { prepareDevDatabases } = await import("./migrate-
|
|
2038
|
+
const { prepareDevDatabases } = await import("./migrate-BJBcm4sG.mjs");
|
|
1852
2039
|
await prepareDevDatabases(workspace, reconciled.env, { seed: !!options.seed });
|
|
1853
|
-
} else await import("./migrate-
|
|
1854
|
-
logger$
|
|
2040
|
+
} else await import("./migrate-BJBcm4sG.mjs").then(({ reportPendingMigrations }) => reportPendingMigrations(workspace, reconciled.env)).catch((error) => {
|
|
2041
|
+
logger$5.log(`⚠️ Could not check for pending migrations: ${error instanceof Error ? error.message : String(error)}`);
|
|
1855
2042
|
});
|
|
1856
2043
|
const appUrls = {};
|
|
1857
2044
|
for (const resource of reconciled.plan.resources) {
|
|
@@ -1872,11 +2059,11 @@ async function workspaceDevCommand(configured, options) {
|
|
|
1872
2059
|
else {
|
|
1873
2060
|
const { filters, unpackaged } = turboFilters(workspace);
|
|
1874
2061
|
turboFilter = filters.flatMap((name) => ["--filter", name]);
|
|
1875
|
-
if (unpackaged.length > 0) logger$
|
|
2062
|
+
if (unpackaged.length > 0) logger$5.warn(`⚠️ No package.json with a name for: ${unpackaged.join(", ")} — turbo cannot run ${unpackaged.length === 1 ? "it" : "them"}.`);
|
|
1876
2063
|
}
|
|
1877
2064
|
const buildOrder = getAppBuildOrder(workspace);
|
|
1878
2065
|
const width = Math.max(...buildOrder.map((name) => name.length));
|
|
1879
|
-
logger$
|
|
2066
|
+
logger$5.log(`\n${workspace.name}: ${appCount} app(s)`);
|
|
1880
2067
|
for (const appName of buildOrder) {
|
|
1881
2068
|
const app = workspace.apps[appName];
|
|
1882
2069
|
if (!app) continue;
|
|
@@ -1884,14 +2071,14 @@ async function workspaceDevCommand(configured, options) {
|
|
|
1884
2071
|
const own = reconciled.plan.resources.find((r) => r.kind === "mobile-app" && appKey(r.id) === appName);
|
|
1885
2072
|
const scheme = own ? reconciled.env[own.envKey] : void 0;
|
|
1886
2073
|
const device = Object.entries(reconciled.env).find(([key]) => key.endsWith("_DEVICE_URL"))?.[1];
|
|
1887
|
-
logger$
|
|
2074
|
+
logger$5.log(` ${appName.padEnd(width)} ${scheme ? `${scheme}://` : "mobile"}${device ? ` — devices reach ${new URL(device).hostname}` : ""}`);
|
|
1888
2075
|
continue;
|
|
1889
2076
|
}
|
|
1890
2077
|
const local = `http://localhost:${app.port}`;
|
|
1891
2078
|
const url = Object.entries(appUrls).find(([id]) => appKey(id) === appName)?.[1];
|
|
1892
|
-
logger$
|
|
2079
|
+
logger$5.log(` ${appName.padEnd(width)} ${url ? `${url} -> ${local}` : local}`);
|
|
1893
2080
|
}
|
|
1894
|
-
logger$
|
|
2081
|
+
logger$5.log("");
|
|
1895
2082
|
const configFiles = [
|
|
1896
2083
|
"gkm.config.ts",
|
|
1897
2084
|
"gkm.config.js",
|
|
@@ -1939,7 +2126,7 @@ async function workspaceDevCommand(configured, options) {
|
|
|
1939
2126
|
...options.fake ? { [FAKE_ENV]: "1" } : {},
|
|
1940
2127
|
[DISCOVERY_QUIET_ENV]: "1"
|
|
1941
2128
|
};
|
|
1942
|
-
logger$
|
|
2129
|
+
logger$5.log(`🚀 Starting ${appCount} app(s) — each builds before it answers, and says so when it is ready`);
|
|
1943
2130
|
const turboProcess = spawn("pnpm", [
|
|
1944
2131
|
"turbo",
|
|
1945
2132
|
"run",
|
|
@@ -1955,7 +2142,7 @@ async function workspaceDevCommand(configured, options) {
|
|
|
1955
2142
|
const shutdown = () => {
|
|
1956
2143
|
if (isShuttingDown) return;
|
|
1957
2144
|
isShuttingDown = true;
|
|
1958
|
-
logger$
|
|
2145
|
+
logger$5.log("\n🛑 Shutting down workspace...");
|
|
1959
2146
|
closeFakes(fakes);
|
|
1960
2147
|
discovery.leave();
|
|
1961
2148
|
const pid = turboProcess.pid;
|
|
@@ -1981,7 +2168,7 @@ async function workspaceDevCommand(configured, options) {
|
|
|
1981
2168
|
process.on("SIGTERM", shutdown);
|
|
1982
2169
|
return new Promise((resolve, reject) => {
|
|
1983
2170
|
turboProcess.on("error", (error) => {
|
|
1984
|
-
logger$
|
|
2171
|
+
logger$5.error("❌ Turbo error:", error);
|
|
1985
2172
|
reject(error);
|
|
1986
2173
|
});
|
|
1987
2174
|
turboProcess.on("exit", (code) => {
|
|
@@ -2000,11 +2187,11 @@ async function entryDevCommand(options) {
|
|
|
2000
2187
|
const entryPath = resolve(process.cwd(), entry);
|
|
2001
2188
|
if (!existsSync(entryPath)) throw new Error(`Entry file not found: ${entryPath}`);
|
|
2002
2189
|
const defaultEnv = loadEnvFiles(".env");
|
|
2003
|
-
if (defaultEnv.loaded.length > 0) logger$
|
|
2190
|
+
if (defaultEnv.loaded.length > 0) logger$5.log(`📦 Loaded env: ${defaultEnv.loaded.join(", ")}`);
|
|
2004
2191
|
const { credentials, resolvedPort, secretsJsonPath, appName } = await prepareEntryCredentials({ explicitPort: options.portExplicit ? options.port : void 0 });
|
|
2005
|
-
if (appName) logger$
|
|
2006
|
-
logger$
|
|
2007
|
-
if (Object.keys(credentials).length > 1) logger$
|
|
2192
|
+
if (appName) logger$5.log(`📦 App: ${appName} (port ${resolvedPort})`);
|
|
2193
|
+
logger$5.log(`🚀 Starting entry file: ${entry} on port ${resolvedPort}`);
|
|
2194
|
+
if (Object.keys(credentials).length > 1) logger$5.log(`🔐 Loaded ${Object.keys(credentials).length - 1} secret(s) + PORT`);
|
|
2008
2195
|
const wrapperDir = join(process.cwd(), ".gkm");
|
|
2009
2196
|
await mkdir(wrapperDir, { recursive: true });
|
|
2010
2197
|
const wrapperPath = join(wrapperDir, "entry-wrapper.ts");
|
|
@@ -2015,7 +2202,7 @@ async function entryDevCommand(options) {
|
|
|
2015
2202
|
const shutdown = () => {
|
|
2016
2203
|
if (isShuttingDown) return;
|
|
2017
2204
|
isShuttingDown = true;
|
|
2018
|
-
logger$
|
|
2205
|
+
logger$5.log("\n🛑 Shutting down...");
|
|
2019
2206
|
runner.stop();
|
|
2020
2207
|
process.exit(0);
|
|
2021
2208
|
};
|
|
@@ -2051,14 +2238,14 @@ var EntryRunner = class {
|
|
|
2051
2238
|
});
|
|
2052
2239
|
let restartTimeout = null;
|
|
2053
2240
|
this.watcher.on("change", (path) => {
|
|
2054
|
-
logger$
|
|
2241
|
+
logger$5.log(`📝 File changed: ${path}`);
|
|
2055
2242
|
if (restartTimeout) clearTimeout(restartTimeout);
|
|
2056
2243
|
restartTimeout = setTimeout(async () => {
|
|
2057
|
-
logger$
|
|
2244
|
+
logger$5.log("🔄 Restarting...");
|
|
2058
2245
|
await this.restart();
|
|
2059
2246
|
}, 300);
|
|
2060
2247
|
});
|
|
2061
|
-
logger$
|
|
2248
|
+
logger$5.log(`👀 Watching for changes in: ${watchDir}`);
|
|
2062
2249
|
}
|
|
2063
2250
|
}
|
|
2064
2251
|
async runProcess() {
|
|
@@ -2074,17 +2261,17 @@ var EntryRunner = class {
|
|
|
2074
2261
|
});
|
|
2075
2262
|
this.isRunning = true;
|
|
2076
2263
|
this.childProcess.on("error", (error) => {
|
|
2077
|
-
logger$
|
|
2264
|
+
logger$5.error("❌ Process error:", error);
|
|
2078
2265
|
});
|
|
2079
2266
|
this.childProcess.on("exit", (code) => {
|
|
2080
|
-
if (code !== null && code !== 0 && code !== 143) logger$
|
|
2267
|
+
if (code !== null && code !== 0 && code !== 143) logger$5.error(`❌ Process exited with code ${code}`);
|
|
2081
2268
|
this.isRunning = false;
|
|
2082
2269
|
});
|
|
2083
2270
|
await new Promise((resolve) => setTimeout(resolve, 500));
|
|
2084
2271
|
if (this.isRunning) {
|
|
2085
|
-
logger$
|
|
2086
|
-
logger$
|
|
2087
|
-
logger$
|
|
2272
|
+
logger$5.log("");
|
|
2273
|
+
logger$5.log(` \x1b[32m✓ Ready\x1b[0m at \x1b[36mhttp://localhost:${this.port}\x1b[0m`);
|
|
2274
|
+
logger$5.log("");
|
|
2088
2275
|
}
|
|
2089
2276
|
}
|
|
2090
2277
|
async restart() {
|
|
@@ -2203,7 +2390,7 @@ start({
|
|
|
2203
2390
|
* lets a console read what this machine is running.
|
|
2204
2391
|
*/
|
|
2205
2392
|
function announce(discovery) {
|
|
2206
|
-
logger$
|
|
2393
|
+
logger$5.log(`🧭 Discovery: ${discovery.connectUrl}`);
|
|
2207
2394
|
}
|
|
2208
2395
|
/** Run `fn` with `console.log` muted — warnings and errors still print. */
|
|
2209
2396
|
async function quietly(fn) {
|
|
@@ -2321,7 +2508,7 @@ var DevServer = class {
|
|
|
2321
2508
|
this.actualPort = this.requestedPort;
|
|
2322
2509
|
} else {
|
|
2323
2510
|
this.actualPort = await findAvailablePort(this.requestedPort);
|
|
2324
|
-
if (this.actualPort !== this.requestedPort) logger$
|
|
2511
|
+
if (this.actualPort !== this.requestedPort) logger$5.log(`ℹ️ Port ${this.requestedPort} was in use, using port ${this.actualPort} instead`);
|
|
2325
2512
|
}
|
|
2326
2513
|
const serverEntryPath = join(this.appRoot, ".gkm", "server", "server.ts");
|
|
2327
2514
|
await this.createServerEntry();
|
|
@@ -2341,11 +2528,11 @@ var DevServer = class {
|
|
|
2341
2528
|
});
|
|
2342
2529
|
this.isRunning = true;
|
|
2343
2530
|
this.serverProcess.on("error", (error) => {
|
|
2344
|
-
logger$
|
|
2531
|
+
logger$5.error("❌ Server error:", error);
|
|
2345
2532
|
});
|
|
2346
2533
|
const child = this.serverProcess;
|
|
2347
2534
|
child.on("exit", (code, signal) => {
|
|
2348
|
-
if (code !== null && code !== 0 && signal !== "SIGTERM") logger$
|
|
2535
|
+
if (code !== null && code !== 0 && signal !== "SIGTERM") logger$5.error(`❌ Server exited with code ${code}`);
|
|
2349
2536
|
this.isRunning = false;
|
|
2350
2537
|
if (this.serverProcess === child) this.onStatus?.("stopped");
|
|
2351
2538
|
});
|
|
@@ -2354,13 +2541,13 @@ var DevServer = class {
|
|
|
2354
2541
|
const local = `http://localhost:${this.actualPort}`;
|
|
2355
2542
|
const address = this.publicUrl ? `${this.publicUrl} -> ${local}` : local;
|
|
2356
2543
|
const seconds = ((Date.now() - this.startTime) / 1e3).toFixed(1);
|
|
2357
|
-
logger$
|
|
2544
|
+
logger$5.log(`\x1b[32m✓\x1b[0m ${this.label} ready in ${seconds}s ${address}`);
|
|
2358
2545
|
const tools = this.selfServing ? [] : [
|
|
2359
2546
|
...this.enableOpenApi ? ["docs /__docs"] : [],
|
|
2360
2547
|
...this.telescope ? [`telescope ${this.telescope.path}`] : [],
|
|
2361
2548
|
...this.databaseApi ? [`db ${this.databaseApi}`] : []
|
|
2362
2549
|
];
|
|
2363
|
-
if (tools.length > 0) logger$
|
|
2550
|
+
if (tools.length > 0) logger$5.log(` ${tools.join(" · ")}`);
|
|
2364
2551
|
this.onStatus?.("ready");
|
|
2365
2552
|
}
|
|
2366
2553
|
}
|
|
@@ -3056,7 +3243,7 @@ ${shutdown}`);
|
|
|
3056
3243
|
}
|
|
3057
3244
|
//#endregion
|
|
3058
3245
|
//#region src/build/index.ts
|
|
3059
|
-
const logger$
|
|
3246
|
+
const logger$4 = console;
|
|
3060
3247
|
/**
|
|
3061
3248
|
* Where `gkm build --stage` leaves the key its bundle's secrets were encrypted
|
|
3062
3249
|
* with, beside the bundle in `.gkm/server/` and readable by its owner alone.
|
|
@@ -3104,7 +3291,7 @@ async function buildCommand(options) {
|
|
|
3104
3291
|
const isAtWorkspaceRoot = cwd === workspaceRoot;
|
|
3105
3292
|
const someAppIsElsewhere = Object.values(loadedConfig.workspace.apps).some((app) => resolve(workspaceRoot, app.path) !== workspaceRoot);
|
|
3106
3293
|
if (isAtWorkspaceRoot && someAppIsElsewhere) {
|
|
3107
|
-
logger$
|
|
3294
|
+
logger$4.log("📦 Detected workspace configuration");
|
|
3108
3295
|
return workspaceBuildCommand(loadedConfig.workspace, options);
|
|
3109
3296
|
}
|
|
3110
3297
|
}
|
|
@@ -3150,13 +3337,13 @@ async function buildOneApp(input) {
|
|
|
3150
3337
|
const { config, options, workspace, appRoot, target } = input;
|
|
3151
3338
|
const cacheBackend = cacheBackendFor(providerOf(workspace));
|
|
3152
3339
|
const production = normalizeProductionConfig(options.production ?? false);
|
|
3153
|
-
if (production) logger$
|
|
3154
|
-
logger$
|
|
3155
|
-
logger$
|
|
3340
|
+
if (production) logger$4.log(`🏭 Building for PRODUCTION`);
|
|
3341
|
+
logger$4.log(`Building for ${target}`);
|
|
3342
|
+
logger$4.log(`Loading constructs from: ${formatRoutes(config.constructs)}`);
|
|
3156
3343
|
const telescope = production ? void 0 : normalizeTelescopeConfig(config.telescope);
|
|
3157
|
-
if (telescope) logger$
|
|
3344
|
+
if (telescope) logger$4.log(`🔭 Telescope enabled at ${telescope.path}`);
|
|
3158
3345
|
const hooks = normalizeHooksConfig(config.hooks, appRoot);
|
|
3159
|
-
if (hooks) logger$
|
|
3346
|
+
if (hooks) logger$4.log(`🪝 Server hooks enabled`);
|
|
3160
3347
|
return buildApp({
|
|
3161
3348
|
config,
|
|
3162
3349
|
workspaceRoot: workspace.root,
|
|
@@ -3198,7 +3385,7 @@ async function writeManifest(input) {
|
|
|
3198
3385
|
});
|
|
3199
3386
|
const path = join(manifestDir, `${target}.ts`);
|
|
3200
3387
|
await writeFile(path, manifestModule(folded, backends));
|
|
3201
|
-
logger$
|
|
3388
|
+
logger$4.log(`Manifest: ${relative(process.cwd(), path)}`);
|
|
3202
3389
|
}
|
|
3203
3390
|
/**
|
|
3204
3391
|
* Turn one app's constructs into its generated entry.
|
|
@@ -3226,11 +3413,11 @@ async function buildApp(input) {
|
|
|
3226
3413
|
});
|
|
3227
3414
|
if (selfServing) {
|
|
3228
3415
|
await writeSurfaceEntry(join(appRoot, ".gkm", "server"), selfServing.source);
|
|
3229
|
-
logger$
|
|
3416
|
+
logger$4.log(`Generated a server for ${selfServing.id} from its own declaration`);
|
|
3230
3417
|
if (production?.bundle && !input.skipBundle) {
|
|
3231
3418
|
const outputDir = join(appRoot, ".gkm", "server");
|
|
3232
3419
|
const entryPoint = await writeSurfaceServer(outputDir, production);
|
|
3233
|
-
const { bundleServer } = await import("./bundler-
|
|
3420
|
+
const { bundleServer } = await import("./bundler-Dvaiud0h.mjs");
|
|
3234
3421
|
await bundleServer({
|
|
3235
3422
|
entryPoint,
|
|
3236
3423
|
outputDir: join(outputDir, "dist"),
|
|
@@ -3239,7 +3426,7 @@ async function buildApp(input) {
|
|
|
3239
3426
|
external: production.external,
|
|
3240
3427
|
...input.stage ? { stage: input.stage } : {}
|
|
3241
3428
|
});
|
|
3242
|
-
logger$
|
|
3429
|
+
logger$4.log(`✅ Bundle complete: .gkm/server/dist/server.mjs`);
|
|
3243
3430
|
}
|
|
3244
3431
|
return { selfServing: selfServing.id };
|
|
3245
3432
|
}
|
|
@@ -3285,14 +3472,14 @@ async function buildApp(input) {
|
|
|
3285
3472
|
topicGenerator.load(code, appRoot, bustCache)
|
|
3286
3473
|
]);
|
|
3287
3474
|
const allEndpoints = servedBy(loadedEndpoints, derived.surface);
|
|
3288
|
-
logger$
|
|
3289
|
-
logger$
|
|
3290
|
-
logger$
|
|
3291
|
-
logger$
|
|
3292
|
-
logger$
|
|
3293
|
-
logger$
|
|
3475
|
+
logger$4.log(`Found ${allEndpoints.length} endpoints`);
|
|
3476
|
+
logger$4.log(`Found ${allFunctions.length} functions`);
|
|
3477
|
+
logger$4.log(`Found ${allCrons.length} crons`);
|
|
3478
|
+
logger$4.log(`Found ${allSubscribers.length} subscribers`);
|
|
3479
|
+
logger$4.log(`Found ${allQueues.length} queues`);
|
|
3480
|
+
logger$4.log(`Found ${allTopics.length} topics`);
|
|
3294
3481
|
if (!input.serveEmpty && allEndpoints.length === 0 && allFunctions.length === 0 && allCrons.length === 0 && allSubscribers.length === 0 && allQueues.length === 0 && allTopics.length === 0) {
|
|
3295
|
-
logger$
|
|
3482
|
+
logger$4.log("No endpoints, functions, crons, subscribers, queues, or topics found to process");
|
|
3296
3483
|
return {};
|
|
3297
3484
|
}
|
|
3298
3485
|
if (production?.subscribers === "include" && derived.surface) {
|
|
@@ -3305,7 +3492,7 @@ async function buildApp(input) {
|
|
|
3305
3492
|
[allQueues.length, "queue consumer"],
|
|
3306
3493
|
[allSubscribers.length, "subscriber"]
|
|
3307
3494
|
].filter(([n]) => n > 0).map(([n, what]) => `${n} ${what}${n === 1 ? "" : "s"}`);
|
|
3308
|
-
if (left.length > 0) logger$
|
|
3495
|
+
if (left.length > 0) logger$4.log(`Serving ${derived.surface.id} only: leaving out ${left.join(", ")} — they run in a Worker's image`);
|
|
3309
3496
|
}
|
|
3310
3497
|
const result = await buildForTarget(target, buildContext, appRoot, workspaceRoot, endpointGenerator, functionGenerator, cronGenerator, subscriberGenerator, queueGenerator, topicGenerator, allEndpoints, allFunctions, allCrons, allSubscribers, allQueues, allTopics, enableOpenApi, input.skipBundle ?? false, input.stage);
|
|
3311
3498
|
await generateOpenApi(allEndpoints.map(({ construct }) => construct), {
|
|
@@ -3328,7 +3515,7 @@ async function buildForTarget(target, context, appRoot, workspaceRoot, endpointG
|
|
|
3328
3515
|
force: true
|
|
3329
3516
|
});
|
|
3330
3517
|
await mkdir(outputDir, { recursive: true });
|
|
3331
|
-
logger$
|
|
3518
|
+
logger$4.log(`\nGenerating handlers for ${target}`);
|
|
3332
3519
|
const options = {
|
|
3333
3520
|
target,
|
|
3334
3521
|
root: workspaceRoot
|
|
@@ -3344,7 +3531,7 @@ async function buildForTarget(target, context, appRoot, workspaceRoot, endpointG
|
|
|
3344
3531
|
queueGenerator.build(context, queues, outputDir, options),
|
|
3345
3532
|
topicGenerator.build(context, topics, outputDir, options)
|
|
3346
3533
|
]);
|
|
3347
|
-
logger$
|
|
3534
|
+
logger$4.log(`Generated ${routes.length} routes, ${functionInfos.length} functions, ${cronInfos.length} crons, ${subscriberInfos.length} subscribers, ${queueInfos.length} queues, ${topicInfos.length} topics for ${target}`);
|
|
3348
3535
|
const fields = {
|
|
3349
3536
|
...context.surface ? { surface: context.surface.id } : {},
|
|
3350
3537
|
functions: functionInfos,
|
|
@@ -3367,8 +3554,8 @@ async function buildForTarget(target, context, appRoot, workspaceRoot, endpointG
|
|
|
3367
3554
|
};
|
|
3368
3555
|
let masterKey;
|
|
3369
3556
|
if (context.production?.bundle && !skipBundle) {
|
|
3370
|
-
logger$
|
|
3371
|
-
const { bundleServer } = await import("./bundler-
|
|
3557
|
+
logger$4.log(`\n📦 Bundling production server...`);
|
|
3558
|
+
const { bundleServer } = await import("./bundler-Dvaiud0h.mjs");
|
|
3372
3559
|
const allConstructs = [
|
|
3373
3560
|
...endpoints.map((e) => e.construct),
|
|
3374
3561
|
...functions.map((f) => f.construct),
|
|
@@ -3385,13 +3572,13 @@ async function buildForTarget(target, context, appRoot, workspaceRoot, endpointG
|
|
|
3385
3572
|
stage,
|
|
3386
3573
|
constructs: allConstructs
|
|
3387
3574
|
})).masterKey;
|
|
3388
|
-
logger$
|
|
3575
|
+
logger$4.log(`✅ Bundle complete: .gkm/server/dist/server.mjs`);
|
|
3389
3576
|
if (masterKey) {
|
|
3390
3577
|
const keyPath = join(outputDir, MASTER_KEY_FILE);
|
|
3391
3578
|
await writeFile(keyPath, `${masterKey}\n`, { mode: 384 });
|
|
3392
3579
|
await chmod(keyPath, 384);
|
|
3393
|
-
logger$
|
|
3394
|
-
logger$
|
|
3580
|
+
logger$4.log(`\n🔐 Secrets encrypted for deployment`);
|
|
3581
|
+
logger$4.log(` Run with GKM_MASTER_KEY set to the key in ${relative(appRoot, keyPath)} (fingerprint ${keyFingerprint(masterKey)})`);
|
|
3395
3582
|
}
|
|
3396
3583
|
}
|
|
3397
3584
|
return {
|
|
@@ -3492,12 +3679,12 @@ async function workspaceBuildCommand(workspace, options) {
|
|
|
3492
3679
|
const apps = Object.entries(workspace.apps);
|
|
3493
3680
|
const backendApps = apps.filter(([, app]) => app.type === "backend");
|
|
3494
3681
|
const frontendApps = apps.filter(([, app]) => app.type === "web");
|
|
3495
|
-
logger$
|
|
3496
|
-
logger$
|
|
3497
|
-
logger$
|
|
3498
|
-
if (options.production) logger$
|
|
3682
|
+
logger$4.log(`\n🏗️ Building workspace: ${workspace.name}`);
|
|
3683
|
+
logger$4.log(` Backend apps: ${backendApps.map(([name]) => name).join(", ") || "none"}`);
|
|
3684
|
+
logger$4.log(` Frontend apps: ${frontendApps.map(([name]) => name).join(", ") || "none"}`);
|
|
3685
|
+
if (options.production) logger$4.log(` 🏭 Production mode enabled`);
|
|
3499
3686
|
const buildOrder = getAppBuildOrder(workspace);
|
|
3500
|
-
logger$
|
|
3687
|
+
logger$4.log(` Build order: ${buildOrder.join(" → ")}`);
|
|
3501
3688
|
try {
|
|
3502
3689
|
const target = options.provider ?? providerOf(workspace);
|
|
3503
3690
|
const builds = [];
|
|
@@ -3505,7 +3692,7 @@ async function workspaceBuildCommand(workspace, options) {
|
|
|
3505
3692
|
const app = workspace.apps[appName];
|
|
3506
3693
|
const config = getAppGkmConfig(workspace, appName);
|
|
3507
3694
|
if (!app || app.type !== "backend" || !config) continue;
|
|
3508
|
-
logger$
|
|
3695
|
+
logger$4.log(`\n⚙️ ${appName}`);
|
|
3509
3696
|
const output = await buildOneApp({
|
|
3510
3697
|
config,
|
|
3511
3698
|
options,
|
|
@@ -3530,8 +3717,8 @@ async function workspaceBuildCommand(workspace, options) {
|
|
|
3530
3717
|
const pm = detectPackageManager();
|
|
3531
3718
|
const [turboBin, turboArgs] = getTurboArgs(pm, filters, { only: true });
|
|
3532
3719
|
if (filters.length > 0) {
|
|
3533
|
-
logger$
|
|
3534
|
-
logger$
|
|
3720
|
+
logger$4.log(`\n📦 Using ${pm} with Turbo for the other apps...\n`);
|
|
3721
|
+
logger$4.log(`Running: ${[turboBin, ...turboArgs].join(" ")}`);
|
|
3535
3722
|
const sandbox = options.sandbox ?? new LocalSandbox({
|
|
3536
3723
|
root: workspace.root,
|
|
3537
3724
|
passEnv: ["TURBO_TOKEN"]
|
|
@@ -3556,15 +3743,15 @@ async function workspaceBuildCommand(workspace, options) {
|
|
|
3556
3743
|
outputPath
|
|
3557
3744
|
});
|
|
3558
3745
|
}
|
|
3559
|
-
logger$
|
|
3560
|
-
logger$
|
|
3746
|
+
logger$4.log(`\n✅ Workspace build complete!`);
|
|
3747
|
+
logger$4.log(`\n📋 Build Summary:`);
|
|
3561
3748
|
for (const result of results) {
|
|
3562
3749
|
const icon = result.type === "backend" ? "⚙️" : result.type === "mobile" ? "📱" : "🌐";
|
|
3563
|
-
logger$
|
|
3750
|
+
logger$4.log(` ${icon} ${result.appName}: ${result.outputPath || "built"}`);
|
|
3564
3751
|
}
|
|
3565
3752
|
} catch (error) {
|
|
3566
3753
|
const errorMessage = error instanceof Error ? error.message : "Build failed";
|
|
3567
|
-
logger$
|
|
3754
|
+
logger$4.log(`\n❌ Build failed: ${errorMessage}`);
|
|
3568
3755
|
for (const [appName, app] of apps) results.push({
|
|
3569
3756
|
appName,
|
|
3570
3757
|
type: app.type,
|
|
@@ -3594,1169 +3781,17 @@ function formatRoutes(routes) {
|
|
|
3594
3781
|
return Array.isArray(routes) ? routes.join(", ") : routes;
|
|
3595
3782
|
}
|
|
3596
3783
|
//#endregion
|
|
3597
|
-
//#region src/migrate/provider.ts
|
|
3598
|
-
/**
|
|
3599
|
-
* One construct's migrations and seeds, read from its folders.
|
|
3600
|
-
*
|
|
3601
|
-
* Two kinds of file, told apart by extension rather than by configuration: a
|
|
3602
|
-
* script exports `up` (and may export `down`) — or `seed(db, { stage })`, in
|
|
3603
|
-
* `seeds/` — and a
|
|
3604
|
-
* `.sql` file is run whole. The second is what makes a folder independent of
|
|
3605
|
-
* the client the app uses — Better Auth's schema is SQL whatever the
|
|
3606
|
-
* application queries with.
|
|
3607
|
-
*/
|
|
3608
|
-
const SCRIPTS = /* @__PURE__ */ new Set([
|
|
3609
|
-
".ts",
|
|
3610
|
-
".mts",
|
|
3611
|
-
".js",
|
|
3612
|
-
".mjs"
|
|
3613
|
-
]);
|
|
3614
|
-
/** Two files in one folder that would be one migration. */
|
|
3615
|
-
var DuplicateMigration = class extends Error {
|
|
3616
|
-
folder;
|
|
3617
|
-
migration;
|
|
3618
|
-
constructor(folder, migration) {
|
|
3619
|
-
super(`${folder} has two migrations named '${migration}' — a script and a .sql file with the same name. A migration is recorded by name, so only one of them could ever run; rename or remove the other.`);
|
|
3620
|
-
this.folder = folder;
|
|
3621
|
-
this.migration = migration;
|
|
3622
|
-
this.name = "DuplicateMigration";
|
|
3623
|
-
}
|
|
3624
|
-
};
|
|
3625
|
-
/** A script in a migrations folder that exports no `up`. */
|
|
3626
|
-
var MigrationHasNoUp = class extends Error {
|
|
3627
|
-
file;
|
|
3628
|
-
constructor(file) {
|
|
3629
|
-
super(`${file} is in a migrations folder but exports no \`up\` function. Export \`async function up(db: Kysely<unknown>)\`, or move the file out of the folder if it is not a migration.`);
|
|
3630
|
-
this.file = file;
|
|
3631
|
-
this.name = "MigrationHasNoUp";
|
|
3632
|
-
}
|
|
3633
|
-
};
|
|
3634
|
-
/** A script in a seeds folder that exports no `seed`. */
|
|
3635
|
-
var SeedHasNoSeed = class extends Error {
|
|
3636
|
-
file;
|
|
3637
|
-
constructor(file) {
|
|
3638
|
-
super(`${file} is in a seeds folder but exports no \`seed\` function. Export \`async function seed(db, { stage })\`, or move the file out of the folder if it is not a seed.`);
|
|
3639
|
-
this.file = file;
|
|
3640
|
-
this.name = "SeedHasNoSeed";
|
|
3641
|
-
}
|
|
3642
|
-
};
|
|
3643
|
-
/**
|
|
3644
|
-
* The migration files in a folder, by name, without reading them.
|
|
3645
|
-
*
|
|
3646
|
-
* Also what `gkm dev` compares against a construct's history to report what
|
|
3647
|
-
* is pending, which is why it is separate from loading them.
|
|
3648
|
-
*/
|
|
3649
|
-
async function migrationFiles(folder) {
|
|
3650
|
-
const entries = await readdir(folder, { withFileTypes: true }).catch((error) => {
|
|
3651
|
-
if (error.code === "ENOENT") return [];
|
|
3652
|
-
throw error;
|
|
3653
|
-
});
|
|
3654
|
-
const files = /* @__PURE__ */ new Map();
|
|
3655
|
-
for (const entry of entries) {
|
|
3656
|
-
if (!entry.isFile() || !isMigration(entry.name)) continue;
|
|
3657
|
-
const name = entry.name.slice(0, -extname(entry.name).length);
|
|
3658
|
-
if (files.has(name)) throw new DuplicateMigration(folder, name);
|
|
3659
|
-
files.set(name, join(folder, entry.name));
|
|
3660
|
-
}
|
|
3661
|
-
return files;
|
|
3662
|
-
}
|
|
3663
|
-
/** A Kysely provider over one construct's folder. */
|
|
3664
|
-
function folderProvider(folder, raw) {
|
|
3665
|
-
return { async getMigrations() {
|
|
3666
|
-
const migrations = {};
|
|
3667
|
-
for (const [name, file] of await migrationFiles(folder)) migrations[name] = extname(file) === ".sql" ? await sqlMigration(file, raw) : await scriptMigration(file);
|
|
3668
|
-
return migrations;
|
|
3669
|
-
} };
|
|
3670
|
-
}
|
|
3671
|
-
/**
|
|
3672
|
-
* The seeds in a folder, loaded, in name order.
|
|
3673
|
-
*
|
|
3674
|
-
* Every one runs on every pass — there is no history to consult — so the order
|
|
3675
|
-
* is the only thing between two seeds where one reads what another writes.
|
|
3676
|
-
* Name them to sort that way.
|
|
3677
|
-
*/
|
|
3678
|
-
async function loadSeeds(folder, raw) {
|
|
3679
|
-
const files = [...await migrationFiles(folder)].sort(([a], [b]) => a < b ? -1 : a > b ? 1 : 0);
|
|
3680
|
-
const seeds = [];
|
|
3681
|
-
for (const [name, file] of files) seeds.push({
|
|
3682
|
-
name,
|
|
3683
|
-
run: extname(file) === ".sql" ? (await sqlMigration(file, raw)).up : await scriptSeed(file)
|
|
3684
|
-
});
|
|
3685
|
-
return seeds;
|
|
3686
|
-
}
|
|
3687
|
-
/**
|
|
3688
|
-
* A seed script, imported as it is on disk now.
|
|
3689
|
-
*
|
|
3690
|
-
* Keyed by its modification time: a seed is run again after it is edited, and
|
|
3691
|
-
* a process that has imported it once would otherwise run the old one.
|
|
3692
|
-
*/
|
|
3693
|
-
async function scriptSeed(file) {
|
|
3694
|
-
const url = pathToFileURL(file);
|
|
3695
|
-
url.searchParams.set("mtime", String((await stat(file)).mtimeMs));
|
|
3696
|
-
const module = await import(url.href);
|
|
3697
|
-
if (typeof module.seed !== "function") throw new SeedHasNoSeed(file);
|
|
3698
|
-
return module.seed;
|
|
3699
|
-
}
|
|
3700
|
-
function isMigration(file) {
|
|
3701
|
-
if (file.endsWith(".d.ts")) return false;
|
|
3702
|
-
if (/\.(spec|test)\.[cm]?[jt]s$/.test(file)) return false;
|
|
3703
|
-
const extension = extname(file);
|
|
3704
|
-
return extension === ".sql" || SCRIPTS.has(extension);
|
|
3705
|
-
}
|
|
3706
|
-
async function scriptMigration(file) {
|
|
3707
|
-
const module = await import(pathToFileURL(file).href);
|
|
3708
|
-
if (typeof module.up !== "function") throw new MigrationHasNoUp(file);
|
|
3709
|
-
return {
|
|
3710
|
-
up: module.up,
|
|
3711
|
-
...typeof module.down === "function" ? { down: module.down } : {}
|
|
3712
|
-
};
|
|
3713
|
-
}
|
|
3714
|
-
/**
|
|
3715
|
-
* A `.sql` file, run whole.
|
|
3716
|
-
*
|
|
3717
|
-
* One query rather than split on `;`: with no parameters, Postgres takes
|
|
3718
|
-
* several statements in one message, and splitting by hand breaks on the first
|
|
3719
|
-
* `;` inside a string or a function body. There is no `down` — a generated
|
|
3720
|
-
* schema is rolled back by a migration that says how, not by guessing.
|
|
3721
|
-
*/
|
|
3722
|
-
async function sqlMigration(file, raw) {
|
|
3723
|
-
const statements = await readFile(file, "utf-8");
|
|
3724
|
-
return { up: async (db) => {
|
|
3725
|
-
await raw.raw(statements).execute(db);
|
|
3726
|
-
} };
|
|
3727
|
-
}
|
|
3728
|
-
//#endregion
|
|
3729
|
-
//#region src/migrate/databases.ts
|
|
3730
|
-
/**
|
|
3731
|
-
* Bring each construct's schema up to its migrations folder.
|
|
3732
|
-
*
|
|
3733
|
-
* Which constructs, in what order, from which folder, is `migrationTargets`'
|
|
3734
|
-
* answer and nobody else's. What is decided here is only how one target is
|
|
3735
|
-
* run: as its owner, in its own schema, with a history of its own.
|
|
3736
|
-
*/
|
|
3737
|
-
/** A folder under `db/` that names no construct. */
|
|
3738
|
-
var UnknownMigrationFolder = class extends Error {
|
|
3739
|
-
folder;
|
|
3740
|
-
known;
|
|
3741
|
-
constructor(folder, known) {
|
|
3742
|
-
super(`${folder} names no database construct, so its migrations would never run. ${known.length > 0 ? `The folders that do: ${known.join(", ")}.` : "This project declares no database."} Rename the folder after the construct it belongs to.`);
|
|
3743
|
-
this.folder = folder;
|
|
3744
|
-
this.known = known;
|
|
3745
|
-
this.name = "UnknownMigrationFolder";
|
|
3746
|
-
}
|
|
3747
|
-
};
|
|
3748
|
-
/** A construct was asked for by a name that takes no migrations. */
|
|
3749
|
-
var NoSuchMigrationTarget = class extends Error {
|
|
3750
|
-
construct;
|
|
3751
|
-
known;
|
|
3752
|
-
constructor(construct, known) {
|
|
3753
|
-
super(`'${construct}' is not a database construct. ` + (known.length > 0 ? `The ones with migrations: ${known.join(", ")}.` : "This project declares no database."));
|
|
3754
|
-
this.construct = construct;
|
|
3755
|
-
this.known = known;
|
|
3756
|
-
this.name = "NoSuchMigrationTarget";
|
|
3757
|
-
}
|
|
3758
|
-
};
|
|
3759
|
-
/**
|
|
3760
|
-
* A migration left directly in a construct's folder, where nothing reads it.
|
|
3761
|
-
*
|
|
3762
|
-
* Migrations moved into `migrations/` so seeds could sit beside them. A file
|
|
3763
|
-
* still at the old level would otherwise never run, and nothing would say so.
|
|
3764
|
-
*/
|
|
3765
|
-
var MigrationsOutsideFolder = class extends Error {
|
|
3766
|
-
folder;
|
|
3767
|
-
files;
|
|
3768
|
-
constructor(folder, files) {
|
|
3769
|
-
super(`${folder} has ${files.join(", ")} directly inside it, where nothing runs them. Migrations live in ${folder}/migrations/ and seeds in ${folder}/seeds/ — move ${files.length === 1 ? "it" : "them"} there.`);
|
|
3770
|
-
this.folder = folder;
|
|
3771
|
-
this.files = files;
|
|
3772
|
-
this.name = "MigrationsOutsideFolder";
|
|
3773
|
-
}
|
|
3774
|
-
};
|
|
3775
|
-
/** A folder inside a construct's folder that is neither `migrations` nor `seeds`. */
|
|
3776
|
-
var UnknownDatabaseFolder = class extends Error {
|
|
3777
|
-
folder;
|
|
3778
|
-
constructor(folder) {
|
|
3779
|
-
super(`${folder} is neither migrations/ nor seeds/, so nothing in it would ever run. A construct's folder holds those two; rename or remove it.`);
|
|
3780
|
-
this.folder = folder;
|
|
3781
|
-
this.name = "UnknownDatabaseFolder";
|
|
3782
|
-
}
|
|
3783
|
-
};
|
|
3784
|
-
/** A seed failed; its own writes are rolled back, the ones before it stay. */
|
|
3785
|
-
var SeedFailed = class extends Error {
|
|
3786
|
-
construct;
|
|
3787
|
-
seed;
|
|
3788
|
-
cause;
|
|
3789
|
-
constructor(construct, seed, cause) {
|
|
3790
|
-
super(`${construct}: seed '${seed}' failed — ${cause instanceof Error ? cause.message : String(cause)}. Its writes were rolled back; fix it and run it again.`);
|
|
3791
|
-
this.construct = construct;
|
|
3792
|
-
this.seed = seed;
|
|
3793
|
-
this.cause = cause;
|
|
3794
|
-
this.name = "SeedFailed";
|
|
3795
|
-
}
|
|
3796
|
-
};
|
|
3797
|
-
/** A construct with roles has no owner URL to migrate with. */
|
|
3798
|
-
var NoOwnerCredential = class extends Error {
|
|
3799
|
-
construct;
|
|
3800
|
-
key;
|
|
3801
|
-
constructor(construct, key) {
|
|
3802
|
-
super(`${construct} has no ${key} to migrate with. Migrations run as the owner role — the runtime one cannot create a table — and nothing resolved its URL. Is the stage reconciled?`);
|
|
3803
|
-
this.construct = construct;
|
|
3804
|
-
this.key = key;
|
|
3805
|
-
this.name = "NoOwnerCredential";
|
|
3806
|
-
}
|
|
3807
|
-
};
|
|
3808
|
-
/** A migration failed; the ones before it in the run stay applied. */
|
|
3809
|
-
var MigrationFailed = class extends Error {
|
|
3810
|
-
construct;
|
|
3811
|
-
migration;
|
|
3812
|
-
cause;
|
|
3813
|
-
constructor(construct, migration, cause) {
|
|
3814
|
-
super(`${construct}: migration ${migration ? `'${migration}' ` : ""}failed — ${cause instanceof Error ? cause.message : String(cause)}`);
|
|
3815
|
-
this.construct = construct;
|
|
3816
|
-
this.migration = migration;
|
|
3817
|
-
this.cause = cause;
|
|
3818
|
-
this.name = "MigrationFailed";
|
|
3819
|
-
}
|
|
3820
|
-
};
|
|
3821
|
-
/** The project's Kysely could not be found from where a database is declared. */
|
|
3822
|
-
var KyselyNotFound = class extends Error {
|
|
3823
|
-
from;
|
|
3824
|
-
constructor(from) {
|
|
3825
|
-
super(`Could not resolve 'kysely' from ${from}. Migrations run with the project's own Kysely — install it where the database is declared.`);
|
|
3826
|
-
this.from = from;
|
|
3827
|
-
this.name = "KyselyNotFound";
|
|
3828
|
-
}
|
|
3829
|
-
};
|
|
3830
|
-
/** Apply every pending migration, construct by construct, parents first. */
|
|
3831
|
-
async function migrateDatabases(options) {
|
|
3832
|
-
const runs = [];
|
|
3833
|
-
for (const target of await targetsFor(options)) {
|
|
3834
|
-
if ((await migrationFiles(join(options.root, target.migrations))).size === 0) {
|
|
3835
|
-
runs.push({
|
|
3836
|
-
target,
|
|
3837
|
-
applied: []
|
|
3838
|
-
});
|
|
3839
|
-
continue;
|
|
3840
|
-
}
|
|
3841
|
-
const applied = await withMigrator(options, target, async (migrator) => {
|
|
3842
|
-
const { error, results = [] } = await migrator.migrateToLatest();
|
|
3843
|
-
if (error) {
|
|
3844
|
-
const failed = results.find((result) => result.status === "Error");
|
|
3845
|
-
throw new MigrationFailed(target.id, failed?.migrationName, error);
|
|
3846
|
-
}
|
|
3847
|
-
return results.filter((result) => result.status === "Success").map((result) => result.migrationName);
|
|
3848
|
-
});
|
|
3849
|
-
runs.push({
|
|
3850
|
-
target,
|
|
3851
|
-
applied
|
|
3852
|
-
});
|
|
3853
|
-
}
|
|
3854
|
-
return runs;
|
|
3855
|
-
}
|
|
3856
|
-
/**
|
|
3857
|
-
* Run every construct's seeds, parents first — each one, every time, on every
|
|
3858
|
-
* stage, production included. Each is handed the stage, so a seed decides
|
|
3859
|
-
* what belongs where.
|
|
3860
|
-
*
|
|
3861
|
-
* Seeds are reference data kept up to date by being run again, so there is no
|
|
3862
|
-
* history: a seed is an upsert, and a changed one is applied by the next run.
|
|
3863
|
-
* Each runs in its own transaction, as the construct's owner, so a failure
|
|
3864
|
-
* leaves nothing half-written. Assumes the schema is migrated —
|
|
3865
|
-
* {@link migrateAndSeed} is what makes that true.
|
|
3866
|
-
*/
|
|
3867
|
-
async function seedDatabases(options) {
|
|
3868
|
-
const runs = [];
|
|
3869
|
-
for (const target of await targetsFor(options)) {
|
|
3870
|
-
const folder = join(options.root, target.seeds);
|
|
3871
|
-
if ((await migrationFiles(folder)).size === 0) {
|
|
3872
|
-
runs.push({
|
|
3873
|
-
target,
|
|
3874
|
-
seeded: []
|
|
3875
|
-
});
|
|
3876
|
-
continue;
|
|
3877
|
-
}
|
|
3878
|
-
const seeded = await withOwner(options, target, async (db, kysely) => {
|
|
3879
|
-
const names = [];
|
|
3880
|
-
for (const seed of await loadSeeds(folder, kysely.sql)) {
|
|
3881
|
-
try {
|
|
3882
|
-
await db.transaction().execute((trx) => seed.run(trx, { stage: options.stage }));
|
|
3883
|
-
} catch (error) {
|
|
3884
|
-
throw new SeedFailed(target.id, seed.name, error);
|
|
3885
|
-
}
|
|
3886
|
-
names.push(seed.name);
|
|
3887
|
-
}
|
|
3888
|
-
return names;
|
|
3889
|
-
});
|
|
3890
|
-
runs.push({
|
|
3891
|
-
target,
|
|
3892
|
-
seeded
|
|
3893
|
-
});
|
|
3894
|
-
}
|
|
3895
|
-
return runs;
|
|
3896
|
-
}
|
|
3897
|
-
/**
|
|
3898
|
-
* Migrate, then seed: a seed only ever runs against the schema it was written
|
|
3899
|
-
* for.
|
|
3900
|
-
*
|
|
3901
|
-
* With `only`, the constructs it lives in are migrated too — a tenant's seed
|
|
3902
|
-
* cannot run before the database that owns its schema exists — and only it is
|
|
3903
|
-
* seeded.
|
|
3904
|
-
*/
|
|
3905
|
-
async function migrateAndSeed(options) {
|
|
3906
|
-
const migrations = [];
|
|
3907
|
-
for (const target of await withParents(options)) migrations.push(...await migrateDatabases({
|
|
3908
|
-
...options,
|
|
3909
|
-
only: target.id
|
|
3910
|
-
}));
|
|
3911
|
-
return {
|
|
3912
|
-
migrations,
|
|
3913
|
-
seeds: await seedDatabases(options)
|
|
3914
|
-
};
|
|
3915
|
-
}
|
|
3916
|
-
/** The targets `only` names, after the ones it lives in; all without it. */
|
|
3917
|
-
async function withParents(options) {
|
|
3918
|
-
const chosen = await targetsFor(options);
|
|
3919
|
-
if (!options.only) return chosen;
|
|
3920
|
-
const all = migrationTargets(options.manifest);
|
|
3921
|
-
const ids = /* @__PURE__ */ new Set();
|
|
3922
|
-
for (const target of chosen) {
|
|
3923
|
-
let current = target;
|
|
3924
|
-
while (current) {
|
|
3925
|
-
ids.add(current.id);
|
|
3926
|
-
const parent = current.of;
|
|
3927
|
-
current = parent ? all.find((t) => t.id === parent) : void 0;
|
|
3928
|
-
}
|
|
3929
|
-
}
|
|
3930
|
-
return all.filter((target) => ids.has(target.id));
|
|
3931
|
-
}
|
|
3932
|
-
/**
|
|
3933
|
-
* What each construct's folder has that its history does not — without
|
|
3934
|
-
* applying any of it. What `gkm dev` reports.
|
|
3935
|
-
*/
|
|
3936
|
-
async function pendingMigrations(options) {
|
|
3937
|
-
const found = [];
|
|
3938
|
-
for (const target of await targetsFor(options)) {
|
|
3939
|
-
if ((await migrationFiles(join(options.root, target.migrations))).size === 0) continue;
|
|
3940
|
-
const pending = await withMigrator(options, target, async (migrator) => (await migrator.getMigrations()).filter((migration) => migration.executedAt === void 0).map((migration) => migration.name));
|
|
3941
|
-
if (pending.length > 0) found.push({
|
|
3942
|
-
target,
|
|
3943
|
-
pending
|
|
3944
|
-
});
|
|
3945
|
-
}
|
|
3946
|
-
return found;
|
|
3947
|
-
}
|
|
3948
|
-
/**
|
|
3949
|
-
* The targets this run covers, with every folder under `db/` accounted for.
|
|
3950
|
-
*
|
|
3951
|
-
* A folder that names no construct is an error rather than something to skip:
|
|
3952
|
-
* a typo in its name is otherwise a migration that silently never runs.
|
|
3953
|
-
*/
|
|
3954
|
-
async function targetsFor(options) {
|
|
3955
|
-
const targets = migrationTargets(options.manifest);
|
|
3956
|
-
const folders = targets.map((target) => target.folder);
|
|
3957
|
-
for (const folder of await foldersIn(join(options.root, MIGRATIONS_ROOT))) {
|
|
3958
|
-
const relative = `${MIGRATIONS_ROOT}/${folder}`;
|
|
3959
|
-
if (!folders.includes(relative)) throw new UnknownMigrationFolder(relative, folders);
|
|
3960
|
-
await checkLayout(options.root, relative);
|
|
3961
|
-
}
|
|
3962
|
-
if (!options.only) return targets;
|
|
3963
|
-
const id = canonicalId(options.only);
|
|
3964
|
-
const chosen = targets.filter((target) => target.id === id);
|
|
3965
|
-
if (chosen.length === 0) throw new NoSuchMigrationTarget(options.only, targets.map((target) => target.id));
|
|
3966
|
-
return chosen;
|
|
3967
|
-
}
|
|
3968
|
-
/**
|
|
3969
|
-
* A construct's folder holds `migrations/` and `seeds/`, and nothing else —
|
|
3970
|
-
* anything beside them is a file or folder nothing would ever run.
|
|
3971
|
-
*/
|
|
3972
|
-
async function checkLayout(root, folder) {
|
|
3973
|
-
const entries = await readdir(join(root, folder), { withFileTypes: true });
|
|
3974
|
-
const loose = entries.filter((entry) => entry.isFile() && !entry.name.startsWith(".")).map((entry) => entry.name);
|
|
3975
|
-
if (loose.length > 0) throw new MigrationsOutsideFolder(folder, loose);
|
|
3976
|
-
for (const entry of entries) if (entry.isDirectory() && !DATABASE_FOLDERS.includes(entry.name)) throw new UnknownDatabaseFolder(`${folder}/${entry.name}`);
|
|
3977
|
-
}
|
|
3978
|
-
async function foldersIn(dir) {
|
|
3979
|
-
return (await readdir(dir, { withFileTypes: true }).catch((error) => {
|
|
3980
|
-
if (error.code === "ENOENT") return [];
|
|
3981
|
-
throw error;
|
|
3982
|
-
})).filter((entry) => entry.isDirectory()).map((entry) => entry.name);
|
|
3983
|
-
}
|
|
3984
|
-
/**
|
|
3985
|
-
* A migrator for one target, connected as its owner.
|
|
3986
|
-
*
|
|
3987
|
-
* No `migrationTableSchema`: the owner role's `search_path` is pinned to the
|
|
3988
|
-
* construct's schema, so the history lands there — `app.kysely_migration` for
|
|
3989
|
-
* the database, `auth_database.kysely_migration` for its tenant — and two
|
|
3990
|
-
* constructs in one Postgres never share one.
|
|
3991
|
-
*
|
|
3992
|
-
* Unordered migrations are allowed. Files are timestamped, so a branch merged
|
|
3993
|
-
* after another's migration ran carries an older one; refusing it would make
|
|
3994
|
-
* every such merge a manual repair.
|
|
3995
|
-
*/
|
|
3996
|
-
async function withMigrator(options, target, run) {
|
|
3997
|
-
return withOwner(options, target, (db, { Migrator, sql }) => run(new Migrator({
|
|
3998
|
-
db,
|
|
3999
|
-
provider: folderProvider(join(options.root, target.migrations), sql),
|
|
4000
|
-
allowUnorderedMigrations: true
|
|
4001
|
-
})));
|
|
4002
|
-
}
|
|
4003
|
-
/**
|
|
4004
|
-
* One target's database, connected as its owner and closed afterwards — what
|
|
4005
|
-
* migrations and seeds both run as.
|
|
4006
|
-
*/
|
|
4007
|
-
async function withOwner(options, target, run) {
|
|
4008
|
-
const kysely = await kyselyFrom(options.sources?.[target.id]?.file ?? (target.of ? options.sources?.[target.of]?.file : void 0) ?? join(options.root, "package.json"));
|
|
4009
|
-
const db = new kysely.Kysely({ dialect: new kysely.PostgresDialect({ pool: new pg.Pool(poolConfig(ownerUrl(options, target))) }) });
|
|
4010
|
-
try {
|
|
4011
|
-
return await run(db, kysely);
|
|
4012
|
-
} finally {
|
|
4013
|
-
await db.destroy();
|
|
4014
|
-
}
|
|
4015
|
-
}
|
|
4016
|
-
/**
|
|
4017
|
-
* The owner's URL, with no fallback to the runtime one.
|
|
4018
|
-
*
|
|
4019
|
-
* The runtime role can create nothing, so falling back would only move the
|
|
4020
|
-
* failure to the first `CREATE TABLE` and make it read like a permissions
|
|
4021
|
-
* bug. The one exception is a database declared `roles: false`, where there is
|
|
4022
|
-
* no owner — both URLs are the master credential by design.
|
|
4023
|
-
*/
|
|
4024
|
-
function ownerUrl(options, target) {
|
|
4025
|
-
const key = provideKey(target.id, "ownerUrl");
|
|
4026
|
-
const owner = options.env[key];
|
|
4027
|
-
if (owner) return owner;
|
|
4028
|
-
const database = options.manifest[target.of ?? target.id];
|
|
4029
|
-
if (database?.kind === "database" && database.roles === false) {
|
|
4030
|
-
const url = options.env[provideKey(target.id, "url")];
|
|
4031
|
-
if (url) return url;
|
|
4032
|
-
}
|
|
4033
|
-
throw new NoOwnerCredential(target.id, key);
|
|
4034
|
-
}
|
|
4035
|
-
/**
|
|
4036
|
-
* Pool settings for a URL, with `?search_path=` as the startup option Postgres
|
|
4037
|
-
* reads. Only a `roles: false` URL carries it — a role pins its own.
|
|
4038
|
-
*/
|
|
4039
|
-
function poolConfig(url) {
|
|
4040
|
-
const parsed = new URL(url);
|
|
4041
|
-
const searchPath = parsed.searchParams.get("search_path");
|
|
4042
|
-
if (!searchPath) return { connectionString: url };
|
|
4043
|
-
parsed.searchParams.delete("search_path");
|
|
4044
|
-
return {
|
|
4045
|
-
connectionString: parsed.toString(),
|
|
4046
|
-
options: `-c search_path=${searchPath}`
|
|
4047
|
-
};
|
|
4048
|
-
}
|
|
4049
|
-
/** Kysely as the project resolves it from `from`. */
|
|
4050
|
-
async function kyselyFrom(from) {
|
|
4051
|
-
const require = createRequire(from);
|
|
4052
|
-
try {
|
|
4053
|
-
const [main, migration] = await Promise.all([import(pathToFileURL(require.resolve("kysely")).href), import(pathToFileURL(require.resolve("kysely/migration")).href)]);
|
|
4054
|
-
return {
|
|
4055
|
-
Kysely: main.Kysely,
|
|
4056
|
-
PostgresDialect: main.PostgresDialect,
|
|
4057
|
-
sql: main.sql,
|
|
4058
|
-
Migrator: migration.Migrator
|
|
4059
|
-
};
|
|
4060
|
-
} catch {
|
|
4061
|
-
throw new KyselyNotFound(from);
|
|
4062
|
-
}
|
|
4063
|
-
}
|
|
4064
|
-
//#endregion
|
|
4065
|
-
//#region src/compose/docker.ts
|
|
4066
|
-
/**
|
|
4067
|
-
* The Docker operations `gkm compose` needs, behind an interface.
|
|
4068
|
-
*
|
|
4069
|
-
* Small on purpose, like reconcile's: the rules about *when* each runs — the
|
|
4070
|
-
* registry is asked before anything is pulled, nothing is started for a tag
|
|
4071
|
-
* that is missing — are asserted against a fake, and this file is only the
|
|
4072
|
-
* wiring to the real CLI.
|
|
4073
|
-
*
|
|
4074
|
-
* Every call is an argument array. Image refs, tags and paths come from the
|
|
4075
|
-
* workspace, and none of them reaches a shell.
|
|
4076
|
-
*/
|
|
4077
|
-
/** How long a stack has to pass its health checks. */
|
|
4078
|
-
const WAIT_TIMEOUT_S = 180;
|
|
4079
|
-
/** `docker compose port` printed nothing a port can be read from. */
|
|
4080
|
-
var ServicePortUnknown = class extends Error {
|
|
4081
|
-
service;
|
|
4082
|
-
inside;
|
|
4083
|
-
output;
|
|
4084
|
-
constructor(service, inside, output) {
|
|
4085
|
-
super(`'${service}' publishes no host port for ${inside} (docker said: ${output.trim() || "nothing"}). Is it running? \`docker compose ps\` shows the stack.`);
|
|
4086
|
-
this.service = service;
|
|
4087
|
-
this.inside = inside;
|
|
4088
|
-
this.output = output;
|
|
4089
|
-
this.name = "ServicePortUnknown";
|
|
4090
|
-
}
|
|
4091
|
-
};
|
|
4092
|
-
function compose(stack, args) {
|
|
4093
|
-
return [
|
|
4094
|
-
"compose",
|
|
4095
|
-
"-p",
|
|
4096
|
-
stack.project,
|
|
4097
|
-
"-f",
|
|
4098
|
-
stack.file,
|
|
4099
|
-
...args
|
|
4100
|
-
];
|
|
4101
|
-
}
|
|
4102
|
-
/** Run a program and keep everything it said, whatever it exits with. */
|
|
4103
|
-
function capture(command, args, timeoutMs = 6e4) {
|
|
4104
|
-
return new Promise((resolve, reject) => {
|
|
4105
|
-
const child = spawn(command, [...args], {
|
|
4106
|
-
stdio: [
|
|
4107
|
-
"ignore",
|
|
4108
|
-
"pipe",
|
|
4109
|
-
"pipe"
|
|
4110
|
-
],
|
|
4111
|
-
shell: false
|
|
4112
|
-
});
|
|
4113
|
-
let stdout = "";
|
|
4114
|
-
let stderr = "";
|
|
4115
|
-
child.stdout.on("data", (chunk) => {
|
|
4116
|
-
stdout += chunk.toString();
|
|
4117
|
-
});
|
|
4118
|
-
child.stderr.on("data", (chunk) => {
|
|
4119
|
-
stderr += chunk.toString();
|
|
4120
|
-
});
|
|
4121
|
-
const timer = setTimeout(() => child.kill("SIGKILL"), timeoutMs);
|
|
4122
|
-
timer.unref?.();
|
|
4123
|
-
child.on("error", (error) => {
|
|
4124
|
-
clearTimeout(timer);
|
|
4125
|
-
reject(error);
|
|
4126
|
-
});
|
|
4127
|
-
child.on("close", (code) => {
|
|
4128
|
-
clearTimeout(timer);
|
|
4129
|
-
resolve({
|
|
4130
|
-
code,
|
|
4131
|
-
stdout,
|
|
4132
|
-
stderr
|
|
4133
|
-
});
|
|
4134
|
-
});
|
|
4135
|
-
});
|
|
4136
|
-
}
|
|
4137
|
-
/**
|
|
4138
|
-
* Whether a failed `docker manifest inspect` means the tag is not there, as
|
|
4139
|
-
* opposed to the registry not answering.
|
|
4140
|
-
*
|
|
4141
|
-
* Read off the message because that is all the CLI gives. Anything not
|
|
4142
|
-
* recognisably "no such tag" is treated as the registry being unreachable:
|
|
4143
|
-
* a registry that refused credentials must not be reported as a tag that was
|
|
4144
|
-
* never pushed, or the fix offered would be the wrong one.
|
|
4145
|
-
*/
|
|
4146
|
-
function isMissingManifest(output) {
|
|
4147
|
-
return /no such manifest|manifest unknown|manifest for .* not found|not found: manifest/i.test(output);
|
|
4148
|
-
}
|
|
4149
|
-
const dockerCompose = {
|
|
4150
|
-
async lookup(ref) {
|
|
4151
|
-
const { code, stdout, stderr } = await capture("docker", [
|
|
4152
|
-
"manifest",
|
|
4153
|
-
"inspect",
|
|
4154
|
-
ref
|
|
4155
|
-
]);
|
|
4156
|
-
if (code === 0) return {
|
|
4157
|
-
ref,
|
|
4158
|
-
status: "found"
|
|
4159
|
-
};
|
|
4160
|
-
const output = `${stderr}\n${stdout}`.trim();
|
|
4161
|
-
return isMissingManifest(output) ? {
|
|
4162
|
-
ref,
|
|
4163
|
-
status: "missing"
|
|
4164
|
-
} : {
|
|
4165
|
-
ref,
|
|
4166
|
-
status: "unreachable",
|
|
4167
|
-
detail: output
|
|
4168
|
-
};
|
|
4169
|
-
},
|
|
4170
|
-
async build(stack, services) {
|
|
4171
|
-
await run$1("docker", compose(stack, ["build", ...services]), {
|
|
4172
|
-
cwd: stack.cwd,
|
|
4173
|
-
env: {
|
|
4174
|
-
...process.env,
|
|
4175
|
-
DOCKER_BUILDKIT: "1"
|
|
4176
|
-
}
|
|
4177
|
-
});
|
|
4178
|
-
},
|
|
4179
|
-
async pull(stack, services) {
|
|
4180
|
-
await run$1("docker", compose(stack, ["pull", ...services]), { cwd: stack.cwd });
|
|
4181
|
-
},
|
|
4182
|
-
async up(stack, services) {
|
|
4183
|
-
await run$1("docker", compose(stack, [
|
|
4184
|
-
"up",
|
|
4185
|
-
"-d",
|
|
4186
|
-
"--wait",
|
|
4187
|
-
`--wait-timeout=${WAIT_TIMEOUT_S}`,
|
|
4188
|
-
...services ? [] : ["--remove-orphans"],
|
|
4189
|
-
...services ?? []
|
|
4190
|
-
]), { cwd: stack.cwd });
|
|
4191
|
-
},
|
|
4192
|
-
async down(stack) {
|
|
4193
|
-
await run$1("docker", compose(stack, ["down"]), { cwd: stack.cwd });
|
|
4194
|
-
},
|
|
4195
|
-
async port(stack, service, inside) {
|
|
4196
|
-
const output = await runOutput("docker", compose(stack, [
|
|
4197
|
-
"port",
|
|
4198
|
-
service,
|
|
4199
|
-
String(inside)
|
|
4200
|
-
]), { cwd: stack.cwd });
|
|
4201
|
-
const port = Number(output.trim().split(":").pop());
|
|
4202
|
-
if (!Number.isInteger(port) || port <= 0) throw new ServicePortUnknown(service, inside, output);
|
|
4203
|
-
return port;
|
|
4204
|
-
},
|
|
4205
|
-
async copyOut(stack, service, from, to) {
|
|
4206
|
-
await mkdir(dirname(to), { recursive: true });
|
|
4207
|
-
await run$1("docker", compose(stack, [
|
|
4208
|
-
"cp",
|
|
4209
|
-
`${service}:${from}`,
|
|
4210
|
-
to
|
|
4211
|
-
]), { cwd: stack.cwd });
|
|
4212
|
-
},
|
|
4213
|
-
async digest(ref) {
|
|
4214
|
-
const { code, stdout } = await capture("docker", [
|
|
4215
|
-
"image",
|
|
4216
|
-
"inspect",
|
|
4217
|
-
"--format={{json .RepoDigests}} {{.Id}}",
|
|
4218
|
-
ref
|
|
4219
|
-
]);
|
|
4220
|
-
if (code !== 0) return void 0;
|
|
4221
|
-
const [json = "[]", id] = stdout.trim().split(" ");
|
|
4222
|
-
const repository = ref.split("@")[0].replace(/:[\w][\w.-]*$/, "");
|
|
4223
|
-
try {
|
|
4224
|
-
const digests = JSON.parse(json);
|
|
4225
|
-
const match = Array.isArray(digests) ? digests.find((entry) => typeof entry === "string" && entry.startsWith(`${repository}@`)) : void 0;
|
|
4226
|
-
if (match) return match.slice(repository.length + 1);
|
|
4227
|
-
} catch {}
|
|
4228
|
-
return id || void 0;
|
|
4229
|
-
}
|
|
4230
|
-
};
|
|
4231
|
-
//#endregion
|
|
4232
|
-
//#region src/compose/images.ts
|
|
4233
|
-
/**
|
|
4234
|
-
* The tag a site's image carries for a release.
|
|
4235
|
-
*
|
|
4236
|
-
* A site's public URLs are inlined when it is built, so one bundle serves one
|
|
4237
|
-
* stage: `v1.4.0-production` and `v1.4.0-staging` are two images of the same
|
|
4238
|
-
* commit. A backend reads its URLs at runtime, so one image serves every stage
|
|
4239
|
-
* and its tag is the release's alone.
|
|
4240
|
-
*/
|
|
4241
|
-
function siteTag(tag, stage) {
|
|
4242
|
-
return `${tag}-${stage}`;
|
|
4243
|
-
}
|
|
4244
|
-
/** Images a tag names that the registry does not have. */
|
|
4245
|
-
var ImageTagNotFound = class extends Error {
|
|
4246
|
-
tag;
|
|
4247
|
-
refs;
|
|
4248
|
-
constructor(tag, refs) {
|
|
4249
|
-
super(`The registry has no image for ${refs.length === 1 ? "this app" : `${refs.length} apps`} at '${tag}':\n` + refs.map((ref) => ` - ${ref}`).join("\n") + `\nNothing was pulled or started. Push '${tag}' for every app from CI, pick a tag that exists, or run without --tag to build from this checkout.`);
|
|
4250
|
-
this.tag = tag;
|
|
4251
|
-
this.refs = refs;
|
|
4252
|
-
this.name = "ImageTagNotFound";
|
|
4253
|
-
}
|
|
4254
|
-
};
|
|
4255
|
-
/** The registry did not answer, or did not let this machine in. */
|
|
4256
|
-
var RegistryUnreachable = class extends Error {
|
|
4257
|
-
ref;
|
|
4258
|
-
detail;
|
|
4259
|
-
constructor(ref, detail) {
|
|
4260
|
-
super(`Could not ask the registry about ${ref}: ${detail || "no answer"}\nNothing was pulled or started. Check the registry is reachable and that this machine is logged in to it (\`docker login <registry>\`).`);
|
|
4261
|
-
this.ref = ref;
|
|
4262
|
-
this.detail = detail;
|
|
4263
|
-
this.name = "RegistryUnreachable";
|
|
4264
|
-
}
|
|
4265
|
-
};
|
|
4266
|
-
/**
|
|
4267
|
-
* Assert every image exists, before anything else happens.
|
|
4268
|
-
*
|
|
4269
|
-
* Asked all at once and reported all at once: a release missing two images is
|
|
4270
|
-
* one problem with two names, and finding the second only after fixing the
|
|
4271
|
-
* first is a slower way to learn the same thing.
|
|
4272
|
-
*
|
|
4273
|
-
* @throws {RegistryUnreachable} when the registry cannot say
|
|
4274
|
-
* @throws {ImageTagNotFound} listing every missing image
|
|
4275
|
-
*/
|
|
4276
|
-
async function assertImagesExist(docker, tag, images) {
|
|
4277
|
-
const lookups = await Promise.all(images.map((image) => docker.lookup(image.ref)));
|
|
4278
|
-
const unreachable = lookups.find((lookup) => lookup.status === "unreachable");
|
|
4279
|
-
if (unreachable?.status === "unreachable") throw new RegistryUnreachable(unreachable.ref, unreachable.detail);
|
|
4280
|
-
const missing = lookups.filter((lookup) => lookup.status === "missing").map((lookup) => lookup.ref);
|
|
4281
|
-
if (missing.length > 0) throw new ImageTagNotFound(tag, missing);
|
|
4282
|
-
}
|
|
4283
|
-
//#endregion
|
|
4284
|
-
//#region src/compose/caddyfile.ts
|
|
4285
|
-
/** The Caddyfile for a stack's edge. */
|
|
4286
|
-
function edgeCaddyfile(sites, options) {
|
|
4287
|
-
const global = options.local ? `{
|
|
4288
|
-
# No ACME and no public DNS: the CA is Caddy's own and the names resolve
|
|
4289
|
-
# to this machine.
|
|
4290
|
-
local_certs
|
|
4291
|
-
auto_https disable_redirects
|
|
4292
|
-
}` : options.email ? `{
|
|
4293
|
-
email ${options.email}
|
|
4294
|
-
}` : "";
|
|
4295
|
-
const blocks = sites.map(({ host, upstream }) => `https://${host} {${options.local ? `
|
|
4296
|
-
tls internal
|
|
4297
|
-
` : ""}
|
|
4298
|
-
# Host and X-Forwarded-* are passed through as Caddy does by default, so
|
|
4299
|
-
# an app builds redirects, cookies and links on the address a caller used.
|
|
4300
|
-
reverse_proxy ${upstream} {
|
|
4301
|
-
# Never buffer: a streamed response — server-sent events, a Next.js
|
|
4302
|
-
# RSC payload — reaches the client as each chunk is written.
|
|
4303
|
-
flush_interval -1
|
|
4304
|
-
}
|
|
4305
|
-
}`);
|
|
4306
|
-
return `${HEADER}${global ? `${global}\n\n` : ""}${blocks.join("\n\n")}\n`;
|
|
4307
|
-
}
|
|
4308
|
-
const HEADER = `# Generated by gkm compose from the construct manifest — do not edit.
|
|
4309
|
-
#
|
|
4310
|
-
# One host per API and site, each in front of its own container.
|
|
4311
|
-
`;
|
|
4312
|
-
//#endregion
|
|
4313
|
-
//#region src/compose/stack.ts
|
|
4314
|
-
/**
|
|
4315
|
-
* The stack a workspace runs as for one stage — pure.
|
|
4316
|
-
*
|
|
4317
|
-
* Everything `gkm compose` writes is decided here, as data: the compose file,
|
|
4318
|
-
* the Caddyfile, one env file per backend, and the image each app runs. The
|
|
4319
|
-
* command around it reads the workspace, asks the registry, writes the files
|
|
4320
|
-
* and starts Docker; none of those decide anything, so all of it can be
|
|
4321
|
-
* asserted without a daemon.
|
|
4322
|
-
*
|
|
4323
|
-
* The values come from the same derivations every other target uses. The
|
|
4324
|
-
* plan is reconcile's `planFor`, the keys an app reads are `appEnvKeys`, and
|
|
4325
|
-
* every URL is `envFor`'s — asked twice, once with each app at its public
|
|
4326
|
-
* address and once on the compose network, because a browser and a sibling
|
|
4327
|
-
* service reach the same API by different names.
|
|
4328
|
-
*/
|
|
4329
|
-
/** Where a stack's files are written, relative to the workspace root. */
|
|
4330
|
-
function stackDir(stage) {
|
|
4331
|
-
return `.gkm/compose/${stage}`;
|
|
4332
|
-
}
|
|
4333
|
-
/**
|
|
4334
|
-
* The compose project a stage's stack runs as.
|
|
4335
|
-
*
|
|
4336
|
-
* The deploy identity's scope, so two workspaces with one name in different
|
|
4337
|
-
* namespaces never share containers or volumes — and the stage, so a
|
|
4338
|
-
* machine can run two stages side by side.
|
|
4339
|
-
*/
|
|
4340
|
-
function composeProject(identity) {
|
|
4341
|
-
return `${identity.scope}-${identity.stage}`;
|
|
4342
|
-
}
|
|
4343
|
-
/** The ports the edge is published on, and the variables that move them. */
|
|
4344
|
-
const EDGE_PORT_ENV = {
|
|
4345
|
-
https: "GKM_COMPOSE_HTTPS_PORT",
|
|
4346
|
-
http: "GKM_COMPOSE_HTTP_PORT"
|
|
4347
|
-
};
|
|
4348
|
-
/**
|
|
4349
|
-
* Containers this target does not run.
|
|
4350
|
-
*
|
|
4351
|
-
* Object storage is the stage's own — an externally hosted bucket whose URL
|
|
4352
|
-
* the stage's secrets hold — and so is a deployed stage's mail. The AWS
|
|
4353
|
-
* emulator never applies: a stack is a server target, whose events are
|
|
4354
|
-
* pg-boss.
|
|
4355
|
-
*/
|
|
4356
|
-
const NOT_RUN = {
|
|
4357
|
-
minio: true,
|
|
4358
|
-
localstack: true,
|
|
4359
|
-
caddy: true
|
|
4360
|
-
};
|
|
4361
|
-
/** Kinds whose `<ID>_URL` a stage must be given, because nothing here runs them. */
|
|
4362
|
-
const EXTERNAL_KINDS = {
|
|
4363
|
-
objects: true,
|
|
4364
|
-
"file-server": true
|
|
4365
|
-
};
|
|
4366
|
-
/** The S3 client's credentials, read beside a bucket's URL when it has them. */
|
|
4367
|
-
const STORAGE_KEYS = [
|
|
4368
|
-
"AWS_ACCESS_KEY_ID",
|
|
4369
|
-
"AWS_SECRET_ACCESS_KEY",
|
|
4370
|
-
"AWS_REGION"
|
|
4371
|
-
];
|
|
4372
|
-
/** A bucket the stack needs and the stage was not given. */
|
|
4373
|
-
var BucketNotConfigured = class extends Error {
|
|
4374
|
-
app;
|
|
4375
|
-
id;
|
|
4376
|
-
key;
|
|
4377
|
-
stage;
|
|
4378
|
-
constructor(app, id, key, stage) {
|
|
4379
|
-
super(`'${app}' uses the bucket '${id}', and gkm compose runs no object storage: the stage needs an externally hosted bucket. Set its URL in the stage's secrets: gkm secrets:set ${key} 's3://…' --stage ${stage}`);
|
|
4380
|
-
this.app = app;
|
|
4381
|
-
this.id = id;
|
|
4382
|
-
this.key = key;
|
|
4383
|
-
this.stage = stage;
|
|
4384
|
-
this.name = "BucketNotConfigured";
|
|
4385
|
-
}
|
|
4386
|
-
};
|
|
4387
|
-
/** A value only the stage's secrets can hold, and they do not. */
|
|
4388
|
-
var StageSecretMissing = class extends Error {
|
|
4389
|
-
app;
|
|
4390
|
-
key;
|
|
4391
|
-
stage;
|
|
4392
|
-
constructor(app, key, stage) {
|
|
4393
|
-
super(`'${app}' reads ${key}, and the stage '${stage}' has none. Nothing in the stack can derive it; set it in the stage's secrets: gkm secrets:set ${key} '…' --stage ${stage}`);
|
|
4394
|
-
this.app = app;
|
|
4395
|
-
this.key = key;
|
|
4396
|
-
this.stage = stage;
|
|
4397
|
-
this.name = "StageSecretMissing";
|
|
4398
|
-
}
|
|
4399
|
-
};
|
|
4400
|
-
/** A deployed stage's secrets with no seed to derive its passwords from. */
|
|
4401
|
-
var StageSeedMissing = class extends Error {
|
|
4402
|
-
stage;
|
|
4403
|
-
constructor(stage) {
|
|
4404
|
-
super(`The stage '${stage}' has no seed in its secrets, and a deployed stage's database passwords are derived from it. Run gkm compose without --dry-run once, or gkm deploy, to generate it.`);
|
|
4405
|
-
this.stage = stage;
|
|
4406
|
-
this.name = "StageSeedMissing";
|
|
4407
|
-
}
|
|
4408
|
-
};
|
|
4409
|
-
/** A workspace with no API and no site to run. */
|
|
4410
|
-
var NothingToCompose = class extends Error {
|
|
4411
|
-
root;
|
|
4412
|
-
constructor(root) {
|
|
4413
|
-
super(`${root} declares no RestApi and no site, so a stack would run nothing. Workers and mobile apps are not run by gkm compose yet.`);
|
|
4414
|
-
this.root = root;
|
|
4415
|
-
this.name = "NothingToCompose";
|
|
4416
|
-
}
|
|
4417
|
-
};
|
|
4418
|
-
/** A value that cannot be written to an env file a line at a time. */
|
|
4419
|
-
var EnvValueMultiline = class extends Error {
|
|
4420
|
-
app;
|
|
4421
|
-
key;
|
|
4422
|
-
constructor(app, key) {
|
|
4423
|
-
super(`${key} for '${app}' spans more than one line, and an env file holds one value per line. Store it on one line — base64, or JSON — and decode it where it is read.`);
|
|
4424
|
-
this.app = app;
|
|
4425
|
-
this.key = key;
|
|
4426
|
-
this.name = "EnvValueMultiline";
|
|
4427
|
-
}
|
|
4428
|
-
};
|
|
4429
|
-
/** The services a stack's apps wait on and the edge waits on in turn. */
|
|
4430
|
-
const HEALTHY = { condition: "service_healthy" };
|
|
4431
|
-
/**
|
|
4432
|
-
* An app's health check: its own server, asked on 127.0.0.1 — not
|
|
4433
|
-
* `localhost`, which resolves to `::1` first in an Alpine container while a
|
|
4434
|
-
* server may listen on IPv4 alone.
|
|
4435
|
-
*/
|
|
4436
|
-
function probe(port, path) {
|
|
4437
|
-
return {
|
|
4438
|
-
test: [
|
|
4439
|
-
"CMD",
|
|
4440
|
-
"wget",
|
|
4441
|
-
"-q",
|
|
4442
|
-
"-O",
|
|
4443
|
-
"/dev/null",
|
|
4444
|
-
`http://127.0.0.1:${port}${path}`
|
|
4445
|
-
],
|
|
4446
|
-
interval: "5s",
|
|
4447
|
-
timeout: "3s",
|
|
4448
|
-
retries: 20,
|
|
4449
|
-
start_period: "10s"
|
|
4450
|
-
};
|
|
4451
|
-
}
|
|
4452
|
-
/** The stack for one stage. */
|
|
4453
|
-
function composeStack(input) {
|
|
4454
|
-
const { workspace, manifest, stage, identity } = input;
|
|
4455
|
-
const local = stage === workspace.stages.local;
|
|
4456
|
-
const project = composeProject(identity);
|
|
4457
|
-
const custom = input.secrets?.custom ?? {};
|
|
4458
|
-
const https = input.ports?.https ?? 443;
|
|
4459
|
-
const http = input.ports?.http ?? 80;
|
|
4460
|
-
const plan = planFor(manifest, stage, provisionOrder(manifest), {
|
|
4461
|
-
localStage: workspace.stages.local,
|
|
4462
|
-
events: DEFAULT_EVENTS.server,
|
|
4463
|
-
cache: DEFAULT_CACHE.server,
|
|
4464
|
-
edge: false
|
|
4465
|
-
});
|
|
4466
|
-
const byId = new Map(plan.resources.map((r) => [r.id, r]));
|
|
4467
|
-
const seed = local ? void 0 : input.secrets?.seed;
|
|
4468
|
-
if (!local && !seed) throw new StageSeedMissing(stage);
|
|
4469
|
-
const credential = {
|
|
4470
|
-
master: local ? "geekmidas" : localRolePassword(workspace.name, plan, "master", seed),
|
|
4471
|
-
...seed ? { seed } : {}
|
|
4472
|
-
};
|
|
4473
|
-
const infra = plan.containers.filter((c) => !NOT_RUN[c]).filter((c) => local || c !== "mailpit").sort();
|
|
4474
|
-
const apps = stackApps(workspace, manifest, plan, {
|
|
4475
|
-
local,
|
|
4476
|
-
stage,
|
|
4477
|
-
https,
|
|
4478
|
-
identity,
|
|
4479
|
-
images: input.images
|
|
4480
|
-
});
|
|
4481
|
-
if (apps.length === 0) throw new NothingToCompose(workspace.root);
|
|
4482
|
-
const derivation = {
|
|
4483
|
-
project: workspace.name,
|
|
4484
|
-
master: credential.master,
|
|
4485
|
-
...credential.seed ? { seed: credential.seed } : {}
|
|
4486
|
-
};
|
|
4487
|
-
const outside = networkEnv(plan, {
|
|
4488
|
-
...derivation,
|
|
4489
|
-
addresses: Object.fromEntries(apps.map((app) => [app.id, app.url]))
|
|
4490
|
-
});
|
|
4491
|
-
const inside = networkEnv(plan, {
|
|
4492
|
-
...derivation,
|
|
4493
|
-
addresses: Object.fromEntries(apps.map((app) => [app.id, `http://${app.name}:${app.port}`]))
|
|
4494
|
-
});
|
|
4495
|
-
const owners = /* @__PURE__ */ new Map();
|
|
4496
|
-
for (const resource of plan.resources) {
|
|
4497
|
-
owners.set(resource.envKey, resource);
|
|
4498
|
-
if (resource.kind === "email") {
|
|
4499
|
-
owners.set(provideKey(resource.id, "from"), resource);
|
|
4500
|
-
owners.set(provideKey(resource.id, "inboxUrl"), resource);
|
|
4501
|
-
}
|
|
4502
|
-
if (resource.kind === "external-api") owners.set(provideKey(resource.id, "credentials"), resource);
|
|
4503
|
-
}
|
|
4504
|
-
const surfaceUrls = new Map(plan.resources.filter((r) => r.kind === "rest-api" || r.kind === "site").map((r) => [r.envKey, r.id]));
|
|
4505
|
-
const appById = new Map(apps.map((app) => [app.id, app]));
|
|
4506
|
-
/**
|
|
4507
|
-
* What a key resolves to for one app, from the stage's secrets where only
|
|
4508
|
-
* they can hold it, and the derivations otherwise.
|
|
4509
|
-
*/
|
|
4510
|
-
const valueFor = (app, key, owner) => {
|
|
4511
|
-
if (owner && EXTERNAL_KINDS[owner.kind] && key === owner.envKey) {
|
|
4512
|
-
const value = custom[key];
|
|
4513
|
-
if (value === void 0) throw new BucketNotConfigured(app.name, owner.id, key, stage);
|
|
4514
|
-
return value;
|
|
4515
|
-
}
|
|
4516
|
-
if (owner && suppliedBy(owner, key, local)) {
|
|
4517
|
-
const value = custom[key];
|
|
4518
|
-
if (value === void 0) throw new StageSecretMissing(app.name, key, stage);
|
|
4519
|
-
return value;
|
|
4520
|
-
}
|
|
4521
|
-
if (custom[key] !== void 0) return custom[key];
|
|
4522
|
-
const surface = surfaceUrls.get(key);
|
|
4523
|
-
if (app.kind === "rest-api" && surface && surface !== app.id) return inside[key];
|
|
4524
|
-
return outside[key];
|
|
4525
|
-
};
|
|
4526
|
-
for (const app of apps) {
|
|
4527
|
-
if (app.kind === "site") {
|
|
4528
|
-
const site = manifest[app.id];
|
|
4529
|
-
if (site?.kind !== "site") continue;
|
|
4530
|
-
const args = {};
|
|
4531
|
-
for (const [key, source] of Object.entries(publicEnvFor(site, manifest))) {
|
|
4532
|
-
const value = valueFor(app, source, owners.get(source));
|
|
4533
|
-
if (value !== void 0) args[key] = value;
|
|
4534
|
-
}
|
|
4535
|
-
if (app.build && Object.keys(args).length > 0) app.build.args = args;
|
|
4536
|
-
continue;
|
|
4537
|
-
}
|
|
4538
|
-
const keys = appEnvKeys(manifest, app.name, input.runnables) ?? /* @__PURE__ */ new Set();
|
|
4539
|
-
const values = {};
|
|
4540
|
-
for (const key of [...keys].sort()) {
|
|
4541
|
-
const owner = owners.get(key) ?? storageOwner(key, plan);
|
|
4542
|
-
if (!local && owner?.kind === "email" && key.endsWith("_INBOX_URL")) continue;
|
|
4543
|
-
if (STORAGE_KEYS.includes(key)) {
|
|
4544
|
-
if (custom[key] !== void 0) values[key] = custom[key];
|
|
4545
|
-
continue;
|
|
4546
|
-
}
|
|
4547
|
-
const value = valueFor(app, key, owner);
|
|
4548
|
-
if (value !== void 0) values[key] = value;
|
|
4549
|
-
}
|
|
4550
|
-
const trusted = provideKey(app.id, "trustedOrigins");
|
|
4551
|
-
if (keys.has(trusted)) {
|
|
4552
|
-
const internal = (byId.get(app.id)?.callers ?? []).map((caller) => appById.get(caller)).filter((caller) => caller?.kind === "rest-api").map((caller) => `http://${caller.name}:${caller.port}`);
|
|
4553
|
-
const origins = [...(values[trusted] ?? "").split(",").filter(Boolean), ...internal];
|
|
4554
|
-
values[trusted] = [...new Set(origins)].join(",");
|
|
4555
|
-
}
|
|
4556
|
-
app.env = {
|
|
4557
|
-
...values,
|
|
4558
|
-
NODE_ENV: "production",
|
|
4559
|
-
PORT: String(app.port),
|
|
4560
|
-
STAGE: stage
|
|
4561
|
-
};
|
|
4562
|
-
for (const [key, value] of Object.entries(app.env)) if (/[\r\n]/.test(value)) throw new EnvValueMultiline(app.name, key);
|
|
4563
|
-
}
|
|
4564
|
-
const compose = stackFile({
|
|
4565
|
-
project,
|
|
4566
|
-
plan,
|
|
4567
|
-
infra,
|
|
4568
|
-
apps,
|
|
4569
|
-
master: credential.master,
|
|
4570
|
-
https,
|
|
4571
|
-
http
|
|
4572
|
-
});
|
|
4573
|
-
const caddyfile = edgeCaddyfile(apps.map((app) => ({
|
|
4574
|
-
host: app.host,
|
|
4575
|
-
upstream: `${app.name}:${app.port}`
|
|
4576
|
-
})), { local });
|
|
4577
|
-
const dockerfiles = {};
|
|
4578
|
-
for (const app of apps) {
|
|
4579
|
-
if (!app.build) continue;
|
|
4580
|
-
dockerfiles[app.build.dockerfile] = app.kind === "site" ? siteDockerfile(app.name, workspace.apps[app.name], {
|
|
4581
|
-
turboPackage: input.packages?.[app.name] ?? app.name,
|
|
4582
|
-
packageManager: input.packageManager ?? "pnpm",
|
|
4583
|
-
manifest
|
|
4584
|
-
}) : generateSlimDockerfile({
|
|
4585
|
-
imageName: app.name,
|
|
4586
|
-
baseImage: "node:22-alpine",
|
|
4587
|
-
port: app.port,
|
|
4588
|
-
healthCheckPath: "/health",
|
|
4589
|
-
prebuilt: true,
|
|
4590
|
-
packageManager: input.packageManager ?? "pnpm",
|
|
4591
|
-
bundle: `${app.path}/.gkm/server/dist/server.mjs`
|
|
4592
|
-
});
|
|
4593
|
-
}
|
|
4594
|
-
return {
|
|
4595
|
-
project,
|
|
4596
|
-
stage,
|
|
4597
|
-
local,
|
|
4598
|
-
apps,
|
|
4599
|
-
infra,
|
|
4600
|
-
compose,
|
|
4601
|
-
caddyfile,
|
|
4602
|
-
dockerfiles,
|
|
4603
|
-
plan,
|
|
4604
|
-
credential
|
|
4605
|
-
};
|
|
4606
|
-
/** Whether only the stage's secrets can supply `key` to `owner`. */
|
|
4607
|
-
function suppliedBy(owner, key, isLocal) {
|
|
4608
|
-
if (owner.kind === "credential") return true;
|
|
4609
|
-
if (owner.kind === "external-api") return key === provideKey(owner.id, "credentials");
|
|
4610
|
-
if (isLocal) return false;
|
|
4611
|
-
if (owner.kind === "secret" || owner.kind === "encryption") return true;
|
|
4612
|
-
return owner.kind === "email" && !key.endsWith("_INBOX_URL");
|
|
4613
|
-
}
|
|
4614
|
-
}
|
|
4615
|
-
/** Which bucket an S3 credential key belongs to — the first one declared. */
|
|
4616
|
-
function storageOwner(key, plan) {
|
|
4617
|
-
if (!STORAGE_KEYS.includes(key)) return void 0;
|
|
4618
|
-
return plan.resources.find((r) => r.kind === "objects");
|
|
4619
|
-
}
|
|
4620
|
-
/** The apps a stack runs: every API and every site, at its stage address. */
|
|
4621
|
-
function stackApps(workspace, manifest, plan, options) {
|
|
4622
|
-
const apps = [];
|
|
4623
|
-
const resources = new Map(plan.resources.map((r) => [r.id, r]));
|
|
4624
|
-
for (const [id, declaration] of Object.entries(manifest)) {
|
|
4625
|
-
if (declaration.kind !== "rest-api" && declaration.kind !== "site") continue;
|
|
4626
|
-
const name = appKey(id);
|
|
4627
|
-
const app = workspace.apps[name];
|
|
4628
|
-
const resource = resources.get(id);
|
|
4629
|
-
if (!app || !resource || !app.port) continue;
|
|
4630
|
-
const host = hostOf(name, app, workspace, resource, options);
|
|
4631
|
-
const tag = declaration.kind === "site" ? siteTag(options.images.tag, options.stage) : options.images.tag;
|
|
4632
|
-
const ref = validateImageRef(imageRef(options.identity, name, options.images.registry, tag));
|
|
4633
|
-
apps.push({
|
|
4634
|
-
app: name,
|
|
4635
|
-
name,
|
|
4636
|
-
id,
|
|
4637
|
-
kind: declaration.kind,
|
|
4638
|
-
path: app.path,
|
|
4639
|
-
port: app.port,
|
|
4640
|
-
host,
|
|
4641
|
-
url: `https://${host}${options.local && options.https !== 443 ? `:${options.https}` : ""}`,
|
|
4642
|
-
ref,
|
|
4643
|
-
tag,
|
|
4644
|
-
...options.images.mode === "build" ? { build: { dockerfile: `${stackDir(options.stage)}/Dockerfile.${name}` } } : {}
|
|
4645
|
-
});
|
|
4646
|
-
}
|
|
4647
|
-
return apps.sort((a, b) => a.name.localeCompare(b.name));
|
|
4648
|
-
}
|
|
4649
|
-
/**
|
|
4650
|
-
* The host an app answers on for a stage: the `gkm dev` edge's
|
|
4651
|
-
* `*.localhost` names for the local stage, and the stage's domain — by the
|
|
4652
|
-
* same rule a deploy follows — for any other.
|
|
4653
|
-
*/
|
|
4654
|
-
function hostOf(name, app, workspace, resource, options) {
|
|
4655
|
-
if (options.local) return hostFor(resource, workspace.name);
|
|
4656
|
-
return resolveHost(name, app, options.stage, workspace.deploy?.domains, isMainFrontendApp(name, app, workspace.apps));
|
|
4657
|
-
}
|
|
4658
|
-
/** The compose document: the infrastructure, the apps, and the edge. */
|
|
4659
|
-
function stackFile(options) {
|
|
4660
|
-
const { project, plan, infra, apps } = options;
|
|
4661
|
-
const derived = composeFor({
|
|
4662
|
-
...plan,
|
|
4663
|
-
containers: [...infra]
|
|
4664
|
-
}, {
|
|
4665
|
-
project,
|
|
4666
|
-
ports: Object.fromEntries(portKeys(infra, plan.fakes).map((key) => [key, 0]))
|
|
4667
|
-
});
|
|
4668
|
-
const services = {};
|
|
4669
|
-
const volumes = {
|
|
4670
|
-
...derived.volumes,
|
|
4671
|
-
"caddy-data": {},
|
|
4672
|
-
"caddy-config": {}
|
|
4673
|
-
};
|
|
4674
|
-
for (const name of infra) {
|
|
4675
|
-
const { ports: _ports, depends_on, profiles: _profiles, ...service } = derived.services[name];
|
|
4676
|
-
services[name] = {
|
|
4677
|
-
...service,
|
|
4678
|
-
...depends_on?.length ? { depends_on: Object.fromEntries(depends_on.map((dep) => [dep, HEALTHY])) } : {}
|
|
4679
|
-
};
|
|
4680
|
-
}
|
|
4681
|
-
const postgres = services.postgres;
|
|
4682
|
-
if (postgres) {
|
|
4683
|
-
postgres.environment = {
|
|
4684
|
-
...postgres.environment,
|
|
4685
|
-
POSTGRES_PASSWORD: options.master
|
|
4686
|
-
};
|
|
4687
|
-
postgres.ports = ["127.0.0.1::5432"];
|
|
4688
|
-
}
|
|
4689
|
-
for (const app of apps) services[app.name] = {
|
|
4690
|
-
image: app.ref,
|
|
4691
|
-
...app.build ? { build: {
|
|
4692
|
-
context: "../../..",
|
|
4693
|
-
dockerfile: app.build.dockerfile,
|
|
4694
|
-
...app.build.args ? { args: app.build.args } : {}
|
|
4695
|
-
} } : {},
|
|
4696
|
-
restart: "unless-stopped",
|
|
4697
|
-
...app.kind === "rest-api" ? {
|
|
4698
|
-
env_file: [{
|
|
4699
|
-
path: `./${app.name}.env`,
|
|
4700
|
-
format: "raw"
|
|
4701
|
-
}],
|
|
4702
|
-
...infra.length ? { depends_on: Object.fromEntries(infra.map((name) => [name, HEALTHY])) } : {}
|
|
4703
|
-
} : {},
|
|
4704
|
-
healthcheck: probe(app.port, app.kind === "site" ? "/" : "/health")
|
|
4705
|
-
};
|
|
4706
|
-
services.caddy = {
|
|
4707
|
-
image: DEFAULT_IMAGES.caddy,
|
|
4708
|
-
restart: "unless-stopped",
|
|
4709
|
-
ports: [`${options.https}:443`, `${options.http}:80`],
|
|
4710
|
-
volumes: [
|
|
4711
|
-
"./Caddyfile:/etc/caddy/Caddyfile:ro",
|
|
4712
|
-
"caddy-data:/data",
|
|
4713
|
-
"caddy-config:/config"
|
|
4714
|
-
],
|
|
4715
|
-
depends_on: Object.fromEntries(apps.map((app) => [app.name, HEALTHY])),
|
|
4716
|
-
healthcheck: {
|
|
4717
|
-
test: [
|
|
4718
|
-
"CMD",
|
|
4719
|
-
"caddy",
|
|
4720
|
-
"version"
|
|
4721
|
-
],
|
|
4722
|
-
interval: "10s",
|
|
4723
|
-
timeout: "5s",
|
|
4724
|
-
retries: 5
|
|
4725
|
-
}
|
|
4726
|
-
};
|
|
4727
|
-
return {
|
|
4728
|
-
name: project,
|
|
4729
|
-
services,
|
|
4730
|
-
volumes
|
|
4731
|
-
};
|
|
4732
|
-
}
|
|
4733
|
-
/** Render an app's env file: one `KEY=value` per line, read raw by compose. */
|
|
4734
|
-
function envFile(env) {
|
|
4735
|
-
return `${Object.entries(env).sort(([a], [b]) => a.localeCompare(b)).map(([key, value]) => `${key}=${value}`).join("\n")}\n`;
|
|
4736
|
-
}
|
|
4737
|
-
//#endregion
|
|
4738
3784
|
//#region src/compose/index.ts
|
|
4739
3785
|
/**
|
|
4740
3786
|
* `gkm compose` — a workspace's APIs and sites for one stage, as one Docker
|
|
4741
3787
|
* Compose stack behind Caddy.
|
|
4742
3788
|
*
|
|
4743
|
-
*
|
|
4744
|
-
*
|
|
4745
|
-
*
|
|
4746
|
-
*
|
|
4747
|
-
*
|
|
4748
|
-
* - The databases exist before the apps do. Postgres comes up alone, its
|
|
4749
|
-
* databases, roles and grants are created and its migrations applied from
|
|
4750
|
-
* this machine, and only then do the apps start — so an app never boots
|
|
4751
|
-
* against a schema that is not there yet.
|
|
4752
|
-
* - What a stage runs is recorded. Each app's tag and the digest it resolved
|
|
4753
|
-
* to are written to the stage's state, so "what is this stage running" has
|
|
4754
|
-
* an exact answer.
|
|
4755
|
-
*
|
|
4756
|
-
* Docker, the registry, the build and the clock are injected, so the rules are
|
|
4757
|
-
* asserted without a daemon; the defaults are the real ones.
|
|
3789
|
+
* A convenience over `gkm deploy --target compose`: the stage defaults to the
|
|
3790
|
+
* local one, `--build`/`--pull` choose the images whatever `--tag` says, and
|
|
3791
|
+
* `--down` stops the stack. Everything else — the order, the image check, the
|
|
3792
|
+
* state, the health checks — is the compose target's, run through the same
|
|
3793
|
+
* `deploy()` as any other target, so there is one way a stack comes up.
|
|
4758
3794
|
*/
|
|
4759
|
-
const logger$4 = console;
|
|
4760
3795
|
/** `--build` and `--pull` together ask for two different things. */
|
|
4761
3796
|
var ComposeModeConflict = class extends Error {
|
|
4762
3797
|
constructor() {
|
|
@@ -4764,320 +3799,52 @@ var ComposeModeConflict = class extends Error {
|
|
|
4764
3799
|
this.name = "ComposeModeConflict";
|
|
4765
3800
|
}
|
|
4766
3801
|
};
|
|
4767
|
-
/** A build with no commit to tag its images with. */
|
|
4768
|
-
var NoGitRevision = class extends Error {
|
|
4769
|
-
root;
|
|
4770
|
-
constructor(root) {
|
|
4771
|
-
super(`Images built here are tagged with the commit they were built from, and ${root} is not in a git repository with a commit. Commit, or pass --tag to name the images yourself.`);
|
|
4772
|
-
this.root = root;
|
|
4773
|
-
this.name = "NoGitRevision";
|
|
4774
|
-
}
|
|
4775
|
-
};
|
|
4776
|
-
/** A port variable that does not hold a port. */
|
|
4777
|
-
var EdgePortInvalid = class extends Error {
|
|
4778
|
-
variable;
|
|
4779
|
-
value;
|
|
4780
|
-
constructor(variable, value) {
|
|
4781
|
-
super(`${variable}=${value} is not a port. Set it to a number between 1 and 65535, or unset it for the default.`);
|
|
4782
|
-
this.variable = variable;
|
|
4783
|
-
this.value = value;
|
|
4784
|
-
this.name = "EdgePortInvalid";
|
|
4785
|
-
}
|
|
4786
|
-
};
|
|
4787
|
-
/** The commit HEAD is at, short — and `-dirty` when the tree has changes. */
|
|
4788
|
-
async function gitRevision(root) {
|
|
4789
|
-
let sha;
|
|
4790
|
-
try {
|
|
4791
|
-
sha = (await runOutput("git", [
|
|
4792
|
-
"rev-parse",
|
|
4793
|
-
"--short",
|
|
4794
|
-
"HEAD"
|
|
4795
|
-
], { cwd: root })).trim();
|
|
4796
|
-
} catch {
|
|
4797
|
-
throw new NoGitRevision(root);
|
|
4798
|
-
}
|
|
4799
|
-
if (!sha) throw new NoGitRevision(root);
|
|
4800
|
-
return (await runOutput("git", ["status", "--porcelain"], { cwd: root }).catch(() => "")).trim() ? `${sha}-dirty` : sha;
|
|
4801
|
-
}
|
|
4802
|
-
/**
|
|
4803
|
-
* Bundle a backend with the gkm that is running — the same build a deploy
|
|
4804
|
-
* makes, so the image holds one file and no dependencies.
|
|
4805
|
-
*/
|
|
4806
|
-
async function bundleWithThisCli(appRoot) {
|
|
4807
|
-
const entry = process.argv[1];
|
|
4808
|
-
if (!entry) return;
|
|
4809
|
-
await run$1(process.execPath, [
|
|
4810
|
-
...process.execArgv,
|
|
4811
|
-
entry,
|
|
4812
|
-
"build",
|
|
4813
|
-
"--provider",
|
|
4814
|
-
"server",
|
|
4815
|
-
"--production"
|
|
4816
|
-
], { cwd: appRoot });
|
|
4817
|
-
}
|
|
4818
|
-
const defaultDeps = {
|
|
4819
|
-
docker: dockerCompose,
|
|
4820
|
-
bundle: bundleWithThisCli,
|
|
4821
|
-
revision: gitRevision
|
|
4822
|
-
};
|
|
4823
|
-
/** The edge's ports, from the environment — 443 and 80 when unset. */
|
|
4824
|
-
function edgePorts(env = process.env) {
|
|
4825
|
-
const read = (variable, fallback) => {
|
|
4826
|
-
const value = env[variable];
|
|
4827
|
-
if (value === void 0 || value === "") return fallback;
|
|
4828
|
-
const port = Number(value);
|
|
4829
|
-
if (!Number.isInteger(port) || port < 1 || port > 65535) throw new EdgePortInvalid(variable, value);
|
|
4830
|
-
return port;
|
|
4831
|
-
};
|
|
4832
|
-
return {
|
|
4833
|
-
https: read(EDGE_PORT_ENV.https, 443),
|
|
4834
|
-
http: read(EDGE_PORT_ENV.http, 80)
|
|
4835
|
-
};
|
|
4836
|
-
}
|
|
4837
3802
|
/** `gkm compose`. */
|
|
4838
3803
|
async function composeCommand(options = {}, deps = {}) {
|
|
4839
|
-
const { docker, bundle, revision } = {
|
|
4840
|
-
...defaultDeps,
|
|
4841
|
-
...deps
|
|
4842
|
-
};
|
|
4843
|
-
const env = deps.env ?? process.env;
|
|
4844
3804
|
if (options.build && options.pull) throw new ComposeModeConflict();
|
|
4845
|
-
const
|
|
4846
|
-
const
|
|
3805
|
+
const cwd = resolve(options.cwd ?? process.cwd());
|
|
3806
|
+
const { workspace } = await loadWorkspaceConfig(cwd);
|
|
4847
3807
|
const stage = options.stage ?? workspace.stages.local;
|
|
4848
|
-
if (!(stage === workspace.stages.local)) assertDeployedStage(workspace.stages, stage);
|
|
4849
|
-
const identity = deployIdentity(workspace, stage);
|
|
4850
|
-
const dir = join(root, stackDir(stage));
|
|
4851
|
-
const file = join(dir, "docker-compose.yml");
|
|
4852
|
-
const ref = {
|
|
4853
|
-
project: composeProject(identity),
|
|
4854
|
-
file,
|
|
4855
|
-
cwd: root
|
|
4856
|
-
};
|
|
4857
3808
|
if (options.down) {
|
|
4858
|
-
|
|
4859
|
-
|
|
3809
|
+
const ref = {
|
|
3810
|
+
project: composeProject(deployIdentity(workspace, stage)),
|
|
3811
|
+
file: join(workspace.root, stackDir(stage), "docker-compose.yml"),
|
|
3812
|
+
cwd: workspace.root
|
|
3813
|
+
};
|
|
3814
|
+
await (deps.docker ?? dockerCompose).down(ref);
|
|
3815
|
+
console.log(`🛑 Stopped ${ref.project}. Its volumes are kept.`);
|
|
4860
3816
|
return;
|
|
4861
3817
|
}
|
|
4862
|
-
|
|
4863
|
-
const
|
|
4864
|
-
const
|
|
4865
|
-
|
|
4866
|
-
cwd
|
|
4867
|
-
sources,
|
|
4868
|
-
runnables
|
|
4869
|
-
});
|
|
4870
|
-
const mode = options.build || !options.pull && !options.tag ? "build" : "pull";
|
|
4871
|
-
const tag = options.tag ?? (mode === "pull" ? "latest" : await revision(root));
|
|
4872
|
-
const { secrets, keep } = await stageSecrets(workspace, manifest, stage);
|
|
4873
|
-
const packageManager = detectPackageManager$1(root);
|
|
4874
|
-
const stack = composeStack({
|
|
4875
|
-
workspace,
|
|
4876
|
-
manifest,
|
|
4877
|
-
runnables,
|
|
3818
|
+
let finished;
|
|
3819
|
+
const mode = options.build ? "build" : options.pull ? "pull" : void 0;
|
|
3820
|
+
const tag = options.tag ?? (options.pull ? "latest" : void 0);
|
|
3821
|
+
const result = await deploy$1({
|
|
3822
|
+
cwd,
|
|
4878
3823
|
stage,
|
|
4879
|
-
|
|
4880
|
-
|
|
4881
|
-
|
|
4882
|
-
|
|
4883
|
-
|
|
4884
|
-
|
|
4885
|
-
secrets,
|
|
4886
|
-
ports: edgePorts(env),
|
|
4887
|
-
packageManager,
|
|
4888
|
-
packages: Object.fromEntries(Object.entries(workspace.apps).map(([name, app]) => [name, appPackageName$1(root, name, app.path)]))
|
|
4889
|
-
});
|
|
4890
|
-
if (mode === "pull" && !options.dryRun) await assertImagesExist(docker, tag, stack.apps);
|
|
4891
|
-
if (options.dryRun) keep.skip();
|
|
4892
|
-
else await keep.write();
|
|
4893
|
-
const files = await writeStack(root, dir, stack);
|
|
4894
|
-
printPlan(stack, mode, files);
|
|
4895
|
-
if (options.dryRun) return {
|
|
4896
|
-
stack,
|
|
4897
|
-
files
|
|
4898
|
-
};
|
|
4899
|
-
const store = deps.state ? await deps.state(workspace) : await createStateStore({
|
|
4900
|
-
config: workspace.state,
|
|
4901
|
-
workspaceRoot: root,
|
|
4902
|
-
workspaceName: workspace.name
|
|
4903
|
-
});
|
|
4904
|
-
const lock = await store.lock(stage, { operation: "compose" });
|
|
4905
|
-
try {
|
|
4906
|
-
const apps = stack.apps.map((app) => app.name);
|
|
4907
|
-
if (mode === "build") {
|
|
4908
|
-
for (const app of stack.apps.filter((a) => a.kind === "rest-api")) {
|
|
4909
|
-
logger$4.log(`\n📦 Bundling ${app.name}…`);
|
|
4910
|
-
await bundle(join(root, app.path));
|
|
3824
|
+
target: "compose",
|
|
3825
|
+
targets: { compose: composeTarget({
|
|
3826
|
+
...deps,
|
|
3827
|
+
...mode ? { mode } : {},
|
|
3828
|
+
report: (done) => {
|
|
3829
|
+
finished = done;
|
|
4911
3830
|
}
|
|
4912
|
-
|
|
4913
|
-
|
|
4914
|
-
}
|
|
4915
|
-
|
|
4916
|
-
|
|
4917
|
-
|
|
4918
|
-
|
|
4919
|
-
logger$4.log(`\n🗄️ Starting ${stack.infra.join(", ")}…`);
|
|
4920
|
-
await docker.up(ref, stack.infra);
|
|
3831
|
+
}) },
|
|
3832
|
+
...tag ? { tag } : {},
|
|
3833
|
+
...options.dryRun ? { dryRun: true } : {},
|
|
3834
|
+
logger: {
|
|
3835
|
+
info: (message) => console.log(message),
|
|
3836
|
+
warn: (message) => console.warn(message),
|
|
3837
|
+
error: (message) => console.error(message)
|
|
4921
3838
|
}
|
|
4922
|
-
|
|
4923
|
-
|
|
4924
|
-
ref,
|
|
4925
|
-
sql: deps.sql ?? pgClient,
|
|
4926
|
-
migrate: deps.migrate ?? migrateDatabases
|
|
4927
|
-
});
|
|
4928
|
-
logger$4.log("\n🚀 Starting the stack…");
|
|
4929
|
-
await docker.up(ref);
|
|
4930
|
-
if (stack.local) await docker.copyOut(ref, "caddy", "/data/caddy/pki/authorities/local/root.crt", join(dir, "caddy-root.crt")).catch(() => {});
|
|
4931
|
-
const images = await recordImages(store, stack, docker, mode);
|
|
4932
|
-
logger$4.log(`\n✅ ${ref.project} is running:`);
|
|
4933
|
-
for (const app of stack.apps) logger$4.log(` ${app.name.padEnd(12)} ${app.url}`);
|
|
4934
|
-
if (stack.local) logger$4.log(`\n🔐 Certificates are from Caddy's local CA: NODE_EXTRA_CA_CERTS=${join(stackDir(stage), "caddy-root.crt")}`);
|
|
4935
|
-
return {
|
|
4936
|
-
stack,
|
|
4937
|
-
files,
|
|
4938
|
-
images
|
|
4939
|
-
};
|
|
4940
|
-
} finally {
|
|
4941
|
-
await lock.release();
|
|
4942
|
-
}
|
|
4943
|
-
}
|
|
4944
|
-
/**
|
|
4945
|
-
* The stage's secrets — and, for a deployed stage, everything it generates
|
|
4946
|
-
* once: its seed, and each declared secret and keyring. Written back before
|
|
4947
|
-
* anything is derived from them, so the next run derives the same passwords.
|
|
4948
|
-
*/
|
|
4949
|
-
async function stageSecrets(workspace, manifest, stage) {
|
|
4950
|
-
const store = await secretsStoreFor(workspace, stage);
|
|
4951
|
-
const stored = await store.read(stage);
|
|
4952
|
-
const nothing = {
|
|
4953
|
-
write: async () => {},
|
|
4954
|
-
skip: () => {}
|
|
4955
|
-
};
|
|
4956
|
-
if (stage === workspace.stages.local) return {
|
|
4957
|
-
secrets: stored,
|
|
4958
|
-
keep: nothing
|
|
4959
|
-
};
|
|
4960
|
-
const { secrets, generated } = withGeneratedSecrets(stored ?? initStageSecrets(stage), manifest);
|
|
4961
|
-
if (generated.length === 0) return {
|
|
4962
|
-
secrets,
|
|
4963
|
-
keep: nothing
|
|
4964
|
-
};
|
|
3839
|
+
}).result;
|
|
3840
|
+
if (!finished) return void 0;
|
|
4965
3841
|
return {
|
|
4966
|
-
|
|
4967
|
-
|
|
4968
|
-
|
|
4969
|
-
|
|
4970
|
-
logger$4.log(`🔑 Generated for "${stage}" (${store.name}): ${generated.join(", ")}`);
|
|
4971
|
-
},
|
|
4972
|
-
skip() {
|
|
4973
|
-
logger$4.log(`🔑 "${stage}" has no ${generated.join(", ")} yet; this dry run used values it did not keep.`);
|
|
4974
|
-
}
|
|
4975
|
-
}
|
|
3842
|
+
stack: finished.stack,
|
|
3843
|
+
files: finished.files,
|
|
3844
|
+
...options.dryRun ? {} : { images: finished.images },
|
|
3845
|
+
deploy: result
|
|
4976
3846
|
};
|
|
4977
3847
|
}
|
|
4978
|
-
/** Write the stack's files: owner-only, since the env files hold secrets. */
|
|
4979
|
-
async function writeStack(root, dir, stack) {
|
|
4980
|
-
await mkdir(dir, {
|
|
4981
|
-
recursive: true,
|
|
4982
|
-
mode: 448
|
|
4983
|
-
});
|
|
4984
|
-
await chmod(dir, 448);
|
|
4985
|
-
const files = [];
|
|
4986
|
-
const write = async (path, content, mode = 420) => {
|
|
4987
|
-
await mkdir(dirname(path), { recursive: true });
|
|
4988
|
-
await writeFile(path, content, { mode });
|
|
4989
|
-
await chmod(path, mode);
|
|
4990
|
-
files.push(path);
|
|
4991
|
-
};
|
|
4992
|
-
await write(join(dir, "docker-compose.yml"), composeYaml(stack));
|
|
4993
|
-
await write(join(dir, "Caddyfile"), stack.caddyfile);
|
|
4994
|
-
for (const app of stack.apps) if (app.env) await write(join(dir, `${app.name}.env`), envFile(app.env), 384);
|
|
4995
|
-
for (const [path, content] of Object.entries(stack.dockerfiles)) await write(join(root, path), content);
|
|
4996
|
-
await ignoreStacks(root);
|
|
4997
|
-
return files;
|
|
4998
|
-
}
|
|
4999
|
-
/** Make sure the build context leaves `.gkm/compose` out. */
|
|
5000
|
-
async function ignoreStacks(root) {
|
|
5001
|
-
const path = join(root, ".dockerignore");
|
|
5002
|
-
if (!existsSync(path)) {
|
|
5003
|
-
await writeFile(path, generateDockerignore());
|
|
5004
|
-
return;
|
|
5005
|
-
}
|
|
5006
|
-
const current = await readFile(path, "utf-8");
|
|
5007
|
-
if (/^\/?\.gkm\/compose\/?$|^\/?\.gkm\/?$/m.test(current)) return;
|
|
5008
|
-
await appendFile(path, `${current.endsWith("\n") ? "" : "\n"}\n# gkm compose's stacks: each app's env file holds its stage's secrets\n.gkm/compose\n`);
|
|
5009
|
-
}
|
|
5010
|
-
function composeYaml(stack) {
|
|
5011
|
-
return `# Generated by gkm compose from the construct manifest — do not edit.
|
|
5012
|
-
# The ${stack.stage} stage's APIs and sites behind one Caddy. Each backend
|
|
5013
|
-
# reads exactly the keys in its own env file beside this one.
|
|
5014
|
-
#
|
|
5015
|
-
# gkm compose${stack.local ? "" : ` --stage ${stack.stage}`} start or update it
|
|
5016
|
-
# gkm compose${stack.local ? "" : ` --stage ${stack.stage}`} --down stop it
|
|
5017
|
-
${stringify(stack.compose, {
|
|
5018
|
-
lineWidth: 0,
|
|
5019
|
-
aliasDuplicateObjects: false
|
|
5020
|
-
})}`;
|
|
5021
|
-
}
|
|
5022
|
-
/** Create what the plan names in the stack's Postgres, then migrate it. */
|
|
5023
|
-
async function prepareDatabases(stack, workspace, manifest, sources, deps) {
|
|
5024
|
-
const port = await deps.docker.port(deps.ref, "postgres", 5432);
|
|
5025
|
-
const statements = postgresStatements(stack.plan, workspace.name, stack.credential.seed);
|
|
5026
|
-
if (statements.length > 0) {
|
|
5027
|
-
logger$4.log("🗄️ Creating databases, roles and grants…");
|
|
5028
|
-
await applyPostgres(deps.sql(port, stack.credential.master), statements);
|
|
5029
|
-
}
|
|
5030
|
-
const env = envFor(stack.plan, {
|
|
5031
|
-
ports: { [primaryPortKey("postgres")]: port },
|
|
5032
|
-
project: workspace.name,
|
|
5033
|
-
master: stack.credential.master,
|
|
5034
|
-
...stack.credential.seed ? { seed: stack.credential.seed } : {}
|
|
5035
|
-
});
|
|
5036
|
-
const runs = await deps.migrate({
|
|
5037
|
-
root: workspace.root,
|
|
5038
|
-
manifest,
|
|
5039
|
-
sources,
|
|
5040
|
-
env
|
|
5041
|
-
});
|
|
5042
|
-
for (const { target, applied } of runs) if (applied.length > 0) logger$4.log(`🗄️ ${target.migrations}: applied ${applied.length}`);
|
|
5043
|
-
}
|
|
5044
|
-
/**
|
|
5045
|
-
* Record each app's image in the stage's state: the tag it ran and the digest
|
|
5046
|
-
* the tag resolved to — or, for an image built here and never pushed, its
|
|
5047
|
-
* content id.
|
|
5048
|
-
*/
|
|
5049
|
-
async function recordImages(store, stack, docker, mode) {
|
|
5050
|
-
if (!await store.read(stack.stage)) await store.write(stack.stage, createEmptyState(stack.stage, "", ""), { expectedVersion: null });
|
|
5051
|
-
const images = {};
|
|
5052
|
-
for (const app of stack.apps) {
|
|
5053
|
-
const digest = await docker.digest(app.ref);
|
|
5054
|
-
images[app.name] = {
|
|
5055
|
-
ref: app.ref,
|
|
5056
|
-
tag: app.tag,
|
|
5057
|
-
...digest ? { digest } : {}
|
|
5058
|
-
};
|
|
5059
|
-
await store.putResource(stack.stage, {
|
|
5060
|
-
key: `compose:${app.name}`,
|
|
5061
|
-
type: "compose-image",
|
|
5062
|
-
status: "ready",
|
|
5063
|
-
...digest ? { id: digest } : {},
|
|
5064
|
-
data: {
|
|
5065
|
-
project: stack.project,
|
|
5066
|
-
ref: app.ref,
|
|
5067
|
-
tag: app.tag,
|
|
5068
|
-
source: mode,
|
|
5069
|
-
...digest ? { digest } : {}
|
|
5070
|
-
}
|
|
5071
|
-
});
|
|
5072
|
-
}
|
|
5073
|
-
return images;
|
|
5074
|
-
}
|
|
5075
|
-
function printPlan(stack, mode, files) {
|
|
5076
|
-
logger$4.log(`\n🧱 ${stack.project} — stage ${stack.stage}`);
|
|
5077
|
-
for (const app of stack.apps) logger$4.log(` ${app.name.padEnd(12)} ${app.url.padEnd(40)} ${mode === "build" ? "build" : "pull"} ${app.ref}`);
|
|
5078
|
-
if (stack.infra.length > 0) logger$4.log(` infrastructure: ${stack.infra.join(", ")}`);
|
|
5079
|
-
logger$4.log(`\n📝 Wrote ${files.length} file(s) under ${stackDir(stack.stage)}/`);
|
|
5080
|
-
}
|
|
5081
3848
|
/**
|
|
5082
3849
|
* Format a fatal error for display.
|
|
5083
3850
|
* Always includes the full stack trace since these are process-ending errors.
|
|
@@ -6483,25 +5250,25 @@ function loadPackageJson() {
|
|
|
6483
5250
|
* Run: pnpm --filter @geekmidas/cli sync-versions
|
|
6484
5251
|
*/
|
|
6485
5252
|
const GEEKMIDAS_VERSIONS = {
|
|
6486
|
-
"@geekmidas/audit": "~10.0.0-alpha.
|
|
6487
|
-
"@geekmidas/auth": "~10.0.0-alpha.
|
|
6488
|
-
"@geekmidas/cache": "~10.0.0-alpha.
|
|
6489
|
-
"@geekmidas/client": "~10.0.0-alpha.
|
|
6490
|
-
"@geekmidas/cloud": "~10.0.0-alpha.
|
|
6491
|
-
"@geekmidas/constructs": "~10.0.0-alpha.
|
|
6492
|
-
"@geekmidas/db": "~10.0.0-alpha.
|
|
6493
|
-
"@geekmidas/emailkit": "~10.0.0-alpha.
|
|
6494
|
-
"@geekmidas/envkit": "~10.0.0-alpha.
|
|
6495
|
-
"@geekmidas/errors": "~10.0.0-alpha.
|
|
6496
|
-
"@geekmidas/events": "~10.0.0-alpha.
|
|
6497
|
-
"@geekmidas/logger": "~10.0.0-alpha.
|
|
6498
|
-
"@geekmidas/manifest": "~10.0.0-alpha.
|
|
6499
|
-
"@geekmidas/rate-limit": "~10.0.0-alpha.
|
|
6500
|
-
"@geekmidas/schema": "~10.0.0-alpha.
|
|
6501
|
-
"@geekmidas/services": "~10.0.0-alpha.
|
|
6502
|
-
"@geekmidas/storage": "~10.0.0-alpha.
|
|
6503
|
-
"@geekmidas/telescope": "~10.0.0-alpha.
|
|
6504
|
-
"@geekmidas/testkit": "~10.0.0-alpha.
|
|
5253
|
+
"@geekmidas/audit": "~10.0.0-alpha.66",
|
|
5254
|
+
"@geekmidas/auth": "~10.0.0-alpha.66",
|
|
5255
|
+
"@geekmidas/cache": "~10.0.0-alpha.66",
|
|
5256
|
+
"@geekmidas/client": "~10.0.0-alpha.66",
|
|
5257
|
+
"@geekmidas/cloud": "~10.0.0-alpha.66",
|
|
5258
|
+
"@geekmidas/constructs": "~10.0.0-alpha.66",
|
|
5259
|
+
"@geekmidas/db": "~10.0.0-alpha.66",
|
|
5260
|
+
"@geekmidas/emailkit": "~10.0.0-alpha.66",
|
|
5261
|
+
"@geekmidas/envkit": "~10.0.0-alpha.66",
|
|
5262
|
+
"@geekmidas/errors": "~10.0.0-alpha.66",
|
|
5263
|
+
"@geekmidas/events": "~10.0.0-alpha.66",
|
|
5264
|
+
"@geekmidas/logger": "~10.0.0-alpha.66",
|
|
5265
|
+
"@geekmidas/manifest": "~10.0.0-alpha.66",
|
|
5266
|
+
"@geekmidas/rate-limit": "~10.0.0-alpha.66",
|
|
5267
|
+
"@geekmidas/schema": "~10.0.0-alpha.66",
|
|
5268
|
+
"@geekmidas/services": "~10.0.0-alpha.66",
|
|
5269
|
+
"@geekmidas/storage": "~10.0.0-alpha.66",
|
|
5270
|
+
"@geekmidas/telescope": "~10.0.0-alpha.66",
|
|
5271
|
+
"@geekmidas/testkit": "~10.0.0-alpha.66",
|
|
6505
5272
|
"@geekmidas/cli": `~${loadPackageJson().version}`
|
|
6506
5273
|
};
|
|
6507
5274
|
//#endregion
|
|
@@ -11183,7 +9950,7 @@ const AWS_REGION = /^[a-z]{2}(-gov)?-[a-z]+-\d$/;
|
|
|
11183
9950
|
*/
|
|
11184
9951
|
async function initCommand(projectName, options = {}) {
|
|
11185
9952
|
const cwd = process.cwd();
|
|
11186
|
-
const detectedPkgManager = detectPackageManager$
|
|
9953
|
+
const detectedPkgManager = detectPackageManager$1(cwd);
|
|
11187
9954
|
if (options.deploy && !deployTargetChoices.some((choice) => choice.value === options.deploy)) throw new UnknownDeployTarget(options.deploy, deployTargetChoices.map((c) => c.value));
|
|
11188
9955
|
if (options.region && !AWS_REGION.test(options.region)) throw new NotAnAwsRegion(options.region);
|
|
11189
9956
|
if (options.stages || options.localStage || options.protectedStage) {
|
|
@@ -12264,8 +11031,8 @@ function testDatabases(reconciled) {
|
|
|
12264
11031
|
* starts without the reference data the app needs.
|
|
12265
11032
|
*/
|
|
12266
11033
|
async function migrateTestStage(workspace, env) {
|
|
12267
|
-
const { migrateAndSeed } = await import("./migrate-
|
|
12268
|
-
const { discover } = await import("./discover-
|
|
11034
|
+
const { migrateAndSeed } = await import("./migrate-BJBcm4sG.mjs");
|
|
11035
|
+
const { discover } = await import("./discover-BFoJWKGD.mjs").then((n) => n.i);
|
|
12269
11036
|
const sources = {};
|
|
12270
11037
|
const manifest = await discover({
|
|
12271
11038
|
patterns: constructGlobs(workspace),
|
|
@@ -12423,7 +11190,7 @@ function withVersion(range, version) {
|
|
|
12423
11190
|
}
|
|
12424
11191
|
async function upgradeCommand(options = {}) {
|
|
12425
11192
|
const cwd = process.cwd();
|
|
12426
|
-
const pm = detectPackageManager$
|
|
11193
|
+
const pm = detectPackageManager$1(cwd);
|
|
12427
11194
|
const root = findWorkspaceRoot(cwd, pm);
|
|
12428
11195
|
logger.log("\n📦 Scanning workspace for @geekmidas packages...\n");
|
|
12429
11196
|
logger.log(` Package manager: ${pm}`);
|
|
@@ -12695,7 +11462,7 @@ program.command("migrate").description("Apply each database construct's migratio
|
|
|
12695
11462
|
try {
|
|
12696
11463
|
const globalOptions = program.opts();
|
|
12697
11464
|
if (globalOptions.cwd) process.chdir(globalOptions.cwd);
|
|
12698
|
-
const { migrateCommand } = await import("./migrate-
|
|
11465
|
+
const { migrateCommand } = await import("./migrate-BJBcm4sG.mjs");
|
|
12699
11466
|
await migrateCommand({
|
|
12700
11467
|
...options,
|
|
12701
11468
|
...construct ? { construct } : {}
|
|
@@ -12709,7 +11476,7 @@ program.command("seed").description("Migrate, then run each database construct's
|
|
|
12709
11476
|
try {
|
|
12710
11477
|
const globalOptions = program.opts();
|
|
12711
11478
|
if (globalOptions.cwd) process.chdir(globalOptions.cwd);
|
|
12712
|
-
const { seedCommand } = await import("./migrate-
|
|
11479
|
+
const { seedCommand } = await import("./migrate-BJBcm4sG.mjs");
|
|
12713
11480
|
await seedCommand({
|
|
12714
11481
|
...options,
|
|
12715
11482
|
...construct ? { construct } : {}
|
|
@@ -12723,7 +11490,7 @@ program.command("migration").description("Write a construct's next migration: an
|
|
|
12723
11490
|
try {
|
|
12724
11491
|
const globalOptions = program.opts();
|
|
12725
11492
|
if (globalOptions.cwd) process.chdir(globalOptions.cwd);
|
|
12726
|
-
const { migrationCommand } = await import("./migrate-
|
|
11493
|
+
const { migrationCommand } = await import("./migrate-BJBcm4sG.mjs");
|
|
12727
11494
|
await migrationCommand({
|
|
12728
11495
|
construct,
|
|
12729
11496
|
...name ? { name } : {}
|
|
@@ -12831,7 +11598,7 @@ program.command("encryption:rotate").description("Add a new current key to an En
|
|
|
12831
11598
|
try {
|
|
12832
11599
|
const globalOptions = program.opts();
|
|
12833
11600
|
if (globalOptions.cwd) process.chdir(globalOptions.cwd);
|
|
12834
|
-
const { encryptionRotateCommand } = await import("./encryption-
|
|
11601
|
+
const { encryptionRotateCommand } = await import("./encryption-CPfENwWO.mjs");
|
|
12835
11602
|
await encryptionRotateCommand(construct, options);
|
|
12836
11603
|
} catch (error) {
|
|
12837
11604
|
console.error(formatError(error));
|
|
@@ -12842,7 +11609,7 @@ program.command("encryption:retire").description("Remove an old key from an Encr
|
|
|
12842
11609
|
try {
|
|
12843
11610
|
const globalOptions = program.opts();
|
|
12844
11611
|
if (globalOptions.cwd) process.chdir(globalOptions.cwd);
|
|
12845
|
-
const { encryptionRetireCommand } = await import("./encryption-
|
|
11612
|
+
const { encryptionRetireCommand } = await import("./encryption-CPfENwWO.mjs");
|
|
12846
11613
|
await encryptionRetireCommand(construct, key, options);
|
|
12847
11614
|
} catch (error) {
|
|
12848
11615
|
console.error(formatError(error));
|
|
@@ -12885,7 +11652,7 @@ program.command("secrets:reconcile").description("Backfill missing custom secret
|
|
|
12885
11652
|
if (globalOptions.cwd) process.chdir(globalOptions.cwd);
|
|
12886
11653
|
const { loadWorkspaceConfig } = await import("./config.mjs");
|
|
12887
11654
|
const { reconcileMissingSecrets } = await import("./reconcile--I7V4Klj.mjs");
|
|
12888
|
-
const { secretsStoreFor } = await import("./
|
|
11655
|
+
const { secretsStoreFor } = await import("./state-uuYqnRVO.mjs").then((n) => n.ft);
|
|
12889
11656
|
const { workspace } = await loadWorkspaceConfig();
|
|
12890
11657
|
const stage = options.stage ?? workspace.stages.local;
|
|
12891
11658
|
const store = await secretsStoreFor(workspace, stage);
|
|
@@ -12894,7 +11661,7 @@ program.command("secrets:reconcile").description("Backfill missing custom secret
|
|
|
12894
11661
|
console.error(`No secrets found for stage "${stage}". Run "gkm secrets:init --stage ${stage}" first.`);
|
|
12895
11662
|
process.exit(1);
|
|
12896
11663
|
}
|
|
12897
|
-
const { derivedContainers } = await import("./
|
|
11664
|
+
const { derivedContainers } = await import("./state-uuYqnRVO.mjs").then((n) => n.j);
|
|
12898
11665
|
const result = reconcileMissingSecrets(secrets, workspace, await derivedContainers(workspace, stage));
|
|
12899
11666
|
if (!result) {
|
|
12900
11667
|
console.log(`\n✓ Secrets for stage "${stage}" are up-to-date`);
|
|
@@ -12908,8 +11675,8 @@ program.command("secrets:reconcile").description("Backfill missing custom secret
|
|
|
12908
11675
|
process.exit(1);
|
|
12909
11676
|
}
|
|
12910
11677
|
});
|
|
12911
|
-
program.command("deploy").description("Deploy a stage through its target").option("--target <name>", "Deploy target: dokploy, sst, or one named in deploy.targets (default: deploy.default)").option("--provider <provider>", "[DEPRECATED] Use --target instead").requiredOption("--stage <stage>", "Deployment stage (e.g., production, staging)").option("--tag <tag>", "Image tag (default: stage-timestamp)").option("--skip-push", "Skip pushing image to registry").option("--skip-build", "Skip build step (use existing build)").option("--json", "Write events as JSON lines instead of progress; never prompts").option("--dry-run", "Show what would be created or reused; change, build and push nothing").action(async (options) => {
|
|
12912
|
-
const { deployCli } = await import("./cli-
|
|
11678
|
+
program.command("deploy").description("Deploy a stage through its target").option("--target <name>", "Deploy target: dokploy, compose, sst, or one named in deploy.targets (default: deploy.default)").option("--provider <provider>", "[DEPRECATED] Use --target instead").requiredOption("--stage <stage>", "Deployment stage (e.g., production, staging)").option("--tag <tag>", "Image tag (default: stage-timestamp; compose: the commit). Compose pulls a given tag").option("--skip-push", "Skip pushing image to registry").option("--skip-build", "Skip build step (use existing build)").option("--json", "Write events as JSON lines instead of progress; never prompts").option("--dry-run", "Show what would be created or reused; change, build and push nothing").option("--atomic", "If the release fails, roll back every app it released, not only the failed ones").action(async (options) => {
|
|
11679
|
+
const { deployCli } = await import("./cli-CfHm0AO7.mjs");
|
|
12913
11680
|
const globalOptions = program.opts();
|
|
12914
11681
|
const code = await deployCli({
|
|
12915
11682
|
cwd: resolve(globalOptions.cwd ?? process.cwd()),
|
|
@@ -12918,10 +11685,29 @@ program.command("deploy").description("Deploy a stage through its target").optio
|
|
|
12918
11685
|
stage: options.stage,
|
|
12919
11686
|
...options.tag ? { tag: options.tag } : {},
|
|
12920
11687
|
...options.json ? { json: true } : {},
|
|
12921
|
-
...options.dryRun ? { dryRun: true } : {}
|
|
11688
|
+
...options.dryRun ? { dryRun: true } : {},
|
|
11689
|
+
...options.atomic ? { atomic: true } : {}
|
|
12922
11690
|
});
|
|
12923
11691
|
if (code !== 0) process.exit(code);
|
|
12924
11692
|
});
|
|
11693
|
+
program.command("deploy:rollback").description("Put a stage's app back on its previous release (Dokploy): one app with --app, every app with --atomic").requiredOption("--stage <stage>", "A deployed stage").option("--app <app>", "The app to roll back").option("--atomic", "Roll back every app that has an earlier release").action(async (options) => {
|
|
11694
|
+
try {
|
|
11695
|
+
const { rollbackStage } = await import("./rollback-DvAo4OKS.mjs").then((n) => n.a);
|
|
11696
|
+
const { terminalCredentials } = await import("./terminal-DUhcZ1ND.mjs");
|
|
11697
|
+
const globalOptions = program.opts();
|
|
11698
|
+
await rollbackStage({
|
|
11699
|
+
cwd: resolve(globalOptions.cwd ?? process.cwd()),
|
|
11700
|
+
stage: options.stage,
|
|
11701
|
+
...options.app ? { app: options.app } : {},
|
|
11702
|
+
...options.atomic ? { atomic: true } : {},
|
|
11703
|
+
credentials: terminalCredentials(),
|
|
11704
|
+
log: (line) => console.log(line)
|
|
11705
|
+
});
|
|
11706
|
+
} catch (error) {
|
|
11707
|
+
console.error(formatError(error));
|
|
11708
|
+
process.exit(1);
|
|
11709
|
+
}
|
|
11710
|
+
});
|
|
12925
11711
|
program.command("deploy:init").description("Initialize Dokploy deployment (create project and application)").option("--endpoint <url>", "Dokploy server URL (uses stored credentials if logged in)").requiredOption("--project <name>", "Project name (creates if not exists)").requiredOption("--app <name>", "Application name").option("--project-id <id>", "Use existing project ID instead of creating").option("--registry-id <id>", "Configure registry for the application").action(async (options) => {
|
|
12926
11712
|
try {
|
|
12927
11713
|
const globalOptions = program.opts();
|
|
@@ -12942,7 +11728,7 @@ program.command("deploy:github").description("Let GitHub Actions deploy a stage:
|
|
|
12942
11728
|
try {
|
|
12943
11729
|
const globalOptions = program.opts();
|
|
12944
11730
|
if (globalOptions.cwd) process.chdir(globalOptions.cwd);
|
|
12945
|
-
const { deployGithubCommand } = await import("./github-
|
|
11731
|
+
const { deployGithubCommand } = await import("./github-MXA9v7Ge.mjs");
|
|
12946
11732
|
await deployGithubCommand(options);
|
|
12947
11733
|
} catch (error) {
|
|
12948
11734
|
console.error(formatError(error));
|
|
@@ -13077,6 +11863,6 @@ program.command("upgrade").description("Upgrade @geekmidas/cli (with --all, ever
|
|
|
13077
11863
|
});
|
|
13078
11864
|
program.parse();
|
|
13079
11865
|
//#endregion
|
|
13080
|
-
export {
|
|
11866
|
+
export { validateDokployToken as a, formatError as i, slug as n, canPrompt as o, stamp as r, prompt as s, generateFullstackCustomSecrets as t };
|
|
13081
11867
|
|
|
13082
11868
|
//# sourceMappingURL=index.mjs.map
|