@geekmidas/cli 10.0.0-alpha.63 → 10.0.0-alpha.65
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 +1 -1
- package/dist/{HostingerProvider-mJ7YruxH.cjs → HostingerProvider-Dlmbfass.cjs} +3 -3
- package/dist/{HostingerProvider-mJ7YruxH.cjs.map → HostingerProvider-Dlmbfass.cjs.map} +1 -1
- package/dist/{HostingerProvider-DBBwpDjn.mjs → HostingerProvider-x9gQTAGZ.mjs} +2 -2
- package/dist/{HostingerProvider-DBBwpDjn.mjs.map → HostingerProvider-x9gQTAGZ.mjs.map} +1 -1
- package/dist/{LegacyStateStore-CP7SZwTP.cjs → LegacyStateStore-BGoo__sO.cjs} +2 -2
- package/dist/{LegacyStateStore-CP7SZwTP.cjs.map → LegacyStateStore-BGoo__sO.cjs.map} +1 -1
- package/dist/{LegacyStateStore-Q3GmKOdw.mjs → LegacyStateStore-BUu1nQ5u.mjs} +2 -2
- package/dist/{LegacyStateStore-Q3GmKOdw.mjs.map → LegacyStateStore-BUu1nQ5u.mjs.map} +1 -1
- package/dist/{LocalStateStore-sS8mvLeq.cjs → LocalStateStore-93urXyQe.cjs} +2 -2
- package/dist/{LocalStateStore-sS8mvLeq.cjs.map → LocalStateStore-93urXyQe.cjs.map} +1 -1
- package/dist/{LocalStateStore-BGIXcrvX.mjs → LocalStateStore-BRhMQRuy.mjs} +2 -2
- package/dist/{LocalStateStore-BGIXcrvX.mjs.map → LocalStateStore-BRhMQRuy.mjs.map} +1 -1
- package/dist/{OpenApiTsGenerator-CjHWFRDd.cjs → OpenApiTsGenerator-BWuDRHe9.cjs} +2 -2
- package/dist/{OpenApiTsGenerator-CjHWFRDd.cjs.map → OpenApiTsGenerator-BWuDRHe9.cjs.map} +1 -1
- package/dist/{OpenApiTsGenerator-CnAwhGxi.mjs → OpenApiTsGenerator-C6v-IeHt.mjs} +2 -2
- package/dist/{OpenApiTsGenerator-CnAwhGxi.mjs.map → OpenApiTsGenerator-C6v-IeHt.mjs.map} +1 -1
- package/dist/{S3StateStore-6jqITH2F.cjs → S3StateStore-F4dxw6kS.cjs} +2 -2
- package/dist/{S3StateStore-6jqITH2F.cjs.map → S3StateStore-F4dxw6kS.cjs.map} +1 -1
- package/dist/{S3StateStore-D7l_lWJ3.mjs → S3StateStore-pw1GhWwt.mjs} +2 -2
- package/dist/{S3StateStore-D7l_lWJ3.mjs.map → S3StateStore-pw1GhWwt.mjs.map} +1 -1
- package/dist/{SSMStateStore-B__F7IJa.cjs → SSMStateStore-Bt9acuAF.cjs} +2 -2
- package/dist/{SSMStateStore-B__F7IJa.cjs.map → SSMStateStore-Bt9acuAF.cjs.map} +1 -1
- package/dist/{SSMStateStore-CAQDVpoc.mjs → SSMStateStore-Dmj7zzFO.mjs} +2 -2
- package/dist/{SSMStateStore-CAQDVpoc.mjs.map → SSMStateStore-Dmj7zzFO.mjs.map} +1 -1
- package/dist/{bundler-DLfOn0ls.cjs → bundler-D6MlIfBT.cjs} +3 -3
- package/dist/{bundler-DLfOn0ls.cjs.map → bundler-D6MlIfBT.cjs.map} +1 -1
- package/dist/{bundler-DyWI41Iy.mjs → bundler-D_14JLZF.mjs} +3 -3
- package/dist/{bundler-DyWI41Iy.mjs.map → bundler-D_14JLZF.mjs.map} +1 -1
- package/dist/{cli-D-ZM-X2s.cjs → cli-BfH1deAT.cjs} +8 -7
- package/dist/{cli-D-ZM-X2s.cjs.map → cli-BfH1deAT.cjs.map} +1 -1
- package/dist/{cli-PkVF28LM.mjs → cli-P56vvrDQ.mjs} +5 -4
- package/dist/{cli-PkVF28LM.mjs.map → cli-P56vvrDQ.mjs.map} +1 -1
- package/dist/compose-391oEKj1.cjs +1982 -0
- package/dist/compose-391oEKj1.cjs.map +1 -0
- package/dist/compose-DprE3ASm.mjs +1862 -0
- package/dist/compose-DprE3ASm.mjs.map +1 -0
- package/dist/{config-C2BUosLm.d.mts → config-BonvTW8p.d.mts} +3 -3
- package/dist/{config-C2BUosLm.d.mts.map → config-BonvTW8p.d.mts.map} +1 -1
- package/dist/{config-CemHVcIr.d.cts → config-V_X_BMpm.d.cts} +3 -3
- package/dist/{config-CemHVcIr.d.cts.map → config-V_X_BMpm.d.cts.map} +1 -1
- package/dist/config.cjs +416 -17
- package/dist/config.cjs.map +1 -0
- package/dist/config.d.cts +3 -3
- package/dist/config.d.mts +3 -3
- package/dist/config.mjs +401 -2
- package/dist/config.mjs.map +1 -0
- package/dist/credentials-Bzbg23n8.cjs +298 -0
- package/dist/credentials-Bzbg23n8.cjs.map +1 -0
- package/dist/credentials-Dq1MnWQf.mjs +233 -0
- package/dist/credentials-Dq1MnWQf.mjs.map +1 -0
- package/dist/{define-CHBIEc5j.mjs → define-DR8xGYfL.mjs} +6 -3
- package/dist/define-DR8xGYfL.mjs.map +1 -0
- package/dist/{define-Cv_QvDrg.cjs → define-DzRz8FgH.cjs} +6 -3
- package/dist/define-DzRz8FgH.cjs.map +1 -0
- package/dist/deploy/public.cjs +39 -33
- package/dist/deploy/public.cjs.map +1 -1
- package/dist/deploy/public.d.cts +44 -5
- package/dist/deploy/public.d.cts.map +1 -1
- package/dist/deploy/public.d.mts +44 -5
- package/dist/deploy/public.d.mts.map +1 -1
- package/dist/deploy/public.mjs +15 -12
- package/dist/deploy/public.mjs.map +1 -1
- package/dist/{deploy-CGW-QY9D.mjs → deploy-9MBG_QLd.mjs} +11 -9
- package/dist/deploy-9MBG_QLd.mjs.map +1 -0
- package/dist/{deploy-BiMrP9i7.cjs → deploy-B80idPUZ.cjs} +17 -15
- package/dist/deploy-B80idPUZ.cjs.map +1 -0
- package/dist/{deploy-CG8bfFij.mjs → deploy-CZ5nlpsu.mjs} +16 -119
- package/dist/deploy-CZ5nlpsu.mjs.map +1 -0
- package/dist/{deploy-ABBI0lf1.cjs → deploy-Cy8igpQF.cjs} +48 -151
- package/dist/deploy-Cy8igpQF.cjs.map +1 -0
- package/dist/derive-B4vDpzgP.mjs +150 -0
- package/dist/derive-B4vDpzgP.mjs.map +1 -0
- package/dist/derive-DAbo49sm.cjs +161 -0
- package/dist/derive-DAbo49sm.cjs.map +1 -0
- package/dist/{discover-C_6COHEv.cjs → discover-DHdECueV.cjs} +6 -157
- package/dist/discover-DHdECueV.cjs.map +1 -0
- package/dist/{discover-BaT6bpnA.mjs → discover-LMrqsWSU.mjs} +8 -117
- package/dist/discover-LMrqsWSU.mjs.map +1 -0
- package/dist/{dokploy-CwdmlyQL.mjs → dokploy-BdtBePjp.mjs} +3 -3
- package/dist/{dokploy-CwdmlyQL.mjs.map → dokploy-BdtBePjp.mjs.map} +1 -1
- package/dist/{dokploy-QJxMhxdf.cjs → dokploy-iFY8MY-L.cjs} +3 -3
- package/dist/{dokploy-QJxMhxdf.cjs.map → dokploy-iFY8MY-L.cjs.map} +1 -1
- package/dist/{encryption-DXw3jj0c.cjs → encryption-B1wJwgae.cjs} +6 -6
- package/dist/{encryption-DXw3jj0c.cjs.map → encryption-B1wJwgae.cjs.map} +1 -1
- package/dist/{encryption-CbQG_ayW.mjs → encryption-DF_hgIpl.mjs} +5 -5
- package/dist/{encryption-CbQG_ayW.mjs.map → encryption-DF_hgIpl.mjs.map} +1 -1
- package/dist/{github-ciwbIwVa.mjs → github-B0YYE98x.mjs} +4 -4
- package/dist/{github-ciwbIwVa.mjs.map → github-B0YYE98x.mjs.map} +1 -1
- package/dist/{github-BH_mETav.cjs → github-CYcmTdFs.cjs} +4 -4
- package/dist/{github-BH_mETav.cjs.map → github-CYcmTdFs.cjs.map} +1 -1
- package/dist/{index-DrKzpHl-.d.mts → index-DPUAGJaK.d.cts} +16 -3
- package/dist/index-DPUAGJaK.d.cts.map +1 -0
- package/dist/{index-BtXDZQuF.d.cts → index-DwKD_8lt.d.mts} +16 -3
- package/dist/index-DwKD_8lt.d.mts.map +1 -0
- package/dist/index.cjs +280 -1849
- package/dist/index.cjs.map +1 -1
- package/dist/index.mjs +221 -1774
- package/dist/index.mjs.map +1 -1
- package/dist/local-BdH4XN0E.cjs +409 -0
- package/dist/local-BdH4XN0E.cjs.map +1 -0
- package/dist/local-CWR4Y_kW.mjs +350 -0
- package/dist/local-CWR4Y_kW.mjs.map +1 -0
- package/dist/{migrate-ImBKTBcW.mjs → migrate-B2qL5wj4.mjs} +8 -7
- package/dist/{migrate-ImBKTBcW.mjs.map → migrate-B2qL5wj4.mjs.map} +1 -1
- package/dist/{migrate-6oa9y_f2.cjs → migrate-q4pPAXtO.cjs} +21 -20
- package/dist/{migrate-6oa9y_f2.cjs.map → migrate-q4pPAXtO.cjs.map} +1 -1
- package/dist/openapi.cjs +2 -2
- package/dist/openapi.d.cts +1 -1
- package/dist/openapi.d.mts +1 -1
- package/dist/openapi.mjs +2 -2
- package/dist/provider-C9_-45d5.cjs +56 -0
- package/dist/provider-C9_-45d5.cjs.map +1 -0
- package/dist/provider-q4lsYzYG.mjs +45 -0
- package/dist/provider-q4lsYzYG.mjs.map +1 -0
- package/dist/{ready-Bpdnn2ON.mjs → ready-bQfu9GES.mjs} +3 -3
- package/dist/{ready-Bpdnn2ON.mjs.map → ready-bQfu9GES.mjs.map} +1 -1
- package/dist/{ready-BiG0rt4h.cjs → ready-brEhh68f.cjs} +3 -3
- package/dist/{ready-BiG0rt4h.cjs.map → ready-brEhh68f.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-DLkIXTct.mjs +262 -0
- package/dist/resolve-DLkIXTct.mjs.map +1 -0
- package/dist/resolve-N_i4XNd-.cjs +315 -0
- package/dist/resolve-N_i4XNd-.cjs.map +1 -0
- package/dist/{runtime-Dn_n-29S.d.cts → runtime-BnD7Nn96.d.cts} +2 -2
- package/dist/{runtime-CXPlkZzs.d.mts.map → runtime-BnD7Nn96.d.cts.map} +1 -1
- package/dist/{runtime-CXPlkZzs.d.mts → runtime-j-tmFL65.d.mts} +2 -2
- package/dist/{runtime-Dn_n-29S.d.cts.map → runtime-j-tmFL65.d.mts.map} +1 -1
- package/dist/sandbox/discover-worker.cjs +1 -1
- package/dist/sandbox/discover-worker.mjs +1 -1
- package/dist/sandbox-CL--ZDui.cjs +164 -0
- package/dist/sandbox-CL--ZDui.cjs.map +1 -0
- package/dist/sandbox-Dm247NGx.mjs +117 -0
- package/dist/sandbox-Dm247NGx.mjs.map +1 -0
- package/dist/sniffer-BSwaL-dX.cjs +2236 -0
- package/dist/sniffer-BSwaL-dX.cjs.map +1 -0
- package/dist/sniffer-BZ-OJO4Y.mjs +2068 -0
- package/dist/sniffer-BZ-OJO4Y.mjs.map +1 -0
- package/dist/sst-B2HbrDR3.mjs +430 -0
- package/dist/sst-B2HbrDR3.mjs.map +1 -0
- package/dist/sst-DcfsVH6x.cjs +453 -0
- package/dist/sst-DcfsVH6x.cjs.map +1 -0
- package/dist/{sniffer-aO_-s5vq.mjs → state-B8YwmUZU.mjs} +1204 -3236
- package/dist/state-B8YwmUZU.mjs.map +1 -0
- package/dist/{sniffer-D_SYc_pO.cjs → state-DF26e_lo.cjs} +1251 -3494
- package/dist/state-DF26e_lo.cjs.map +1 -0
- package/dist/{store-KvNQwYaB.cjs → store-CJF1Tqkq.cjs} +4 -4
- package/dist/{store-KvNQwYaB.cjs.map → store-CJF1Tqkq.cjs.map} +1 -1
- package/dist/{store-BS8A7NcA.mjs → store-CLiHnqOY.mjs} +2 -2
- package/dist/{store-BS8A7NcA.mjs.map → store-CLiHnqOY.mjs.map} +1 -1
- package/dist/target/public.cjs +6 -6
- package/dist/target/public.d.cts +3 -3
- package/dist/target/public.d.mts +3 -3
- package/dist/target/public.mjs +3 -3
- package/dist/terminal-Bs5E1oxm.cjs +88 -0
- package/dist/terminal-Bs5E1oxm.cjs.map +1 -0
- package/dist/terminal-CfbiYUbZ.mjs +84 -0
- package/dist/terminal-CfbiYUbZ.mjs.map +1 -0
- package/dist/{types-DIA7Li-J.d.cts → types-BCzMWBb6.d.mts} +84 -17
- package/dist/types-BCzMWBb6.d.mts.map +1 -0
- package/dist/{types-CfLKGfC_.d.mts → types-ByMWkXKP.d.cts} +85 -16
- package/dist/types-ByMWkXKP.d.cts.map +1 -0
- package/dist/utils-D2Nk1Nv2.cjs +205 -0
- package/dist/utils-D2Nk1Nv2.cjs.map +1 -0
- package/dist/utils-D_fxUqvU.mjs +156 -0
- package/dist/utils-D_fxUqvU.mjs.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-Kl4NG9IH.cjs → workspace-BewAHZIo.cjs} +41 -6
- package/dist/workspace-BewAHZIo.cjs.map +1 -0
- package/dist/{workspace-Dh7rr7l4.mjs → workspace-C06_pW4g.mjs} +30 -7
- package/dist/workspace-C06_pW4g.mjs.map +1 -0
- package/package.json +13 -13
- package/dist/config-BEJYKd4s.cjs +0 -648
- package/dist/config-BEJYKd4s.cjs.map +0 -1
- package/dist/config-LKKCVCnC.mjs +0 -547
- package/dist/config-LKKCVCnC.mjs.map +0 -1
- package/dist/define-CHBIEc5j.mjs.map +0 -1
- package/dist/define-Cv_QvDrg.cjs.map +0 -1
- package/dist/deploy-ABBI0lf1.cjs.map +0 -1
- package/dist/deploy-BiMrP9i7.cjs.map +0 -1
- package/dist/deploy-CG8bfFij.mjs.map +0 -1
- package/dist/deploy-CGW-QY9D.mjs.map +0 -1
- package/dist/discover-BaT6bpnA.mjs.map +0 -1
- package/dist/discover-C_6COHEv.cjs.map +0 -1
- package/dist/home-DOzBaWaL.cjs +0 -33
- package/dist/home-DOzBaWaL.cjs.map +0 -1
- package/dist/home-GI7pJl1M.mjs +0 -22
- package/dist/home-GI7pJl1M.mjs.map +0 -1
- package/dist/index-BtXDZQuF.d.cts.map +0 -1
- package/dist/index-DrKzpHl-.d.mts.map +0 -1
- package/dist/resolve-B1T0hUaR.mjs +0 -170
- package/dist/resolve-B1T0hUaR.mjs.map +0 -1
- package/dist/resolve-xytBTtG4.cjs +0 -217
- package/dist/resolve-xytBTtG4.cjs.map +0 -1
- package/dist/runtime-CMwtq0AL.cjs +0 -163
- package/dist/runtime-CMwtq0AL.cjs.map +0 -1
- package/dist/runtime-DbakJS8u.mjs +0 -134
- package/dist/runtime-DbakJS8u.mjs.map +0 -1
- package/dist/sniffer-D_SYc_pO.cjs.map +0 -1
- package/dist/sniffer-aO_-s5vq.mjs.map +0 -1
- package/dist/terminal-D0f1u0LG.cjs +0 -576
- package/dist/terminal-D0f1u0LG.cjs.map +0 -1
- package/dist/terminal-gMxAo9Dj.mjs +0 -517
- package/dist/terminal-gMxAo9Dj.mjs.map +0 -1
- package/dist/types-CfLKGfC_.d.mts.map +0 -1
- package/dist/types-DIA7Li-J.d.cts.map +0 -1
- package/dist/workspace-Dh7rr7l4.mjs.map +0 -1
- package/dist/workspace-Kl4NG9IH.cjs.map +0 -1
package/dist/index.mjs
CHANGED
|
@@ -1,34 +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 { a as
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
8
|
-
import {
|
|
9
|
-
import {
|
|
3
|
+
import { C as loginCommand, E as whoamiCommand, S as keyFingerprint, g as createStateStore, n as sniffAppEnvironment, r as DokployApi, w as logoutCommand } from "./sniffer-BZ-OJO4Y.mjs";
|
|
4
|
+
import { i as CommandFailed, o as DEFAULT_TIMEOUT_MS, t as LocalSandbox } from "./local-CWR4Y_kW.mjs";
|
|
5
|
+
import { a as getDokployCredentials, o as getDokployRegistryId, s as getDokployToken } from "./credentials-Dq1MnWQf.mjs";
|
|
6
|
+
import { l as withOwningTsconfigJsx, r as discover, u as ConstructGenerator } from "./discover-LMrqsWSU.mjs";
|
|
7
|
+
import { t as appKey } from "./derive-B4vDpzgP.mjs";
|
|
8
|
+
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-C06_pW4g.mjs";
|
|
9
|
+
import { ConfigNotFound, NotABackendApp, NotInAnApp, loadAppConfig, loadConfig, loadWorkspaceConfig, loadWorkspaceSettings, parseModuleConfig } from "./config.mjs";
|
|
10
|
+
import { a as withCustomSecret, i as toEmbeddableSecrets, n as maskPassword } from "./storage-BzaUxmqe.mjs";
|
|
11
|
+
import { a as getKeyPath, i as FileSecretsStore, n as secretsStoreFor, s as projectKey, u as deployIdentity } from "./store-CLiHnqOY.mjs";
|
|
12
|
+
import { D as constructGlobs, E as backendsOf$1, I as quoteIdentifier, J as providerOf, K as cacheBackendFor, M as forgetState, O as derivedContainers, P as postgresDatabaseNames, Q as fakeKey, R as pgClient, T as FAKE_ENV, Z as describeServices, _ as dockerCommand, at as loadEnvFiles, et as primaryPortKey, it as isPortAvailable, j as LOCAL_CA_PATH, k as reconcileWorkspace, nt as createEntryWrapper, ot as loadSecretsForApp, rt as findAvailablePort, st as prepareEntryCredentials, tt as createCredentialsPreload } from "./state-B8YwmUZU.mjs";
|
|
13
|
+
import { i as telemetryFor, n as EndpointGenerator, r as runtimeFor } from "./OpenApiTsGenerator-C6v-IeHt.mjs";
|
|
10
14
|
import { generateOpenApi, openapiCommand, resolveOpenApiConfig } from "./openapi.mjs";
|
|
11
|
-
import { n as DEFAULT_CACHE, r as DEFAULT_EVENTS } from "./fakes-C9RiT6XG.mjs";
|
|
12
15
|
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";
|
|
13
|
-
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-
|
|
16
|
+
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-bQfu9GES.mjs";
|
|
14
17
|
import { n as withCompute, r as withRoutes, t as manifestModule } from "./emit-C1e8qD08.mjs";
|
|
15
|
-
import { t as
|
|
18
|
+
import { t as deploy$1 } from "./deploy-9MBG_QLd.mjs";
|
|
19
|
+
import { b as dockerCompose, t as composeTarget, v as composeProject, y as stackDir } from "./compose-DprE3ASm.mjs";
|
|
20
|
+
import { t as LocalStateStore } from "./LocalStateStore-BRhMQRuy.mjs";
|
|
21
|
+
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";
|
|
16
22
|
import { createRequire } from "node:module";
|
|
17
|
-
import { basename, dirname,
|
|
23
|
+
import { basename, dirname, isAbsolute, join, relative, resolve } from "node:path";
|
|
18
24
|
import { Command } from "commander";
|
|
19
25
|
import { existsSync, linkSync, mkdirSync, readFileSync, readdirSync, renameSync, unlinkSync, writeFileSync } from "node:fs";
|
|
20
|
-
import {
|
|
26
|
+
import { chmod, mkdir, readFile, rm, writeFile } from "node:fs/promises";
|
|
21
27
|
import { homedir } from "node:os";
|
|
22
28
|
import { promisify } from "node:util";
|
|
23
|
-
import {
|
|
29
|
+
import { canonicalId, kebabCase, migrationFolder, provideKey, schemeBase, serviceKey } from "@geekmidas/manifest";
|
|
24
30
|
import fg from "fast-glob";
|
|
25
31
|
import { pathToFileURL } from "node:url";
|
|
26
32
|
import { execFile, execSync, spawn } from "node:child_process";
|
|
27
33
|
import chokidar from "chokidar";
|
|
28
34
|
import { connect } from "node:net";
|
|
29
35
|
import { createHash, randomBytes, timingSafeEqual } from "node:crypto";
|
|
30
|
-
import
|
|
31
|
-
import { parse as parse$1, parseDocument, stringify } from "yaml";
|
|
36
|
+
import { parseDocument } from "yaml";
|
|
32
37
|
import prompts from "prompts";
|
|
33
38
|
import { createServer as createServer$1 } from "node:http";
|
|
34
39
|
import { Readable } from "node:stream";
|
|
@@ -41,7 +46,7 @@ import { Function } from "@geekmidas/constructs/functions";
|
|
|
41
46
|
import { Queue } from "@geekmidas/constructs/queue";
|
|
42
47
|
import { Topic } from "@geekmidas/constructs/topic";
|
|
43
48
|
//#region package.json
|
|
44
|
-
var version = "10.0.0-alpha.
|
|
49
|
+
var version = "10.0.0-alpha.65";
|
|
45
50
|
//#endregion
|
|
46
51
|
//#region src/trust/index.ts
|
|
47
52
|
/**
|
|
@@ -58,7 +63,7 @@ var version = "10.0.0-alpha.63";
|
|
|
58
63
|
* worse trade than one that tells you what to type. Caddy's own `caddy trust`
|
|
59
64
|
* draws the line in the same place.
|
|
60
65
|
*/
|
|
61
|
-
const logger$
|
|
66
|
+
const logger$7 = console;
|
|
62
67
|
const run = promisify(execFile);
|
|
63
68
|
/**
|
|
64
69
|
* How each platform is told about a new authority.
|
|
@@ -110,15 +115,15 @@ async function trustCommand(options = {}) {
|
|
|
110
115
|
const certificate = join(workspace.root, LOCAL_CA_PATH);
|
|
111
116
|
if (!existsSync(certificate)) throw new NoLocalAuthority(certificate);
|
|
112
117
|
const { describe, asks, steps } = installation(process.platform, certificate);
|
|
113
|
-
logger$
|
|
114
|
-
logger$
|
|
118
|
+
logger$7.log(`\n🔐 Trusting the local authority in ${describe}`);
|
|
119
|
+
logger$7.log(` ${certificate}\n`);
|
|
115
120
|
if (options.dryRun) {
|
|
116
|
-
for (const [command, args] of steps) logger$
|
|
121
|
+
for (const [command, args] of steps) logger$7.log(` ${command} ${args.join(" ")}`);
|
|
117
122
|
return;
|
|
118
123
|
}
|
|
119
|
-
logger$
|
|
124
|
+
logger$7.log(` ${asks}\n`);
|
|
120
125
|
for (const [command, args] of steps) await run(command, args, { stdio: "inherit" });
|
|
121
|
-
logger$
|
|
126
|
+
logger$7.log("\n✅ Local https addresses are trusted. Restart your browser.\n");
|
|
122
127
|
}
|
|
123
128
|
/**
|
|
124
129
|
* Trust the authority if this machine lacks it and its owner agrees.
|
|
@@ -135,7 +140,7 @@ async function ensureTrusted(root, url, options = {}) {
|
|
|
135
140
|
if (await isTrusted(url)) return;
|
|
136
141
|
if (!options.assumeYes) {
|
|
137
142
|
if (!process.stdin.isTTY) {
|
|
138
|
-
logger$
|
|
143
|
+
logger$7.log(" Local https addresses are not trusted here. Run \"gkm trust\", or \"gkm setup --yes\".");
|
|
139
144
|
return;
|
|
140
145
|
}
|
|
141
146
|
if (await declined(root)) return;
|
|
@@ -147,7 +152,7 @@ async function ensureTrusted(root, url, options = {}) {
|
|
|
147
152
|
});
|
|
148
153
|
if (!install) {
|
|
149
154
|
await recordDeclined(root);
|
|
150
|
-
logger$
|
|
155
|
+
logger$7.log(" Skipped. Run \"gkm trust\" later.");
|
|
151
156
|
return;
|
|
152
157
|
}
|
|
153
158
|
}
|
|
@@ -1312,7 +1317,7 @@ function closeFakes(served) {
|
|
|
1312
1317
|
}
|
|
1313
1318
|
//#endregion
|
|
1314
1319
|
//#region src/exec/index.ts
|
|
1315
|
-
const logger$
|
|
1320
|
+
const logger$6 = console;
|
|
1316
1321
|
/** `gkm exec` was given nothing to run. */
|
|
1317
1322
|
var NoCommandSpecified = class extends Error {
|
|
1318
1323
|
constructor() {
|
|
@@ -1335,17 +1340,17 @@ async function execCommand(commandArgs, options = {}) {
|
|
|
1335
1340
|
const cwd = options.cwd ?? process.cwd();
|
|
1336
1341
|
if (!commandArgs[0]) throw new NoCommandSpecified();
|
|
1337
1342
|
const defaultEnv = loadEnvFiles(".env");
|
|
1338
|
-
if (defaultEnv.loaded.length > 0) logger$
|
|
1343
|
+
if (defaultEnv.loaded.length > 0) logger$6.log(`📦 Loaded env: ${defaultEnv.loaded.join(", ")}`);
|
|
1339
1344
|
const { credentials, secretsJsonPath, appName } = await prepareEntryCredentials({ cwd });
|
|
1340
|
-
if (appName) logger$
|
|
1345
|
+
if (appName) logger$6.log(`📦 App: ${appName}`);
|
|
1341
1346
|
const secretCount = Object.keys(credentials).filter((k) => k !== "PORT").length;
|
|
1342
|
-
if (secretCount > 0) logger$
|
|
1347
|
+
if (secretCount > 0) logger$6.log(`🔐 Loaded ${secretCount} secret(s)`);
|
|
1343
1348
|
const preloadDir = join(cwd, ".gkm");
|
|
1344
1349
|
await mkdir(preloadDir, { recursive: true });
|
|
1345
1350
|
const preloadPath = join(preloadDir, "credentials-preload.mjs");
|
|
1346
1351
|
await createCredentialsPreload(preloadPath, secretsJsonPath);
|
|
1347
1352
|
const [cmd, ...args] = commandArgs;
|
|
1348
|
-
logger$
|
|
1353
|
+
logger$6.log(`🚀 Running: ${[cmd, ...args].join(" ")}`);
|
|
1349
1354
|
const nodeOptions = [(process.env.NODE_OPTIONS ?? "").replace(/--import[= ]\S+/g, "").trim(), `--import=${preloadPath}`].filter(Boolean).join(" ");
|
|
1350
1355
|
const child = spawn(cmd, args, {
|
|
1351
1356
|
cwd,
|
|
@@ -1359,7 +1364,7 @@ async function execCommand(commandArgs, options = {}) {
|
|
|
1359
1364
|
const exitCode = await new Promise((resolve) => {
|
|
1360
1365
|
child.on("close", (code) => resolve(code ?? 0));
|
|
1361
1366
|
child.on("error", (error) => {
|
|
1362
|
-
logger$
|
|
1367
|
+
logger$6.error(`Failed to run command: ${error.message}`);
|
|
1363
1368
|
resolve(1);
|
|
1364
1369
|
});
|
|
1365
1370
|
});
|
|
@@ -1367,7 +1372,7 @@ async function execCommand(commandArgs, options = {}) {
|
|
|
1367
1372
|
}
|
|
1368
1373
|
//#endregion
|
|
1369
1374
|
//#region src/dev/index.ts
|
|
1370
|
-
const logger$
|
|
1375
|
+
const logger$5 = console;
|
|
1371
1376
|
/**
|
|
1372
1377
|
* Normalize telescope configuration
|
|
1373
1378
|
* @internal Exported for testing
|
|
@@ -1437,13 +1442,13 @@ async function devCommand(options) {
|
|
|
1437
1442
|
if (options.subscribers === false) process.env[SUBSCRIBERS_ENV] = "off";
|
|
1438
1443
|
if (options.entry) return entryDevCommand(options);
|
|
1439
1444
|
const defaultEnv = loadEnvFiles(".env");
|
|
1440
|
-
if (defaultEnv.loaded.length > 0) logger$
|
|
1445
|
+
if (defaultEnv.loaded.length > 0) logger$5.log(`📦 Loaded env: ${defaultEnv.loaded.join(", ")}`);
|
|
1441
1446
|
const appConfig = await loadAppConfig().catch((error) => {
|
|
1442
1447
|
if (error instanceof NotInAnApp || error instanceof NotABackendApp) return;
|
|
1443
1448
|
throw error;
|
|
1444
1449
|
});
|
|
1445
1450
|
if (!appConfig) {
|
|
1446
|
-
logger$
|
|
1451
|
+
logger$5.log("🔎 Reading gkm.config.ts and the constructs");
|
|
1447
1452
|
const { workspace: everything } = await loadWorkspaceConfig();
|
|
1448
1453
|
return workspaceDevCommand(everything, options);
|
|
1449
1454
|
}
|
|
@@ -1456,12 +1461,12 @@ async function devCommand(options) {
|
|
|
1456
1461
|
holder: holderOf
|
|
1457
1462
|
});
|
|
1458
1463
|
if (assigned.running.length > 0) throw new WorkspacePortsInUse(assigned.running);
|
|
1459
|
-
for (const { app, from, to, holder } of assigned.moved) logger$
|
|
1464
|
+
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}`);
|
|
1460
1465
|
const workspace = withAppPorts(appConfig.workspace, assigned.ports);
|
|
1461
1466
|
const workspaceAppPort = assigned.ports[workspaceAppName] ?? appConfig.app.port;
|
|
1462
1467
|
process.env[APP_TAG_ENV] = appTag(appConfig.workspaceRoot, workspaceAppName);
|
|
1463
1468
|
if (appConfig.app.entry) {
|
|
1464
|
-
logger$
|
|
1469
|
+
logger$5.log(`📄 Using entry point: ${appConfig.app.entry}`);
|
|
1465
1470
|
return entryDevCommand({
|
|
1466
1471
|
...options,
|
|
1467
1472
|
entry: appConfig.app.entry,
|
|
@@ -1471,8 +1476,8 @@ async function devCommand(options) {
|
|
|
1471
1476
|
}
|
|
1472
1477
|
if (config.env) {
|
|
1473
1478
|
const { loaded, missing } = loadEnvFiles(config.env, appRoot);
|
|
1474
|
-
if (loaded.length > 0) logger$
|
|
1475
|
-
if (missing.length > 0) logger$
|
|
1479
|
+
if (loaded.length > 0) logger$5.log(`📦 Loaded env: ${loaded.join(", ")}`);
|
|
1480
|
+
if (missing.length > 0) logger$5.warn(`⚠️ Missing env files: ${missing.join(", ")}`);
|
|
1476
1481
|
}
|
|
1477
1482
|
const telescope = normalizeTelescopeConfig(config.telescope);
|
|
1478
1483
|
const hooks = normalizeHooksConfig(config.hooks, appRoot);
|
|
@@ -1491,7 +1496,7 @@ async function devCommand(options) {
|
|
|
1491
1496
|
bustCache,
|
|
1492
1497
|
serveEmpty: true
|
|
1493
1498
|
}));
|
|
1494
|
-
logger$
|
|
1499
|
+
logger$5.log(`🔨 Building ${workspaceAppName ?? "the app"}`);
|
|
1495
1500
|
const initial = await build();
|
|
1496
1501
|
const runtime = config.runtime ?? "node";
|
|
1497
1502
|
let secretsJsonPath;
|
|
@@ -1502,11 +1507,11 @@ async function devCommand(options) {
|
|
|
1502
1507
|
const reconciled = await reconcileWorkspace(workspace, {
|
|
1503
1508
|
stage: workspace.stages.local,
|
|
1504
1509
|
...options.fake ? { fake: true } : {},
|
|
1505
|
-
progress: (message) => logger$
|
|
1510
|
+
progress: (message) => logger$5.log(message)
|
|
1506
1511
|
});
|
|
1507
1512
|
if (reconciled.services.length > 0) {
|
|
1508
|
-
logger$
|
|
1509
|
-
for (const line of describeServices(reconciled.services)) logger$
|
|
1513
|
+
logger$5.log("🐳 Services");
|
|
1514
|
+
for (const line of describeServices(reconciled.services)) logger$5.log(line);
|
|
1510
1515
|
}
|
|
1511
1516
|
Object.assign(appSecrets, reconciled.env);
|
|
1512
1517
|
const own = reconciled.plan.resources.find((r) => (r.kind === "rest-api" || r.kind === "site") && appKey(r.id) === workspaceAppName);
|
|
@@ -1573,11 +1578,11 @@ async function devCommand(options) {
|
|
|
1573
1578
|
cwd: appRoot
|
|
1574
1579
|
});
|
|
1575
1580
|
watcher.on("error", (error) => {
|
|
1576
|
-
logger$
|
|
1581
|
+
logger$5.error("❌ Watcher error:", error);
|
|
1577
1582
|
});
|
|
1578
1583
|
let rebuildTimeout = null;
|
|
1579
1584
|
watcher.on("change", async (path) => {
|
|
1580
|
-
logger$
|
|
1585
|
+
logger$5.log(`🔄 ${path} changed — rebuilding`);
|
|
1581
1586
|
if (rebuildTimeout) clearTimeout(rebuildTimeout);
|
|
1582
1587
|
rebuildTimeout = setTimeout(async () => {
|
|
1583
1588
|
try {
|
|
@@ -1586,8 +1591,8 @@ async function devCommand(options) {
|
|
|
1586
1591
|
await devServer.restart();
|
|
1587
1592
|
} catch (error) {
|
|
1588
1593
|
const err = error;
|
|
1589
|
-
logger$
|
|
1590
|
-
if (err.stack) logger$
|
|
1594
|
+
logger$5.error(`❌ Rebuild failed: ${err.message}`);
|
|
1595
|
+
if (err.stack) logger$5.error(err.stack);
|
|
1591
1596
|
}
|
|
1592
1597
|
}, 300);
|
|
1593
1598
|
});
|
|
@@ -1595,13 +1600,13 @@ async function devCommand(options) {
|
|
|
1595
1600
|
const shutdown = () => {
|
|
1596
1601
|
if (isShuttingDown) return;
|
|
1597
1602
|
isShuttingDown = true;
|
|
1598
|
-
logger$
|
|
1603
|
+
logger$5.log("\n🛑 Shutting down...");
|
|
1599
1604
|
Promise.all([
|
|
1600
1605
|
watcher.close(),
|
|
1601
1606
|
devServer.stop(),
|
|
1602
1607
|
discovery.leave()
|
|
1603
1608
|
]).catch((err) => {
|
|
1604
|
-
logger$
|
|
1609
|
+
logger$5.error("Error during shutdown:", err);
|
|
1605
1610
|
}).finally(() => {
|
|
1606
1611
|
process.exit(0);
|
|
1607
1612
|
});
|
|
@@ -1805,13 +1810,13 @@ async function workspaceDevCommand(configured, options) {
|
|
|
1805
1810
|
holder: holderOf
|
|
1806
1811
|
});
|
|
1807
1812
|
if (assigned.running.length > 0) throw new WorkspacePortsInUse(assigned.running);
|
|
1808
|
-
for (const { app, from, to, holder } of assigned.moved) logger$
|
|
1813
|
+
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}`);
|
|
1809
1814
|
const workspace = withAppPorts(configured, assigned.ports);
|
|
1810
1815
|
const appCount = Object.keys(workspace.apps).length;
|
|
1811
1816
|
const frontendApps = Object.entries(workspace.apps).filter(([_, app]) => app.type === "web");
|
|
1812
1817
|
const conflicts = checkPortConflicts(workspace);
|
|
1813
1818
|
if (conflicts.length > 0) {
|
|
1814
|
-
for (const conflict of conflicts) logger$
|
|
1819
|
+
for (const conflict of conflicts) logger$5.error(`❌ Port conflict: Apps "${conflict.app1}" and "${conflict.app2}" both use port ${conflict.port}`);
|
|
1815
1820
|
throw new Error("Port conflicts detected. Please assign unique ports to each app.");
|
|
1816
1821
|
}
|
|
1817
1822
|
if (frontendApps.length > 0) {
|
|
@@ -1820,10 +1825,10 @@ async function workspaceDevCommand(configured, options) {
|
|
|
1820
1825
|
for (const result of validationResults) {
|
|
1821
1826
|
if (!result.valid) {
|
|
1822
1827
|
hasErrors = true;
|
|
1823
|
-
logger$
|
|
1824
|
-
for (const error of result.errors) logger$
|
|
1828
|
+
logger$5.error(`\n❌ Frontend app "${result.appName}" validation failed:`);
|
|
1829
|
+
for (const error of result.errors) logger$5.error(` • ${error}`);
|
|
1825
1830
|
}
|
|
1826
|
-
for (const warning of result.warnings) logger$
|
|
1831
|
+
for (const warning of result.warnings) logger$5.warn(` ⚠️ ${result.appName}: ${warning}`);
|
|
1827
1832
|
}
|
|
1828
1833
|
if (hasErrors) throw new Error("Frontend app validation failed. Fix the issues above and try again.");
|
|
1829
1834
|
}
|
|
@@ -1831,24 +1836,24 @@ async function workspaceDevCommand(configured, options) {
|
|
|
1831
1836
|
const reconciled = await reconcileWorkspace(workspace, {
|
|
1832
1837
|
stage: workspace.stages.local,
|
|
1833
1838
|
...options.fake ? { fake: true } : {},
|
|
1834
|
-
progress: (message) => logger$
|
|
1839
|
+
progress: (message) => logger$5.log(message)
|
|
1835
1840
|
});
|
|
1836
1841
|
if (reconciled.services.length > 0) {
|
|
1837
|
-
logger$
|
|
1838
|
-
for (const line of describeServices(reconciled.services)) logger$
|
|
1842
|
+
logger$5.log("🐳 Services");
|
|
1843
|
+
for (const line of describeServices(reconciled.services)) logger$5.log(line);
|
|
1839
1844
|
}
|
|
1840
1845
|
const secretsEnv = {
|
|
1841
1846
|
...rawSecrets,
|
|
1842
1847
|
...reconciled.env
|
|
1843
1848
|
};
|
|
1844
1849
|
const fakes = options.fake ? await serveFakes(reconciled.fakes, reconciled.ports) : [];
|
|
1845
|
-
for (const { id, port } of fakes) logger$
|
|
1846
|
-
logger$
|
|
1850
|
+
for (const { id, port } of fakes) logger$5.log(`🎭 ${id} fake: http://localhost:${port}`);
|
|
1851
|
+
logger$5.log(options.migrate || options.seed ? "🗄️ Migrating the databases" : "🗄️ Checking for pending migrations");
|
|
1847
1852
|
if (options.migrate || options.seed) {
|
|
1848
|
-
const { prepareDevDatabases } = await import("./migrate-
|
|
1853
|
+
const { prepareDevDatabases } = await import("./migrate-B2qL5wj4.mjs");
|
|
1849
1854
|
await prepareDevDatabases(workspace, reconciled.env, { seed: !!options.seed });
|
|
1850
|
-
} else await import("./migrate-
|
|
1851
|
-
logger$
|
|
1855
|
+
} else await import("./migrate-B2qL5wj4.mjs").then(({ reportPendingMigrations }) => reportPendingMigrations(workspace, reconciled.env)).catch((error) => {
|
|
1856
|
+
logger$5.log(`⚠️ Could not check for pending migrations: ${error instanceof Error ? error.message : String(error)}`);
|
|
1852
1857
|
});
|
|
1853
1858
|
const appUrls = {};
|
|
1854
1859
|
for (const resource of reconciled.plan.resources) {
|
|
@@ -1869,11 +1874,11 @@ async function workspaceDevCommand(configured, options) {
|
|
|
1869
1874
|
else {
|
|
1870
1875
|
const { filters, unpackaged } = turboFilters(workspace);
|
|
1871
1876
|
turboFilter = filters.flatMap((name) => ["--filter", name]);
|
|
1872
|
-
if (unpackaged.length > 0) logger$
|
|
1877
|
+
if (unpackaged.length > 0) logger$5.warn(`⚠️ No package.json with a name for: ${unpackaged.join(", ")} — turbo cannot run ${unpackaged.length === 1 ? "it" : "them"}.`);
|
|
1873
1878
|
}
|
|
1874
1879
|
const buildOrder = getAppBuildOrder(workspace);
|
|
1875
1880
|
const width = Math.max(...buildOrder.map((name) => name.length));
|
|
1876
|
-
logger$
|
|
1881
|
+
logger$5.log(`\n${workspace.name}: ${appCount} app(s)`);
|
|
1877
1882
|
for (const appName of buildOrder) {
|
|
1878
1883
|
const app = workspace.apps[appName];
|
|
1879
1884
|
if (!app) continue;
|
|
@@ -1881,14 +1886,14 @@ async function workspaceDevCommand(configured, options) {
|
|
|
1881
1886
|
const own = reconciled.plan.resources.find((r) => r.kind === "mobile-app" && appKey(r.id) === appName);
|
|
1882
1887
|
const scheme = own ? reconciled.env[own.envKey] : void 0;
|
|
1883
1888
|
const device = Object.entries(reconciled.env).find(([key]) => key.endsWith("_DEVICE_URL"))?.[1];
|
|
1884
|
-
logger$
|
|
1889
|
+
logger$5.log(` ${appName.padEnd(width)} ${scheme ? `${scheme}://` : "mobile"}${device ? ` — devices reach ${new URL(device).hostname}` : ""}`);
|
|
1885
1890
|
continue;
|
|
1886
1891
|
}
|
|
1887
1892
|
const local = `http://localhost:${app.port}`;
|
|
1888
1893
|
const url = Object.entries(appUrls).find(([id]) => appKey(id) === appName)?.[1];
|
|
1889
|
-
logger$
|
|
1894
|
+
logger$5.log(` ${appName.padEnd(width)} ${url ? `${url} -> ${local}` : local}`);
|
|
1890
1895
|
}
|
|
1891
|
-
logger$
|
|
1896
|
+
logger$5.log("");
|
|
1892
1897
|
const configFiles = [
|
|
1893
1898
|
"gkm.config.ts",
|
|
1894
1899
|
"gkm.config.js",
|
|
@@ -1936,7 +1941,7 @@ async function workspaceDevCommand(configured, options) {
|
|
|
1936
1941
|
...options.fake ? { [FAKE_ENV]: "1" } : {},
|
|
1937
1942
|
[DISCOVERY_QUIET_ENV]: "1"
|
|
1938
1943
|
};
|
|
1939
|
-
logger$
|
|
1944
|
+
logger$5.log(`🚀 Starting ${appCount} app(s) — each builds before it answers, and says so when it is ready`);
|
|
1940
1945
|
const turboProcess = spawn("pnpm", [
|
|
1941
1946
|
"turbo",
|
|
1942
1947
|
"run",
|
|
@@ -1952,7 +1957,7 @@ async function workspaceDevCommand(configured, options) {
|
|
|
1952
1957
|
const shutdown = () => {
|
|
1953
1958
|
if (isShuttingDown) return;
|
|
1954
1959
|
isShuttingDown = true;
|
|
1955
|
-
logger$
|
|
1960
|
+
logger$5.log("\n🛑 Shutting down workspace...");
|
|
1956
1961
|
closeFakes(fakes);
|
|
1957
1962
|
discovery.leave();
|
|
1958
1963
|
const pid = turboProcess.pid;
|
|
@@ -1978,7 +1983,7 @@ async function workspaceDevCommand(configured, options) {
|
|
|
1978
1983
|
process.on("SIGTERM", shutdown);
|
|
1979
1984
|
return new Promise((resolve, reject) => {
|
|
1980
1985
|
turboProcess.on("error", (error) => {
|
|
1981
|
-
logger$
|
|
1986
|
+
logger$5.error("❌ Turbo error:", error);
|
|
1982
1987
|
reject(error);
|
|
1983
1988
|
});
|
|
1984
1989
|
turboProcess.on("exit", (code) => {
|
|
@@ -1997,11 +2002,11 @@ async function entryDevCommand(options) {
|
|
|
1997
2002
|
const entryPath = resolve(process.cwd(), entry);
|
|
1998
2003
|
if (!existsSync(entryPath)) throw new Error(`Entry file not found: ${entryPath}`);
|
|
1999
2004
|
const defaultEnv = loadEnvFiles(".env");
|
|
2000
|
-
if (defaultEnv.loaded.length > 0) logger$
|
|
2005
|
+
if (defaultEnv.loaded.length > 0) logger$5.log(`📦 Loaded env: ${defaultEnv.loaded.join(", ")}`);
|
|
2001
2006
|
const { credentials, resolvedPort, secretsJsonPath, appName } = await prepareEntryCredentials({ explicitPort: options.portExplicit ? options.port : void 0 });
|
|
2002
|
-
if (appName) logger$
|
|
2003
|
-
logger$
|
|
2004
|
-
if (Object.keys(credentials).length > 1) logger$
|
|
2007
|
+
if (appName) logger$5.log(`📦 App: ${appName} (port ${resolvedPort})`);
|
|
2008
|
+
logger$5.log(`🚀 Starting entry file: ${entry} on port ${resolvedPort}`);
|
|
2009
|
+
if (Object.keys(credentials).length > 1) logger$5.log(`🔐 Loaded ${Object.keys(credentials).length - 1} secret(s) + PORT`);
|
|
2005
2010
|
const wrapperDir = join(process.cwd(), ".gkm");
|
|
2006
2011
|
await mkdir(wrapperDir, { recursive: true });
|
|
2007
2012
|
const wrapperPath = join(wrapperDir, "entry-wrapper.ts");
|
|
@@ -2012,7 +2017,7 @@ async function entryDevCommand(options) {
|
|
|
2012
2017
|
const shutdown = () => {
|
|
2013
2018
|
if (isShuttingDown) return;
|
|
2014
2019
|
isShuttingDown = true;
|
|
2015
|
-
logger$
|
|
2020
|
+
logger$5.log("\n🛑 Shutting down...");
|
|
2016
2021
|
runner.stop();
|
|
2017
2022
|
process.exit(0);
|
|
2018
2023
|
};
|
|
@@ -2048,14 +2053,14 @@ var EntryRunner = class {
|
|
|
2048
2053
|
});
|
|
2049
2054
|
let restartTimeout = null;
|
|
2050
2055
|
this.watcher.on("change", (path) => {
|
|
2051
|
-
logger$
|
|
2056
|
+
logger$5.log(`📝 File changed: ${path}`);
|
|
2052
2057
|
if (restartTimeout) clearTimeout(restartTimeout);
|
|
2053
2058
|
restartTimeout = setTimeout(async () => {
|
|
2054
|
-
logger$
|
|
2059
|
+
logger$5.log("🔄 Restarting...");
|
|
2055
2060
|
await this.restart();
|
|
2056
2061
|
}, 300);
|
|
2057
2062
|
});
|
|
2058
|
-
logger$
|
|
2063
|
+
logger$5.log(`👀 Watching for changes in: ${watchDir}`);
|
|
2059
2064
|
}
|
|
2060
2065
|
}
|
|
2061
2066
|
async runProcess() {
|
|
@@ -2071,17 +2076,17 @@ var EntryRunner = class {
|
|
|
2071
2076
|
});
|
|
2072
2077
|
this.isRunning = true;
|
|
2073
2078
|
this.childProcess.on("error", (error) => {
|
|
2074
|
-
logger$
|
|
2079
|
+
logger$5.error("❌ Process error:", error);
|
|
2075
2080
|
});
|
|
2076
2081
|
this.childProcess.on("exit", (code) => {
|
|
2077
|
-
if (code !== null && code !== 0 && code !== 143) logger$
|
|
2082
|
+
if (code !== null && code !== 0 && code !== 143) logger$5.error(`❌ Process exited with code ${code}`);
|
|
2078
2083
|
this.isRunning = false;
|
|
2079
2084
|
});
|
|
2080
2085
|
await new Promise((resolve) => setTimeout(resolve, 500));
|
|
2081
2086
|
if (this.isRunning) {
|
|
2082
|
-
logger$
|
|
2083
|
-
logger$
|
|
2084
|
-
logger$
|
|
2087
|
+
logger$5.log("");
|
|
2088
|
+
logger$5.log(` \x1b[32m✓ Ready\x1b[0m at \x1b[36mhttp://localhost:${this.port}\x1b[0m`);
|
|
2089
|
+
logger$5.log("");
|
|
2085
2090
|
}
|
|
2086
2091
|
}
|
|
2087
2092
|
async restart() {
|
|
@@ -2200,7 +2205,7 @@ start({
|
|
|
2200
2205
|
* lets a console read what this machine is running.
|
|
2201
2206
|
*/
|
|
2202
2207
|
function announce(discovery) {
|
|
2203
|
-
logger$
|
|
2208
|
+
logger$5.log(`🧭 Discovery: ${discovery.connectUrl}`);
|
|
2204
2209
|
}
|
|
2205
2210
|
/** Run `fn` with `console.log` muted — warnings and errors still print. */
|
|
2206
2211
|
async function quietly(fn) {
|
|
@@ -2318,7 +2323,7 @@ var DevServer = class {
|
|
|
2318
2323
|
this.actualPort = this.requestedPort;
|
|
2319
2324
|
} else {
|
|
2320
2325
|
this.actualPort = await findAvailablePort(this.requestedPort);
|
|
2321
|
-
if (this.actualPort !== this.requestedPort) logger$
|
|
2326
|
+
if (this.actualPort !== this.requestedPort) logger$5.log(`ℹ️ Port ${this.requestedPort} was in use, using port ${this.actualPort} instead`);
|
|
2322
2327
|
}
|
|
2323
2328
|
const serverEntryPath = join(this.appRoot, ".gkm", "server", "server.ts");
|
|
2324
2329
|
await this.createServerEntry();
|
|
@@ -2338,11 +2343,11 @@ var DevServer = class {
|
|
|
2338
2343
|
});
|
|
2339
2344
|
this.isRunning = true;
|
|
2340
2345
|
this.serverProcess.on("error", (error) => {
|
|
2341
|
-
logger$
|
|
2346
|
+
logger$5.error("❌ Server error:", error);
|
|
2342
2347
|
});
|
|
2343
2348
|
const child = this.serverProcess;
|
|
2344
2349
|
child.on("exit", (code, signal) => {
|
|
2345
|
-
if (code !== null && code !== 0 && signal !== "SIGTERM") logger$
|
|
2350
|
+
if (code !== null && code !== 0 && signal !== "SIGTERM") logger$5.error(`❌ Server exited with code ${code}`);
|
|
2346
2351
|
this.isRunning = false;
|
|
2347
2352
|
if (this.serverProcess === child) this.onStatus?.("stopped");
|
|
2348
2353
|
});
|
|
@@ -2351,13 +2356,13 @@ var DevServer = class {
|
|
|
2351
2356
|
const local = `http://localhost:${this.actualPort}`;
|
|
2352
2357
|
const address = this.publicUrl ? `${this.publicUrl} -> ${local}` : local;
|
|
2353
2358
|
const seconds = ((Date.now() - this.startTime) / 1e3).toFixed(1);
|
|
2354
|
-
logger$
|
|
2359
|
+
logger$5.log(`\x1b[32m✓\x1b[0m ${this.label} ready in ${seconds}s ${address}`);
|
|
2355
2360
|
const tools = this.selfServing ? [] : [
|
|
2356
2361
|
...this.enableOpenApi ? ["docs /__docs"] : [],
|
|
2357
2362
|
...this.telescope ? [`telescope ${this.telescope.path}`] : [],
|
|
2358
2363
|
...this.databaseApi ? [`db ${this.databaseApi}`] : []
|
|
2359
2364
|
];
|
|
2360
|
-
if (tools.length > 0) logger$
|
|
2365
|
+
if (tools.length > 0) logger$5.log(` ${tools.join(" · ")}`);
|
|
2361
2366
|
this.onStatus?.("ready");
|
|
2362
2367
|
}
|
|
2363
2368
|
}
|
|
@@ -3053,7 +3058,7 @@ ${shutdown}`);
|
|
|
3053
3058
|
}
|
|
3054
3059
|
//#endregion
|
|
3055
3060
|
//#region src/build/index.ts
|
|
3056
|
-
const logger$
|
|
3061
|
+
const logger$4 = console;
|
|
3057
3062
|
/**
|
|
3058
3063
|
* Where `gkm build --stage` leaves the key its bundle's secrets were encrypted
|
|
3059
3064
|
* with, beside the bundle in `.gkm/server/` and readable by its owner alone.
|
|
@@ -3101,7 +3106,7 @@ async function buildCommand(options) {
|
|
|
3101
3106
|
const isAtWorkspaceRoot = cwd === workspaceRoot;
|
|
3102
3107
|
const someAppIsElsewhere = Object.values(loadedConfig.workspace.apps).some((app) => resolve(workspaceRoot, app.path) !== workspaceRoot);
|
|
3103
3108
|
if (isAtWorkspaceRoot && someAppIsElsewhere) {
|
|
3104
|
-
logger$
|
|
3109
|
+
logger$4.log("📦 Detected workspace configuration");
|
|
3105
3110
|
return workspaceBuildCommand(loadedConfig.workspace, options);
|
|
3106
3111
|
}
|
|
3107
3112
|
}
|
|
@@ -3147,13 +3152,13 @@ async function buildOneApp(input) {
|
|
|
3147
3152
|
const { config, options, workspace, appRoot, target } = input;
|
|
3148
3153
|
const cacheBackend = cacheBackendFor(providerOf(workspace));
|
|
3149
3154
|
const production = normalizeProductionConfig(options.production ?? false);
|
|
3150
|
-
if (production) logger$
|
|
3151
|
-
logger$
|
|
3152
|
-
logger$
|
|
3155
|
+
if (production) logger$4.log(`🏭 Building for PRODUCTION`);
|
|
3156
|
+
logger$4.log(`Building for ${target}`);
|
|
3157
|
+
logger$4.log(`Loading constructs from: ${formatRoutes(config.constructs)}`);
|
|
3153
3158
|
const telescope = production ? void 0 : normalizeTelescopeConfig(config.telescope);
|
|
3154
|
-
if (telescope) logger$
|
|
3159
|
+
if (telescope) logger$4.log(`🔭 Telescope enabled at ${telescope.path}`);
|
|
3155
3160
|
const hooks = normalizeHooksConfig(config.hooks, appRoot);
|
|
3156
|
-
if (hooks) logger$
|
|
3161
|
+
if (hooks) logger$4.log(`🪝 Server hooks enabled`);
|
|
3157
3162
|
return buildApp({
|
|
3158
3163
|
config,
|
|
3159
3164
|
workspaceRoot: workspace.root,
|
|
@@ -3195,7 +3200,7 @@ async function writeManifest(input) {
|
|
|
3195
3200
|
});
|
|
3196
3201
|
const path = join(manifestDir, `${target}.ts`);
|
|
3197
3202
|
await writeFile(path, manifestModule(folded, backends));
|
|
3198
|
-
logger$
|
|
3203
|
+
logger$4.log(`Manifest: ${relative(process.cwd(), path)}`);
|
|
3199
3204
|
}
|
|
3200
3205
|
/**
|
|
3201
3206
|
* Turn one app's constructs into its generated entry.
|
|
@@ -3223,11 +3228,11 @@ async function buildApp(input) {
|
|
|
3223
3228
|
});
|
|
3224
3229
|
if (selfServing) {
|
|
3225
3230
|
await writeSurfaceEntry(join(appRoot, ".gkm", "server"), selfServing.source);
|
|
3226
|
-
logger$
|
|
3231
|
+
logger$4.log(`Generated a server for ${selfServing.id} from its own declaration`);
|
|
3227
3232
|
if (production?.bundle && !input.skipBundle) {
|
|
3228
3233
|
const outputDir = join(appRoot, ".gkm", "server");
|
|
3229
3234
|
const entryPoint = await writeSurfaceServer(outputDir, production);
|
|
3230
|
-
const { bundleServer } = await import("./bundler-
|
|
3235
|
+
const { bundleServer } = await import("./bundler-D_14JLZF.mjs");
|
|
3231
3236
|
await bundleServer({
|
|
3232
3237
|
entryPoint,
|
|
3233
3238
|
outputDir: join(outputDir, "dist"),
|
|
@@ -3236,7 +3241,7 @@ async function buildApp(input) {
|
|
|
3236
3241
|
external: production.external,
|
|
3237
3242
|
...input.stage ? { stage: input.stage } : {}
|
|
3238
3243
|
});
|
|
3239
|
-
logger$
|
|
3244
|
+
logger$4.log(`✅ Bundle complete: .gkm/server/dist/server.mjs`);
|
|
3240
3245
|
}
|
|
3241
3246
|
return { selfServing: selfServing.id };
|
|
3242
3247
|
}
|
|
@@ -3282,14 +3287,14 @@ async function buildApp(input) {
|
|
|
3282
3287
|
topicGenerator.load(code, appRoot, bustCache)
|
|
3283
3288
|
]);
|
|
3284
3289
|
const allEndpoints = servedBy(loadedEndpoints, derived.surface);
|
|
3285
|
-
logger$
|
|
3286
|
-
logger$
|
|
3287
|
-
logger$
|
|
3288
|
-
logger$
|
|
3289
|
-
logger$
|
|
3290
|
-
logger$
|
|
3290
|
+
logger$4.log(`Found ${allEndpoints.length} endpoints`);
|
|
3291
|
+
logger$4.log(`Found ${allFunctions.length} functions`);
|
|
3292
|
+
logger$4.log(`Found ${allCrons.length} crons`);
|
|
3293
|
+
logger$4.log(`Found ${allSubscribers.length} subscribers`);
|
|
3294
|
+
logger$4.log(`Found ${allQueues.length} queues`);
|
|
3295
|
+
logger$4.log(`Found ${allTopics.length} topics`);
|
|
3291
3296
|
if (!input.serveEmpty && allEndpoints.length === 0 && allFunctions.length === 0 && allCrons.length === 0 && allSubscribers.length === 0 && allQueues.length === 0 && allTopics.length === 0) {
|
|
3292
|
-
logger$
|
|
3297
|
+
logger$4.log("No endpoints, functions, crons, subscribers, queues, or topics found to process");
|
|
3293
3298
|
return {};
|
|
3294
3299
|
}
|
|
3295
3300
|
if (production?.subscribers === "include" && derived.surface) {
|
|
@@ -3302,7 +3307,7 @@ async function buildApp(input) {
|
|
|
3302
3307
|
[allQueues.length, "queue consumer"],
|
|
3303
3308
|
[allSubscribers.length, "subscriber"]
|
|
3304
3309
|
].filter(([n]) => n > 0).map(([n, what]) => `${n} ${what}${n === 1 ? "" : "s"}`);
|
|
3305
|
-
if (left.length > 0) logger$
|
|
3310
|
+
if (left.length > 0) logger$4.log(`Serving ${derived.surface.id} only: leaving out ${left.join(", ")} — they run in a Worker's image`);
|
|
3306
3311
|
}
|
|
3307
3312
|
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);
|
|
3308
3313
|
await generateOpenApi(allEndpoints.map(({ construct }) => construct), {
|
|
@@ -3325,7 +3330,7 @@ async function buildForTarget(target, context, appRoot, workspaceRoot, endpointG
|
|
|
3325
3330
|
force: true
|
|
3326
3331
|
});
|
|
3327
3332
|
await mkdir(outputDir, { recursive: true });
|
|
3328
|
-
logger$
|
|
3333
|
+
logger$4.log(`\nGenerating handlers for ${target}`);
|
|
3329
3334
|
const options = {
|
|
3330
3335
|
target,
|
|
3331
3336
|
root: workspaceRoot
|
|
@@ -3341,7 +3346,7 @@ async function buildForTarget(target, context, appRoot, workspaceRoot, endpointG
|
|
|
3341
3346
|
queueGenerator.build(context, queues, outputDir, options),
|
|
3342
3347
|
topicGenerator.build(context, topics, outputDir, options)
|
|
3343
3348
|
]);
|
|
3344
|
-
logger$
|
|
3349
|
+
logger$4.log(`Generated ${routes.length} routes, ${functionInfos.length} functions, ${cronInfos.length} crons, ${subscriberInfos.length} subscribers, ${queueInfos.length} queues, ${topicInfos.length} topics for ${target}`);
|
|
3345
3350
|
const fields = {
|
|
3346
3351
|
...context.surface ? { surface: context.surface.id } : {},
|
|
3347
3352
|
functions: functionInfos,
|
|
@@ -3364,8 +3369,8 @@ async function buildForTarget(target, context, appRoot, workspaceRoot, endpointG
|
|
|
3364
3369
|
};
|
|
3365
3370
|
let masterKey;
|
|
3366
3371
|
if (context.production?.bundle && !skipBundle) {
|
|
3367
|
-
logger$
|
|
3368
|
-
const { bundleServer } = await import("./bundler-
|
|
3372
|
+
logger$4.log(`\n📦 Bundling production server...`);
|
|
3373
|
+
const { bundleServer } = await import("./bundler-D_14JLZF.mjs");
|
|
3369
3374
|
const allConstructs = [
|
|
3370
3375
|
...endpoints.map((e) => e.construct),
|
|
3371
3376
|
...functions.map((f) => f.construct),
|
|
@@ -3382,13 +3387,13 @@ async function buildForTarget(target, context, appRoot, workspaceRoot, endpointG
|
|
|
3382
3387
|
stage,
|
|
3383
3388
|
constructs: allConstructs
|
|
3384
3389
|
})).masterKey;
|
|
3385
|
-
logger$
|
|
3390
|
+
logger$4.log(`✅ Bundle complete: .gkm/server/dist/server.mjs`);
|
|
3386
3391
|
if (masterKey) {
|
|
3387
3392
|
const keyPath = join(outputDir, MASTER_KEY_FILE);
|
|
3388
3393
|
await writeFile(keyPath, `${masterKey}\n`, { mode: 384 });
|
|
3389
3394
|
await chmod(keyPath, 384);
|
|
3390
|
-
logger$
|
|
3391
|
-
logger$
|
|
3395
|
+
logger$4.log(`\n🔐 Secrets encrypted for deployment`);
|
|
3396
|
+
logger$4.log(` Run with GKM_MASTER_KEY set to the key in ${relative(appRoot, keyPath)} (fingerprint ${keyFingerprint(masterKey)})`);
|
|
3392
3397
|
}
|
|
3393
3398
|
}
|
|
3394
3399
|
return {
|
|
@@ -3405,7 +3410,7 @@ async function buildForTarget(target, context, appRoot, workspaceRoot, endpointG
|
|
|
3405
3410
|
* Detect available package manager.
|
|
3406
3411
|
* @internal Exported for testing
|
|
3407
3412
|
*/
|
|
3408
|
-
function detectPackageManager
|
|
3413
|
+
function detectPackageManager() {
|
|
3409
3414
|
if (existsSync("pnpm-lock.yaml")) return "pnpm";
|
|
3410
3415
|
if (existsSync("yarn.lock")) return "yarn";
|
|
3411
3416
|
return "npm";
|
|
@@ -3489,12 +3494,12 @@ async function workspaceBuildCommand(workspace, options) {
|
|
|
3489
3494
|
const apps = Object.entries(workspace.apps);
|
|
3490
3495
|
const backendApps = apps.filter(([, app]) => app.type === "backend");
|
|
3491
3496
|
const frontendApps = apps.filter(([, app]) => app.type === "web");
|
|
3492
|
-
logger$
|
|
3493
|
-
logger$
|
|
3494
|
-
logger$
|
|
3495
|
-
if (options.production) logger$
|
|
3497
|
+
logger$4.log(`\n🏗️ Building workspace: ${workspace.name}`);
|
|
3498
|
+
logger$4.log(` Backend apps: ${backendApps.map(([name]) => name).join(", ") || "none"}`);
|
|
3499
|
+
logger$4.log(` Frontend apps: ${frontendApps.map(([name]) => name).join(", ") || "none"}`);
|
|
3500
|
+
if (options.production) logger$4.log(` 🏭 Production mode enabled`);
|
|
3496
3501
|
const buildOrder = getAppBuildOrder(workspace);
|
|
3497
|
-
logger$
|
|
3502
|
+
logger$4.log(` Build order: ${buildOrder.join(" → ")}`);
|
|
3498
3503
|
try {
|
|
3499
3504
|
const target = options.provider ?? providerOf(workspace);
|
|
3500
3505
|
const builds = [];
|
|
@@ -3502,7 +3507,7 @@ async function workspaceBuildCommand(workspace, options) {
|
|
|
3502
3507
|
const app = workspace.apps[appName];
|
|
3503
3508
|
const config = getAppGkmConfig(workspace, appName);
|
|
3504
3509
|
if (!app || app.type !== "backend" || !config) continue;
|
|
3505
|
-
logger$
|
|
3510
|
+
logger$4.log(`\n⚙️ ${appName}`);
|
|
3506
3511
|
const output = await buildOneApp({
|
|
3507
3512
|
config,
|
|
3508
3513
|
options,
|
|
@@ -3524,11 +3529,11 @@ async function workspaceBuildCommand(workspace, options) {
|
|
|
3524
3529
|
});
|
|
3525
3530
|
const { filters, unpackaged } = turboFilters(workspace, { exclude: "backend" });
|
|
3526
3531
|
if (unpackaged.length > 0) throw new Error(`No package.json for workspace app(s): ${unpackaged.join(", ")}. Each app needs one — turbo builds packages, and gkm reads the name back to know which app it is building.`);
|
|
3527
|
-
const pm = detectPackageManager
|
|
3532
|
+
const pm = detectPackageManager();
|
|
3528
3533
|
const [turboBin, turboArgs] = getTurboArgs(pm, filters, { only: true });
|
|
3529
3534
|
if (filters.length > 0) {
|
|
3530
|
-
logger$
|
|
3531
|
-
logger$
|
|
3535
|
+
logger$4.log(`\n📦 Using ${pm} with Turbo for the other apps...\n`);
|
|
3536
|
+
logger$4.log(`Running: ${[turboBin, ...turboArgs].join(" ")}`);
|
|
3532
3537
|
const sandbox = options.sandbox ?? new LocalSandbox({
|
|
3533
3538
|
root: workspace.root,
|
|
3534
3539
|
passEnv: ["TURBO_TOKEN"]
|
|
@@ -3553,15 +3558,15 @@ async function workspaceBuildCommand(workspace, options) {
|
|
|
3553
3558
|
outputPath
|
|
3554
3559
|
});
|
|
3555
3560
|
}
|
|
3556
|
-
logger$
|
|
3557
|
-
logger$
|
|
3561
|
+
logger$4.log(`\n✅ Workspace build complete!`);
|
|
3562
|
+
logger$4.log(`\n📋 Build Summary:`);
|
|
3558
3563
|
for (const result of results) {
|
|
3559
3564
|
const icon = result.type === "backend" ? "⚙️" : result.type === "mobile" ? "📱" : "🌐";
|
|
3560
|
-
logger$
|
|
3565
|
+
logger$4.log(` ${icon} ${result.appName}: ${result.outputPath || "built"}`);
|
|
3561
3566
|
}
|
|
3562
3567
|
} catch (error) {
|
|
3563
3568
|
const errorMessage = error instanceof Error ? error.message : "Build failed";
|
|
3564
|
-
logger$
|
|
3569
|
+
logger$4.log(`\n❌ Build failed: ${errorMessage}`);
|
|
3565
3570
|
for (const [appName, app] of apps) results.push({
|
|
3566
3571
|
appName,
|
|
3567
3572
|
type: app.type,
|
|
@@ -3591,1169 +3596,17 @@ function formatRoutes(routes) {
|
|
|
3591
3596
|
return Array.isArray(routes) ? routes.join(", ") : routes;
|
|
3592
3597
|
}
|
|
3593
3598
|
//#endregion
|
|
3594
|
-
//#region src/migrate/provider.ts
|
|
3595
|
-
/**
|
|
3596
|
-
* One construct's migrations and seeds, read from its folders.
|
|
3597
|
-
*
|
|
3598
|
-
* Two kinds of file, told apart by extension rather than by configuration: a
|
|
3599
|
-
* script exports `up` (and may export `down`) — or `seed(db, { stage })`, in
|
|
3600
|
-
* `seeds/` — and a
|
|
3601
|
-
* `.sql` file is run whole. The second is what makes a folder independent of
|
|
3602
|
-
* the client the app uses — Better Auth's schema is SQL whatever the
|
|
3603
|
-
* application queries with.
|
|
3604
|
-
*/
|
|
3605
|
-
const SCRIPTS = /* @__PURE__ */ new Set([
|
|
3606
|
-
".ts",
|
|
3607
|
-
".mts",
|
|
3608
|
-
".js",
|
|
3609
|
-
".mjs"
|
|
3610
|
-
]);
|
|
3611
|
-
/** Two files in one folder that would be one migration. */
|
|
3612
|
-
var DuplicateMigration = class extends Error {
|
|
3613
|
-
folder;
|
|
3614
|
-
migration;
|
|
3615
|
-
constructor(folder, migration) {
|
|
3616
|
-
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.`);
|
|
3617
|
-
this.folder = folder;
|
|
3618
|
-
this.migration = migration;
|
|
3619
|
-
this.name = "DuplicateMigration";
|
|
3620
|
-
}
|
|
3621
|
-
};
|
|
3622
|
-
/** A script in a migrations folder that exports no `up`. */
|
|
3623
|
-
var MigrationHasNoUp = class extends Error {
|
|
3624
|
-
file;
|
|
3625
|
-
constructor(file) {
|
|
3626
|
-
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.`);
|
|
3627
|
-
this.file = file;
|
|
3628
|
-
this.name = "MigrationHasNoUp";
|
|
3629
|
-
}
|
|
3630
|
-
};
|
|
3631
|
-
/** A script in a seeds folder that exports no `seed`. */
|
|
3632
|
-
var SeedHasNoSeed = class extends Error {
|
|
3633
|
-
file;
|
|
3634
|
-
constructor(file) {
|
|
3635
|
-
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.`);
|
|
3636
|
-
this.file = file;
|
|
3637
|
-
this.name = "SeedHasNoSeed";
|
|
3638
|
-
}
|
|
3639
|
-
};
|
|
3640
|
-
/**
|
|
3641
|
-
* The migration files in a folder, by name, without reading them.
|
|
3642
|
-
*
|
|
3643
|
-
* Also what `gkm dev` compares against a construct's history to report what
|
|
3644
|
-
* is pending, which is why it is separate from loading them.
|
|
3645
|
-
*/
|
|
3646
|
-
async function migrationFiles(folder) {
|
|
3647
|
-
const entries = await readdir(folder, { withFileTypes: true }).catch((error) => {
|
|
3648
|
-
if (error.code === "ENOENT") return [];
|
|
3649
|
-
throw error;
|
|
3650
|
-
});
|
|
3651
|
-
const files = /* @__PURE__ */ new Map();
|
|
3652
|
-
for (const entry of entries) {
|
|
3653
|
-
if (!entry.isFile() || !isMigration(entry.name)) continue;
|
|
3654
|
-
const name = entry.name.slice(0, -extname(entry.name).length);
|
|
3655
|
-
if (files.has(name)) throw new DuplicateMigration(folder, name);
|
|
3656
|
-
files.set(name, join(folder, entry.name));
|
|
3657
|
-
}
|
|
3658
|
-
return files;
|
|
3659
|
-
}
|
|
3660
|
-
/** A Kysely provider over one construct's folder. */
|
|
3661
|
-
function folderProvider(folder, raw) {
|
|
3662
|
-
return { async getMigrations() {
|
|
3663
|
-
const migrations = {};
|
|
3664
|
-
for (const [name, file] of await migrationFiles(folder)) migrations[name] = extname(file) === ".sql" ? await sqlMigration(file, raw) : await scriptMigration(file);
|
|
3665
|
-
return migrations;
|
|
3666
|
-
} };
|
|
3667
|
-
}
|
|
3668
|
-
/**
|
|
3669
|
-
* The seeds in a folder, loaded, in name order.
|
|
3670
|
-
*
|
|
3671
|
-
* Every one runs on every pass — there is no history to consult — so the order
|
|
3672
|
-
* is the only thing between two seeds where one reads what another writes.
|
|
3673
|
-
* Name them to sort that way.
|
|
3674
|
-
*/
|
|
3675
|
-
async function loadSeeds(folder, raw) {
|
|
3676
|
-
const files = [...await migrationFiles(folder)].sort(([a], [b]) => a < b ? -1 : a > b ? 1 : 0);
|
|
3677
|
-
const seeds = [];
|
|
3678
|
-
for (const [name, file] of files) seeds.push({
|
|
3679
|
-
name,
|
|
3680
|
-
run: extname(file) === ".sql" ? (await sqlMigration(file, raw)).up : await scriptSeed(file)
|
|
3681
|
-
});
|
|
3682
|
-
return seeds;
|
|
3683
|
-
}
|
|
3684
|
-
/**
|
|
3685
|
-
* A seed script, imported as it is on disk now.
|
|
3686
|
-
*
|
|
3687
|
-
* Keyed by its modification time: a seed is run again after it is edited, and
|
|
3688
|
-
* a process that has imported it once would otherwise run the old one.
|
|
3689
|
-
*/
|
|
3690
|
-
async function scriptSeed(file) {
|
|
3691
|
-
const url = pathToFileURL(file);
|
|
3692
|
-
url.searchParams.set("mtime", String((await stat(file)).mtimeMs));
|
|
3693
|
-
const module = await import(url.href);
|
|
3694
|
-
if (typeof module.seed !== "function") throw new SeedHasNoSeed(file);
|
|
3695
|
-
return module.seed;
|
|
3696
|
-
}
|
|
3697
|
-
function isMigration(file) {
|
|
3698
|
-
if (file.endsWith(".d.ts")) return false;
|
|
3699
|
-
if (/\.(spec|test)\.[cm]?[jt]s$/.test(file)) return false;
|
|
3700
|
-
const extension = extname(file);
|
|
3701
|
-
return extension === ".sql" || SCRIPTS.has(extension);
|
|
3702
|
-
}
|
|
3703
|
-
async function scriptMigration(file) {
|
|
3704
|
-
const module = await import(pathToFileURL(file).href);
|
|
3705
|
-
if (typeof module.up !== "function") throw new MigrationHasNoUp(file);
|
|
3706
|
-
return {
|
|
3707
|
-
up: module.up,
|
|
3708
|
-
...typeof module.down === "function" ? { down: module.down } : {}
|
|
3709
|
-
};
|
|
3710
|
-
}
|
|
3711
|
-
/**
|
|
3712
|
-
* A `.sql` file, run whole.
|
|
3713
|
-
*
|
|
3714
|
-
* One query rather than split on `;`: with no parameters, Postgres takes
|
|
3715
|
-
* several statements in one message, and splitting by hand breaks on the first
|
|
3716
|
-
* `;` inside a string or a function body. There is no `down` — a generated
|
|
3717
|
-
* schema is rolled back by a migration that says how, not by guessing.
|
|
3718
|
-
*/
|
|
3719
|
-
async function sqlMigration(file, raw) {
|
|
3720
|
-
const statements = await readFile(file, "utf-8");
|
|
3721
|
-
return { up: async (db) => {
|
|
3722
|
-
await raw.raw(statements).execute(db);
|
|
3723
|
-
} };
|
|
3724
|
-
}
|
|
3725
|
-
//#endregion
|
|
3726
|
-
//#region src/migrate/databases.ts
|
|
3727
|
-
/**
|
|
3728
|
-
* Bring each construct's schema up to its migrations folder.
|
|
3729
|
-
*
|
|
3730
|
-
* Which constructs, in what order, from which folder, is `migrationTargets`'
|
|
3731
|
-
* answer and nobody else's. What is decided here is only how one target is
|
|
3732
|
-
* run: as its owner, in its own schema, with a history of its own.
|
|
3733
|
-
*/
|
|
3734
|
-
/** A folder under `db/` that names no construct. */
|
|
3735
|
-
var UnknownMigrationFolder = class extends Error {
|
|
3736
|
-
folder;
|
|
3737
|
-
known;
|
|
3738
|
-
constructor(folder, known) {
|
|
3739
|
-
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.`);
|
|
3740
|
-
this.folder = folder;
|
|
3741
|
-
this.known = known;
|
|
3742
|
-
this.name = "UnknownMigrationFolder";
|
|
3743
|
-
}
|
|
3744
|
-
};
|
|
3745
|
-
/** A construct was asked for by a name that takes no migrations. */
|
|
3746
|
-
var NoSuchMigrationTarget = class extends Error {
|
|
3747
|
-
construct;
|
|
3748
|
-
known;
|
|
3749
|
-
constructor(construct, known) {
|
|
3750
|
-
super(`'${construct}' is not a database construct. ` + (known.length > 0 ? `The ones with migrations: ${known.join(", ")}.` : "This project declares no database."));
|
|
3751
|
-
this.construct = construct;
|
|
3752
|
-
this.known = known;
|
|
3753
|
-
this.name = "NoSuchMigrationTarget";
|
|
3754
|
-
}
|
|
3755
|
-
};
|
|
3756
|
-
/**
|
|
3757
|
-
* A migration left directly in a construct's folder, where nothing reads it.
|
|
3758
|
-
*
|
|
3759
|
-
* Migrations moved into `migrations/` so seeds could sit beside them. A file
|
|
3760
|
-
* still at the old level would otherwise never run, and nothing would say so.
|
|
3761
|
-
*/
|
|
3762
|
-
var MigrationsOutsideFolder = class extends Error {
|
|
3763
|
-
folder;
|
|
3764
|
-
files;
|
|
3765
|
-
constructor(folder, files) {
|
|
3766
|
-
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.`);
|
|
3767
|
-
this.folder = folder;
|
|
3768
|
-
this.files = files;
|
|
3769
|
-
this.name = "MigrationsOutsideFolder";
|
|
3770
|
-
}
|
|
3771
|
-
};
|
|
3772
|
-
/** A folder inside a construct's folder that is neither `migrations` nor `seeds`. */
|
|
3773
|
-
var UnknownDatabaseFolder = class extends Error {
|
|
3774
|
-
folder;
|
|
3775
|
-
constructor(folder) {
|
|
3776
|
-
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.`);
|
|
3777
|
-
this.folder = folder;
|
|
3778
|
-
this.name = "UnknownDatabaseFolder";
|
|
3779
|
-
}
|
|
3780
|
-
};
|
|
3781
|
-
/** A seed failed; its own writes are rolled back, the ones before it stay. */
|
|
3782
|
-
var SeedFailed = class extends Error {
|
|
3783
|
-
construct;
|
|
3784
|
-
seed;
|
|
3785
|
-
cause;
|
|
3786
|
-
constructor(construct, seed, cause) {
|
|
3787
|
-
super(`${construct}: seed '${seed}' failed — ${cause instanceof Error ? cause.message : String(cause)}. Its writes were rolled back; fix it and run it again.`);
|
|
3788
|
-
this.construct = construct;
|
|
3789
|
-
this.seed = seed;
|
|
3790
|
-
this.cause = cause;
|
|
3791
|
-
this.name = "SeedFailed";
|
|
3792
|
-
}
|
|
3793
|
-
};
|
|
3794
|
-
/** A construct with roles has no owner URL to migrate with. */
|
|
3795
|
-
var NoOwnerCredential = class extends Error {
|
|
3796
|
-
construct;
|
|
3797
|
-
key;
|
|
3798
|
-
constructor(construct, key) {
|
|
3799
|
-
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?`);
|
|
3800
|
-
this.construct = construct;
|
|
3801
|
-
this.key = key;
|
|
3802
|
-
this.name = "NoOwnerCredential";
|
|
3803
|
-
}
|
|
3804
|
-
};
|
|
3805
|
-
/** A migration failed; the ones before it in the run stay applied. */
|
|
3806
|
-
var MigrationFailed = class extends Error {
|
|
3807
|
-
construct;
|
|
3808
|
-
migration;
|
|
3809
|
-
cause;
|
|
3810
|
-
constructor(construct, migration, cause) {
|
|
3811
|
-
super(`${construct}: migration ${migration ? `'${migration}' ` : ""}failed — ${cause instanceof Error ? cause.message : String(cause)}`);
|
|
3812
|
-
this.construct = construct;
|
|
3813
|
-
this.migration = migration;
|
|
3814
|
-
this.cause = cause;
|
|
3815
|
-
this.name = "MigrationFailed";
|
|
3816
|
-
}
|
|
3817
|
-
};
|
|
3818
|
-
/** The project's Kysely could not be found from where a database is declared. */
|
|
3819
|
-
var KyselyNotFound = class extends Error {
|
|
3820
|
-
from;
|
|
3821
|
-
constructor(from) {
|
|
3822
|
-
super(`Could not resolve 'kysely' from ${from}. Migrations run with the project's own Kysely — install it where the database is declared.`);
|
|
3823
|
-
this.from = from;
|
|
3824
|
-
this.name = "KyselyNotFound";
|
|
3825
|
-
}
|
|
3826
|
-
};
|
|
3827
|
-
/** Apply every pending migration, construct by construct, parents first. */
|
|
3828
|
-
async function migrateDatabases(options) {
|
|
3829
|
-
const runs = [];
|
|
3830
|
-
for (const target of await targetsFor(options)) {
|
|
3831
|
-
if ((await migrationFiles(join(options.root, target.migrations))).size === 0) {
|
|
3832
|
-
runs.push({
|
|
3833
|
-
target,
|
|
3834
|
-
applied: []
|
|
3835
|
-
});
|
|
3836
|
-
continue;
|
|
3837
|
-
}
|
|
3838
|
-
const applied = await withMigrator(options, target, async (migrator) => {
|
|
3839
|
-
const { error, results = [] } = await migrator.migrateToLatest();
|
|
3840
|
-
if (error) {
|
|
3841
|
-
const failed = results.find((result) => result.status === "Error");
|
|
3842
|
-
throw new MigrationFailed(target.id, failed?.migrationName, error);
|
|
3843
|
-
}
|
|
3844
|
-
return results.filter((result) => result.status === "Success").map((result) => result.migrationName);
|
|
3845
|
-
});
|
|
3846
|
-
runs.push({
|
|
3847
|
-
target,
|
|
3848
|
-
applied
|
|
3849
|
-
});
|
|
3850
|
-
}
|
|
3851
|
-
return runs;
|
|
3852
|
-
}
|
|
3853
|
-
/**
|
|
3854
|
-
* Run every construct's seeds, parents first — each one, every time, on every
|
|
3855
|
-
* stage, production included. Each is handed the stage, so a seed decides
|
|
3856
|
-
* what belongs where.
|
|
3857
|
-
*
|
|
3858
|
-
* Seeds are reference data kept up to date by being run again, so there is no
|
|
3859
|
-
* history: a seed is an upsert, and a changed one is applied by the next run.
|
|
3860
|
-
* Each runs in its own transaction, as the construct's owner, so a failure
|
|
3861
|
-
* leaves nothing half-written. Assumes the schema is migrated —
|
|
3862
|
-
* {@link migrateAndSeed} is what makes that true.
|
|
3863
|
-
*/
|
|
3864
|
-
async function seedDatabases(options) {
|
|
3865
|
-
const runs = [];
|
|
3866
|
-
for (const target of await targetsFor(options)) {
|
|
3867
|
-
const folder = join(options.root, target.seeds);
|
|
3868
|
-
if ((await migrationFiles(folder)).size === 0) {
|
|
3869
|
-
runs.push({
|
|
3870
|
-
target,
|
|
3871
|
-
seeded: []
|
|
3872
|
-
});
|
|
3873
|
-
continue;
|
|
3874
|
-
}
|
|
3875
|
-
const seeded = await withOwner(options, target, async (db, kysely) => {
|
|
3876
|
-
const names = [];
|
|
3877
|
-
for (const seed of await loadSeeds(folder, kysely.sql)) {
|
|
3878
|
-
try {
|
|
3879
|
-
await db.transaction().execute((trx) => seed.run(trx, { stage: options.stage }));
|
|
3880
|
-
} catch (error) {
|
|
3881
|
-
throw new SeedFailed(target.id, seed.name, error);
|
|
3882
|
-
}
|
|
3883
|
-
names.push(seed.name);
|
|
3884
|
-
}
|
|
3885
|
-
return names;
|
|
3886
|
-
});
|
|
3887
|
-
runs.push({
|
|
3888
|
-
target,
|
|
3889
|
-
seeded
|
|
3890
|
-
});
|
|
3891
|
-
}
|
|
3892
|
-
return runs;
|
|
3893
|
-
}
|
|
3894
|
-
/**
|
|
3895
|
-
* Migrate, then seed: a seed only ever runs against the schema it was written
|
|
3896
|
-
* for.
|
|
3897
|
-
*
|
|
3898
|
-
* With `only`, the constructs it lives in are migrated too — a tenant's seed
|
|
3899
|
-
* cannot run before the database that owns its schema exists — and only it is
|
|
3900
|
-
* seeded.
|
|
3901
|
-
*/
|
|
3902
|
-
async function migrateAndSeed(options) {
|
|
3903
|
-
const migrations = [];
|
|
3904
|
-
for (const target of await withParents(options)) migrations.push(...await migrateDatabases({
|
|
3905
|
-
...options,
|
|
3906
|
-
only: target.id
|
|
3907
|
-
}));
|
|
3908
|
-
return {
|
|
3909
|
-
migrations,
|
|
3910
|
-
seeds: await seedDatabases(options)
|
|
3911
|
-
};
|
|
3912
|
-
}
|
|
3913
|
-
/** The targets `only` names, after the ones it lives in; all without it. */
|
|
3914
|
-
async function withParents(options) {
|
|
3915
|
-
const chosen = await targetsFor(options);
|
|
3916
|
-
if (!options.only) return chosen;
|
|
3917
|
-
const all = migrationTargets(options.manifest);
|
|
3918
|
-
const ids = /* @__PURE__ */ new Set();
|
|
3919
|
-
for (const target of chosen) {
|
|
3920
|
-
let current = target;
|
|
3921
|
-
while (current) {
|
|
3922
|
-
ids.add(current.id);
|
|
3923
|
-
const parent = current.of;
|
|
3924
|
-
current = parent ? all.find((t) => t.id === parent) : void 0;
|
|
3925
|
-
}
|
|
3926
|
-
}
|
|
3927
|
-
return all.filter((target) => ids.has(target.id));
|
|
3928
|
-
}
|
|
3929
|
-
/**
|
|
3930
|
-
* What each construct's folder has that its history does not — without
|
|
3931
|
-
* applying any of it. What `gkm dev` reports.
|
|
3932
|
-
*/
|
|
3933
|
-
async function pendingMigrations(options) {
|
|
3934
|
-
const found = [];
|
|
3935
|
-
for (const target of await targetsFor(options)) {
|
|
3936
|
-
if ((await migrationFiles(join(options.root, target.migrations))).size === 0) continue;
|
|
3937
|
-
const pending = await withMigrator(options, target, async (migrator) => (await migrator.getMigrations()).filter((migration) => migration.executedAt === void 0).map((migration) => migration.name));
|
|
3938
|
-
if (pending.length > 0) found.push({
|
|
3939
|
-
target,
|
|
3940
|
-
pending
|
|
3941
|
-
});
|
|
3942
|
-
}
|
|
3943
|
-
return found;
|
|
3944
|
-
}
|
|
3945
|
-
/**
|
|
3946
|
-
* The targets this run covers, with every folder under `db/` accounted for.
|
|
3947
|
-
*
|
|
3948
|
-
* A folder that names no construct is an error rather than something to skip:
|
|
3949
|
-
* a typo in its name is otherwise a migration that silently never runs.
|
|
3950
|
-
*/
|
|
3951
|
-
async function targetsFor(options) {
|
|
3952
|
-
const targets = migrationTargets(options.manifest);
|
|
3953
|
-
const folders = targets.map((target) => target.folder);
|
|
3954
|
-
for (const folder of await foldersIn(join(options.root, MIGRATIONS_ROOT))) {
|
|
3955
|
-
const relative = `${MIGRATIONS_ROOT}/${folder}`;
|
|
3956
|
-
if (!folders.includes(relative)) throw new UnknownMigrationFolder(relative, folders);
|
|
3957
|
-
await checkLayout(options.root, relative);
|
|
3958
|
-
}
|
|
3959
|
-
if (!options.only) return targets;
|
|
3960
|
-
const id = canonicalId(options.only);
|
|
3961
|
-
const chosen = targets.filter((target) => target.id === id);
|
|
3962
|
-
if (chosen.length === 0) throw new NoSuchMigrationTarget(options.only, targets.map((target) => target.id));
|
|
3963
|
-
return chosen;
|
|
3964
|
-
}
|
|
3965
|
-
/**
|
|
3966
|
-
* A construct's folder holds `migrations/` and `seeds/`, and nothing else —
|
|
3967
|
-
* anything beside them is a file or folder nothing would ever run.
|
|
3968
|
-
*/
|
|
3969
|
-
async function checkLayout(root, folder) {
|
|
3970
|
-
const entries = await readdir(join(root, folder), { withFileTypes: true });
|
|
3971
|
-
const loose = entries.filter((entry) => entry.isFile() && !entry.name.startsWith(".")).map((entry) => entry.name);
|
|
3972
|
-
if (loose.length > 0) throw new MigrationsOutsideFolder(folder, loose);
|
|
3973
|
-
for (const entry of entries) if (entry.isDirectory() && !DATABASE_FOLDERS.includes(entry.name)) throw new UnknownDatabaseFolder(`${folder}/${entry.name}`);
|
|
3974
|
-
}
|
|
3975
|
-
async function foldersIn(dir) {
|
|
3976
|
-
return (await readdir(dir, { withFileTypes: true }).catch((error) => {
|
|
3977
|
-
if (error.code === "ENOENT") return [];
|
|
3978
|
-
throw error;
|
|
3979
|
-
})).filter((entry) => entry.isDirectory()).map((entry) => entry.name);
|
|
3980
|
-
}
|
|
3981
|
-
/**
|
|
3982
|
-
* A migrator for one target, connected as its owner.
|
|
3983
|
-
*
|
|
3984
|
-
* No `migrationTableSchema`: the owner role's `search_path` is pinned to the
|
|
3985
|
-
* construct's schema, so the history lands there — `app.kysely_migration` for
|
|
3986
|
-
* the database, `auth_database.kysely_migration` for its tenant — and two
|
|
3987
|
-
* constructs in one Postgres never share one.
|
|
3988
|
-
*
|
|
3989
|
-
* Unordered migrations are allowed. Files are timestamped, so a branch merged
|
|
3990
|
-
* after another's migration ran carries an older one; refusing it would make
|
|
3991
|
-
* every such merge a manual repair.
|
|
3992
|
-
*/
|
|
3993
|
-
async function withMigrator(options, target, run) {
|
|
3994
|
-
return withOwner(options, target, (db, { Migrator, sql }) => run(new Migrator({
|
|
3995
|
-
db,
|
|
3996
|
-
provider: folderProvider(join(options.root, target.migrations), sql),
|
|
3997
|
-
allowUnorderedMigrations: true
|
|
3998
|
-
})));
|
|
3999
|
-
}
|
|
4000
|
-
/**
|
|
4001
|
-
* One target's database, connected as its owner and closed afterwards — what
|
|
4002
|
-
* migrations and seeds both run as.
|
|
4003
|
-
*/
|
|
4004
|
-
async function withOwner(options, target, run) {
|
|
4005
|
-
const kysely = await kyselyFrom(options.sources?.[target.id]?.file ?? (target.of ? options.sources?.[target.of]?.file : void 0) ?? join(options.root, "package.json"));
|
|
4006
|
-
const db = new kysely.Kysely({ dialect: new kysely.PostgresDialect({ pool: new pg.Pool(poolConfig(ownerUrl(options, target))) }) });
|
|
4007
|
-
try {
|
|
4008
|
-
return await run(db, kysely);
|
|
4009
|
-
} finally {
|
|
4010
|
-
await db.destroy();
|
|
4011
|
-
}
|
|
4012
|
-
}
|
|
4013
|
-
/**
|
|
4014
|
-
* The owner's URL, with no fallback to the runtime one.
|
|
4015
|
-
*
|
|
4016
|
-
* The runtime role can create nothing, so falling back would only move the
|
|
4017
|
-
* failure to the first `CREATE TABLE` and make it read like a permissions
|
|
4018
|
-
* bug. The one exception is a database declared `roles: false`, where there is
|
|
4019
|
-
* no owner — both URLs are the master credential by design.
|
|
4020
|
-
*/
|
|
4021
|
-
function ownerUrl(options, target) {
|
|
4022
|
-
const key = provideKey(target.id, "ownerUrl");
|
|
4023
|
-
const owner = options.env[key];
|
|
4024
|
-
if (owner) return owner;
|
|
4025
|
-
const database = options.manifest[target.of ?? target.id];
|
|
4026
|
-
if (database?.kind === "database" && database.roles === false) {
|
|
4027
|
-
const url = options.env[provideKey(target.id, "url")];
|
|
4028
|
-
if (url) return url;
|
|
4029
|
-
}
|
|
4030
|
-
throw new NoOwnerCredential(target.id, key);
|
|
4031
|
-
}
|
|
4032
|
-
/**
|
|
4033
|
-
* Pool settings for a URL, with `?search_path=` as the startup option Postgres
|
|
4034
|
-
* reads. Only a `roles: false` URL carries it — a role pins its own.
|
|
4035
|
-
*/
|
|
4036
|
-
function poolConfig(url) {
|
|
4037
|
-
const parsed = new URL(url);
|
|
4038
|
-
const searchPath = parsed.searchParams.get("search_path");
|
|
4039
|
-
if (!searchPath) return { connectionString: url };
|
|
4040
|
-
parsed.searchParams.delete("search_path");
|
|
4041
|
-
return {
|
|
4042
|
-
connectionString: parsed.toString(),
|
|
4043
|
-
options: `-c search_path=${searchPath}`
|
|
4044
|
-
};
|
|
4045
|
-
}
|
|
4046
|
-
/** Kysely as the project resolves it from `from`. */
|
|
4047
|
-
async function kyselyFrom(from) {
|
|
4048
|
-
const require = createRequire(from);
|
|
4049
|
-
try {
|
|
4050
|
-
const [main, migration] = await Promise.all([import(pathToFileURL(require.resolve("kysely")).href), import(pathToFileURL(require.resolve("kysely/migration")).href)]);
|
|
4051
|
-
return {
|
|
4052
|
-
Kysely: main.Kysely,
|
|
4053
|
-
PostgresDialect: main.PostgresDialect,
|
|
4054
|
-
sql: main.sql,
|
|
4055
|
-
Migrator: migration.Migrator
|
|
4056
|
-
};
|
|
4057
|
-
} catch {
|
|
4058
|
-
throw new KyselyNotFound(from);
|
|
4059
|
-
}
|
|
4060
|
-
}
|
|
4061
|
-
//#endregion
|
|
4062
|
-
//#region src/compose/docker.ts
|
|
4063
|
-
/**
|
|
4064
|
-
* The Docker operations `gkm compose` needs, behind an interface.
|
|
4065
|
-
*
|
|
4066
|
-
* Small on purpose, like reconcile's: the rules about *when* each runs — the
|
|
4067
|
-
* registry is asked before anything is pulled, nothing is started for a tag
|
|
4068
|
-
* that is missing — are asserted against a fake, and this file is only the
|
|
4069
|
-
* wiring to the real CLI.
|
|
4070
|
-
*
|
|
4071
|
-
* Every call is an argument array. Image refs, tags and paths come from the
|
|
4072
|
-
* workspace, and none of them reaches a shell.
|
|
4073
|
-
*/
|
|
4074
|
-
/** How long a stack has to pass its health checks. */
|
|
4075
|
-
const WAIT_TIMEOUT_S = 180;
|
|
4076
|
-
/** `docker compose port` printed nothing a port can be read from. */
|
|
4077
|
-
var ServicePortUnknown = class extends Error {
|
|
4078
|
-
service;
|
|
4079
|
-
inside;
|
|
4080
|
-
output;
|
|
4081
|
-
constructor(service, inside, output) {
|
|
4082
|
-
super(`'${service}' publishes no host port for ${inside} (docker said: ${output.trim() || "nothing"}). Is it running? \`docker compose ps\` shows the stack.`);
|
|
4083
|
-
this.service = service;
|
|
4084
|
-
this.inside = inside;
|
|
4085
|
-
this.output = output;
|
|
4086
|
-
this.name = "ServicePortUnknown";
|
|
4087
|
-
}
|
|
4088
|
-
};
|
|
4089
|
-
function compose(stack, args) {
|
|
4090
|
-
return [
|
|
4091
|
-
"compose",
|
|
4092
|
-
"-p",
|
|
4093
|
-
stack.project,
|
|
4094
|
-
"-f",
|
|
4095
|
-
stack.file,
|
|
4096
|
-
...args
|
|
4097
|
-
];
|
|
4098
|
-
}
|
|
4099
|
-
/** Run a program and keep everything it said, whatever it exits with. */
|
|
4100
|
-
function capture(command, args, timeoutMs = 6e4) {
|
|
4101
|
-
return new Promise((resolve, reject) => {
|
|
4102
|
-
const child = spawn(command, [...args], {
|
|
4103
|
-
stdio: [
|
|
4104
|
-
"ignore",
|
|
4105
|
-
"pipe",
|
|
4106
|
-
"pipe"
|
|
4107
|
-
],
|
|
4108
|
-
shell: false
|
|
4109
|
-
});
|
|
4110
|
-
let stdout = "";
|
|
4111
|
-
let stderr = "";
|
|
4112
|
-
child.stdout.on("data", (chunk) => {
|
|
4113
|
-
stdout += chunk.toString();
|
|
4114
|
-
});
|
|
4115
|
-
child.stderr.on("data", (chunk) => {
|
|
4116
|
-
stderr += chunk.toString();
|
|
4117
|
-
});
|
|
4118
|
-
const timer = setTimeout(() => child.kill("SIGKILL"), timeoutMs);
|
|
4119
|
-
timer.unref?.();
|
|
4120
|
-
child.on("error", (error) => {
|
|
4121
|
-
clearTimeout(timer);
|
|
4122
|
-
reject(error);
|
|
4123
|
-
});
|
|
4124
|
-
child.on("close", (code) => {
|
|
4125
|
-
clearTimeout(timer);
|
|
4126
|
-
resolve({
|
|
4127
|
-
code,
|
|
4128
|
-
stdout,
|
|
4129
|
-
stderr
|
|
4130
|
-
});
|
|
4131
|
-
});
|
|
4132
|
-
});
|
|
4133
|
-
}
|
|
4134
|
-
/**
|
|
4135
|
-
* Whether a failed `docker manifest inspect` means the tag is not there, as
|
|
4136
|
-
* opposed to the registry not answering.
|
|
4137
|
-
*
|
|
4138
|
-
* Read off the message because that is all the CLI gives. Anything not
|
|
4139
|
-
* recognisably "no such tag" is treated as the registry being unreachable:
|
|
4140
|
-
* a registry that refused credentials must not be reported as a tag that was
|
|
4141
|
-
* never pushed, or the fix offered would be the wrong one.
|
|
4142
|
-
*/
|
|
4143
|
-
function isMissingManifest(output) {
|
|
4144
|
-
return /no such manifest|manifest unknown|manifest for .* not found|not found: manifest/i.test(output);
|
|
4145
|
-
}
|
|
4146
|
-
const dockerCompose = {
|
|
4147
|
-
async lookup(ref) {
|
|
4148
|
-
const { code, stdout, stderr } = await capture("docker", [
|
|
4149
|
-
"manifest",
|
|
4150
|
-
"inspect",
|
|
4151
|
-
ref
|
|
4152
|
-
]);
|
|
4153
|
-
if (code === 0) return {
|
|
4154
|
-
ref,
|
|
4155
|
-
status: "found"
|
|
4156
|
-
};
|
|
4157
|
-
const output = `${stderr}\n${stdout}`.trim();
|
|
4158
|
-
return isMissingManifest(output) ? {
|
|
4159
|
-
ref,
|
|
4160
|
-
status: "missing"
|
|
4161
|
-
} : {
|
|
4162
|
-
ref,
|
|
4163
|
-
status: "unreachable",
|
|
4164
|
-
detail: output
|
|
4165
|
-
};
|
|
4166
|
-
},
|
|
4167
|
-
async build(stack, services) {
|
|
4168
|
-
await run$1("docker", compose(stack, ["build", ...services]), {
|
|
4169
|
-
cwd: stack.cwd,
|
|
4170
|
-
env: {
|
|
4171
|
-
...process.env,
|
|
4172
|
-
DOCKER_BUILDKIT: "1"
|
|
4173
|
-
}
|
|
4174
|
-
});
|
|
4175
|
-
},
|
|
4176
|
-
async pull(stack, services) {
|
|
4177
|
-
await run$1("docker", compose(stack, ["pull", ...services]), { cwd: stack.cwd });
|
|
4178
|
-
},
|
|
4179
|
-
async up(stack, services) {
|
|
4180
|
-
await run$1("docker", compose(stack, [
|
|
4181
|
-
"up",
|
|
4182
|
-
"-d",
|
|
4183
|
-
"--wait",
|
|
4184
|
-
`--wait-timeout=${WAIT_TIMEOUT_S}`,
|
|
4185
|
-
...services ? [] : ["--remove-orphans"],
|
|
4186
|
-
...services ?? []
|
|
4187
|
-
]), { cwd: stack.cwd });
|
|
4188
|
-
},
|
|
4189
|
-
async down(stack) {
|
|
4190
|
-
await run$1("docker", compose(stack, ["down"]), { cwd: stack.cwd });
|
|
4191
|
-
},
|
|
4192
|
-
async port(stack, service, inside) {
|
|
4193
|
-
const output = await runOutput("docker", compose(stack, [
|
|
4194
|
-
"port",
|
|
4195
|
-
service,
|
|
4196
|
-
String(inside)
|
|
4197
|
-
]), { cwd: stack.cwd });
|
|
4198
|
-
const port = Number(output.trim().split(":").pop());
|
|
4199
|
-
if (!Number.isInteger(port) || port <= 0) throw new ServicePortUnknown(service, inside, output);
|
|
4200
|
-
return port;
|
|
4201
|
-
},
|
|
4202
|
-
async copyOut(stack, service, from, to) {
|
|
4203
|
-
await mkdir(dirname(to), { recursive: true });
|
|
4204
|
-
await run$1("docker", compose(stack, [
|
|
4205
|
-
"cp",
|
|
4206
|
-
`${service}:${from}`,
|
|
4207
|
-
to
|
|
4208
|
-
]), { cwd: stack.cwd });
|
|
4209
|
-
},
|
|
4210
|
-
async digest(ref) {
|
|
4211
|
-
const { code, stdout } = await capture("docker", [
|
|
4212
|
-
"image",
|
|
4213
|
-
"inspect",
|
|
4214
|
-
"--format={{json .RepoDigests}} {{.Id}}",
|
|
4215
|
-
ref
|
|
4216
|
-
]);
|
|
4217
|
-
if (code !== 0) return void 0;
|
|
4218
|
-
const [json = "[]", id] = stdout.trim().split(" ");
|
|
4219
|
-
const repository = ref.split("@")[0].replace(/:[\w][\w.-]*$/, "");
|
|
4220
|
-
try {
|
|
4221
|
-
const digests = JSON.parse(json);
|
|
4222
|
-
const match = Array.isArray(digests) ? digests.find((entry) => typeof entry === "string" && entry.startsWith(`${repository}@`)) : void 0;
|
|
4223
|
-
if (match) return match.slice(repository.length + 1);
|
|
4224
|
-
} catch {}
|
|
4225
|
-
return id || void 0;
|
|
4226
|
-
}
|
|
4227
|
-
};
|
|
4228
|
-
//#endregion
|
|
4229
|
-
//#region src/compose/images.ts
|
|
4230
|
-
/**
|
|
4231
|
-
* The tag a site's image carries for a release.
|
|
4232
|
-
*
|
|
4233
|
-
* A site's public URLs are inlined when it is built, so one bundle serves one
|
|
4234
|
-
* stage: `v1.4.0-production` and `v1.4.0-staging` are two images of the same
|
|
4235
|
-
* commit. A backend reads its URLs at runtime, so one image serves every stage
|
|
4236
|
-
* and its tag is the release's alone.
|
|
4237
|
-
*/
|
|
4238
|
-
function siteTag(tag, stage) {
|
|
4239
|
-
return `${tag}-${stage}`;
|
|
4240
|
-
}
|
|
4241
|
-
/** Images a tag names that the registry does not have. */
|
|
4242
|
-
var ImageTagNotFound = class extends Error {
|
|
4243
|
-
tag;
|
|
4244
|
-
refs;
|
|
4245
|
-
constructor(tag, refs) {
|
|
4246
|
-
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.`);
|
|
4247
|
-
this.tag = tag;
|
|
4248
|
-
this.refs = refs;
|
|
4249
|
-
this.name = "ImageTagNotFound";
|
|
4250
|
-
}
|
|
4251
|
-
};
|
|
4252
|
-
/** The registry did not answer, or did not let this machine in. */
|
|
4253
|
-
var RegistryUnreachable = class extends Error {
|
|
4254
|
-
ref;
|
|
4255
|
-
detail;
|
|
4256
|
-
constructor(ref, detail) {
|
|
4257
|
-
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>\`).`);
|
|
4258
|
-
this.ref = ref;
|
|
4259
|
-
this.detail = detail;
|
|
4260
|
-
this.name = "RegistryUnreachable";
|
|
4261
|
-
}
|
|
4262
|
-
};
|
|
4263
|
-
/**
|
|
4264
|
-
* Assert every image exists, before anything else happens.
|
|
4265
|
-
*
|
|
4266
|
-
* Asked all at once and reported all at once: a release missing two images is
|
|
4267
|
-
* one problem with two names, and finding the second only after fixing the
|
|
4268
|
-
* first is a slower way to learn the same thing.
|
|
4269
|
-
*
|
|
4270
|
-
* @throws {RegistryUnreachable} when the registry cannot say
|
|
4271
|
-
* @throws {ImageTagNotFound} listing every missing image
|
|
4272
|
-
*/
|
|
4273
|
-
async function assertImagesExist(docker, tag, images) {
|
|
4274
|
-
const lookups = await Promise.all(images.map((image) => docker.lookup(image.ref)));
|
|
4275
|
-
const unreachable = lookups.find((lookup) => lookup.status === "unreachable");
|
|
4276
|
-
if (unreachable?.status === "unreachable") throw new RegistryUnreachable(unreachable.ref, unreachable.detail);
|
|
4277
|
-
const missing = lookups.filter((lookup) => lookup.status === "missing").map((lookup) => lookup.ref);
|
|
4278
|
-
if (missing.length > 0) throw new ImageTagNotFound(tag, missing);
|
|
4279
|
-
}
|
|
4280
|
-
//#endregion
|
|
4281
|
-
//#region src/compose/caddyfile.ts
|
|
4282
|
-
/** The Caddyfile for a stack's edge. */
|
|
4283
|
-
function edgeCaddyfile(sites, options) {
|
|
4284
|
-
const global = options.local ? `{
|
|
4285
|
-
# No ACME and no public DNS: the CA is Caddy's own and the names resolve
|
|
4286
|
-
# to this machine.
|
|
4287
|
-
local_certs
|
|
4288
|
-
auto_https disable_redirects
|
|
4289
|
-
}` : options.email ? `{
|
|
4290
|
-
email ${options.email}
|
|
4291
|
-
}` : "";
|
|
4292
|
-
const blocks = sites.map(({ host, upstream }) => `https://${host} {${options.local ? `
|
|
4293
|
-
tls internal
|
|
4294
|
-
` : ""}
|
|
4295
|
-
# Host and X-Forwarded-* are passed through as Caddy does by default, so
|
|
4296
|
-
# an app builds redirects, cookies and links on the address a caller used.
|
|
4297
|
-
reverse_proxy ${upstream} {
|
|
4298
|
-
# Never buffer: a streamed response — server-sent events, a Next.js
|
|
4299
|
-
# RSC payload — reaches the client as each chunk is written.
|
|
4300
|
-
flush_interval -1
|
|
4301
|
-
}
|
|
4302
|
-
}`);
|
|
4303
|
-
return `${HEADER}${global ? `${global}\n\n` : ""}${blocks.join("\n\n")}\n`;
|
|
4304
|
-
}
|
|
4305
|
-
const HEADER = `# Generated by gkm compose from the construct manifest — do not edit.
|
|
4306
|
-
#
|
|
4307
|
-
# One host per API and site, each in front of its own container.
|
|
4308
|
-
`;
|
|
4309
|
-
//#endregion
|
|
4310
|
-
//#region src/compose/stack.ts
|
|
4311
|
-
/**
|
|
4312
|
-
* The stack a workspace runs as for one stage — pure.
|
|
4313
|
-
*
|
|
4314
|
-
* Everything `gkm compose` writes is decided here, as data: the compose file,
|
|
4315
|
-
* the Caddyfile, one env file per backend, and the image each app runs. The
|
|
4316
|
-
* command around it reads the workspace, asks the registry, writes the files
|
|
4317
|
-
* and starts Docker; none of those decide anything, so all of it can be
|
|
4318
|
-
* asserted without a daemon.
|
|
4319
|
-
*
|
|
4320
|
-
* The values come from the same derivations every other target uses. The
|
|
4321
|
-
* plan is reconcile's `planFor`, the keys an app reads are `appEnvKeys`, and
|
|
4322
|
-
* every URL is `envFor`'s — asked twice, once with each app at its public
|
|
4323
|
-
* address and once on the compose network, because a browser and a sibling
|
|
4324
|
-
* service reach the same API by different names.
|
|
4325
|
-
*/
|
|
4326
|
-
/** Where a stack's files are written, relative to the workspace root. */
|
|
4327
|
-
function stackDir(stage) {
|
|
4328
|
-
return `.gkm/compose/${stage}`;
|
|
4329
|
-
}
|
|
4330
|
-
/**
|
|
4331
|
-
* The compose project a stage's stack runs as.
|
|
4332
|
-
*
|
|
4333
|
-
* The deploy identity's scope, so two workspaces with one name in different
|
|
4334
|
-
* namespaces never share containers or volumes — and the stage, so a
|
|
4335
|
-
* machine can run two stages side by side.
|
|
4336
|
-
*/
|
|
4337
|
-
function composeProject(identity) {
|
|
4338
|
-
return `${identity.scope}-${identity.stage}`;
|
|
4339
|
-
}
|
|
4340
|
-
/** The ports the edge is published on, and the variables that move them. */
|
|
4341
|
-
const EDGE_PORT_ENV = {
|
|
4342
|
-
https: "GKM_COMPOSE_HTTPS_PORT",
|
|
4343
|
-
http: "GKM_COMPOSE_HTTP_PORT"
|
|
4344
|
-
};
|
|
4345
|
-
/**
|
|
4346
|
-
* Containers this target does not run.
|
|
4347
|
-
*
|
|
4348
|
-
* Object storage is the stage's own — an externally hosted bucket whose URL
|
|
4349
|
-
* the stage's secrets hold — and so is a deployed stage's mail. The AWS
|
|
4350
|
-
* emulator never applies: a stack is a server target, whose events are
|
|
4351
|
-
* pg-boss.
|
|
4352
|
-
*/
|
|
4353
|
-
const NOT_RUN = {
|
|
4354
|
-
minio: true,
|
|
4355
|
-
localstack: true,
|
|
4356
|
-
caddy: true
|
|
4357
|
-
};
|
|
4358
|
-
/** Kinds whose `<ID>_URL` a stage must be given, because nothing here runs them. */
|
|
4359
|
-
const EXTERNAL_KINDS = {
|
|
4360
|
-
objects: true,
|
|
4361
|
-
"file-server": true
|
|
4362
|
-
};
|
|
4363
|
-
/** The S3 client's credentials, read beside a bucket's URL when it has them. */
|
|
4364
|
-
const STORAGE_KEYS = [
|
|
4365
|
-
"AWS_ACCESS_KEY_ID",
|
|
4366
|
-
"AWS_SECRET_ACCESS_KEY",
|
|
4367
|
-
"AWS_REGION"
|
|
4368
|
-
];
|
|
4369
|
-
/** A bucket the stack needs and the stage was not given. */
|
|
4370
|
-
var BucketNotConfigured = class extends Error {
|
|
4371
|
-
app;
|
|
4372
|
-
id;
|
|
4373
|
-
key;
|
|
4374
|
-
stage;
|
|
4375
|
-
constructor(app, id, key, stage) {
|
|
4376
|
-
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}`);
|
|
4377
|
-
this.app = app;
|
|
4378
|
-
this.id = id;
|
|
4379
|
-
this.key = key;
|
|
4380
|
-
this.stage = stage;
|
|
4381
|
-
this.name = "BucketNotConfigured";
|
|
4382
|
-
}
|
|
4383
|
-
};
|
|
4384
|
-
/** A value only the stage's secrets can hold, and they do not. */
|
|
4385
|
-
var StageSecretMissing = class extends Error {
|
|
4386
|
-
app;
|
|
4387
|
-
key;
|
|
4388
|
-
stage;
|
|
4389
|
-
constructor(app, key, stage) {
|
|
4390
|
-
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}`);
|
|
4391
|
-
this.app = app;
|
|
4392
|
-
this.key = key;
|
|
4393
|
-
this.stage = stage;
|
|
4394
|
-
this.name = "StageSecretMissing";
|
|
4395
|
-
}
|
|
4396
|
-
};
|
|
4397
|
-
/** A deployed stage's secrets with no seed to derive its passwords from. */
|
|
4398
|
-
var StageSeedMissing = class extends Error {
|
|
4399
|
-
stage;
|
|
4400
|
-
constructor(stage) {
|
|
4401
|
-
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.`);
|
|
4402
|
-
this.stage = stage;
|
|
4403
|
-
this.name = "StageSeedMissing";
|
|
4404
|
-
}
|
|
4405
|
-
};
|
|
4406
|
-
/** A workspace with no API and no site to run. */
|
|
4407
|
-
var NothingToCompose = class extends Error {
|
|
4408
|
-
root;
|
|
4409
|
-
constructor(root) {
|
|
4410
|
-
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.`);
|
|
4411
|
-
this.root = root;
|
|
4412
|
-
this.name = "NothingToCompose";
|
|
4413
|
-
}
|
|
4414
|
-
};
|
|
4415
|
-
/** A value that cannot be written to an env file a line at a time. */
|
|
4416
|
-
var EnvValueMultiline = class extends Error {
|
|
4417
|
-
app;
|
|
4418
|
-
key;
|
|
4419
|
-
constructor(app, key) {
|
|
4420
|
-
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.`);
|
|
4421
|
-
this.app = app;
|
|
4422
|
-
this.key = key;
|
|
4423
|
-
this.name = "EnvValueMultiline";
|
|
4424
|
-
}
|
|
4425
|
-
};
|
|
4426
|
-
/** The services a stack's apps wait on and the edge waits on in turn. */
|
|
4427
|
-
const HEALTHY = { condition: "service_healthy" };
|
|
4428
|
-
/**
|
|
4429
|
-
* An app's health check: its own server, asked on 127.0.0.1 — not
|
|
4430
|
-
* `localhost`, which resolves to `::1` first in an Alpine container while a
|
|
4431
|
-
* server may listen on IPv4 alone.
|
|
4432
|
-
*/
|
|
4433
|
-
function probe(port, path) {
|
|
4434
|
-
return {
|
|
4435
|
-
test: [
|
|
4436
|
-
"CMD",
|
|
4437
|
-
"wget",
|
|
4438
|
-
"-q",
|
|
4439
|
-
"-O",
|
|
4440
|
-
"/dev/null",
|
|
4441
|
-
`http://127.0.0.1:${port}${path}`
|
|
4442
|
-
],
|
|
4443
|
-
interval: "5s",
|
|
4444
|
-
timeout: "3s",
|
|
4445
|
-
retries: 20,
|
|
4446
|
-
start_period: "10s"
|
|
4447
|
-
};
|
|
4448
|
-
}
|
|
4449
|
-
/** The stack for one stage. */
|
|
4450
|
-
function composeStack(input) {
|
|
4451
|
-
const { workspace, manifest, stage, identity } = input;
|
|
4452
|
-
const local = stage === workspace.stages.local;
|
|
4453
|
-
const project = composeProject(identity);
|
|
4454
|
-
const custom = input.secrets?.custom ?? {};
|
|
4455
|
-
const https = input.ports?.https ?? 443;
|
|
4456
|
-
const http = input.ports?.http ?? 80;
|
|
4457
|
-
const plan = planFor(manifest, stage, provisionOrder(manifest), {
|
|
4458
|
-
localStage: workspace.stages.local,
|
|
4459
|
-
events: DEFAULT_EVENTS.server,
|
|
4460
|
-
cache: DEFAULT_CACHE.server,
|
|
4461
|
-
edge: false
|
|
4462
|
-
});
|
|
4463
|
-
const byId = new Map(plan.resources.map((r) => [r.id, r]));
|
|
4464
|
-
const seed = local ? void 0 : input.secrets?.seed;
|
|
4465
|
-
if (!local && !seed) throw new StageSeedMissing(stage);
|
|
4466
|
-
const credential = {
|
|
4467
|
-
master: local ? "geekmidas" : localRolePassword(workspace.name, plan, "master", seed),
|
|
4468
|
-
...seed ? { seed } : {}
|
|
4469
|
-
};
|
|
4470
|
-
const infra = plan.containers.filter((c) => !NOT_RUN[c]).filter((c) => local || c !== "mailpit").sort();
|
|
4471
|
-
const apps = stackApps(workspace, manifest, plan, {
|
|
4472
|
-
local,
|
|
4473
|
-
stage,
|
|
4474
|
-
https,
|
|
4475
|
-
identity,
|
|
4476
|
-
images: input.images
|
|
4477
|
-
});
|
|
4478
|
-
if (apps.length === 0) throw new NothingToCompose(workspace.root);
|
|
4479
|
-
const derivation = {
|
|
4480
|
-
project: workspace.name,
|
|
4481
|
-
master: credential.master,
|
|
4482
|
-
...credential.seed ? { seed: credential.seed } : {}
|
|
4483
|
-
};
|
|
4484
|
-
const outside = networkEnv(plan, {
|
|
4485
|
-
...derivation,
|
|
4486
|
-
addresses: Object.fromEntries(apps.map((app) => [app.id, app.url]))
|
|
4487
|
-
});
|
|
4488
|
-
const inside = networkEnv(plan, {
|
|
4489
|
-
...derivation,
|
|
4490
|
-
addresses: Object.fromEntries(apps.map((app) => [app.id, `http://${app.name}:${app.port}`]))
|
|
4491
|
-
});
|
|
4492
|
-
const owners = /* @__PURE__ */ new Map();
|
|
4493
|
-
for (const resource of plan.resources) {
|
|
4494
|
-
owners.set(resource.envKey, resource);
|
|
4495
|
-
if (resource.kind === "email") {
|
|
4496
|
-
owners.set(provideKey(resource.id, "from"), resource);
|
|
4497
|
-
owners.set(provideKey(resource.id, "inboxUrl"), resource);
|
|
4498
|
-
}
|
|
4499
|
-
if (resource.kind === "external-api") owners.set(provideKey(resource.id, "credentials"), resource);
|
|
4500
|
-
}
|
|
4501
|
-
const surfaceUrls = new Map(plan.resources.filter((r) => r.kind === "rest-api" || r.kind === "site").map((r) => [r.envKey, r.id]));
|
|
4502
|
-
const appById = new Map(apps.map((app) => [app.id, app]));
|
|
4503
|
-
/**
|
|
4504
|
-
* What a key resolves to for one app, from the stage's secrets where only
|
|
4505
|
-
* they can hold it, and the derivations otherwise.
|
|
4506
|
-
*/
|
|
4507
|
-
const valueFor = (app, key, owner) => {
|
|
4508
|
-
if (owner && EXTERNAL_KINDS[owner.kind] && key === owner.envKey) {
|
|
4509
|
-
const value = custom[key];
|
|
4510
|
-
if (value === void 0) throw new BucketNotConfigured(app.name, owner.id, key, stage);
|
|
4511
|
-
return value;
|
|
4512
|
-
}
|
|
4513
|
-
if (owner && suppliedBy(owner, key, local)) {
|
|
4514
|
-
const value = custom[key];
|
|
4515
|
-
if (value === void 0) throw new StageSecretMissing(app.name, key, stage);
|
|
4516
|
-
return value;
|
|
4517
|
-
}
|
|
4518
|
-
if (custom[key] !== void 0) return custom[key];
|
|
4519
|
-
const surface = surfaceUrls.get(key);
|
|
4520
|
-
if (app.kind === "rest-api" && surface && surface !== app.id) return inside[key];
|
|
4521
|
-
return outside[key];
|
|
4522
|
-
};
|
|
4523
|
-
for (const app of apps) {
|
|
4524
|
-
if (app.kind === "site") {
|
|
4525
|
-
const site = manifest[app.id];
|
|
4526
|
-
if (site?.kind !== "site") continue;
|
|
4527
|
-
const args = {};
|
|
4528
|
-
for (const [key, source] of Object.entries(publicEnvFor(site, manifest))) {
|
|
4529
|
-
const value = valueFor(app, source, owners.get(source));
|
|
4530
|
-
if (value !== void 0) args[key] = value;
|
|
4531
|
-
}
|
|
4532
|
-
if (app.build && Object.keys(args).length > 0) app.build.args = args;
|
|
4533
|
-
continue;
|
|
4534
|
-
}
|
|
4535
|
-
const keys = appEnvKeys(manifest, app.name, input.runnables) ?? /* @__PURE__ */ new Set();
|
|
4536
|
-
const values = {};
|
|
4537
|
-
for (const key of [...keys].sort()) {
|
|
4538
|
-
const owner = owners.get(key) ?? storageOwner(key, plan);
|
|
4539
|
-
if (!local && owner?.kind === "email" && key.endsWith("_INBOX_URL")) continue;
|
|
4540
|
-
if (STORAGE_KEYS.includes(key)) {
|
|
4541
|
-
if (custom[key] !== void 0) values[key] = custom[key];
|
|
4542
|
-
continue;
|
|
4543
|
-
}
|
|
4544
|
-
const value = valueFor(app, key, owner);
|
|
4545
|
-
if (value !== void 0) values[key] = value;
|
|
4546
|
-
}
|
|
4547
|
-
const trusted = provideKey(app.id, "trustedOrigins");
|
|
4548
|
-
if (keys.has(trusted)) {
|
|
4549
|
-
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}`);
|
|
4550
|
-
const origins = [...(values[trusted] ?? "").split(",").filter(Boolean), ...internal];
|
|
4551
|
-
values[trusted] = [...new Set(origins)].join(",");
|
|
4552
|
-
}
|
|
4553
|
-
app.env = {
|
|
4554
|
-
...values,
|
|
4555
|
-
NODE_ENV: "production",
|
|
4556
|
-
PORT: String(app.port),
|
|
4557
|
-
STAGE: stage
|
|
4558
|
-
};
|
|
4559
|
-
for (const [key, value] of Object.entries(app.env)) if (/[\r\n]/.test(value)) throw new EnvValueMultiline(app.name, key);
|
|
4560
|
-
}
|
|
4561
|
-
const compose = stackFile({
|
|
4562
|
-
project,
|
|
4563
|
-
plan,
|
|
4564
|
-
infra,
|
|
4565
|
-
apps,
|
|
4566
|
-
master: credential.master,
|
|
4567
|
-
https,
|
|
4568
|
-
http
|
|
4569
|
-
});
|
|
4570
|
-
const caddyfile = edgeCaddyfile(apps.map((app) => ({
|
|
4571
|
-
host: app.host,
|
|
4572
|
-
upstream: `${app.name}:${app.port}`
|
|
4573
|
-
})), { local });
|
|
4574
|
-
const dockerfiles = {};
|
|
4575
|
-
for (const app of apps) {
|
|
4576
|
-
if (!app.build) continue;
|
|
4577
|
-
dockerfiles[app.build.dockerfile] = app.kind === "site" ? siteDockerfile(app.name, workspace.apps[app.name], {
|
|
4578
|
-
turboPackage: input.packages?.[app.name] ?? app.name,
|
|
4579
|
-
packageManager: input.packageManager ?? "pnpm",
|
|
4580
|
-
manifest
|
|
4581
|
-
}) : generateSlimDockerfile({
|
|
4582
|
-
imageName: app.name,
|
|
4583
|
-
baseImage: "node:22-alpine",
|
|
4584
|
-
port: app.port,
|
|
4585
|
-
healthCheckPath: "/health",
|
|
4586
|
-
prebuilt: true,
|
|
4587
|
-
packageManager: input.packageManager ?? "pnpm",
|
|
4588
|
-
bundle: `${app.path}/.gkm/server/dist/server.mjs`
|
|
4589
|
-
});
|
|
4590
|
-
}
|
|
4591
|
-
return {
|
|
4592
|
-
project,
|
|
4593
|
-
stage,
|
|
4594
|
-
local,
|
|
4595
|
-
apps,
|
|
4596
|
-
infra,
|
|
4597
|
-
compose,
|
|
4598
|
-
caddyfile,
|
|
4599
|
-
dockerfiles,
|
|
4600
|
-
plan,
|
|
4601
|
-
credential
|
|
4602
|
-
};
|
|
4603
|
-
/** Whether only the stage's secrets can supply `key` to `owner`. */
|
|
4604
|
-
function suppliedBy(owner, key, isLocal) {
|
|
4605
|
-
if (owner.kind === "credential") return true;
|
|
4606
|
-
if (owner.kind === "external-api") return key === provideKey(owner.id, "credentials");
|
|
4607
|
-
if (isLocal) return false;
|
|
4608
|
-
if (owner.kind === "secret" || owner.kind === "encryption") return true;
|
|
4609
|
-
return owner.kind === "email" && !key.endsWith("_INBOX_URL");
|
|
4610
|
-
}
|
|
4611
|
-
}
|
|
4612
|
-
/** Which bucket an S3 credential key belongs to — the first one declared. */
|
|
4613
|
-
function storageOwner(key, plan) {
|
|
4614
|
-
if (!STORAGE_KEYS.includes(key)) return void 0;
|
|
4615
|
-
return plan.resources.find((r) => r.kind === "objects");
|
|
4616
|
-
}
|
|
4617
|
-
/** The apps a stack runs: every API and every site, at its stage address. */
|
|
4618
|
-
function stackApps(workspace, manifest, plan, options) {
|
|
4619
|
-
const apps = [];
|
|
4620
|
-
const resources = new Map(plan.resources.map((r) => [r.id, r]));
|
|
4621
|
-
for (const [id, declaration] of Object.entries(manifest)) {
|
|
4622
|
-
if (declaration.kind !== "rest-api" && declaration.kind !== "site") continue;
|
|
4623
|
-
const name = appKey(id);
|
|
4624
|
-
const app = workspace.apps[name];
|
|
4625
|
-
const resource = resources.get(id);
|
|
4626
|
-
if (!app || !resource || !app.port) continue;
|
|
4627
|
-
const host = hostOf(name, app, workspace, resource, options);
|
|
4628
|
-
const tag = declaration.kind === "site" ? siteTag(options.images.tag, options.stage) : options.images.tag;
|
|
4629
|
-
const ref = validateImageRef(imageRef(options.identity, name, options.images.registry, tag));
|
|
4630
|
-
apps.push({
|
|
4631
|
-
app: name,
|
|
4632
|
-
name,
|
|
4633
|
-
id,
|
|
4634
|
-
kind: declaration.kind,
|
|
4635
|
-
path: app.path,
|
|
4636
|
-
port: app.port,
|
|
4637
|
-
host,
|
|
4638
|
-
url: `https://${host}${options.local && options.https !== 443 ? `:${options.https}` : ""}`,
|
|
4639
|
-
ref,
|
|
4640
|
-
tag,
|
|
4641
|
-
...options.images.mode === "build" ? { build: { dockerfile: `${stackDir(options.stage)}/Dockerfile.${name}` } } : {}
|
|
4642
|
-
});
|
|
4643
|
-
}
|
|
4644
|
-
return apps.sort((a, b) => a.name.localeCompare(b.name));
|
|
4645
|
-
}
|
|
4646
|
-
/**
|
|
4647
|
-
* The host an app answers on for a stage: the `gkm dev` edge's
|
|
4648
|
-
* `*.localhost` names for the local stage, and the stage's domain — by the
|
|
4649
|
-
* same rule a deploy follows — for any other.
|
|
4650
|
-
*/
|
|
4651
|
-
function hostOf(name, app, workspace, resource, options) {
|
|
4652
|
-
if (options.local) return hostFor(resource, workspace.name);
|
|
4653
|
-
return resolveHost(name, app, options.stage, workspace.deploy?.domains, isMainFrontendApp(name, app, workspace.apps));
|
|
4654
|
-
}
|
|
4655
|
-
/** The compose document: the infrastructure, the apps, and the edge. */
|
|
4656
|
-
function stackFile(options) {
|
|
4657
|
-
const { project, plan, infra, apps } = options;
|
|
4658
|
-
const derived = composeFor({
|
|
4659
|
-
...plan,
|
|
4660
|
-
containers: [...infra]
|
|
4661
|
-
}, {
|
|
4662
|
-
project,
|
|
4663
|
-
ports: Object.fromEntries(portKeys(infra, plan.fakes).map((key) => [key, 0]))
|
|
4664
|
-
});
|
|
4665
|
-
const services = {};
|
|
4666
|
-
const volumes = {
|
|
4667
|
-
...derived.volumes,
|
|
4668
|
-
"caddy-data": {},
|
|
4669
|
-
"caddy-config": {}
|
|
4670
|
-
};
|
|
4671
|
-
for (const name of infra) {
|
|
4672
|
-
const { ports: _ports, depends_on, profiles: _profiles, ...service } = derived.services[name];
|
|
4673
|
-
services[name] = {
|
|
4674
|
-
...service,
|
|
4675
|
-
...depends_on?.length ? { depends_on: Object.fromEntries(depends_on.map((dep) => [dep, HEALTHY])) } : {}
|
|
4676
|
-
};
|
|
4677
|
-
}
|
|
4678
|
-
const postgres = services.postgres;
|
|
4679
|
-
if (postgres) {
|
|
4680
|
-
postgres.environment = {
|
|
4681
|
-
...postgres.environment,
|
|
4682
|
-
POSTGRES_PASSWORD: options.master
|
|
4683
|
-
};
|
|
4684
|
-
postgres.ports = ["127.0.0.1::5432"];
|
|
4685
|
-
}
|
|
4686
|
-
for (const app of apps) services[app.name] = {
|
|
4687
|
-
image: app.ref,
|
|
4688
|
-
...app.build ? { build: {
|
|
4689
|
-
context: "../../..",
|
|
4690
|
-
dockerfile: app.build.dockerfile,
|
|
4691
|
-
...app.build.args ? { args: app.build.args } : {}
|
|
4692
|
-
} } : {},
|
|
4693
|
-
restart: "unless-stopped",
|
|
4694
|
-
...app.kind === "rest-api" ? {
|
|
4695
|
-
env_file: [{
|
|
4696
|
-
path: `./${app.name}.env`,
|
|
4697
|
-
format: "raw"
|
|
4698
|
-
}],
|
|
4699
|
-
...infra.length ? { depends_on: Object.fromEntries(infra.map((name) => [name, HEALTHY])) } : {}
|
|
4700
|
-
} : {},
|
|
4701
|
-
healthcheck: probe(app.port, app.kind === "site" ? "/" : "/health")
|
|
4702
|
-
};
|
|
4703
|
-
services.caddy = {
|
|
4704
|
-
image: DEFAULT_IMAGES.caddy,
|
|
4705
|
-
restart: "unless-stopped",
|
|
4706
|
-
ports: [`${options.https}:443`, `${options.http}:80`],
|
|
4707
|
-
volumes: [
|
|
4708
|
-
"./Caddyfile:/etc/caddy/Caddyfile:ro",
|
|
4709
|
-
"caddy-data:/data",
|
|
4710
|
-
"caddy-config:/config"
|
|
4711
|
-
],
|
|
4712
|
-
depends_on: Object.fromEntries(apps.map((app) => [app.name, HEALTHY])),
|
|
4713
|
-
healthcheck: {
|
|
4714
|
-
test: [
|
|
4715
|
-
"CMD",
|
|
4716
|
-
"caddy",
|
|
4717
|
-
"version"
|
|
4718
|
-
],
|
|
4719
|
-
interval: "10s",
|
|
4720
|
-
timeout: "5s",
|
|
4721
|
-
retries: 5
|
|
4722
|
-
}
|
|
4723
|
-
};
|
|
4724
|
-
return {
|
|
4725
|
-
name: project,
|
|
4726
|
-
services,
|
|
4727
|
-
volumes
|
|
4728
|
-
};
|
|
4729
|
-
}
|
|
4730
|
-
/** Render an app's env file: one `KEY=value` per line, read raw by compose. */
|
|
4731
|
-
function envFile(env) {
|
|
4732
|
-
return `${Object.entries(env).sort(([a], [b]) => a.localeCompare(b)).map(([key, value]) => `${key}=${value}`).join("\n")}\n`;
|
|
4733
|
-
}
|
|
4734
|
-
//#endregion
|
|
4735
3599
|
//#region src/compose/index.ts
|
|
4736
3600
|
/**
|
|
4737
3601
|
* `gkm compose` — a workspace's APIs and sites for one stage, as one Docker
|
|
4738
3602
|
* Compose stack behind Caddy.
|
|
4739
3603
|
*
|
|
4740
|
-
*
|
|
4741
|
-
*
|
|
4742
|
-
*
|
|
4743
|
-
*
|
|
4744
|
-
*
|
|
4745
|
-
* - The databases exist before the apps do. Postgres comes up alone, its
|
|
4746
|
-
* databases, roles and grants are created and its migrations applied from
|
|
4747
|
-
* this machine, and only then do the apps start — so an app never boots
|
|
4748
|
-
* against a schema that is not there yet.
|
|
4749
|
-
* - What a stage runs is recorded. Each app's tag and the digest it resolved
|
|
4750
|
-
* to are written to the stage's state, so "what is this stage running" has
|
|
4751
|
-
* an exact answer.
|
|
4752
|
-
*
|
|
4753
|
-
* Docker, the registry, the build and the clock are injected, so the rules are
|
|
4754
|
-
* asserted without a daemon; the defaults are the real ones.
|
|
3604
|
+
* A convenience over `gkm deploy --target compose`: the stage defaults to the
|
|
3605
|
+
* local one, `--build`/`--pull` choose the images whatever `--tag` says, and
|
|
3606
|
+
* `--down` stops the stack. Everything else — the order, the image check, the
|
|
3607
|
+
* state, the health checks — is the compose target's, run through the same
|
|
3608
|
+
* `deploy()` as any other target, so there is one way a stack comes up.
|
|
4755
3609
|
*/
|
|
4756
|
-
const logger$4 = console;
|
|
4757
3610
|
/** `--build` and `--pull` together ask for two different things. */
|
|
4758
3611
|
var ComposeModeConflict = class extends Error {
|
|
4759
3612
|
constructor() {
|
|
@@ -4761,320 +3614,52 @@ var ComposeModeConflict = class extends Error {
|
|
|
4761
3614
|
this.name = "ComposeModeConflict";
|
|
4762
3615
|
}
|
|
4763
3616
|
};
|
|
4764
|
-
/** A build with no commit to tag its images with. */
|
|
4765
|
-
var NoGitRevision = class extends Error {
|
|
4766
|
-
root;
|
|
4767
|
-
constructor(root) {
|
|
4768
|
-
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.`);
|
|
4769
|
-
this.root = root;
|
|
4770
|
-
this.name = "NoGitRevision";
|
|
4771
|
-
}
|
|
4772
|
-
};
|
|
4773
|
-
/** A port variable that does not hold a port. */
|
|
4774
|
-
var EdgePortInvalid = class extends Error {
|
|
4775
|
-
variable;
|
|
4776
|
-
value;
|
|
4777
|
-
constructor(variable, value) {
|
|
4778
|
-
super(`${variable}=${value} is not a port. Set it to a number between 1 and 65535, or unset it for the default.`);
|
|
4779
|
-
this.variable = variable;
|
|
4780
|
-
this.value = value;
|
|
4781
|
-
this.name = "EdgePortInvalid";
|
|
4782
|
-
}
|
|
4783
|
-
};
|
|
4784
|
-
/** The commit HEAD is at, short — and `-dirty` when the tree has changes. */
|
|
4785
|
-
async function gitRevision(root) {
|
|
4786
|
-
let sha;
|
|
4787
|
-
try {
|
|
4788
|
-
sha = (await runOutput("git", [
|
|
4789
|
-
"rev-parse",
|
|
4790
|
-
"--short",
|
|
4791
|
-
"HEAD"
|
|
4792
|
-
], { cwd: root })).trim();
|
|
4793
|
-
} catch {
|
|
4794
|
-
throw new NoGitRevision(root);
|
|
4795
|
-
}
|
|
4796
|
-
if (!sha) throw new NoGitRevision(root);
|
|
4797
|
-
return (await runOutput("git", ["status", "--porcelain"], { cwd: root }).catch(() => "")).trim() ? `${sha}-dirty` : sha;
|
|
4798
|
-
}
|
|
4799
|
-
/**
|
|
4800
|
-
* Bundle a backend with the gkm that is running — the same build a deploy
|
|
4801
|
-
* makes, so the image holds one file and no dependencies.
|
|
4802
|
-
*/
|
|
4803
|
-
async function bundleWithThisCli(appRoot) {
|
|
4804
|
-
const entry = process.argv[1];
|
|
4805
|
-
if (!entry) return;
|
|
4806
|
-
await run$1(process.execPath, [
|
|
4807
|
-
...process.execArgv,
|
|
4808
|
-
entry,
|
|
4809
|
-
"build",
|
|
4810
|
-
"--provider",
|
|
4811
|
-
"server",
|
|
4812
|
-
"--production"
|
|
4813
|
-
], { cwd: appRoot });
|
|
4814
|
-
}
|
|
4815
|
-
const defaultDeps = {
|
|
4816
|
-
docker: dockerCompose,
|
|
4817
|
-
bundle: bundleWithThisCli,
|
|
4818
|
-
revision: gitRevision
|
|
4819
|
-
};
|
|
4820
|
-
/** The edge's ports, from the environment — 443 and 80 when unset. */
|
|
4821
|
-
function edgePorts(env = process.env) {
|
|
4822
|
-
const read = (variable, fallback) => {
|
|
4823
|
-
const value = env[variable];
|
|
4824
|
-
if (value === void 0 || value === "") return fallback;
|
|
4825
|
-
const port = Number(value);
|
|
4826
|
-
if (!Number.isInteger(port) || port < 1 || port > 65535) throw new EdgePortInvalid(variable, value);
|
|
4827
|
-
return port;
|
|
4828
|
-
};
|
|
4829
|
-
return {
|
|
4830
|
-
https: read(EDGE_PORT_ENV.https, 443),
|
|
4831
|
-
http: read(EDGE_PORT_ENV.http, 80)
|
|
4832
|
-
};
|
|
4833
|
-
}
|
|
4834
3617
|
/** `gkm compose`. */
|
|
4835
3618
|
async function composeCommand(options = {}, deps = {}) {
|
|
4836
|
-
const { docker, bundle, revision } = {
|
|
4837
|
-
...defaultDeps,
|
|
4838
|
-
...deps
|
|
4839
|
-
};
|
|
4840
|
-
const env = deps.env ?? process.env;
|
|
4841
3619
|
if (options.build && options.pull) throw new ComposeModeConflict();
|
|
4842
|
-
const
|
|
4843
|
-
const
|
|
3620
|
+
const cwd = resolve(options.cwd ?? process.cwd());
|
|
3621
|
+
const { workspace } = await loadWorkspaceConfig(cwd);
|
|
4844
3622
|
const stage = options.stage ?? workspace.stages.local;
|
|
4845
|
-
if (!(stage === workspace.stages.local)) assertDeployedStage(workspace.stages, stage);
|
|
4846
|
-
const identity = deployIdentity(workspace, stage);
|
|
4847
|
-
const dir = join(root, stackDir(stage));
|
|
4848
|
-
const file = join(dir, "docker-compose.yml");
|
|
4849
|
-
const ref = {
|
|
4850
|
-
project: composeProject(identity),
|
|
4851
|
-
file,
|
|
4852
|
-
cwd: root
|
|
4853
|
-
};
|
|
4854
3623
|
if (options.down) {
|
|
4855
|
-
|
|
4856
|
-
|
|
3624
|
+
const ref = {
|
|
3625
|
+
project: composeProject(deployIdentity(workspace, stage)),
|
|
3626
|
+
file: join(workspace.root, stackDir(stage), "docker-compose.yml"),
|
|
3627
|
+
cwd: workspace.root
|
|
3628
|
+
};
|
|
3629
|
+
await (deps.docker ?? dockerCompose).down(ref);
|
|
3630
|
+
console.log(`🛑 Stopped ${ref.project}. Its volumes are kept.`);
|
|
4857
3631
|
return;
|
|
4858
3632
|
}
|
|
4859
|
-
|
|
4860
|
-
const
|
|
4861
|
-
const
|
|
4862
|
-
|
|
4863
|
-
cwd
|
|
4864
|
-
sources,
|
|
4865
|
-
runnables
|
|
4866
|
-
});
|
|
4867
|
-
const mode = options.build || !options.pull && !options.tag ? "build" : "pull";
|
|
4868
|
-
const tag = options.tag ?? (mode === "pull" ? "latest" : await revision(root));
|
|
4869
|
-
const { secrets, keep } = await stageSecrets(workspace, manifest, stage);
|
|
4870
|
-
const packageManager = detectPackageManager$2(root);
|
|
4871
|
-
const stack = composeStack({
|
|
4872
|
-
workspace,
|
|
4873
|
-
manifest,
|
|
4874
|
-
runnables,
|
|
3633
|
+
let finished;
|
|
3634
|
+
const mode = options.build ? "build" : options.pull ? "pull" : void 0;
|
|
3635
|
+
const tag = options.tag ?? (options.pull ? "latest" : void 0);
|
|
3636
|
+
const result = await deploy$1({
|
|
3637
|
+
cwd,
|
|
4875
3638
|
stage,
|
|
4876
|
-
|
|
4877
|
-
|
|
4878
|
-
|
|
4879
|
-
|
|
4880
|
-
|
|
4881
|
-
|
|
4882
|
-
secrets,
|
|
4883
|
-
ports: edgePorts(env),
|
|
4884
|
-
packageManager,
|
|
4885
|
-
packages: Object.fromEntries(Object.entries(workspace.apps).map(([name, app]) => [name, appPackageName$1(root, name, app.path)]))
|
|
4886
|
-
});
|
|
4887
|
-
if (mode === "pull" && !options.dryRun) await assertImagesExist(docker, tag, stack.apps);
|
|
4888
|
-
if (options.dryRun) keep.skip();
|
|
4889
|
-
else await keep.write();
|
|
4890
|
-
const files = await writeStack(root, dir, stack);
|
|
4891
|
-
printPlan(stack, mode, files);
|
|
4892
|
-
if (options.dryRun) return {
|
|
4893
|
-
stack,
|
|
4894
|
-
files
|
|
4895
|
-
};
|
|
4896
|
-
const store = deps.state ? await deps.state(workspace) : await createStateStore({
|
|
4897
|
-
config: workspace.state,
|
|
4898
|
-
workspaceRoot: root,
|
|
4899
|
-
workspaceName: workspace.name
|
|
4900
|
-
});
|
|
4901
|
-
const lock = await store.lock(stage, { operation: "compose" });
|
|
4902
|
-
try {
|
|
4903
|
-
const apps = stack.apps.map((app) => app.name);
|
|
4904
|
-
if (mode === "build") {
|
|
4905
|
-
for (const app of stack.apps.filter((a) => a.kind === "rest-api")) {
|
|
4906
|
-
logger$4.log(`\n📦 Bundling ${app.name}…`);
|
|
4907
|
-
await bundle(join(root, app.path));
|
|
3639
|
+
target: "compose",
|
|
3640
|
+
targets: { compose: composeTarget({
|
|
3641
|
+
...deps,
|
|
3642
|
+
...mode ? { mode } : {},
|
|
3643
|
+
report: (done) => {
|
|
3644
|
+
finished = done;
|
|
4908
3645
|
}
|
|
4909
|
-
|
|
4910
|
-
|
|
4911
|
-
}
|
|
4912
|
-
|
|
4913
|
-
|
|
4914
|
-
|
|
4915
|
-
|
|
4916
|
-
logger$4.log(`\n🗄️ Starting ${stack.infra.join(", ")}…`);
|
|
4917
|
-
await docker.up(ref, stack.infra);
|
|
3646
|
+
}) },
|
|
3647
|
+
...tag ? { tag } : {},
|
|
3648
|
+
...options.dryRun ? { dryRun: true } : {},
|
|
3649
|
+
logger: {
|
|
3650
|
+
info: (message) => console.log(message),
|
|
3651
|
+
warn: (message) => console.warn(message),
|
|
3652
|
+
error: (message) => console.error(message)
|
|
4918
3653
|
}
|
|
4919
|
-
|
|
4920
|
-
|
|
4921
|
-
ref,
|
|
4922
|
-
sql: deps.sql ?? pgClient,
|
|
4923
|
-
migrate: deps.migrate ?? migrateDatabases
|
|
4924
|
-
});
|
|
4925
|
-
logger$4.log("\n🚀 Starting the stack…");
|
|
4926
|
-
await docker.up(ref);
|
|
4927
|
-
if (stack.local) await docker.copyOut(ref, "caddy", "/data/caddy/pki/authorities/local/root.crt", join(dir, "caddy-root.crt")).catch(() => {});
|
|
4928
|
-
const images = await recordImages(store, stack, docker, mode);
|
|
4929
|
-
logger$4.log(`\n✅ ${ref.project} is running:`);
|
|
4930
|
-
for (const app of stack.apps) logger$4.log(` ${app.name.padEnd(12)} ${app.url}`);
|
|
4931
|
-
if (stack.local) logger$4.log(`\n🔐 Certificates are from Caddy's local CA: NODE_EXTRA_CA_CERTS=${join(stackDir(stage), "caddy-root.crt")}`);
|
|
4932
|
-
return {
|
|
4933
|
-
stack,
|
|
4934
|
-
files,
|
|
4935
|
-
images
|
|
4936
|
-
};
|
|
4937
|
-
} finally {
|
|
4938
|
-
await lock.release();
|
|
4939
|
-
}
|
|
4940
|
-
}
|
|
4941
|
-
/**
|
|
4942
|
-
* The stage's secrets — and, for a deployed stage, everything it generates
|
|
4943
|
-
* once: its seed, and each declared secret and keyring. Written back before
|
|
4944
|
-
* anything is derived from them, so the next run derives the same passwords.
|
|
4945
|
-
*/
|
|
4946
|
-
async function stageSecrets(workspace, manifest, stage) {
|
|
4947
|
-
const store = await secretsStoreFor(workspace, stage);
|
|
4948
|
-
const stored = await store.read(stage);
|
|
4949
|
-
const nothing = {
|
|
4950
|
-
write: async () => {},
|
|
4951
|
-
skip: () => {}
|
|
4952
|
-
};
|
|
4953
|
-
if (stage === workspace.stages.local) return {
|
|
4954
|
-
secrets: stored,
|
|
4955
|
-
keep: nothing
|
|
4956
|
-
};
|
|
4957
|
-
const { secrets, generated } = withGeneratedSecrets(stored ?? initStageSecrets(stage), manifest);
|
|
4958
|
-
if (generated.length === 0) return {
|
|
4959
|
-
secrets,
|
|
4960
|
-
keep: nothing
|
|
4961
|
-
};
|
|
3654
|
+
}).result;
|
|
3655
|
+
if (!finished) return void 0;
|
|
4962
3656
|
return {
|
|
4963
|
-
|
|
4964
|
-
|
|
4965
|
-
|
|
4966
|
-
|
|
4967
|
-
logger$4.log(`🔑 Generated for "${stage}" (${store.name}): ${generated.join(", ")}`);
|
|
4968
|
-
},
|
|
4969
|
-
skip() {
|
|
4970
|
-
logger$4.log(`🔑 "${stage}" has no ${generated.join(", ")} yet; this dry run used values it did not keep.`);
|
|
4971
|
-
}
|
|
4972
|
-
}
|
|
3657
|
+
stack: finished.stack,
|
|
3658
|
+
files: finished.files,
|
|
3659
|
+
...options.dryRun ? {} : { images: finished.images },
|
|
3660
|
+
deploy: result
|
|
4973
3661
|
};
|
|
4974
3662
|
}
|
|
4975
|
-
/** Write the stack's files: owner-only, since the env files hold secrets. */
|
|
4976
|
-
async function writeStack(root, dir, stack) {
|
|
4977
|
-
await mkdir(dir, {
|
|
4978
|
-
recursive: true,
|
|
4979
|
-
mode: 448
|
|
4980
|
-
});
|
|
4981
|
-
await chmod(dir, 448);
|
|
4982
|
-
const files = [];
|
|
4983
|
-
const write = async (path, content, mode = 420) => {
|
|
4984
|
-
await mkdir(dirname(path), { recursive: true });
|
|
4985
|
-
await writeFile(path, content, { mode });
|
|
4986
|
-
await chmod(path, mode);
|
|
4987
|
-
files.push(path);
|
|
4988
|
-
};
|
|
4989
|
-
await write(join(dir, "docker-compose.yml"), composeYaml(stack));
|
|
4990
|
-
await write(join(dir, "Caddyfile"), stack.caddyfile);
|
|
4991
|
-
for (const app of stack.apps) if (app.env) await write(join(dir, `${app.name}.env`), envFile(app.env), 384);
|
|
4992
|
-
for (const [path, content] of Object.entries(stack.dockerfiles)) await write(join(root, path), content);
|
|
4993
|
-
await ignoreStacks(root);
|
|
4994
|
-
return files;
|
|
4995
|
-
}
|
|
4996
|
-
/** Make sure the build context leaves `.gkm/compose` out. */
|
|
4997
|
-
async function ignoreStacks(root) {
|
|
4998
|
-
const path = join(root, ".dockerignore");
|
|
4999
|
-
if (!existsSync(path)) {
|
|
5000
|
-
await writeFile(path, generateDockerignore());
|
|
5001
|
-
return;
|
|
5002
|
-
}
|
|
5003
|
-
const current = await readFile(path, "utf-8");
|
|
5004
|
-
if (/^\/?\.gkm\/compose\/?$|^\/?\.gkm\/?$/m.test(current)) return;
|
|
5005
|
-
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`);
|
|
5006
|
-
}
|
|
5007
|
-
function composeYaml(stack) {
|
|
5008
|
-
return `# Generated by gkm compose from the construct manifest — do not edit.
|
|
5009
|
-
# The ${stack.stage} stage's APIs and sites behind one Caddy. Each backend
|
|
5010
|
-
# reads exactly the keys in its own env file beside this one.
|
|
5011
|
-
#
|
|
5012
|
-
# gkm compose${stack.local ? "" : ` --stage ${stack.stage}`} start or update it
|
|
5013
|
-
# gkm compose${stack.local ? "" : ` --stage ${stack.stage}`} --down stop it
|
|
5014
|
-
${stringify(stack.compose, {
|
|
5015
|
-
lineWidth: 0,
|
|
5016
|
-
aliasDuplicateObjects: false
|
|
5017
|
-
})}`;
|
|
5018
|
-
}
|
|
5019
|
-
/** Create what the plan names in the stack's Postgres, then migrate it. */
|
|
5020
|
-
async function prepareDatabases(stack, workspace, manifest, sources, deps) {
|
|
5021
|
-
const port = await deps.docker.port(deps.ref, "postgres", 5432);
|
|
5022
|
-
const statements = postgresStatements(stack.plan, workspace.name, stack.credential.seed);
|
|
5023
|
-
if (statements.length > 0) {
|
|
5024
|
-
logger$4.log("🗄️ Creating databases, roles and grants…");
|
|
5025
|
-
await applyPostgres(deps.sql(port, stack.credential.master), statements);
|
|
5026
|
-
}
|
|
5027
|
-
const env = envFor(stack.plan, {
|
|
5028
|
-
ports: { [primaryPortKey("postgres")]: port },
|
|
5029
|
-
project: workspace.name,
|
|
5030
|
-
master: stack.credential.master,
|
|
5031
|
-
...stack.credential.seed ? { seed: stack.credential.seed } : {}
|
|
5032
|
-
});
|
|
5033
|
-
const runs = await deps.migrate({
|
|
5034
|
-
root: workspace.root,
|
|
5035
|
-
manifest,
|
|
5036
|
-
sources,
|
|
5037
|
-
env
|
|
5038
|
-
});
|
|
5039
|
-
for (const { target, applied } of runs) if (applied.length > 0) logger$4.log(`🗄️ ${target.migrations}: applied ${applied.length}`);
|
|
5040
|
-
}
|
|
5041
|
-
/**
|
|
5042
|
-
* Record each app's image in the stage's state: the tag it ran and the digest
|
|
5043
|
-
* the tag resolved to — or, for an image built here and never pushed, its
|
|
5044
|
-
* content id.
|
|
5045
|
-
*/
|
|
5046
|
-
async function recordImages(store, stack, docker, mode) {
|
|
5047
|
-
if (!await store.read(stack.stage)) await store.write(stack.stage, createEmptyState(stack.stage, "", ""), { expectedVersion: null });
|
|
5048
|
-
const images = {};
|
|
5049
|
-
for (const app of stack.apps) {
|
|
5050
|
-
const digest = await docker.digest(app.ref);
|
|
5051
|
-
images[app.name] = {
|
|
5052
|
-
ref: app.ref,
|
|
5053
|
-
tag: app.tag,
|
|
5054
|
-
...digest ? { digest } : {}
|
|
5055
|
-
};
|
|
5056
|
-
await store.putResource(stack.stage, {
|
|
5057
|
-
key: `compose:${app.name}`,
|
|
5058
|
-
type: "compose-image",
|
|
5059
|
-
status: "ready",
|
|
5060
|
-
...digest ? { id: digest } : {},
|
|
5061
|
-
data: {
|
|
5062
|
-
project: stack.project,
|
|
5063
|
-
ref: app.ref,
|
|
5064
|
-
tag: app.tag,
|
|
5065
|
-
source: mode,
|
|
5066
|
-
...digest ? { digest } : {}
|
|
5067
|
-
}
|
|
5068
|
-
});
|
|
5069
|
-
}
|
|
5070
|
-
return images;
|
|
5071
|
-
}
|
|
5072
|
-
function printPlan(stack, mode, files) {
|
|
5073
|
-
logger$4.log(`\n🧱 ${stack.project} — stage ${stack.stage}`);
|
|
5074
|
-
for (const app of stack.apps) logger$4.log(` ${app.name.padEnd(12)} ${app.url.padEnd(40)} ${mode === "build" ? "build" : "pull"} ${app.ref}`);
|
|
5075
|
-
if (stack.infra.length > 0) logger$4.log(` infrastructure: ${stack.infra.join(", ")}`);
|
|
5076
|
-
logger$4.log(`\n📝 Wrote ${files.length} file(s) under ${stackDir(stack.stage)}/`);
|
|
5077
|
-
}
|
|
5078
3663
|
/**
|
|
5079
3664
|
* Format a fatal error for display.
|
|
5080
3665
|
* Always includes the full stack trace since these are process-ending errors.
|
|
@@ -6480,25 +5065,25 @@ function loadPackageJson() {
|
|
|
6480
5065
|
* Run: pnpm --filter @geekmidas/cli sync-versions
|
|
6481
5066
|
*/
|
|
6482
5067
|
const GEEKMIDAS_VERSIONS = {
|
|
6483
|
-
"@geekmidas/audit": "~10.0.0-alpha.
|
|
6484
|
-
"@geekmidas/auth": "~10.0.0-alpha.
|
|
6485
|
-
"@geekmidas/cache": "~10.0.0-alpha.
|
|
6486
|
-
"@geekmidas/client": "~10.0.0-alpha.
|
|
6487
|
-
"@geekmidas/cloud": "~10.0.0-alpha.
|
|
6488
|
-
"@geekmidas/constructs": "~10.0.0-alpha.
|
|
6489
|
-
"@geekmidas/db": "~10.0.0-alpha.
|
|
6490
|
-
"@geekmidas/emailkit": "~10.0.0-alpha.
|
|
6491
|
-
"@geekmidas/envkit": "~10.0.0-alpha.
|
|
6492
|
-
"@geekmidas/errors": "~10.0.0-alpha.
|
|
6493
|
-
"@geekmidas/events": "~10.0.0-alpha.
|
|
6494
|
-
"@geekmidas/logger": "~10.0.0-alpha.
|
|
6495
|
-
"@geekmidas/manifest": "~10.0.0-alpha.
|
|
6496
|
-
"@geekmidas/rate-limit": "~10.0.0-alpha.
|
|
6497
|
-
"@geekmidas/schema": "~10.0.0-alpha.
|
|
6498
|
-
"@geekmidas/services": "~10.0.0-alpha.
|
|
6499
|
-
"@geekmidas/storage": "~10.0.0-alpha.
|
|
6500
|
-
"@geekmidas/telescope": "~10.0.0-alpha.
|
|
6501
|
-
"@geekmidas/testkit": "~10.0.0-alpha.
|
|
5068
|
+
"@geekmidas/audit": "~10.0.0-alpha.65",
|
|
5069
|
+
"@geekmidas/auth": "~10.0.0-alpha.65",
|
|
5070
|
+
"@geekmidas/cache": "~10.0.0-alpha.65",
|
|
5071
|
+
"@geekmidas/client": "~10.0.0-alpha.65",
|
|
5072
|
+
"@geekmidas/cloud": "~10.0.0-alpha.65",
|
|
5073
|
+
"@geekmidas/constructs": "~10.0.0-alpha.65",
|
|
5074
|
+
"@geekmidas/db": "~10.0.0-alpha.65",
|
|
5075
|
+
"@geekmidas/emailkit": "~10.0.0-alpha.65",
|
|
5076
|
+
"@geekmidas/envkit": "~10.0.0-alpha.65",
|
|
5077
|
+
"@geekmidas/errors": "~10.0.0-alpha.65",
|
|
5078
|
+
"@geekmidas/events": "~10.0.0-alpha.65",
|
|
5079
|
+
"@geekmidas/logger": "~10.0.0-alpha.65",
|
|
5080
|
+
"@geekmidas/manifest": "~10.0.0-alpha.65",
|
|
5081
|
+
"@geekmidas/rate-limit": "~10.0.0-alpha.65",
|
|
5082
|
+
"@geekmidas/schema": "~10.0.0-alpha.65",
|
|
5083
|
+
"@geekmidas/services": "~10.0.0-alpha.65",
|
|
5084
|
+
"@geekmidas/storage": "~10.0.0-alpha.65",
|
|
5085
|
+
"@geekmidas/telescope": "~10.0.0-alpha.65",
|
|
5086
|
+
"@geekmidas/testkit": "~10.0.0-alpha.65",
|
|
6502
5087
|
"@geekmidas/cli": `~${loadPackageJson().version}`
|
|
6503
5088
|
};
|
|
6504
5089
|
//#endregion
|
|
@@ -7181,8 +5766,10 @@ ${pm.setup}
|
|
|
7181
5766
|
- name: Install
|
|
7182
5767
|
run: ${pm.install}
|
|
7183
5768
|
${stageSecrets}
|
|
5769
|
+
# Builds, releases and health-checks the stage through deploy.default
|
|
5770
|
+
# in gkm.config.ts, and fails the job if any of it fails.
|
|
7184
5771
|
- name: Deploy
|
|
7185
|
-
run: ${pm.
|
|
5772
|
+
run: ${pm.exec} gkm deploy --stage "$STAGE"
|
|
7186
5773
|
env:
|
|
7187
5774
|
STAGE: \${{ matrix.stage }}${deployEnv}
|
|
7188
5775
|
`;
|
|
@@ -8035,7 +6622,7 @@ function deployPackage(options) {
|
|
|
8035
6622
|
case "sst": {
|
|
8036
6623
|
const v = GEEKMIDAS_VERSIONS;
|
|
8037
6624
|
return {
|
|
8038
|
-
scripts: perStage(options, (stage) => `gkm
|
|
6625
|
+
scripts: perStage(options, (stage) => `gkm deploy --stage ${stage}`),
|
|
8039
6626
|
dependencies: {
|
|
8040
6627
|
"@geekmidas/cloud": v["@geekmidas/cloud"],
|
|
8041
6628
|
"@geekmidas/db": v["@geekmidas/db"],
|
|
@@ -8078,7 +6665,7 @@ function generateDeployFiles(options) {
|
|
|
8078
6665
|
* constructs the application declares, through \`fromManifest\`. What is here
|
|
8079
6666
|
* is what varies by stage, and the inputs a declaration cannot carry.
|
|
8080
6667
|
*
|
|
8081
|
-
* Deploy with \`
|
|
6668
|
+
* Deploy with \`gkm deploy --stage <stage>\`, which builds the manifest first.
|
|
8082
6669
|
*/
|
|
8083
6670
|
const region = '${options.region}';
|
|
8084
6671
|
|
|
@@ -8112,7 +6699,7 @@ ${hasDatabase ? " const vpc = new sst.aws.Vpc('Vpc', { nat: 'ec2' });\n\n" :
|
|
|
8112
6699
|
hostedZoneId: '',
|
|
8113
6700
|
});
|
|
8114
6701
|
|
|
8115
|
-
|
|
6702
|
+
const provisioned = fromManifest(
|
|
8116
6703
|
new Stack(app, '${canonicalId(options.name)}'),
|
|
8117
6704
|
constructs,
|
|
8118
6705
|
{
|
|
@@ -8120,6 +6707,15 @@ ${inputs.join("\n")}
|
|
|
8120
6707
|
},
|
|
8121
6708
|
backends,
|
|
8122
6709
|
);
|
|
6710
|
+
|
|
6711
|
+
// Each surface's URL, by its id: SST prints these and writes them to
|
|
6712
|
+
// .sst/outputs.json, where \`gkm deploy\` reads them to health-check
|
|
6713
|
+
// what it released.
|
|
6714
|
+
return Object.fromEntries(
|
|
6715
|
+
Object.entries(constructs)
|
|
6716
|
+
.filter(([, c]) => c.kind === 'rest-api' || c.kind === 'site')
|
|
6717
|
+
.map(([id]) => [id, provisioned[id]!.provides().url]),
|
|
6718
|
+
);
|
|
8123
6719
|
},
|
|
8124
6720
|
});
|
|
8125
6721
|
`
|
|
@@ -8530,7 +7126,7 @@ ${workspaceConstructsGlobs(options.routesStructure, dirname(options.apiPath)).ma
|
|
|
8530
7126
|
function deployBlock(options) {
|
|
8531
7127
|
if (options.deployTarget !== "sst") return "";
|
|
8532
7128
|
return `
|
|
8533
|
-
//
|
|
7129
|
+
// \`gkm deploy\` runs \`sst deploy\` on the manifest \`gkm build\` writes to
|
|
8534
7130
|
// \`.gkm/manifest/aws.ts\`. Each construct becomes a managed AWS resource.
|
|
8535
7131
|
deploy: { default: 'sst' },
|
|
8536
7132
|
`;
|
|
@@ -11159,155 +9755,6 @@ function isFullstackTemplate(name) {
|
|
|
11159
9755
|
return name === "fullstack";
|
|
11160
9756
|
}
|
|
11161
9757
|
//#endregion
|
|
11162
|
-
//#region src/init/utils.ts
|
|
11163
|
-
/**
|
|
11164
|
-
* Detect the package manager being used based on lockfiles or npm_config_user_agent
|
|
11165
|
-
*/
|
|
11166
|
-
function detectPackageManager(cwd = process.cwd()) {
|
|
11167
|
-
if (existsSync(join(cwd, "pnpm-lock.yaml"))) return "pnpm";
|
|
11168
|
-
if (existsSync(join(cwd, "yarn.lock"))) return "yarn";
|
|
11169
|
-
if (existsSync(join(cwd, "bun.lockb"))) return "bun";
|
|
11170
|
-
if (existsSync(join(cwd, "package-lock.json"))) return "npm";
|
|
11171
|
-
const userAgent = process.env.npm_config_user_agent || "";
|
|
11172
|
-
if (userAgent.includes("pnpm")) return "pnpm";
|
|
11173
|
-
if (userAgent.includes("yarn")) return "yarn";
|
|
11174
|
-
if (userAgent.includes("bun")) return "bun";
|
|
11175
|
-
return "npm";
|
|
11176
|
-
}
|
|
11177
|
-
/** `gkm init`'s flags, which a name can swallow when a space goes missing. */
|
|
11178
|
-
const INIT_FLAGS = [
|
|
11179
|
-
"yes",
|
|
11180
|
-
"name",
|
|
11181
|
-
"template",
|
|
11182
|
-
"monorepo",
|
|
11183
|
-
"api-path",
|
|
11184
|
-
"pm",
|
|
11185
|
-
"skip-install"
|
|
11186
|
-
];
|
|
11187
|
-
/**
|
|
11188
|
-
* Validate project name for npm package naming conventions
|
|
11189
|
-
*/
|
|
11190
|
-
function validateProjectName(name) {
|
|
11191
|
-
if (!name) return "Project name is required";
|
|
11192
|
-
if (!/^[a-z0-9-_@/.]+$/i.test(name)) return "Project name can only contain letters, numbers, hyphens, underscores, @, /, and .";
|
|
11193
|
-
const flag = name.match(/--(\w[\w-]*)$/)?.[1];
|
|
11194
|
-
if (flag && INIT_FLAGS.includes(flag)) return `"${name}" ends in the --${flag} flag. Did you mean \`${name.slice(0, -(flag.length + 2))} --${flag}\`?`;
|
|
11195
|
-
if ([
|
|
11196
|
-
"node_modules",
|
|
11197
|
-
".git",
|
|
11198
|
-
"package.json",
|
|
11199
|
-
"src"
|
|
11200
|
-
].includes(name.toLowerCase())) return `"${name}" is a reserved name`;
|
|
11201
|
-
return true;
|
|
11202
|
-
}
|
|
11203
|
-
/**
|
|
11204
|
-
* Check if a directory already exists at the target path
|
|
11205
|
-
*/
|
|
11206
|
-
function checkDirectoryExists(name, cwd = process.cwd()) {
|
|
11207
|
-
const targetPath = join(cwd, name);
|
|
11208
|
-
if (existsSync(targetPath)) return `Directory "${name}" already exists`;
|
|
11209
|
-
return true;
|
|
11210
|
-
}
|
|
11211
|
-
/**
|
|
11212
|
-
* Get the install command for a package manager
|
|
11213
|
-
*/
|
|
11214
|
-
function getInstallCommand(pkgManager) {
|
|
11215
|
-
switch (pkgManager) {
|
|
11216
|
-
case "pnpm": return "pnpm install";
|
|
11217
|
-
case "yarn": return "yarn";
|
|
11218
|
-
case "bun": return "bun install";
|
|
11219
|
-
default: return "npm install";
|
|
11220
|
-
}
|
|
11221
|
-
}
|
|
11222
|
-
/**
|
|
11223
|
-
* Get the dev command for a package manager
|
|
11224
|
-
*/
|
|
11225
|
-
function getRunCommand(pkgManager, script) {
|
|
11226
|
-
switch (pkgManager) {
|
|
11227
|
-
case "pnpm": return `pnpm ${script}`;
|
|
11228
|
-
case "yarn": return `yarn ${script}`;
|
|
11229
|
-
case "bun": return `bun run ${script}`;
|
|
11230
|
-
default: return `npm run ${script}`;
|
|
11231
|
-
}
|
|
11232
|
-
}
|
|
11233
|
-
/**
|
|
11234
|
-
* Run a binary the project installed, through its package manager — so it is
|
|
11235
|
-
* the project's own version, not whatever \`npx\` would fetch.
|
|
11236
|
-
*/
|
|
11237
|
-
function getExecCommand(pkgManager, command) {
|
|
11238
|
-
switch (pkgManager) {
|
|
11239
|
-
case "pnpm": return `pnpm exec ${command}`;
|
|
11240
|
-
case "yarn": return `yarn ${command}`;
|
|
11241
|
-
case "bun": return `bunx ${command}`;
|
|
11242
|
-
default: return `npx --no-install ${command}`;
|
|
11243
|
-
}
|
|
11244
|
-
}
|
|
11245
|
-
const lockfileByPm = {
|
|
11246
|
-
pnpm: "pnpm-lock.yaml",
|
|
11247
|
-
yarn: "yarn.lock",
|
|
11248
|
-
npm: "package-lock.json",
|
|
11249
|
-
bun: "bun.lockb"
|
|
11250
|
-
};
|
|
11251
|
-
/**
|
|
11252
|
-
* Find the workspace/project root by walking up from cwd.
|
|
11253
|
-
* Checks for PM-specific workspace config, package.json#workspaces,
|
|
11254
|
-
* and lockfiles.
|
|
11255
|
-
*/
|
|
11256
|
-
function findWorkspaceRoot(cwd, pm) {
|
|
11257
|
-
let dir = cwd;
|
|
11258
|
-
const root = parse(dir).root;
|
|
11259
|
-
const lockfile = lockfileByPm[pm];
|
|
11260
|
-
while (dir !== root) {
|
|
11261
|
-
if (pm === "pnpm" && existsSync(join(dir, "pnpm-workspace.yaml"))) return dir;
|
|
11262
|
-
const pkgJsonPath = join(dir, "package.json");
|
|
11263
|
-
if (existsSync(pkgJsonPath)) try {
|
|
11264
|
-
if (JSON.parse(readFileSync(pkgJsonPath, "utf-8")).workspaces) return dir;
|
|
11265
|
-
} catch {}
|
|
11266
|
-
if (existsSync(join(dir, lockfile))) return dir;
|
|
11267
|
-
dir = dirname(dir);
|
|
11268
|
-
}
|
|
11269
|
-
return cwd;
|
|
11270
|
-
}
|
|
11271
|
-
/**
|
|
11272
|
-
* Get workspace package glob patterns from pnpm-workspace.yaml
|
|
11273
|
-
* or package.json#workspaces.
|
|
11274
|
-
*/
|
|
11275
|
-
function getWorkspaceGlobs(root) {
|
|
11276
|
-
const pnpmWorkspacePath = join(root, "pnpm-workspace.yaml");
|
|
11277
|
-
if (existsSync(pnpmWorkspacePath)) {
|
|
11278
|
-
const content = readFileSync(pnpmWorkspacePath, "utf-8");
|
|
11279
|
-
return parse$1(content)?.packages ?? [];
|
|
11280
|
-
}
|
|
11281
|
-
const rootPkgJsonPath = join(root, "package.json");
|
|
11282
|
-
if (existsSync(rootPkgJsonPath)) {
|
|
11283
|
-
const pkg = JSON.parse(readFileSync(rootPkgJsonPath, "utf-8"));
|
|
11284
|
-
if (Array.isArray(pkg.workspaces)) return pkg.workspaces;
|
|
11285
|
-
if (pkg.workspaces?.packages) return pkg.workspaces.packages;
|
|
11286
|
-
}
|
|
11287
|
-
return [];
|
|
11288
|
-
}
|
|
11289
|
-
/**
|
|
11290
|
-
* Find all package.json files across a workspace.
|
|
11291
|
-
* Returns the root package.json plus all workspace member package.json paths.
|
|
11292
|
-
*/
|
|
11293
|
-
function findWorkspacePackages(cwd, pm) {
|
|
11294
|
-
const workspaceRoot = findWorkspaceRoot(cwd, pm);
|
|
11295
|
-
const results = [];
|
|
11296
|
-
const rootPkgJson = join(workspaceRoot, "package.json");
|
|
11297
|
-
if (existsSync(rootPkgJson)) results.push(rootPkgJson);
|
|
11298
|
-
const globs = getWorkspaceGlobs(workspaceRoot);
|
|
11299
|
-
for (const glob of globs) {
|
|
11300
|
-
const pattern = `${glob}/package.json`;
|
|
11301
|
-
const matches = fg.sync(pattern, {
|
|
11302
|
-
cwd: workspaceRoot,
|
|
11303
|
-
absolute: true,
|
|
11304
|
-
ignore: ["**/node_modules/**"]
|
|
11305
|
-
});
|
|
11306
|
-
results.push(...matches);
|
|
11307
|
-
}
|
|
11308
|
-
return [...new Set(results)];
|
|
11309
|
-
}
|
|
11310
|
-
//#endregion
|
|
11311
9758
|
//#region src/init/index.ts
|
|
11312
9759
|
/** `' staging, prod '` → `['staging', 'prod']`. */
|
|
11313
9760
|
const stageList = (value) => value.split(",").map((name) => name.trim()).filter(Boolean);
|
|
@@ -11318,7 +9765,7 @@ const AWS_REGION = /^[a-z]{2}(-gov)?-[a-z]+-\d$/;
|
|
|
11318
9765
|
*/
|
|
11319
9766
|
async function initCommand(projectName, options = {}) {
|
|
11320
9767
|
const cwd = process.cwd();
|
|
11321
|
-
const detectedPkgManager = detectPackageManager(cwd);
|
|
9768
|
+
const detectedPkgManager = detectPackageManager$1(cwd);
|
|
11322
9769
|
if (options.deploy && !deployTargetChoices.some((choice) => choice.value === options.deploy)) throw new UnknownDeployTarget(options.deploy, deployTargetChoices.map((c) => c.value));
|
|
11323
9770
|
if (options.region && !AWS_REGION.test(options.region)) throw new NotAnAwsRegion(options.region);
|
|
11324
9771
|
if (options.stages || options.localStage || options.protectedStage) {
|
|
@@ -11664,7 +10111,7 @@ function printNextSteps(projectName, options, pkgManager) {
|
|
|
11664
10111
|
}
|
|
11665
10112
|
if (options.deployTarget === "sst") {
|
|
11666
10113
|
console.log("🚀 Deployment (AWS, through SST):");
|
|
11667
|
-
for (const stage of deployed) console.log(` ${getRunCommand(pkgManager, `deploy:${stage}`)} # gkm build,
|
|
10114
|
+
for (const stage of deployed) console.log(` ${getRunCommand(pkgManager, `deploy:${stage}`)} # gkm deploy: build, sst deploy, health checks`);
|
|
11668
10115
|
console.log(` Uses your AWS credentials, in ${options.region}.`);
|
|
11669
10116
|
if (options.constructs.mail) console.log(" Set MAIL_FROM to a sender verified in SES first.");
|
|
11670
10117
|
console.log("");
|
|
@@ -12399,8 +10846,8 @@ function testDatabases(reconciled) {
|
|
|
12399
10846
|
* starts without the reference data the app needs.
|
|
12400
10847
|
*/
|
|
12401
10848
|
async function migrateTestStage(workspace, env) {
|
|
12402
|
-
const { migrateAndSeed } = await import("./migrate-
|
|
12403
|
-
const { discover } = await import("./discover-
|
|
10849
|
+
const { migrateAndSeed } = await import("./migrate-B2qL5wj4.mjs");
|
|
10850
|
+
const { discover } = await import("./discover-LMrqsWSU.mjs").then((n) => n.i);
|
|
12404
10851
|
const sources = {};
|
|
12405
10852
|
const manifest = await discover({
|
|
12406
10853
|
patterns: constructGlobs(workspace),
|
|
@@ -12558,7 +11005,7 @@ function withVersion(range, version) {
|
|
|
12558
11005
|
}
|
|
12559
11006
|
async function upgradeCommand(options = {}) {
|
|
12560
11007
|
const cwd = process.cwd();
|
|
12561
|
-
const pm = detectPackageManager(cwd);
|
|
11008
|
+
const pm = detectPackageManager$1(cwd);
|
|
12562
11009
|
const root = findWorkspaceRoot(cwd, pm);
|
|
12563
11010
|
logger.log("\n📦 Scanning workspace for @geekmidas packages...\n");
|
|
12564
11011
|
logger.log(` Package manager: ${pm}`);
|
|
@@ -12830,7 +11277,7 @@ program.command("migrate").description("Apply each database construct's migratio
|
|
|
12830
11277
|
try {
|
|
12831
11278
|
const globalOptions = program.opts();
|
|
12832
11279
|
if (globalOptions.cwd) process.chdir(globalOptions.cwd);
|
|
12833
|
-
const { migrateCommand } = await import("./migrate-
|
|
11280
|
+
const { migrateCommand } = await import("./migrate-B2qL5wj4.mjs");
|
|
12834
11281
|
await migrateCommand({
|
|
12835
11282
|
...options,
|
|
12836
11283
|
...construct ? { construct } : {}
|
|
@@ -12844,7 +11291,7 @@ program.command("seed").description("Migrate, then run each database construct's
|
|
|
12844
11291
|
try {
|
|
12845
11292
|
const globalOptions = program.opts();
|
|
12846
11293
|
if (globalOptions.cwd) process.chdir(globalOptions.cwd);
|
|
12847
|
-
const { seedCommand } = await import("./migrate-
|
|
11294
|
+
const { seedCommand } = await import("./migrate-B2qL5wj4.mjs");
|
|
12848
11295
|
await seedCommand({
|
|
12849
11296
|
...options,
|
|
12850
11297
|
...construct ? { construct } : {}
|
|
@@ -12858,7 +11305,7 @@ program.command("migration").description("Write a construct's next migration: an
|
|
|
12858
11305
|
try {
|
|
12859
11306
|
const globalOptions = program.opts();
|
|
12860
11307
|
if (globalOptions.cwd) process.chdir(globalOptions.cwd);
|
|
12861
|
-
const { migrationCommand } = await import("./migrate-
|
|
11308
|
+
const { migrationCommand } = await import("./migrate-B2qL5wj4.mjs");
|
|
12862
11309
|
await migrationCommand({
|
|
12863
11310
|
construct,
|
|
12864
11311
|
...name ? { name } : {}
|
|
@@ -12966,7 +11413,7 @@ program.command("encryption:rotate").description("Add a new current key to an En
|
|
|
12966
11413
|
try {
|
|
12967
11414
|
const globalOptions = program.opts();
|
|
12968
11415
|
if (globalOptions.cwd) process.chdir(globalOptions.cwd);
|
|
12969
|
-
const { encryptionRotateCommand } = await import("./encryption-
|
|
11416
|
+
const { encryptionRotateCommand } = await import("./encryption-DF_hgIpl.mjs");
|
|
12970
11417
|
await encryptionRotateCommand(construct, options);
|
|
12971
11418
|
} catch (error) {
|
|
12972
11419
|
console.error(formatError(error));
|
|
@@ -12977,7 +11424,7 @@ program.command("encryption:retire").description("Remove an old key from an Encr
|
|
|
12977
11424
|
try {
|
|
12978
11425
|
const globalOptions = program.opts();
|
|
12979
11426
|
if (globalOptions.cwd) process.chdir(globalOptions.cwd);
|
|
12980
|
-
const { encryptionRetireCommand } = await import("./encryption-
|
|
11427
|
+
const { encryptionRetireCommand } = await import("./encryption-DF_hgIpl.mjs");
|
|
12981
11428
|
await encryptionRetireCommand(construct, key, options);
|
|
12982
11429
|
} catch (error) {
|
|
12983
11430
|
console.error(formatError(error));
|
|
@@ -13020,7 +11467,7 @@ program.command("secrets:reconcile").description("Backfill missing custom secret
|
|
|
13020
11467
|
if (globalOptions.cwd) process.chdir(globalOptions.cwd);
|
|
13021
11468
|
const { loadWorkspaceConfig } = await import("./config.mjs");
|
|
13022
11469
|
const { reconcileMissingSecrets } = await import("./reconcile--I7V4Klj.mjs");
|
|
13023
|
-
const { secretsStoreFor } = await import("./store-
|
|
11470
|
+
const { secretsStoreFor } = await import("./store-CLiHnqOY.mjs").then((n) => n.r);
|
|
13024
11471
|
const { workspace } = await loadWorkspaceConfig();
|
|
13025
11472
|
const stage = options.stage ?? workspace.stages.local;
|
|
13026
11473
|
const store = await secretsStoreFor(workspace, stage);
|
|
@@ -13029,7 +11476,7 @@ program.command("secrets:reconcile").description("Backfill missing custom secret
|
|
|
13029
11476
|
console.error(`No secrets found for stage "${stage}". Run "gkm secrets:init --stage ${stage}" first.`);
|
|
13030
11477
|
process.exit(1);
|
|
13031
11478
|
}
|
|
13032
|
-
const { derivedContainers } = await import("./
|
|
11479
|
+
const { derivedContainers } = await import("./state-B8YwmUZU.mjs").then((n) => n.A);
|
|
13033
11480
|
const result = reconcileMissingSecrets(secrets, workspace, await derivedContainers(workspace, stage));
|
|
13034
11481
|
if (!result) {
|
|
13035
11482
|
console.log(`\n✓ Secrets for stage "${stage}" are up-to-date`);
|
|
@@ -13043,8 +11490,8 @@ program.command("secrets:reconcile").description("Backfill missing custom secret
|
|
|
13043
11490
|
process.exit(1);
|
|
13044
11491
|
}
|
|
13045
11492
|
});
|
|
13046
|
-
program.command("deploy").description("Deploy a stage through its target").option("--target <name>", "Deploy target: dokploy, 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) => {
|
|
13047
|
-
const { deployCli } = await import("./cli-
|
|
11493
|
+
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").action(async (options) => {
|
|
11494
|
+
const { deployCli } = await import("./cli-P56vvrDQ.mjs");
|
|
13048
11495
|
const globalOptions = program.opts();
|
|
13049
11496
|
const code = await deployCli({
|
|
13050
11497
|
cwd: resolve(globalOptions.cwd ?? process.cwd()),
|
|
@@ -13077,7 +11524,7 @@ program.command("deploy:github").description("Let GitHub Actions deploy a stage:
|
|
|
13077
11524
|
try {
|
|
13078
11525
|
const globalOptions = program.opts();
|
|
13079
11526
|
if (globalOptions.cwd) process.chdir(globalOptions.cwd);
|
|
13080
|
-
const { deployGithubCommand } = await import("./github-
|
|
11527
|
+
const { deployGithubCommand } = await import("./github-B0YYE98x.mjs");
|
|
13081
11528
|
await deployGithubCommand(options);
|
|
13082
11529
|
} catch (error) {
|
|
13083
11530
|
console.error(formatError(error));
|
|
@@ -13212,6 +11659,6 @@ program.command("upgrade").description("Upgrade @geekmidas/cli (with --all, ever
|
|
|
13212
11659
|
});
|
|
13213
11660
|
program.parse();
|
|
13214
11661
|
//#endregion
|
|
13215
|
-
export {
|
|
11662
|
+
export { formatError as i, slug as n, stamp as r, generateFullstackCustomSecrets as t };
|
|
13216
11663
|
|
|
13217
11664
|
//# sourceMappingURL=index.mjs.map
|