@geekmidas/cli 10.0.0-alpha.63 → 10.0.0-alpha.65
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/dist/{HostingerProvider-mJ7YruxH.cjs → HostingerProvider-Dlmbfass.cjs} +3 -3
- package/dist/{HostingerProvider-mJ7YruxH.cjs.map → HostingerProvider-Dlmbfass.cjs.map} +1 -1
- package/dist/{HostingerProvider-DBBwpDjn.mjs → HostingerProvider-x9gQTAGZ.mjs} +2 -2
- package/dist/{HostingerProvider-DBBwpDjn.mjs.map → HostingerProvider-x9gQTAGZ.mjs.map} +1 -1
- package/dist/{LegacyStateStore-CP7SZwTP.cjs → LegacyStateStore-BGoo__sO.cjs} +2 -2
- package/dist/{LegacyStateStore-CP7SZwTP.cjs.map → LegacyStateStore-BGoo__sO.cjs.map} +1 -1
- package/dist/{LegacyStateStore-Q3GmKOdw.mjs → LegacyStateStore-BUu1nQ5u.mjs} +2 -2
- package/dist/{LegacyStateStore-Q3GmKOdw.mjs.map → LegacyStateStore-BUu1nQ5u.mjs.map} +1 -1
- package/dist/{LocalStateStore-sS8mvLeq.cjs → LocalStateStore-93urXyQe.cjs} +2 -2
- package/dist/{LocalStateStore-sS8mvLeq.cjs.map → LocalStateStore-93urXyQe.cjs.map} +1 -1
- package/dist/{LocalStateStore-BGIXcrvX.mjs → LocalStateStore-BRhMQRuy.mjs} +2 -2
- package/dist/{LocalStateStore-BGIXcrvX.mjs.map → LocalStateStore-BRhMQRuy.mjs.map} +1 -1
- package/dist/{OpenApiTsGenerator-CjHWFRDd.cjs → OpenApiTsGenerator-BWuDRHe9.cjs} +2 -2
- package/dist/{OpenApiTsGenerator-CjHWFRDd.cjs.map → OpenApiTsGenerator-BWuDRHe9.cjs.map} +1 -1
- package/dist/{OpenApiTsGenerator-CnAwhGxi.mjs → OpenApiTsGenerator-C6v-IeHt.mjs} +2 -2
- package/dist/{OpenApiTsGenerator-CnAwhGxi.mjs.map → OpenApiTsGenerator-C6v-IeHt.mjs.map} +1 -1
- package/dist/{S3StateStore-6jqITH2F.cjs → S3StateStore-F4dxw6kS.cjs} +2 -2
- package/dist/{S3StateStore-6jqITH2F.cjs.map → S3StateStore-F4dxw6kS.cjs.map} +1 -1
- package/dist/{S3StateStore-D7l_lWJ3.mjs → S3StateStore-pw1GhWwt.mjs} +2 -2
- package/dist/{S3StateStore-D7l_lWJ3.mjs.map → S3StateStore-pw1GhWwt.mjs.map} +1 -1
- package/dist/{SSMStateStore-B__F7IJa.cjs → SSMStateStore-Bt9acuAF.cjs} +2 -2
- package/dist/{SSMStateStore-B__F7IJa.cjs.map → SSMStateStore-Bt9acuAF.cjs.map} +1 -1
- package/dist/{SSMStateStore-CAQDVpoc.mjs → SSMStateStore-Dmj7zzFO.mjs} +2 -2
- package/dist/{SSMStateStore-CAQDVpoc.mjs.map → SSMStateStore-Dmj7zzFO.mjs.map} +1 -1
- package/dist/{bundler-DLfOn0ls.cjs → bundler-D6MlIfBT.cjs} +3 -3
- package/dist/{bundler-DLfOn0ls.cjs.map → bundler-D6MlIfBT.cjs.map} +1 -1
- package/dist/{bundler-DyWI41Iy.mjs → bundler-D_14JLZF.mjs} +3 -3
- package/dist/{bundler-DyWI41Iy.mjs.map → bundler-D_14JLZF.mjs.map} +1 -1
- package/dist/{cli-D-ZM-X2s.cjs → cli-BfH1deAT.cjs} +8 -7
- package/dist/{cli-D-ZM-X2s.cjs.map → cli-BfH1deAT.cjs.map} +1 -1
- package/dist/{cli-PkVF28LM.mjs → cli-P56vvrDQ.mjs} +5 -4
- package/dist/{cli-PkVF28LM.mjs.map → cli-P56vvrDQ.mjs.map} +1 -1
- package/dist/compose-391oEKj1.cjs +1982 -0
- package/dist/compose-391oEKj1.cjs.map +1 -0
- package/dist/compose-DprE3ASm.mjs +1862 -0
- package/dist/compose-DprE3ASm.mjs.map +1 -0
- package/dist/{config-C2BUosLm.d.mts → config-BonvTW8p.d.mts} +3 -3
- package/dist/{config-C2BUosLm.d.mts.map → config-BonvTW8p.d.mts.map} +1 -1
- package/dist/{config-CemHVcIr.d.cts → config-V_X_BMpm.d.cts} +3 -3
- package/dist/{config-CemHVcIr.d.cts.map → config-V_X_BMpm.d.cts.map} +1 -1
- package/dist/config.cjs +416 -17
- package/dist/config.cjs.map +1 -0
- package/dist/config.d.cts +3 -3
- package/dist/config.d.mts +3 -3
- package/dist/config.mjs +401 -2
- package/dist/config.mjs.map +1 -0
- package/dist/credentials-Bzbg23n8.cjs +298 -0
- package/dist/credentials-Bzbg23n8.cjs.map +1 -0
- package/dist/credentials-Dq1MnWQf.mjs +233 -0
- package/dist/credentials-Dq1MnWQf.mjs.map +1 -0
- package/dist/{define-CHBIEc5j.mjs → define-DR8xGYfL.mjs} +6 -3
- package/dist/define-DR8xGYfL.mjs.map +1 -0
- package/dist/{define-Cv_QvDrg.cjs → define-DzRz8FgH.cjs} +6 -3
- package/dist/define-DzRz8FgH.cjs.map +1 -0
- package/dist/deploy/public.cjs +39 -33
- package/dist/deploy/public.cjs.map +1 -1
- package/dist/deploy/public.d.cts +44 -5
- package/dist/deploy/public.d.cts.map +1 -1
- package/dist/deploy/public.d.mts +44 -5
- package/dist/deploy/public.d.mts.map +1 -1
- package/dist/deploy/public.mjs +15 -12
- package/dist/deploy/public.mjs.map +1 -1
- package/dist/{deploy-CGW-QY9D.mjs → deploy-9MBG_QLd.mjs} +11 -9
- package/dist/deploy-9MBG_QLd.mjs.map +1 -0
- package/dist/{deploy-BiMrP9i7.cjs → deploy-B80idPUZ.cjs} +17 -15
- package/dist/deploy-B80idPUZ.cjs.map +1 -0
- package/dist/{deploy-CG8bfFij.mjs → deploy-CZ5nlpsu.mjs} +16 -119
- package/dist/deploy-CZ5nlpsu.mjs.map +1 -0
- package/dist/{deploy-ABBI0lf1.cjs → deploy-Cy8igpQF.cjs} +48 -151
- package/dist/deploy-Cy8igpQF.cjs.map +1 -0
- package/dist/derive-B4vDpzgP.mjs +150 -0
- package/dist/derive-B4vDpzgP.mjs.map +1 -0
- package/dist/derive-DAbo49sm.cjs +161 -0
- package/dist/derive-DAbo49sm.cjs.map +1 -0
- package/dist/{discover-C_6COHEv.cjs → discover-DHdECueV.cjs} +6 -157
- package/dist/discover-DHdECueV.cjs.map +1 -0
- package/dist/{discover-BaT6bpnA.mjs → discover-LMrqsWSU.mjs} +8 -117
- package/dist/discover-LMrqsWSU.mjs.map +1 -0
- package/dist/{dokploy-CwdmlyQL.mjs → dokploy-BdtBePjp.mjs} +3 -3
- package/dist/{dokploy-CwdmlyQL.mjs.map → dokploy-BdtBePjp.mjs.map} +1 -1
- package/dist/{dokploy-QJxMhxdf.cjs → dokploy-iFY8MY-L.cjs} +3 -3
- package/dist/{dokploy-QJxMhxdf.cjs.map → dokploy-iFY8MY-L.cjs.map} +1 -1
- package/dist/{encryption-DXw3jj0c.cjs → encryption-B1wJwgae.cjs} +6 -6
- package/dist/{encryption-DXw3jj0c.cjs.map → encryption-B1wJwgae.cjs.map} +1 -1
- package/dist/{encryption-CbQG_ayW.mjs → encryption-DF_hgIpl.mjs} +5 -5
- package/dist/{encryption-CbQG_ayW.mjs.map → encryption-DF_hgIpl.mjs.map} +1 -1
- package/dist/{github-ciwbIwVa.mjs → github-B0YYE98x.mjs} +4 -4
- package/dist/{github-ciwbIwVa.mjs.map → github-B0YYE98x.mjs.map} +1 -1
- package/dist/{github-BH_mETav.cjs → github-CYcmTdFs.cjs} +4 -4
- package/dist/{github-BH_mETav.cjs.map → github-CYcmTdFs.cjs.map} +1 -1
- package/dist/{index-DrKzpHl-.d.mts → index-DPUAGJaK.d.cts} +16 -3
- package/dist/index-DPUAGJaK.d.cts.map +1 -0
- package/dist/{index-BtXDZQuF.d.cts → index-DwKD_8lt.d.mts} +16 -3
- package/dist/index-DwKD_8lt.d.mts.map +1 -0
- package/dist/index.cjs +280 -1849
- package/dist/index.cjs.map +1 -1
- package/dist/index.mjs +221 -1774
- package/dist/index.mjs.map +1 -1
- package/dist/local-BdH4XN0E.cjs +409 -0
- package/dist/local-BdH4XN0E.cjs.map +1 -0
- package/dist/local-CWR4Y_kW.mjs +350 -0
- package/dist/local-CWR4Y_kW.mjs.map +1 -0
- package/dist/{migrate-ImBKTBcW.mjs → migrate-B2qL5wj4.mjs} +8 -7
- package/dist/{migrate-ImBKTBcW.mjs.map → migrate-B2qL5wj4.mjs.map} +1 -1
- package/dist/{migrate-6oa9y_f2.cjs → migrate-q4pPAXtO.cjs} +21 -20
- package/dist/{migrate-6oa9y_f2.cjs.map → migrate-q4pPAXtO.cjs.map} +1 -1
- package/dist/openapi.cjs +2 -2
- package/dist/openapi.d.cts +1 -1
- package/dist/openapi.d.mts +1 -1
- package/dist/openapi.mjs +2 -2
- package/dist/provider-C9_-45d5.cjs +56 -0
- package/dist/provider-C9_-45d5.cjs.map +1 -0
- package/dist/provider-q4lsYzYG.mjs +45 -0
- package/dist/provider-q4lsYzYG.mjs.map +1 -0
- package/dist/{ready-Bpdnn2ON.mjs → ready-bQfu9GES.mjs} +3 -3
- package/dist/{ready-Bpdnn2ON.mjs.map → ready-bQfu9GES.mjs.map} +1 -1
- package/dist/{ready-BiG0rt4h.cjs → ready-brEhh68f.cjs} +3 -3
- package/dist/{ready-BiG0rt4h.cjs.map → ready-brEhh68f.cjs.map} +1 -1
- package/dist/reconcile/public.cjs +1 -1
- package/dist/reconcile/public.d.cts +1 -1
- package/dist/reconcile/public.d.mts +1 -1
- package/dist/reconcile/public.mjs +1 -1
- package/dist/resolve-DLkIXTct.mjs +262 -0
- package/dist/resolve-DLkIXTct.mjs.map +1 -0
- package/dist/resolve-N_i4XNd-.cjs +315 -0
- package/dist/resolve-N_i4XNd-.cjs.map +1 -0
- package/dist/{runtime-Dn_n-29S.d.cts → runtime-BnD7Nn96.d.cts} +2 -2
- package/dist/{runtime-CXPlkZzs.d.mts.map → runtime-BnD7Nn96.d.cts.map} +1 -1
- package/dist/{runtime-CXPlkZzs.d.mts → runtime-j-tmFL65.d.mts} +2 -2
- package/dist/{runtime-Dn_n-29S.d.cts.map → runtime-j-tmFL65.d.mts.map} +1 -1
- package/dist/sandbox/discover-worker.cjs +1 -1
- package/dist/sandbox/discover-worker.mjs +1 -1
- package/dist/sandbox-CL--ZDui.cjs +164 -0
- package/dist/sandbox-CL--ZDui.cjs.map +1 -0
- package/dist/sandbox-Dm247NGx.mjs +117 -0
- package/dist/sandbox-Dm247NGx.mjs.map +1 -0
- package/dist/sniffer-BSwaL-dX.cjs +2236 -0
- package/dist/sniffer-BSwaL-dX.cjs.map +1 -0
- package/dist/sniffer-BZ-OJO4Y.mjs +2068 -0
- package/dist/sniffer-BZ-OJO4Y.mjs.map +1 -0
- package/dist/sst-B2HbrDR3.mjs +430 -0
- package/dist/sst-B2HbrDR3.mjs.map +1 -0
- package/dist/sst-DcfsVH6x.cjs +453 -0
- package/dist/sst-DcfsVH6x.cjs.map +1 -0
- package/dist/{sniffer-aO_-s5vq.mjs → state-B8YwmUZU.mjs} +1204 -3236
- package/dist/state-B8YwmUZU.mjs.map +1 -0
- package/dist/{sniffer-D_SYc_pO.cjs → state-DF26e_lo.cjs} +1251 -3494
- package/dist/state-DF26e_lo.cjs.map +1 -0
- package/dist/{store-KvNQwYaB.cjs → store-CJF1Tqkq.cjs} +4 -4
- package/dist/{store-KvNQwYaB.cjs.map → store-CJF1Tqkq.cjs.map} +1 -1
- package/dist/{store-BS8A7NcA.mjs → store-CLiHnqOY.mjs} +2 -2
- package/dist/{store-BS8A7NcA.mjs.map → store-CLiHnqOY.mjs.map} +1 -1
- package/dist/target/public.cjs +6 -6
- package/dist/target/public.d.cts +3 -3
- package/dist/target/public.d.mts +3 -3
- package/dist/target/public.mjs +3 -3
- package/dist/terminal-Bs5E1oxm.cjs +88 -0
- package/dist/terminal-Bs5E1oxm.cjs.map +1 -0
- package/dist/terminal-CfbiYUbZ.mjs +84 -0
- package/dist/terminal-CfbiYUbZ.mjs.map +1 -0
- package/dist/{types-DIA7Li-J.d.cts → types-BCzMWBb6.d.mts} +84 -17
- package/dist/types-BCzMWBb6.d.mts.map +1 -0
- package/dist/{types-CfLKGfC_.d.mts → types-ByMWkXKP.d.cts} +85 -16
- package/dist/types-ByMWkXKP.d.cts.map +1 -0
- package/dist/utils-D2Nk1Nv2.cjs +205 -0
- package/dist/utils-D2Nk1Nv2.cjs.map +1 -0
- package/dist/utils-D_fxUqvU.mjs +156 -0
- package/dist/utils-D_fxUqvU.mjs.map +1 -0
- package/dist/vitest.cjs +1 -1
- package/dist/vitest.mjs +1 -1
- package/dist/workspace/index.cjs +3 -1
- package/dist/workspace/index.d.cts +3 -3
- package/dist/workspace/index.d.mts +3 -3
- package/dist/workspace/index.mjs +2 -2
- package/dist/{workspace-Kl4NG9IH.cjs → workspace-BewAHZIo.cjs} +41 -6
- package/dist/workspace-BewAHZIo.cjs.map +1 -0
- package/dist/{workspace-Dh7rr7l4.mjs → workspace-C06_pW4g.mjs} +30 -7
- package/dist/workspace-C06_pW4g.mjs.map +1 -0
- package/package.json +13 -13
- package/dist/config-BEJYKd4s.cjs +0 -648
- package/dist/config-BEJYKd4s.cjs.map +0 -1
- package/dist/config-LKKCVCnC.mjs +0 -547
- package/dist/config-LKKCVCnC.mjs.map +0 -1
- package/dist/define-CHBIEc5j.mjs.map +0 -1
- package/dist/define-Cv_QvDrg.cjs.map +0 -1
- package/dist/deploy-ABBI0lf1.cjs.map +0 -1
- package/dist/deploy-BiMrP9i7.cjs.map +0 -1
- package/dist/deploy-CG8bfFij.mjs.map +0 -1
- package/dist/deploy-CGW-QY9D.mjs.map +0 -1
- package/dist/discover-BaT6bpnA.mjs.map +0 -1
- package/dist/discover-C_6COHEv.cjs.map +0 -1
- package/dist/home-DOzBaWaL.cjs +0 -33
- package/dist/home-DOzBaWaL.cjs.map +0 -1
- package/dist/home-GI7pJl1M.mjs +0 -22
- package/dist/home-GI7pJl1M.mjs.map +0 -1
- package/dist/index-BtXDZQuF.d.cts.map +0 -1
- package/dist/index-DrKzpHl-.d.mts.map +0 -1
- package/dist/resolve-B1T0hUaR.mjs +0 -170
- package/dist/resolve-B1T0hUaR.mjs.map +0 -1
- package/dist/resolve-xytBTtG4.cjs +0 -217
- package/dist/resolve-xytBTtG4.cjs.map +0 -1
- package/dist/runtime-CMwtq0AL.cjs +0 -163
- package/dist/runtime-CMwtq0AL.cjs.map +0 -1
- package/dist/runtime-DbakJS8u.mjs +0 -134
- package/dist/runtime-DbakJS8u.mjs.map +0 -1
- package/dist/sniffer-D_SYc_pO.cjs.map +0 -1
- package/dist/sniffer-aO_-s5vq.mjs.map +0 -1
- package/dist/terminal-D0f1u0LG.cjs +0 -576
- package/dist/terminal-D0f1u0LG.cjs.map +0 -1
- package/dist/terminal-gMxAo9Dj.mjs +0 -517
- package/dist/terminal-gMxAo9Dj.mjs.map +0 -1
- package/dist/types-CfLKGfC_.d.mts.map +0 -1
- package/dist/types-DIA7Li-J.d.cts.map +0 -1
- package/dist/workspace-Dh7rr7l4.mjs.map +0 -1
- package/dist/workspace-Kl4NG9IH.cjs.map +0 -1
|
@@ -0,0 +1,2236 @@
|
|
|
1
|
+
const require_rolldown_runtime = require("./rolldown-runtime-C0BPl7ul.cjs");
|
|
2
|
+
const require_local = require("./local-BdH4XN0E.cjs");
|
|
3
|
+
const require_credentials = require("./credentials-Bzbg23n8.cjs");
|
|
4
|
+
const require_discover = require("./discover-DHdECueV.cjs");
|
|
5
|
+
const require_sandbox = require("./sandbox-CL--ZDui.cjs");
|
|
6
|
+
const require_derive = require("./derive-DAbo49sm.cjs");
|
|
7
|
+
const require_define = require("./define-DzRz8FgH.cjs");
|
|
8
|
+
const require_workspace = require("./workspace-BewAHZIo.cjs");
|
|
9
|
+
const require_store = require("./store-CJF1Tqkq.cjs");
|
|
10
|
+
const require_state = require("./state-DF26e_lo.cjs");
|
|
11
|
+
const require_resolve = require("./resolve-N_i4XNd-.cjs");
|
|
12
|
+
let node_path = require("node:path");
|
|
13
|
+
let node_process = require("node:process");
|
|
14
|
+
let node_readline_promises = require("node:readline/promises");
|
|
15
|
+
node_readline_promises = require_rolldown_runtime.__toESM(node_readline_promises, 1);
|
|
16
|
+
let node_fs = require("node:fs");
|
|
17
|
+
let node_os = require("node:os");
|
|
18
|
+
let node_url = require("node:url");
|
|
19
|
+
let node_crypto = require("node:crypto");
|
|
20
|
+
//#region src/prompt.ts
|
|
21
|
+
/**
|
|
22
|
+
* Asking the person at the terminal.
|
|
23
|
+
*
|
|
24
|
+
* CLI layer only: a command's action may prompt, nothing it calls may. A
|
|
25
|
+
* library call (`deploy()`) that needs something it was not given raises a
|
|
26
|
+
* named error instead, and the command decides whether to ask.
|
|
27
|
+
*/
|
|
28
|
+
/** A prompt was needed with no terminal to ask at. */
|
|
29
|
+
var PromptNeedsTerminal = class extends Error {
|
|
30
|
+
question;
|
|
31
|
+
instead;
|
|
32
|
+
constructor(question, instead) {
|
|
33
|
+
super(`Interactive input required. ${instead}`);
|
|
34
|
+
this.question = question;
|
|
35
|
+
this.instead = instead;
|
|
36
|
+
this.name = "PromptNeedsTerminal";
|
|
37
|
+
}
|
|
38
|
+
};
|
|
39
|
+
/** Whether there is a person at a terminal to ask. */
|
|
40
|
+
function canPrompt() {
|
|
41
|
+
return process.stdin.isTTY === true;
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Ask `message` and resolve with the line typed — without echoing it when
|
|
45
|
+
* `hidden`. Ctrl+C at a hidden prompt exits, as it would anywhere else in a
|
|
46
|
+
* terminal program: raw mode has taken the signal away from the shell.
|
|
47
|
+
*/
|
|
48
|
+
async function prompt(message, options) {
|
|
49
|
+
if (!canPrompt()) throw new PromptNeedsTerminal(message, options.instead);
|
|
50
|
+
if (options.hidden) {
|
|
51
|
+
process.stdout.write(message);
|
|
52
|
+
return new Promise((resolve, reject) => {
|
|
53
|
+
let value = "";
|
|
54
|
+
const cleanup = () => {
|
|
55
|
+
process.stdin.setRawMode(false);
|
|
56
|
+
process.stdin.pause();
|
|
57
|
+
process.stdin.removeListener("data", onData);
|
|
58
|
+
process.stdin.removeListener("error", onError);
|
|
59
|
+
};
|
|
60
|
+
const onError = (err) => {
|
|
61
|
+
cleanup();
|
|
62
|
+
reject(err);
|
|
63
|
+
};
|
|
64
|
+
const onData = (char) => {
|
|
65
|
+
const c = char.toString();
|
|
66
|
+
if (c === "\n" || c === "\r") {
|
|
67
|
+
cleanup();
|
|
68
|
+
process.stdout.write("\n");
|
|
69
|
+
resolve(value);
|
|
70
|
+
} else if (c === "") {
|
|
71
|
+
cleanup();
|
|
72
|
+
process.stdout.write("\n");
|
|
73
|
+
process.exit(1);
|
|
74
|
+
} else if (c === "" || c === "\b") {
|
|
75
|
+
if (value.length > 0) value = value.slice(0, -1);
|
|
76
|
+
} else value += c;
|
|
77
|
+
};
|
|
78
|
+
process.stdin.setRawMode(true);
|
|
79
|
+
process.stdin.resume();
|
|
80
|
+
process.stdin.on("data", onData);
|
|
81
|
+
process.stdin.on("error", onError);
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
const rl = node_readline_promises.createInterface({
|
|
85
|
+
input: node_process.stdin,
|
|
86
|
+
output: node_process.stdout
|
|
87
|
+
});
|
|
88
|
+
try {
|
|
89
|
+
return await rl.question(message);
|
|
90
|
+
} finally {
|
|
91
|
+
rl.close();
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
//#endregion
|
|
95
|
+
//#region src/auth/index.ts
|
|
96
|
+
const logger = console;
|
|
97
|
+
/**
|
|
98
|
+
* Validate Dokploy token by making a test API call
|
|
99
|
+
*/
|
|
100
|
+
async function validateDokployToken(endpoint, token) {
|
|
101
|
+
const { DokployApi } = await Promise.resolve().then(() => dokploy_api_exports);
|
|
102
|
+
return new DokployApi({
|
|
103
|
+
baseUrl: endpoint,
|
|
104
|
+
token
|
|
105
|
+
}).validateToken();
|
|
106
|
+
}
|
|
107
|
+
/** What answers a login prompt without a terminal. */
|
|
108
|
+
const LOGIN_INSTEAD = "Please provide --token option.";
|
|
109
|
+
/**
|
|
110
|
+
* Login to a service
|
|
111
|
+
*/
|
|
112
|
+
async function loginCommand(options) {
|
|
113
|
+
const { provider, token: providedToken, endpoint: providedEndpoint } = options;
|
|
114
|
+
if (provider === "dokploy") {
|
|
115
|
+
logger.log("\n🔐 Logging in to Dokploy...\n");
|
|
116
|
+
let endpoint = providedEndpoint;
|
|
117
|
+
if (!endpoint) endpoint = await prompt("Dokploy URL (e.g., https://dokploy.example.com): ", { instead: LOGIN_INSTEAD });
|
|
118
|
+
endpoint = endpoint.replace(/\/$/, "");
|
|
119
|
+
try {
|
|
120
|
+
new URL(endpoint);
|
|
121
|
+
} catch {
|
|
122
|
+
logger.error("Invalid URL format");
|
|
123
|
+
process.exit(1);
|
|
124
|
+
}
|
|
125
|
+
let token = providedToken;
|
|
126
|
+
if (!token) {
|
|
127
|
+
logger.log(`\nGenerate a token at: ${endpoint}/settings/profile\n`);
|
|
128
|
+
token = await prompt("API Token: ", {
|
|
129
|
+
hidden: true,
|
|
130
|
+
instead: LOGIN_INSTEAD
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
if (!token) {
|
|
134
|
+
logger.error("Token is required");
|
|
135
|
+
process.exit(1);
|
|
136
|
+
}
|
|
137
|
+
logger.log("\nValidating credentials...");
|
|
138
|
+
if (!await validateDokployToken(endpoint, token)) {
|
|
139
|
+
logger.error("\n✗ Invalid credentials. Please check your token and try again.");
|
|
140
|
+
process.exit(1);
|
|
141
|
+
}
|
|
142
|
+
await require_credentials.storeDokployCredentials(token, endpoint);
|
|
143
|
+
logger.log("\n✓ Successfully logged in to Dokploy!");
|
|
144
|
+
logger.log(` Endpoint: ${endpoint}`);
|
|
145
|
+
logger.log(` Credentials stored in: ${require_credentials.getCredentialsPath()}`);
|
|
146
|
+
logger.log("\nYou can now use deploy commands without setting DOKPLOY_API_TOKEN.");
|
|
147
|
+
}
|
|
148
|
+
if (provider === "hostinger") {
|
|
149
|
+
logger.log("\n🔐 Logging in to Hostinger...\n");
|
|
150
|
+
let token = providedToken;
|
|
151
|
+
if (!token) {
|
|
152
|
+
logger.log("\nGenerate a token at: https://hpanel.hostinger.com/profile/api\n");
|
|
153
|
+
token = await prompt("API Token: ", {
|
|
154
|
+
hidden: true,
|
|
155
|
+
instead: LOGIN_INSTEAD
|
|
156
|
+
});
|
|
157
|
+
}
|
|
158
|
+
if (!token) {
|
|
159
|
+
logger.error("Token is required");
|
|
160
|
+
process.exit(1);
|
|
161
|
+
}
|
|
162
|
+
await require_credentials.storeHostingerToken(token);
|
|
163
|
+
logger.log("\n✓ Hostinger token stored.");
|
|
164
|
+
logger.log(` Credentials stored in: ${require_credentials.getCredentialsPath()}`);
|
|
165
|
+
logger.log(" Not validated here: every DNS endpoint is scoped to a domain, so the\n first deploy that touches DNS is where a bad token reports itself.");
|
|
166
|
+
logger.log("\nYou can now use it as a DNS provider without setting HOSTINGER_API_TOKEN.");
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
/**
|
|
170
|
+
* Logout from a service
|
|
171
|
+
*/
|
|
172
|
+
async function logoutCommand(options) {
|
|
173
|
+
const { provider = "dokploy" } = options;
|
|
174
|
+
if (provider === "all") {
|
|
175
|
+
if (await require_credentials.removeDokployCredentials()) logger.log("\n✓ Logged out from all services");
|
|
176
|
+
else logger.log("\nNo stored credentials found");
|
|
177
|
+
return;
|
|
178
|
+
}
|
|
179
|
+
if (provider === "dokploy") {
|
|
180
|
+
if (await require_credentials.removeDokployCredentials()) logger.log("\n✓ Logged out from Dokploy");
|
|
181
|
+
else logger.log("\nNo Dokploy credentials found");
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
/**
|
|
185
|
+
* Show current login status
|
|
186
|
+
*/
|
|
187
|
+
async function whoamiCommand() {
|
|
188
|
+
logger.log("\n📋 Current credentials:\n");
|
|
189
|
+
const dokploy = await require_credentials.getDokployCredentials();
|
|
190
|
+
if (dokploy) {
|
|
191
|
+
logger.log(" Dokploy:");
|
|
192
|
+
logger.log(` Endpoint: ${dokploy.endpoint}`);
|
|
193
|
+
logger.log(` Token: ${maskToken(dokploy.token)}`);
|
|
194
|
+
} else logger.log(" Dokploy: Not logged in");
|
|
195
|
+
logger.log(`\n Credentials file: ${require_credentials.getCredentialsPath()}`);
|
|
196
|
+
}
|
|
197
|
+
/**
|
|
198
|
+
* Mask a token for display
|
|
199
|
+
*/
|
|
200
|
+
function maskToken(token) {
|
|
201
|
+
if (token.length <= 8) return "****";
|
|
202
|
+
return `${token.slice(0, 4)}...${token.slice(-4)}`;
|
|
203
|
+
}
|
|
204
|
+
//#endregion
|
|
205
|
+
//#region src/workspace/client-generator.ts
|
|
206
|
+
/**
|
|
207
|
+
* Normalize routes to an array of patterns.
|
|
208
|
+
* Handles a glob or a list of them.
|
|
209
|
+
* @internal Exported for use in dev command
|
|
210
|
+
*/
|
|
211
|
+
function normalizeRoutes(routes) {
|
|
212
|
+
if (!routes) return [];
|
|
213
|
+
return Array.isArray(routes) ? routes : [routes];
|
|
214
|
+
}
|
|
215
|
+
//#endregion
|
|
216
|
+
//#region src/secrets/encryption.ts
|
|
217
|
+
var encryption_exports = /* @__PURE__ */ require_rolldown_runtime.__exportAll({
|
|
218
|
+
encryptSecrets: () => encryptSecrets,
|
|
219
|
+
generateDefineOptions: () => generateDefineOptions,
|
|
220
|
+
keyFingerprint: () => keyFingerprint
|
|
221
|
+
});
|
|
222
|
+
/** AES-256-GCM configuration */
|
|
223
|
+
const ALGORITHM = "aes-256-gcm";
|
|
224
|
+
const KEY_LENGTH = 32;
|
|
225
|
+
const IV_LENGTH = 12;
|
|
226
|
+
/**
|
|
227
|
+
* Encrypt secrets for embedding in a bundle.
|
|
228
|
+
* Uses AES-256-GCM with a randomly generated ephemeral key.
|
|
229
|
+
*
|
|
230
|
+
* @param secrets - Key-value pairs to encrypt
|
|
231
|
+
* @returns Encrypted payload with ephemeral master key
|
|
232
|
+
*/
|
|
233
|
+
function encryptSecrets(secrets) {
|
|
234
|
+
const masterKey = (0, node_crypto.randomBytes)(KEY_LENGTH);
|
|
235
|
+
const iv = (0, node_crypto.randomBytes)(IV_LENGTH);
|
|
236
|
+
const plaintext = JSON.stringify(secrets);
|
|
237
|
+
const cipher = (0, node_crypto.createCipheriv)(ALGORITHM, masterKey, iv);
|
|
238
|
+
const ciphertext = Buffer.concat([cipher.update(plaintext, "utf-8"), cipher.final()]);
|
|
239
|
+
const authTag = cipher.getAuthTag();
|
|
240
|
+
return {
|
|
241
|
+
encrypted: Buffer.concat([ciphertext, authTag]).toString("base64"),
|
|
242
|
+
iv: iv.toString("hex"),
|
|
243
|
+
masterKey: masterKey.toString("hex")
|
|
244
|
+
};
|
|
245
|
+
}
|
|
246
|
+
/**
|
|
247
|
+
* Generate the define options for tsdown/esbuild.
|
|
248
|
+
* These will be injected at build time.
|
|
249
|
+
*/
|
|
250
|
+
function generateDefineOptions(payload) {
|
|
251
|
+
return {
|
|
252
|
+
__GKM_ENCRYPTED_CREDENTIALS__: JSON.stringify(payload.encrypted),
|
|
253
|
+
__GKM_CREDENTIALS_IV__: JSON.stringify(payload.iv)
|
|
254
|
+
};
|
|
255
|
+
}
|
|
256
|
+
/**
|
|
257
|
+
* A short, one-way name for a master key: the first 8 hex characters of its
|
|
258
|
+
* SHA-256.
|
|
259
|
+
*
|
|
260
|
+
* Printed where the key itself used to be, so a person can tell which key a
|
|
261
|
+
* build used — and match it against the one they hold — without the output
|
|
262
|
+
* carrying a value that decrypts every secret of the stage.
|
|
263
|
+
*/
|
|
264
|
+
function keyFingerprint(masterKey) {
|
|
265
|
+
return (0, node_crypto.createHash)("sha256").update(masterKey).digest("hex").slice(0, 8);
|
|
266
|
+
}
|
|
267
|
+
//#endregion
|
|
268
|
+
//#region src/target/secrets.ts
|
|
269
|
+
/**
|
|
270
|
+
* A stage's secrets as a target sees them: read and written through the
|
|
271
|
+
* stage's own store, and masked in everything the run prints once read.
|
|
272
|
+
*/
|
|
273
|
+
/**
|
|
274
|
+
* Shorter values are left alone: what a progress line legitimately prints —
|
|
275
|
+
* a port, a stage, an app name — would otherwise come out as `***` wherever
|
|
276
|
+
* a secret happened to share it.
|
|
277
|
+
*/
|
|
278
|
+
const SHORTEST_MASKED = 6;
|
|
279
|
+
/** The values a run must never print, and the rewrite that keeps it so. */
|
|
280
|
+
var Redactor = class {
|
|
281
|
+
values = /* @__PURE__ */ new Set();
|
|
282
|
+
add(value) {
|
|
283
|
+
if (value && value.length >= SHORTEST_MASKED) this.values.add(value);
|
|
284
|
+
}
|
|
285
|
+
/** `text` with every masked value replaced by `***`, longest first. */
|
|
286
|
+
redact(text) {
|
|
287
|
+
if (this.values.size === 0) return text;
|
|
288
|
+
let out = text;
|
|
289
|
+
const longestFirst = [...this.values].sort((a, b) => b.length - a.length);
|
|
290
|
+
for (const value of longestFirst) out = out.replaceAll(value, "***");
|
|
291
|
+
return out;
|
|
292
|
+
}
|
|
293
|
+
};
|
|
294
|
+
/** Every value in `secrets` that is a secret rather than a label. */
|
|
295
|
+
function secretValues(secrets) {
|
|
296
|
+
return [
|
|
297
|
+
secrets.seed,
|
|
298
|
+
...Object.values(secrets.custom ?? {}),
|
|
299
|
+
...Object.values(secrets.urls ?? {}),
|
|
300
|
+
...Object.values(secrets.services ?? {}).map((service) => service?.password)
|
|
301
|
+
].filter((value) => typeof value === "string");
|
|
302
|
+
}
|
|
303
|
+
/**
|
|
304
|
+
* The stage's secrets through the store `secrets.store` names for it. Opening
|
|
305
|
+
* one builds a client and makes no request, so a target that never reads them
|
|
306
|
+
* costs nothing.
|
|
307
|
+
*/
|
|
308
|
+
async function deploySecrets(workspace, stage, redactor, options = {}) {
|
|
309
|
+
const store = await require_store.secretsStoreFor(workspace, stage, options);
|
|
310
|
+
const mask = (secrets) => {
|
|
311
|
+
for (const value of secretValues(secrets)) redactor.add(value);
|
|
312
|
+
};
|
|
313
|
+
return {
|
|
314
|
+
store: store.name,
|
|
315
|
+
async read() {
|
|
316
|
+
const secrets = await store.read(stage);
|
|
317
|
+
if (secrets) mask(secrets);
|
|
318
|
+
return secrets;
|
|
319
|
+
},
|
|
320
|
+
async write(secrets) {
|
|
321
|
+
mask(secrets);
|
|
322
|
+
await store.write(stage, secrets);
|
|
323
|
+
},
|
|
324
|
+
mask: (value) => redactor.add(value)
|
|
325
|
+
};
|
|
326
|
+
}
|
|
327
|
+
//#endregion
|
|
328
|
+
//#region src/deploy/events.ts
|
|
329
|
+
/** An error as an event carries it: its name and message, nothing else. */
|
|
330
|
+
function eventError(error) {
|
|
331
|
+
if (error instanceof Error) return {
|
|
332
|
+
name: error.name,
|
|
333
|
+
message: error.message
|
|
334
|
+
};
|
|
335
|
+
return {
|
|
336
|
+
name: "Error",
|
|
337
|
+
message: String(error)
|
|
338
|
+
};
|
|
339
|
+
}
|
|
340
|
+
//#endregion
|
|
341
|
+
//#region src/deploy/StateProvider.ts
|
|
342
|
+
/**
|
|
343
|
+
* Check if value is a StateProvider implementation.
|
|
344
|
+
*/
|
|
345
|
+
function isStateProvider(value) {
|
|
346
|
+
return typeof value === "object" && value !== null && typeof value.read === "function" && typeof value.write === "function";
|
|
347
|
+
}
|
|
348
|
+
//#endregion
|
|
349
|
+
//#region src/deploy/StateStore.ts
|
|
350
|
+
/**
|
|
351
|
+
* Deploy state store.
|
|
352
|
+
*
|
|
353
|
+
* `StateProvider` reads and writes a whole stage at once, with nothing to stop
|
|
354
|
+
* two runs of the same stage from overwriting each other and nowhere to record
|
|
355
|
+
* a resource the moment it exists. A `StateStore` adds the three things a
|
|
356
|
+
* deploy needs to be safe against crashes and concurrent runs:
|
|
357
|
+
*
|
|
358
|
+
* - `lock(stage)`: one run per stage. A second gets `StateLocked`, naming the
|
|
359
|
+
* holder, instead of racing the first.
|
|
360
|
+
* - Versioned `read` and conditional `write`: a write names the version it was
|
|
361
|
+
* based on, and raises `StateVersionConflict` when the stored state moved on
|
|
362
|
+
* underneath it, so nothing is lost silently even without the lock.
|
|
363
|
+
* - Per-resource records: `putResource` journals one resource at a time, so a
|
|
364
|
+
* run that dies half way leaves behind the ids of everything it created.
|
|
365
|
+
*
|
|
366
|
+
* Built-in stores: `LocalStateStore`, `SSMStateStore`, `S3StateStore`. A
|
|
367
|
+
* custom `StateProvider` is wrapped by `LegacyStateStore`, which keeps it
|
|
368
|
+
* working but cannot lock.
|
|
369
|
+
*/
|
|
370
|
+
var StateLocked = class extends Error {
|
|
371
|
+
stage;
|
|
372
|
+
holder;
|
|
373
|
+
location;
|
|
374
|
+
constructor(stage, holder, location) {
|
|
375
|
+
const by = holder ? `${holder.owner}@${holder.host} (pid ${holder.pid}) since ${holder.acquiredAt}${holder.operation ? ` for '${holder.operation}'` : ""}` : "a run that did not record who it was";
|
|
376
|
+
super(`Deploy state for stage '${stage}' is locked by ${by} (${location}). Wait for that run to finish; if it crashed, release the lock with \`gkm state:unlock --stage ${stage}\`.`);
|
|
377
|
+
this.stage = stage;
|
|
378
|
+
this.holder = holder;
|
|
379
|
+
this.location = location;
|
|
380
|
+
this.name = "StateLocked";
|
|
381
|
+
}
|
|
382
|
+
};
|
|
383
|
+
var StateVersionConflict = class extends Error {
|
|
384
|
+
stage;
|
|
385
|
+
expectedVersion;
|
|
386
|
+
actualVersion;
|
|
387
|
+
constructor(stage, expectedVersion, actualVersion) {
|
|
388
|
+
super(`Deploy state for stage '${stage}' changed since it was read (expected ${expectedVersion ? `version ${expectedVersion}` : "no state"}, found ${actualVersion ? `version ${actualVersion}` : "no state"}). Another run wrote it: read the state again and re-apply the change, and hold the stage's lock so runs take turns.`);
|
|
389
|
+
this.stage = stage;
|
|
390
|
+
this.expectedVersion = expectedVersion;
|
|
391
|
+
this.actualVersion = actualVersion;
|
|
392
|
+
this.name = "StateVersionConflict";
|
|
393
|
+
}
|
|
394
|
+
};
|
|
395
|
+
var StageStateMissing = class extends Error {
|
|
396
|
+
stage;
|
|
397
|
+
constructor(stage) {
|
|
398
|
+
super(`There is no deploy state for stage '${stage}' to record a resource in. Write the stage's state first (write with expectedVersion: null).`);
|
|
399
|
+
this.stage = stage;
|
|
400
|
+
this.name = "StageStateMissing";
|
|
401
|
+
}
|
|
402
|
+
};
|
|
403
|
+
var StateUnreadable = class extends Error {
|
|
404
|
+
stage;
|
|
405
|
+
location;
|
|
406
|
+
constructor(stage, location, cause) {
|
|
407
|
+
super(`Deploy state for stage '${stage}' at ${location} is not valid JSON. Restore it from a backup (or the provider's history) rather than deploying without it — a deploy without state recreates every resource.`, { cause });
|
|
408
|
+
this.stage = stage;
|
|
409
|
+
this.location = location;
|
|
410
|
+
this.name = "StateUnreadable";
|
|
411
|
+
}
|
|
412
|
+
};
|
|
413
|
+
var StateSchemaTooNew = class extends Error {
|
|
414
|
+
stage;
|
|
415
|
+
schemaVersion;
|
|
416
|
+
constructor(stage, schemaVersion) {
|
|
417
|
+
super(`Deploy state for stage '${stage}' has schema version ${String(schemaVersion)}, newer than this @geekmidas/cli understands (2). Upgrade @geekmidas/cli to the version that wrote it.`);
|
|
418
|
+
this.stage = stage;
|
|
419
|
+
this.schemaVersion = schemaVersion;
|
|
420
|
+
this.name = "StateSchemaTooNew";
|
|
421
|
+
}
|
|
422
|
+
};
|
|
423
|
+
/**
|
|
424
|
+
* Not thrown: emitted (once per store) when a custom `StateProvider` stands
|
|
425
|
+
* in for a store, so a deploy that cannot be protected from a concurrent one
|
|
426
|
+
* says so.
|
|
427
|
+
*/
|
|
428
|
+
var StateStoreWithoutLocking = class extends Error {
|
|
429
|
+
provider;
|
|
430
|
+
constructor(provider) {
|
|
431
|
+
super(`The custom state provider '${provider}' has no lock and no versions, so two deploys of the same stage can still overwrite each other. Implement StateStore (lock, versioned read, conditional write) or use the 'local', 'ssm' or 's3' state provider.`);
|
|
432
|
+
this.provider = provider;
|
|
433
|
+
this.name = "StateStoreWithoutLocking";
|
|
434
|
+
}
|
|
435
|
+
};
|
|
436
|
+
var StateStoreNeedsWorkspaceName = class extends Error {
|
|
437
|
+
provider;
|
|
438
|
+
constructor(provider) {
|
|
439
|
+
super(`The '${provider}' state provider keys state by workspace name. Set "name" in gkm.config.ts.`);
|
|
440
|
+
this.provider = provider;
|
|
441
|
+
this.name = "StateStoreNeedsWorkspaceName";
|
|
442
|
+
}
|
|
443
|
+
};
|
|
444
|
+
var UnknownStateProvider = class extends Error {
|
|
445
|
+
config;
|
|
446
|
+
constructor(config) {
|
|
447
|
+
super(`Unknown state provider ${JSON.stringify(config)}. Use 'local', 'ssm', 's3', or an object implementing StateStore or StateProvider.`);
|
|
448
|
+
this.config = config;
|
|
449
|
+
this.name = "UnknownStateProvider";
|
|
450
|
+
}
|
|
451
|
+
};
|
|
452
|
+
/** A lock or write that could not get at the state backend in time. */
|
|
453
|
+
var StateStoreBusy = class extends Error {
|
|
454
|
+
stage;
|
|
455
|
+
location;
|
|
456
|
+
constructor(stage, location) {
|
|
457
|
+
super(`Timed out waiting to write deploy state for stage '${stage}' (${location}). If no other gkm process is running, remove ${location}.`);
|
|
458
|
+
this.stage = stage;
|
|
459
|
+
this.location = location;
|
|
460
|
+
this.name = "StateStoreBusy";
|
|
461
|
+
}
|
|
462
|
+
};
|
|
463
|
+
function decodeDocument(stage, body, location) {
|
|
464
|
+
let parsed;
|
|
465
|
+
try {
|
|
466
|
+
parsed = JSON.parse(body);
|
|
467
|
+
} catch (error) {
|
|
468
|
+
throw new StateUnreadable(stage, location, error);
|
|
469
|
+
}
|
|
470
|
+
if (typeof parsed !== "object" || parsed === null) throw new StateUnreadable(stage, location, parsed);
|
|
471
|
+
const schemaVersion = parsed.schemaVersion;
|
|
472
|
+
if (schemaVersion === void 0) return {
|
|
473
|
+
schemaVersion: 1,
|
|
474
|
+
state: parsed
|
|
475
|
+
};
|
|
476
|
+
if (schemaVersion === 2) return {
|
|
477
|
+
schemaVersion: 2,
|
|
478
|
+
document: parsed
|
|
479
|
+
};
|
|
480
|
+
throw new StateSchemaTooNew(stage, schemaVersion);
|
|
481
|
+
}
|
|
482
|
+
function encodeDocument(document) {
|
|
483
|
+
return JSON.stringify(document, null, 2);
|
|
484
|
+
}
|
|
485
|
+
/**
|
|
486
|
+
* v1 state as a v2 document. The ids v1 kept are seeded as `ready` records,
|
|
487
|
+
* so the first journaled deploy after an upgrade adopts them rather than
|
|
488
|
+
* treating them as unknown.
|
|
489
|
+
*/
|
|
490
|
+
function migrateV1(stage, state) {
|
|
491
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
492
|
+
const resources = {};
|
|
493
|
+
const seed = (type, name, id) => {
|
|
494
|
+
if (!id) return;
|
|
495
|
+
const key = name ? `${type}:${name}` : type;
|
|
496
|
+
resources[key] = {
|
|
497
|
+
key,
|
|
498
|
+
type,
|
|
499
|
+
id,
|
|
500
|
+
status: "ready",
|
|
501
|
+
updatedAt: now
|
|
502
|
+
};
|
|
503
|
+
};
|
|
504
|
+
seed("project", null, state.projectId);
|
|
505
|
+
seed("environment", null, state.environmentId);
|
|
506
|
+
for (const [app, id] of Object.entries(state.applications ?? {})) seed("application", app, id);
|
|
507
|
+
seed("postgres", null, state.services?.postgresId);
|
|
508
|
+
seed("redis", null, state.services?.redisId);
|
|
509
|
+
seed("backup-destination", null, state.backups?.destinationId);
|
|
510
|
+
return {
|
|
511
|
+
schemaVersion: 2,
|
|
512
|
+
stage,
|
|
513
|
+
serial: 1,
|
|
514
|
+
state,
|
|
515
|
+
resources,
|
|
516
|
+
updatedAt: now
|
|
517
|
+
};
|
|
518
|
+
}
|
|
519
|
+
function newLockHolder(options) {
|
|
520
|
+
let owner = "unknown";
|
|
521
|
+
try {
|
|
522
|
+
owner = (0, node_os.userInfo)().username;
|
|
523
|
+
} catch {}
|
|
524
|
+
return {
|
|
525
|
+
id: (0, node_crypto.randomUUID)(),
|
|
526
|
+
owner,
|
|
527
|
+
host: (0, node_os.hostname)(),
|
|
528
|
+
pid: process.pid,
|
|
529
|
+
acquiredAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
530
|
+
operation: options?.operation
|
|
531
|
+
};
|
|
532
|
+
}
|
|
533
|
+
function contentVersion(body) {
|
|
534
|
+
return (0, node_crypto.createHash)("sha256").update(body).digest("hex").slice(0, 16);
|
|
535
|
+
}
|
|
536
|
+
/**
|
|
537
|
+
* A store over a backend that can read and conditionally write one blob per
|
|
538
|
+
* stage, and create a lock object only if it does not exist. Migration and
|
|
539
|
+
* resource records are the same for every such backend, so they live here.
|
|
540
|
+
*/
|
|
541
|
+
var DocumentStateStore = class {
|
|
542
|
+
async lock(stage, options) {
|
|
543
|
+
const holder = newLockHolder(options);
|
|
544
|
+
await this.createLock(stage, holder);
|
|
545
|
+
let released = false;
|
|
546
|
+
return {
|
|
547
|
+
stage,
|
|
548
|
+
holder,
|
|
549
|
+
release: async () => {
|
|
550
|
+
if (released) return;
|
|
551
|
+
released = true;
|
|
552
|
+
await this.removeLock(stage, holder);
|
|
553
|
+
}
|
|
554
|
+
};
|
|
555
|
+
}
|
|
556
|
+
async forceUnlock(stage) {
|
|
557
|
+
const holder = await this.readLock(stage);
|
|
558
|
+
await this.removeLock(stage, null);
|
|
559
|
+
return holder;
|
|
560
|
+
}
|
|
561
|
+
async read(stage) {
|
|
562
|
+
const current = await this.readDocument(stage);
|
|
563
|
+
if (!current) return null;
|
|
564
|
+
return {
|
|
565
|
+
state: current.document.state,
|
|
566
|
+
resources: current.document.resources,
|
|
567
|
+
version: current.version
|
|
568
|
+
};
|
|
569
|
+
}
|
|
570
|
+
async write(stage, state, options) {
|
|
571
|
+
if (options.expectedVersion === null) {
|
|
572
|
+
const document = {
|
|
573
|
+
schemaVersion: 2,
|
|
574
|
+
stage,
|
|
575
|
+
serial: 1,
|
|
576
|
+
state,
|
|
577
|
+
resources: {},
|
|
578
|
+
updatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
579
|
+
};
|
|
580
|
+
return this.writeRaw(stage, encodeDocument(document), null);
|
|
581
|
+
}
|
|
582
|
+
return this.mutate(stage, options.expectedVersion, (document) => ({
|
|
583
|
+
...document,
|
|
584
|
+
state
|
|
585
|
+
}));
|
|
586
|
+
}
|
|
587
|
+
async putResource(stage, record, options) {
|
|
588
|
+
return this.mutate(stage, options?.expectedVersion, (document) => ({
|
|
589
|
+
...document,
|
|
590
|
+
resources: {
|
|
591
|
+
...document.resources,
|
|
592
|
+
[record.key]: {
|
|
593
|
+
...record,
|
|
594
|
+
updatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
595
|
+
}
|
|
596
|
+
}
|
|
597
|
+
}));
|
|
598
|
+
}
|
|
599
|
+
async deleteResource(stage, key, options) {
|
|
600
|
+
return this.mutate(stage, options?.expectedVersion, (document) => {
|
|
601
|
+
const { [key]: _removed, ...resources } = document.resources;
|
|
602
|
+
return {
|
|
603
|
+
...document,
|
|
604
|
+
resources
|
|
605
|
+
};
|
|
606
|
+
});
|
|
607
|
+
}
|
|
608
|
+
async mutate(stage, expectedVersion, change) {
|
|
609
|
+
const current = await this.readDocument(stage);
|
|
610
|
+
if (!current) {
|
|
611
|
+
if (expectedVersion !== void 0) throw new StateVersionConflict(stage, expectedVersion, null);
|
|
612
|
+
throw new StageStateMissing(stage);
|
|
613
|
+
}
|
|
614
|
+
if (expectedVersion !== void 0 && current.version !== expectedVersion) throw new StateVersionConflict(stage, expectedVersion, current.version);
|
|
615
|
+
const next = change(current.document);
|
|
616
|
+
next.serial = current.document.serial + 1;
|
|
617
|
+
next.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
618
|
+
return this.writeRaw(stage, encodeDocument(next), current.version);
|
|
619
|
+
}
|
|
620
|
+
/**
|
|
621
|
+
* Reads the stage's document, migrating v1 in place on the way. The
|
|
622
|
+
* migration is itself a conditional write: if another run migrated (or
|
|
623
|
+
* wrote) first, its result is read instead.
|
|
624
|
+
*/
|
|
625
|
+
async readDocument(stage, attempt = 0) {
|
|
626
|
+
const raw = await this.readRaw(stage);
|
|
627
|
+
if (!raw) return null;
|
|
628
|
+
const decoded = decodeDocument(stage, raw.body, this.location(stage));
|
|
629
|
+
if (decoded.schemaVersion === 2) return {
|
|
630
|
+
document: decoded.document,
|
|
631
|
+
version: raw.version
|
|
632
|
+
};
|
|
633
|
+
await this.writeV1Backup(stage, raw.body);
|
|
634
|
+
const document = migrateV1(stage, decoded.state);
|
|
635
|
+
try {
|
|
636
|
+
return {
|
|
637
|
+
document,
|
|
638
|
+
version: await this.writeRaw(stage, encodeDocument(document), raw.version)
|
|
639
|
+
};
|
|
640
|
+
} catch (error) {
|
|
641
|
+
if (error instanceof StateVersionConflict && attempt < 2) return this.readDocument(stage, attempt + 1);
|
|
642
|
+
throw error;
|
|
643
|
+
}
|
|
644
|
+
}
|
|
645
|
+
};
|
|
646
|
+
function isStateStore(value) {
|
|
647
|
+
return typeof value === "object" && value !== null && typeof value.lock === "function" && typeof value.read === "function" && typeof value.write === "function" && typeof value.putResource === "function";
|
|
648
|
+
}
|
|
649
|
+
/**
|
|
650
|
+
* The store for a workspace's `state` config:
|
|
651
|
+
*
|
|
652
|
+
* - none / `'local'`: `LocalStateStore` under `.gkm/`
|
|
653
|
+
* - `'ssm'`: `SSMStateStore`, with no local cache — SSM is read directly, so
|
|
654
|
+
* a stale local copy can never win over it
|
|
655
|
+
* - `'s3'`: `S3StateStore`
|
|
656
|
+
* - a `StateStore` object: used as is
|
|
657
|
+
* - a `StateProvider` object: wrapped by `LegacyStateStore`
|
|
658
|
+
*/
|
|
659
|
+
async function createStateStore(options) {
|
|
660
|
+
const { config, workspaceRoot, workspaceName } = options;
|
|
661
|
+
if (!config || config.provider === "local") {
|
|
662
|
+
const { LocalStateStore } = await Promise.resolve().then(() => require("./LocalStateStore-93urXyQe.cjs")).then((n) => n.LocalStateStore_exports);
|
|
663
|
+
return new LocalStateStore(workspaceRoot);
|
|
664
|
+
}
|
|
665
|
+
if (isStateStore(config.provider)) return config.provider;
|
|
666
|
+
if (isStateProvider(config.provider)) {
|
|
667
|
+
const { LegacyStateStore } = await Promise.resolve().then(() => require("./LegacyStateStore-BGoo__sO.cjs"));
|
|
668
|
+
return new LegacyStateStore(config.provider, { warn: options.warn });
|
|
669
|
+
}
|
|
670
|
+
if (config.provider === "ssm") {
|
|
671
|
+
if (!workspaceName) throw new StateStoreNeedsWorkspaceName("ssm");
|
|
672
|
+
const { SSMStateStore } = await Promise.resolve().then(() => require("./SSMStateStore-Bt9acuAF.cjs"));
|
|
673
|
+
const ssm = config;
|
|
674
|
+
return SSMStateStore.create({
|
|
675
|
+
workspaceName,
|
|
676
|
+
region: ssm.region,
|
|
677
|
+
profile: ssm.profile
|
|
678
|
+
});
|
|
679
|
+
}
|
|
680
|
+
if (config.provider === "s3") {
|
|
681
|
+
if (!workspaceName) throw new StateStoreNeedsWorkspaceName("s3");
|
|
682
|
+
const { S3StateStore } = await Promise.resolve().then(() => require("./S3StateStore-F4dxw6kS.cjs"));
|
|
683
|
+
const s3 = config;
|
|
684
|
+
return S3StateStore.create({
|
|
685
|
+
workspaceName,
|
|
686
|
+
bucket: s3.bucket,
|
|
687
|
+
prefix: s3.prefix,
|
|
688
|
+
region: s3.region,
|
|
689
|
+
profile: s3.profile
|
|
690
|
+
});
|
|
691
|
+
}
|
|
692
|
+
throw new UnknownStateProvider(config);
|
|
693
|
+
}
|
|
694
|
+
//#endregion
|
|
695
|
+
//#region src/deploy/orchestrate.ts
|
|
696
|
+
/**
|
|
697
|
+
* The run around a target: what every deploy does whatever it deploys
|
|
698
|
+
* through.
|
|
699
|
+
*
|
|
700
|
+
* It loads the workspace, resolves the target, works out the apps and the
|
|
701
|
+
* identity, takes the stage's lock, and calls the target's phases in order,
|
|
702
|
+
* reporting each as it starts, finishes or fails. Everything that is about
|
|
703
|
+
* *where* the stage goes is the target's.
|
|
704
|
+
*/
|
|
705
|
+
/** Apps were asked for by name that the workspace does not have. */
|
|
706
|
+
var UnknownDeployApps = class extends Error {
|
|
707
|
+
apps;
|
|
708
|
+
available;
|
|
709
|
+
constructor(apps, available) {
|
|
710
|
+
super(`Unknown apps: ${apps.join(", ")}\nAvailable apps: ${available.join(", ")}`);
|
|
711
|
+
this.apps = apps;
|
|
712
|
+
this.available = available;
|
|
713
|
+
this.name = "UnknownDeployApps";
|
|
714
|
+
}
|
|
715
|
+
};
|
|
716
|
+
/** Every app asked for deploys through some other target. */
|
|
717
|
+
var NoDeployableApps = class extends Error {
|
|
718
|
+
stage;
|
|
719
|
+
target;
|
|
720
|
+
constructor(stage, target) {
|
|
721
|
+
super(`No apps to deploy through "${target}": every selected app deploys somewhere else. Pick the target with --target, or set each app's deploy in gkm.config.ts.`);
|
|
722
|
+
this.stage = stage;
|
|
723
|
+
this.target = target;
|
|
724
|
+
this.name = "NoDeployableApps";
|
|
725
|
+
}
|
|
726
|
+
};
|
|
727
|
+
/** A failed rollback, after the failure that called for it. */
|
|
728
|
+
var RollbackFailed = class extends Error {
|
|
729
|
+
target;
|
|
730
|
+
failure;
|
|
731
|
+
constructor(target, failure, cause) {
|
|
732
|
+
super(`Deploying through "${target}" failed, and so did rolling it back: ${cause instanceof Error ? cause.message : String(cause)}. The stage may be part-released; deploy again, or roll it back by hand. The first failure: ${failure instanceof Error ? failure.message : String(failure)}`, { cause });
|
|
733
|
+
this.target = target;
|
|
734
|
+
this.failure = failure;
|
|
735
|
+
this.name = "RollbackFailed";
|
|
736
|
+
}
|
|
737
|
+
};
|
|
738
|
+
/**
|
|
739
|
+
* Generate image tag from stage and timestamp
|
|
740
|
+
*/
|
|
741
|
+
function generateTag(stage) {
|
|
742
|
+
return `${stage}-${(/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-").slice(0, 19)}`;
|
|
743
|
+
}
|
|
744
|
+
/** Why an app that deploys through another target is left out. */
|
|
745
|
+
function elsewhere(target, stage) {
|
|
746
|
+
const instead = require_define.builtinTarget(target)?.instead?.(stage);
|
|
747
|
+
if (instead) return `it deploys through "${target}" — ${instead}`;
|
|
748
|
+
return `it deploys through "${target}" — run \`gkm deploy --target ${target} --stage ${stage}\``;
|
|
749
|
+
}
|
|
750
|
+
/**
|
|
751
|
+
* Deploy a workspace's stage through its target.
|
|
752
|
+
*
|
|
753
|
+
* The engine under `deploy()` and `gkm deploy`. It never prompts and never
|
|
754
|
+
* exits: credentials come from `ctx.credentials`, progress goes to
|
|
755
|
+
* `ctx.emit` and to `output`, and every failure is thrown.
|
|
756
|
+
*
|
|
757
|
+
* @internal
|
|
758
|
+
*/
|
|
759
|
+
async function runDeploy(source, request, ctx) {
|
|
760
|
+
const { stage } = request;
|
|
761
|
+
const redactor = ctx.redactor ?? new Redactor();
|
|
762
|
+
const emit = (event) => ctx.emit(event.type === "log" ? {
|
|
763
|
+
...event,
|
|
764
|
+
message: redactor.redact(event.message)
|
|
765
|
+
} : event);
|
|
766
|
+
/** Run one phase between its events. */
|
|
767
|
+
const phase = async (name, body) => {
|
|
768
|
+
ctx.signal?.throwIfAborted();
|
|
769
|
+
emit({
|
|
770
|
+
type: "phase.started",
|
|
771
|
+
phase: name
|
|
772
|
+
});
|
|
773
|
+
try {
|
|
774
|
+
const value = await body();
|
|
775
|
+
emit({
|
|
776
|
+
type: "phase.finished",
|
|
777
|
+
phase: name
|
|
778
|
+
});
|
|
779
|
+
return value;
|
|
780
|
+
} catch (error) {
|
|
781
|
+
emit({
|
|
782
|
+
type: "phase.failed",
|
|
783
|
+
phase: name,
|
|
784
|
+
error: eventError(error)
|
|
785
|
+
});
|
|
786
|
+
throw error;
|
|
787
|
+
}
|
|
788
|
+
};
|
|
789
|
+
ctx.signal?.throwIfAborted();
|
|
790
|
+
emit({
|
|
791
|
+
type: "phase.started",
|
|
792
|
+
phase: "validate"
|
|
793
|
+
});
|
|
794
|
+
let prepared;
|
|
795
|
+
try {
|
|
796
|
+
prepared = await prepare(source, request, ctx, emit, redactor);
|
|
797
|
+
} catch (error) {
|
|
798
|
+
emit({
|
|
799
|
+
type: "phase.failed",
|
|
800
|
+
phase: "validate",
|
|
801
|
+
error: eventError(error)
|
|
802
|
+
});
|
|
803
|
+
throw error;
|
|
804
|
+
}
|
|
805
|
+
const { resolved, phaseCtx, store } = prepared;
|
|
806
|
+
const { target } = resolved;
|
|
807
|
+
if (ctx.dryRun) {
|
|
808
|
+
const run = await validate(target, phaseCtx, emit);
|
|
809
|
+
await phase("plan", () => target.plan(phaseCtx, run));
|
|
810
|
+
return target.result(phaseCtx, run);
|
|
811
|
+
}
|
|
812
|
+
let lock;
|
|
813
|
+
try {
|
|
814
|
+
lock = await store.lock(stage, { operation: "deploy" });
|
|
815
|
+
} catch (error) {
|
|
816
|
+
emit({
|
|
817
|
+
type: "phase.failed",
|
|
818
|
+
phase: "validate",
|
|
819
|
+
error: eventError(error)
|
|
820
|
+
});
|
|
821
|
+
throw error;
|
|
822
|
+
}
|
|
823
|
+
try {
|
|
824
|
+
const run = await validate(target, phaseCtx, emit);
|
|
825
|
+
if (target.provision) await phase("provision", () => target.provision(phaseCtx, run));
|
|
826
|
+
if (target.build) await phase("build", () => target.build(phaseCtx, run));
|
|
827
|
+
let releasing = "release";
|
|
828
|
+
try {
|
|
829
|
+
await phase("release", () => target.release(phaseCtx, run));
|
|
830
|
+
releasing = "verify";
|
|
831
|
+
if (target.verify) await phase("verify", () => target.verify(phaseCtx, run));
|
|
832
|
+
} catch (error) {
|
|
833
|
+
if (ctx.signal?.aborted || !target.capabilities.rollback) throw error;
|
|
834
|
+
if (!target.rollback) throw error;
|
|
835
|
+
try {
|
|
836
|
+
await phase("rollback", () => target.rollback(phaseCtx, run, {
|
|
837
|
+
phase: releasing,
|
|
838
|
+
error
|
|
839
|
+
}));
|
|
840
|
+
} catch (rollbackError) {
|
|
841
|
+
throw new RollbackFailed(resolved.name, error, rollbackError);
|
|
842
|
+
}
|
|
843
|
+
throw error;
|
|
844
|
+
}
|
|
845
|
+
return await target.result(phaseCtx, run);
|
|
846
|
+
} finally {
|
|
847
|
+
await lock.release();
|
|
848
|
+
}
|
|
849
|
+
}
|
|
850
|
+
/** `validate`, then the run's `deploy.started`, inside the validate phase. */
|
|
851
|
+
async function validate(target, phaseCtx, emit) {
|
|
852
|
+
try {
|
|
853
|
+
const run = await target.validate(phaseCtx);
|
|
854
|
+
const skipped = new Set(phaseCtx.skipped.map((s) => s.app));
|
|
855
|
+
emit({
|
|
856
|
+
type: "deploy.started",
|
|
857
|
+
stage: phaseCtx.stage,
|
|
858
|
+
target: phaseCtx.target,
|
|
859
|
+
identity: phaseCtx.identity.key,
|
|
860
|
+
tag: phaseCtx.tag,
|
|
861
|
+
apps: phaseCtx.apps.filter((app) => !skipped.has(app)),
|
|
862
|
+
dryRun: phaseCtx.dryRun
|
|
863
|
+
});
|
|
864
|
+
emit({
|
|
865
|
+
type: "phase.finished",
|
|
866
|
+
phase: "validate"
|
|
867
|
+
});
|
|
868
|
+
return run;
|
|
869
|
+
} catch (error) {
|
|
870
|
+
emit({
|
|
871
|
+
type: "phase.failed",
|
|
872
|
+
phase: "validate",
|
|
873
|
+
error: eventError(error)
|
|
874
|
+
});
|
|
875
|
+
throw error;
|
|
876
|
+
}
|
|
877
|
+
}
|
|
878
|
+
/** Everything the phases are handed, worked out before the first of them. */
|
|
879
|
+
async function prepare(source, request, ctx, emit, redactor) {
|
|
880
|
+
const { stage } = request;
|
|
881
|
+
const configured = typeof source === "function" ? await source() : source;
|
|
882
|
+
const local = stage === configured.stages.local;
|
|
883
|
+
if (!local) require_workspace.assertDeployedStage(configured.stages, stage);
|
|
884
|
+
const targetName = request.target ?? configured.deploy?.default ?? "dokploy";
|
|
885
|
+
const resolved = await require_resolve.resolveTarget(targetName, {
|
|
886
|
+
workspace: configured,
|
|
887
|
+
stage,
|
|
888
|
+
...ctx.targets ? { host: ctx.targets } : {}
|
|
889
|
+
});
|
|
890
|
+
if (local && !resolved.target.capabilities.localStage) require_workspace.assertDeployedStage(configured.stages, stage);
|
|
891
|
+
const options = await require_resolve.parseTargetOptions(resolved);
|
|
892
|
+
const manifest = await require_discover.discover({
|
|
893
|
+
patterns: require_state.constructGlobs(configured),
|
|
894
|
+
cwd: configured.root
|
|
895
|
+
});
|
|
896
|
+
const units = require_derive.derivedApps(manifest, configured);
|
|
897
|
+
const workspace = Object.keys(units).length > 0 ? {
|
|
898
|
+
...configured,
|
|
899
|
+
apps: units
|
|
900
|
+
} : configured;
|
|
901
|
+
const identity = require_store.deployIdentity(workspace, stage);
|
|
902
|
+
const tag = request.tag ?? (resolved.target.tag ? await resolved.target.tag({
|
|
903
|
+
cwd: workspace.root,
|
|
904
|
+
stage
|
|
905
|
+
}) : generateTag(stage));
|
|
906
|
+
const buildOrder = require_workspace.getAppBuildOrder(workspace);
|
|
907
|
+
let selected = buildOrder;
|
|
908
|
+
if (request.apps && request.apps.length > 0) {
|
|
909
|
+
const unknown = request.apps.filter((name) => !workspace.apps[name]);
|
|
910
|
+
if (unknown.length > 0) throw new UnknownDeployApps(unknown, Object.keys(workspace.apps));
|
|
911
|
+
selected = buildOrder.filter((name) => request.apps.includes(name));
|
|
912
|
+
}
|
|
913
|
+
const skipped = [];
|
|
914
|
+
const skip = (app, reason) => {
|
|
915
|
+
skipped.push({
|
|
916
|
+
app,
|
|
917
|
+
reason
|
|
918
|
+
});
|
|
919
|
+
emit({
|
|
920
|
+
type: "app.skipped",
|
|
921
|
+
app,
|
|
922
|
+
reason
|
|
923
|
+
});
|
|
924
|
+
};
|
|
925
|
+
const byDefault = configured.deploy?.default ?? "dokploy";
|
|
926
|
+
const apps = selected.filter((name) => {
|
|
927
|
+
const own = workspace.apps[name].resolvedDeployTarget;
|
|
928
|
+
const deploysThrough = own === byDefault ? targetName : own;
|
|
929
|
+
if (deploysThrough === targetName) return true;
|
|
930
|
+
skip(name, elsewhere(deploysThrough, stage));
|
|
931
|
+
return false;
|
|
932
|
+
});
|
|
933
|
+
if (apps.length === 0) throw new NoDeployableApps(stage, targetName);
|
|
934
|
+
const store = await createStateStore({
|
|
935
|
+
config: workspace.state,
|
|
936
|
+
workspaceRoot: workspace.root,
|
|
937
|
+
workspaceName: workspace.name
|
|
938
|
+
});
|
|
939
|
+
const secrets = await deploySecrets(workspace, stage, redactor, { ...ctx.home ? { home: ctx.home } : {} });
|
|
940
|
+
const line = (level) => (message) => emit({
|
|
941
|
+
type: "log",
|
|
942
|
+
level,
|
|
943
|
+
message
|
|
944
|
+
});
|
|
945
|
+
const logger = {
|
|
946
|
+
info: line("info"),
|
|
947
|
+
warn: line("warn"),
|
|
948
|
+
error: line("error")
|
|
949
|
+
};
|
|
950
|
+
return {
|
|
951
|
+
resolved,
|
|
952
|
+
phaseCtx: {
|
|
953
|
+
target: targetName,
|
|
954
|
+
cwd: workspace.root,
|
|
955
|
+
stage,
|
|
956
|
+
tag,
|
|
957
|
+
tagGiven: request.tag !== void 0,
|
|
958
|
+
apps,
|
|
959
|
+
skipped,
|
|
960
|
+
identity,
|
|
961
|
+
workspace,
|
|
962
|
+
manifest,
|
|
963
|
+
options,
|
|
964
|
+
dryRun: ctx.dryRun,
|
|
965
|
+
credentials: ctx.credentials,
|
|
966
|
+
state: store,
|
|
967
|
+
secrets,
|
|
968
|
+
logger,
|
|
969
|
+
signal: ctx.signal ?? new AbortController().signal,
|
|
970
|
+
childOutput: ctx.childOutput ?? "inherit",
|
|
971
|
+
emit,
|
|
972
|
+
skip,
|
|
973
|
+
name: (resource) => require_store.applicationName(identity, resource)
|
|
974
|
+
},
|
|
975
|
+
store
|
|
976
|
+
};
|
|
977
|
+
}
|
|
978
|
+
//#endregion
|
|
979
|
+
//#region src/deploy/dokploy-api.ts
|
|
980
|
+
var dokploy_api_exports = /* @__PURE__ */ require_rolldown_runtime.__exportAll({
|
|
981
|
+
DEFAULT_DOKPLOY_TIMEOUT_MS: () => DEFAULT_DOKPLOY_TIMEOUT_MS,
|
|
982
|
+
DokployApi: () => DokployApi,
|
|
983
|
+
DokployApiError: () => DokployApiError,
|
|
984
|
+
DokployRequestTimedOut: () => DokployRequestTimedOut
|
|
985
|
+
});
|
|
986
|
+
/** Per request, so a stalled server fails the deploy, not the CI job. */
|
|
987
|
+
const DEFAULT_DOKPLOY_TIMEOUT_MS = 3e4;
|
|
988
|
+
/**
|
|
989
|
+
* Dokploy accepted the connection but did not answer in time.
|
|
990
|
+
*
|
|
991
|
+
* Not retried: a request that arrived and stalled may still be acted on, and
|
|
992
|
+
* sending it again could repeat a deploy or a create the server is already
|
|
993
|
+
* doing.
|
|
994
|
+
*/
|
|
995
|
+
var DokployRequestTimedOut = class extends Error {
|
|
996
|
+
baseUrl;
|
|
997
|
+
endpoint;
|
|
998
|
+
timeoutMs;
|
|
999
|
+
constructor(baseUrl, endpoint, timeoutMs) {
|
|
1000
|
+
super(`Dokploy at ${baseUrl} did not answer ${endpoint} within ${timeoutMs}ms. Check that the Dokploy server is healthy, or raise \`timeoutMs\` if this call is known to be slow.`);
|
|
1001
|
+
this.baseUrl = baseUrl;
|
|
1002
|
+
this.endpoint = endpoint;
|
|
1003
|
+
this.timeoutMs = timeoutMs;
|
|
1004
|
+
this.name = "DokployRequestTimedOut";
|
|
1005
|
+
}
|
|
1006
|
+
};
|
|
1007
|
+
/**
|
|
1008
|
+
* Waits `ms` between retries, or rejects with the signal's reason the moment
|
|
1009
|
+
* it aborts — a caller who gave up should not sit through the backoff.
|
|
1010
|
+
*/
|
|
1011
|
+
function pause(ms, signal) {
|
|
1012
|
+
return new Promise((resolve, reject) => {
|
|
1013
|
+
if (signal?.aborted) {
|
|
1014
|
+
reject(signal.reason);
|
|
1015
|
+
return;
|
|
1016
|
+
}
|
|
1017
|
+
const onAbort = () => {
|
|
1018
|
+
clearTimeout(timer);
|
|
1019
|
+
reject(signal?.reason);
|
|
1020
|
+
};
|
|
1021
|
+
const timer = setTimeout(() => {
|
|
1022
|
+
signal?.removeEventListener("abort", onAbort);
|
|
1023
|
+
resolve();
|
|
1024
|
+
}, ms);
|
|
1025
|
+
signal?.addEventListener("abort", onAbort, { once: true });
|
|
1026
|
+
});
|
|
1027
|
+
}
|
|
1028
|
+
var DokployApiError = class extends Error {
|
|
1029
|
+
status;
|
|
1030
|
+
statusText;
|
|
1031
|
+
issues;
|
|
1032
|
+
constructor(message, status, statusText, issues) {
|
|
1033
|
+
super(message);
|
|
1034
|
+
this.status = status;
|
|
1035
|
+
this.statusText = statusText;
|
|
1036
|
+
this.issues = issues;
|
|
1037
|
+
this.name = "DokployApiError";
|
|
1038
|
+
}
|
|
1039
|
+
};
|
|
1040
|
+
/**
|
|
1041
|
+
* Dokploy API client
|
|
1042
|
+
*/
|
|
1043
|
+
var DokployApi = class {
|
|
1044
|
+
baseUrl;
|
|
1045
|
+
token;
|
|
1046
|
+
timeoutMs;
|
|
1047
|
+
signal;
|
|
1048
|
+
constructor(options) {
|
|
1049
|
+
this.baseUrl = options.baseUrl.replace(/\/$/, "");
|
|
1050
|
+
this.token = options.token;
|
|
1051
|
+
this.timeoutMs = options.timeoutMs ?? 3e4;
|
|
1052
|
+
this.signal = options.signal;
|
|
1053
|
+
}
|
|
1054
|
+
/**
|
|
1055
|
+
* Make a GET request to the Dokploy API
|
|
1056
|
+
*/
|
|
1057
|
+
async get(endpoint) {
|
|
1058
|
+
return this.request("GET", endpoint);
|
|
1059
|
+
}
|
|
1060
|
+
/**
|
|
1061
|
+
* Make a POST request to the Dokploy API
|
|
1062
|
+
*/
|
|
1063
|
+
async post(endpoint, body) {
|
|
1064
|
+
return this.request("POST", endpoint, body);
|
|
1065
|
+
}
|
|
1066
|
+
/**
|
|
1067
|
+
* Make a request to the Dokploy API
|
|
1068
|
+
*/
|
|
1069
|
+
async request(method, endpoint, body) {
|
|
1070
|
+
const url = `${this.baseUrl}/api/${endpoint}`;
|
|
1071
|
+
let response;
|
|
1072
|
+
let timeout;
|
|
1073
|
+
let lastError;
|
|
1074
|
+
for (let attempt = 1; attempt <= 3; attempt++) {
|
|
1075
|
+
this.signal?.throwIfAborted();
|
|
1076
|
+
timeout = AbortSignal.timeout(this.timeoutMs);
|
|
1077
|
+
try {
|
|
1078
|
+
response = await fetch(url, {
|
|
1079
|
+
method,
|
|
1080
|
+
headers: {
|
|
1081
|
+
"Content-Type": "application/json",
|
|
1082
|
+
"x-api-key": this.token
|
|
1083
|
+
},
|
|
1084
|
+
body: body ? JSON.stringify(body) : void 0,
|
|
1085
|
+
signal: this.signal ? AbortSignal.any([this.signal, timeout]) : timeout
|
|
1086
|
+
});
|
|
1087
|
+
break;
|
|
1088
|
+
} catch (error) {
|
|
1089
|
+
const stopped = this.stopped(timeout, endpoint);
|
|
1090
|
+
if (stopped) throw stopped;
|
|
1091
|
+
lastError = error;
|
|
1092
|
+
if (attempt === 3) break;
|
|
1093
|
+
await pause(attempt * 750, this.signal);
|
|
1094
|
+
}
|
|
1095
|
+
}
|
|
1096
|
+
if (!response || !timeout) {
|
|
1097
|
+
const detail = lastError instanceof Error ? lastError.message : String(lastError);
|
|
1098
|
+
throw new DokployApiError(`Could not reach Dokploy at ${this.baseUrl} (${endpoint}): ${detail}`, 0, "Network error");
|
|
1099
|
+
}
|
|
1100
|
+
try {
|
|
1101
|
+
return await this.read(response);
|
|
1102
|
+
} catch (error) {
|
|
1103
|
+
throw this.stopped(timeout, endpoint) ?? error;
|
|
1104
|
+
}
|
|
1105
|
+
}
|
|
1106
|
+
/**
|
|
1107
|
+
* What a request cut short should reject with: the caller's own reason
|
|
1108
|
+
* when they aborted, `DokployRequestTimedOut` when the deadline passed,
|
|
1109
|
+
* nothing when it was neither.
|
|
1110
|
+
*/
|
|
1111
|
+
stopped(timeout, endpoint) {
|
|
1112
|
+
if (this.signal?.aborted) return this.signal.reason;
|
|
1113
|
+
if (timeout.aborted) return new DokployRequestTimedOut(this.baseUrl, endpoint, this.timeoutMs);
|
|
1114
|
+
}
|
|
1115
|
+
/** Turns a response into its JSON body, or a `DokployApiError`. */
|
|
1116
|
+
async read(response) {
|
|
1117
|
+
if (!response.ok) {
|
|
1118
|
+
let errorMessage = `Dokploy API error: ${response.status} ${response.statusText}`;
|
|
1119
|
+
let issues;
|
|
1120
|
+
try {
|
|
1121
|
+
const errorBody = await response.json();
|
|
1122
|
+
if (errorBody.message) errorMessage = `Dokploy API error: ${errorBody.message}`;
|
|
1123
|
+
if (errorBody.issues?.length) {
|
|
1124
|
+
issues = errorBody.issues;
|
|
1125
|
+
errorMessage += `\n Issues: ${errorBody.issues.map((i) => i.message).join(", ")}`;
|
|
1126
|
+
}
|
|
1127
|
+
} catch {}
|
|
1128
|
+
throw new DokployApiError(errorMessage, response.status, response.statusText, issues);
|
|
1129
|
+
}
|
|
1130
|
+
const text = await response.text();
|
|
1131
|
+
if (!text || text.trim() === "") return;
|
|
1132
|
+
return JSON.parse(text);
|
|
1133
|
+
}
|
|
1134
|
+
/**
|
|
1135
|
+
* Every external port published on this server, across every project.
|
|
1136
|
+
*
|
|
1137
|
+
* Dokploy knows what it has bound; guessing does not. A port that looks free
|
|
1138
|
+
* from here can be held by a service in a project this deploy has never
|
|
1139
|
+
* heard of — which surfaces as `Port 5432 is already in use by container
|
|
1140
|
+
* "…"`, naming something the caller cannot see.
|
|
1141
|
+
*
|
|
1142
|
+
* `project.all` is a summary, so each project is fetched: the per-service
|
|
1143
|
+
* `externalPort` only appears on `project.one`.
|
|
1144
|
+
*/
|
|
1145
|
+
async publishedPorts() {
|
|
1146
|
+
const ports = /* @__PURE__ */ new Set();
|
|
1147
|
+
const projects = await this.listProjects();
|
|
1148
|
+
for (const project of projects) {
|
|
1149
|
+
const details = await this.getProject(project.projectId);
|
|
1150
|
+
for (const environment of details.environments ?? []) {
|
|
1151
|
+
const resources = [
|
|
1152
|
+
...environment.postgres ?? [],
|
|
1153
|
+
...environment.redis ?? [],
|
|
1154
|
+
...environment.applications ?? []
|
|
1155
|
+
];
|
|
1156
|
+
for (const resource of resources) if (typeof resource.externalPort === "number") ports.add(resource.externalPort);
|
|
1157
|
+
}
|
|
1158
|
+
}
|
|
1159
|
+
return ports;
|
|
1160
|
+
}
|
|
1161
|
+
/**
|
|
1162
|
+
* Validate the API token by making a test request
|
|
1163
|
+
*/
|
|
1164
|
+
async validateToken() {
|
|
1165
|
+
try {
|
|
1166
|
+
await this.get("project.all");
|
|
1167
|
+
return true;
|
|
1168
|
+
} catch {
|
|
1169
|
+
return false;
|
|
1170
|
+
}
|
|
1171
|
+
}
|
|
1172
|
+
/**
|
|
1173
|
+
* List all projects
|
|
1174
|
+
*/
|
|
1175
|
+
async listProjects() {
|
|
1176
|
+
return this.get("project.all");
|
|
1177
|
+
}
|
|
1178
|
+
/**
|
|
1179
|
+
* Get a single project by ID
|
|
1180
|
+
*/
|
|
1181
|
+
async getProject(projectId) {
|
|
1182
|
+
return this.get(`project.one?projectId=${projectId}`);
|
|
1183
|
+
}
|
|
1184
|
+
/**
|
|
1185
|
+
* Create a new project
|
|
1186
|
+
*/
|
|
1187
|
+
async createProject(name, description) {
|
|
1188
|
+
return this.post("project.create", {
|
|
1189
|
+
name,
|
|
1190
|
+
description: description ?? `Created by gkm CLI`
|
|
1191
|
+
});
|
|
1192
|
+
}
|
|
1193
|
+
/**
|
|
1194
|
+
* Change a project's name or description.
|
|
1195
|
+
*
|
|
1196
|
+
* The name is sent with the description because `project.update` validates
|
|
1197
|
+
* the whole project on some Dokploy versions, and a missing name fails it.
|
|
1198
|
+
*/
|
|
1199
|
+
async updateProject(projectId, updates) {
|
|
1200
|
+
await this.post("project.update", {
|
|
1201
|
+
projectId,
|
|
1202
|
+
...updates
|
|
1203
|
+
});
|
|
1204
|
+
}
|
|
1205
|
+
/**
|
|
1206
|
+
* Delete a project and all its resources
|
|
1207
|
+
*/
|
|
1208
|
+
async deleteProject(projectId) {
|
|
1209
|
+
await this.post("project.remove", { projectId });
|
|
1210
|
+
}
|
|
1211
|
+
/**
|
|
1212
|
+
* Create an environment in a project
|
|
1213
|
+
*/
|
|
1214
|
+
async createEnvironment(projectId, name, description) {
|
|
1215
|
+
return this.post("environment.create", {
|
|
1216
|
+
projectId,
|
|
1217
|
+
name,
|
|
1218
|
+
description: description ?? `${name} environment`
|
|
1219
|
+
});
|
|
1220
|
+
}
|
|
1221
|
+
/**
|
|
1222
|
+
* List all applications in a project
|
|
1223
|
+
*/
|
|
1224
|
+
async listApplications(projectId) {
|
|
1225
|
+
return this.listInProject(projectId, (env) => env.applications);
|
|
1226
|
+
}
|
|
1227
|
+
/**
|
|
1228
|
+
* Everything of one kind in a project, across its environments.
|
|
1229
|
+
*
|
|
1230
|
+
* From `project.one`, because the per-kind `*.all?projectId=` endpoints
|
|
1231
|
+
* return **404** on current Dokploy: resources live under environments now.
|
|
1232
|
+
* Those calls used to be wrapped in `catch { return [] }`, which turned "I
|
|
1233
|
+
* could not ask" into "there are none" — and for a find-or-create that is
|
|
1234
|
+
* the worst available answer. Every deploy created a second application, a
|
|
1235
|
+
* second Postgres and a second Redis, silently, for as long as the project
|
|
1236
|
+
* existed.
|
|
1237
|
+
*
|
|
1238
|
+
* A failure here is not swallowed: creating a duplicate is worse than
|
|
1239
|
+
* stopping.
|
|
1240
|
+
*/
|
|
1241
|
+
async listInProject(projectId, pick) {
|
|
1242
|
+
return ((await this.getProject(projectId)).environments ?? []).flatMap((environment) => (pick(environment) ?? []).map((resource) => ({
|
|
1243
|
+
...resource,
|
|
1244
|
+
environmentId: environment.environmentId
|
|
1245
|
+
})));
|
|
1246
|
+
}
|
|
1247
|
+
/**
|
|
1248
|
+
* Find an application by name in a project
|
|
1249
|
+
*/
|
|
1250
|
+
async findApplicationByName(projectId, name, environmentId) {
|
|
1251
|
+
const applications = await this.listApplications(projectId);
|
|
1252
|
+
const normalizedName = name.toLowerCase().replace(/[^a-z0-9-]/g, "-");
|
|
1253
|
+
return applications.find((app) => (!environmentId || app.environmentId === environmentId) && (app.name === name || app.appName === normalizedName));
|
|
1254
|
+
}
|
|
1255
|
+
/**
|
|
1256
|
+
* Create a new application
|
|
1257
|
+
*/
|
|
1258
|
+
async createApplication(name, projectId, environmentId) {
|
|
1259
|
+
return this.post("application.create", {
|
|
1260
|
+
name,
|
|
1261
|
+
projectId,
|
|
1262
|
+
environmentId,
|
|
1263
|
+
appName: name.toLowerCase().replace(/[^a-z0-9-]/g, "-")
|
|
1264
|
+
});
|
|
1265
|
+
}
|
|
1266
|
+
/**
|
|
1267
|
+
* Find or create an application by name
|
|
1268
|
+
*/
|
|
1269
|
+
async findOrCreateApplication(name, projectId, environmentId) {
|
|
1270
|
+
const existing = await this.findApplicationByName(projectId, name, environmentId);
|
|
1271
|
+
if (existing) return {
|
|
1272
|
+
application: existing,
|
|
1273
|
+
created: false
|
|
1274
|
+
};
|
|
1275
|
+
return {
|
|
1276
|
+
application: await this.createApplication(name, projectId, environmentId),
|
|
1277
|
+
created: true
|
|
1278
|
+
};
|
|
1279
|
+
}
|
|
1280
|
+
/**
|
|
1281
|
+
* Get an application by ID
|
|
1282
|
+
*/
|
|
1283
|
+
async getApplication(applicationId) {
|
|
1284
|
+
try {
|
|
1285
|
+
return await this.get(`application.one?applicationId=${applicationId}`);
|
|
1286
|
+
} catch {
|
|
1287
|
+
return null;
|
|
1288
|
+
}
|
|
1289
|
+
}
|
|
1290
|
+
/**
|
|
1291
|
+
* Update an application
|
|
1292
|
+
*/
|
|
1293
|
+
async updateApplication(applicationId, updates) {
|
|
1294
|
+
await this.post("application.update", {
|
|
1295
|
+
applicationId,
|
|
1296
|
+
...updates
|
|
1297
|
+
});
|
|
1298
|
+
}
|
|
1299
|
+
/**
|
|
1300
|
+
* Save environment variables for an application
|
|
1301
|
+
*/
|
|
1302
|
+
async saveApplicationEnv(applicationId, env, options) {
|
|
1303
|
+
await this.post("application.saveEnvironment", {
|
|
1304
|
+
applicationId,
|
|
1305
|
+
env,
|
|
1306
|
+
buildArgs: options?.buildArgs ?? null,
|
|
1307
|
+
buildSecrets: options?.buildSecrets ?? null,
|
|
1308
|
+
createEnvFile: false
|
|
1309
|
+
});
|
|
1310
|
+
}
|
|
1311
|
+
/**
|
|
1312
|
+
* Configure application to use Docker provider (pull from registry)
|
|
1313
|
+
*
|
|
1314
|
+
* For private registries, either:
|
|
1315
|
+
* - Use `registryId` if the registry is configured in Dokploy
|
|
1316
|
+
* - Or provide `username`, `password`, and `registryUrl` directly
|
|
1317
|
+
*/
|
|
1318
|
+
async saveDockerProvider(applicationId, dockerImage, options) {
|
|
1319
|
+
await this.post("application.saveDockerProvider", {
|
|
1320
|
+
applicationId,
|
|
1321
|
+
dockerImage,
|
|
1322
|
+
...options,
|
|
1323
|
+
username: options?.username ?? null,
|
|
1324
|
+
password: options?.password ?? null,
|
|
1325
|
+
registryUrl: options?.registryUrl ?? null
|
|
1326
|
+
});
|
|
1327
|
+
}
|
|
1328
|
+
/**
|
|
1329
|
+
* Deploy an application
|
|
1330
|
+
*/
|
|
1331
|
+
async deployApplication(applicationId) {
|
|
1332
|
+
await this.post("application.deploy", { applicationId });
|
|
1333
|
+
}
|
|
1334
|
+
/**
|
|
1335
|
+
* Delete an application
|
|
1336
|
+
*/
|
|
1337
|
+
async deleteApplication(applicationId) {
|
|
1338
|
+
await this.post("application.remove", { applicationId });
|
|
1339
|
+
}
|
|
1340
|
+
/**
|
|
1341
|
+
* List all registries
|
|
1342
|
+
*/
|
|
1343
|
+
async listRegistries() {
|
|
1344
|
+
return this.get("registry.all");
|
|
1345
|
+
}
|
|
1346
|
+
/**
|
|
1347
|
+
* Create a new registry
|
|
1348
|
+
*/
|
|
1349
|
+
async createRegistry(registryName, registryUrl, username, password, options) {
|
|
1350
|
+
return this.post("registry.create", {
|
|
1351
|
+
registryName,
|
|
1352
|
+
registryUrl,
|
|
1353
|
+
username,
|
|
1354
|
+
password,
|
|
1355
|
+
imagePrefix: options?.imagePrefix
|
|
1356
|
+
});
|
|
1357
|
+
}
|
|
1358
|
+
/**
|
|
1359
|
+
* Get a registry by ID
|
|
1360
|
+
*/
|
|
1361
|
+
async getRegistry(registryId) {
|
|
1362
|
+
return this.get(`registry.one?registryId=${registryId}`);
|
|
1363
|
+
}
|
|
1364
|
+
/**
|
|
1365
|
+
* Update a registry
|
|
1366
|
+
*/
|
|
1367
|
+
async updateRegistry(registryId, updates) {
|
|
1368
|
+
await this.post("registry.update", {
|
|
1369
|
+
registryId,
|
|
1370
|
+
...updates
|
|
1371
|
+
});
|
|
1372
|
+
}
|
|
1373
|
+
/**
|
|
1374
|
+
* Delete a registry
|
|
1375
|
+
*/
|
|
1376
|
+
async deleteRegistry(registryId) {
|
|
1377
|
+
await this.post("registry.remove", { registryId });
|
|
1378
|
+
}
|
|
1379
|
+
/**
|
|
1380
|
+
* List all Postgres databases in a project
|
|
1381
|
+
*/
|
|
1382
|
+
async listPostgres(projectId) {
|
|
1383
|
+
return this.listInProject(projectId, (env) => env.postgres);
|
|
1384
|
+
}
|
|
1385
|
+
/**
|
|
1386
|
+
* Find a Postgres database by name in a project
|
|
1387
|
+
*/
|
|
1388
|
+
async findPostgresByName(projectId, name) {
|
|
1389
|
+
const databases = await this.listPostgres(projectId);
|
|
1390
|
+
const normalizedName = name.toLowerCase().replace(/[^a-z0-9-]/g, "-");
|
|
1391
|
+
const match = databases.find((db) => db.name === name || db.appName === normalizedName);
|
|
1392
|
+
if (!match) return void 0;
|
|
1393
|
+
return this.getPostgres(match.postgresId);
|
|
1394
|
+
}
|
|
1395
|
+
/**
|
|
1396
|
+
* Create a new Postgres database
|
|
1397
|
+
*/
|
|
1398
|
+
async createPostgres(name, projectId, environmentId, options) {
|
|
1399
|
+
return this.post("postgres.create", {
|
|
1400
|
+
name,
|
|
1401
|
+
projectId,
|
|
1402
|
+
environmentId,
|
|
1403
|
+
appName: options?.appName ?? name.toLowerCase().replace(/[^a-z0-9-]/g, "-"),
|
|
1404
|
+
databaseName: options?.databaseName,
|
|
1405
|
+
databaseUser: options?.databaseUser ?? "postgres",
|
|
1406
|
+
databasePassword: options?.databasePassword,
|
|
1407
|
+
dockerImage: options?.dockerImage ?? "postgres:18",
|
|
1408
|
+
description: options?.description ?? `Postgres database for ${name}`
|
|
1409
|
+
});
|
|
1410
|
+
}
|
|
1411
|
+
/**
|
|
1412
|
+
* Find or create a Postgres database by name
|
|
1413
|
+
*/
|
|
1414
|
+
async findOrCreatePostgres(name, projectId, environmentId, options) {
|
|
1415
|
+
const existing = await this.findPostgresByName(projectId, name);
|
|
1416
|
+
if (existing) return {
|
|
1417
|
+
postgres: existing,
|
|
1418
|
+
created: false
|
|
1419
|
+
};
|
|
1420
|
+
return {
|
|
1421
|
+
postgres: await this.createPostgres(name, projectId, environmentId, options),
|
|
1422
|
+
created: true
|
|
1423
|
+
};
|
|
1424
|
+
}
|
|
1425
|
+
/**
|
|
1426
|
+
* Get a Postgres database by ID
|
|
1427
|
+
*/
|
|
1428
|
+
async getPostgres(postgresId) {
|
|
1429
|
+
return this.get(`postgres.one?postgresId=${postgresId}`);
|
|
1430
|
+
}
|
|
1431
|
+
/**
|
|
1432
|
+
* Deploy a Postgres database
|
|
1433
|
+
*/
|
|
1434
|
+
async deployPostgres(postgresId) {
|
|
1435
|
+
await this.post("postgres.deploy", { postgresId });
|
|
1436
|
+
}
|
|
1437
|
+
/**
|
|
1438
|
+
* Save environment variables for Postgres
|
|
1439
|
+
*/
|
|
1440
|
+
async savePostgresEnv(postgresId, env) {
|
|
1441
|
+
await this.post("postgres.saveEnvironment", {
|
|
1442
|
+
postgresId,
|
|
1443
|
+
env
|
|
1444
|
+
});
|
|
1445
|
+
}
|
|
1446
|
+
/**
|
|
1447
|
+
* Set external port for Postgres (for external access)
|
|
1448
|
+
*/
|
|
1449
|
+
async savePostgresExternalPort(postgresId, externalPort) {
|
|
1450
|
+
await this.post("postgres.saveExternalPort", {
|
|
1451
|
+
postgresId,
|
|
1452
|
+
externalPort
|
|
1453
|
+
});
|
|
1454
|
+
}
|
|
1455
|
+
/**
|
|
1456
|
+
* Update Postgres configuration
|
|
1457
|
+
*/
|
|
1458
|
+
async updatePostgres(postgresId, updates) {
|
|
1459
|
+
await this.post("postgres.update", {
|
|
1460
|
+
postgresId,
|
|
1461
|
+
...updates
|
|
1462
|
+
});
|
|
1463
|
+
}
|
|
1464
|
+
/**
|
|
1465
|
+
* Delete a Postgres database
|
|
1466
|
+
*/
|
|
1467
|
+
async deletePostgres(postgresId) {
|
|
1468
|
+
await this.post("postgres.remove", { postgresId });
|
|
1469
|
+
}
|
|
1470
|
+
/**
|
|
1471
|
+
* The compose stacks in a project.
|
|
1472
|
+
*
|
|
1473
|
+
* Dokploy has first-class primitives for Postgres and Redis and none for
|
|
1474
|
+
* MinIO, so an object store is a compose stack — the one kind whose contents
|
|
1475
|
+
* this target writes rather than configures.
|
|
1476
|
+
*/
|
|
1477
|
+
async listCompose(projectId) {
|
|
1478
|
+
return this.listInProject(projectId, (env) => env.compose);
|
|
1479
|
+
}
|
|
1480
|
+
/** A compose stack by name, hydrated — `project.one` returns summaries. */
|
|
1481
|
+
async findComposeByName(projectId, name) {
|
|
1482
|
+
const summary = (await this.listCompose(projectId)).find((compose) => compose.name === name);
|
|
1483
|
+
return summary ? this.getCompose(summary.composeId) : void 0;
|
|
1484
|
+
}
|
|
1485
|
+
async getCompose(composeId) {
|
|
1486
|
+
return this.get(`compose.one?composeId=${composeId}`);
|
|
1487
|
+
}
|
|
1488
|
+
/**
|
|
1489
|
+
* A compose stack carrying the file this target wrote.
|
|
1490
|
+
*
|
|
1491
|
+
* Created empty and then updated, because `compose.create` takes no file:
|
|
1492
|
+
* it defaults to `sourceType: 'github'` and expects a repository. `raw` is
|
|
1493
|
+
* what makes the file ours.
|
|
1494
|
+
*/
|
|
1495
|
+
async findOrCreateCompose(name, projectId, environmentId, composeFile) {
|
|
1496
|
+
const existing = await this.findComposeByName(projectId, name);
|
|
1497
|
+
const compose = existing ?? await this.post("compose.create", {
|
|
1498
|
+
name,
|
|
1499
|
+
environmentId,
|
|
1500
|
+
composeType: "docker-compose"
|
|
1501
|
+
});
|
|
1502
|
+
await this.post("compose.update", {
|
|
1503
|
+
composeId: compose.composeId,
|
|
1504
|
+
sourceType: "raw",
|
|
1505
|
+
composeFile
|
|
1506
|
+
});
|
|
1507
|
+
return {
|
|
1508
|
+
compose,
|
|
1509
|
+
created: !existing
|
|
1510
|
+
};
|
|
1511
|
+
}
|
|
1512
|
+
async deployCompose(composeId) {
|
|
1513
|
+
await this.post("compose.deploy", { composeId });
|
|
1514
|
+
}
|
|
1515
|
+
async deleteCompose(composeId) {
|
|
1516
|
+
await this.post("compose.delete", {
|
|
1517
|
+
composeId,
|
|
1518
|
+
deleteVolumes: false
|
|
1519
|
+
});
|
|
1520
|
+
}
|
|
1521
|
+
/**
|
|
1522
|
+
* List all Redis instances in a project
|
|
1523
|
+
*/
|
|
1524
|
+
async listRedis(projectId) {
|
|
1525
|
+
return this.listInProject(projectId, (env) => env.redis);
|
|
1526
|
+
}
|
|
1527
|
+
/**
|
|
1528
|
+
* Find a Redis instance by name in a project
|
|
1529
|
+
*/
|
|
1530
|
+
async findRedisByName(projectId, name) {
|
|
1531
|
+
const instances = await this.listRedis(projectId);
|
|
1532
|
+
const normalizedName = name.toLowerCase().replace(/[^a-z0-9-]/g, "-");
|
|
1533
|
+
const match = instances.find((redis) => redis.name === name || redis.appName === normalizedName);
|
|
1534
|
+
return match ? this.getRedis(match.redisId) : void 0;
|
|
1535
|
+
}
|
|
1536
|
+
/**
|
|
1537
|
+
* Create a new Redis instance
|
|
1538
|
+
*/
|
|
1539
|
+
async createRedis(name, projectId, environmentId, options) {
|
|
1540
|
+
return this.post("redis.create", {
|
|
1541
|
+
name,
|
|
1542
|
+
projectId,
|
|
1543
|
+
environmentId,
|
|
1544
|
+
appName: options?.appName ?? name.toLowerCase().replace(/[^a-z0-9-]/g, "-"),
|
|
1545
|
+
databasePassword: options?.databasePassword,
|
|
1546
|
+
dockerImage: options?.dockerImage ?? "redis:8",
|
|
1547
|
+
description: options?.description ?? `Redis instance for ${name}`
|
|
1548
|
+
});
|
|
1549
|
+
}
|
|
1550
|
+
/**
|
|
1551
|
+
* Find or create a Redis instance by name
|
|
1552
|
+
*/
|
|
1553
|
+
async findOrCreateRedis(name, projectId, environmentId, options) {
|
|
1554
|
+
const existing = await this.findRedisByName(projectId, name);
|
|
1555
|
+
if (existing) return {
|
|
1556
|
+
redis: existing,
|
|
1557
|
+
created: false
|
|
1558
|
+
};
|
|
1559
|
+
return {
|
|
1560
|
+
redis: await this.createRedis(name, projectId, environmentId, options),
|
|
1561
|
+
created: true
|
|
1562
|
+
};
|
|
1563
|
+
}
|
|
1564
|
+
/**
|
|
1565
|
+
* Get a Redis instance by ID
|
|
1566
|
+
*/
|
|
1567
|
+
async getRedis(redisId) {
|
|
1568
|
+
return this.get(`redis.one?redisId=${redisId}`);
|
|
1569
|
+
}
|
|
1570
|
+
/**
|
|
1571
|
+
* Deploy a Redis instance
|
|
1572
|
+
*/
|
|
1573
|
+
async deployRedis(redisId) {
|
|
1574
|
+
await this.post("redis.deploy", { redisId });
|
|
1575
|
+
}
|
|
1576
|
+
/**
|
|
1577
|
+
* Save environment variables for Redis
|
|
1578
|
+
*/
|
|
1579
|
+
async saveRedisEnv(redisId, env) {
|
|
1580
|
+
await this.post("redis.saveEnvironment", {
|
|
1581
|
+
redisId,
|
|
1582
|
+
env
|
|
1583
|
+
});
|
|
1584
|
+
}
|
|
1585
|
+
/**
|
|
1586
|
+
* Set external port for Redis (for external access)
|
|
1587
|
+
*/
|
|
1588
|
+
async saveRedisExternalPort(redisId, externalPort) {
|
|
1589
|
+
await this.post("redis.saveExternalPort", {
|
|
1590
|
+
redisId,
|
|
1591
|
+
externalPort
|
|
1592
|
+
});
|
|
1593
|
+
}
|
|
1594
|
+
/**
|
|
1595
|
+
* Update Redis configuration
|
|
1596
|
+
*/
|
|
1597
|
+
async updateRedis(redisId, updates) {
|
|
1598
|
+
await this.post("redis.update", {
|
|
1599
|
+
redisId,
|
|
1600
|
+
...updates
|
|
1601
|
+
});
|
|
1602
|
+
}
|
|
1603
|
+
/**
|
|
1604
|
+
* Delete a Redis instance
|
|
1605
|
+
*/
|
|
1606
|
+
async deleteRedis(redisId) {
|
|
1607
|
+
await this.post("redis.remove", { redisId });
|
|
1608
|
+
}
|
|
1609
|
+
/**
|
|
1610
|
+
* Create a new domain for an application
|
|
1611
|
+
*/
|
|
1612
|
+
async createDomain(options) {
|
|
1613
|
+
return this.post("domain.create", options);
|
|
1614
|
+
}
|
|
1615
|
+
/**
|
|
1616
|
+
* Update an existing domain
|
|
1617
|
+
*/
|
|
1618
|
+
async updateDomain(domainId, updates) {
|
|
1619
|
+
await this.post("domain.update", {
|
|
1620
|
+
domainId,
|
|
1621
|
+
...updates
|
|
1622
|
+
});
|
|
1623
|
+
}
|
|
1624
|
+
/**
|
|
1625
|
+
* Delete a domain
|
|
1626
|
+
*/
|
|
1627
|
+
async deleteDomain(domainId) {
|
|
1628
|
+
await this.post("domain.delete", { domainId });
|
|
1629
|
+
}
|
|
1630
|
+
/**
|
|
1631
|
+
* Get a domain by ID
|
|
1632
|
+
*/
|
|
1633
|
+
async getDomain(domainId) {
|
|
1634
|
+
return this.get(`domain.one?domainId=${domainId}`);
|
|
1635
|
+
}
|
|
1636
|
+
/**
|
|
1637
|
+
* Get all domains for an application
|
|
1638
|
+
*/
|
|
1639
|
+
async getDomainsByApplicationId(applicationId) {
|
|
1640
|
+
return this.get(`domain.byApplicationId?applicationId=${applicationId}`);
|
|
1641
|
+
}
|
|
1642
|
+
/**
|
|
1643
|
+
* Validate a domain and trigger SSL certificate generation
|
|
1644
|
+
*
|
|
1645
|
+
* This should be called after DNS records are created and propagated.
|
|
1646
|
+
* It triggers Let's Encrypt certificate generation for HTTPS domains.
|
|
1647
|
+
*
|
|
1648
|
+
* @param domain - The domain hostname to validate (e.g., 'api.example.com')
|
|
1649
|
+
*/
|
|
1650
|
+
async validateDomain(domain) {
|
|
1651
|
+
return this.post("domain.validateDomain", { domain });
|
|
1652
|
+
}
|
|
1653
|
+
/**
|
|
1654
|
+
* Auto-generate a domain name for an application
|
|
1655
|
+
*/
|
|
1656
|
+
async generateDomain(appName, serverId) {
|
|
1657
|
+
return this.post("domain.generateDomain", {
|
|
1658
|
+
appName,
|
|
1659
|
+
serverId
|
|
1660
|
+
});
|
|
1661
|
+
}
|
|
1662
|
+
/**
|
|
1663
|
+
* List all backup destinations
|
|
1664
|
+
*/
|
|
1665
|
+
async listDestinations() {
|
|
1666
|
+
return this.get("destination.all");
|
|
1667
|
+
}
|
|
1668
|
+
/**
|
|
1669
|
+
* Get a destination by ID
|
|
1670
|
+
*/
|
|
1671
|
+
async getDestination(destinationId) {
|
|
1672
|
+
return this.get(`destination.one?destinationId=${destinationId}`);
|
|
1673
|
+
}
|
|
1674
|
+
/**
|
|
1675
|
+
* Create a new S3 backup destination
|
|
1676
|
+
*/
|
|
1677
|
+
async createDestination(options) {
|
|
1678
|
+
return this.post("destination.create", { ...options });
|
|
1679
|
+
}
|
|
1680
|
+
/**
|
|
1681
|
+
* Find a destination by name
|
|
1682
|
+
*/
|
|
1683
|
+
async findDestinationByName(name) {
|
|
1684
|
+
return (await this.listDestinations()).find((d) => d.name === name);
|
|
1685
|
+
}
|
|
1686
|
+
/**
|
|
1687
|
+
* Find or create a destination by name
|
|
1688
|
+
*/
|
|
1689
|
+
async findOrCreateDestination(name, options) {
|
|
1690
|
+
const existing = await this.findDestinationByName(name);
|
|
1691
|
+
if (existing) return {
|
|
1692
|
+
destination: existing,
|
|
1693
|
+
created: false
|
|
1694
|
+
};
|
|
1695
|
+
return {
|
|
1696
|
+
destination: await this.createDestination({
|
|
1697
|
+
name,
|
|
1698
|
+
...options
|
|
1699
|
+
}),
|
|
1700
|
+
created: true
|
|
1701
|
+
};
|
|
1702
|
+
}
|
|
1703
|
+
/**
|
|
1704
|
+
* Update a destination
|
|
1705
|
+
*/
|
|
1706
|
+
async updateDestination(destinationId, updates) {
|
|
1707
|
+
await this.post("destination.update", {
|
|
1708
|
+
destinationId,
|
|
1709
|
+
...updates
|
|
1710
|
+
});
|
|
1711
|
+
}
|
|
1712
|
+
/**
|
|
1713
|
+
* Delete a destination
|
|
1714
|
+
*/
|
|
1715
|
+
async deleteDestination(destinationId) {
|
|
1716
|
+
await this.post("destination.remove", { destinationId });
|
|
1717
|
+
}
|
|
1718
|
+
/**
|
|
1719
|
+
* Test connection to a destination
|
|
1720
|
+
*/
|
|
1721
|
+
async testDestinationConnection(destinationId) {
|
|
1722
|
+
return this.post("destination.testConnection", { destinationId });
|
|
1723
|
+
}
|
|
1724
|
+
/**
|
|
1725
|
+
* Create a backup schedule for postgres
|
|
1726
|
+
*/
|
|
1727
|
+
async createPostgresBackup(options) {
|
|
1728
|
+
return this.post("backup.create", {
|
|
1729
|
+
...options,
|
|
1730
|
+
databaseType: "postgres"
|
|
1731
|
+
});
|
|
1732
|
+
}
|
|
1733
|
+
/**
|
|
1734
|
+
* List backups for a postgres database
|
|
1735
|
+
*/
|
|
1736
|
+
async listPostgresBackups(postgresId) {
|
|
1737
|
+
return this.get(`backup.all?postgresId=${postgresId}&databaseType=postgres`);
|
|
1738
|
+
}
|
|
1739
|
+
/**
|
|
1740
|
+
* Get a backup by ID
|
|
1741
|
+
*/
|
|
1742
|
+
async getBackup(backupId) {
|
|
1743
|
+
return this.get(`backup.one?backupId=${backupId}`);
|
|
1744
|
+
}
|
|
1745
|
+
/**
|
|
1746
|
+
* Update a backup schedule
|
|
1747
|
+
*/
|
|
1748
|
+
async updateBackup(backupId, updates) {
|
|
1749
|
+
await this.post("backup.update", {
|
|
1750
|
+
backupId,
|
|
1751
|
+
...updates
|
|
1752
|
+
});
|
|
1753
|
+
}
|
|
1754
|
+
/**
|
|
1755
|
+
* Delete a backup schedule
|
|
1756
|
+
*/
|
|
1757
|
+
async deleteBackup(backupId) {
|
|
1758
|
+
await this.post("backup.remove", { backupId });
|
|
1759
|
+
}
|
|
1760
|
+
/**
|
|
1761
|
+
* Trigger a manual backup run
|
|
1762
|
+
*/
|
|
1763
|
+
async runBackupManually(backupId) {
|
|
1764
|
+
await this.post("backup.manualBackup", { backupId });
|
|
1765
|
+
}
|
|
1766
|
+
};
|
|
1767
|
+
//#endregion
|
|
1768
|
+
//#region src/deploy/sniffer.ts
|
|
1769
|
+
const __filename$1 = (0, node_url.fileURLToPath)(require("url").pathToFileURL(__filename).href);
|
|
1770
|
+
const __dirname$1 = (0, node_path.dirname)(__filename$1);
|
|
1771
|
+
/**
|
|
1772
|
+
* Resolve the path to a sniffer helper file.
|
|
1773
|
+
* Handles both dev (.ts with tsx) and production (.mjs from dist).
|
|
1774
|
+
*
|
|
1775
|
+
* In production: sniffer.ts is bundled into dist/index.mjs, but sniffer helper
|
|
1776
|
+
* files are output to dist/deploy/ as standalone modules for subprocess loading.
|
|
1777
|
+
*
|
|
1778
|
+
* In development: All files are in src/deploy/ and loaded via tsx.
|
|
1779
|
+
*/
|
|
1780
|
+
function resolveSnifferFile(baseName) {
|
|
1781
|
+
const deployMjsPath = (0, node_path.resolve)(__dirname$1, "deploy", `${baseName}.mjs`);
|
|
1782
|
+
if ((0, node_fs.existsSync)(deployMjsPath)) return deployMjsPath;
|
|
1783
|
+
const mjsPath = (0, node_path.resolve)(__dirname$1, `${baseName}.mjs`);
|
|
1784
|
+
if ((0, node_fs.existsSync)(mjsPath)) return mjsPath;
|
|
1785
|
+
const tsPath = (0, node_path.resolve)(__dirname$1, `${baseName}.ts`);
|
|
1786
|
+
if ((0, node_fs.existsSync)(tsPath)) return tsPath;
|
|
1787
|
+
return tsPath;
|
|
1788
|
+
}
|
|
1789
|
+
/**
|
|
1790
|
+
* Run a sniffer worker in the sandbox, with the sandbox's environment and
|
|
1791
|
+
* nothing of the deploy's: the deploy's Dokploy token, registry login and AWS
|
|
1792
|
+
* keys stay where they are. A worker that outlives the timeout is killed, and
|
|
1793
|
+
* comes back as the error the caller reports.
|
|
1794
|
+
*/
|
|
1795
|
+
async function runSniffer(run, workspacePath, cwd, nodeArgs) {
|
|
1796
|
+
const sandbox = run.sandbox ?? require_sandbox.activeSandbox() ?? new require_local.LocalSandbox({ root: (0, node_path.resolve)(workspacePath) });
|
|
1797
|
+
try {
|
|
1798
|
+
const result = await sandbox.exec("node", nodeArgs, {
|
|
1799
|
+
cwd,
|
|
1800
|
+
env: { ...sandbox.env },
|
|
1801
|
+
timeoutMs: run.timeoutMs ?? 3e4,
|
|
1802
|
+
output: "capture"
|
|
1803
|
+
});
|
|
1804
|
+
return {
|
|
1805
|
+
stdout: result.stdout,
|
|
1806
|
+
stderr: result.stderr,
|
|
1807
|
+
code: result.exitCode
|
|
1808
|
+
};
|
|
1809
|
+
} catch (error) {
|
|
1810
|
+
return { error: error instanceof Error ? error : new Error(String(error)) };
|
|
1811
|
+
}
|
|
1812
|
+
}
|
|
1813
|
+
/** The worker's JSON answer: the last object on stdout naming `envVars`. */
|
|
1814
|
+
function snifferAnswer(stdout) {
|
|
1815
|
+
try {
|
|
1816
|
+
const jsonMatch = stdout.match(/\{[^{}]*"envVars"[^{}]*\}[^{]*$/);
|
|
1817
|
+
return jsonMatch ? JSON.parse(jsonMatch[0]) : void 0;
|
|
1818
|
+
} catch {
|
|
1819
|
+
return;
|
|
1820
|
+
}
|
|
1821
|
+
}
|
|
1822
|
+
/**
|
|
1823
|
+
* Get required environment variables for an app.
|
|
1824
|
+
*
|
|
1825
|
+
* Detection strategy (in order):
|
|
1826
|
+
* 1. Frontend apps: Returns empty (no server secrets)
|
|
1827
|
+
* 2. Apps with `requiredEnv`: Uses explicit list from config
|
|
1828
|
+
* 3. Entry apps: Imports entry file in subprocess to capture config.parse() calls
|
|
1829
|
+
* 4. Route-based apps: Loads route files and calls getEnvironment() on each construct
|
|
1830
|
+
* 5. Apps with `envParser` (no routes): Runs SnifferEnvironmentParser to detect usage
|
|
1831
|
+
* 6. Apps with neither: Returns empty
|
|
1832
|
+
*
|
|
1833
|
+
* This function handles "fire and forget" async operations gracefully,
|
|
1834
|
+
* capturing errors and unhandled rejections without failing the build.
|
|
1835
|
+
*
|
|
1836
|
+
* @param app - The normalized app configuration
|
|
1837
|
+
* @param appName - The name of the app
|
|
1838
|
+
* @param workspacePath - Absolute path to the workspace root
|
|
1839
|
+
* @param options - Optional configuration for sniffing behavior
|
|
1840
|
+
* @returns The sniffed environment with required variables
|
|
1841
|
+
*/
|
|
1842
|
+
async function sniffAppEnvironment(app, appName, workspacePath, options = {}) {
|
|
1843
|
+
const { logWarnings = true, markOptional = false } = options;
|
|
1844
|
+
const run = {
|
|
1845
|
+
...options.sandbox ? { sandbox: options.sandbox } : {},
|
|
1846
|
+
...options.timeoutMs ? { timeoutMs: options.timeoutMs } : {}
|
|
1847
|
+
};
|
|
1848
|
+
if (app.type === "web" || app.type === "mobile") {
|
|
1849
|
+
const publicPrefix = require_workspace.getPublicEnvPrefix(app.framework);
|
|
1850
|
+
const depVars = publicPrefix ? (app.dependencies ?? []).map((dep) => `${publicPrefix}${dep.toUpperCase()}_URL`) : [];
|
|
1851
|
+
if (app.config) {
|
|
1852
|
+
const sniffedVars = [];
|
|
1853
|
+
const sniffedOptional = [];
|
|
1854
|
+
const configPaths = [];
|
|
1855
|
+
if (app.config.client) configPaths.push(app.config.client);
|
|
1856
|
+
if (app.config.server) configPaths.push(app.config.server);
|
|
1857
|
+
for (const configPath of configPaths) {
|
|
1858
|
+
const result = await sniffEntryFile(configPath, app.path, workspacePath, run);
|
|
1859
|
+
if (logWarnings && result.error) require_discover.output.warn(`[sniffer] ${appName}: Config file "${configPath}" threw error during sniffing (env vars still captured): ${result.error.message}`);
|
|
1860
|
+
sniffedVars.push(...result.envVars);
|
|
1861
|
+
sniffedOptional.push(...result.optionalEnvVars);
|
|
1862
|
+
}
|
|
1863
|
+
const optionalEnvVars = [...new Set(sniffedOptional)];
|
|
1864
|
+
return {
|
|
1865
|
+
appName,
|
|
1866
|
+
requiredEnvVars: applyMarkOptional([.../* @__PURE__ */ new Set([...depVars, ...sniffedVars])], optionalEnvVars, markOptional),
|
|
1867
|
+
optionalEnvVars
|
|
1868
|
+
};
|
|
1869
|
+
}
|
|
1870
|
+
return {
|
|
1871
|
+
appName,
|
|
1872
|
+
requiredEnvVars: depVars,
|
|
1873
|
+
optionalEnvVars: []
|
|
1874
|
+
};
|
|
1875
|
+
}
|
|
1876
|
+
if (app.entry) {
|
|
1877
|
+
const result = await sniffEntryFile(app.entry, app.path, workspacePath, run);
|
|
1878
|
+
if (logWarnings && result.error) require_discover.output.warn(`[sniffer] ${appName}: Entry file threw error during sniffing (env vars still captured): ${result.error.message}`);
|
|
1879
|
+
return {
|
|
1880
|
+
appName,
|
|
1881
|
+
requiredEnvVars: applyMarkOptional(result.envVars, result.optionalEnvVars, markOptional),
|
|
1882
|
+
optionalEnvVars: result.optionalEnvVars
|
|
1883
|
+
};
|
|
1884
|
+
}
|
|
1885
|
+
if (app.routes) {
|
|
1886
|
+
const result = await sniffRouteFiles(normalizeRoutes(app.routes), app.path, workspacePath, run);
|
|
1887
|
+
if (logWarnings && result.error) require_discover.output.warn(`[sniffer] ${appName}: Route sniffing threw error (env vars still captured): ${result.error.message}`);
|
|
1888
|
+
return {
|
|
1889
|
+
appName,
|
|
1890
|
+
requiredEnvVars: applyMarkOptional(result.envVars, result.optionalEnvVars, markOptional),
|
|
1891
|
+
optionalEnvVars: result.optionalEnvVars
|
|
1892
|
+
};
|
|
1893
|
+
}
|
|
1894
|
+
if (app.envParser) {
|
|
1895
|
+
const result = await sniffEnvParser(app.envParser, app.path, workspacePath, run);
|
|
1896
|
+
if (logWarnings) {
|
|
1897
|
+
if (result.error) require_discover.output.warn(`[sniffer] ${appName}: envParser threw error during sniffing (env vars still captured): ${result.error.message}`);
|
|
1898
|
+
if (result.unhandledRejections.length > 0) require_discover.output.warn(`[sniffer] ${appName}: Fire-and-forget rejections during sniffing (suppressed): ${result.unhandledRejections.map((e) => e.message).join(", ")}`);
|
|
1899
|
+
}
|
|
1900
|
+
return {
|
|
1901
|
+
appName,
|
|
1902
|
+
requiredEnvVars: applyMarkOptional(result.envVars, result.optionalEnvVars, markOptional),
|
|
1903
|
+
optionalEnvVars: result.optionalEnvVars
|
|
1904
|
+
};
|
|
1905
|
+
}
|
|
1906
|
+
return {
|
|
1907
|
+
appName,
|
|
1908
|
+
requiredEnvVars: [],
|
|
1909
|
+
optionalEnvVars: []
|
|
1910
|
+
};
|
|
1911
|
+
}
|
|
1912
|
+
/**
|
|
1913
|
+
* Apply the `?` suffix to optional variables in an env var list.
|
|
1914
|
+
* Only modifies the list when `markOptional` is true and there are optional vars.
|
|
1915
|
+
*/
|
|
1916
|
+
function applyMarkOptional(envVars, optionalEnvVars, markOptional) {
|
|
1917
|
+
if (!markOptional || optionalEnvVars.length === 0) return envVars;
|
|
1918
|
+
const optionalSet = new Set(optionalEnvVars);
|
|
1919
|
+
return envVars.map((v) => optionalSet.has(v) ? `${v}?` : v);
|
|
1920
|
+
}
|
|
1921
|
+
/**
|
|
1922
|
+
* Sniff an entry file by importing it in a subprocess.
|
|
1923
|
+
*
|
|
1924
|
+
* Entry apps call `config.parse()` at module load time. To capture which
|
|
1925
|
+
* env vars are accessed, we:
|
|
1926
|
+
* 1. Spawn a subprocess with a module loader hook
|
|
1927
|
+
* 2. The loader intercepts `@geekmidas/envkit` and replaces EnvironmentParser
|
|
1928
|
+
* with SnifferEnvironmentParser
|
|
1929
|
+
* 3. Import the entry file (triggers config.parse())
|
|
1930
|
+
* 4. Capture and return the accessed env var names
|
|
1931
|
+
*
|
|
1932
|
+
* This approach provides process isolation - each app is sniffed in its own
|
|
1933
|
+
* subprocess, preventing module cache pollution.
|
|
1934
|
+
*
|
|
1935
|
+
* @param entryPath - Relative path to the entry file (e.g., './src/index.ts')
|
|
1936
|
+
* @param appPath - The app's path relative to workspace (e.g., 'apps/auth')
|
|
1937
|
+
* @param workspacePath - Absolute path to workspace root
|
|
1938
|
+
* @returns EntrySniffResult with env vars and optional error
|
|
1939
|
+
*/
|
|
1940
|
+
async function sniffEntryFile(entryPath, appPath, workspacePath, run = {}) {
|
|
1941
|
+
const fullEntryPath = (0, node_path.resolve)(workspacePath, appPath, entryPath);
|
|
1942
|
+
const loaderPath = resolveSnifferFile("sniffer-loader");
|
|
1943
|
+
const workerPath = resolveSnifferFile("sniffer-worker");
|
|
1944
|
+
const ran = await runSniffer(run, workspacePath, (0, node_path.resolve)(workspacePath, appPath), require_discover.nodeWithTsx(workerPath, [fullEntryPath], [loaderPath]));
|
|
1945
|
+
if ("error" in ran) return {
|
|
1946
|
+
envVars: [],
|
|
1947
|
+
optionalEnvVars: [],
|
|
1948
|
+
error: ran.error
|
|
1949
|
+
};
|
|
1950
|
+
const answer = snifferAnswer(ran.stdout);
|
|
1951
|
+
if (answer) return {
|
|
1952
|
+
envVars: answer.envVars || [],
|
|
1953
|
+
optionalEnvVars: answer.optionalEnvVars || [],
|
|
1954
|
+
error: answer.error ? new Error(answer.error) : void 0
|
|
1955
|
+
};
|
|
1956
|
+
return {
|
|
1957
|
+
envVars: [],
|
|
1958
|
+
optionalEnvVars: [],
|
|
1959
|
+
error: /* @__PURE__ */ new Error(`Failed to sniff entry file (exit code ${ran.code}): ${ran.stderr || ran.stdout || "No output"}`)
|
|
1960
|
+
};
|
|
1961
|
+
}
|
|
1962
|
+
/**
|
|
1963
|
+
* Sniff route files by loading constructs and calling getEnvironment().
|
|
1964
|
+
*
|
|
1965
|
+
* Route-based apps have endpoints, functions, crons, and subscribers that
|
|
1966
|
+
* use services. Each service's register() method accesses environment variables.
|
|
1967
|
+
*
|
|
1968
|
+
* This runs in a subprocess with tsx loader to properly handle TypeScript
|
|
1969
|
+
* compilation and path alias resolution (e.g., `src/...` imports).
|
|
1970
|
+
*
|
|
1971
|
+
* @param routes - Glob pattern(s) for route files
|
|
1972
|
+
* @param appPath - The app's path relative to workspace (e.g., 'apps/api')
|
|
1973
|
+
* @param workspacePath - Absolute path to workspace root
|
|
1974
|
+
* @returns EntrySniffResult with env vars and optional error
|
|
1975
|
+
*/
|
|
1976
|
+
async function sniffRouteFiles(routes, appPath, workspacePath, run = {}) {
|
|
1977
|
+
const fullAppPath = (0, node_path.resolve)(workspacePath, appPath);
|
|
1978
|
+
const workerPath = resolveSnifferFile("sniffer-routes-worker");
|
|
1979
|
+
const pattern = (Array.isArray(routes) ? routes : [routes])[0];
|
|
1980
|
+
if (!pattern) return {
|
|
1981
|
+
envVars: [],
|
|
1982
|
+
optionalEnvVars: [],
|
|
1983
|
+
error: /* @__PURE__ */ new Error("No route patterns provided")
|
|
1984
|
+
};
|
|
1985
|
+
const ran = await runSniffer(run, workspacePath, fullAppPath, require_discover.nodeWithTsx(workerPath, [fullAppPath, pattern]));
|
|
1986
|
+
if ("error" in ran) return {
|
|
1987
|
+
envVars: [],
|
|
1988
|
+
optionalEnvVars: [],
|
|
1989
|
+
error: ran.error
|
|
1990
|
+
};
|
|
1991
|
+
if (ran.stderr) ran.stderr.split("\n").filter((line) => line.trim()).forEach((line) => require_discover.output.warn(line));
|
|
1992
|
+
const answer = snifferAnswer(ran.stdout);
|
|
1993
|
+
if (answer) return {
|
|
1994
|
+
envVars: answer.envVars || [],
|
|
1995
|
+
optionalEnvVars: answer.optionalEnvVars || [],
|
|
1996
|
+
error: answer.error ? new Error(answer.error) : void 0
|
|
1997
|
+
};
|
|
1998
|
+
return {
|
|
1999
|
+
envVars: [],
|
|
2000
|
+
optionalEnvVars: [],
|
|
2001
|
+
error: /* @__PURE__ */ new Error(`Failed to sniff route files (exit code ${ran.code}): ${ran.stderr || ran.stdout || "No output"}`)
|
|
2002
|
+
};
|
|
2003
|
+
}
|
|
2004
|
+
/**
|
|
2005
|
+
* Run the SnifferEnvironmentParser on an envParser module to detect
|
|
2006
|
+
* which environment variables it accesses.
|
|
2007
|
+
*
|
|
2008
|
+
* In a subprocess, like the entry and route sniffers: the module is the
|
|
2009
|
+
* project's code, and it was imported into the deploy itself before — with
|
|
2010
|
+
* every credential the deploy held in reach. Fire-and-forget rejections are
|
|
2011
|
+
* collected there with the shared `sniffWithFireAndForget`.
|
|
2012
|
+
*
|
|
2013
|
+
* @param envParserPath - The envParser config (e.g., './src/config/env#envParser')
|
|
2014
|
+
* @param appPath - The app's path relative to workspace
|
|
2015
|
+
* @param workspacePath - Absolute path to workspace root
|
|
2016
|
+
* @returns SniffResult with env vars and any errors encountered
|
|
2017
|
+
*/
|
|
2018
|
+
async function sniffEnvParser(envParserPath, appPath, workspacePath, run = {}) {
|
|
2019
|
+
const [modulePath, exportName = "default"] = envParserPath.split("#");
|
|
2020
|
+
if (!modulePath) return {
|
|
2021
|
+
envVars: [],
|
|
2022
|
+
optionalEnvVars: [],
|
|
2023
|
+
unhandledRejections: []
|
|
2024
|
+
};
|
|
2025
|
+
const appRoot = (0, node_path.resolve)(workspacePath, appPath);
|
|
2026
|
+
const fullPath = (0, node_path.resolve)(appRoot, modulePath);
|
|
2027
|
+
const workerPath = resolveSnifferFile("sniffer-envparser-worker");
|
|
2028
|
+
const ran = await runSniffer(run, workspacePath, appRoot, require_discover.nodeWithTsx(workerPath, [fullPath, exportName]));
|
|
2029
|
+
if ("error" in ran) return {
|
|
2030
|
+
envVars: [],
|
|
2031
|
+
optionalEnvVars: [],
|
|
2032
|
+
unhandledRejections: [],
|
|
2033
|
+
error: ran.error
|
|
2034
|
+
};
|
|
2035
|
+
const answer = snifferAnswer(ran.stdout);
|
|
2036
|
+
if (!answer) return {
|
|
2037
|
+
envVars: [],
|
|
2038
|
+
optionalEnvVars: [],
|
|
2039
|
+
unhandledRejections: [],
|
|
2040
|
+
error: /* @__PURE__ */ new Error(`Failed to sniff envParser (exit code ${ran.code}): ${ran.stderr || ran.stdout || "No output"}`)
|
|
2041
|
+
};
|
|
2042
|
+
for (const warning of answer.warnings ?? []) require_discover.output.warn(warning);
|
|
2043
|
+
return {
|
|
2044
|
+
envVars: answer.envVars ?? [],
|
|
2045
|
+
optionalEnvVars: answer.optionalEnvVars ?? [],
|
|
2046
|
+
unhandledRejections: (answer.unhandledRejections ?? []).map((message) => new Error(message)),
|
|
2047
|
+
...answer.error ? { error: new Error(answer.error) } : {}
|
|
2048
|
+
};
|
|
2049
|
+
}
|
|
2050
|
+
/**
|
|
2051
|
+
* Sniff environment requirements for multiple apps.
|
|
2052
|
+
*
|
|
2053
|
+
* @param apps - Map of app name to app config
|
|
2054
|
+
* @param workspacePath - Absolute path to workspace root
|
|
2055
|
+
* @param options - Optional configuration for sniffing behavior
|
|
2056
|
+
* @returns Map of app name to sniffed environment
|
|
2057
|
+
*/
|
|
2058
|
+
async function sniffAllApps(apps, workspacePath, options = {}) {
|
|
2059
|
+
const results = /* @__PURE__ */ new Map();
|
|
2060
|
+
for (const [appName, app] of Object.entries(apps)) {
|
|
2061
|
+
const sniffed = await sniffAppEnvironment(app, appName, workspacePath, options);
|
|
2062
|
+
results.set(appName, sniffed);
|
|
2063
|
+
}
|
|
2064
|
+
return results;
|
|
2065
|
+
}
|
|
2066
|
+
//#endregion
|
|
2067
|
+
Object.defineProperty(exports, "DocumentStateStore", {
|
|
2068
|
+
enumerable: true,
|
|
2069
|
+
get: function() {
|
|
2070
|
+
return DocumentStateStore;
|
|
2071
|
+
}
|
|
2072
|
+
});
|
|
2073
|
+
Object.defineProperty(exports, "DokployApi", {
|
|
2074
|
+
enumerable: true,
|
|
2075
|
+
get: function() {
|
|
2076
|
+
return DokployApi;
|
|
2077
|
+
}
|
|
2078
|
+
});
|
|
2079
|
+
Object.defineProperty(exports, "DokployApiError", {
|
|
2080
|
+
enumerable: true,
|
|
2081
|
+
get: function() {
|
|
2082
|
+
return DokployApiError;
|
|
2083
|
+
}
|
|
2084
|
+
});
|
|
2085
|
+
Object.defineProperty(exports, "NoDeployableApps", {
|
|
2086
|
+
enumerable: true,
|
|
2087
|
+
get: function() {
|
|
2088
|
+
return NoDeployableApps;
|
|
2089
|
+
}
|
|
2090
|
+
});
|
|
2091
|
+
Object.defineProperty(exports, "Redactor", {
|
|
2092
|
+
enumerable: true,
|
|
2093
|
+
get: function() {
|
|
2094
|
+
return Redactor;
|
|
2095
|
+
}
|
|
2096
|
+
});
|
|
2097
|
+
Object.defineProperty(exports, "RollbackFailed", {
|
|
2098
|
+
enumerable: true,
|
|
2099
|
+
get: function() {
|
|
2100
|
+
return RollbackFailed;
|
|
2101
|
+
}
|
|
2102
|
+
});
|
|
2103
|
+
Object.defineProperty(exports, "StageStateMissing", {
|
|
2104
|
+
enumerable: true,
|
|
2105
|
+
get: function() {
|
|
2106
|
+
return StageStateMissing;
|
|
2107
|
+
}
|
|
2108
|
+
});
|
|
2109
|
+
Object.defineProperty(exports, "StateLocked", {
|
|
2110
|
+
enumerable: true,
|
|
2111
|
+
get: function() {
|
|
2112
|
+
return StateLocked;
|
|
2113
|
+
}
|
|
2114
|
+
});
|
|
2115
|
+
Object.defineProperty(exports, "StateStoreBusy", {
|
|
2116
|
+
enumerable: true,
|
|
2117
|
+
get: function() {
|
|
2118
|
+
return StateStoreBusy;
|
|
2119
|
+
}
|
|
2120
|
+
});
|
|
2121
|
+
Object.defineProperty(exports, "StateStoreWithoutLocking", {
|
|
2122
|
+
enumerable: true,
|
|
2123
|
+
get: function() {
|
|
2124
|
+
return StateStoreWithoutLocking;
|
|
2125
|
+
}
|
|
2126
|
+
});
|
|
2127
|
+
Object.defineProperty(exports, "StateVersionConflict", {
|
|
2128
|
+
enumerable: true,
|
|
2129
|
+
get: function() {
|
|
2130
|
+
return StateVersionConflict;
|
|
2131
|
+
}
|
|
2132
|
+
});
|
|
2133
|
+
Object.defineProperty(exports, "UnknownDeployApps", {
|
|
2134
|
+
enumerable: true,
|
|
2135
|
+
get: function() {
|
|
2136
|
+
return UnknownDeployApps;
|
|
2137
|
+
}
|
|
2138
|
+
});
|
|
2139
|
+
Object.defineProperty(exports, "canPrompt", {
|
|
2140
|
+
enumerable: true,
|
|
2141
|
+
get: function() {
|
|
2142
|
+
return canPrompt;
|
|
2143
|
+
}
|
|
2144
|
+
});
|
|
2145
|
+
Object.defineProperty(exports, "contentVersion", {
|
|
2146
|
+
enumerable: true,
|
|
2147
|
+
get: function() {
|
|
2148
|
+
return contentVersion;
|
|
2149
|
+
}
|
|
2150
|
+
});
|
|
2151
|
+
Object.defineProperty(exports, "createStateStore", {
|
|
2152
|
+
enumerable: true,
|
|
2153
|
+
get: function() {
|
|
2154
|
+
return createStateStore;
|
|
2155
|
+
}
|
|
2156
|
+
});
|
|
2157
|
+
Object.defineProperty(exports, "encryptSecrets", {
|
|
2158
|
+
enumerable: true,
|
|
2159
|
+
get: function() {
|
|
2160
|
+
return encryptSecrets;
|
|
2161
|
+
}
|
|
2162
|
+
});
|
|
2163
|
+
Object.defineProperty(exports, "encryption_exports", {
|
|
2164
|
+
enumerable: true,
|
|
2165
|
+
get: function() {
|
|
2166
|
+
return encryption_exports;
|
|
2167
|
+
}
|
|
2168
|
+
});
|
|
2169
|
+
Object.defineProperty(exports, "eventError", {
|
|
2170
|
+
enumerable: true,
|
|
2171
|
+
get: function() {
|
|
2172
|
+
return eventError;
|
|
2173
|
+
}
|
|
2174
|
+
});
|
|
2175
|
+
Object.defineProperty(exports, "keyFingerprint", {
|
|
2176
|
+
enumerable: true,
|
|
2177
|
+
get: function() {
|
|
2178
|
+
return keyFingerprint;
|
|
2179
|
+
}
|
|
2180
|
+
});
|
|
2181
|
+
Object.defineProperty(exports, "loginCommand", {
|
|
2182
|
+
enumerable: true,
|
|
2183
|
+
get: function() {
|
|
2184
|
+
return loginCommand;
|
|
2185
|
+
}
|
|
2186
|
+
});
|
|
2187
|
+
Object.defineProperty(exports, "logoutCommand", {
|
|
2188
|
+
enumerable: true,
|
|
2189
|
+
get: function() {
|
|
2190
|
+
return logoutCommand;
|
|
2191
|
+
}
|
|
2192
|
+
});
|
|
2193
|
+
Object.defineProperty(exports, "newLockHolder", {
|
|
2194
|
+
enumerable: true,
|
|
2195
|
+
get: function() {
|
|
2196
|
+
return newLockHolder;
|
|
2197
|
+
}
|
|
2198
|
+
});
|
|
2199
|
+
Object.defineProperty(exports, "prompt", {
|
|
2200
|
+
enumerable: true,
|
|
2201
|
+
get: function() {
|
|
2202
|
+
return prompt;
|
|
2203
|
+
}
|
|
2204
|
+
});
|
|
2205
|
+
Object.defineProperty(exports, "runDeploy", {
|
|
2206
|
+
enumerable: true,
|
|
2207
|
+
get: function() {
|
|
2208
|
+
return runDeploy;
|
|
2209
|
+
}
|
|
2210
|
+
});
|
|
2211
|
+
Object.defineProperty(exports, "sniffAllApps", {
|
|
2212
|
+
enumerable: true,
|
|
2213
|
+
get: function() {
|
|
2214
|
+
return sniffAllApps;
|
|
2215
|
+
}
|
|
2216
|
+
});
|
|
2217
|
+
Object.defineProperty(exports, "sniffAppEnvironment", {
|
|
2218
|
+
enumerable: true,
|
|
2219
|
+
get: function() {
|
|
2220
|
+
return sniffAppEnvironment;
|
|
2221
|
+
}
|
|
2222
|
+
});
|
|
2223
|
+
Object.defineProperty(exports, "validateDokployToken", {
|
|
2224
|
+
enumerable: true,
|
|
2225
|
+
get: function() {
|
|
2226
|
+
return validateDokployToken;
|
|
2227
|
+
}
|
|
2228
|
+
});
|
|
2229
|
+
Object.defineProperty(exports, "whoamiCommand", {
|
|
2230
|
+
enumerable: true,
|
|
2231
|
+
get: function() {
|
|
2232
|
+
return whoamiCommand;
|
|
2233
|
+
}
|
|
2234
|
+
});
|
|
2235
|
+
|
|
2236
|
+
//# sourceMappingURL=sniffer-BSwaL-dX.cjs.map
|