@geekmidas/cli 10.0.0-alpha.116 → 10.0.0-alpha.118
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 +7 -2
- package/dist/{GoDaddyProvider-BDvIMTO-.cjs → GoDaddyProvider-B6SPHBCU.cjs} +2 -2
- package/dist/{GoDaddyProvider-BDvIMTO-.cjs.map → GoDaddyProvider-B6SPHBCU.cjs.map} +1 -1
- package/dist/{GoDaddyProvider-eW1mre1g.mjs → GoDaddyProvider-CcjbFkLt.mjs} +2 -2
- package/dist/{GoDaddyProvider-eW1mre1g.mjs.map → GoDaddyProvider-CcjbFkLt.mjs.map} +1 -1
- package/dist/{LegacyStateStore-DFeykTd8.cjs → LegacyStateStore-2l5jw_VI.cjs} +3 -3
- package/dist/{LegacyStateStore-DFeykTd8.cjs.map → LegacyStateStore-2l5jw_VI.cjs.map} +1 -1
- package/dist/{LegacyStateStore-Ba1_x0cr.mjs → LegacyStateStore-D-D6r_JT.mjs} +3 -3
- package/dist/{LegacyStateStore-Ba1_x0cr.mjs.map → LegacyStateStore-D-D6r_JT.mjs.map} +1 -1
- package/dist/{LocalStateStore-C1rUUvBr.mjs → LocalStateStore-DIAY7j1O.mjs} +2 -2
- package/dist/{LocalStateStore-C1rUUvBr.mjs.map → LocalStateStore-DIAY7j1O.mjs.map} +1 -1
- package/dist/{LocalStateStore-CV-04jpI.cjs → LocalStateStore-Dg3rvwwe.cjs} +2 -2
- package/dist/{LocalStateStore-CV-04jpI.cjs.map → LocalStateStore-Dg3rvwwe.cjs.map} +1 -1
- package/dist/{S3StateStore-Dszt4fN5.mjs → S3StateStore-BKi1eamw.mjs} +2 -2
- package/dist/{S3StateStore-Dszt4fN5.mjs.map → S3StateStore-BKi1eamw.mjs.map} +1 -1
- package/dist/{S3StateStore-CqYi-1sb.cjs → S3StateStore-BdcgQNhO.cjs} +2 -2
- package/dist/{S3StateStore-CqYi-1sb.cjs.map → S3StateStore-BdcgQNhO.cjs.map} +1 -1
- package/dist/{SSMStateStore-zpHjDCAW.cjs → SSMStateStore-CS-pAKT9.cjs} +2 -2
- package/dist/{SSMStateStore-zpHjDCAW.cjs.map → SSMStateStore-CS-pAKT9.cjs.map} +1 -1
- package/dist/{SSMStateStore-BYRUyMkE.mjs → SSMStateStore-DHgaNW75.mjs} +2 -2
- package/dist/{SSMStateStore-BYRUyMkE.mjs.map → SSMStateStore-DHgaNW75.mjs.map} +1 -1
- package/dist/{add-0XI8_HOf.cjs → add-6VPdRbRT.cjs} +3 -3
- package/dist/{add-0XI8_HOf.cjs.map → add-6VPdRbRT.cjs.map} +1 -1
- package/dist/{add-DXdJjp0b.mjs → add-BcZXHa3Z.mjs} +3 -3
- package/dist/{add-DXdJjp0b.mjs.map → add-BcZXHa3Z.mjs.map} +1 -1
- package/dist/{bundler-CgcwaVI0.mjs → bundler-Ciim6amq.mjs} +7 -59
- package/dist/bundler-Ciim6amq.mjs.map +1 -0
- package/dist/{bundler-B2_2-Dum.cjs → bundler-jjWtQ882.cjs} +6 -58
- package/dist/bundler-jjWtQ882.cjs.map +1 -0
- package/dist/{cli-H0uB_znq.mjs → cli-BkxlWYfI.mjs} +2 -2
- package/dist/{cli-H0uB_znq.mjs.map → cli-BkxlWYfI.mjs.map} +1 -1
- package/dist/{cli-5k6kV_i4.cjs → cli-CQ2ij0Mi.cjs} +2 -2
- package/dist/{cli-5k6kV_i4.cjs.map → cli-CQ2ij0Mi.cjs.map} +1 -1
- package/dist/{compose-CYeMW2_o.cjs → compose-JOasAHcd.cjs} +4 -4
- package/dist/{compose-CYeMW2_o.cjs.map → compose-JOasAHcd.cjs.map} +1 -1
- package/dist/{compose-BEiJWrB7.mjs → compose-qLTNYJAY.mjs} +4 -4
- package/dist/{compose-BEiJWrB7.mjs.map → compose-qLTNYJAY.mjs.map} +1 -1
- package/dist/deploy/public.cjs +6 -6
- package/dist/deploy/public.d.cts.map +1 -1
- package/dist/deploy/public.d.mts.map +1 -1
- package/dist/deploy/public.mjs +6 -6
- package/dist/{deploy-Q5qo1OP-.cjs → deploy-D-liNXfx.cjs} +5 -5
- package/dist/{deploy-Q5qo1OP-.cjs.map → deploy-D-liNXfx.cjs.map} +1 -1
- package/dist/{deploy-Cyx5fSb7.mjs → deploy-_g-2dX38.mjs} +5 -5
- package/dist/{deploy-Cyx5fSb7.mjs.map → deploy-_g-2dX38.mjs.map} +1 -1
- package/dist/{dns-CsApfzLc.mjs → dns-DGq4xkJT.mjs} +2 -2
- package/dist/{dns-CsApfzLc.mjs.map → dns-DGq4xkJT.mjs.map} +1 -1
- package/dist/{dns-v9viladZ.cjs → dns-DfQKqAyt.cjs} +2 -2
- package/dist/{dns-v9viladZ.cjs.map → dns-DfQKqAyt.cjs.map} +1 -1
- package/dist/{dokploy-DfUo_QRw.mjs → dokploy-ClIPZaPH.mjs} +2 -2
- package/dist/{dokploy-DfUo_QRw.mjs.map → dokploy-ClIPZaPH.mjs.map} +1 -1
- package/dist/{dokploy-Di7rC2z0.cjs → dokploy-D17I2BCE.cjs} +2 -2
- package/dist/{dokploy-Di7rC2z0.cjs.map → dokploy-D17I2BCE.cjs.map} +1 -1
- package/dist/{drivers-D11JHxrD.mjs → drivers-3klmg9Sk.mjs} +22 -4
- package/dist/drivers-3klmg9Sk.mjs.map +1 -0
- package/dist/{drivers-GKaA6ITM.cjs → drivers-CoqD5zYP.cjs} +27 -3
- package/dist/drivers-CoqD5zYP.cjs.map +1 -0
- package/dist/{eas-CTfRmndW.mjs → eas-DJXhARYb.mjs} +2 -2
- package/dist/{eas-CTfRmndW.mjs.map → eas-DJXhARYb.mjs.map} +1 -1
- package/dist/{eas-CIvNT6qa.cjs → eas-b95MCG14.cjs} +2 -2
- package/dist/{eas-CIvNT6qa.cjs.map → eas-b95MCG14.cjs.map} +1 -1
- package/dist/{encryption--U4NsK7N.cjs → encryption-C_NRQ9cY.cjs} +2 -2
- package/dist/{encryption--U4NsK7N.cjs.map → encryption-C_NRQ9cY.cjs.map} +1 -1
- package/dist/{encryption-BpTPmd7h.mjs → encryption-CldvLtip.mjs} +2 -2
- package/dist/{encryption-BpTPmd7h.mjs.map → encryption-CldvLtip.mjs.map} +1 -1
- package/dist/{engine-BFmHfek1.mjs → engine-BBnTj8OD.mjs} +12 -6
- package/dist/engine-BBnTj8OD.mjs.map +1 -0
- package/dist/{engine-Ovl1PB6i.cjs → engine-CKfdvEVt.cjs} +11 -5
- package/dist/engine-CKfdvEVt.cjs.map +1 -0
- package/dist/{github-tWAGuR9i.mjs → github-BMPci2wg.mjs} +4 -4
- package/dist/{github-tWAGuR9i.mjs.map → github-BMPci2wg.mjs.map} +1 -1
- package/dist/{github-CTlPQ5Ra.cjs → github-D5Ck3srU.cjs} +4 -4
- package/dist/{github-CTlPQ5Ra.cjs.map → github-D5Ck3srU.cjs.map} +1 -1
- package/dist/{googlePlay-BNH729It.cjs → googlePlay-C-GBRDMp.cjs} +3 -3
- package/dist/{googlePlay-BNH729It.cjs.map → googlePlay-C-GBRDMp.cjs.map} +1 -1
- package/dist/{googlePlay-m1De-qIJ.mjs → googlePlay-fw0VWly7.mjs} +3 -3
- package/dist/{googlePlay-m1De-qIJ.mjs.map → googlePlay-fw0VWly7.mjs.map} +1 -1
- package/dist/index.cjs +245 -139
- package/dist/index.cjs.map +1 -1
- package/dist/index.mjs +245 -139
- package/dist/index.mjs.map +1 -1
- package/dist/{migrate-4B4RgAjU.mjs → migrate-1bHqjfIe.mjs} +2 -2
- package/dist/{migrate-4B4RgAjU.mjs.map → migrate-1bHqjfIe.mjs.map} +1 -1
- package/dist/{migrate-DZ2--SaO.cjs → migrate-BndHuwqw.cjs} +2 -2
- package/dist/{migrate-DZ2--SaO.cjs.map → migrate-BndHuwqw.cjs.map} +1 -1
- package/dist/{migrate-X0Y7fz3N.mjs → migrate-WyWhSivN.mjs} +2 -2
- package/dist/{migrate-X0Y7fz3N.mjs.map → migrate-WyWhSivN.mjs.map} +1 -1
- package/dist/{migrate-BYEmpoIg.cjs → migrate-rke2Qvj2.cjs} +2 -2
- package/dist/{migrate-BYEmpoIg.cjs.map → migrate-rke2Qvj2.cjs.map} +1 -1
- package/dist/{publish-BUvYjiZH.cjs → publish-B22IhdS9.cjs} +2 -2
- package/dist/{publish-BUvYjiZH.cjs.map → publish-B22IhdS9.cjs.map} +1 -1
- package/dist/{publish-hhgraEhO.mjs → publish-BBG4zx8E.mjs} +2 -2
- package/dist/{publish-hhgraEhO.mjs.map → publish-BBG4zx8E.mjs.map} +1 -1
- package/dist/{ready-CIt5SOtg.mjs → ready-4hEoQ_cr.mjs} +2 -2
- package/dist/{ready-CIt5SOtg.mjs.map → ready-4hEoQ_cr.mjs.map} +1 -1
- package/dist/{ready-BMFjxBYA.cjs → ready-gc9IFByS.cjs} +2 -2
- package/dist/{ready-BMFjxBYA.cjs.map → ready-gc9IFByS.cjs.map} +1 -1
- package/dist/{resolve-DotGHvtX.mjs → resolve-CC6ndodR.mjs} +3 -3
- package/dist/{resolve-DotGHvtX.mjs.map → resolve-CC6ndodR.mjs.map} +1 -1
- package/dist/{resolve-DdFKTx8l.cjs → resolve-isFTuJp4.cjs} +3 -3
- package/dist/{resolve-DdFKTx8l.cjs.map → resolve-isFTuJp4.cjs.map} +1 -1
- package/dist/{rollback-9t9G3c_u.mjs → rollback-DaEmEC2Y.mjs} +4 -4
- package/dist/{rollback-9t9G3c_u.mjs.map → rollback-DaEmEC2Y.mjs.map} +1 -1
- package/dist/{rollback-D4xeUQvG.cjs → rollback-Lj9bQvHq.cjs} +4 -4
- package/dist/{rollback-D4xeUQvG.cjs.map → rollback-Lj9bQvHq.cjs.map} +1 -1
- package/dist/{seeds-DbKNcs3o.cjs → seeds-CF9Nx6_g.cjs} +33 -6
- package/dist/seeds-CF9Nx6_g.cjs.map +1 -0
- package/dist/{seeds-CRkKeyW0.mjs → seeds-CYzoBDg2.mjs} +33 -6
- package/dist/seeds-CYzoBDg2.mjs.map +1 -0
- package/dist/{serverSetup-DlRKRfTi.mjs → serverSetup-0h1Vh5un.mjs} +5 -5
- package/dist/{serverSetup-DlRKRfTi.mjs.map → serverSetup-0h1Vh5un.mjs.map} +1 -1
- package/dist/{serverSetup-CcWgNtiC.cjs → serverSetup-CbuJK4zg.cjs} +5 -5
- package/dist/{serverSetup-CcWgNtiC.cjs.map → serverSetup-CbuJK4zg.cjs.map} +1 -1
- package/dist/{sniffer-CakkKKqQ.mjs → sniffer-CRJHtBaK.mjs} +6 -6
- package/dist/{sniffer-CakkKKqQ.mjs.map → sniffer-CRJHtBaK.mjs.map} +1 -1
- package/dist/{sniffer-B6T6za4n.cjs → sniffer-Dz3Va1Jo.cjs} +6 -6
- package/dist/{sniffer-B6T6za4n.cjs.map → sniffer-Dz3Va1Jo.cjs.map} +1 -1
- package/dist/{stage-Mp6qQIoD.mjs → stage-CIo8s2WT.mjs} +4 -4
- package/dist/{stage-Mp6qQIoD.mjs.map → stage-CIo8s2WT.mjs.map} +1 -1
- package/dist/{stage-W3PUt7ar.cjs → stage-y9qFelxT.cjs} +4 -4
- package/dist/{stage-W3PUt7ar.cjs.map → stage-y9qFelxT.cjs.map} +1 -1
- package/dist/target/public.cjs +1 -1
- package/dist/target/public.mjs +1 -1
- package/dist/vitest.cjs +1 -1
- package/dist/vitest.mjs +1 -1
- package/package.json +15 -15
- package/dist/bundler-B2_2-Dum.cjs.map +0 -1
- package/dist/bundler-CgcwaVI0.mjs.map +0 -1
- package/dist/drivers-D11JHxrD.mjs.map +0 -1
- package/dist/drivers-GKaA6ITM.cjs.map +0 -1
- package/dist/engine-BFmHfek1.mjs.map +0 -1
- package/dist/engine-Ovl1PB6i.cjs.map +0 -1
- package/dist/seeds-CRkKeyW0.mjs.map +0 -1
- package/dist/seeds-DbKNcs3o.cjs.map +0 -1
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import { t as __exportAll } from "./rolldown-runtime-BhDjJH2R.mjs";
|
|
2
2
|
import { t as GkmError } from "./errors-D8Gwp5kU.mjs";
|
|
3
|
-
import { C as dockerCompose, a as edgePorts, b as dockerHost, d as composeStack, g as edgeAlias, s as deployedStackSecrets, x as sshAccess, y as composeServer } from "./compose-
|
|
3
|
+
import { C as dockerCompose, a as edgePorts, b as dockerHost, d as composeStack, g as edgeAlias, s as deployedStackSecrets, x as sshAccess, y as composeServer } from "./compose-qLTNYJAY.mjs";
|
|
4
4
|
import { N as assertDeployedStage } from "./workspace-D1QbNd55.mjs";
|
|
5
|
-
import { S as deploysWithCompose } from "./dns-
|
|
6
|
-
import { It as secretsStoreFor, dt as constructGlobs } from "./seeds-
|
|
5
|
+
import { S as deploysWithCompose } from "./dns-DGq4xkJT.mjs";
|
|
6
|
+
import { It as secretsStoreFor, dt as constructGlobs } from "./seeds-CYzoBDg2.mjs";
|
|
7
7
|
import { n as deployIdentity } from "./identity-CBXr4wNi.mjs";
|
|
8
8
|
import { i as discover } from "./discover-Ca8U75dl.mjs";
|
|
9
9
|
import { NotInAnApp, loadWorkspaceAppInfo, loadWorkspaceConfig } from "./config.mjs";
|
|
@@ -299,4 +299,4 @@ function run(commandArgs, credentials, cwd) {
|
|
|
299
299
|
//#endregion
|
|
300
300
|
export { stage_exports as n, loadStage as t };
|
|
301
301
|
|
|
302
|
-
//# sourceMappingURL=stage-
|
|
302
|
+
//# sourceMappingURL=stage-CIo8s2WT.mjs.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"stage-Mp6qQIoD.mjs","names":[],"sources":["../src/exec/stage.ts"],"sourcesContent":["/**\n * `gkm exec --stage <stage> -- <command>`: a command run on this machine as\n * a deployed compose stage's app — a data import, a backfill, a report.\n *\n * The app's environment is the one its container runs with, derived the way\n * the deploy derives it (`composeStack`) from the stage's secrets, read from\n * the stage's store the way the deploy reads them. Only what the container\n * reaches on the stack's network is moved: each service it names that the\n * server publishes on its loopback — Postgres, MinIO — is reached through an\n * SSH tunnel on the deploy's own login (`ServerAccess.tunnel`), and each\n * sibling app through its public address.\n *\n * Nothing is written to disk. The values exist only in the command's\n * environment, and its Credentials are filled from there by a preload that\n * is a `data:` URL naming the keys — never their values.\n */\n\nimport { spawn } from 'node:child_process';\nimport { constants } from 'node:os';\nimport type { ConstructManifest } from '@geekmidas/manifest';\nimport { type ComposeDocker, dockerCompose } from '../compose/docker.js';\nimport { deployedStackSecrets } from '../compose/secrets.js';\nimport {\n\ttype ComposeServer,\n\tcomposeServer,\n\tdockerHost,\n\ttype ServerAccess,\n\tsshAccess,\n\ttype Tunnel,\n} from '../compose/server.js';\nimport { type ComposeStack, composeStack } from '../compose/stack.js';\nimport { edgeAlias } from '../compose/traefik.js';\nimport {\n\tloadWorkspaceAppInfo,\n\tloadWorkspaceConfig,\n\tNotInAnApp,\n} from '../config.js';\nimport { deployIdentity } from '../deploy/identity.js';\nimport { GkmError } from '../errors';\nimport { prompt } from '../prompt.js';\nimport { deploysWithCompose } from '../providers/dns.js';\nimport { discover } from '../reconcile/discover.js';\nimport { constructGlobs } from '../reconcile/workspace.js';\nimport { secretsStoreFor } from '../secrets/store.js';\nimport type { StageSecrets } from '../secrets/types.js';\nimport { edgePorts } from '../target/compose/phases.js';\nimport { assertDeployedStage } from '../workspace/stages.js';\nimport type { NormalizedWorkspace } from '../workspace/types.js';\n\nconst logger = console;\n\n/** The stage is not one the compose target deploys. */\nexport class ExecStageNotComposed extends GkmError {\n\tconstructor(readonly stage: string) {\n\t\tsuper(\n\t\t\t`gkm exec --stage runs a command against a compose stage's server, and ` +\n\t\t\t\t`'${stage}' is not deployed with compose. Set deploy.default: 'compose' ` +\n\t\t\t\t'in gkm.config.ts, or run the command where the stage runs.',\n\t\t);\n\t\tthis.name = 'ExecStageNotComposed';\n\t}\n}\n\n/** The stage's secrets lack what its deploy generates: it was never deployed. */\nexport class ExecStageNotDeployed extends GkmError {\n\tconstructor(\n\t\treadonly stage: string,\n\t\treadonly missing: readonly string[],\n\t) {\n\t\tsuper(\n\t\t\t`'${stage}' has no ${missing.join(', ')} in its secrets yet, so what its ` +\n\t\t\t\t'apps run with is not decided. Deploy it first: ' +\n\t\t\t\t`gkm deploy --stage ${stage}.`,\n\t\t);\n\t\tthis.name = 'ExecStageNotDeployed';\n\t}\n}\n\n/** The directory is not one of the stack's apps. */\nexport class ExecAppNotInStack extends GkmError {\n\tconstructor(\n\t\treadonly stage: string,\n\t\treadonly app: string | undefined,\n\t\treadonly apps: readonly string[],\n\t) {\n\t\tsuper(\n\t\t\t`${app ? `'${app}' is not` : 'This directory is not in'} one of the apps ` +\n\t\t\t\t`'${stage}' runs (${apps.join(', ')}), so there is no environment to run ` +\n\t\t\t\t`as. Run from an app's directory: cd apps/${apps[0] ?? '<app>'} && ` +\n\t\t\t\t`gkm exec --stage ${stage} -- <command>.`,\n\t\t);\n\t\tthis.name = 'ExecAppNotInStack';\n\t}\n}\n\n/** A protected stage, and nobody said yes. */\nexport class ExecNotConfirmed extends GkmError {\n\tconstructor(readonly stage: string) {\n\t\tsuper(`Nothing was run against '${stage}'.`);\n\t\tthis.name = 'ExecNotConfirmed';\n\t}\n}\n\nexport interface StageExecOptions {\n\tstage: string;\n\t/** The AWS profile for the stage's account, where its secrets are kept. */\n\tprofile?: string;\n\t/**\n\t * The SSH login to reach the server as, in place of\n\t * `deploy.compose.server.<stage>.user` — a person's own, where the\n\t * deploy's key lives only in CI.\n\t */\n\tsshUser?: string;\n\t/** Run against a protected stage without asking. */\n\tyes?: boolean;\n\tcwd?: string;\n}\n\n/** What the command needs from outside the process, so tests can stand in. */\nexport interface StageExecDeps {\n\t/** The server's Docker, asked which port each service publishes. */\n\tdocker: ComposeDocker;\n\t/** The tunnels to the server's loopback. */\n\tserver: ServerAccess;\n\t/** Asked before running against a protected stage. */\n\tconfirm: (question: string) => Promise<boolean>;\n\t/** Where the edge's ports are read from, as the deploy reads them. */\n\tenv: NodeJS.ProcessEnv;\n}\n\nexport const defaultStageExecDeps: StageExecDeps = {\n\tdocker: dockerCompose,\n\tserver: sshAccess,\n\tconfirm: async (question) => {\n\t\tconst answer = await prompt(`${question} [y/N] `, {\n\t\t\tinstead: 'Pass --yes to run without asking.',\n\t\t});\n\t\treturn /^y(es)?$/i.test(answer.trim());\n\t},\n\tenv: process.env,\n};\n\n/** A service on the stack's network the app's environment names. */\nexport interface StackAddress {\n\tservice: string;\n\tport: number;\n}\n\n/**\n * The environment an app runs with on the stage: a backend's container's,\n * over the keys of each worker built from it; a site's or a mobile app's,\n * the public values its build reads.\n *\n * @throws {ExecAppNotInStack}\n */\nexport function appEnvironment(\n\tstack: Pick<ComposeStack, 'stage' | 'apps' | 'workers' | 'mobileApps'>,\n\tappName: string | undefined,\n): Record<string, string> {\n\tconst mobile = stack.mobileApps.find(\n\t\t(candidate) => candidate.name === appName,\n\t);\n\tif (mobile) return { ...mobile.values };\n\tconst app = stack.apps.find((candidate) => candidate.name === appName);\n\tif (!app) {\n\t\tthrow new ExecAppNotInStack(stack.stage, appName, [\n\t\t\t...stack.apps.map((candidate) => candidate.name),\n\t\t\t...stack.mobileApps.map((candidate) => candidate.name),\n\t\t]);\n\t}\n\tif (app.kind === 'site') return { ...app.build?.args };\n\tconst workers = stack.workers.filter((worker) => worker.host === app.name);\n\treturn Object.assign(\n\t\t{},\n\t\t...workers.map((worker) => worker.env ?? {}),\n\t\tapp.env ?? {},\n\t);\n}\n\n/** `host:port`, at a URL's authority — after `//` or a login's `@`. */\nfunction authority(host: string, port: number): RegExp {\n\treturn new RegExp(\n\t\t`(?<=[/@,])${host.replace(/[.-]/g, '\\\\$&')}:${port}(?!\\\\d)`,\n\t\t'g',\n\t);\n}\n\n/**\n * Each of the stack's services the environment reaches on the stack's\n * network, once: `postgres:5432`, `minio:9000`.\n */\nexport function stackAddresses(\n\tenv: Readonly<Record<string, string>>,\n\tstack: Pick<ComposeStack, 'infra'>,\n): StackAddress[] {\n\tconst found = new Map<string, StackAddress>();\n\tconst pattern = /(?<=[/@,])([a-z0-9-]+):(\\d+)(?!\\d)/g;\n\tfor (const value of Object.values(env)) {\n\t\tfor (const match of value.matchAll(pattern)) {\n\t\t\tconst service = match[1]!;\n\t\t\tif (!stack.infra.includes(service)) continue;\n\t\t\tconst port = Number(match[2]);\n\t\t\tfound.set(`${service}:${port}`, { service, port });\n\t\t}\n\t}\n\treturn [...found.values()];\n}\n\n/**\n * The environment as this machine reaches it: each service at its tunnel's\n * local port, and each sibling app at its public address. What has no\n * tunnel is left as the container has it.\n */\nexport function throughTunnels(\n\tenv: Readonly<Record<string, string>>,\n\tstack: Pick<ComposeStack, 'apps' | 'project'>,\n\ttunnels: ReadonlyMap<string, number>,\n): Record<string, string> {\n\tconst rewrites: [RegExp, string][] = [];\n\tfor (const [address, port] of tunnels) {\n\t\tconst [service, inside] = address.split(':');\n\t\trewrites.push([authority(service!, Number(inside)), `127.0.0.1:${port}`]);\n\t}\n\tfor (const app of stack.apps) {\n\t\tfor (const host of [app.name, edgeAlias(stack.project, app.name)]) {\n\t\t\trewrites.push([\n\t\t\t\tnew RegExp(\n\t\t\t\t\t`http://${host.replace(/[.-]/g, '\\\\$&')}:${app.port}(?!\\\\d)`,\n\t\t\t\t\t'g',\n\t\t\t\t),\n\t\t\t\tapp.url,\n\t\t\t]);\n\t\t}\n\t}\n\tconst out: Record<string, string> = {};\n\tfor (const [key, value] of Object.entries(env)) {\n\t\tout[key] = rewrites.reduce(\n\t\t\t(current, [pattern, replacement]) =>\n\t\t\t\tcurrent.replace(pattern, () => replacement),\n\t\t\tvalue,\n\t\t);\n\t}\n\treturn out;\n}\n\n/**\n * The preload that fills Credentials from the command's own environment: a\n * `data:` URL holding the keys, so no file — and no value — is written.\n */\nexport function credentialsImport(keys: readonly string[]): string {\n\tconst source =\n\t\t'globalThis.__gkm_credentials__=Object.fromEntries(' +\n\t\t`${JSON.stringify(keys)}.map((k)=>[k,process.env[k]]));`;\n\treturn `--import=data:text/javascript,${encodeURIComponent(source)}`;\n}\n\n/** A deployed compose stage, as its deploy has it, and the app here. */\nexport interface LoadedStage {\n\tworkspace: NormalizedWorkspace;\n\tmanifest: ConstructManifest;\n\tstack: ComposeStack;\n\t/** The stage's secrets, as its deploy reads them. */\n\tsecrets: StageSecrets;\n\t/** The app this directory is, if it is one. */\n\tappName: string | undefined;\n\tprotectedStage: boolean;\n}\n\n/**\n * The stage's stack, and the app this directory is, as the deploy has them:\n * its secrets read from its store, its values derived as the deploy derives\n * them. Nothing is reached but the store — no server, no SSH.\n *\n * @throws {UndeclaredStage}, {ExecStageNotComposed}, {ExecStageNotDeployed}\n */\nexport async function loadStage(\n\toptions: Pick<StageExecOptions, 'stage' | 'profile' | 'cwd'>,\n\tenv: NodeJS.ProcessEnv = process.env,\n): Promise<LoadedStage> {\n\tconst cwd = options.cwd ?? process.cwd();\n\tconst { stage } = options;\n\t// Outside an app the workspace is still loaded, so the stage is checked\n\t// before the directory is refused.\n\tconst info = await loadWorkspaceAppInfo(cwd).catch((error) => {\n\t\tif (error instanceof NotInAnApp) return undefined;\n\t\tthrow error;\n\t});\n\tconst workspace =\n\t\tinfo?.workspace ?? (await loadWorkspaceConfig(cwd)).workspace;\n\n\tassertDeployedStage(workspace.stages, stage);\n\tif (!deploysWithCompose(workspace)) throw new ExecStageNotComposed(stage);\n\n\tconst runnables: Record<string, string[]> = {};\n\tconst background: Record<string, string[]> = {};\n\tconst manifest: ConstructManifest = await discover({\n\t\tpatterns: constructGlobs(workspace),\n\t\tcwd: workspace.root,\n\t\trunnables,\n\t\tbackground,\n\t});\n\n\tconst store = await secretsStoreFor(workspace, stage, {\n\t\t...(options.profile ? { profile: options.profile } : {}),\n\t});\n\tconst { secrets, generated } = deployedStackSecrets(\n\t\tworkspace,\n\t\tstage,\n\t\tawait store.read(stage),\n\t\tmanifest,\n\t);\n\tif (generated.length > 0) throw new ExecStageNotDeployed(stage, generated);\n\n\tconst stack = composeStack({\n\t\tworkspace,\n\t\tmanifest,\n\t\trunnables,\n\t\tbackground,\n\t\tstage,\n\t\tidentity: deployIdentity(workspace, stage),\n\t\t// Built, so a site's public values are resolved as its build has them.\n\t\timages: { mode: 'build', tag: 'exec' },\n\t\tsecrets,\n\t\tports: edgePorts(env),\n\t});\n\treturn {\n\t\tworkspace,\n\t\tmanifest,\n\t\tstack,\n\t\tsecrets,\n\t\tappName: info?.appName,\n\t\tprotectedStage: workspace.stages.protected?.includes(stage) ?? false,\n\t};\n}\n\n/** The stage's server, as SSH reaches it: `--ssh-user` over the config's. */\nfunction stageServer(\n\tloaded: LoadedStage,\n\toptions: StageExecOptions,\n): ComposeServer {\n\tconst server = composeServer({\n\t\tstage: options.stage,\n\t\tlocal: false,\n\t\tconfig: loaded.workspace.deploy?.compose?.server,\n\t\tcustom: loaded.secrets.custom,\n\t})!;\n\tif (options.sshUser) server.user = options.sshUser;\n\treturn server;\n}\n\n/**\n * Run `commandArgs` as the app this directory is, on the stage — through a\n * tunnel to each service it reaches — and return its exit code. Every tunnel\n * is closed once it exits, whatever it exits with.\n *\n * @throws {ExecStageNotComposed}, {ExecStageNotDeployed}, {ExecAppNotInStack},\n * {ExecNotConfirmed}, {ComposeServerUnreachable}\n */\nexport async function execOnStage(\n\tcommandArgs: readonly [string, ...string[]],\n\toptions: StageExecOptions,\n\tdeps: StageExecDeps = defaultStageExecDeps,\n): Promise<number> {\n\tconst { stage } = options;\n\tconst loaded = await loadStage(options, deps.env);\n\tconst { stack, appName, protectedStage } = loaded;\n\tconst env = appEnvironment(stack, appName);\n\n\tif (protectedStage && !options.yes) {\n\t\tconst question = `'${stage}' is a protected stage. Run ${commandArgs.join(' ')} against it?`;\n\t\tif (!(await deps.confirm(question))) throw new ExecNotConfirmed(stage);\n\t}\n\n\t// Only what reaches a service on the stack needs the server: a site's or\n\t// a mobile app's values are public addresses, and need no SSH at all.\n\tconst addresses = stackAddresses(env, stack);\n\tif (addresses.length === 0) {\n\t\tlogger.log(`📦 App: ${appName} on '${stage}'`);\n\t\tlogger.log(`🔐 Loaded ${Object.keys(env).length} value(s) for '${stage}'`);\n\t\treturn run(commandArgs, env, options.cwd);\n\t}\n\n\tconst server = stageServer(loaded, options);\n\tlogger.log(`📦 App: ${appName} on '${stage}' (${server.host})`);\n\n\t// The deploy's own check: SSH logs in and Docker answers, before any\n\t// tunnel is asked for — so a key the server refuses is said as that.\n\tawait deps.server.check(server);\n\n\tconst tunnels: Tunnel[] = [];\n\tconst ports = new Map<string, number>();\n\ttry {\n\t\tfor (const address of addresses) {\n\t\t\tconst published = await deps.docker.published(\n\t\t\t\t{ host: dockerHost(server) },\n\t\t\t\tstack.project,\n\t\t\t\taddress.service,\n\t\t\t\taddress.port,\n\t\t\t);\n\t\t\tif (published === undefined) {\n\t\t\t\tlogger.log(\n\t\t\t\t\t`⚠️ ${address.service} is not published on the server (no running ` +\n\t\t\t\t\t\t`${stack.project} ${address.service} publishes ${address.port}), so ` +\n\t\t\t\t\t\t'it is unreachable from here; its address is passed on unchanged.',\n\t\t\t\t);\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\tconst tunnel = await deps.server.tunnel(server, published);\n\t\t\ttunnels.push(tunnel);\n\t\t\tports.set(`${address.service}:${address.port}`, tunnel.port);\n\t\t\tlogger.log(`🚇 ${address.service} → 127.0.0.1:${tunnel.port} (over SSH)`);\n\t\t}\n\n\t\tconst credentials = throughTunnels(env, stack, ports);\n\t\tlogger.log(\n\t\t\t`🔐 Loaded ${Object.keys(credentials).length} value(s) for '${stage}'`,\n\t\t);\n\t\treturn await run(commandArgs, credentials, options.cwd);\n\t} finally {\n\t\tawait Promise.all(tunnels.map((tunnel) => tunnel.close()));\n\t}\n}\n\n/**\n * The command, with the values in its environment and its Credentials. A\n * Ctrl-C reaches it — it is in the terminal's process group — and this\n * process waits for it to exit rather than leaving before the tunnels close.\n */\nfunction run(\n\tcommandArgs: readonly [string, ...string[]],\n\tcredentials: Record<string, string>,\n\tcwd: string | undefined,\n): Promise<number> {\n\tconst [cmd, ...args] = commandArgs;\n\tlogger.log(`🚀 Running: ${commandArgs.join(' ')}`);\n\n\tconst inherited = (process.env.NODE_OPTIONS ?? '')\n\t\t.replace(/--import[= ]\\S+/g, '')\n\t\t.trim();\n\tconst nodeOptions = [inherited, credentialsImport(Object.keys(credentials))]\n\t\t.filter(Boolean)\n\t\t.join(' ');\n\n\tconst wait = () => {};\n\tprocess.on('SIGINT', wait);\n\tprocess.on('SIGTERM', wait);\n\treturn new Promise<number>((resolve) => {\n\t\tconst child = spawn(cmd, args, {\n\t\t\t...(cwd ? { cwd } : {}),\n\t\t\tstdio: 'inherit',\n\t\t\tenv: { ...process.env, ...credentials, NODE_OPTIONS: nodeOptions },\n\t\t});\n\t\tchild.on('close', (code, signal) => {\n\t\t\tresolve(code ?? 128 + (signal ? constants.signals[signal] : 0));\n\t\t});\n\t\tchild.on('error', (error) => {\n\t\t\tlogger.error(`Failed to run command: ${error.message}`);\n\t\t\tresolve(1);\n\t\t});\n\t}).finally(() => {\n\t\tprocess.off('SIGINT', wait);\n\t\tprocess.off('SIGTERM', wait);\n\t});\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"stage-CIo8s2WT.mjs","names":[],"sources":["../src/exec/stage.ts"],"sourcesContent":["/**\n * `gkm exec --stage <stage> -- <command>`: a command run on this machine as\n * a deployed compose stage's app — a data import, a backfill, a report.\n *\n * The app's environment is the one its container runs with, derived the way\n * the deploy derives it (`composeStack`) from the stage's secrets, read from\n * the stage's store the way the deploy reads them. Only what the container\n * reaches on the stack's network is moved: each service it names that the\n * server publishes on its loopback — Postgres, MinIO — is reached through an\n * SSH tunnel on the deploy's own login (`ServerAccess.tunnel`), and each\n * sibling app through its public address.\n *\n * Nothing is written to disk. The values exist only in the command's\n * environment, and its Credentials are filled from there by a preload that\n * is a `data:` URL naming the keys — never their values.\n */\n\nimport { spawn } from 'node:child_process';\nimport { constants } from 'node:os';\nimport type { ConstructManifest } from '@geekmidas/manifest';\nimport { type ComposeDocker, dockerCompose } from '../compose/docker.js';\nimport { deployedStackSecrets } from '../compose/secrets.js';\nimport {\n\ttype ComposeServer,\n\tcomposeServer,\n\tdockerHost,\n\ttype ServerAccess,\n\tsshAccess,\n\ttype Tunnel,\n} from '../compose/server.js';\nimport { type ComposeStack, composeStack } from '../compose/stack.js';\nimport { edgeAlias } from '../compose/traefik.js';\nimport {\n\tloadWorkspaceAppInfo,\n\tloadWorkspaceConfig,\n\tNotInAnApp,\n} from '../config.js';\nimport { deployIdentity } from '../deploy/identity.js';\nimport { GkmError } from '../errors';\nimport { prompt } from '../prompt.js';\nimport { deploysWithCompose } from '../providers/dns.js';\nimport { discover } from '../reconcile/discover.js';\nimport { constructGlobs } from '../reconcile/workspace.js';\nimport { secretsStoreFor } from '../secrets/store.js';\nimport type { StageSecrets } from '../secrets/types.js';\nimport { edgePorts } from '../target/compose/phases.js';\nimport { assertDeployedStage } from '../workspace/stages.js';\nimport type { NormalizedWorkspace } from '../workspace/types.js';\n\nconst logger = console;\n\n/** The stage is not one the compose target deploys. */\nexport class ExecStageNotComposed extends GkmError {\n\tconstructor(readonly stage: string) {\n\t\tsuper(\n\t\t\t`gkm exec --stage runs a command against a compose stage's server, and ` +\n\t\t\t\t`'${stage}' is not deployed with compose. Set deploy.default: 'compose' ` +\n\t\t\t\t'in gkm.config.ts, or run the command where the stage runs.',\n\t\t);\n\t\tthis.name = 'ExecStageNotComposed';\n\t}\n}\n\n/** The stage's secrets lack what its deploy generates: it was never deployed. */\nexport class ExecStageNotDeployed extends GkmError {\n\tconstructor(\n\t\treadonly stage: string,\n\t\treadonly missing: readonly string[],\n\t) {\n\t\tsuper(\n\t\t\t`'${stage}' has no ${missing.join(', ')} in its secrets yet, so what its ` +\n\t\t\t\t'apps run with is not decided. Deploy it first: ' +\n\t\t\t\t`gkm deploy --stage ${stage}.`,\n\t\t);\n\t\tthis.name = 'ExecStageNotDeployed';\n\t}\n}\n\n/** The directory is not one of the stack's apps. */\nexport class ExecAppNotInStack extends GkmError {\n\tconstructor(\n\t\treadonly stage: string,\n\t\treadonly app: string | undefined,\n\t\treadonly apps: readonly string[],\n\t) {\n\t\tsuper(\n\t\t\t`${app ? `'${app}' is not` : 'This directory is not in'} one of the apps ` +\n\t\t\t\t`'${stage}' runs (${apps.join(', ')}), so there is no environment to run ` +\n\t\t\t\t`as. Run from an app's directory: cd apps/${apps[0] ?? '<app>'} && ` +\n\t\t\t\t`gkm exec --stage ${stage} -- <command>.`,\n\t\t);\n\t\tthis.name = 'ExecAppNotInStack';\n\t}\n}\n\n/** A protected stage, and nobody said yes. */\nexport class ExecNotConfirmed extends GkmError {\n\tconstructor(readonly stage: string) {\n\t\tsuper(`Nothing was run against '${stage}'.`);\n\t\tthis.name = 'ExecNotConfirmed';\n\t}\n}\n\nexport interface StageExecOptions {\n\tstage: string;\n\t/** The AWS profile for the stage's account, where its secrets are kept. */\n\tprofile?: string;\n\t/**\n\t * The SSH login to reach the server as, in place of\n\t * `deploy.compose.server.<stage>.user` — a person's own, where the\n\t * deploy's key lives only in CI.\n\t */\n\tsshUser?: string;\n\t/** Run against a protected stage without asking. */\n\tyes?: boolean;\n\tcwd?: string;\n}\n\n/** What the command needs from outside the process, so tests can stand in. */\nexport interface StageExecDeps {\n\t/** The server's Docker, asked which port each service publishes. */\n\tdocker: ComposeDocker;\n\t/** The tunnels to the server's loopback. */\n\tserver: ServerAccess;\n\t/** Asked before running against a protected stage. */\n\tconfirm: (question: string) => Promise<boolean>;\n\t/** Where the edge's ports are read from, as the deploy reads them. */\n\tenv: NodeJS.ProcessEnv;\n}\n\nexport const defaultStageExecDeps: StageExecDeps = {\n\tdocker: dockerCompose,\n\tserver: sshAccess,\n\tconfirm: async (question) => {\n\t\tconst answer = await prompt(`${question} [y/N] `, {\n\t\t\tinstead: 'Pass --yes to run without asking.',\n\t\t});\n\t\treturn /^y(es)?$/i.test(answer.trim());\n\t},\n\tenv: process.env,\n};\n\n/** A service on the stack's network the app's environment names. */\nexport interface StackAddress {\n\tservice: string;\n\tport: number;\n}\n\n/**\n * The environment an app runs with on the stage: a backend's container's,\n * over the keys of each worker built from it; a site's or a mobile app's,\n * the public values its build reads.\n *\n * @throws {ExecAppNotInStack}\n */\nexport function appEnvironment(\n\tstack: Pick<ComposeStack, 'stage' | 'apps' | 'workers' | 'mobileApps'>,\n\tappName: string | undefined,\n): Record<string, string> {\n\tconst mobile = stack.mobileApps.find(\n\t\t(candidate) => candidate.name === appName,\n\t);\n\tif (mobile) return { ...mobile.values };\n\tconst app = stack.apps.find((candidate) => candidate.name === appName);\n\tif (!app) {\n\t\tthrow new ExecAppNotInStack(stack.stage, appName, [\n\t\t\t...stack.apps.map((candidate) => candidate.name),\n\t\t\t...stack.mobileApps.map((candidate) => candidate.name),\n\t\t]);\n\t}\n\tif (app.kind === 'site') return { ...app.build?.args };\n\tconst workers = stack.workers.filter((worker) => worker.host === app.name);\n\treturn Object.assign(\n\t\t{},\n\t\t...workers.map((worker) => worker.env ?? {}),\n\t\tapp.env ?? {},\n\t);\n}\n\n/** `host:port`, at a URL's authority — after `//` or a login's `@`. */\nfunction authority(host: string, port: number): RegExp {\n\treturn new RegExp(\n\t\t`(?<=[/@,])${host.replace(/[.-]/g, '\\\\$&')}:${port}(?!\\\\d)`,\n\t\t'g',\n\t);\n}\n\n/**\n * Each of the stack's services the environment reaches on the stack's\n * network, once: `postgres:5432`, `minio:9000`.\n */\nexport function stackAddresses(\n\tenv: Readonly<Record<string, string>>,\n\tstack: Pick<ComposeStack, 'infra'>,\n): StackAddress[] {\n\tconst found = new Map<string, StackAddress>();\n\tconst pattern = /(?<=[/@,])([a-z0-9-]+):(\\d+)(?!\\d)/g;\n\tfor (const value of Object.values(env)) {\n\t\tfor (const match of value.matchAll(pattern)) {\n\t\t\tconst service = match[1]!;\n\t\t\tif (!stack.infra.includes(service)) continue;\n\t\t\tconst port = Number(match[2]);\n\t\t\tfound.set(`${service}:${port}`, { service, port });\n\t\t}\n\t}\n\treturn [...found.values()];\n}\n\n/**\n * The environment as this machine reaches it: each service at its tunnel's\n * local port, and each sibling app at its public address. What has no\n * tunnel is left as the container has it.\n */\nexport function throughTunnels(\n\tenv: Readonly<Record<string, string>>,\n\tstack: Pick<ComposeStack, 'apps' | 'project'>,\n\ttunnels: ReadonlyMap<string, number>,\n): Record<string, string> {\n\tconst rewrites: [RegExp, string][] = [];\n\tfor (const [address, port] of tunnels) {\n\t\tconst [service, inside] = address.split(':');\n\t\trewrites.push([authority(service!, Number(inside)), `127.0.0.1:${port}`]);\n\t}\n\tfor (const app of stack.apps) {\n\t\tfor (const host of [app.name, edgeAlias(stack.project, app.name)]) {\n\t\t\trewrites.push([\n\t\t\t\tnew RegExp(\n\t\t\t\t\t`http://${host.replace(/[.-]/g, '\\\\$&')}:${app.port}(?!\\\\d)`,\n\t\t\t\t\t'g',\n\t\t\t\t),\n\t\t\t\tapp.url,\n\t\t\t]);\n\t\t}\n\t}\n\tconst out: Record<string, string> = {};\n\tfor (const [key, value] of Object.entries(env)) {\n\t\tout[key] = rewrites.reduce(\n\t\t\t(current, [pattern, replacement]) =>\n\t\t\t\tcurrent.replace(pattern, () => replacement),\n\t\t\tvalue,\n\t\t);\n\t}\n\treturn out;\n}\n\n/**\n * The preload that fills Credentials from the command's own environment: a\n * `data:` URL holding the keys, so no file — and no value — is written.\n */\nexport function credentialsImport(keys: readonly string[]): string {\n\tconst source =\n\t\t'globalThis.__gkm_credentials__=Object.fromEntries(' +\n\t\t`${JSON.stringify(keys)}.map((k)=>[k,process.env[k]]));`;\n\treturn `--import=data:text/javascript,${encodeURIComponent(source)}`;\n}\n\n/** A deployed compose stage, as its deploy has it, and the app here. */\nexport interface LoadedStage {\n\tworkspace: NormalizedWorkspace;\n\tmanifest: ConstructManifest;\n\tstack: ComposeStack;\n\t/** The stage's secrets, as its deploy reads them. */\n\tsecrets: StageSecrets;\n\t/** The app this directory is, if it is one. */\n\tappName: string | undefined;\n\tprotectedStage: boolean;\n}\n\n/**\n * The stage's stack, and the app this directory is, as the deploy has them:\n * its secrets read from its store, its values derived as the deploy derives\n * them. Nothing is reached but the store — no server, no SSH.\n *\n * @throws {UndeclaredStage}, {ExecStageNotComposed}, {ExecStageNotDeployed}\n */\nexport async function loadStage(\n\toptions: Pick<StageExecOptions, 'stage' | 'profile' | 'cwd'>,\n\tenv: NodeJS.ProcessEnv = process.env,\n): Promise<LoadedStage> {\n\tconst cwd = options.cwd ?? process.cwd();\n\tconst { stage } = options;\n\t// Outside an app the workspace is still loaded, so the stage is checked\n\t// before the directory is refused.\n\tconst info = await loadWorkspaceAppInfo(cwd).catch((error) => {\n\t\tif (error instanceof NotInAnApp) return undefined;\n\t\tthrow error;\n\t});\n\tconst workspace =\n\t\tinfo?.workspace ?? (await loadWorkspaceConfig(cwd)).workspace;\n\n\tassertDeployedStage(workspace.stages, stage);\n\tif (!deploysWithCompose(workspace)) throw new ExecStageNotComposed(stage);\n\n\tconst runnables: Record<string, string[]> = {};\n\tconst background: Record<string, string[]> = {};\n\tconst manifest: ConstructManifest = await discover({\n\t\tpatterns: constructGlobs(workspace),\n\t\tcwd: workspace.root,\n\t\trunnables,\n\t\tbackground,\n\t});\n\n\tconst store = await secretsStoreFor(workspace, stage, {\n\t\t...(options.profile ? { profile: options.profile } : {}),\n\t});\n\tconst { secrets, generated } = deployedStackSecrets(\n\t\tworkspace,\n\t\tstage,\n\t\tawait store.read(stage),\n\t\tmanifest,\n\t);\n\tif (generated.length > 0) throw new ExecStageNotDeployed(stage, generated);\n\n\tconst stack = composeStack({\n\t\tworkspace,\n\t\tmanifest,\n\t\trunnables,\n\t\tbackground,\n\t\tstage,\n\t\tidentity: deployIdentity(workspace, stage),\n\t\t// Built, so a site's public values are resolved as its build has them.\n\t\timages: { mode: 'build', tag: 'exec' },\n\t\tsecrets,\n\t\tports: edgePorts(env),\n\t});\n\treturn {\n\t\tworkspace,\n\t\tmanifest,\n\t\tstack,\n\t\tsecrets,\n\t\tappName: info?.appName,\n\t\tprotectedStage: workspace.stages.protected?.includes(stage) ?? false,\n\t};\n}\n\n/** The stage's server, as SSH reaches it: `--ssh-user` over the config's. */\nfunction stageServer(\n\tloaded: LoadedStage,\n\toptions: StageExecOptions,\n): ComposeServer {\n\tconst server = composeServer({\n\t\tstage: options.stage,\n\t\tlocal: false,\n\t\tconfig: loaded.workspace.deploy?.compose?.server,\n\t\tcustom: loaded.secrets.custom,\n\t})!;\n\tif (options.sshUser) server.user = options.sshUser;\n\treturn server;\n}\n\n/**\n * Run `commandArgs` as the app this directory is, on the stage — through a\n * tunnel to each service it reaches — and return its exit code. Every tunnel\n * is closed once it exits, whatever it exits with.\n *\n * @throws {ExecStageNotComposed}, {ExecStageNotDeployed}, {ExecAppNotInStack},\n * {ExecNotConfirmed}, {ComposeServerUnreachable}\n */\nexport async function execOnStage(\n\tcommandArgs: readonly [string, ...string[]],\n\toptions: StageExecOptions,\n\tdeps: StageExecDeps = defaultStageExecDeps,\n): Promise<number> {\n\tconst { stage } = options;\n\tconst loaded = await loadStage(options, deps.env);\n\tconst { stack, appName, protectedStage } = loaded;\n\tconst env = appEnvironment(stack, appName);\n\n\tif (protectedStage && !options.yes) {\n\t\tconst question = `'${stage}' is a protected stage. Run ${commandArgs.join(' ')} against it?`;\n\t\tif (!(await deps.confirm(question))) throw new ExecNotConfirmed(stage);\n\t}\n\n\t// Only what reaches a service on the stack needs the server: a site's or\n\t// a mobile app's values are public addresses, and need no SSH at all.\n\tconst addresses = stackAddresses(env, stack);\n\tif (addresses.length === 0) {\n\t\tlogger.log(`📦 App: ${appName} on '${stage}'`);\n\t\tlogger.log(`🔐 Loaded ${Object.keys(env).length} value(s) for '${stage}'`);\n\t\treturn run(commandArgs, env, options.cwd);\n\t}\n\n\tconst server = stageServer(loaded, options);\n\tlogger.log(`📦 App: ${appName} on '${stage}' (${server.host})`);\n\n\t// The deploy's own check: SSH logs in and Docker answers, before any\n\t// tunnel is asked for — so a key the server refuses is said as that.\n\tawait deps.server.check(server);\n\n\tconst tunnels: Tunnel[] = [];\n\tconst ports = new Map<string, number>();\n\ttry {\n\t\tfor (const address of addresses) {\n\t\t\tconst published = await deps.docker.published(\n\t\t\t\t{ host: dockerHost(server) },\n\t\t\t\tstack.project,\n\t\t\t\taddress.service,\n\t\t\t\taddress.port,\n\t\t\t);\n\t\t\tif (published === undefined) {\n\t\t\t\tlogger.log(\n\t\t\t\t\t`⚠️ ${address.service} is not published on the server (no running ` +\n\t\t\t\t\t\t`${stack.project} ${address.service} publishes ${address.port}), so ` +\n\t\t\t\t\t\t'it is unreachable from here; its address is passed on unchanged.',\n\t\t\t\t);\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\tconst tunnel = await deps.server.tunnel(server, published);\n\t\t\ttunnels.push(tunnel);\n\t\t\tports.set(`${address.service}:${address.port}`, tunnel.port);\n\t\t\tlogger.log(`🚇 ${address.service} → 127.0.0.1:${tunnel.port} (over SSH)`);\n\t\t}\n\n\t\tconst credentials = throughTunnels(env, stack, ports);\n\t\tlogger.log(\n\t\t\t`🔐 Loaded ${Object.keys(credentials).length} value(s) for '${stage}'`,\n\t\t);\n\t\treturn await run(commandArgs, credentials, options.cwd);\n\t} finally {\n\t\tawait Promise.all(tunnels.map((tunnel) => tunnel.close()));\n\t}\n}\n\n/**\n * The command, with the values in its environment and its Credentials. A\n * Ctrl-C reaches it — it is in the terminal's process group — and this\n * process waits for it to exit rather than leaving before the tunnels close.\n */\nfunction run(\n\tcommandArgs: readonly [string, ...string[]],\n\tcredentials: Record<string, string>,\n\tcwd: string | undefined,\n): Promise<number> {\n\tconst [cmd, ...args] = commandArgs;\n\tlogger.log(`🚀 Running: ${commandArgs.join(' ')}`);\n\n\tconst inherited = (process.env.NODE_OPTIONS ?? '')\n\t\t.replace(/--import[= ]\\S+/g, '')\n\t\t.trim();\n\tconst nodeOptions = [inherited, credentialsImport(Object.keys(credentials))]\n\t\t.filter(Boolean)\n\t\t.join(' ');\n\n\tconst wait = () => {};\n\tprocess.on('SIGINT', wait);\n\tprocess.on('SIGTERM', wait);\n\treturn new Promise<number>((resolve) => {\n\t\tconst child = spawn(cmd, args, {\n\t\t\t...(cwd ? { cwd } : {}),\n\t\t\tstdio: 'inherit',\n\t\t\tenv: { ...process.env, ...credentials, NODE_OPTIONS: nodeOptions },\n\t\t});\n\t\tchild.on('close', (code, signal) => {\n\t\t\tresolve(code ?? 128 + (signal ? constants.signals[signal] : 0));\n\t\t});\n\t\tchild.on('error', (error) => {\n\t\t\tlogger.error(`Failed to run command: ${error.message}`);\n\t\t\tresolve(1);\n\t\t});\n\t}).finally(() => {\n\t\tprocess.off('SIGINT', wait);\n\t\tprocess.off('SIGTERM', wait);\n\t});\n}\n"],"mappings":"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"}
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
const require_rolldown_runtime = require("./rolldown-runtime-C0BPl7ul.cjs");
|
|
2
2
|
const require_errors = require("./errors-DMC4Umsb.cjs");
|
|
3
|
-
const require_compose = require("./compose-
|
|
3
|
+
const require_compose = require("./compose-JOasAHcd.cjs");
|
|
4
4
|
const require_workspace = require("./workspace-BwqQnool.cjs");
|
|
5
|
-
const require_dns = require("./dns-
|
|
6
|
-
const require_seeds = require("./seeds-
|
|
5
|
+
const require_dns = require("./dns-DfQKqAyt.cjs");
|
|
6
|
+
const require_seeds = require("./seeds-CF9Nx6_g.cjs");
|
|
7
7
|
const require_identity = require("./identity-CbLfgQNT.cjs");
|
|
8
8
|
const require_discover = require("./discover-Bsr7HjqJ.cjs");
|
|
9
9
|
const require_config = require("./config.cjs");
|
|
@@ -310,4 +310,4 @@ Object.defineProperty(exports, "stage_exports", {
|
|
|
310
310
|
}
|
|
311
311
|
});
|
|
312
312
|
|
|
313
|
-
//# sourceMappingURL=stage-
|
|
313
|
+
//# sourceMappingURL=stage-y9qFelxT.cjs.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"stage-W3PUt7ar.cjs","names":["GkmError","dockerCompose","sshAccess","prompt","edgeAlias","loadWorkspaceAppInfo","NotInAnApp","loadWorkspaceConfig","deploysWithCompose","discover","constructGlobs","secretsStoreFor","deployedStackSecrets","composeStack","deployIdentity","edgePorts","composeServer","dockerHost","spawn","constants"],"sources":["../src/exec/stage.ts"],"sourcesContent":["/**\n * `gkm exec --stage <stage> -- <command>`: a command run on this machine as\n * a deployed compose stage's app — a data import, a backfill, a report.\n *\n * The app's environment is the one its container runs with, derived the way\n * the deploy derives it (`composeStack`) from the stage's secrets, read from\n * the stage's store the way the deploy reads them. Only what the container\n * reaches on the stack's network is moved: each service it names that the\n * server publishes on its loopback — Postgres, MinIO — is reached through an\n * SSH tunnel on the deploy's own login (`ServerAccess.tunnel`), and each\n * sibling app through its public address.\n *\n * Nothing is written to disk. The values exist only in the command's\n * environment, and its Credentials are filled from there by a preload that\n * is a `data:` URL naming the keys — never their values.\n */\n\nimport { spawn } from 'node:child_process';\nimport { constants } from 'node:os';\nimport type { ConstructManifest } from '@geekmidas/manifest';\nimport { type ComposeDocker, dockerCompose } from '../compose/docker.js';\nimport { deployedStackSecrets } from '../compose/secrets.js';\nimport {\n\ttype ComposeServer,\n\tcomposeServer,\n\tdockerHost,\n\ttype ServerAccess,\n\tsshAccess,\n\ttype Tunnel,\n} from '../compose/server.js';\nimport { type ComposeStack, composeStack } from '../compose/stack.js';\nimport { edgeAlias } from '../compose/traefik.js';\nimport {\n\tloadWorkspaceAppInfo,\n\tloadWorkspaceConfig,\n\tNotInAnApp,\n} from '../config.js';\nimport { deployIdentity } from '../deploy/identity.js';\nimport { GkmError } from '../errors';\nimport { prompt } from '../prompt.js';\nimport { deploysWithCompose } from '../providers/dns.js';\nimport { discover } from '../reconcile/discover.js';\nimport { constructGlobs } from '../reconcile/workspace.js';\nimport { secretsStoreFor } from '../secrets/store.js';\nimport type { StageSecrets } from '../secrets/types.js';\nimport { edgePorts } from '../target/compose/phases.js';\nimport { assertDeployedStage } from '../workspace/stages.js';\nimport type { NormalizedWorkspace } from '../workspace/types.js';\n\nconst logger = console;\n\n/** The stage is not one the compose target deploys. */\nexport class ExecStageNotComposed extends GkmError {\n\tconstructor(readonly stage: string) {\n\t\tsuper(\n\t\t\t`gkm exec --stage runs a command against a compose stage's server, and ` +\n\t\t\t\t`'${stage}' is not deployed with compose. Set deploy.default: 'compose' ` +\n\t\t\t\t'in gkm.config.ts, or run the command where the stage runs.',\n\t\t);\n\t\tthis.name = 'ExecStageNotComposed';\n\t}\n}\n\n/** The stage's secrets lack what its deploy generates: it was never deployed. */\nexport class ExecStageNotDeployed extends GkmError {\n\tconstructor(\n\t\treadonly stage: string,\n\t\treadonly missing: readonly string[],\n\t) {\n\t\tsuper(\n\t\t\t`'${stage}' has no ${missing.join(', ')} in its secrets yet, so what its ` +\n\t\t\t\t'apps run with is not decided. Deploy it first: ' +\n\t\t\t\t`gkm deploy --stage ${stage}.`,\n\t\t);\n\t\tthis.name = 'ExecStageNotDeployed';\n\t}\n}\n\n/** The directory is not one of the stack's apps. */\nexport class ExecAppNotInStack extends GkmError {\n\tconstructor(\n\t\treadonly stage: string,\n\t\treadonly app: string | undefined,\n\t\treadonly apps: readonly string[],\n\t) {\n\t\tsuper(\n\t\t\t`${app ? `'${app}' is not` : 'This directory is not in'} one of the apps ` +\n\t\t\t\t`'${stage}' runs (${apps.join(', ')}), so there is no environment to run ` +\n\t\t\t\t`as. Run from an app's directory: cd apps/${apps[0] ?? '<app>'} && ` +\n\t\t\t\t`gkm exec --stage ${stage} -- <command>.`,\n\t\t);\n\t\tthis.name = 'ExecAppNotInStack';\n\t}\n}\n\n/** A protected stage, and nobody said yes. */\nexport class ExecNotConfirmed extends GkmError {\n\tconstructor(readonly stage: string) {\n\t\tsuper(`Nothing was run against '${stage}'.`);\n\t\tthis.name = 'ExecNotConfirmed';\n\t}\n}\n\nexport interface StageExecOptions {\n\tstage: string;\n\t/** The AWS profile for the stage's account, where its secrets are kept. */\n\tprofile?: string;\n\t/**\n\t * The SSH login to reach the server as, in place of\n\t * `deploy.compose.server.<stage>.user` — a person's own, where the\n\t * deploy's key lives only in CI.\n\t */\n\tsshUser?: string;\n\t/** Run against a protected stage without asking. */\n\tyes?: boolean;\n\tcwd?: string;\n}\n\n/** What the command needs from outside the process, so tests can stand in. */\nexport interface StageExecDeps {\n\t/** The server's Docker, asked which port each service publishes. */\n\tdocker: ComposeDocker;\n\t/** The tunnels to the server's loopback. */\n\tserver: ServerAccess;\n\t/** Asked before running against a protected stage. */\n\tconfirm: (question: string) => Promise<boolean>;\n\t/** Where the edge's ports are read from, as the deploy reads them. */\n\tenv: NodeJS.ProcessEnv;\n}\n\nexport const defaultStageExecDeps: StageExecDeps = {\n\tdocker: dockerCompose,\n\tserver: sshAccess,\n\tconfirm: async (question) => {\n\t\tconst answer = await prompt(`${question} [y/N] `, {\n\t\t\tinstead: 'Pass --yes to run without asking.',\n\t\t});\n\t\treturn /^y(es)?$/i.test(answer.trim());\n\t},\n\tenv: process.env,\n};\n\n/** A service on the stack's network the app's environment names. */\nexport interface StackAddress {\n\tservice: string;\n\tport: number;\n}\n\n/**\n * The environment an app runs with on the stage: a backend's container's,\n * over the keys of each worker built from it; a site's or a mobile app's,\n * the public values its build reads.\n *\n * @throws {ExecAppNotInStack}\n */\nexport function appEnvironment(\n\tstack: Pick<ComposeStack, 'stage' | 'apps' | 'workers' | 'mobileApps'>,\n\tappName: string | undefined,\n): Record<string, string> {\n\tconst mobile = stack.mobileApps.find(\n\t\t(candidate) => candidate.name === appName,\n\t);\n\tif (mobile) return { ...mobile.values };\n\tconst app = stack.apps.find((candidate) => candidate.name === appName);\n\tif (!app) {\n\t\tthrow new ExecAppNotInStack(stack.stage, appName, [\n\t\t\t...stack.apps.map((candidate) => candidate.name),\n\t\t\t...stack.mobileApps.map((candidate) => candidate.name),\n\t\t]);\n\t}\n\tif (app.kind === 'site') return { ...app.build?.args };\n\tconst workers = stack.workers.filter((worker) => worker.host === app.name);\n\treturn Object.assign(\n\t\t{},\n\t\t...workers.map((worker) => worker.env ?? {}),\n\t\tapp.env ?? {},\n\t);\n}\n\n/** `host:port`, at a URL's authority — after `//` or a login's `@`. */\nfunction authority(host: string, port: number): RegExp {\n\treturn new RegExp(\n\t\t`(?<=[/@,])${host.replace(/[.-]/g, '\\\\$&')}:${port}(?!\\\\d)`,\n\t\t'g',\n\t);\n}\n\n/**\n * Each of the stack's services the environment reaches on the stack's\n * network, once: `postgres:5432`, `minio:9000`.\n */\nexport function stackAddresses(\n\tenv: Readonly<Record<string, string>>,\n\tstack: Pick<ComposeStack, 'infra'>,\n): StackAddress[] {\n\tconst found = new Map<string, StackAddress>();\n\tconst pattern = /(?<=[/@,])([a-z0-9-]+):(\\d+)(?!\\d)/g;\n\tfor (const value of Object.values(env)) {\n\t\tfor (const match of value.matchAll(pattern)) {\n\t\t\tconst service = match[1]!;\n\t\t\tif (!stack.infra.includes(service)) continue;\n\t\t\tconst port = Number(match[2]);\n\t\t\tfound.set(`${service}:${port}`, { service, port });\n\t\t}\n\t}\n\treturn [...found.values()];\n}\n\n/**\n * The environment as this machine reaches it: each service at its tunnel's\n * local port, and each sibling app at its public address. What has no\n * tunnel is left as the container has it.\n */\nexport function throughTunnels(\n\tenv: Readonly<Record<string, string>>,\n\tstack: Pick<ComposeStack, 'apps' | 'project'>,\n\ttunnels: ReadonlyMap<string, number>,\n): Record<string, string> {\n\tconst rewrites: [RegExp, string][] = [];\n\tfor (const [address, port] of tunnels) {\n\t\tconst [service, inside] = address.split(':');\n\t\trewrites.push([authority(service!, Number(inside)), `127.0.0.1:${port}`]);\n\t}\n\tfor (const app of stack.apps) {\n\t\tfor (const host of [app.name, edgeAlias(stack.project, app.name)]) {\n\t\t\trewrites.push([\n\t\t\t\tnew RegExp(\n\t\t\t\t\t`http://${host.replace(/[.-]/g, '\\\\$&')}:${app.port}(?!\\\\d)`,\n\t\t\t\t\t'g',\n\t\t\t\t),\n\t\t\t\tapp.url,\n\t\t\t]);\n\t\t}\n\t}\n\tconst out: Record<string, string> = {};\n\tfor (const [key, value] of Object.entries(env)) {\n\t\tout[key] = rewrites.reduce(\n\t\t\t(current, [pattern, replacement]) =>\n\t\t\t\tcurrent.replace(pattern, () => replacement),\n\t\t\tvalue,\n\t\t);\n\t}\n\treturn out;\n}\n\n/**\n * The preload that fills Credentials from the command's own environment: a\n * `data:` URL holding the keys, so no file — and no value — is written.\n */\nexport function credentialsImport(keys: readonly string[]): string {\n\tconst source =\n\t\t'globalThis.__gkm_credentials__=Object.fromEntries(' +\n\t\t`${JSON.stringify(keys)}.map((k)=>[k,process.env[k]]));`;\n\treturn `--import=data:text/javascript,${encodeURIComponent(source)}`;\n}\n\n/** A deployed compose stage, as its deploy has it, and the app here. */\nexport interface LoadedStage {\n\tworkspace: NormalizedWorkspace;\n\tmanifest: ConstructManifest;\n\tstack: ComposeStack;\n\t/** The stage's secrets, as its deploy reads them. */\n\tsecrets: StageSecrets;\n\t/** The app this directory is, if it is one. */\n\tappName: string | undefined;\n\tprotectedStage: boolean;\n}\n\n/**\n * The stage's stack, and the app this directory is, as the deploy has them:\n * its secrets read from its store, its values derived as the deploy derives\n * them. Nothing is reached but the store — no server, no SSH.\n *\n * @throws {UndeclaredStage}, {ExecStageNotComposed}, {ExecStageNotDeployed}\n */\nexport async function loadStage(\n\toptions: Pick<StageExecOptions, 'stage' | 'profile' | 'cwd'>,\n\tenv: NodeJS.ProcessEnv = process.env,\n): Promise<LoadedStage> {\n\tconst cwd = options.cwd ?? process.cwd();\n\tconst { stage } = options;\n\t// Outside an app the workspace is still loaded, so the stage is checked\n\t// before the directory is refused.\n\tconst info = await loadWorkspaceAppInfo(cwd).catch((error) => {\n\t\tif (error instanceof NotInAnApp) return undefined;\n\t\tthrow error;\n\t});\n\tconst workspace =\n\t\tinfo?.workspace ?? (await loadWorkspaceConfig(cwd)).workspace;\n\n\tassertDeployedStage(workspace.stages, stage);\n\tif (!deploysWithCompose(workspace)) throw new ExecStageNotComposed(stage);\n\n\tconst runnables: Record<string, string[]> = {};\n\tconst background: Record<string, string[]> = {};\n\tconst manifest: ConstructManifest = await discover({\n\t\tpatterns: constructGlobs(workspace),\n\t\tcwd: workspace.root,\n\t\trunnables,\n\t\tbackground,\n\t});\n\n\tconst store = await secretsStoreFor(workspace, stage, {\n\t\t...(options.profile ? { profile: options.profile } : {}),\n\t});\n\tconst { secrets, generated } = deployedStackSecrets(\n\t\tworkspace,\n\t\tstage,\n\t\tawait store.read(stage),\n\t\tmanifest,\n\t);\n\tif (generated.length > 0) throw new ExecStageNotDeployed(stage, generated);\n\n\tconst stack = composeStack({\n\t\tworkspace,\n\t\tmanifest,\n\t\trunnables,\n\t\tbackground,\n\t\tstage,\n\t\tidentity: deployIdentity(workspace, stage),\n\t\t// Built, so a site's public values are resolved as its build has them.\n\t\timages: { mode: 'build', tag: 'exec' },\n\t\tsecrets,\n\t\tports: edgePorts(env),\n\t});\n\treturn {\n\t\tworkspace,\n\t\tmanifest,\n\t\tstack,\n\t\tsecrets,\n\t\tappName: info?.appName,\n\t\tprotectedStage: workspace.stages.protected?.includes(stage) ?? false,\n\t};\n}\n\n/** The stage's server, as SSH reaches it: `--ssh-user` over the config's. */\nfunction stageServer(\n\tloaded: LoadedStage,\n\toptions: StageExecOptions,\n): ComposeServer {\n\tconst server = composeServer({\n\t\tstage: options.stage,\n\t\tlocal: false,\n\t\tconfig: loaded.workspace.deploy?.compose?.server,\n\t\tcustom: loaded.secrets.custom,\n\t})!;\n\tif (options.sshUser) server.user = options.sshUser;\n\treturn server;\n}\n\n/**\n * Run `commandArgs` as the app this directory is, on the stage — through a\n * tunnel to each service it reaches — and return its exit code. Every tunnel\n * is closed once it exits, whatever it exits with.\n *\n * @throws {ExecStageNotComposed}, {ExecStageNotDeployed}, {ExecAppNotInStack},\n * {ExecNotConfirmed}, {ComposeServerUnreachable}\n */\nexport async function execOnStage(\n\tcommandArgs: readonly [string, ...string[]],\n\toptions: StageExecOptions,\n\tdeps: StageExecDeps = defaultStageExecDeps,\n): Promise<number> {\n\tconst { stage } = options;\n\tconst loaded = await loadStage(options, deps.env);\n\tconst { stack, appName, protectedStage } = loaded;\n\tconst env = appEnvironment(stack, appName);\n\n\tif (protectedStage && !options.yes) {\n\t\tconst question = `'${stage}' is a protected stage. Run ${commandArgs.join(' ')} against it?`;\n\t\tif (!(await deps.confirm(question))) throw new ExecNotConfirmed(stage);\n\t}\n\n\t// Only what reaches a service on the stack needs the server: a site's or\n\t// a mobile app's values are public addresses, and need no SSH at all.\n\tconst addresses = stackAddresses(env, stack);\n\tif (addresses.length === 0) {\n\t\tlogger.log(`📦 App: ${appName} on '${stage}'`);\n\t\tlogger.log(`🔐 Loaded ${Object.keys(env).length} value(s) for '${stage}'`);\n\t\treturn run(commandArgs, env, options.cwd);\n\t}\n\n\tconst server = stageServer(loaded, options);\n\tlogger.log(`📦 App: ${appName} on '${stage}' (${server.host})`);\n\n\t// The deploy's own check: SSH logs in and Docker answers, before any\n\t// tunnel is asked for — so a key the server refuses is said as that.\n\tawait deps.server.check(server);\n\n\tconst tunnels: Tunnel[] = [];\n\tconst ports = new Map<string, number>();\n\ttry {\n\t\tfor (const address of addresses) {\n\t\t\tconst published = await deps.docker.published(\n\t\t\t\t{ host: dockerHost(server) },\n\t\t\t\tstack.project,\n\t\t\t\taddress.service,\n\t\t\t\taddress.port,\n\t\t\t);\n\t\t\tif (published === undefined) {\n\t\t\t\tlogger.log(\n\t\t\t\t\t`⚠️ ${address.service} is not published on the server (no running ` +\n\t\t\t\t\t\t`${stack.project} ${address.service} publishes ${address.port}), so ` +\n\t\t\t\t\t\t'it is unreachable from here; its address is passed on unchanged.',\n\t\t\t\t);\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\tconst tunnel = await deps.server.tunnel(server, published);\n\t\t\ttunnels.push(tunnel);\n\t\t\tports.set(`${address.service}:${address.port}`, tunnel.port);\n\t\t\tlogger.log(`🚇 ${address.service} → 127.0.0.1:${tunnel.port} (over SSH)`);\n\t\t}\n\n\t\tconst credentials = throughTunnels(env, stack, ports);\n\t\tlogger.log(\n\t\t\t`🔐 Loaded ${Object.keys(credentials).length} value(s) for '${stage}'`,\n\t\t);\n\t\treturn await run(commandArgs, credentials, options.cwd);\n\t} finally {\n\t\tawait Promise.all(tunnels.map((tunnel) => tunnel.close()));\n\t}\n}\n\n/**\n * The command, with the values in its environment and its Credentials. A\n * Ctrl-C reaches it — it is in the terminal's process group — and this\n * process waits for it to exit rather than leaving before the tunnels close.\n */\nfunction run(\n\tcommandArgs: readonly [string, ...string[]],\n\tcredentials: Record<string, string>,\n\tcwd: string | undefined,\n): Promise<number> {\n\tconst [cmd, ...args] = commandArgs;\n\tlogger.log(`🚀 Running: ${commandArgs.join(' ')}`);\n\n\tconst inherited = (process.env.NODE_OPTIONS ?? '')\n\t\t.replace(/--import[= ]\\S+/g, '')\n\t\t.trim();\n\tconst nodeOptions = [inherited, credentialsImport(Object.keys(credentials))]\n\t\t.filter(Boolean)\n\t\t.join(' ');\n\n\tconst wait = () => {};\n\tprocess.on('SIGINT', wait);\n\tprocess.on('SIGTERM', wait);\n\treturn new Promise<number>((resolve) => {\n\t\tconst child = spawn(cmd, args, {\n\t\t\t...(cwd ? { cwd } : {}),\n\t\t\tstdio: 'inherit',\n\t\t\tenv: { ...process.env, ...credentials, NODE_OPTIONS: nodeOptions },\n\t\t});\n\t\tchild.on('close', (code, signal) => {\n\t\t\tresolve(code ?? 128 + (signal ? constants.signals[signal] : 0));\n\t\t});\n\t\tchild.on('error', (error) => {\n\t\t\tlogger.error(`Failed to run command: ${error.message}`);\n\t\t\tresolve(1);\n\t\t});\n\t}).finally(() => {\n\t\tprocess.off('SIGINT', wait);\n\t\tprocess.off('SIGTERM', wait);\n\t});\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAiDA,MAAM,SAAS;;AAGf,IAAa,uBAAb,cAA0CA,eAAAA,SAAS;CAC7B;CAArB,YAAY,OAAwB;EACnC,MACC,0EACK,MAAM,yHAEZ;EALoB,KAAA,QAAA;EAMpB,KAAK,OAAO;CACb;AACD;;AAGA,IAAa,uBAAb,cAA0CA,eAAAA,SAAS;CAExC;CACA;CAFV,YACC,OACA,SACC;EACD,MACC,IAAI,MAAM,WAAW,QAAQ,KAAK,IAAI,EAAE,qGAEjB,MAAM,EAC9B;EAPS,KAAA,QAAA;EACA,KAAA,UAAA;EAOT,KAAK,OAAO;CACb;AACD;;AAGA,IAAa,oBAAb,cAAuCA,eAAAA,SAAS;CAErC;CACA;CACA;CAHV,YACC,OACA,KACA,MACC;EACD,MACC,GAAG,MAAM,IAAI,IAAI,YAAY,2BAA2B,oBACnD,MAAM,UAAU,KAAK,KAAK,IAAI,EAAE,gFACQ,KAAK,MAAM,QAAQ,uBAC3C,MAAM,eAC5B;EATS,KAAA,QAAA;EACA,KAAA,MAAA;EACA,KAAA,OAAA;EAQT,KAAK,OAAO;CACb;AACD;;AAGA,IAAa,mBAAb,cAAsCA,eAAAA,SAAS;CACzB;CAArB,YAAY,OAAwB;EACnC,MAAM,4BAA4B,MAAM,GAAG;EADvB,KAAA,QAAA;EAEpB,KAAK,OAAO;CACb;AACD;AA6BA,MAAa,uBAAsC;CAClD,QAAQC,gBAAAA;CACR,QAAQC,gBAAAA;CACR,SAAS,OAAO,aAAa;EAC5B,MAAM,SAAS,MAAMC,cAAAA,OAAO,GAAG,SAAS,UAAU,EACjD,SAAS,oCACV,CAAC;EACD,OAAO,YAAY,KAAK,OAAO,KAAK,CAAC;CACtC;CACA,KAAK,QAAQ;AACd;;;;;;;;AAeA,SAAgB,eACf,OACA,SACyB;CACzB,MAAM,SAAS,MAAM,WAAW,MAC9B,cAAc,UAAU,SAAS,OACnC;CACA,IAAI,QAAQ,OAAO,EAAE,GAAG,OAAO,OAAO;CACtC,MAAM,MAAM,MAAM,KAAK,MAAM,cAAc,UAAU,SAAS,OAAO;CACrE,IAAI,CAAC,KACJ,MAAM,IAAI,kBAAkB,MAAM,OAAO,SAAS,CACjD,GAAG,MAAM,KAAK,KAAK,cAAc,UAAU,IAAI,GAC/C,GAAG,MAAM,WAAW,KAAK,cAAc,UAAU,IAAI,CACtD,CAAC;CAEF,IAAI,IAAI,SAAS,QAAQ,OAAO,EAAE,GAAG,IAAI,OAAO,KAAK;CACrD,MAAM,UAAU,MAAM,QAAQ,QAAQ,WAAW,OAAO,SAAS,IAAI,IAAI;CACzE,OAAO,OAAO,OACb,CAAC,GACD,GAAG,QAAQ,KAAK,WAAW,OAAO,OAAO,CAAC,CAAC,GAC3C,IAAI,OAAO,CAAC,CACb;AACD;;AAGA,SAAS,UAAU,MAAc,MAAsB;CACtD,OAAO,IAAI,OACV,aAAa,KAAK,QAAQ,SAAS,MAAM,EAAE,GAAG,KAAK,UACnD,GACD;AACD;;;;;AAMA,SAAgB,eACf,KACA,OACiB;CACjB,MAAM,wBAAQ,IAAI,IAA0B;CAC5C,MAAM,UAAU;CAChB,KAAK,MAAM,SAAS,OAAO,OAAO,GAAG,GACpC,KAAK,MAAM,SAAS,MAAM,SAAS,OAAO,GAAG;EAC5C,MAAM,UAAU,MAAM;EACtB,IAAI,CAAC,MAAM,MAAM,SAAS,OAAO,GAAG;EACpC,MAAM,OAAO,OAAO,MAAM,EAAE;EAC5B,MAAM,IAAI,GAAG,QAAQ,GAAG,QAAQ;GAAE;GAAS;EAAK,CAAC;CAClD;CAED,OAAO,CAAC,GAAG,MAAM,OAAO,CAAC;AAC1B;;;;;;AAOA,SAAgB,eACf,KACA,OACA,SACyB;CACzB,MAAM,WAA+B,CAAC;CACtC,KAAK,MAAM,CAAC,SAAS,SAAS,SAAS;EACtC,MAAM,CAAC,SAAS,UAAU,QAAQ,MAAM,GAAG;EAC3C,SAAS,KAAK,CAAC,UAAU,SAAU,OAAO,MAAM,CAAC,GAAG,aAAa,MAAM,CAAC;CACzE;CACA,KAAK,MAAM,OAAO,MAAM,MACvB,KAAK,MAAM,QAAQ,CAAC,IAAI,MAAMC,gBAAAA,UAAU,MAAM,SAAS,IAAI,IAAI,CAAC,GAC/D,SAAS,KAAK,CACb,IAAI,OACH,UAAU,KAAK,QAAQ,SAAS,MAAM,EAAE,GAAG,IAAI,KAAK,UACpD,GACD,GACA,IAAI,GACL,CAAC;CAGH,MAAM,MAA8B,CAAC;CACrC,KAAK,MAAM,CAAC,KAAK,UAAU,OAAO,QAAQ,GAAG,GAC5C,IAAI,OAAO,SAAS,QAClB,SAAS,CAAC,SAAS,iBACnB,QAAQ,QAAQ,eAAe,WAAW,GAC3C,KACD;CAED,OAAO;AACR;;;;;AAMA,SAAgB,kBAAkB,MAAiC;CAClE,MAAM,SACL,qDACG,KAAK,UAAU,IAAI,EAAE;CACzB,OAAO,iCAAiC,mBAAmB,MAAM;AAClE;;;;;;;;AAqBA,eAAsB,UACrB,SACA,MAAyB,QAAQ,KACV;CACvB,MAAM,MAAM,QAAQ,OAAO,QAAQ,IAAI;CACvC,MAAM,EAAE,UAAU;CAGlB,MAAM,OAAO,MAAMC,eAAAA,qBAAqB,GAAG,CAAC,CAAC,OAAO,UAAU;EAC7D,IAAI,iBAAiBC,eAAAA,YAAY,OAAO,KAAA;EACxC,MAAM;CACP,CAAC;CACD,MAAM,YACL,MAAM,cAAc,MAAMC,eAAAA,oBAAoB,GAAG,EAAA,CAAG;CAErD,kBAAA,oBAAoB,UAAU,QAAQ,KAAK;CAC3C,IAAI,CAACC,YAAAA,mBAAmB,SAAS,GAAG,MAAM,IAAI,qBAAqB,KAAK;CAExE,MAAM,YAAsC,CAAC;CAC7C,MAAM,aAAuC,CAAC;CAC9C,MAAM,WAA8B,MAAMC,iBAAAA,SAAS;EAClD,UAAUC,cAAAA,eAAe,SAAS;EAClC,KAAK,UAAU;EACf;EACA;CACD,CAAC;CAED,MAAM,QAAQ,MAAMC,cAAAA,gBAAgB,WAAW,OAAO,EACrD,GAAI,QAAQ,UAAU,EAAE,SAAS,QAAQ,QAAQ,IAAI,CAAC,EACvD,CAAC;CACD,MAAM,EAAE,SAAS,cAAcC,gBAAAA,qBAC9B,WACA,OACA,MAAM,MAAM,KAAK,KAAK,GACtB,QACD;CACA,IAAI,UAAU,SAAS,GAAG,MAAM,IAAI,qBAAqB,OAAO,SAAS;CAczE,OAAO;EACN;EACA;EACA,OAfaC,gBAAAA,aAAa;GAC1B;GACA;GACA;GACA;GACA;GACA,UAAUC,iBAAAA,eAAe,WAAW,KAAK;GAEzC,QAAQ;IAAE,MAAM;IAAS,KAAK;GAAO;GACrC;GACA,OAAOC,gBAAAA,UAAU,GAAG;EACrB,CAIK;EACJ;EACA,SAAS,MAAM;EACf,gBAAgB,UAAU,OAAO,WAAW,SAAS,KAAK,KAAK;CAChE;AACD;;AAGA,SAAS,YACR,QACA,SACgB;CAChB,MAAM,SAASC,gBAAAA,cAAc;EAC5B,OAAO,QAAQ;EACf,OAAO;EACP,QAAQ,OAAO,UAAU,QAAQ,SAAS;EAC1C,QAAQ,OAAO,QAAQ;CACxB,CAAC;CACD,IAAI,QAAQ,SAAS,OAAO,OAAO,QAAQ;CAC3C,OAAO;AACR;;;;;;;;;AAUA,eAAsB,YACrB,aACA,SACA,OAAsB,sBACJ;CAClB,MAAM,EAAE,UAAU;CAClB,MAAM,SAAS,MAAM,UAAU,SAAS,KAAK,GAAG;CAChD,MAAM,EAAE,OAAO,SAAS,mBAAmB;CAC3C,MAAM,MAAM,eAAe,OAAO,OAAO;CAEzC,IAAI,kBAAkB,CAAC,QAAQ,KAAK;EACnC,MAAM,WAAW,IAAI,MAAM,8BAA8B,YAAY,KAAK,GAAG,EAAE;EAC/E,IAAI,CAAE,MAAM,KAAK,QAAQ,QAAQ,GAAI,MAAM,IAAI,iBAAiB,KAAK;CACtE;CAIA,MAAM,YAAY,eAAe,KAAK,KAAK;CAC3C,IAAI,UAAU,WAAW,GAAG;EAC3B,OAAO,IAAI,WAAW,QAAQ,OAAO,MAAM,EAAE;EAC7C,OAAO,IAAI,aAAa,OAAO,KAAK,GAAG,CAAC,CAAC,OAAO,iBAAiB,MAAM,EAAE;EACzE,OAAO,IAAI,aAAa,KAAK,QAAQ,GAAG;CACzC;CAEA,MAAM,SAAS,YAAY,QAAQ,OAAO;CAC1C,OAAO,IAAI,WAAW,QAAQ,OAAO,MAAM,KAAK,OAAO,KAAK,EAAE;CAI9D,MAAM,KAAK,OAAO,MAAM,MAAM;CAE9B,MAAM,UAAoB,CAAC;CAC3B,MAAM,wBAAQ,IAAI,IAAoB;CACtC,IAAI;EACH,KAAK,MAAM,WAAW,WAAW;GAChC,MAAM,YAAY,MAAM,KAAK,OAAO,UACnC,EAAE,MAAMC,gBAAAA,WAAW,MAAM,EAAE,GAC3B,MAAM,SACN,QAAQ,SACR,QAAQ,IACT;GACA,IAAI,cAAc,KAAA,GAAW;IAC5B,OAAO,IACN,OAAO,QAAQ,QAAQ,8CACnB,MAAM,QAAQ,GAAG,QAAQ,QAAQ,aAAa,QAAQ,KAAK,uEAEhE;IACA;GACD;GACA,MAAM,SAAS,MAAM,KAAK,OAAO,OAAO,QAAQ,SAAS;GACzD,QAAQ,KAAK,MAAM;GACnB,MAAM,IAAI,GAAG,QAAQ,QAAQ,GAAG,QAAQ,QAAQ,OAAO,IAAI;GAC3D,OAAO,IAAI,MAAM,QAAQ,QAAQ,eAAe,OAAO,KAAK,YAAY;EACzE;EAEA,MAAM,cAAc,eAAe,KAAK,OAAO,KAAK;EACpD,OAAO,IACN,aAAa,OAAO,KAAK,WAAW,CAAC,CAAC,OAAO,iBAAiB,MAAM,EACrE;EACA,OAAO,MAAM,IAAI,aAAa,aAAa,QAAQ,GAAG;CACvD,UAAU;EACT,MAAM,QAAQ,IAAI,QAAQ,KAAK,WAAW,OAAO,MAAM,CAAC,CAAC;CAC1D;AACD;;;;;;AAOA,SAAS,IACR,aACA,aACA,KACkB;CAClB,MAAM,CAAC,KAAK,GAAG,QAAQ;CACvB,OAAO,IAAI,eAAe,YAAY,KAAK,GAAG,GAAG;CAKjD,MAAM,cAAc,EAHD,QAAQ,IAAI,gBAAgB,GAAA,CAC7C,QAAQ,oBAAoB,EAAE,CAAC,CAC/B,KAC2B,GAAG,kBAAkB,OAAO,KAAK,WAAW,CAAC,CAAC,CAAC,CAC1E,OAAO,OAAO,CAAC,CACf,KAAK,GAAG;CAEV,MAAM,aAAa,CAAC;CACpB,QAAQ,GAAG,UAAU,IAAI;CACzB,QAAQ,GAAG,WAAW,IAAI;CAC1B,OAAO,IAAI,SAAiB,YAAY;EACvC,MAAM,SAAA,GAAQC,mBAAAA,MAAAA,CAAM,KAAK,MAAM;GAC9B,GAAI,MAAM,EAAE,IAAI,IAAI,CAAC;GACrB,OAAO;GACP,KAAK;IAAE,GAAG,QAAQ;IAAK,GAAG;IAAa,cAAc;GAAY;EAClE,CAAC;EACD,MAAM,GAAG,UAAU,MAAM,WAAW;GACnC,QAAQ,QAAQ,OAAO,SAASC,QAAAA,UAAU,QAAQ,UAAU,EAAE;EAC/D,CAAC;EACD,MAAM,GAAG,UAAU,UAAU;GAC5B,OAAO,MAAM,0BAA0B,MAAM,SAAS;GACtD,QAAQ,CAAC;EACV,CAAC;CACF,CAAC,CAAC,CAAC,cAAc;EAChB,QAAQ,IAAI,UAAU,IAAI;EAC1B,QAAQ,IAAI,WAAW,IAAI;CAC5B,CAAC;AACF"}
|
|
1
|
+
{"version":3,"file":"stage-y9qFelxT.cjs","names":["GkmError","dockerCompose","sshAccess","prompt","edgeAlias","loadWorkspaceAppInfo","NotInAnApp","loadWorkspaceConfig","deploysWithCompose","discover","constructGlobs","secretsStoreFor","deployedStackSecrets","composeStack","deployIdentity","edgePorts","composeServer","dockerHost","spawn","constants"],"sources":["../src/exec/stage.ts"],"sourcesContent":["/**\n * `gkm exec --stage <stage> -- <command>`: a command run on this machine as\n * a deployed compose stage's app — a data import, a backfill, a report.\n *\n * The app's environment is the one its container runs with, derived the way\n * the deploy derives it (`composeStack`) from the stage's secrets, read from\n * the stage's store the way the deploy reads them. Only what the container\n * reaches on the stack's network is moved: each service it names that the\n * server publishes on its loopback — Postgres, MinIO — is reached through an\n * SSH tunnel on the deploy's own login (`ServerAccess.tunnel`), and each\n * sibling app through its public address.\n *\n * Nothing is written to disk. The values exist only in the command's\n * environment, and its Credentials are filled from there by a preload that\n * is a `data:` URL naming the keys — never their values.\n */\n\nimport { spawn } from 'node:child_process';\nimport { constants } from 'node:os';\nimport type { ConstructManifest } from '@geekmidas/manifest';\nimport { type ComposeDocker, dockerCompose } from '../compose/docker.js';\nimport { deployedStackSecrets } from '../compose/secrets.js';\nimport {\n\ttype ComposeServer,\n\tcomposeServer,\n\tdockerHost,\n\ttype ServerAccess,\n\tsshAccess,\n\ttype Tunnel,\n} from '../compose/server.js';\nimport { type ComposeStack, composeStack } from '../compose/stack.js';\nimport { edgeAlias } from '../compose/traefik.js';\nimport {\n\tloadWorkspaceAppInfo,\n\tloadWorkspaceConfig,\n\tNotInAnApp,\n} from '../config.js';\nimport { deployIdentity } from '../deploy/identity.js';\nimport { GkmError } from '../errors';\nimport { prompt } from '../prompt.js';\nimport { deploysWithCompose } from '../providers/dns.js';\nimport { discover } from '../reconcile/discover.js';\nimport { constructGlobs } from '../reconcile/workspace.js';\nimport { secretsStoreFor } from '../secrets/store.js';\nimport type { StageSecrets } from '../secrets/types.js';\nimport { edgePorts } from '../target/compose/phases.js';\nimport { assertDeployedStage } from '../workspace/stages.js';\nimport type { NormalizedWorkspace } from '../workspace/types.js';\n\nconst logger = console;\n\n/** The stage is not one the compose target deploys. */\nexport class ExecStageNotComposed extends GkmError {\n\tconstructor(readonly stage: string) {\n\t\tsuper(\n\t\t\t`gkm exec --stage runs a command against a compose stage's server, and ` +\n\t\t\t\t`'${stage}' is not deployed with compose. Set deploy.default: 'compose' ` +\n\t\t\t\t'in gkm.config.ts, or run the command where the stage runs.',\n\t\t);\n\t\tthis.name = 'ExecStageNotComposed';\n\t}\n}\n\n/** The stage's secrets lack what its deploy generates: it was never deployed. */\nexport class ExecStageNotDeployed extends GkmError {\n\tconstructor(\n\t\treadonly stage: string,\n\t\treadonly missing: readonly string[],\n\t) {\n\t\tsuper(\n\t\t\t`'${stage}' has no ${missing.join(', ')} in its secrets yet, so what its ` +\n\t\t\t\t'apps run with is not decided. Deploy it first: ' +\n\t\t\t\t`gkm deploy --stage ${stage}.`,\n\t\t);\n\t\tthis.name = 'ExecStageNotDeployed';\n\t}\n}\n\n/** The directory is not one of the stack's apps. */\nexport class ExecAppNotInStack extends GkmError {\n\tconstructor(\n\t\treadonly stage: string,\n\t\treadonly app: string | undefined,\n\t\treadonly apps: readonly string[],\n\t) {\n\t\tsuper(\n\t\t\t`${app ? `'${app}' is not` : 'This directory is not in'} one of the apps ` +\n\t\t\t\t`'${stage}' runs (${apps.join(', ')}), so there is no environment to run ` +\n\t\t\t\t`as. Run from an app's directory: cd apps/${apps[0] ?? '<app>'} && ` +\n\t\t\t\t`gkm exec --stage ${stage} -- <command>.`,\n\t\t);\n\t\tthis.name = 'ExecAppNotInStack';\n\t}\n}\n\n/** A protected stage, and nobody said yes. */\nexport class ExecNotConfirmed extends GkmError {\n\tconstructor(readonly stage: string) {\n\t\tsuper(`Nothing was run against '${stage}'.`);\n\t\tthis.name = 'ExecNotConfirmed';\n\t}\n}\n\nexport interface StageExecOptions {\n\tstage: string;\n\t/** The AWS profile for the stage's account, where its secrets are kept. */\n\tprofile?: string;\n\t/**\n\t * The SSH login to reach the server as, in place of\n\t * `deploy.compose.server.<stage>.user` — a person's own, where the\n\t * deploy's key lives only in CI.\n\t */\n\tsshUser?: string;\n\t/** Run against a protected stage without asking. */\n\tyes?: boolean;\n\tcwd?: string;\n}\n\n/** What the command needs from outside the process, so tests can stand in. */\nexport interface StageExecDeps {\n\t/** The server's Docker, asked which port each service publishes. */\n\tdocker: ComposeDocker;\n\t/** The tunnels to the server's loopback. */\n\tserver: ServerAccess;\n\t/** Asked before running against a protected stage. */\n\tconfirm: (question: string) => Promise<boolean>;\n\t/** Where the edge's ports are read from, as the deploy reads them. */\n\tenv: NodeJS.ProcessEnv;\n}\n\nexport const defaultStageExecDeps: StageExecDeps = {\n\tdocker: dockerCompose,\n\tserver: sshAccess,\n\tconfirm: async (question) => {\n\t\tconst answer = await prompt(`${question} [y/N] `, {\n\t\t\tinstead: 'Pass --yes to run without asking.',\n\t\t});\n\t\treturn /^y(es)?$/i.test(answer.trim());\n\t},\n\tenv: process.env,\n};\n\n/** A service on the stack's network the app's environment names. */\nexport interface StackAddress {\n\tservice: string;\n\tport: number;\n}\n\n/**\n * The environment an app runs with on the stage: a backend's container's,\n * over the keys of each worker built from it; a site's or a mobile app's,\n * the public values its build reads.\n *\n * @throws {ExecAppNotInStack}\n */\nexport function appEnvironment(\n\tstack: Pick<ComposeStack, 'stage' | 'apps' | 'workers' | 'mobileApps'>,\n\tappName: string | undefined,\n): Record<string, string> {\n\tconst mobile = stack.mobileApps.find(\n\t\t(candidate) => candidate.name === appName,\n\t);\n\tif (mobile) return { ...mobile.values };\n\tconst app = stack.apps.find((candidate) => candidate.name === appName);\n\tif (!app) {\n\t\tthrow new ExecAppNotInStack(stack.stage, appName, [\n\t\t\t...stack.apps.map((candidate) => candidate.name),\n\t\t\t...stack.mobileApps.map((candidate) => candidate.name),\n\t\t]);\n\t}\n\tif (app.kind === 'site') return { ...app.build?.args };\n\tconst workers = stack.workers.filter((worker) => worker.host === app.name);\n\treturn Object.assign(\n\t\t{},\n\t\t...workers.map((worker) => worker.env ?? {}),\n\t\tapp.env ?? {},\n\t);\n}\n\n/** `host:port`, at a URL's authority — after `//` or a login's `@`. */\nfunction authority(host: string, port: number): RegExp {\n\treturn new RegExp(\n\t\t`(?<=[/@,])${host.replace(/[.-]/g, '\\\\$&')}:${port}(?!\\\\d)`,\n\t\t'g',\n\t);\n}\n\n/**\n * Each of the stack's services the environment reaches on the stack's\n * network, once: `postgres:5432`, `minio:9000`.\n */\nexport function stackAddresses(\n\tenv: Readonly<Record<string, string>>,\n\tstack: Pick<ComposeStack, 'infra'>,\n): StackAddress[] {\n\tconst found = new Map<string, StackAddress>();\n\tconst pattern = /(?<=[/@,])([a-z0-9-]+):(\\d+)(?!\\d)/g;\n\tfor (const value of Object.values(env)) {\n\t\tfor (const match of value.matchAll(pattern)) {\n\t\t\tconst service = match[1]!;\n\t\t\tif (!stack.infra.includes(service)) continue;\n\t\t\tconst port = Number(match[2]);\n\t\t\tfound.set(`${service}:${port}`, { service, port });\n\t\t}\n\t}\n\treturn [...found.values()];\n}\n\n/**\n * The environment as this machine reaches it: each service at its tunnel's\n * local port, and each sibling app at its public address. What has no\n * tunnel is left as the container has it.\n */\nexport function throughTunnels(\n\tenv: Readonly<Record<string, string>>,\n\tstack: Pick<ComposeStack, 'apps' | 'project'>,\n\ttunnels: ReadonlyMap<string, number>,\n): Record<string, string> {\n\tconst rewrites: [RegExp, string][] = [];\n\tfor (const [address, port] of tunnels) {\n\t\tconst [service, inside] = address.split(':');\n\t\trewrites.push([authority(service!, Number(inside)), `127.0.0.1:${port}`]);\n\t}\n\tfor (const app of stack.apps) {\n\t\tfor (const host of [app.name, edgeAlias(stack.project, app.name)]) {\n\t\t\trewrites.push([\n\t\t\t\tnew RegExp(\n\t\t\t\t\t`http://${host.replace(/[.-]/g, '\\\\$&')}:${app.port}(?!\\\\d)`,\n\t\t\t\t\t'g',\n\t\t\t\t),\n\t\t\t\tapp.url,\n\t\t\t]);\n\t\t}\n\t}\n\tconst out: Record<string, string> = {};\n\tfor (const [key, value] of Object.entries(env)) {\n\t\tout[key] = rewrites.reduce(\n\t\t\t(current, [pattern, replacement]) =>\n\t\t\t\tcurrent.replace(pattern, () => replacement),\n\t\t\tvalue,\n\t\t);\n\t}\n\treturn out;\n}\n\n/**\n * The preload that fills Credentials from the command's own environment: a\n * `data:` URL holding the keys, so no file — and no value — is written.\n */\nexport function credentialsImport(keys: readonly string[]): string {\n\tconst source =\n\t\t'globalThis.__gkm_credentials__=Object.fromEntries(' +\n\t\t`${JSON.stringify(keys)}.map((k)=>[k,process.env[k]]));`;\n\treturn `--import=data:text/javascript,${encodeURIComponent(source)}`;\n}\n\n/** A deployed compose stage, as its deploy has it, and the app here. */\nexport interface LoadedStage {\n\tworkspace: NormalizedWorkspace;\n\tmanifest: ConstructManifest;\n\tstack: ComposeStack;\n\t/** The stage's secrets, as its deploy reads them. */\n\tsecrets: StageSecrets;\n\t/** The app this directory is, if it is one. */\n\tappName: string | undefined;\n\tprotectedStage: boolean;\n}\n\n/**\n * The stage's stack, and the app this directory is, as the deploy has them:\n * its secrets read from its store, its values derived as the deploy derives\n * them. Nothing is reached but the store — no server, no SSH.\n *\n * @throws {UndeclaredStage}, {ExecStageNotComposed}, {ExecStageNotDeployed}\n */\nexport async function loadStage(\n\toptions: Pick<StageExecOptions, 'stage' | 'profile' | 'cwd'>,\n\tenv: NodeJS.ProcessEnv = process.env,\n): Promise<LoadedStage> {\n\tconst cwd = options.cwd ?? process.cwd();\n\tconst { stage } = options;\n\t// Outside an app the workspace is still loaded, so the stage is checked\n\t// before the directory is refused.\n\tconst info = await loadWorkspaceAppInfo(cwd).catch((error) => {\n\t\tif (error instanceof NotInAnApp) return undefined;\n\t\tthrow error;\n\t});\n\tconst workspace =\n\t\tinfo?.workspace ?? (await loadWorkspaceConfig(cwd)).workspace;\n\n\tassertDeployedStage(workspace.stages, stage);\n\tif (!deploysWithCompose(workspace)) throw new ExecStageNotComposed(stage);\n\n\tconst runnables: Record<string, string[]> = {};\n\tconst background: Record<string, string[]> = {};\n\tconst manifest: ConstructManifest = await discover({\n\t\tpatterns: constructGlobs(workspace),\n\t\tcwd: workspace.root,\n\t\trunnables,\n\t\tbackground,\n\t});\n\n\tconst store = await secretsStoreFor(workspace, stage, {\n\t\t...(options.profile ? { profile: options.profile } : {}),\n\t});\n\tconst { secrets, generated } = deployedStackSecrets(\n\t\tworkspace,\n\t\tstage,\n\t\tawait store.read(stage),\n\t\tmanifest,\n\t);\n\tif (generated.length > 0) throw new ExecStageNotDeployed(stage, generated);\n\n\tconst stack = composeStack({\n\t\tworkspace,\n\t\tmanifest,\n\t\trunnables,\n\t\tbackground,\n\t\tstage,\n\t\tidentity: deployIdentity(workspace, stage),\n\t\t// Built, so a site's public values are resolved as its build has them.\n\t\timages: { mode: 'build', tag: 'exec' },\n\t\tsecrets,\n\t\tports: edgePorts(env),\n\t});\n\treturn {\n\t\tworkspace,\n\t\tmanifest,\n\t\tstack,\n\t\tsecrets,\n\t\tappName: info?.appName,\n\t\tprotectedStage: workspace.stages.protected?.includes(stage) ?? false,\n\t};\n}\n\n/** The stage's server, as SSH reaches it: `--ssh-user` over the config's. */\nfunction stageServer(\n\tloaded: LoadedStage,\n\toptions: StageExecOptions,\n): ComposeServer {\n\tconst server = composeServer({\n\t\tstage: options.stage,\n\t\tlocal: false,\n\t\tconfig: loaded.workspace.deploy?.compose?.server,\n\t\tcustom: loaded.secrets.custom,\n\t})!;\n\tif (options.sshUser) server.user = options.sshUser;\n\treturn server;\n}\n\n/**\n * Run `commandArgs` as the app this directory is, on the stage — through a\n * tunnel to each service it reaches — and return its exit code. Every tunnel\n * is closed once it exits, whatever it exits with.\n *\n * @throws {ExecStageNotComposed}, {ExecStageNotDeployed}, {ExecAppNotInStack},\n * {ExecNotConfirmed}, {ComposeServerUnreachable}\n */\nexport async function execOnStage(\n\tcommandArgs: readonly [string, ...string[]],\n\toptions: StageExecOptions,\n\tdeps: StageExecDeps = defaultStageExecDeps,\n): Promise<number> {\n\tconst { stage } = options;\n\tconst loaded = await loadStage(options, deps.env);\n\tconst { stack, appName, protectedStage } = loaded;\n\tconst env = appEnvironment(stack, appName);\n\n\tif (protectedStage && !options.yes) {\n\t\tconst question = `'${stage}' is a protected stage. Run ${commandArgs.join(' ')} against it?`;\n\t\tif (!(await deps.confirm(question))) throw new ExecNotConfirmed(stage);\n\t}\n\n\t// Only what reaches a service on the stack needs the server: a site's or\n\t// a mobile app's values are public addresses, and need no SSH at all.\n\tconst addresses = stackAddresses(env, stack);\n\tif (addresses.length === 0) {\n\t\tlogger.log(`📦 App: ${appName} on '${stage}'`);\n\t\tlogger.log(`🔐 Loaded ${Object.keys(env).length} value(s) for '${stage}'`);\n\t\treturn run(commandArgs, env, options.cwd);\n\t}\n\n\tconst server = stageServer(loaded, options);\n\tlogger.log(`📦 App: ${appName} on '${stage}' (${server.host})`);\n\n\t// The deploy's own check: SSH logs in and Docker answers, before any\n\t// tunnel is asked for — so a key the server refuses is said as that.\n\tawait deps.server.check(server);\n\n\tconst tunnels: Tunnel[] = [];\n\tconst ports = new Map<string, number>();\n\ttry {\n\t\tfor (const address of addresses) {\n\t\t\tconst published = await deps.docker.published(\n\t\t\t\t{ host: dockerHost(server) },\n\t\t\t\tstack.project,\n\t\t\t\taddress.service,\n\t\t\t\taddress.port,\n\t\t\t);\n\t\t\tif (published === undefined) {\n\t\t\t\tlogger.log(\n\t\t\t\t\t`⚠️ ${address.service} is not published on the server (no running ` +\n\t\t\t\t\t\t`${stack.project} ${address.service} publishes ${address.port}), so ` +\n\t\t\t\t\t\t'it is unreachable from here; its address is passed on unchanged.',\n\t\t\t\t);\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\tconst tunnel = await deps.server.tunnel(server, published);\n\t\t\ttunnels.push(tunnel);\n\t\t\tports.set(`${address.service}:${address.port}`, tunnel.port);\n\t\t\tlogger.log(`🚇 ${address.service} → 127.0.0.1:${tunnel.port} (over SSH)`);\n\t\t}\n\n\t\tconst credentials = throughTunnels(env, stack, ports);\n\t\tlogger.log(\n\t\t\t`🔐 Loaded ${Object.keys(credentials).length} value(s) for '${stage}'`,\n\t\t);\n\t\treturn await run(commandArgs, credentials, options.cwd);\n\t} finally {\n\t\tawait Promise.all(tunnels.map((tunnel) => tunnel.close()));\n\t}\n}\n\n/**\n * The command, with the values in its environment and its Credentials. A\n * Ctrl-C reaches it — it is in the terminal's process group — and this\n * process waits for it to exit rather than leaving before the tunnels close.\n */\nfunction run(\n\tcommandArgs: readonly [string, ...string[]],\n\tcredentials: Record<string, string>,\n\tcwd: string | undefined,\n): Promise<number> {\n\tconst [cmd, ...args] = commandArgs;\n\tlogger.log(`🚀 Running: ${commandArgs.join(' ')}`);\n\n\tconst inherited = (process.env.NODE_OPTIONS ?? '')\n\t\t.replace(/--import[= ]\\S+/g, '')\n\t\t.trim();\n\tconst nodeOptions = [inherited, credentialsImport(Object.keys(credentials))]\n\t\t.filter(Boolean)\n\t\t.join(' ');\n\n\tconst wait = () => {};\n\tprocess.on('SIGINT', wait);\n\tprocess.on('SIGTERM', wait);\n\treturn new Promise<number>((resolve) => {\n\t\tconst child = spawn(cmd, args, {\n\t\t\t...(cwd ? { cwd } : {}),\n\t\t\tstdio: 'inherit',\n\t\t\tenv: { ...process.env, ...credentials, NODE_OPTIONS: nodeOptions },\n\t\t});\n\t\tchild.on('close', (code, signal) => {\n\t\t\tresolve(code ?? 128 + (signal ? constants.signals[signal] : 0));\n\t\t});\n\t\tchild.on('error', (error) => {\n\t\t\tlogger.error(`Failed to run command: ${error.message}`);\n\t\t\tresolve(1);\n\t\t});\n\t}).finally(() => {\n\t\tprocess.off('SIGINT', wait);\n\t\tprocess.off('SIGTERM', wait);\n\t});\n}\n"],"mappings":"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"}
|
package/dist/target/public.cjs
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
2
2
|
const require_define = require("../define-crmMcXFS.cjs");
|
|
3
|
-
const require_resolve = require("../resolve-
|
|
3
|
+
const require_resolve = require("../resolve-isFTuJp4.cjs");
|
|
4
4
|
const require_provider = require("../provider-BqpjG9QM.cjs");
|
|
5
5
|
exports.DeployTargetNotYetSupported = require_define.DeployTargetNotYetSupported;
|
|
6
6
|
exports.InvalidTargetOptions = require_resolve.InvalidTargetOptions;
|
package/dist/target/public.mjs
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
1
|
import { a as UnknownDeployTarget, i as DeployTargetNotYetSupported, n as isDeployTarget, t as defineTarget } from "../define-6poNqm5O.mjs";
|
|
2
|
-
import { i as TargetRuntimeMismatch, l as TargetPackageNotFound, n as TargetEntryInvalid, r as TargetPackageInvalid, s as TargetRuntimeUndeclared, t as InvalidTargetOptions } from "../resolve-
|
|
2
|
+
import { i as TargetRuntimeMismatch, l as TargetPackageNotFound, n as TargetEntryInvalid, r as TargetPackageInvalid, s as TargetRuntimeUndeclared, t as InvalidTargetOptions } from "../resolve-CC6ndodR.mjs";
|
|
3
3
|
import { t as ProviderRemoved } from "../provider-CEOKk8NS.mjs";
|
|
4
4
|
export { DeployTargetNotYetSupported, InvalidTargetOptions, ProviderRemoved, TargetEntryInvalid, TargetPackageInvalid, TargetPackageNotFound, TargetRuntimeMismatch, TargetRuntimeUndeclared, UnknownDeployTarget, defineTarget, isDeployTarget };
|
package/dist/vitest.cjs
CHANGED
package/dist/vitest.mjs
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { i as TEST_MANIFEST_ENV, n as TEST_READY_FILE } from "./ready-
|
|
1
|
+
import { i as TEST_MANIFEST_ENV, n as TEST_READY_FILE } from "./ready-4hEoQ_cr.mjs";
|
|
2
2
|
import { dirname, join } from "node:path";
|
|
3
3
|
import { readFileSync } from "node:fs";
|
|
4
4
|
import { execFileSync } from "node:child_process";
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@geekmidas/cli",
|
|
3
|
-
"version": "10.0.0-alpha.
|
|
3
|
+
"version": "10.0.0-alpha.118",
|
|
4
4
|
"description": "CLI tools for building Lambda handlers, server applications, and generating OpenAPI specs",
|
|
5
5
|
"private": false,
|
|
6
6
|
"type": "module",
|
|
@@ -120,16 +120,16 @@
|
|
|
120
120
|
"prompts": "~2.4.2",
|
|
121
121
|
"tsx": "~4.23.15",
|
|
122
122
|
"yaml": "~2.9.1",
|
|
123
|
-
"@geekmidas/cache": "10.0.0-alpha.
|
|
124
|
-
"@geekmidas/constructs": "~10.0.0-alpha.
|
|
125
|
-
"@geekmidas/db": "10.0.0-alpha.
|
|
126
|
-
"@geekmidas/envkit": "~10.0.0-alpha.
|
|
127
|
-
"@geekmidas/errors": "~10.0.0-alpha.
|
|
128
|
-
"@geekmidas/logger": "~10.0.0-alpha.
|
|
129
|
-
"@geekmidas/manifest": "10.0.0-alpha.
|
|
130
|
-
"@geekmidas/schema": "~10.0.0-alpha.
|
|
131
|
-
"@geekmidas/services": "~10.0.0-alpha.
|
|
132
|
-
"@geekmidas/storage": "~10.0.0-alpha.
|
|
123
|
+
"@geekmidas/cache": "10.0.0-alpha.118",
|
|
124
|
+
"@geekmidas/constructs": "~10.0.0-alpha.118",
|
|
125
|
+
"@geekmidas/db": "10.0.0-alpha.118",
|
|
126
|
+
"@geekmidas/envkit": "~10.0.0-alpha.118",
|
|
127
|
+
"@geekmidas/errors": "~10.0.0-alpha.118",
|
|
128
|
+
"@geekmidas/logger": "~10.0.0-alpha.118",
|
|
129
|
+
"@geekmidas/manifest": "10.0.0-alpha.118",
|
|
130
|
+
"@geekmidas/schema": "~10.0.0-alpha.118",
|
|
131
|
+
"@geekmidas/services": "~10.0.0-alpha.118",
|
|
132
|
+
"@geekmidas/storage": "~10.0.0-alpha.118"
|
|
133
133
|
},
|
|
134
134
|
"devDependencies": {
|
|
135
135
|
"@hono/node-server": "~1.19.5",
|
|
@@ -144,12 +144,12 @@
|
|
|
144
144
|
"typescript": "~7.0.2",
|
|
145
145
|
"vitest": "~5.0.2",
|
|
146
146
|
"zod": "~4.6.5",
|
|
147
|
-
"@geekmidas/emailkit": "~10.0.0-alpha.
|
|
148
|
-
"@geekmidas/events": "~10.0.0-alpha.
|
|
149
|
-
"@geekmidas/testkit": "10.0.0-alpha.
|
|
147
|
+
"@geekmidas/emailkit": "~10.0.0-alpha.118",
|
|
148
|
+
"@geekmidas/events": "~10.0.0-alpha.118",
|
|
149
|
+
"@geekmidas/testkit": "10.0.0-alpha.118"
|
|
150
150
|
},
|
|
151
151
|
"peerDependencies": {
|
|
152
|
-
"@geekmidas/telescope": "~10.0.0-alpha.
|
|
152
|
+
"@geekmidas/telescope": "~10.0.0-alpha.118"
|
|
153
153
|
},
|
|
154
154
|
"peerDependenciesMeta": {
|
|
155
155
|
"@geekmidas/telescope": {
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"bundler-B2_2-Dum.cjs","names":["require","createRequire","join","dirname","existsSync","realpathSync","readFileSync","mkdir","spawnSync","writeFile"],"sources":["../src/build/bundler.ts"],"sourcesContent":["import { spawnSync } from 'node:child_process';\nimport { existsSync, readFileSync, realpathSync } from 'node:fs';\nimport { mkdir, writeFile } from 'node:fs/promises';\nimport { createRequire } from 'node:module';\nimport { dirname, join } from 'node:path';\nimport type { Construct } from '@geekmidas/constructs';\n\n/**\n * Banner to inject into ESM bundle for CJS compatibility.\n * Creates a `require` function using Node's createRequire for packages\n * that internally use CommonJS require() for Node builtins.\n */\nconst ESM_CJS_COMPAT_BANNER =\n\t'import { createRequire } from \"module\"; const require = createRequire(import.meta.url);';\n\nexport interface BundleOptions {\n\t/** Entry point file (e.g., .gkm/server/server.ts) */\n\tentryPoint: string;\n\t/** Output directory for bundled files */\n\toutputDir: string;\n\t/** The bundle's file name in `outputDir`. Defaults to `server.mjs`. */\n\toutfile?: string;\n\t/** Minify the output (default: true) */\n\tminify: boolean;\n\t/** Generate sourcemaps (default: false) */\n\tsourcemap: boolean;\n\t/** Packages to exclude from bundling */\n\texternal: string[];\n\t/** Stage for secrets injection (optional) */\n\tstage?: string;\n\t/** Constructs to validate environment variables for */\n\tconstructs?: Construct[];\n}\n\nexport interface BundleResult {\n\t/** Path to the bundled output */\n\toutputPath: string;\n\t/** Ephemeral master key for deployment (only if stage was provided) */\n\tmasterKey?: string;\n}\n\n/**\n * The esbuild binary, resolved from this package rather than from PATH.\n *\n * It used to be `npx esbuild`, which is two assumptions: that esbuild is\n * installed somewhere npx can find, and that the *app's* directory is where to\n * look. Neither holds — the CLI did not depend on esbuild at all, so a deploy\n * run from an app that had not installed it failed at the bundle step with\n * `sh: esbuild: command not found`, after provisioning had already happened.\n *\n * The same fix `bin/gkm.mjs` makes for tsx: resolve from this package's own\n * `node_modules`, so the tool the CLI shells out to is the one it depends on.\n */\nfunction esbuildBinary(): string {\n\tconst require = createRequire(import.meta.url);\n\n\ttry {\n\t\t// The package root, then its bin — `require.resolve('esbuild')` gives the\n\t\t// JS entry point, which is a library rather than the CLI.\n\t\tconst pkg = require.resolve('esbuild/package.json');\n\t\treturn join(dirname(pkg), 'bin', 'esbuild');\n\t} catch {\n\t\t// Better a PATH lookup than a hard failure: a project that installed\n\t\t// esbuild itself still works.\n\t\treturn 'esbuild';\n\t}\n}\n\n/**\n * Collect the environment variables a build cannot proceed without.\n *\n * Uses the SnifferEnvironmentParser to detect which env vars each service\n * reads, and keeps only the ones it cannot do without. `markOptional` is what\n * separates them: the sniffer already knows which reads went through\n * `.optional()` or `.default()`, and asking for that distinction is the\n * difference between a build that stops and one that proceeds correctly.\n *\n * Without it, a key that is *absent by design* failed the build. A surface\n * publishes `AUTH_COOKIE_DOMAIN` only when there is a domain to widen a cookie\n * to — one host has nothing to share it with, so the derivation correctly\n * yields nothing, and Better Auth reads it as optional for exactly that\n * reason. Treating every sniffed read as required made the honest answer\n * indistinguishable from a missing secret.\n *\n * @param constructs - Array of constructs to analyze\n * @returns Deduplicated, sorted names of the required variables\n */\nasync function collectRequiredEnvVars(\n\tconstructs: Construct[],\n): Promise<string[]> {\n\t// A key read optionally by one construct and required by another stays\n\t// required: the strictest read is the one that fails at runtime, and taking\n\t// the union of the required reads is what says so.\n\tconst required = new Set<string>();\n\n\tfor (const construct of constructs) {\n\t\tfor (const name of await construct.getEnvironment({ markOptional: true })) {\n\t\t\tif (!name.endsWith('?')) required.add(name);\n\t\t}\n\t}\n\n\treturn Array.from(required).sort();\n}\n\n/** `<dir>/node_modules/<name>`, from `from` up to the root; the first found. */\nfunction installed(name: string, from: string): string | undefined {\n\tfor (let dir = from; ; dir = dirname(dir)) {\n\t\tconst candidate = join(dir, 'node_modules', name, 'package.json');\n\t\tif (existsSync(candidate)) return realpathSync(dirname(candidate));\n\t\tif (dirname(dir) === dir) return undefined;\n\t}\n}\n\n/**\n * The optional peers of the @geekmidas packages an app reaches that are not\n * installed where those packages would resolve them.\n *\n * Each is imported only on the path that needs it — `@aws-sdk/client-kms`\n * for a `kms:` key, say — so an app that never takes that path never\n * installs it. esbuild resolves a dynamic import all the same, and fails\n * the bundle on one it cannot find; left external instead, it is only\n * looked for if that path runs, where its absence says what to install.\n */\nexport function missingOptionalPeers(from: string): string[] {\n\tconst missing = new Set<string>();\n\tconst seen = new Set<string>();\n\tconst visit = (dir: string) => {\n\t\tif (seen.has(dir)) return;\n\t\tseen.add(dir);\n\t\tconst pkg = JSON.parse(readFileSync(join(dir, 'package.json'), 'utf-8'));\n\t\tfor (const [peer, meta] of Object.entries(\n\t\t\t(pkg.peerDependenciesMeta ?? {}) as Record<\n\t\t\t\tstring,\n\t\t\t\t{ optional?: boolean }\n\t\t\t>,\n\t\t)) {\n\t\t\tif (meta?.optional && !installed(peer, dir)) missing.add(peer);\n\t\t}\n\t\tfor (const name of Object.keys({\n\t\t\t...pkg.dependencies,\n\t\t\t...pkg.peerDependencies,\n\t\t})) {\n\t\t\tif (!name.startsWith('@geekmidas/')) continue;\n\t\t\tconst found = installed(name, dir);\n\t\t\tif (found) visit(found);\n\t\t}\n\t};\n\n\t// The app's own package.json: the nearest above the entry.\n\tfor (let dir = from; ; dir = dirname(dir)) {\n\t\tif (existsSync(join(dir, 'package.json'))) {\n\t\t\tconst pkg = JSON.parse(readFileSync(join(dir, 'package.json'), 'utf-8'));\n\t\t\tfor (const name of Object.keys({\n\t\t\t\t...pkg.dependencies,\n\t\t\t\t...pkg.devDependencies,\n\t\t\t})) {\n\t\t\t\tif (!name.startsWith('@geekmidas/')) continue;\n\t\t\t\tconst found = installed(name, dir);\n\t\t\t\tif (found) visit(found);\n\t\t\t}\n\t\t\tbreak;\n\t\t}\n\t\tif (dirname(dir) === dir) break;\n\t}\n\treturn [...missing].sort();\n}\n\n/**\n * Bundle the server application using esbuild.\n * Creates a fully standalone bundle with all dependencies included.\n *\n * @param options - Bundle configuration options\n * @returns Bundle result with output path and optional master key\n */\n\nexport async function bundleServer(\n\toptions: BundleOptions,\n): Promise<BundleResult> {\n\tconst {\n\t\tentryPoint,\n\t\toutputDir,\n\t\tminify,\n\t\tsourcemap,\n\t\texternal,\n\t\tstage,\n\t\tconstructs,\n\t} = options;\n\n\t// Ensure output directory exists\n\tawait mkdir(outputDir, { recursive: true });\n\n\tconst mjsOutput = join(outputDir, options.outfile ?? 'server.mjs');\n\n\t// Build command-line arguments for esbuild\n\tconst args = [\n\t\tesbuildBinary(),\n\t\tentryPoint,\n\t\t'--bundle',\n\t\t'--platform=node',\n\t\t'--target=node22',\n\t\t'--format=esm',\n\t\t`--outfile=${mjsOutput}`,\n\t\t'--packages=bundle', // Bundle all dependencies for standalone output\n\t\t`--banner:js=${ESM_CJS_COMPAT_BANNER}`, // CJS compatibility for packages like pino\n\t];\n\n\tif (minify) {\n\t\t// Names kept: an error's class name is what a log line's `err.type` and\n\t\t// a response's `name` say, and minified they read `_6`.\n\t\targs.push('--minify', '--keep-names');\n\t}\n\n\tif (sourcemap) {\n\t\targs.push('--sourcemap');\n\t}\n\n\t// Add external packages (user-specified), and the optional peers a\n\t// @geekmidas package only loads on a path this app may never take\n\tfor (const ext of [\n\t\t...external,\n\t\t...missingOptionalPeers(dirname(entryPoint)),\n\t]) {\n\t\targs.push(`--external:${ext}`);\n\t}\n\n\t// Handle secrets injection if stage is provided\n\tlet masterKey: string | undefined;\n\n\tif (stage) {\n\t\tconst {\n\t\t\ttoEmbeddableSecrets,\n\t\t\tvalidateEnvironmentVariables,\n\t\t\tinitStageSecrets,\n\t\t} = await import('../secrets/storage');\n\t\tconst { FileSecretsStore, secretsStoreFor } = await import(\n\t\t\t'../secrets/store'\n\t\t);\n\t\tconst { ConfigNotFound, loadWorkspaceSettings } = await import('../config');\n\t\tconst { encryptSecrets, generateDefineOptions } = await import(\n\t\t\t'../secrets/encryption'\n\t\t);\n\n\t\t// The stage's own store — SSM or Secrets Manager for a deployed stage\n\t\t// kept there. Outside\n\t\t// any workspace nothing names one, so it is the file.\n\t\tconst store = await loadWorkspaceSettings().then(\n\t\t\t(workspace) => secretsStoreFor(workspace, stage),\n\t\t\t(error) => {\n\t\t\t\tif (error instanceof ConfigNotFound) {\n\t\t\t\t\treturn new FileSecretsStore(process.cwd());\n\t\t\t\t}\n\t\t\t\tthrow error;\n\t\t\t},\n\t\t);\n\t\tlet secrets = await store.read(stage);\n\n\t\tif (!secrets) {\n\t\t\t// Auto-initialize secrets for the stage\n\t\t\tconsole.log(` Initializing secrets for stage \"${stage}\"...`);\n\t\t\tsecrets = initStageSecrets(stage);\n\t\t\tawait store.write(stage, secrets);\n\t\t\tconsole.log(` ✓ Created the \"${stage}\" secrets (${store.name})`);\n\t\t}\n\n\t\t// Validate environment variables if constructs are provided\n\t\tif (constructs && constructs.length > 0) {\n\t\t\tconsole.log(' Analyzing environment variable requirements...');\n\t\t\tconst requiredVars = await collectRequiredEnvVars(constructs);\n\n\t\t\tif (requiredVars.length > 0) {\n\t\t\t\tconst validation = validateEnvironmentVariables(requiredVars, secrets);\n\n\t\t\t\tif (!validation.valid) {\n\t\t\t\t\tconst errorMessage = [\n\t\t\t\t\t\t`Missing environment variables for stage \"${stage}\":`,\n\t\t\t\t\t\t'',\n\t\t\t\t\t\t...validation.missing.map((v) => ` ❌ ${v}`),\n\t\t\t\t\t\t'',\n\t\t\t\t\t\t'To fix this, either:',\n\t\t\t\t\t\t` 1. Add the missing variables to the \"${stage}\" secrets using:`,\n\t\t\t\t\t\t` gkm secrets:set <KEY> <VALUE> --stage ${stage}`,\n\t\t\t\t\t\t'',\n\t\t\t\t\t\t` 2. Or import from a JSON file:`,\n\t\t\t\t\t\t` gkm secrets:import secrets.json --stage ${stage}`,\n\t\t\t\t\t\t'',\n\t\t\t\t\t\t'Required variables:',\n\t\t\t\t\t\t...validation.required.map((v) =>\n\t\t\t\t\t\t\tvalidation.missing.includes(v) ? ` ❌ ${v}` : ` ✓ ${v}`,\n\t\t\t\t\t\t),\n\t\t\t\t\t].join('\\n');\n\n\t\t\t\t\tthrow new Error(errorMessage);\n\t\t\t\t}\n\n\t\t\t\tconsole.log(\n\t\t\t\t\t` ✓ All ${requiredVars.length} required environment variables found`,\n\t\t\t\t);\n\t\t\t}\n\t\t}\n\n\t\t// Convert to embeddable format and encrypt\n\t\tconst embeddable = toEmbeddableSecrets(secrets);\n\t\tconst encrypted = encryptSecrets(embeddable);\n\t\tmasterKey = encrypted.masterKey;\n\n\t\t// Add define options for build-time injection using esbuild's --define:KEY=VALUE format\n\t\tconst defines = generateDefineOptions(encrypted);\n\t\tfor (const [key, value] of Object.entries(defines)) {\n\t\t\t// `value` is already a JS string literal; quoting it again would\n\t\t\t// embed the quotes, and the IV would not parse as hex.\n\t\t\targs.push(`--define:${key}=${value}`);\n\t\t}\n\n\t\tconsole.log(` Secrets encrypted for stage \"${stage}\"`);\n\t} else {\n\t\t// Credentials encrypted elsewhere — an image build hands them in as a\n\t\t// BuildKit secret and writes them beside the app's `.gkm/server` — are\n\t\t// embedded as they are; whoever encrypted them holds the key.\n\t\tconst gkmDir = join(outputDir, '..', '..');\n\t\tconst encryptedPath = join(gkmDir, 'credentials.enc');\n\t\tconst ivPath = join(gkmDir, 'credentials.iv');\n\t\tif (existsSync(encryptedPath) && existsSync(ivPath)) {\n\t\t\tconst { generateDefineOptions } = await import('../secrets/encryption');\n\t\t\tconst defines = generateDefineOptions({\n\t\t\t\tencrypted: readFileSync(encryptedPath, 'utf-8').trim(),\n\t\t\t\tiv: readFileSync(ivPath, 'utf-8').trim(),\n\t\t\t\tmasterKey: '',\n\t\t\t});\n\t\t\tfor (const [key, value] of Object.entries(defines)) {\n\t\t\t\targs.push(`--define:${key}=${value}`);\n\t\t\t}\n\t\t\tconsole.log(' Embedded the encrypted credentials the build was given');\n\t\t}\n\t}\n\n\ttry {\n\t\t// Run esbuild with command-line arguments\n\t\tconst [cmd, ...cmdArgs] = args as [string, ...string[]];\n\t\tconst result = spawnSync(cmd, cmdArgs, {\n\t\t\tcwd: process.cwd(),\n\t\t\tstdio: 'inherit',\n\t\t\tshell: process.platform === 'win32', // Only use shell on Windows for npx resolution\n\t\t});\n\n\t\tif (result.error) {\n\t\t\tthrow result.error;\n\t\t}\n\t\tif (result.status !== 0) {\n\t\t\tthrow new Error(`esbuild exited with code ${result.status}`);\n\t\t}\n\n\t\t// Add shebang to the bundled file\n\t\tconst { readFile } = await import('node:fs/promises');\n\t\tconst content = await readFile(mjsOutput, 'utf-8');\n\t\tif (!content.startsWith('#!')) {\n\t\t\tawait writeFile(mjsOutput, `#!/usr/bin/env node\\n${content}`);\n\t\t}\n\t} catch (error) {\n\t\tthrow new Error(\n\t\t\t`Failed to bundle server: ${error instanceof Error ? error.message : 'Unknown error'}`,\n\t\t);\n\t}\n\n\treturn {\n\t\toutputPath: mjsOutput,\n\t\tmasterKey,\n\t};\n}\n"],"mappings":"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"}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"bundler-CgcwaVI0.mjs","names":[],"sources":["../src/build/bundler.ts"],"sourcesContent":["import { spawnSync } from 'node:child_process';\nimport { existsSync, readFileSync, realpathSync } from 'node:fs';\nimport { mkdir, writeFile } from 'node:fs/promises';\nimport { createRequire } from 'node:module';\nimport { dirname, join } from 'node:path';\nimport type { Construct } from '@geekmidas/constructs';\n\n/**\n * Banner to inject into ESM bundle for CJS compatibility.\n * Creates a `require` function using Node's createRequire for packages\n * that internally use CommonJS require() for Node builtins.\n */\nconst ESM_CJS_COMPAT_BANNER =\n\t'import { createRequire } from \"module\"; const require = createRequire(import.meta.url);';\n\nexport interface BundleOptions {\n\t/** Entry point file (e.g., .gkm/server/server.ts) */\n\tentryPoint: string;\n\t/** Output directory for bundled files */\n\toutputDir: string;\n\t/** The bundle's file name in `outputDir`. Defaults to `server.mjs`. */\n\toutfile?: string;\n\t/** Minify the output (default: true) */\n\tminify: boolean;\n\t/** Generate sourcemaps (default: false) */\n\tsourcemap: boolean;\n\t/** Packages to exclude from bundling */\n\texternal: string[];\n\t/** Stage for secrets injection (optional) */\n\tstage?: string;\n\t/** Constructs to validate environment variables for */\n\tconstructs?: Construct[];\n}\n\nexport interface BundleResult {\n\t/** Path to the bundled output */\n\toutputPath: string;\n\t/** Ephemeral master key for deployment (only if stage was provided) */\n\tmasterKey?: string;\n}\n\n/**\n * The esbuild binary, resolved from this package rather than from PATH.\n *\n * It used to be `npx esbuild`, which is two assumptions: that esbuild is\n * installed somewhere npx can find, and that the *app's* directory is where to\n * look. Neither holds — the CLI did not depend on esbuild at all, so a deploy\n * run from an app that had not installed it failed at the bundle step with\n * `sh: esbuild: command not found`, after provisioning had already happened.\n *\n * The same fix `bin/gkm.mjs` makes for tsx: resolve from this package's own\n * `node_modules`, so the tool the CLI shells out to is the one it depends on.\n */\nfunction esbuildBinary(): string {\n\tconst require = createRequire(import.meta.url);\n\n\ttry {\n\t\t// The package root, then its bin — `require.resolve('esbuild')` gives the\n\t\t// JS entry point, which is a library rather than the CLI.\n\t\tconst pkg = require.resolve('esbuild/package.json');\n\t\treturn join(dirname(pkg), 'bin', 'esbuild');\n\t} catch {\n\t\t// Better a PATH lookup than a hard failure: a project that installed\n\t\t// esbuild itself still works.\n\t\treturn 'esbuild';\n\t}\n}\n\n/**\n * Collect the environment variables a build cannot proceed without.\n *\n * Uses the SnifferEnvironmentParser to detect which env vars each service\n * reads, and keeps only the ones it cannot do without. `markOptional` is what\n * separates them: the sniffer already knows which reads went through\n * `.optional()` or `.default()`, and asking for that distinction is the\n * difference between a build that stops and one that proceeds correctly.\n *\n * Without it, a key that is *absent by design* failed the build. A surface\n * publishes `AUTH_COOKIE_DOMAIN` only when there is a domain to widen a cookie\n * to — one host has nothing to share it with, so the derivation correctly\n * yields nothing, and Better Auth reads it as optional for exactly that\n * reason. Treating every sniffed read as required made the honest answer\n * indistinguishable from a missing secret.\n *\n * @param constructs - Array of constructs to analyze\n * @returns Deduplicated, sorted names of the required variables\n */\nasync function collectRequiredEnvVars(\n\tconstructs: Construct[],\n): Promise<string[]> {\n\t// A key read optionally by one construct and required by another stays\n\t// required: the strictest read is the one that fails at runtime, and taking\n\t// the union of the required reads is what says so.\n\tconst required = new Set<string>();\n\n\tfor (const construct of constructs) {\n\t\tfor (const name of await construct.getEnvironment({ markOptional: true })) {\n\t\t\tif (!name.endsWith('?')) required.add(name);\n\t\t}\n\t}\n\n\treturn Array.from(required).sort();\n}\n\n/** `<dir>/node_modules/<name>`, from `from` up to the root; the first found. */\nfunction installed(name: string, from: string): string | undefined {\n\tfor (let dir = from; ; dir = dirname(dir)) {\n\t\tconst candidate = join(dir, 'node_modules', name, 'package.json');\n\t\tif (existsSync(candidate)) return realpathSync(dirname(candidate));\n\t\tif (dirname(dir) === dir) return undefined;\n\t}\n}\n\n/**\n * The optional peers of the @geekmidas packages an app reaches that are not\n * installed where those packages would resolve them.\n *\n * Each is imported only on the path that needs it — `@aws-sdk/client-kms`\n * for a `kms:` key, say — so an app that never takes that path never\n * installs it. esbuild resolves a dynamic import all the same, and fails\n * the bundle on one it cannot find; left external instead, it is only\n * looked for if that path runs, where its absence says what to install.\n */\nexport function missingOptionalPeers(from: string): string[] {\n\tconst missing = new Set<string>();\n\tconst seen = new Set<string>();\n\tconst visit = (dir: string) => {\n\t\tif (seen.has(dir)) return;\n\t\tseen.add(dir);\n\t\tconst pkg = JSON.parse(readFileSync(join(dir, 'package.json'), 'utf-8'));\n\t\tfor (const [peer, meta] of Object.entries(\n\t\t\t(pkg.peerDependenciesMeta ?? {}) as Record<\n\t\t\t\tstring,\n\t\t\t\t{ optional?: boolean }\n\t\t\t>,\n\t\t)) {\n\t\t\tif (meta?.optional && !installed(peer, dir)) missing.add(peer);\n\t\t}\n\t\tfor (const name of Object.keys({\n\t\t\t...pkg.dependencies,\n\t\t\t...pkg.peerDependencies,\n\t\t})) {\n\t\t\tif (!name.startsWith('@geekmidas/')) continue;\n\t\t\tconst found = installed(name, dir);\n\t\t\tif (found) visit(found);\n\t\t}\n\t};\n\n\t// The app's own package.json: the nearest above the entry.\n\tfor (let dir = from; ; dir = dirname(dir)) {\n\t\tif (existsSync(join(dir, 'package.json'))) {\n\t\t\tconst pkg = JSON.parse(readFileSync(join(dir, 'package.json'), 'utf-8'));\n\t\t\tfor (const name of Object.keys({\n\t\t\t\t...pkg.dependencies,\n\t\t\t\t...pkg.devDependencies,\n\t\t\t})) {\n\t\t\t\tif (!name.startsWith('@geekmidas/')) continue;\n\t\t\t\tconst found = installed(name, dir);\n\t\t\t\tif (found) visit(found);\n\t\t\t}\n\t\t\tbreak;\n\t\t}\n\t\tif (dirname(dir) === dir) break;\n\t}\n\treturn [...missing].sort();\n}\n\n/**\n * Bundle the server application using esbuild.\n * Creates a fully standalone bundle with all dependencies included.\n *\n * @param options - Bundle configuration options\n * @returns Bundle result with output path and optional master key\n */\n\nexport async function bundleServer(\n\toptions: BundleOptions,\n): Promise<BundleResult> {\n\tconst {\n\t\tentryPoint,\n\t\toutputDir,\n\t\tminify,\n\t\tsourcemap,\n\t\texternal,\n\t\tstage,\n\t\tconstructs,\n\t} = options;\n\n\t// Ensure output directory exists\n\tawait mkdir(outputDir, { recursive: true });\n\n\tconst mjsOutput = join(outputDir, options.outfile ?? 'server.mjs');\n\n\t// Build command-line arguments for esbuild\n\tconst args = [\n\t\tesbuildBinary(),\n\t\tentryPoint,\n\t\t'--bundle',\n\t\t'--platform=node',\n\t\t'--target=node22',\n\t\t'--format=esm',\n\t\t`--outfile=${mjsOutput}`,\n\t\t'--packages=bundle', // Bundle all dependencies for standalone output\n\t\t`--banner:js=${ESM_CJS_COMPAT_BANNER}`, // CJS compatibility for packages like pino\n\t];\n\n\tif (minify) {\n\t\t// Names kept: an error's class name is what a log line's `err.type` and\n\t\t// a response's `name` say, and minified they read `_6`.\n\t\targs.push('--minify', '--keep-names');\n\t}\n\n\tif (sourcemap) {\n\t\targs.push('--sourcemap');\n\t}\n\n\t// Add external packages (user-specified), and the optional peers a\n\t// @geekmidas package only loads on a path this app may never take\n\tfor (const ext of [\n\t\t...external,\n\t\t...missingOptionalPeers(dirname(entryPoint)),\n\t]) {\n\t\targs.push(`--external:${ext}`);\n\t}\n\n\t// Handle secrets injection if stage is provided\n\tlet masterKey: string | undefined;\n\n\tif (stage) {\n\t\tconst {\n\t\t\ttoEmbeddableSecrets,\n\t\t\tvalidateEnvironmentVariables,\n\t\t\tinitStageSecrets,\n\t\t} = await import('../secrets/storage');\n\t\tconst { FileSecretsStore, secretsStoreFor } = await import(\n\t\t\t'../secrets/store'\n\t\t);\n\t\tconst { ConfigNotFound, loadWorkspaceSettings } = await import('../config');\n\t\tconst { encryptSecrets, generateDefineOptions } = await import(\n\t\t\t'../secrets/encryption'\n\t\t);\n\n\t\t// The stage's own store — SSM or Secrets Manager for a deployed stage\n\t\t// kept there. Outside\n\t\t// any workspace nothing names one, so it is the file.\n\t\tconst store = await loadWorkspaceSettings().then(\n\t\t\t(workspace) => secretsStoreFor(workspace, stage),\n\t\t\t(error) => {\n\t\t\t\tif (error instanceof ConfigNotFound) {\n\t\t\t\t\treturn new FileSecretsStore(process.cwd());\n\t\t\t\t}\n\t\t\t\tthrow error;\n\t\t\t},\n\t\t);\n\t\tlet secrets = await store.read(stage);\n\n\t\tif (!secrets) {\n\t\t\t// Auto-initialize secrets for the stage\n\t\t\tconsole.log(` Initializing secrets for stage \"${stage}\"...`);\n\t\t\tsecrets = initStageSecrets(stage);\n\t\t\tawait store.write(stage, secrets);\n\t\t\tconsole.log(` ✓ Created the \"${stage}\" secrets (${store.name})`);\n\t\t}\n\n\t\t// Validate environment variables if constructs are provided\n\t\tif (constructs && constructs.length > 0) {\n\t\t\tconsole.log(' Analyzing environment variable requirements...');\n\t\t\tconst requiredVars = await collectRequiredEnvVars(constructs);\n\n\t\t\tif (requiredVars.length > 0) {\n\t\t\t\tconst validation = validateEnvironmentVariables(requiredVars, secrets);\n\n\t\t\t\tif (!validation.valid) {\n\t\t\t\t\tconst errorMessage = [\n\t\t\t\t\t\t`Missing environment variables for stage \"${stage}\":`,\n\t\t\t\t\t\t'',\n\t\t\t\t\t\t...validation.missing.map((v) => ` ❌ ${v}`),\n\t\t\t\t\t\t'',\n\t\t\t\t\t\t'To fix this, either:',\n\t\t\t\t\t\t` 1. Add the missing variables to the \"${stage}\" secrets using:`,\n\t\t\t\t\t\t` gkm secrets:set <KEY> <VALUE> --stage ${stage}`,\n\t\t\t\t\t\t'',\n\t\t\t\t\t\t` 2. Or import from a JSON file:`,\n\t\t\t\t\t\t` gkm secrets:import secrets.json --stage ${stage}`,\n\t\t\t\t\t\t'',\n\t\t\t\t\t\t'Required variables:',\n\t\t\t\t\t\t...validation.required.map((v) =>\n\t\t\t\t\t\t\tvalidation.missing.includes(v) ? ` ❌ ${v}` : ` ✓ ${v}`,\n\t\t\t\t\t\t),\n\t\t\t\t\t].join('\\n');\n\n\t\t\t\t\tthrow new Error(errorMessage);\n\t\t\t\t}\n\n\t\t\t\tconsole.log(\n\t\t\t\t\t` ✓ All ${requiredVars.length} required environment variables found`,\n\t\t\t\t);\n\t\t\t}\n\t\t}\n\n\t\t// Convert to embeddable format and encrypt\n\t\tconst embeddable = toEmbeddableSecrets(secrets);\n\t\tconst encrypted = encryptSecrets(embeddable);\n\t\tmasterKey = encrypted.masterKey;\n\n\t\t// Add define options for build-time injection using esbuild's --define:KEY=VALUE format\n\t\tconst defines = generateDefineOptions(encrypted);\n\t\tfor (const [key, value] of Object.entries(defines)) {\n\t\t\t// `value` is already a JS string literal; quoting it again would\n\t\t\t// embed the quotes, and the IV would not parse as hex.\n\t\t\targs.push(`--define:${key}=${value}`);\n\t\t}\n\n\t\tconsole.log(` Secrets encrypted for stage \"${stage}\"`);\n\t} else {\n\t\t// Credentials encrypted elsewhere — an image build hands them in as a\n\t\t// BuildKit secret and writes them beside the app's `.gkm/server` — are\n\t\t// embedded as they are; whoever encrypted them holds the key.\n\t\tconst gkmDir = join(outputDir, '..', '..');\n\t\tconst encryptedPath = join(gkmDir, 'credentials.enc');\n\t\tconst ivPath = join(gkmDir, 'credentials.iv');\n\t\tif (existsSync(encryptedPath) && existsSync(ivPath)) {\n\t\t\tconst { generateDefineOptions } = await import('../secrets/encryption');\n\t\t\tconst defines = generateDefineOptions({\n\t\t\t\tencrypted: readFileSync(encryptedPath, 'utf-8').trim(),\n\t\t\t\tiv: readFileSync(ivPath, 'utf-8').trim(),\n\t\t\t\tmasterKey: '',\n\t\t\t});\n\t\t\tfor (const [key, value] of Object.entries(defines)) {\n\t\t\t\targs.push(`--define:${key}=${value}`);\n\t\t\t}\n\t\t\tconsole.log(' Embedded the encrypted credentials the build was given');\n\t\t}\n\t}\n\n\ttry {\n\t\t// Run esbuild with command-line arguments\n\t\tconst [cmd, ...cmdArgs] = args as [string, ...string[]];\n\t\tconst result = spawnSync(cmd, cmdArgs, {\n\t\t\tcwd: process.cwd(),\n\t\t\tstdio: 'inherit',\n\t\t\tshell: process.platform === 'win32', // Only use shell on Windows for npx resolution\n\t\t});\n\n\t\tif (result.error) {\n\t\t\tthrow result.error;\n\t\t}\n\t\tif (result.status !== 0) {\n\t\t\tthrow new Error(`esbuild exited with code ${result.status}`);\n\t\t}\n\n\t\t// Add shebang to the bundled file\n\t\tconst { readFile } = await import('node:fs/promises');\n\t\tconst content = await readFile(mjsOutput, 'utf-8');\n\t\tif (!content.startsWith('#!')) {\n\t\t\tawait writeFile(mjsOutput, `#!/usr/bin/env node\\n${content}`);\n\t\t}\n\t} catch (error) {\n\t\tthrow new Error(\n\t\t\t`Failed to bundle server: ${error instanceof Error ? error.message : 'Unknown error'}`,\n\t\t);\n\t}\n\n\treturn {\n\t\toutputPath: mjsOutput,\n\t\tmasterKey,\n\t};\n}\n"],"mappings":"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"}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"drivers-D11JHxrD.mjs","names":[],"sources":["../src/generators/drivers.ts"],"sourcesContent":["/**\n * Driver registration for generated entry points.\n *\n * A construct declares that it needs object storage and is handed one URL; the\n * scheme in that URL picks the driver that builds the client. Which drivers\n * exist is therefore the *entry point's* decision, not the construct's and not\n * the application's — an app that never talks to S3 should never resolve the AWS\n * SDK, and application code that registers a driver has named a provider in the\n * one layer this design keeps provider-free.\n *\n * So the generated entries register: `gkm dev`'s server for local development,\n * and each Lambda handler for its own target. Registration is an explicit call\n * rather than an import side effect, because a side-effecting module is exactly\n * what a bundler is entitled to drop.\n */\n\nimport { existsSync, readFileSync } from 'node:fs';\nimport { dirname, join, resolve } from 'node:path';\nimport {\n\ttype CacheBackend,\n\tDEFAULT_CACHE,\n\ttype EventsBackend,\n\tholdsEveryCache,\n} from '../types.js';\n\n/** The `s3://` driver, which serves MinIO locally and S3 deployed. */\nconst S3 = {\n\timports: `import { registerStorageDriver } from '@geekmidas/storage';\\nimport { s3Driver } from '@geekmidas/storage/aws';`,\n\tsetup: 'registerStorageDriver(s3Driver);',\n};\n\n/** What a generated entry needs in order to register the drivers it uses. */\nexport interface RuntimeDrivers {\n\t/** Import lines, or `''` when there is nothing to register. */\n\timports: string;\n\t/** The registration calls, or `''`. */\n\tsetup: string;\n}\n\n/** @deprecated The same shape, from when only storage had drivers. */\nexport type StorageDrivers = RuntimeDrivers;\n\nconst NONE: RuntimeDrivers = { imports: '', setup: '' };\n\n/** The Redis wire protocol's drivers: both schemes, plain and TLS. */\nconst REDIS: RuntimeDrivers = {\n\timports: `import { registerCacheDriver } from '@geekmidas/cache';\\nimport { redisCacheDriver, redissCacheDriver } from '@geekmidas/cache/redis';`,\n\tsetup:\n\t\t'registerCacheDriver(redisCacheDriver);\\nregisterCacheDriver(redissCacheDriver);',\n};\n\n/**\n * The cache driver for a backend — exactly one, never all three.\n *\n * Keyed off the *backend* rather than off a dependency, because unlike storage\n * the backend already says which driver is right: a project caching in Postgres\n * resolves `pg` and never `ioredis`, and one caching in Upstash resolves\n * neither. That is the whole reason the drivers live behind separate subpaths.\n *\n * Both HTTP schemes are registered for Upstash, because the local proxy speaks\n * `http://` and Upstash speaks `https://`, and an entry that registered one\n * would work in exactly one of the two places.\n */\nconst CACHE_DRIVERS: Record<CacheBackend, RuntimeDrivers> = {\n\tupstash: {\n\t\timports: `import { registerCacheDriver } from '@geekmidas/cache';\\nimport { upstashCacheDriver, upstashInsecureCacheDriver } from '@geekmidas/cache/upstash';`,\n\t\tsetup:\n\t\t\t'registerCacheDriver(upstashCacheDriver);\\nregisterCacheDriver(upstashInsecureCacheDriver);',\n\t},\n\telasticache: REDIS,\n\t// The stack's own Redis speaks the same wire protocol — and a managed\n\t// Redis set in its place may be `rediss://`.\n\tredis: REDIS,\n\tdb: {\n\t\timports: `import { registerCacheDriver } from '@geekmidas/cache';\\nimport { postgresCacheDriver } from '@geekmidas/cache/postgres';`,\n\t\tsetup: 'registerCacheDriver(postgresCacheDriver);',\n\t},\n};\n\n/**\n * The events drivers for a broker — only its own, so a bundle never resolves\n * another broker's client library.\n *\n * Keyed off the backend, as the cache is: the target already says which broker\n * carries the app's topics and queues. SNS registers SQS beside it, because on\n * AWS a topic is `sns://` and a queue is `sqs://`, and an entry registering one\n * would publish to topics and fail on the first queue.\n */\nconst EVENTS_DRIVERS: Record<EventsBackend, RuntimeDrivers> = {\n\tpgboss: {\n\t\timports: `import { registerEventsDriver } from '@geekmidas/events';\\nimport { pgbossEventsDriver } from '@geekmidas/events/pgboss';`,\n\t\tsetup: 'registerEventsDriver(pgbossEventsDriver);',\n\t},\n\tsns: {\n\t\timports: `import { registerEventsDriver } from '@geekmidas/events';\\nimport { snsEventsDriver } from '@geekmidas/events/sns';\\nimport { sqsEventsDriver } from '@geekmidas/events/sqs';`,\n\t\tsetup:\n\t\t\t'registerEventsDriver(snsEventsDriver);\\nregisterEventsDriver(sqsEventsDriver);',\n\t},\n\trabbitmq: {\n\t\timports: `import { registerEventsDriver } from '@geekmidas/events';\\nimport { rabbitmqEventsDriver } from '@geekmidas/events/rabbitmq';`,\n\t\tsetup: 'registerEventsDriver(rabbitmqEventsDriver);',\n\t},\n};\n\n/**\n * Everything a generated entry should register: object storage, the cache,\n * and the events broker.\n *\n * Merged here rather than threaded separately, because a generated file has one\n * import block and one setup block whatever fills them.\n */\nexport function driversFor(options: {\n\tappRoot: string;\n\t/**\n\t * The cache backends actually in play — normally one, and more than one only\n\t * where an app declares two caches that live in different places.\n\t *\n\t * Absent or empty means nothing declared a cache, so no cache driver is\n\t * registered at all.\n\t */\n\tcache?: CacheBackend | readonly CacheBackend[] | false;\n\t/**\n\t * The events brokers in play — see {@link eventsBackendsIn}. Absent or\n\t * empty means nothing declared a topic or a queue, so no events driver is\n\t * registered and `@geekmidas/events` is never resolved.\n\t */\n\tevents?: EventsBackend | readonly EventsBackend[] | false;\n}): RuntimeDrivers {\n\tconst backends = listOf(options.cache);\n\tconst brokers = listOf(options.events);\n\n\tconst parts = [\n\t\tstorageDriversFor(options.appRoot),\n\t\t...[...new Set(backends)].map(\n\t\t\t(backend) => CACHE_DRIVERS[backend] ?? CACHE_DRIVERS[DEFAULT_CACHE.aws],\n\t\t),\n\t\t...[...new Set(brokers)].map((broker) => EVENTS_DRIVERS[broker]),\n\t].filter((part) => part.imports || part.setup);\n\n\tif (parts.length === 0) return NONE;\n\n\treturn {\n\t\timports: parts.map((part) => part.imports).join('\\n'),\n\t\tsetup: parts.map((part) => part.setup).join('\\n'),\n\t};\n}\n\nfunction listOf<T extends string>(\n\tvalue: T | readonly T[] | false | undefined,\n): readonly T[] {\n\tif (value === false || value === undefined) return [];\n\treturn typeof value === 'string' ? [value] : (value as readonly T[]);\n}\n\n/**\n * Which events brokers an app's entry has to speak: the target's, when the app\n * declares a `Topic` or a `Queue` — and none at all when it declares neither,\n * so a project without events never resolves `@geekmidas/events`.\n *\n * A worker on pg-boss counts too: a server schedules its crons through the\n * pg-boss broker, which the plan provisions for a declared worker beside a\n * declared database (`workerBroker` in `reconcile/plan.ts`) whether or not\n * anything publishes.\n */\nexport function eventsBackendsIn(\n\tmanifest: Record<string, { kind: string }>,\n\tconfigured: EventsBackend,\n): EventsBackend[] {\n\tconst kinds = new Set(Object.values(manifest).map((d) => d.kind));\n\tconst declared =\n\t\tkinds.has('topic') ||\n\t\tkinds.has('queue') ||\n\t\t(configured === 'pgboss' && kinds.has('worker') && kinds.has('database'));\n\n\treturn declared ? [configured] : [];\n}\n\n/**\n * Which cache backends an app's entry actually has to speak.\n *\n * The config answers this for a cache that named nowhere, and the *declaration*\n * answers it for one that named a database — `orders.cache('Sessions')` is a\n * statement about the application, so a deployment cannot move that cache and\n * the entry cannot register a driver for somewhere else.\n *\n * Reading only the config is how an entry ends up registering the Upstash\n * driver for a URL the target composed as `postgres://`, which fails at the\n * first request with `UnregisteredCacheScheme` and a stack that points at the\n * cache rather than at the config that disagreed.\n */\nexport function cacheBackendsIn(\n\tmanifest: Record<string, { kind: string; of?: string }>,\n\tconfigured: CacheBackend,\n): CacheBackend[] {\n\tconst caches = Object.values(manifest).filter((d) => d.kind === 'cache');\n\n\t// The stack's own Redis holds every cache, one that named a database too.\n\treturn [\n\t\t...new Set(\n\t\t\tcaches.map((c) =>\n\t\t\t\tc.of && !holdsEveryCache(configured) ? 'db' : configured,\n\t\t\t),\n\t\t),\n\t];\n}\n\n/**\n * The drivers an app's entry points should register.\n *\n * Keyed off the dependency rather than off a declared bucket, because the entry\n * is generated before discovery has run on the watcher's rebuild path — and an\n * app that installed `@geekmidas/storage` has already paid for the SDK it would\n * otherwise resolve lazily.\n *\n * \"Installed\" the way Node resolves it: the app's own `package.json`, or any\n * directory above it. A workspace that lists `@geekmidas/storage` once, at its\n * root, is the common case — and reading only the app's file registered no S3\n * driver there, so the first service to inject a bucket threw\n * `UnregisteredStorageScheme`. A queue whose consumer depended on one logged\n * that once and was never polled.\n */\nexport function storageDriversFor(appRoot: string): RuntimeDrivers {\n\treturn installedFrom(appRoot, '@geekmidas/storage') ? S3 : NONE;\n}\n\n/**\n * Whether `pkg` is a dependency of the app at `appRoot` the way Node resolves\n * it: listed in the app's own `package.json`, or in any directory above it.\n */\nexport function installedFrom(appRoot: string, pkg: string): boolean {\n\tfor (let dir = resolve(appRoot); ; dir = dirname(dir)) {\n\t\tif (lists(join(dir, 'package.json'), pkg)) return true;\n\t\tif (dirname(dir) === dir) return false;\n\t}\n}\n\nfunction lists(manifest: string, dependency: string): boolean {\n\tif (!existsSync(manifest)) return false;\n\n\ttry {\n\t\tconst pkg = JSON.parse(readFileSync(manifest, 'utf8')) as {\n\t\t\tdependencies?: Record<string, string>;\n\t\t\tdevDependencies?: Record<string, string>;\n\t\t};\n\n\t\treturn Boolean(\n\t\t\tpkg.dependencies?.[dependency] ?? pkg.devDependencies?.[dependency],\n\t\t);\n\t} catch {\n\t\t// An unreadable package.json is the build's problem, not this function's.\n\t\treturn false;\n\t}\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;AA0BA,MAAM,KAAK;CACV,SAAS;CACT,OAAO;AACR;AAaA,MAAM,OAAuB;CAAE,SAAS;CAAI,OAAO;AAAG;;AAGtD,MAAM,QAAwB;CAC7B,SAAS;CACT,OACC;AACF;;;;;;;;;;;;;AAcA,MAAM,gBAAsD;CAC3D,SAAS;EACR,SAAS;EACT,OACC;CACF;CACA,aAAa;CAGb,OAAO;CACP,IAAI;EACH,SAAS;EACT,OAAO;CACR;AACD;;;;;;;;;;AAWA,MAAM,iBAAwD;CAC7D,QAAQ;EACP,SAAS;EACT,OAAO;CACR;CACA,KAAK;EACJ,SAAS;EACT,OACC;CACF;CACA,UAAU;EACT,SAAS;EACT,OAAO;CACR;AACD;;;;;;;;AASA,SAAgB,WAAW,SAgBR;CAClB,MAAM,WAAW,OAAO,QAAQ,KAAK;CACrC,MAAM,UAAU,OAAO,QAAQ,MAAM;CAErC,MAAM,QAAQ;EACb,kBAAkB,QAAQ,OAAO;EACjC,GAAG,CAAC,GAAG,IAAI,IAAI,QAAQ,CAAC,CAAC,CAAC,KACxB,YAAY,cAAc,YAAY,cAAc,cAAc,IACpE;EACA,GAAG,CAAC,GAAG,IAAI,IAAI,OAAO,CAAC,CAAC,CAAC,KAAK,WAAW,eAAe,OAAO;CAChE,CAAC,CAAC,QAAQ,SAAS,KAAK,WAAW,KAAK,KAAK;CAE7C,IAAI,MAAM,WAAW,GAAG,OAAO;CAE/B,OAAO;EACN,SAAS,MAAM,KAAK,SAAS,KAAK,OAAO,CAAC,CAAC,KAAK,IAAI;EACpD,OAAO,MAAM,KAAK,SAAS,KAAK,KAAK,CAAC,CAAC,KAAK,IAAI;CACjD;AACD;AAEA,SAAS,OACR,OACe;CACf,IAAI,UAAU,SAAS,UAAU,KAAA,GAAW,OAAO,CAAC;CACpD,OAAO,OAAO,UAAU,WAAW,CAAC,KAAK,IAAK;AAC/C;;;;;;;;;;;AAYA,SAAgB,iBACf,UACA,YACkB;CAClB,MAAM,QAAQ,IAAI,IAAI,OAAO,OAAO,QAAQ,CAAC,CAAC,KAAK,MAAM,EAAE,IAAI,CAAC;CAMhE,OAJC,MAAM,IAAI,OAAO,KACjB,MAAM,IAAI,OAAO,KAChB,eAAe,YAAY,MAAM,IAAI,QAAQ,KAAK,MAAM,IAAI,UAAU,IAEtD,CAAC,UAAU,IAAI,CAAC;AACnC;;;;;;;;;;;;;;AAeA,SAAgB,gBACf,UACA,YACiB;CACjB,MAAM,SAAS,OAAO,OAAO,QAAQ,CAAC,CAAC,QAAQ,MAAM,EAAE,SAAS,OAAO;CAGvE,OAAO,CACN,GAAG,IAAI,IACN,OAAO,KAAK,MACX,EAAE,MAAM,CAAC,gBAAgB,UAAU,IAAI,OAAO,UAC/C,CACD,CACD;AACD;;;;;;;;;;;;;;;;AAiBA,SAAgB,kBAAkB,SAAiC;CAClE,OAAO,cAAc,SAAS,oBAAoB,IAAI,KAAK;AAC5D;;;;;AAMA,SAAgB,cAAc,SAAiB,KAAsB;CACpE,KAAK,IAAI,MAAM,QAAQ,OAAO,IAAK,MAAM,QAAQ,GAAG,GAAG;EACtD,IAAI,MAAM,KAAK,KAAK,cAAc,GAAG,GAAG,GAAG,OAAO;EAClD,IAAI,QAAQ,GAAG,MAAM,KAAK,OAAO;CAClC;AACD;AAEA,SAAS,MAAM,UAAkB,YAA6B;CAC7D,IAAI,CAAC,WAAW,QAAQ,GAAG,OAAO;CAElC,IAAI;EACH,MAAM,MAAM,KAAK,MAAM,aAAa,UAAU,MAAM,CAAC;EAKrD,OAAO,QACN,IAAI,eAAe,eAAe,IAAI,kBAAkB,WACzD;CACD,QAAQ;EAEP,OAAO;CACR;AACD"}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"drivers-GKaA6ITM.cjs","names":["DEFAULT_CACHE","holdsEveryCache","resolve","dirname","join","existsSync","readFileSync"],"sources":["../src/generators/drivers.ts"],"sourcesContent":["/**\n * Driver registration for generated entry points.\n *\n * A construct declares that it needs object storage and is handed one URL; the\n * scheme in that URL picks the driver that builds the client. Which drivers\n * exist is therefore the *entry point's* decision, not the construct's and not\n * the application's — an app that never talks to S3 should never resolve the AWS\n * SDK, and application code that registers a driver has named a provider in the\n * one layer this design keeps provider-free.\n *\n * So the generated entries register: `gkm dev`'s server for local development,\n * and each Lambda handler for its own target. Registration is an explicit call\n * rather than an import side effect, because a side-effecting module is exactly\n * what a bundler is entitled to drop.\n */\n\nimport { existsSync, readFileSync } from 'node:fs';\nimport { dirname, join, resolve } from 'node:path';\nimport {\n\ttype CacheBackend,\n\tDEFAULT_CACHE,\n\ttype EventsBackend,\n\tholdsEveryCache,\n} from '../types.js';\n\n/** The `s3://` driver, which serves MinIO locally and S3 deployed. */\nconst S3 = {\n\timports: `import { registerStorageDriver } from '@geekmidas/storage';\\nimport { s3Driver } from '@geekmidas/storage/aws';`,\n\tsetup: 'registerStorageDriver(s3Driver);',\n};\n\n/** What a generated entry needs in order to register the drivers it uses. */\nexport interface RuntimeDrivers {\n\t/** Import lines, or `''` when there is nothing to register. */\n\timports: string;\n\t/** The registration calls, or `''`. */\n\tsetup: string;\n}\n\n/** @deprecated The same shape, from when only storage had drivers. */\nexport type StorageDrivers = RuntimeDrivers;\n\nconst NONE: RuntimeDrivers = { imports: '', setup: '' };\n\n/** The Redis wire protocol's drivers: both schemes, plain and TLS. */\nconst REDIS: RuntimeDrivers = {\n\timports: `import { registerCacheDriver } from '@geekmidas/cache';\\nimport { redisCacheDriver, redissCacheDriver } from '@geekmidas/cache/redis';`,\n\tsetup:\n\t\t'registerCacheDriver(redisCacheDriver);\\nregisterCacheDriver(redissCacheDriver);',\n};\n\n/**\n * The cache driver for a backend — exactly one, never all three.\n *\n * Keyed off the *backend* rather than off a dependency, because unlike storage\n * the backend already says which driver is right: a project caching in Postgres\n * resolves `pg` and never `ioredis`, and one caching in Upstash resolves\n * neither. That is the whole reason the drivers live behind separate subpaths.\n *\n * Both HTTP schemes are registered for Upstash, because the local proxy speaks\n * `http://` and Upstash speaks `https://`, and an entry that registered one\n * would work in exactly one of the two places.\n */\nconst CACHE_DRIVERS: Record<CacheBackend, RuntimeDrivers> = {\n\tupstash: {\n\t\timports: `import { registerCacheDriver } from '@geekmidas/cache';\\nimport { upstashCacheDriver, upstashInsecureCacheDriver } from '@geekmidas/cache/upstash';`,\n\t\tsetup:\n\t\t\t'registerCacheDriver(upstashCacheDriver);\\nregisterCacheDriver(upstashInsecureCacheDriver);',\n\t},\n\telasticache: REDIS,\n\t// The stack's own Redis speaks the same wire protocol — and a managed\n\t// Redis set in its place may be `rediss://`.\n\tredis: REDIS,\n\tdb: {\n\t\timports: `import { registerCacheDriver } from '@geekmidas/cache';\\nimport { postgresCacheDriver } from '@geekmidas/cache/postgres';`,\n\t\tsetup: 'registerCacheDriver(postgresCacheDriver);',\n\t},\n};\n\n/**\n * The events drivers for a broker — only its own, so a bundle never resolves\n * another broker's client library.\n *\n * Keyed off the backend, as the cache is: the target already says which broker\n * carries the app's topics and queues. SNS registers SQS beside it, because on\n * AWS a topic is `sns://` and a queue is `sqs://`, and an entry registering one\n * would publish to topics and fail on the first queue.\n */\nconst EVENTS_DRIVERS: Record<EventsBackend, RuntimeDrivers> = {\n\tpgboss: {\n\t\timports: `import { registerEventsDriver } from '@geekmidas/events';\\nimport { pgbossEventsDriver } from '@geekmidas/events/pgboss';`,\n\t\tsetup: 'registerEventsDriver(pgbossEventsDriver);',\n\t},\n\tsns: {\n\t\timports: `import { registerEventsDriver } from '@geekmidas/events';\\nimport { snsEventsDriver } from '@geekmidas/events/sns';\\nimport { sqsEventsDriver } from '@geekmidas/events/sqs';`,\n\t\tsetup:\n\t\t\t'registerEventsDriver(snsEventsDriver);\\nregisterEventsDriver(sqsEventsDriver);',\n\t},\n\trabbitmq: {\n\t\timports: `import { registerEventsDriver } from '@geekmidas/events';\\nimport { rabbitmqEventsDriver } from '@geekmidas/events/rabbitmq';`,\n\t\tsetup: 'registerEventsDriver(rabbitmqEventsDriver);',\n\t},\n};\n\n/**\n * Everything a generated entry should register: object storage, the cache,\n * and the events broker.\n *\n * Merged here rather than threaded separately, because a generated file has one\n * import block and one setup block whatever fills them.\n */\nexport function driversFor(options: {\n\tappRoot: string;\n\t/**\n\t * The cache backends actually in play — normally one, and more than one only\n\t * where an app declares two caches that live in different places.\n\t *\n\t * Absent or empty means nothing declared a cache, so no cache driver is\n\t * registered at all.\n\t */\n\tcache?: CacheBackend | readonly CacheBackend[] | false;\n\t/**\n\t * The events brokers in play — see {@link eventsBackendsIn}. Absent or\n\t * empty means nothing declared a topic or a queue, so no events driver is\n\t * registered and `@geekmidas/events` is never resolved.\n\t */\n\tevents?: EventsBackend | readonly EventsBackend[] | false;\n}): RuntimeDrivers {\n\tconst backends = listOf(options.cache);\n\tconst brokers = listOf(options.events);\n\n\tconst parts = [\n\t\tstorageDriversFor(options.appRoot),\n\t\t...[...new Set(backends)].map(\n\t\t\t(backend) => CACHE_DRIVERS[backend] ?? CACHE_DRIVERS[DEFAULT_CACHE.aws],\n\t\t),\n\t\t...[...new Set(brokers)].map((broker) => EVENTS_DRIVERS[broker]),\n\t].filter((part) => part.imports || part.setup);\n\n\tif (parts.length === 0) return NONE;\n\n\treturn {\n\t\timports: parts.map((part) => part.imports).join('\\n'),\n\t\tsetup: parts.map((part) => part.setup).join('\\n'),\n\t};\n}\n\nfunction listOf<T extends string>(\n\tvalue: T | readonly T[] | false | undefined,\n): readonly T[] {\n\tif (value === false || value === undefined) return [];\n\treturn typeof value === 'string' ? [value] : (value as readonly T[]);\n}\n\n/**\n * Which events brokers an app's entry has to speak: the target's, when the app\n * declares a `Topic` or a `Queue` — and none at all when it declares neither,\n * so a project without events never resolves `@geekmidas/events`.\n *\n * A worker on pg-boss counts too: a server schedules its crons through the\n * pg-boss broker, which the plan provisions for a declared worker beside a\n * declared database (`workerBroker` in `reconcile/plan.ts`) whether or not\n * anything publishes.\n */\nexport function eventsBackendsIn(\n\tmanifest: Record<string, { kind: string }>,\n\tconfigured: EventsBackend,\n): EventsBackend[] {\n\tconst kinds = new Set(Object.values(manifest).map((d) => d.kind));\n\tconst declared =\n\t\tkinds.has('topic') ||\n\t\tkinds.has('queue') ||\n\t\t(configured === 'pgboss' && kinds.has('worker') && kinds.has('database'));\n\n\treturn declared ? [configured] : [];\n}\n\n/**\n * Which cache backends an app's entry actually has to speak.\n *\n * The config answers this for a cache that named nowhere, and the *declaration*\n * answers it for one that named a database — `orders.cache('Sessions')` is a\n * statement about the application, so a deployment cannot move that cache and\n * the entry cannot register a driver for somewhere else.\n *\n * Reading only the config is how an entry ends up registering the Upstash\n * driver for a URL the target composed as `postgres://`, which fails at the\n * first request with `UnregisteredCacheScheme` and a stack that points at the\n * cache rather than at the config that disagreed.\n */\nexport function cacheBackendsIn(\n\tmanifest: Record<string, { kind: string; of?: string }>,\n\tconfigured: CacheBackend,\n): CacheBackend[] {\n\tconst caches = Object.values(manifest).filter((d) => d.kind === 'cache');\n\n\t// The stack's own Redis holds every cache, one that named a database too.\n\treturn [\n\t\t...new Set(\n\t\t\tcaches.map((c) =>\n\t\t\t\tc.of && !holdsEveryCache(configured) ? 'db' : configured,\n\t\t\t),\n\t\t),\n\t];\n}\n\n/**\n * The drivers an app's entry points should register.\n *\n * Keyed off the dependency rather than off a declared bucket, because the entry\n * is generated before discovery has run on the watcher's rebuild path — and an\n * app that installed `@geekmidas/storage` has already paid for the SDK it would\n * otherwise resolve lazily.\n *\n * \"Installed\" the way Node resolves it: the app's own `package.json`, or any\n * directory above it. A workspace that lists `@geekmidas/storage` once, at its\n * root, is the common case — and reading only the app's file registered no S3\n * driver there, so the first service to inject a bucket threw\n * `UnregisteredStorageScheme`. A queue whose consumer depended on one logged\n * that once and was never polled.\n */\nexport function storageDriversFor(appRoot: string): RuntimeDrivers {\n\treturn installedFrom(appRoot, '@geekmidas/storage') ? S3 : NONE;\n}\n\n/**\n * Whether `pkg` is a dependency of the app at `appRoot` the way Node resolves\n * it: listed in the app's own `package.json`, or in any directory above it.\n */\nexport function installedFrom(appRoot: string, pkg: string): boolean {\n\tfor (let dir = resolve(appRoot); ; dir = dirname(dir)) {\n\t\tif (lists(join(dir, 'package.json'), pkg)) return true;\n\t\tif (dirname(dir) === dir) return false;\n\t}\n}\n\nfunction lists(manifest: string, dependency: string): boolean {\n\tif (!existsSync(manifest)) return false;\n\n\ttry {\n\t\tconst pkg = JSON.parse(readFileSync(manifest, 'utf8')) as {\n\t\t\tdependencies?: Record<string, string>;\n\t\t\tdevDependencies?: Record<string, string>;\n\t\t};\n\n\t\treturn Boolean(\n\t\t\tpkg.dependencies?.[dependency] ?? pkg.devDependencies?.[dependency],\n\t\t);\n\t} catch {\n\t\t// An unreadable package.json is the build's problem, not this function's.\n\t\treturn false;\n\t}\n}\n"],"mappings":"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"}
|