@geekmidas/cli 10.0.0-alpha.64 → 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-Bx2lHHvu.cjs → HostingerProvider-Dlmbfass.cjs} +3 -3
- package/dist/{HostingerProvider-Bx2lHHvu.cjs.map → HostingerProvider-Dlmbfass.cjs.map} +1 -1
- package/dist/{HostingerProvider-Cd4aS5Kj.mjs → HostingerProvider-x9gQTAGZ.mjs} +2 -2
- package/dist/{HostingerProvider-Cd4aS5Kj.mjs.map → HostingerProvider-x9gQTAGZ.mjs.map} +1 -1
- package/dist/{LegacyStateStore-C2abV49N.cjs → LegacyStateStore-BGoo__sO.cjs} +2 -2
- package/dist/{LegacyStateStore-C2abV49N.cjs.map → LegacyStateStore-BGoo__sO.cjs.map} +1 -1
- package/dist/{LegacyStateStore-DIgkGaUP.mjs → LegacyStateStore-BUu1nQ5u.mjs} +2 -2
- package/dist/{LegacyStateStore-DIgkGaUP.mjs.map → LegacyStateStore-BUu1nQ5u.mjs.map} +1 -1
- package/dist/{LocalStateStore-DiJqKyPy.cjs → LocalStateStore-93urXyQe.cjs} +2 -2
- package/dist/{LocalStateStore-DiJqKyPy.cjs.map → LocalStateStore-93urXyQe.cjs.map} +1 -1
- package/dist/{LocalStateStore-Cy1ZBW7U.mjs → LocalStateStore-BRhMQRuy.mjs} +2 -2
- package/dist/{LocalStateStore-Cy1ZBW7U.mjs.map → LocalStateStore-BRhMQRuy.mjs.map} +1 -1
- package/dist/{S3StateStore-Ds0V1Lgm.cjs → S3StateStore-F4dxw6kS.cjs} +2 -2
- package/dist/{S3StateStore-Ds0V1Lgm.cjs.map → S3StateStore-F4dxw6kS.cjs.map} +1 -1
- package/dist/{S3StateStore-D3x6ivBL.mjs → S3StateStore-pw1GhWwt.mjs} +2 -2
- package/dist/{S3StateStore-D3x6ivBL.mjs.map → S3StateStore-pw1GhWwt.mjs.map} +1 -1
- package/dist/{SSMStateStore-CLipvx47.cjs → SSMStateStore-Bt9acuAF.cjs} +2 -2
- package/dist/{SSMStateStore-CLipvx47.cjs.map → SSMStateStore-Bt9acuAF.cjs.map} +1 -1
- package/dist/{SSMStateStore-DfF174xQ.mjs → SSMStateStore-Dmj7zzFO.mjs} +2 -2
- package/dist/{SSMStateStore-DfF174xQ.mjs.map → SSMStateStore-Dmj7zzFO.mjs.map} +1 -1
- package/dist/{bundler-C8uJf324.cjs → bundler-D6MlIfBT.cjs} +3 -3
- package/dist/{bundler-C8uJf324.cjs.map → bundler-D6MlIfBT.cjs.map} +1 -1
- package/dist/{bundler-DYgm9xsd.mjs → bundler-D_14JLZF.mjs} +3 -3
- package/dist/{bundler-DYgm9xsd.mjs.map → bundler-D_14JLZF.mjs.map} +1 -1
- package/dist/{cli-D3jofJ6j.cjs → cli-BfH1deAT.cjs} +6 -6
- package/dist/{cli-D3jofJ6j.cjs.map → cli-BfH1deAT.cjs.map} +1 -1
- package/dist/{cli-Bogz-zEj.mjs → cli-P56vvrDQ.mjs} +5 -5
- package/dist/{cli-Bogz-zEj.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-DqNRiQOp.d.mts → config-BonvTW8p.d.mts} +3 -3
- package/dist/{config-Be-4_I6H.d.cts.map → config-BonvTW8p.d.mts.map} +1 -1
- package/dist/{config-Be-4_I6H.d.cts → config-V_X_BMpm.d.cts} +3 -3
- package/dist/{config-DqNRiQOp.d.mts.map → config-V_X_BMpm.d.cts.map} +1 -1
- package/dist/config.cjs +1 -1
- package/dist/config.d.cts +3 -3
- package/dist/config.d.mts +3 -3
- package/dist/config.mjs +1 -1
- package/dist/{credentials-B70vmLcm.cjs → credentials-Bzbg23n8.cjs} +176 -3
- package/dist/credentials-Bzbg23n8.cjs.map +1 -0
- package/dist/{credentials-s0wMeZBU.mjs → credentials-Dq1MnWQf.mjs} +128 -3
- package/dist/credentials-Dq1MnWQf.mjs.map +1 -0
- package/dist/{define-CYs0PEfZ.mjs → define-DR8xGYfL.mjs} +5 -1
- package/dist/define-DR8xGYfL.mjs.map +1 -0
- package/dist/{define-zUsP72ym.cjs → define-DzRz8FgH.cjs} +5 -1
- package/dist/define-DzRz8FgH.cjs.map +1 -0
- package/dist/deploy/public.cjs +18 -18
- package/dist/deploy/public.cjs.map +1 -1
- package/dist/deploy/public.d.cts +7 -4
- package/dist/deploy/public.d.cts.map +1 -1
- package/dist/deploy/public.d.mts +7 -4
- package/dist/deploy/public.d.mts.map +1 -1
- package/dist/deploy/public.mjs +11 -11
- package/dist/{deploy-Dc2LBJaK.mjs → deploy-9MBG_QLd.mjs} +8 -7
- package/dist/deploy-9MBG_QLd.mjs.map +1 -0
- package/dist/{deploy-yUcR2OlW.cjs → deploy-B80idPUZ.cjs} +11 -10
- package/dist/deploy-B80idPUZ.cjs.map +1 -0
- package/dist/{deploy-ZRXIPp1c.mjs → deploy-CZ5nlpsu.mjs} +13 -119
- package/dist/deploy-CZ5nlpsu.mjs.map +1 -0
- package/dist/{deploy-55rJkdKz.cjs → deploy-Cy8igpQF.cjs} +39 -145
- package/dist/deploy-Cy8igpQF.cjs.map +1 -0
- package/dist/{dokploy-CepdXuMl.mjs → dokploy-BdtBePjp.mjs} +3 -3
- package/dist/{dokploy-CepdXuMl.mjs.map → dokploy-BdtBePjp.mjs.map} +1 -1
- package/dist/{dokploy-D1d37rvM.cjs → dokploy-iFY8MY-L.cjs} +3 -3
- package/dist/{dokploy-D1d37rvM.cjs.map → dokploy-iFY8MY-L.cjs.map} +1 -1
- package/dist/{encryption-C9X98u2F.cjs → encryption-B1wJwgae.cjs} +5 -5
- package/dist/{encryption-C9X98u2F.cjs.map → encryption-B1wJwgae.cjs.map} +1 -1
- package/dist/{encryption-CKHjMpCr.mjs → encryption-DF_hgIpl.mjs} +4 -4
- package/dist/{encryption-CKHjMpCr.mjs.map → encryption-DF_hgIpl.mjs.map} +1 -1
- package/dist/{github-D-byNI7T.mjs → github-B0YYE98x.mjs} +3 -3
- package/dist/{github-D-byNI7T.mjs.map → github-B0YYE98x.mjs.map} +1 -1
- package/dist/{github-Dd3tMa1u.cjs → github-CYcmTdFs.cjs} +3 -3
- package/dist/{github-Dd3tMa1u.cjs.map → github-CYcmTdFs.cjs.map} +1 -1
- package/dist/{index-CfE9Y885.d.cts → index-DPUAGJaK.d.cts} +16 -3
- package/dist/index-DPUAGJaK.d.cts.map +1 -0
- package/dist/{index-CJ2qsSpp.d.mts → index-DwKD_8lt.d.mts} +16 -3
- package/dist/index-DwKD_8lt.d.mts.map +1 -0
- package/dist/index.cjs +233 -1667
- package/dist/index.cjs.map +1 -1
- package/dist/index.mjs +196 -1614
- package/dist/index.mjs.map +1 -1
- package/dist/{local-C7wnrcYK.cjs → local-BdH4XN0E.cjs} +1 -178
- package/dist/local-BdH4XN0E.cjs.map +1 -0
- package/dist/{local-Cd8ZYQG3.mjs → local-CWR4Y_kW.mjs} +3 -126
- package/dist/local-CWR4Y_kW.mjs.map +1 -0
- package/dist/{migrate-B4gngTLe.mjs → migrate-B2qL5wj4.mjs} +6 -5
- package/dist/{migrate-B4gngTLe.mjs.map → migrate-B2qL5wj4.mjs.map} +1 -1
- package/dist/{migrate-tx9-P9uz.cjs → migrate-q4pPAXtO.cjs} +19 -18
- package/dist/{migrate-tx9-P9uz.cjs.map → migrate-q4pPAXtO.cjs.map} +1 -1
- package/dist/openapi.d.cts +1 -1
- package/dist/openapi.d.mts +1 -1
- 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/reconcile/public.d.cts +1 -1
- package/dist/reconcile/public.d.mts +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-CJEYK4Gz.d.mts → runtime-BnD7Nn96.d.cts} +2 -2
- package/dist/{runtime-CJEYK4Gz.d.mts.map → runtime-BnD7Nn96.d.cts.map} +1 -1
- package/dist/{runtime-Cjq1jlVX.d.cts → runtime-j-tmFL65.d.mts} +2 -2
- package/dist/{runtime-Cjq1jlVX.d.cts.map → runtime-j-tmFL65.d.mts.map} +1 -1
- 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-Cper040I.mjs → sst-B2HbrDR3.mjs} +4 -4
- package/dist/{sst-Cper040I.mjs.map → sst-B2HbrDR3.mjs.map} +1 -1
- package/dist/{sst-BdALKEas.cjs → sst-DcfsVH6x.cjs} +4 -4
- package/dist/{sst-BdALKEas.cjs.map → sst-DcfsVH6x.cjs.map} +1 -1
- package/dist/{sniffer-49KPbDgq.mjs → state-B8YwmUZU.mjs} +878 -2465
- package/dist/state-B8YwmUZU.mjs.map +1 -0
- package/dist/{sniffer-ByhbEW5A.cjs → state-DF26e_lo.cjs} +811 -2525
- package/dist/state-DF26e_lo.cjs.map +1 -0
- package/dist/{store-D_2ByRtW.cjs → store-CJF1Tqkq.cjs} +2 -2
- package/dist/{store-D_2ByRtW.cjs.map → store-CJF1Tqkq.cjs.map} +1 -1
- package/dist/{store-BaaonhkJ.mjs → store-CLiHnqOY.mjs} +2 -2
- package/dist/{store-BaaonhkJ.mjs.map → store-CLiHnqOY.mjs.map} +1 -1
- package/dist/target/public.cjs +6 -6
- package/dist/target/public.d.cts +2 -2
- package/dist/target/public.d.mts +2 -2
- package/dist/target/public.mjs +3 -3
- package/dist/terminal-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-B_bEQvb9.d.mts → types-BCzMWBb6.d.mts} +43 -11
- package/dist/types-BCzMWBb6.d.mts.map +1 -0
- package/dist/{types-DkrDOMxw.d.cts → types-ByMWkXKP.d.cts} +43 -11
- package/dist/types-ByMWkXKP.d.cts.map +1 -0
- 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-BwgWJK7h.cjs → workspace-BewAHZIo.cjs} +38 -3
- package/dist/workspace-BewAHZIo.cjs.map +1 -0
- package/dist/{workspace-Bwit4q9O.mjs → workspace-C06_pW4g.mjs} +27 -4
- package/dist/workspace-C06_pW4g.mjs.map +1 -0
- package/package.json +13 -13
- package/dist/credentials-B70vmLcm.cjs.map +0 -1
- package/dist/credentials-s0wMeZBU.mjs.map +0 -1
- package/dist/define-CYs0PEfZ.mjs.map +0 -1
- package/dist/define-zUsP72ym.cjs.map +0 -1
- package/dist/deploy-55rJkdKz.cjs.map +0 -1
- package/dist/deploy-Dc2LBJaK.mjs.map +0 -1
- package/dist/deploy-ZRXIPp1c.mjs.map +0 -1
- package/dist/deploy-yUcR2OlW.cjs.map +0 -1
- package/dist/index-CJ2qsSpp.d.mts.map +0 -1
- package/dist/index-CfE9Y885.d.cts.map +0 -1
- package/dist/local-C7wnrcYK.cjs.map +0 -1
- package/dist/local-Cd8ZYQG3.mjs.map +0 -1
- package/dist/resolve-BXsNQjMR.cjs +0 -220
- package/dist/resolve-BXsNQjMR.cjs.map +0 -1
- package/dist/resolve-DZ2IJ0qY.mjs +0 -173
- package/dist/resolve-DZ2IJ0qY.mjs.map +0 -1
- package/dist/runtime-BGNDrHzh.cjs +0 -163
- package/dist/runtime-BGNDrHzh.cjs.map +0 -1
- package/dist/runtime-Cmn-FeYO.mjs +0 -134
- package/dist/runtime-Cmn-FeYO.mjs.map +0 -1
- package/dist/sniffer-49KPbDgq.mjs.map +0 -1
- package/dist/sniffer-ByhbEW5A.cjs.map +0 -1
- package/dist/terminal-Bdv--jhY.cjs +0 -481
- package/dist/terminal-Bdv--jhY.cjs.map +0 -1
- package/dist/terminal-D7Ntz6WE.mjs +0 -440
- package/dist/terminal-D7Ntz6WE.mjs.map +0 -1
- package/dist/types-B_bEQvb9.d.mts.map +0 -1
- package/dist/types-DkrDOMxw.d.cts.map +0 -1
- package/dist/workspace-BwgWJK7h.cjs.map +0 -1
- package/dist/workspace-Bwit4q9O.mjs.map +0 -1
|
@@ -1,24 +1,19 @@
|
|
|
1
1
|
const require_rolldown_runtime = require("./rolldown-runtime-C0BPl7ul.cjs");
|
|
2
|
-
const require_local = require("./local-
|
|
2
|
+
const require_local = require("./local-BdH4XN0E.cjs");
|
|
3
3
|
const require_discover = require("./discover-DHdECueV.cjs");
|
|
4
|
-
const require_sandbox = require("./sandbox-CL--ZDui.cjs");
|
|
5
4
|
const require_derive = require("./derive-DAbo49sm.cjs");
|
|
6
|
-
const require_workspace = require("./workspace-
|
|
5
|
+
const require_workspace = require("./workspace-BewAHZIo.cjs");
|
|
7
6
|
const require_config = require("./config.cjs");
|
|
8
7
|
const require_storage = require("./storage-CSGoC3bK.cjs");
|
|
9
|
-
const require_store = require("./store-
|
|
10
|
-
const
|
|
8
|
+
const require_store = require("./store-CJF1Tqkq.cjs");
|
|
9
|
+
const require_resolve = require("./resolve-N_i4XNd-.cjs");
|
|
11
10
|
const require_fakes = require("./fakes-zW-jNAbQ.cjs");
|
|
12
11
|
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
12
|
let node_fs = require("node:fs");
|
|
17
13
|
let node_fs_promises = require("node:fs/promises");
|
|
18
14
|
let node_os = require("node:os");
|
|
19
15
|
let node_util = require("node:util");
|
|
20
16
|
let _geekmidas_manifest = require("@geekmidas/manifest");
|
|
21
|
-
let node_url = require("node:url");
|
|
22
17
|
let node_child_process = require("node:child_process");
|
|
23
18
|
let node_net = require("node:net");
|
|
24
19
|
let dotenv = require("dotenv");
|
|
@@ -35,191 +30,6 @@ let _aws_sdk_client_sqs = require("@aws-sdk/client-sqs");
|
|
|
35
30
|
let pg = require("pg");
|
|
36
31
|
let yaml = require("yaml");
|
|
37
32
|
let _geekmidas_cache_postgres = require("@geekmidas/cache/postgres");
|
|
38
|
-
//#region src/prompt.ts
|
|
39
|
-
/**
|
|
40
|
-
* Asking the person at the terminal.
|
|
41
|
-
*
|
|
42
|
-
* CLI layer only: a command's action may prompt, nothing it calls may. A
|
|
43
|
-
* library call (`deploy()`) that needs something it was not given raises a
|
|
44
|
-
* named error instead, and the command decides whether to ask.
|
|
45
|
-
*/
|
|
46
|
-
/** A prompt was needed with no terminal to ask at. */
|
|
47
|
-
var PromptNeedsTerminal = class extends Error {
|
|
48
|
-
question;
|
|
49
|
-
instead;
|
|
50
|
-
constructor(question, instead) {
|
|
51
|
-
super(`Interactive input required. ${instead}`);
|
|
52
|
-
this.question = question;
|
|
53
|
-
this.instead = instead;
|
|
54
|
-
this.name = "PromptNeedsTerminal";
|
|
55
|
-
}
|
|
56
|
-
};
|
|
57
|
-
/** Whether there is a person at a terminal to ask. */
|
|
58
|
-
function canPrompt() {
|
|
59
|
-
return process.stdin.isTTY === true;
|
|
60
|
-
}
|
|
61
|
-
/**
|
|
62
|
-
* Ask `message` and resolve with the line typed — without echoing it when
|
|
63
|
-
* `hidden`. Ctrl+C at a hidden prompt exits, as it would anywhere else in a
|
|
64
|
-
* terminal program: raw mode has taken the signal away from the shell.
|
|
65
|
-
*/
|
|
66
|
-
async function prompt(message, options) {
|
|
67
|
-
if (!canPrompt()) throw new PromptNeedsTerminal(message, options.instead);
|
|
68
|
-
if (options.hidden) {
|
|
69
|
-
process.stdout.write(message);
|
|
70
|
-
return new Promise((resolve, reject) => {
|
|
71
|
-
let value = "";
|
|
72
|
-
const cleanup = () => {
|
|
73
|
-
process.stdin.setRawMode(false);
|
|
74
|
-
process.stdin.pause();
|
|
75
|
-
process.stdin.removeListener("data", onData);
|
|
76
|
-
process.stdin.removeListener("error", onError);
|
|
77
|
-
};
|
|
78
|
-
const onError = (err) => {
|
|
79
|
-
cleanup();
|
|
80
|
-
reject(err);
|
|
81
|
-
};
|
|
82
|
-
const onData = (char) => {
|
|
83
|
-
const c = char.toString();
|
|
84
|
-
if (c === "\n" || c === "\r") {
|
|
85
|
-
cleanup();
|
|
86
|
-
process.stdout.write("\n");
|
|
87
|
-
resolve(value);
|
|
88
|
-
} else if (c === "") {
|
|
89
|
-
cleanup();
|
|
90
|
-
process.stdout.write("\n");
|
|
91
|
-
process.exit(1);
|
|
92
|
-
} else if (c === "" || c === "\b") {
|
|
93
|
-
if (value.length > 0) value = value.slice(0, -1);
|
|
94
|
-
} else value += c;
|
|
95
|
-
};
|
|
96
|
-
process.stdin.setRawMode(true);
|
|
97
|
-
process.stdin.resume();
|
|
98
|
-
process.stdin.on("data", onData);
|
|
99
|
-
process.stdin.on("error", onError);
|
|
100
|
-
});
|
|
101
|
-
}
|
|
102
|
-
const rl = node_readline_promises.createInterface({
|
|
103
|
-
input: node_process.stdin,
|
|
104
|
-
output: node_process.stdout
|
|
105
|
-
});
|
|
106
|
-
try {
|
|
107
|
-
return await rl.question(message);
|
|
108
|
-
} finally {
|
|
109
|
-
rl.close();
|
|
110
|
-
}
|
|
111
|
-
}
|
|
112
|
-
//#endregion
|
|
113
|
-
//#region src/auth/index.ts
|
|
114
|
-
const logger$2 = console;
|
|
115
|
-
/**
|
|
116
|
-
* Validate Dokploy token by making a test API call
|
|
117
|
-
*/
|
|
118
|
-
async function validateDokployToken(endpoint, token) {
|
|
119
|
-
const { DokployApi } = await Promise.resolve().then(() => dokploy_api_exports);
|
|
120
|
-
return new DokployApi({
|
|
121
|
-
baseUrl: endpoint,
|
|
122
|
-
token
|
|
123
|
-
}).validateToken();
|
|
124
|
-
}
|
|
125
|
-
/** What answers a login prompt without a terminal. */
|
|
126
|
-
const LOGIN_INSTEAD = "Please provide --token option.";
|
|
127
|
-
/**
|
|
128
|
-
* Login to a service
|
|
129
|
-
*/
|
|
130
|
-
async function loginCommand(options) {
|
|
131
|
-
const { provider, token: providedToken, endpoint: providedEndpoint } = options;
|
|
132
|
-
if (provider === "dokploy") {
|
|
133
|
-
logger$2.log("\n🔐 Logging in to Dokploy...\n");
|
|
134
|
-
let endpoint = providedEndpoint;
|
|
135
|
-
if (!endpoint) endpoint = await prompt("Dokploy URL (e.g., https://dokploy.example.com): ", { instead: LOGIN_INSTEAD });
|
|
136
|
-
endpoint = endpoint.replace(/\/$/, "");
|
|
137
|
-
try {
|
|
138
|
-
new URL(endpoint);
|
|
139
|
-
} catch {
|
|
140
|
-
logger$2.error("Invalid URL format");
|
|
141
|
-
process.exit(1);
|
|
142
|
-
}
|
|
143
|
-
let token = providedToken;
|
|
144
|
-
if (!token) {
|
|
145
|
-
logger$2.log(`\nGenerate a token at: ${endpoint}/settings/profile\n`);
|
|
146
|
-
token = await prompt("API Token: ", {
|
|
147
|
-
hidden: true,
|
|
148
|
-
instead: LOGIN_INSTEAD
|
|
149
|
-
});
|
|
150
|
-
}
|
|
151
|
-
if (!token) {
|
|
152
|
-
logger$2.error("Token is required");
|
|
153
|
-
process.exit(1);
|
|
154
|
-
}
|
|
155
|
-
logger$2.log("\nValidating credentials...");
|
|
156
|
-
if (!await validateDokployToken(endpoint, token)) {
|
|
157
|
-
logger$2.error("\n✗ Invalid credentials. Please check your token and try again.");
|
|
158
|
-
process.exit(1);
|
|
159
|
-
}
|
|
160
|
-
await require_local.storeDokployCredentials(token, endpoint);
|
|
161
|
-
logger$2.log("\n✓ Successfully logged in to Dokploy!");
|
|
162
|
-
logger$2.log(` Endpoint: ${endpoint}`);
|
|
163
|
-
logger$2.log(` Credentials stored in: ${require_local.getCredentialsPath()}`);
|
|
164
|
-
logger$2.log("\nYou can now use deploy commands without setting DOKPLOY_API_TOKEN.");
|
|
165
|
-
}
|
|
166
|
-
if (provider === "hostinger") {
|
|
167
|
-
logger$2.log("\n🔐 Logging in to Hostinger...\n");
|
|
168
|
-
let token = providedToken;
|
|
169
|
-
if (!token) {
|
|
170
|
-
logger$2.log("\nGenerate a token at: https://hpanel.hostinger.com/profile/api\n");
|
|
171
|
-
token = await prompt("API Token: ", {
|
|
172
|
-
hidden: true,
|
|
173
|
-
instead: LOGIN_INSTEAD
|
|
174
|
-
});
|
|
175
|
-
}
|
|
176
|
-
if (!token) {
|
|
177
|
-
logger$2.error("Token is required");
|
|
178
|
-
process.exit(1);
|
|
179
|
-
}
|
|
180
|
-
await require_local.storeHostingerToken(token);
|
|
181
|
-
logger$2.log("\n✓ Hostinger token stored.");
|
|
182
|
-
logger$2.log(` Credentials stored in: ${require_local.getCredentialsPath()}`);
|
|
183
|
-
logger$2.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.");
|
|
184
|
-
logger$2.log("\nYou can now use it as a DNS provider without setting HOSTINGER_API_TOKEN.");
|
|
185
|
-
}
|
|
186
|
-
}
|
|
187
|
-
/**
|
|
188
|
-
* Logout from a service
|
|
189
|
-
*/
|
|
190
|
-
async function logoutCommand(options) {
|
|
191
|
-
const { provider = "dokploy" } = options;
|
|
192
|
-
if (provider === "all") {
|
|
193
|
-
if (await require_local.removeDokployCredentials()) logger$2.log("\n✓ Logged out from all services");
|
|
194
|
-
else logger$2.log("\nNo stored credentials found");
|
|
195
|
-
return;
|
|
196
|
-
}
|
|
197
|
-
if (provider === "dokploy") {
|
|
198
|
-
if (await require_local.removeDokployCredentials()) logger$2.log("\n✓ Logged out from Dokploy");
|
|
199
|
-
else logger$2.log("\nNo Dokploy credentials found");
|
|
200
|
-
}
|
|
201
|
-
}
|
|
202
|
-
/**
|
|
203
|
-
* Show current login status
|
|
204
|
-
*/
|
|
205
|
-
async function whoamiCommand() {
|
|
206
|
-
logger$2.log("\n📋 Current credentials:\n");
|
|
207
|
-
const dokploy = await require_local.getDokployCredentials();
|
|
208
|
-
if (dokploy) {
|
|
209
|
-
logger$2.log(" Dokploy:");
|
|
210
|
-
logger$2.log(` Endpoint: ${dokploy.endpoint}`);
|
|
211
|
-
logger$2.log(` Token: ${maskToken(dokploy.token)}`);
|
|
212
|
-
} else logger$2.log(" Dokploy: Not logged in");
|
|
213
|
-
logger$2.log(`\n Credentials file: ${require_local.getCredentialsPath()}`);
|
|
214
|
-
}
|
|
215
|
-
/**
|
|
216
|
-
* Mask a token for display
|
|
217
|
-
*/
|
|
218
|
-
function maskToken(token) {
|
|
219
|
-
if (token.length <= 8) return "****";
|
|
220
|
-
return `${token.slice(0, 4)}...${token.slice(-4)}`;
|
|
221
|
-
}
|
|
222
|
-
//#endregion
|
|
223
33
|
//#region src/credentials/index.ts
|
|
224
34
|
const logger$1 = console;
|
|
225
35
|
/**
|
|
@@ -456,17 +266,6 @@ async function prepareEntryCredentials(options) {
|
|
|
456
266
|
};
|
|
457
267
|
}
|
|
458
268
|
//#endregion
|
|
459
|
-
//#region src/workspace/client-generator.ts
|
|
460
|
-
/**
|
|
461
|
-
* Normalize routes to an array of patterns.
|
|
462
|
-
* Handles a glob or a list of them.
|
|
463
|
-
* @internal Exported for use in dev command
|
|
464
|
-
*/
|
|
465
|
-
function normalizeRoutes(routes) {
|
|
466
|
-
if (!routes) return [];
|
|
467
|
-
return Array.isArray(routes) ? routes : [routes];
|
|
468
|
-
}
|
|
469
|
-
//#endregion
|
|
470
269
|
//#region src/reconcile/containers.ts
|
|
471
270
|
/**
|
|
472
271
|
* What gkm knows about each container it can derive.
|
|
@@ -674,7 +473,7 @@ function volumeOf(container) {
|
|
|
674
473
|
*/
|
|
675
474
|
function providerOf(workspace) {
|
|
676
475
|
const target = workspace.deploy?.default;
|
|
677
|
-
return target ?
|
|
476
|
+
return target ? require_resolve.runtimeOf(target, workspace) : "aws";
|
|
678
477
|
}
|
|
679
478
|
/**
|
|
680
479
|
* The cache backend a target implies.
|
|
@@ -3361,883 +3160,637 @@ function localAddress(port) {
|
|
|
3361
3160
|
return `http://localhost:${port}`;
|
|
3362
3161
|
}
|
|
3363
3162
|
//#endregion
|
|
3364
|
-
//#region src/
|
|
3365
|
-
|
|
3366
|
-
|
|
3367
|
-
|
|
3368
|
-
|
|
3369
|
-
});
|
|
3370
|
-
|
|
3371
|
-
|
|
3372
|
-
|
|
3373
|
-
|
|
3374
|
-
/**
|
|
3375
|
-
* Encrypt secrets for embedding in a bundle.
|
|
3376
|
-
* Uses AES-256-GCM with a randomly generated ephemeral key.
|
|
3377
|
-
*
|
|
3378
|
-
* @param secrets - Key-value pairs to encrypt
|
|
3379
|
-
* @returns Encrypted payload with ephemeral master key
|
|
3380
|
-
*/
|
|
3381
|
-
function encryptSecrets(secrets) {
|
|
3382
|
-
const masterKey = (0, node_crypto.randomBytes)(KEY_LENGTH);
|
|
3383
|
-
const iv = (0, node_crypto.randomBytes)(IV_LENGTH);
|
|
3384
|
-
const plaintext = JSON.stringify(secrets);
|
|
3385
|
-
const cipher = (0, node_crypto.createCipheriv)(ALGORITHM, masterKey, iv);
|
|
3386
|
-
const ciphertext = Buffer.concat([cipher.update(plaintext, "utf-8"), cipher.final()]);
|
|
3387
|
-
const authTag = cipher.getAuthTag();
|
|
3388
|
-
return {
|
|
3389
|
-
encrypted: Buffer.concat([ciphertext, authTag]).toString("base64"),
|
|
3390
|
-
iv: iv.toString("hex"),
|
|
3391
|
-
masterKey: masterKey.toString("hex")
|
|
3392
|
-
};
|
|
3393
|
-
}
|
|
3163
|
+
//#region src/deploy/domain.ts
|
|
3164
|
+
/** A stage was deployed that `deploy.domains` gives no domain. */
|
|
3165
|
+
var NoDomainForStage = class extends Error {
|
|
3166
|
+
stage;
|
|
3167
|
+
constructor(stage) {
|
|
3168
|
+
super(`No domain for stage "${stage}". Add it to gkm.config.ts: deploy: { domains: { ${stage}: 'example.com' } }.`);
|
|
3169
|
+
this.stage = stage;
|
|
3170
|
+
this.name = "NoDomainForStage";
|
|
3171
|
+
}
|
|
3172
|
+
};
|
|
3394
3173
|
/**
|
|
3395
|
-
*
|
|
3396
|
-
*
|
|
3174
|
+
* The hostname an app answers on for a stage.
|
|
3175
|
+
*
|
|
3176
|
+
* 1. An explicit `app.domain` — a string, or one per stage — wins.
|
|
3177
|
+
* 2. Otherwise from the stage's base domain in `deploy.domains`: the root site
|
|
3178
|
+
* answers on the domain itself, and every other app on
|
|
3179
|
+
* `{subdomain}.{domain}` — its declaration's `subdomain`, or its own name.
|
|
3180
|
+
*
|
|
3181
|
+
* @throws {NoDomainForStage} when `deploy.domains` names no domain for it
|
|
3397
3182
|
*/
|
|
3398
|
-
function
|
|
3399
|
-
|
|
3400
|
-
|
|
3401
|
-
|
|
3402
|
-
}
|
|
3183
|
+
function resolveHost(appName, app, stage, domains, isMainFrontend) {
|
|
3184
|
+
if (app.domain) {
|
|
3185
|
+
if (typeof app.domain === "string") return app.domain;
|
|
3186
|
+
if (app.domain[stage]) return app.domain[stage];
|
|
3187
|
+
}
|
|
3188
|
+
const baseDomain = domains?.[stage];
|
|
3189
|
+
if (!baseDomain) throw new NoDomainForStage(stage);
|
|
3190
|
+
if (isMainFrontend) return baseDomain;
|
|
3191
|
+
return `${app.subdomain ?? appName}.${baseDomain}`;
|
|
3403
3192
|
}
|
|
3404
3193
|
/**
|
|
3405
|
-
*
|
|
3406
|
-
*
|
|
3194
|
+
* Whether this app is the site the base domain points at — see `rootSite`,
|
|
3195
|
+
* which the local edge asks too.
|
|
3407
3196
|
*
|
|
3408
|
-
*
|
|
3409
|
-
* build used — and match it against the one they hold — without the output
|
|
3410
|
-
* carrying a value that decrypts every secret of the stage.
|
|
3197
|
+
* @throws {AmbiguousRootSite} when several sites could be the root and none says it is
|
|
3411
3198
|
*/
|
|
3412
|
-
function
|
|
3413
|
-
|
|
3199
|
+
function isMainFrontendApp(appName, app, allApps) {
|
|
3200
|
+
if (app.type !== "web") return false;
|
|
3201
|
+
return rootSite(Object.entries(allApps).filter(([, a]) => a.type === "web").map(([name, a]) => ({
|
|
3202
|
+
name,
|
|
3203
|
+
root: a.root
|
|
3204
|
+
}))) === appName;
|
|
3414
3205
|
}
|
|
3415
|
-
//#endregion
|
|
3416
|
-
//#region src/deploy/generated.ts
|
|
3417
3206
|
/**
|
|
3418
|
-
*
|
|
3419
|
-
* provision and nobody else issues.
|
|
3420
|
-
*
|
|
3421
|
-
* - **A seed**, which salts every password a deploy derives — a database's
|
|
3422
|
-
* master and roles, a bucket's root user. Derived from repo facts alone
|
|
3423
|
-
* (project, stage, construct id), those passwords were computable by anyone
|
|
3424
|
-
* who could read the repo.
|
|
3425
|
-
* - **Each `secret` construct's value** — an auth server's signing secret —
|
|
3426
|
-
* random, rather than derived.
|
|
3427
|
-
* - **Each `encryption` construct's keyring** — one random key and an index
|
|
3428
|
-
* key. Later keys are added by `gkm encryption:rotate`, never by a deploy.
|
|
3207
|
+
* Get public URL arg names from app dependencies.
|
|
3429
3208
|
*
|
|
3430
|
-
*
|
|
3431
|
-
*
|
|
3432
|
-
* a
|
|
3433
|
-
* `gkm secrets:set` is kept, never replaced.
|
|
3434
|
-
*/
|
|
3435
|
-
/**
|
|
3436
|
-
* The stage's secrets with its seed and every declared `secret`'s value
|
|
3437
|
-
* present, generating what is missing. Pure: the caller writes the result
|
|
3438
|
-
* back to the stage's store when `generated` is not empty.
|
|
3209
|
+
* @param app - The web/mobile app configuration
|
|
3210
|
+
* @returns Array of arg names like 'NEXT_PUBLIC_API_URL', or [] for frameworks
|
|
3211
|
+
* without a public prefix (e.g. Remix).
|
|
3439
3212
|
*/
|
|
3440
|
-
function
|
|
3441
|
-
const
|
|
3442
|
-
|
|
3443
|
-
|
|
3444
|
-
if (declaration?.kind !== "secret" && declaration?.kind !== "encryption") continue;
|
|
3445
|
-
const key = declaration.provides?.[0];
|
|
3446
|
-
if (!key || custom[key]) continue;
|
|
3447
|
-
custom[key] = declaration.kind === "encryption" ? (0, _geekmidas_constructs_encryption.generateKeyring)() : random();
|
|
3448
|
-
generated.push(key);
|
|
3449
|
-
}
|
|
3450
|
-
const seed = secrets.seed ?? random();
|
|
3451
|
-
if (!secrets.seed) generated.push("seed");
|
|
3452
|
-
return {
|
|
3453
|
-
secrets: generated.length ? {
|
|
3454
|
-
...secrets,
|
|
3455
|
-
seed,
|
|
3456
|
-
custom,
|
|
3457
|
-
updatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
3458
|
-
} : secrets,
|
|
3459
|
-
generated
|
|
3460
|
-
};
|
|
3461
|
-
}
|
|
3462
|
-
/** 256 bits, URL-safe. */
|
|
3463
|
-
function random() {
|
|
3464
|
-
return (0, node_crypto.randomBytes)(32).toString("base64url");
|
|
3213
|
+
function getPublicUrlArgNames(app) {
|
|
3214
|
+
const prefix = require_workspace.getPublicEnvPrefix(app.framework);
|
|
3215
|
+
if (!prefix) return [];
|
|
3216
|
+
return app.dependencies.map((dep) => `${prefix}${dep.toUpperCase()}_URL`);
|
|
3465
3217
|
}
|
|
3466
3218
|
//#endregion
|
|
3467
|
-
//#region src/
|
|
3219
|
+
//#region src/docker/imageRef.ts
|
|
3468
3220
|
/**
|
|
3469
|
-
*
|
|
3221
|
+
* An image reference, checked before it reaches `docker`.
|
|
3222
|
+
*
|
|
3223
|
+
* The parts come from the workspace — a registry in `gkm.config.ts`, an image
|
|
3224
|
+
* name from the project, a tag from `--tag` — and an argument array already
|
|
3225
|
+
* keeps them out of a shell. What it cannot do is stop `docker` reading a value
|
|
3226
|
+
* that starts with `-` as one of its own flags, so a ref is held to Docker's
|
|
3227
|
+
* own grammar (distribution/reference) and anything else is refused by name
|
|
3228
|
+
* before a build starts.
|
|
3470
3229
|
*/
|
|
3471
|
-
|
|
3472
|
-
|
|
3230
|
+
const DOMAIN_COMPONENT = "(?:[a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9-]*[a-zA-Z0-9])";
|
|
3231
|
+
const DOMAIN = `${DOMAIN_COMPONENT}(?:\\.${DOMAIN_COMPONENT})*(?::[0-9]+)?`;
|
|
3232
|
+
const PATH_COMPONENT = "[a-z0-9]+(?:(?:[._]|__|-+)[a-z0-9]+)*";
|
|
3233
|
+
const REFERENCE = new RegExp(`^(?:${DOMAIN}/)?${PATH_COMPONENT}(?:/${PATH_COMPONENT})*(?::[\\w][\\w.-]{0,127})?(?:@[A-Za-z][A-Za-z0-9]*(?:[-_+.][A-Za-z][A-Za-z0-9]*)*:[0-9a-fA-F]{32,})?$`);
|
|
3234
|
+
/** Docker refuses a repository name longer than this. */
|
|
3235
|
+
const MAX_NAME_LENGTH = 255;
|
|
3236
|
+
/** A value that is not an image reference was about to be built or pushed. */
|
|
3237
|
+
var ImageRefInvalid = class extends Error {
|
|
3238
|
+
ref;
|
|
3239
|
+
constructor(ref) {
|
|
3240
|
+
super(`'${ref}' is not a valid image reference. An image is \`[registry/]name[:tag]\`: the name lowercase letters, digits and . _ - separators, the tag up to 128 of letters, digits, _ . and -, not starting with . or -. Check docker.registry in gkm.config.ts and the --tag passed.`);
|
|
3241
|
+
this.ref = ref;
|
|
3242
|
+
this.name = "ImageRefInvalid";
|
|
3243
|
+
}
|
|
3244
|
+
};
|
|
3245
|
+
/** Throw {@link ImageRefInvalid} unless `ref` is a Docker image reference. */
|
|
3246
|
+
function validateImageRef(ref) {
|
|
3247
|
+
const name = ref.split("@")[0].replace(/:[\w][\w.-]*$/, "");
|
|
3248
|
+
if (!REFERENCE.test(ref) || name.length > MAX_NAME_LENGTH) throw new ImageRefInvalid(ref);
|
|
3249
|
+
return ref;
|
|
3473
3250
|
}
|
|
3474
3251
|
//#endregion
|
|
3475
|
-
//#region src/
|
|
3252
|
+
//#region src/docker/templates.ts
|
|
3253
|
+
const LOCKFILES = [
|
|
3254
|
+
["pnpm-lock.yaml", "pnpm"],
|
|
3255
|
+
["bun.lockb", "bun"],
|
|
3256
|
+
["yarn.lock", "yarn"],
|
|
3257
|
+
["package-lock.json", "npm"]
|
|
3258
|
+
];
|
|
3476
3259
|
/**
|
|
3477
|
-
*
|
|
3478
|
-
*
|
|
3479
|
-
* `StateProvider` reads and writes a whole stage at once, with nothing to stop
|
|
3480
|
-
* two runs of the same stage from overwriting each other and nowhere to record
|
|
3481
|
-
* a resource the moment it exists. A `StateStore` adds the three things a
|
|
3482
|
-
* deploy needs to be safe against crashes and concurrent runs:
|
|
3483
|
-
*
|
|
3484
|
-
* - `lock(stage)`: one run per stage. A second gets `StateLocked`, naming the
|
|
3485
|
-
* holder, instead of racing the first.
|
|
3486
|
-
* - Versioned `read` and conditional `write`: a write names the version it was
|
|
3487
|
-
* based on, and raises `StateVersionConflict` when the stored state moved on
|
|
3488
|
-
* underneath it, so nothing is lost silently even without the lock.
|
|
3489
|
-
* - Per-resource records: `putResource` journals one resource at a time, so a
|
|
3490
|
-
* run that dies half way leaves behind the ids of everything it created.
|
|
3491
|
-
*
|
|
3492
|
-
* Built-in stores: `LocalStateStore`, `SSMStateStore`, `S3StateStore`. A
|
|
3493
|
-
* custom `StateProvider` is wrapped by `LegacyStateStore`, which keeps it
|
|
3494
|
-
* working but cannot lock.
|
|
3260
|
+
* Detect package manager from lockfiles
|
|
3261
|
+
* Walks up the directory tree to find lockfile (for monorepos)
|
|
3495
3262
|
*/
|
|
3496
|
-
|
|
3497
|
-
|
|
3498
|
-
|
|
3499
|
-
|
|
3500
|
-
|
|
3501
|
-
|
|
3502
|
-
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}\`.`);
|
|
3503
|
-
this.stage = stage;
|
|
3504
|
-
this.holder = holder;
|
|
3505
|
-
this.location = location;
|
|
3506
|
-
this.name = "StateLocked";
|
|
3507
|
-
}
|
|
3508
|
-
};
|
|
3509
|
-
var StateVersionConflict = class extends Error {
|
|
3510
|
-
stage;
|
|
3511
|
-
expectedVersion;
|
|
3512
|
-
actualVersion;
|
|
3513
|
-
constructor(stage, expectedVersion, actualVersion) {
|
|
3514
|
-
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.`);
|
|
3515
|
-
this.stage = stage;
|
|
3516
|
-
this.expectedVersion = expectedVersion;
|
|
3517
|
-
this.actualVersion = actualVersion;
|
|
3518
|
-
this.name = "StateVersionConflict";
|
|
3519
|
-
}
|
|
3520
|
-
};
|
|
3521
|
-
var StageStateMissing = class extends Error {
|
|
3522
|
-
stage;
|
|
3523
|
-
constructor(stage) {
|
|
3524
|
-
super(`There is no deploy state for stage '${stage}' to record a resource in. Write the stage's state first (write with expectedVersion: null).`);
|
|
3525
|
-
this.stage = stage;
|
|
3526
|
-
this.name = "StageStateMissing";
|
|
3527
|
-
}
|
|
3528
|
-
};
|
|
3529
|
-
var StateUnreadable = class extends Error {
|
|
3530
|
-
stage;
|
|
3531
|
-
location;
|
|
3532
|
-
constructor(stage, location, cause) {
|
|
3533
|
-
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 });
|
|
3534
|
-
this.stage = stage;
|
|
3535
|
-
this.location = location;
|
|
3536
|
-
this.name = "StateUnreadable";
|
|
3537
|
-
}
|
|
3538
|
-
};
|
|
3539
|
-
var StateSchemaTooNew = class extends Error {
|
|
3540
|
-
stage;
|
|
3541
|
-
schemaVersion;
|
|
3542
|
-
constructor(stage, schemaVersion) {
|
|
3543
|
-
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.`);
|
|
3544
|
-
this.stage = stage;
|
|
3545
|
-
this.schemaVersion = schemaVersion;
|
|
3546
|
-
this.name = "StateSchemaTooNew";
|
|
3263
|
+
function detectPackageManager(cwd = process.cwd()) {
|
|
3264
|
+
let dir = cwd;
|
|
3265
|
+
const root = (0, node_path.parse)(dir).root;
|
|
3266
|
+
while (dir !== root) {
|
|
3267
|
+
for (const [lockfile, pm] of LOCKFILES) if ((0, node_fs.existsSync)((0, node_path.join)(dir, lockfile))) return pm;
|
|
3268
|
+
dir = (0, node_path.dirname)(dir);
|
|
3547
3269
|
}
|
|
3548
|
-
|
|
3270
|
+
for (const [lockfile, pm] of LOCKFILES) if ((0, node_fs.existsSync)((0, node_path.join)(root, lockfile))) return pm;
|
|
3271
|
+
return "pnpm";
|
|
3272
|
+
}
|
|
3549
3273
|
/**
|
|
3550
|
-
*
|
|
3551
|
-
*
|
|
3552
|
-
* says so.
|
|
3274
|
+
* Find the lockfile path by walking up the directory tree
|
|
3275
|
+
* Returns the full path to the lockfile, or null if not found
|
|
3553
3276
|
*/
|
|
3554
|
-
|
|
3555
|
-
|
|
3556
|
-
|
|
3557
|
-
|
|
3558
|
-
|
|
3559
|
-
|
|
3560
|
-
|
|
3561
|
-
}
|
|
3562
|
-
|
|
3563
|
-
provider;
|
|
3564
|
-
constructor(provider) {
|
|
3565
|
-
super(`The '${provider}' state provider keys state by workspace name. Set "name" in gkm.config.ts.`);
|
|
3566
|
-
this.provider = provider;
|
|
3567
|
-
this.name = "StateStoreNeedsWorkspaceName";
|
|
3568
|
-
}
|
|
3569
|
-
};
|
|
3570
|
-
var UnknownStateProvider = class extends Error {
|
|
3571
|
-
config;
|
|
3572
|
-
constructor(config) {
|
|
3573
|
-
super(`Unknown state provider ${JSON.stringify(config)}. Use 'local', 'ssm', 's3', or an object implementing StateStore or StateProvider.`);
|
|
3574
|
-
this.config = config;
|
|
3575
|
-
this.name = "UnknownStateProvider";
|
|
3576
|
-
}
|
|
3577
|
-
};
|
|
3578
|
-
/** A lock or write that could not get at the state backend in time. */
|
|
3579
|
-
var StateStoreBusy = class extends Error {
|
|
3580
|
-
stage;
|
|
3581
|
-
location;
|
|
3582
|
-
constructor(stage, location) {
|
|
3583
|
-
super(`Timed out waiting to write deploy state for stage '${stage}' (${location}). If no other gkm process is running, remove ${location}.`);
|
|
3584
|
-
this.stage = stage;
|
|
3585
|
-
this.location = location;
|
|
3586
|
-
this.name = "StateStoreBusy";
|
|
3277
|
+
function findLockfilePath(cwd = process.cwd()) {
|
|
3278
|
+
let dir = cwd;
|
|
3279
|
+
const root = (0, node_path.parse)(dir).root;
|
|
3280
|
+
while (dir !== root) {
|
|
3281
|
+
for (const [lockfile] of LOCKFILES) {
|
|
3282
|
+
const lockfilePath = (0, node_path.join)(dir, lockfile);
|
|
3283
|
+
if ((0, node_fs.existsSync)(lockfilePath)) return lockfilePath;
|
|
3284
|
+
}
|
|
3285
|
+
dir = (0, node_path.dirname)(dir);
|
|
3587
3286
|
}
|
|
3588
|
-
|
|
3589
|
-
|
|
3590
|
-
|
|
3591
|
-
try {
|
|
3592
|
-
parsed = JSON.parse(body);
|
|
3593
|
-
} catch (error) {
|
|
3594
|
-
throw new StateUnreadable(stage, location, error);
|
|
3287
|
+
for (const [lockfile] of LOCKFILES) {
|
|
3288
|
+
const lockfilePath = (0, node_path.join)(root, lockfile);
|
|
3289
|
+
if ((0, node_fs.existsSync)(lockfilePath)) return lockfilePath;
|
|
3595
3290
|
}
|
|
3596
|
-
|
|
3597
|
-
const schemaVersion = parsed.schemaVersion;
|
|
3598
|
-
if (schemaVersion === void 0) return {
|
|
3599
|
-
schemaVersion: 1,
|
|
3600
|
-
state: parsed
|
|
3601
|
-
};
|
|
3602
|
-
if (schemaVersion === 2) return {
|
|
3603
|
-
schemaVersion: 2,
|
|
3604
|
-
document: parsed
|
|
3605
|
-
};
|
|
3606
|
-
throw new StateSchemaTooNew(stage, schemaVersion);
|
|
3607
|
-
}
|
|
3608
|
-
function encodeDocument(document) {
|
|
3609
|
-
return JSON.stringify(document, null, 2);
|
|
3291
|
+
return null;
|
|
3610
3292
|
}
|
|
3611
3293
|
/**
|
|
3612
|
-
*
|
|
3613
|
-
* so the first journaled deploy after an upgrade adopts them rather than
|
|
3614
|
-
* treating them as unknown.
|
|
3294
|
+
* Check if we're in a monorepo (lockfile is in a parent directory)
|
|
3615
3295
|
*/
|
|
3616
|
-
function
|
|
3617
|
-
const
|
|
3618
|
-
|
|
3619
|
-
|
|
3620
|
-
if (!id) return;
|
|
3621
|
-
const key = name ? `${type}:${name}` : type;
|
|
3622
|
-
resources[key] = {
|
|
3623
|
-
key,
|
|
3624
|
-
type,
|
|
3625
|
-
id,
|
|
3626
|
-
status: "ready",
|
|
3627
|
-
updatedAt: now
|
|
3628
|
-
};
|
|
3629
|
-
};
|
|
3630
|
-
seed("project", null, state.projectId);
|
|
3631
|
-
seed("environment", null, state.environmentId);
|
|
3632
|
-
for (const [app, id] of Object.entries(state.applications ?? {})) seed("application", app, id);
|
|
3633
|
-
seed("postgres", null, state.services?.postgresId);
|
|
3634
|
-
seed("redis", null, state.services?.redisId);
|
|
3635
|
-
seed("backup-destination", null, state.backups?.destinationId);
|
|
3636
|
-
return {
|
|
3637
|
-
schemaVersion: 2,
|
|
3638
|
-
stage,
|
|
3639
|
-
serial: 1,
|
|
3640
|
-
state,
|
|
3641
|
-
resources,
|
|
3642
|
-
updatedAt: now
|
|
3643
|
-
};
|
|
3644
|
-
}
|
|
3645
|
-
function newLockHolder(options) {
|
|
3646
|
-
let owner = "unknown";
|
|
3647
|
-
try {
|
|
3648
|
-
owner = (0, node_os.userInfo)().username;
|
|
3649
|
-
} catch {}
|
|
3650
|
-
return {
|
|
3651
|
-
id: (0, node_crypto.randomUUID)(),
|
|
3652
|
-
owner,
|
|
3653
|
-
host: (0, node_os.hostname)(),
|
|
3654
|
-
pid: process.pid,
|
|
3655
|
-
acquiredAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
3656
|
-
operation: options?.operation
|
|
3657
|
-
};
|
|
3658
|
-
}
|
|
3659
|
-
function contentVersion(body) {
|
|
3660
|
-
return (0, node_crypto.createHash)("sha256").update(body).digest("hex").slice(0, 16);
|
|
3296
|
+
function isMonorepo(cwd = process.cwd()) {
|
|
3297
|
+
const lockfilePath = findLockfilePath(cwd);
|
|
3298
|
+
if (!lockfilePath) return false;
|
|
3299
|
+
return (0, node_path.dirname)(lockfilePath) !== cwd;
|
|
3661
3300
|
}
|
|
3662
3301
|
/**
|
|
3663
|
-
*
|
|
3664
|
-
* stage, and create a lock object only if it does not exist. Migration and
|
|
3665
|
-
* resource records are the same for every such backend, so they live here.
|
|
3302
|
+
* Check if turbo.json exists (walks up directory tree)
|
|
3666
3303
|
*/
|
|
3667
|
-
|
|
3668
|
-
|
|
3669
|
-
|
|
3670
|
-
|
|
3671
|
-
|
|
3672
|
-
|
|
3673
|
-
stage,
|
|
3674
|
-
holder,
|
|
3675
|
-
release: async () => {
|
|
3676
|
-
if (released) return;
|
|
3677
|
-
released = true;
|
|
3678
|
-
await this.removeLock(stage, holder);
|
|
3679
|
-
}
|
|
3680
|
-
};
|
|
3681
|
-
}
|
|
3682
|
-
async forceUnlock(stage) {
|
|
3683
|
-
const holder = await this.readLock(stage);
|
|
3684
|
-
await this.removeLock(stage, null);
|
|
3685
|
-
return holder;
|
|
3686
|
-
}
|
|
3687
|
-
async read(stage) {
|
|
3688
|
-
const current = await this.readDocument(stage);
|
|
3689
|
-
if (!current) return null;
|
|
3690
|
-
return {
|
|
3691
|
-
state: current.document.state,
|
|
3692
|
-
resources: current.document.resources,
|
|
3693
|
-
version: current.version
|
|
3694
|
-
};
|
|
3695
|
-
}
|
|
3696
|
-
async write(stage, state, options) {
|
|
3697
|
-
if (options.expectedVersion === null) {
|
|
3698
|
-
const document = {
|
|
3699
|
-
schemaVersion: 2,
|
|
3700
|
-
stage,
|
|
3701
|
-
serial: 1,
|
|
3702
|
-
state,
|
|
3703
|
-
resources: {},
|
|
3704
|
-
updatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
3705
|
-
};
|
|
3706
|
-
return this.writeRaw(stage, encodeDocument(document), null);
|
|
3707
|
-
}
|
|
3708
|
-
return this.mutate(stage, options.expectedVersion, (document) => ({
|
|
3709
|
-
...document,
|
|
3710
|
-
state
|
|
3711
|
-
}));
|
|
3712
|
-
}
|
|
3713
|
-
async putResource(stage, record, options) {
|
|
3714
|
-
return this.mutate(stage, options?.expectedVersion, (document) => ({
|
|
3715
|
-
...document,
|
|
3716
|
-
resources: {
|
|
3717
|
-
...document.resources,
|
|
3718
|
-
[record.key]: {
|
|
3719
|
-
...record,
|
|
3720
|
-
updatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
3721
|
-
}
|
|
3722
|
-
}
|
|
3723
|
-
}));
|
|
3724
|
-
}
|
|
3725
|
-
async deleteResource(stage, key, options) {
|
|
3726
|
-
return this.mutate(stage, options?.expectedVersion, (document) => {
|
|
3727
|
-
const { [key]: _removed, ...resources } = document.resources;
|
|
3728
|
-
return {
|
|
3729
|
-
...document,
|
|
3730
|
-
resources
|
|
3731
|
-
};
|
|
3732
|
-
});
|
|
3733
|
-
}
|
|
3734
|
-
async mutate(stage, expectedVersion, change) {
|
|
3735
|
-
const current = await this.readDocument(stage);
|
|
3736
|
-
if (!current) {
|
|
3737
|
-
if (expectedVersion !== void 0) throw new StateVersionConflict(stage, expectedVersion, null);
|
|
3738
|
-
throw new StageStateMissing(stage);
|
|
3739
|
-
}
|
|
3740
|
-
if (expectedVersion !== void 0 && current.version !== expectedVersion) throw new StateVersionConflict(stage, expectedVersion, current.version);
|
|
3741
|
-
const next = change(current.document);
|
|
3742
|
-
next.serial = current.document.serial + 1;
|
|
3743
|
-
next.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
3744
|
-
return this.writeRaw(stage, encodeDocument(next), current.version);
|
|
3745
|
-
}
|
|
3746
|
-
/**
|
|
3747
|
-
* Reads the stage's document, migrating v1 in place on the way. The
|
|
3748
|
-
* migration is itself a conditional write: if another run migrated (or
|
|
3749
|
-
* wrote) first, its result is read instead.
|
|
3750
|
-
*/
|
|
3751
|
-
async readDocument(stage, attempt = 0) {
|
|
3752
|
-
const raw = await this.readRaw(stage);
|
|
3753
|
-
if (!raw) return null;
|
|
3754
|
-
const decoded = decodeDocument(stage, raw.body, this.location(stage));
|
|
3755
|
-
if (decoded.schemaVersion === 2) return {
|
|
3756
|
-
document: decoded.document,
|
|
3757
|
-
version: raw.version
|
|
3758
|
-
};
|
|
3759
|
-
await this.writeV1Backup(stage, raw.body);
|
|
3760
|
-
const document = migrateV1(stage, decoded.state);
|
|
3761
|
-
try {
|
|
3762
|
-
return {
|
|
3763
|
-
document,
|
|
3764
|
-
version: await this.writeRaw(stage, encodeDocument(document), raw.version)
|
|
3765
|
-
};
|
|
3766
|
-
} catch (error) {
|
|
3767
|
-
if (error instanceof StateVersionConflict && attempt < 2) return this.readDocument(stage, attempt + 1);
|
|
3768
|
-
throw error;
|
|
3769
|
-
}
|
|
3770
|
-
}
|
|
3771
|
-
};
|
|
3772
|
-
function isStateStore(value) {
|
|
3773
|
-
return typeof value === "object" && value !== null && typeof value.lock === "function" && typeof value.read === "function" && typeof value.write === "function" && typeof value.putResource === "function";
|
|
3774
|
-
}
|
|
3775
|
-
/**
|
|
3776
|
-
* The store for a workspace's `state` config:
|
|
3777
|
-
*
|
|
3778
|
-
* - none / `'local'`: `LocalStateStore` under `.gkm/`
|
|
3779
|
-
* - `'ssm'`: `SSMStateStore`, with no local cache — SSM is read directly, so
|
|
3780
|
-
* a stale local copy can never win over it
|
|
3781
|
-
* - `'s3'`: `S3StateStore`
|
|
3782
|
-
* - a `StateStore` object: used as is
|
|
3783
|
-
* - a `StateProvider` object: wrapped by `LegacyStateStore`
|
|
3784
|
-
*/
|
|
3785
|
-
async function createStateStore(options) {
|
|
3786
|
-
const { config, workspaceRoot, workspaceName } = options;
|
|
3787
|
-
if (!config || config.provider === "local") {
|
|
3788
|
-
const { LocalStateStore } = await Promise.resolve().then(() => require("./LocalStateStore-DiJqKyPy.cjs")).then((n) => n.LocalStateStore_exports);
|
|
3789
|
-
return new LocalStateStore(workspaceRoot);
|
|
3790
|
-
}
|
|
3791
|
-
if (isStateStore(config.provider)) return config.provider;
|
|
3792
|
-
if (isStateProvider(config.provider)) {
|
|
3793
|
-
const { LegacyStateStore } = await Promise.resolve().then(() => require("./LegacyStateStore-C2abV49N.cjs"));
|
|
3794
|
-
return new LegacyStateStore(config.provider, { warn: options.warn });
|
|
3795
|
-
}
|
|
3796
|
-
if (config.provider === "ssm") {
|
|
3797
|
-
if (!workspaceName) throw new StateStoreNeedsWorkspaceName("ssm");
|
|
3798
|
-
const { SSMStateStore } = await Promise.resolve().then(() => require("./SSMStateStore-CLipvx47.cjs"));
|
|
3799
|
-
const ssm = config;
|
|
3800
|
-
return SSMStateStore.create({
|
|
3801
|
-
workspaceName,
|
|
3802
|
-
region: ssm.region,
|
|
3803
|
-
profile: ssm.profile
|
|
3804
|
-
});
|
|
3805
|
-
}
|
|
3806
|
-
if (config.provider === "s3") {
|
|
3807
|
-
if (!workspaceName) throw new StateStoreNeedsWorkspaceName("s3");
|
|
3808
|
-
const { S3StateStore } = await Promise.resolve().then(() => require("./S3StateStore-Ds0V1Lgm.cjs"));
|
|
3809
|
-
const s3 = config;
|
|
3810
|
-
return S3StateStore.create({
|
|
3811
|
-
workspaceName,
|
|
3812
|
-
bucket: s3.bucket,
|
|
3813
|
-
prefix: s3.prefix,
|
|
3814
|
-
region: s3.region,
|
|
3815
|
-
profile: s3.profile
|
|
3816
|
-
});
|
|
3304
|
+
function hasTurboConfig(cwd = process.cwd()) {
|
|
3305
|
+
let dir = cwd;
|
|
3306
|
+
const root = (0, node_path.parse)(dir).root;
|
|
3307
|
+
while (dir !== root) {
|
|
3308
|
+
if ((0, node_fs.existsSync)((0, node_path.join)(dir, "turbo.json"))) return true;
|
|
3309
|
+
dir = (0, node_path.dirname)(dir);
|
|
3817
3310
|
}
|
|
3818
|
-
|
|
3311
|
+
return (0, node_fs.existsSync)((0, node_path.join)(root, "turbo.json"));
|
|
3819
3312
|
}
|
|
3820
|
-
//#endregion
|
|
3821
|
-
//#region src/deploy/state.ts
|
|
3822
|
-
/**
|
|
3823
|
-
* Deploy state management for Dokploy deployments
|
|
3824
|
-
*
|
|
3825
|
-
* Stores resource IDs (applications, services) per stage to avoid
|
|
3826
|
-
* re-creating resources on subsequent deploys.
|
|
3827
|
-
*/
|
|
3828
3313
|
/**
|
|
3829
|
-
*
|
|
3314
|
+
* Get install command for turbo builds (without frozen lockfile)
|
|
3315
|
+
* Turbo prune creates a subset that may not perfectly match the lockfile
|
|
3830
3316
|
*/
|
|
3831
|
-
function
|
|
3317
|
+
function getTurboInstallCmd(pm) {
|
|
3832
3318
|
return {
|
|
3833
|
-
|
|
3834
|
-
|
|
3835
|
-
|
|
3836
|
-
|
|
3837
|
-
|
|
3838
|
-
services: {},
|
|
3839
|
-
lastDeployedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
3840
|
-
};
|
|
3841
|
-
}
|
|
3842
|
-
/**
|
|
3843
|
-
* Set application ID in state (mutates state)
|
|
3844
|
-
*/
|
|
3845
|
-
function setApplicationId(state, appName, applicationId) {
|
|
3846
|
-
state.applications[appName] = applicationId;
|
|
3847
|
-
}
|
|
3848
|
-
/**
|
|
3849
|
-
* Record the image an app was deployed with (mutates state)
|
|
3850
|
-
*/
|
|
3851
|
-
function setDeployedImage(state, appName, image) {
|
|
3852
|
-
state.images = {
|
|
3853
|
-
...state.images,
|
|
3854
|
-
[appName]: image
|
|
3855
|
-
};
|
|
3856
|
-
}
|
|
3857
|
-
/**
|
|
3858
|
-
* Get a generated secret for an app
|
|
3859
|
-
*/
|
|
3860
|
-
function getGeneratedSecret(state, appName, secretName) {
|
|
3861
|
-
return state?.generatedSecrets?.[appName]?.[secretName];
|
|
3862
|
-
}
|
|
3863
|
-
/**
|
|
3864
|
-
* Set a generated secret for an app (mutates state)
|
|
3865
|
-
*/
|
|
3866
|
-
function setGeneratedSecret(state, appName, secretName, value) {
|
|
3867
|
-
if (!state.generatedSecrets) state.generatedSecrets = {};
|
|
3868
|
-
if (!state.generatedSecrets[appName]) state.generatedSecrets[appName] = {};
|
|
3869
|
-
state.generatedSecrets[appName][secretName] = value;
|
|
3319
|
+
pnpm: "pnpm install",
|
|
3320
|
+
npm: "npm install",
|
|
3321
|
+
yarn: "yarn install",
|
|
3322
|
+
bun: "bun install"
|
|
3323
|
+
}[pm];
|
|
3870
3324
|
}
|
|
3871
3325
|
/**
|
|
3872
|
-
*
|
|
3326
|
+
* Get package manager specific commands and paths
|
|
3873
3327
|
*/
|
|
3874
|
-
function
|
|
3875
|
-
|
|
3876
|
-
|
|
3877
|
-
|
|
3878
|
-
|
|
3879
|
-
|
|
3328
|
+
function getPmConfig(pm) {
|
|
3329
|
+
return {
|
|
3330
|
+
pnpm: {
|
|
3331
|
+
install: "corepack enable && corepack prepare pnpm@latest --activate",
|
|
3332
|
+
lockfile: "pnpm-lock.yaml",
|
|
3333
|
+
fetch: "pnpm fetch",
|
|
3334
|
+
installCmd: "pnpm install --frozen-lockfile --offline",
|
|
3335
|
+
cacheTarget: "/root/.local/share/pnpm/store",
|
|
3336
|
+
cacheId: "pnpm",
|
|
3337
|
+
run: "pnpm",
|
|
3338
|
+
exec: "pnpm exec",
|
|
3339
|
+
dlx: "pnpm dlx",
|
|
3340
|
+
addGlobal: "pnpm add -g"
|
|
3341
|
+
},
|
|
3342
|
+
npm: {
|
|
3343
|
+
install: "",
|
|
3344
|
+
lockfile: "package-lock.json",
|
|
3345
|
+
fetch: "",
|
|
3346
|
+
installCmd: "npm ci",
|
|
3347
|
+
cacheTarget: "/root/.npm",
|
|
3348
|
+
cacheId: "npm",
|
|
3349
|
+
run: "npm run",
|
|
3350
|
+
exec: "npx",
|
|
3351
|
+
dlx: "npx",
|
|
3352
|
+
addGlobal: "npm install -g"
|
|
3353
|
+
},
|
|
3354
|
+
yarn: {
|
|
3355
|
+
install: "corepack enable && corepack prepare yarn@stable --activate",
|
|
3356
|
+
lockfile: "yarn.lock",
|
|
3357
|
+
fetch: "",
|
|
3358
|
+
installCmd: "yarn install --frozen-lockfile",
|
|
3359
|
+
cacheTarget: "/root/.yarn/cache",
|
|
3360
|
+
cacheId: "yarn",
|
|
3361
|
+
run: "yarn",
|
|
3362
|
+
exec: "yarn exec",
|
|
3363
|
+
dlx: "yarn dlx",
|
|
3364
|
+
addGlobal: "yarn global add"
|
|
3365
|
+
},
|
|
3366
|
+
bun: {
|
|
3367
|
+
install: "npm install -g bun",
|
|
3368
|
+
lockfile: "bun.lockb",
|
|
3369
|
+
fetch: "",
|
|
3370
|
+
installCmd: "bun install --frozen-lockfile",
|
|
3371
|
+
cacheTarget: "/root/.bun/install/cache",
|
|
3372
|
+
cacheId: "bun",
|
|
3373
|
+
run: "bun run",
|
|
3374
|
+
exec: "bunx",
|
|
3375
|
+
dlx: "bunx",
|
|
3376
|
+
addGlobal: "bun add -g"
|
|
3377
|
+
}
|
|
3378
|
+
}[pm];
|
|
3880
3379
|
}
|
|
3881
3380
|
/**
|
|
3882
|
-
*
|
|
3381
|
+
* Generate a multi-stage Dockerfile for building from source
|
|
3382
|
+
* Optimized for build speed with:
|
|
3383
|
+
* - BuildKit cache mounts for package manager store
|
|
3384
|
+
* - pnpm fetch for better layer caching (when using pnpm)
|
|
3385
|
+
* - Optional turbo prune for monorepos
|
|
3883
3386
|
*/
|
|
3884
|
-
function
|
|
3885
|
-
|
|
3387
|
+
function generateMultiStageDockerfile(options) {
|
|
3388
|
+
const { baseImage, port, healthCheckPath, turbo, turboPackage, packageManager } = options;
|
|
3389
|
+
if (turbo) return generateTurboDockerfile({
|
|
3390
|
+
...options,
|
|
3391
|
+
turboPackage: turboPackage ?? "api"
|
|
3392
|
+
});
|
|
3393
|
+
const pm = getPmConfig(packageManager);
|
|
3394
|
+
return `# syntax=docker/dockerfile:1
|
|
3395
|
+
# Stage 1: Dependencies
|
|
3396
|
+
FROM ${baseImage} AS deps
|
|
3397
|
+
|
|
3398
|
+
WORKDIR /app
|
|
3399
|
+
${pm.install ? `\n# Install ${packageManager}\nRUN ${pm.install}\n` : ""}
|
|
3400
|
+
${packageManager === "pnpm" ? `# Copy lockfile first for better caching
|
|
3401
|
+
COPY ${pm.lockfile} ./
|
|
3402
|
+
|
|
3403
|
+
# Fetch dependencies (downloads to virtual store, cached separately)
|
|
3404
|
+
RUN --mount=type=cache,id=${pm.cacheId},target=${pm.cacheTarget} \\
|
|
3405
|
+
${pm.fetch}
|
|
3406
|
+
|
|
3407
|
+
# Copy package.json after fetch
|
|
3408
|
+
COPY package.json ./
|
|
3409
|
+
|
|
3410
|
+
# Install from cache (fast - no network needed)
|
|
3411
|
+
RUN --mount=type=cache,id=${pm.cacheId},target=${pm.cacheTarget} \\
|
|
3412
|
+
${pm.installCmd}` : `# Copy package files
|
|
3413
|
+
COPY package.json ${pm.lockfile} ./
|
|
3414
|
+
|
|
3415
|
+
# Install dependencies with cache
|
|
3416
|
+
RUN --mount=type=cache,id=${pm.cacheId},target=${pm.cacheTarget} \\
|
|
3417
|
+
${pm.installCmd}`}
|
|
3418
|
+
|
|
3419
|
+
# Stage 2: Build
|
|
3420
|
+
FROM deps AS builder
|
|
3421
|
+
|
|
3422
|
+
WORKDIR /app
|
|
3423
|
+
|
|
3424
|
+
# Copy source (deps already installed)
|
|
3425
|
+
COPY . .
|
|
3426
|
+
|
|
3427
|
+
# Build any workspace packages the app depends on.
|
|
3428
|
+
#
|
|
3429
|
+
# A no-op for a project whose dependencies come from a registry, and required
|
|
3430
|
+
# for one whose CLI is a sibling package: \`turbo prune\` honours .gitignore, so a
|
|
3431
|
+
# workspace package's \`dist\` never reaches the image and its bin has nothing to
|
|
3432
|
+
# import. \`--if-present\` is what makes the same Dockerfile correct for both.
|
|
3433
|
+
RUN ${pm.run} --if-present build
|
|
3434
|
+
|
|
3435
|
+
# Build production server using gkm
|
|
3436
|
+
RUN ${pm.exec} gkm build --provider server --production
|
|
3437
|
+
|
|
3438
|
+
# Stage 3: Production
|
|
3439
|
+
FROM ${baseImage} AS runner
|
|
3440
|
+
|
|
3441
|
+
WORKDIR /app
|
|
3442
|
+
|
|
3443
|
+
# Install tini for proper signal handling as PID 1
|
|
3444
|
+
RUN apk add --no-cache tini
|
|
3445
|
+
|
|
3446
|
+
# Create non-root user
|
|
3447
|
+
RUN addgroup --system --gid 1001 nodejs && \\
|
|
3448
|
+
adduser --system --uid 1001 hono
|
|
3449
|
+
|
|
3450
|
+
# Copy bundled server
|
|
3451
|
+
COPY --from=builder --chown=hono:nodejs /app/.gkm/server/dist/server.mjs ./
|
|
3452
|
+
|
|
3453
|
+
# Environment
|
|
3454
|
+
ENV NODE_ENV=production
|
|
3455
|
+
ENV PORT=${port}
|
|
3456
|
+
|
|
3457
|
+
# Health check
|
|
3458
|
+
HEALTHCHECK --interval=30s --timeout=10s --start-period=30s --retries=3 \\
|
|
3459
|
+
CMD wget -qO- http://localhost:${port}${healthCheckPath} > /dev/null 2>&1 || exit 1
|
|
3460
|
+
|
|
3461
|
+
# Switch to non-root user
|
|
3462
|
+
USER hono
|
|
3463
|
+
|
|
3464
|
+
EXPOSE ${port}
|
|
3465
|
+
|
|
3466
|
+
# Use tini as entrypoint to handle PID 1 responsibilities
|
|
3467
|
+
ENTRYPOINT ["/sbin/tini", "--"]
|
|
3468
|
+
CMD ["node", "server.mjs"]
|
|
3469
|
+
`;
|
|
3886
3470
|
}
|
|
3887
3471
|
/**
|
|
3888
|
-
*
|
|
3472
|
+
* Generate a Dockerfile optimized for Turbo monorepos
|
|
3473
|
+
* Uses turbo prune to create minimal Docker context
|
|
3889
3474
|
*/
|
|
3890
|
-
function
|
|
3891
|
-
|
|
3892
|
-
|
|
3893
|
-
|
|
3894
|
-
|
|
3895
|
-
|
|
3896
|
-
|
|
3897
|
-
|
|
3898
|
-
|
|
3899
|
-
|
|
3900
|
-
|
|
3901
|
-
|
|
3902
|
-
|
|
3475
|
+
function generateTurboDockerfile(options) {
|
|
3476
|
+
const { baseImage, port, healthCheckPath, turboPackage, packageManager } = options;
|
|
3477
|
+
const pm = getPmConfig(packageManager);
|
|
3478
|
+
const installPm = pm.install ? `RUN ${pm.install}` : "";
|
|
3479
|
+
const turboInstallCmd = getTurboInstallCmd(packageManager);
|
|
3480
|
+
return `# syntax=docker/dockerfile:1
|
|
3481
|
+
# Stage 1: Prune monorepo
|
|
3482
|
+
FROM ${baseImage} AS pruner
|
|
3483
|
+
|
|
3484
|
+
WORKDIR /app
|
|
3485
|
+
|
|
3486
|
+
${installPm}
|
|
3487
|
+
|
|
3488
|
+
COPY . .
|
|
3489
|
+
|
|
3490
|
+
# Prune to only include necessary packages
|
|
3491
|
+
RUN ${packageManager === "pnpm" ? "pnpm dlx turbo" : "npx turbo"} prune ${turboPackage} --docker
|
|
3492
|
+
|
|
3493
|
+
# Stage 2: Install dependencies
|
|
3494
|
+
FROM ${baseImage} AS deps
|
|
3495
|
+
|
|
3496
|
+
WORKDIR /app
|
|
3497
|
+
|
|
3498
|
+
${installPm}
|
|
3499
|
+
|
|
3500
|
+
# Copy pruned lockfile and package.jsons
|
|
3501
|
+
COPY --from=pruner /app/out/${pm.lockfile} ./
|
|
3502
|
+
COPY --from=pruner /app/out/json/ ./
|
|
3503
|
+
|
|
3504
|
+
# Install dependencies (no frozen-lockfile since turbo prune creates a subset)
|
|
3505
|
+
RUN --mount=type=cache,id=${pm.cacheId},target=${pm.cacheTarget} \\
|
|
3506
|
+
${turboInstallCmd}
|
|
3507
|
+
|
|
3508
|
+
# Stage 3: Build
|
|
3509
|
+
FROM deps AS builder
|
|
3510
|
+
|
|
3511
|
+
WORKDIR /app
|
|
3512
|
+
|
|
3513
|
+
# Copy pruned source
|
|
3514
|
+
COPY --from=pruner /app/out/full/ ./
|
|
3515
|
+
|
|
3516
|
+
# Build any workspace packages the app depends on.
|
|
3517
|
+
#
|
|
3518
|
+
# A no-op for a project whose dependencies come from a registry, and required
|
|
3519
|
+
# for one whose CLI is a sibling package: \`turbo prune\` honours .gitignore, so a
|
|
3520
|
+
# workspace package's \`dist\` never reaches the image and its bin has nothing to
|
|
3521
|
+
# import. \`--if-present\` is what makes the same Dockerfile correct for both.
|
|
3522
|
+
RUN ${pm.run} --if-present build
|
|
3523
|
+
|
|
3524
|
+
# Build production server using gkm
|
|
3525
|
+
RUN ${pm.exec} gkm build --provider server --production
|
|
3526
|
+
|
|
3527
|
+
# Stage 4: Production
|
|
3528
|
+
FROM ${baseImage} AS runner
|
|
3529
|
+
|
|
3530
|
+
WORKDIR /app
|
|
3531
|
+
|
|
3532
|
+
RUN apk add --no-cache tini
|
|
3533
|
+
|
|
3534
|
+
RUN addgroup --system --gid 1001 nodejs && \\
|
|
3535
|
+
adduser --system --uid 1001 hono
|
|
3536
|
+
|
|
3537
|
+
COPY --from=builder --chown=hono:nodejs /app/.gkm/server/dist/server.mjs ./
|
|
3538
|
+
|
|
3539
|
+
ENV NODE_ENV=production
|
|
3540
|
+
ENV PORT=${port}
|
|
3541
|
+
|
|
3542
|
+
HEALTHCHECK --interval=30s --timeout=10s --start-period=30s --retries=3 \\
|
|
3543
|
+
CMD wget -qO- http://localhost:${port}${healthCheckPath} > /dev/null 2>&1 || exit 1
|
|
3544
|
+
|
|
3545
|
+
USER hono
|
|
3546
|
+
|
|
3547
|
+
EXPOSE ${port}
|
|
3548
|
+
|
|
3549
|
+
ENTRYPOINT ["/sbin/tini", "--"]
|
|
3550
|
+
CMD ["node", "server.mjs"]
|
|
3551
|
+
`;
|
|
3903
3552
|
}
|
|
3904
3553
|
/**
|
|
3905
|
-
*
|
|
3554
|
+
* Generate a slim Dockerfile for pre-built bundles
|
|
3906
3555
|
*/
|
|
3907
|
-
function
|
|
3908
|
-
|
|
3556
|
+
function generateSlimDockerfile(options) {
|
|
3557
|
+
const { baseImage, port, healthCheckPath, bundle = ".gkm/server/dist/server.mjs" } = options;
|
|
3558
|
+
return `# Slim Dockerfile for pre-built production bundle
|
|
3559
|
+
FROM ${baseImage}
|
|
3560
|
+
|
|
3561
|
+
WORKDIR /app
|
|
3562
|
+
|
|
3563
|
+
# Install tini for proper signal handling as PID 1
|
|
3564
|
+
# Handles SIGTERM propagation and zombie process reaping
|
|
3565
|
+
RUN apk add --no-cache tini
|
|
3566
|
+
|
|
3567
|
+
# Create non-root user
|
|
3568
|
+
RUN addgroup --system --gid 1001 nodejs && \\
|
|
3569
|
+
adduser --system --uid 1001 hono
|
|
3570
|
+
|
|
3571
|
+
# Copy pre-built bundle
|
|
3572
|
+
COPY ${bundle} ./
|
|
3573
|
+
|
|
3574
|
+
# Environment
|
|
3575
|
+
ENV NODE_ENV=production
|
|
3576
|
+
ENV PORT=${port}
|
|
3577
|
+
|
|
3578
|
+
# Health check
|
|
3579
|
+
HEALTHCHECK --interval=30s --timeout=10s --start-period=30s --retries=3 \\
|
|
3580
|
+
CMD wget -qO- http://localhost:${port}${healthCheckPath} > /dev/null 2>&1 || exit 1
|
|
3581
|
+
|
|
3582
|
+
# Switch to non-root user
|
|
3583
|
+
USER hono
|
|
3584
|
+
|
|
3585
|
+
EXPOSE ${port}
|
|
3586
|
+
|
|
3587
|
+
# Use tini as entrypoint to handle PID 1 responsibilities
|
|
3588
|
+
ENTRYPOINT ["/sbin/tini", "--"]
|
|
3589
|
+
CMD ["node", "server.mjs"]
|
|
3590
|
+
`;
|
|
3909
3591
|
}
|
|
3910
3592
|
/**
|
|
3911
|
-
*
|
|
3593
|
+
* Generate .dockerignore file
|
|
3912
3594
|
*/
|
|
3913
|
-
function
|
|
3914
|
-
|
|
3595
|
+
function generateDockerignore() {
|
|
3596
|
+
return `# Dependencies
|
|
3597
|
+
node_modules
|
|
3598
|
+
.pnpm-store
|
|
3599
|
+
|
|
3600
|
+
# Build output (except what we need)
|
|
3601
|
+
.gkm/aws*
|
|
3602
|
+
.gkm/server/*.ts
|
|
3603
|
+
!.gkm/server/dist
|
|
3604
|
+
|
|
3605
|
+
# The key gkm build --stage encrypted secrets with: runtime env only, never
|
|
3606
|
+
# the build context
|
|
3607
|
+
.gkm/server/master.key
|
|
3608
|
+
|
|
3609
|
+
# gkm compose's stacks: each app's env file holds its stage's secrets
|
|
3610
|
+
.gkm/compose
|
|
3611
|
+
|
|
3612
|
+
# IDE and editor
|
|
3613
|
+
.idea
|
|
3614
|
+
.vscode
|
|
3615
|
+
*.swp
|
|
3616
|
+
*.swo
|
|
3617
|
+
|
|
3618
|
+
# Git
|
|
3619
|
+
.git
|
|
3620
|
+
.gitignore
|
|
3621
|
+
|
|
3622
|
+
# Logs
|
|
3623
|
+
*.log
|
|
3624
|
+
npm-debug.log*
|
|
3625
|
+
pnpm-debug.log*
|
|
3626
|
+
|
|
3627
|
+
# Test files
|
|
3628
|
+
**/*.test.ts
|
|
3629
|
+
**/*.spec.ts
|
|
3630
|
+
**/__tests__
|
|
3631
|
+
coverage
|
|
3632
|
+
|
|
3633
|
+
# Documentation
|
|
3634
|
+
docs
|
|
3635
|
+
*.md
|
|
3636
|
+
!README.md
|
|
3637
|
+
|
|
3638
|
+
# Environment files (handle secrets separately)
|
|
3639
|
+
.env
|
|
3640
|
+
.env.*
|
|
3641
|
+
!.env.example
|
|
3642
|
+
|
|
3643
|
+
# Docker files (don't copy recursively)
|
|
3644
|
+
Dockerfile*
|
|
3645
|
+
docker-compose*
|
|
3646
|
+
.dockerignore
|
|
3647
|
+
`;
|
|
3915
3648
|
}
|
|
3916
3649
|
/**
|
|
3917
|
-
*
|
|
3650
|
+
* Generate docker-entrypoint.sh for custom startup logic
|
|
3918
3651
|
*/
|
|
3919
|
-
function
|
|
3920
|
-
|
|
3652
|
+
function generateDockerEntrypoint() {
|
|
3653
|
+
return `#!/bin/sh
|
|
3654
|
+
set -e
|
|
3655
|
+
|
|
3656
|
+
# Run any custom startup scripts here
|
|
3657
|
+
# Example: wait for database
|
|
3658
|
+
# until nc -z $DB_HOST $DB_PORT; do
|
|
3659
|
+
# echo "Waiting for database..."
|
|
3660
|
+
# sleep 1
|
|
3661
|
+
# done
|
|
3662
|
+
|
|
3663
|
+
# Execute the main command
|
|
3664
|
+
exec "$@"
|
|
3665
|
+
`;
|
|
3921
3666
|
}
|
|
3922
|
-
//#endregion
|
|
3923
|
-
//#region src/deploy/domain.ts
|
|
3924
|
-
/** A stage was deployed that `deploy.domains` gives no domain. */
|
|
3925
|
-
var NoDomainForStage = class extends Error {
|
|
3926
|
-
stage;
|
|
3927
|
-
constructor(stage) {
|
|
3928
|
-
super(`No domain for stage "${stage}". Add it to gkm.config.ts: deploy: { domains: { ${stage}: 'example.com' } }.`);
|
|
3929
|
-
this.stage = stage;
|
|
3930
|
-
this.name = "NoDomainForStage";
|
|
3931
|
-
}
|
|
3932
|
-
};
|
|
3933
3667
|
/**
|
|
3934
|
-
*
|
|
3935
|
-
*
|
|
3936
|
-
* 1. An explicit `app.domain` — a string, or one per stage — wins.
|
|
3937
|
-
* 2. Otherwise from the stage's base domain in `deploy.domains`: the root site
|
|
3938
|
-
* answers on the domain itself, and every other app on
|
|
3939
|
-
* `{subdomain}.{domain}` — its declaration's `subdomain`, or its own name.
|
|
3940
|
-
*
|
|
3941
|
-
* @throws {NoDomainForStage} when `deploy.domains` names no domain for it
|
|
3668
|
+
* Resolve Docker configuration from GkmConfig with defaults
|
|
3942
3669
|
*/
|
|
3943
|
-
function
|
|
3944
|
-
|
|
3945
|
-
|
|
3946
|
-
|
|
3947
|
-
|
|
3948
|
-
|
|
3949
|
-
|
|
3950
|
-
|
|
3951
|
-
return `${app.subdomain ?? appName}.${baseDomain}`;
|
|
3670
|
+
function resolveDockerConfig(config) {
|
|
3671
|
+
const docker = config.docker ?? {};
|
|
3672
|
+
return {
|
|
3673
|
+
registry: docker.registry ?? "",
|
|
3674
|
+
imageName: docker.imageName ?? config.name ?? "api",
|
|
3675
|
+
baseImage: docker.baseImage ?? "node:22-alpine",
|
|
3676
|
+
port: docker.port ?? 3e3
|
|
3677
|
+
};
|
|
3952
3678
|
}
|
|
3953
3679
|
/**
|
|
3954
|
-
*
|
|
3955
|
-
*
|
|
3956
|
-
*
|
|
3957
|
-
* @throws {AmbiguousRootSite} when several sites could be the root and none says it is
|
|
3680
|
+
* Generate a Dockerfile for Next.js frontend apps using standalone output.
|
|
3681
|
+
* Uses turbo prune for monorepo optimization.
|
|
3682
|
+
* @internal Exported for testing
|
|
3958
3683
|
*/
|
|
3959
|
-
function
|
|
3960
|
-
|
|
3961
|
-
return rootSite(Object.entries(allApps).filter(([, a]) => a.type === "web").map(([name, a]) => ({
|
|
3962
|
-
name,
|
|
3963
|
-
root: a.root
|
|
3964
|
-
}))) === appName;
|
|
3965
|
-
}
|
|
3966
|
-
/**
|
|
3967
|
-
* Get public URL arg names from app dependencies.
|
|
3968
|
-
*
|
|
3969
|
-
* @param app - The web/mobile app configuration
|
|
3970
|
-
* @returns Array of arg names like 'NEXT_PUBLIC_API_URL', or [] for frameworks
|
|
3971
|
-
* without a public prefix (e.g. Remix).
|
|
3972
|
-
*/
|
|
3973
|
-
function getPublicUrlArgNames(app) {
|
|
3974
|
-
const prefix = require_workspace.getPublicEnvPrefix(app.framework);
|
|
3975
|
-
if (!prefix) return [];
|
|
3976
|
-
return app.dependencies.map((dep) => `${prefix}${dep.toUpperCase()}_URL`);
|
|
3977
|
-
}
|
|
3978
|
-
//#endregion
|
|
3979
|
-
//#region src/docker/imageRef.ts
|
|
3980
|
-
/**
|
|
3981
|
-
* An image reference, checked before it reaches `docker`.
|
|
3982
|
-
*
|
|
3983
|
-
* The parts come from the workspace — a registry in `gkm.config.ts`, an image
|
|
3984
|
-
* name from the project, a tag from `--tag` — and an argument array already
|
|
3985
|
-
* keeps them out of a shell. What it cannot do is stop `docker` reading a value
|
|
3986
|
-
* that starts with `-` as one of its own flags, so a ref is held to Docker's
|
|
3987
|
-
* own grammar (distribution/reference) and anything else is refused by name
|
|
3988
|
-
* before a build starts.
|
|
3989
|
-
*/
|
|
3990
|
-
const DOMAIN_COMPONENT = "(?:[a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9-]*[a-zA-Z0-9])";
|
|
3991
|
-
const DOMAIN = `${DOMAIN_COMPONENT}(?:\\.${DOMAIN_COMPONENT})*(?::[0-9]+)?`;
|
|
3992
|
-
const PATH_COMPONENT = "[a-z0-9]+(?:(?:[._]|__|-+)[a-z0-9]+)*";
|
|
3993
|
-
const REFERENCE = new RegExp(`^(?:${DOMAIN}/)?${PATH_COMPONENT}(?:/${PATH_COMPONENT})*(?::[\\w][\\w.-]{0,127})?(?:@[A-Za-z][A-Za-z0-9]*(?:[-_+.][A-Za-z][A-Za-z0-9]*)*:[0-9a-fA-F]{32,})?$`);
|
|
3994
|
-
/** Docker refuses a repository name longer than this. */
|
|
3995
|
-
const MAX_NAME_LENGTH = 255;
|
|
3996
|
-
/** A value that is not an image reference was about to be built or pushed. */
|
|
3997
|
-
var ImageRefInvalid = class extends Error {
|
|
3998
|
-
ref;
|
|
3999
|
-
constructor(ref) {
|
|
4000
|
-
super(`'${ref}' is not a valid image reference. An image is \`[registry/]name[:tag]\`: the name lowercase letters, digits and . _ - separators, the tag up to 128 of letters, digits, _ . and -, not starting with . or -. Check docker.registry in gkm.config.ts and the --tag passed.`);
|
|
4001
|
-
this.ref = ref;
|
|
4002
|
-
this.name = "ImageRefInvalid";
|
|
4003
|
-
}
|
|
4004
|
-
};
|
|
4005
|
-
/** Throw {@link ImageRefInvalid} unless `ref` is a Docker image reference. */
|
|
4006
|
-
function validateImageRef(ref) {
|
|
4007
|
-
const name = ref.split("@")[0].replace(/:[\w][\w.-]*$/, "");
|
|
4008
|
-
if (!REFERENCE.test(ref) || name.length > MAX_NAME_LENGTH) throw new ImageRefInvalid(ref);
|
|
4009
|
-
return ref;
|
|
4010
|
-
}
|
|
4011
|
-
//#endregion
|
|
4012
|
-
//#region src/docker/templates.ts
|
|
4013
|
-
const LOCKFILES = [
|
|
4014
|
-
["pnpm-lock.yaml", "pnpm"],
|
|
4015
|
-
["bun.lockb", "bun"],
|
|
4016
|
-
["yarn.lock", "yarn"],
|
|
4017
|
-
["package-lock.json", "npm"]
|
|
4018
|
-
];
|
|
4019
|
-
/**
|
|
4020
|
-
* Detect package manager from lockfiles
|
|
4021
|
-
* Walks up the directory tree to find lockfile (for monorepos)
|
|
4022
|
-
*/
|
|
4023
|
-
function detectPackageManager(cwd = process.cwd()) {
|
|
4024
|
-
let dir = cwd;
|
|
4025
|
-
const root = (0, node_path.parse)(dir).root;
|
|
4026
|
-
while (dir !== root) {
|
|
4027
|
-
for (const [lockfile, pm] of LOCKFILES) if ((0, node_fs.existsSync)((0, node_path.join)(dir, lockfile))) return pm;
|
|
4028
|
-
dir = (0, node_path.dirname)(dir);
|
|
4029
|
-
}
|
|
4030
|
-
for (const [lockfile, pm] of LOCKFILES) if ((0, node_fs.existsSync)((0, node_path.join)(root, lockfile))) return pm;
|
|
4031
|
-
return "pnpm";
|
|
4032
|
-
}
|
|
4033
|
-
/**
|
|
4034
|
-
* Find the lockfile path by walking up the directory tree
|
|
4035
|
-
* Returns the full path to the lockfile, or null if not found
|
|
4036
|
-
*/
|
|
4037
|
-
function findLockfilePath(cwd = process.cwd()) {
|
|
4038
|
-
let dir = cwd;
|
|
4039
|
-
const root = (0, node_path.parse)(dir).root;
|
|
4040
|
-
while (dir !== root) {
|
|
4041
|
-
for (const [lockfile] of LOCKFILES) {
|
|
4042
|
-
const lockfilePath = (0, node_path.join)(dir, lockfile);
|
|
4043
|
-
if ((0, node_fs.existsSync)(lockfilePath)) return lockfilePath;
|
|
4044
|
-
}
|
|
4045
|
-
dir = (0, node_path.dirname)(dir);
|
|
4046
|
-
}
|
|
4047
|
-
for (const [lockfile] of LOCKFILES) {
|
|
4048
|
-
const lockfilePath = (0, node_path.join)(root, lockfile);
|
|
4049
|
-
if ((0, node_fs.existsSync)(lockfilePath)) return lockfilePath;
|
|
4050
|
-
}
|
|
4051
|
-
return null;
|
|
4052
|
-
}
|
|
4053
|
-
/**
|
|
4054
|
-
* Check if we're in a monorepo (lockfile is in a parent directory)
|
|
4055
|
-
*/
|
|
4056
|
-
function isMonorepo(cwd = process.cwd()) {
|
|
4057
|
-
const lockfilePath = findLockfilePath(cwd);
|
|
4058
|
-
if (!lockfilePath) return false;
|
|
4059
|
-
return (0, node_path.dirname)(lockfilePath) !== cwd;
|
|
4060
|
-
}
|
|
4061
|
-
/**
|
|
4062
|
-
* Check if turbo.json exists (walks up directory tree)
|
|
4063
|
-
*/
|
|
4064
|
-
function hasTurboConfig(cwd = process.cwd()) {
|
|
4065
|
-
let dir = cwd;
|
|
4066
|
-
const root = (0, node_path.parse)(dir).root;
|
|
4067
|
-
while (dir !== root) {
|
|
4068
|
-
if ((0, node_fs.existsSync)((0, node_path.join)(dir, "turbo.json"))) return true;
|
|
4069
|
-
dir = (0, node_path.dirname)(dir);
|
|
4070
|
-
}
|
|
4071
|
-
return (0, node_fs.existsSync)((0, node_path.join)(root, "turbo.json"));
|
|
4072
|
-
}
|
|
4073
|
-
/**
|
|
4074
|
-
* Get install command for turbo builds (without frozen lockfile)
|
|
4075
|
-
* Turbo prune creates a subset that may not perfectly match the lockfile
|
|
4076
|
-
*/
|
|
4077
|
-
function getTurboInstallCmd(pm) {
|
|
4078
|
-
return {
|
|
4079
|
-
pnpm: "pnpm install",
|
|
4080
|
-
npm: "npm install",
|
|
4081
|
-
yarn: "yarn install",
|
|
4082
|
-
bun: "bun install"
|
|
4083
|
-
}[pm];
|
|
4084
|
-
}
|
|
4085
|
-
/**
|
|
4086
|
-
* Get package manager specific commands and paths
|
|
4087
|
-
*/
|
|
4088
|
-
function getPmConfig(pm) {
|
|
4089
|
-
return {
|
|
4090
|
-
pnpm: {
|
|
4091
|
-
install: "corepack enable && corepack prepare pnpm@latest --activate",
|
|
4092
|
-
lockfile: "pnpm-lock.yaml",
|
|
4093
|
-
fetch: "pnpm fetch",
|
|
4094
|
-
installCmd: "pnpm install --frozen-lockfile --offline",
|
|
4095
|
-
cacheTarget: "/root/.local/share/pnpm/store",
|
|
4096
|
-
cacheId: "pnpm",
|
|
4097
|
-
run: "pnpm",
|
|
4098
|
-
exec: "pnpm exec",
|
|
4099
|
-
dlx: "pnpm dlx",
|
|
4100
|
-
addGlobal: "pnpm add -g"
|
|
4101
|
-
},
|
|
4102
|
-
npm: {
|
|
4103
|
-
install: "",
|
|
4104
|
-
lockfile: "package-lock.json",
|
|
4105
|
-
fetch: "",
|
|
4106
|
-
installCmd: "npm ci",
|
|
4107
|
-
cacheTarget: "/root/.npm",
|
|
4108
|
-
cacheId: "npm",
|
|
4109
|
-
run: "npm run",
|
|
4110
|
-
exec: "npx",
|
|
4111
|
-
dlx: "npx",
|
|
4112
|
-
addGlobal: "npm install -g"
|
|
4113
|
-
},
|
|
4114
|
-
yarn: {
|
|
4115
|
-
install: "corepack enable && corepack prepare yarn@stable --activate",
|
|
4116
|
-
lockfile: "yarn.lock",
|
|
4117
|
-
fetch: "",
|
|
4118
|
-
installCmd: "yarn install --frozen-lockfile",
|
|
4119
|
-
cacheTarget: "/root/.yarn/cache",
|
|
4120
|
-
cacheId: "yarn",
|
|
4121
|
-
run: "yarn",
|
|
4122
|
-
exec: "yarn exec",
|
|
4123
|
-
dlx: "yarn dlx",
|
|
4124
|
-
addGlobal: "yarn global add"
|
|
4125
|
-
},
|
|
4126
|
-
bun: {
|
|
4127
|
-
install: "npm install -g bun",
|
|
4128
|
-
lockfile: "bun.lockb",
|
|
4129
|
-
fetch: "",
|
|
4130
|
-
installCmd: "bun install --frozen-lockfile",
|
|
4131
|
-
cacheTarget: "/root/.bun/install/cache",
|
|
4132
|
-
cacheId: "bun",
|
|
4133
|
-
run: "bun run",
|
|
4134
|
-
exec: "bunx",
|
|
4135
|
-
dlx: "bunx",
|
|
4136
|
-
addGlobal: "bun add -g"
|
|
4137
|
-
}
|
|
4138
|
-
}[pm];
|
|
4139
|
-
}
|
|
4140
|
-
/**
|
|
4141
|
-
* Generate a multi-stage Dockerfile for building from source
|
|
4142
|
-
* Optimized for build speed with:
|
|
4143
|
-
* - BuildKit cache mounts for package manager store
|
|
4144
|
-
* - pnpm fetch for better layer caching (when using pnpm)
|
|
4145
|
-
* - Optional turbo prune for monorepos
|
|
4146
|
-
*/
|
|
4147
|
-
function generateMultiStageDockerfile(options) {
|
|
4148
|
-
const { baseImage, port, healthCheckPath, turbo, turboPackage, packageManager } = options;
|
|
4149
|
-
if (turbo) return generateTurboDockerfile({
|
|
4150
|
-
...options,
|
|
4151
|
-
turboPackage: turboPackage ?? "api"
|
|
4152
|
-
});
|
|
3684
|
+
function generateNextjsDockerfile(options) {
|
|
3685
|
+
const { baseImage, port, appPath, turboPackage, packageManager, publicUrlArgs = ["NEXT_PUBLIC_API_URL", "NEXT_PUBLIC_AUTH_URL"] } = options;
|
|
4153
3686
|
const pm = getPmConfig(packageManager);
|
|
3687
|
+
const installPm = pm.install ? `RUN ${pm.install}` : "";
|
|
3688
|
+
const turboInstallCmd = getTurboInstallCmd(packageManager);
|
|
3689
|
+
const turboCmd = packageManager === "pnpm" ? "pnpm dlx turbo" : "npx turbo";
|
|
3690
|
+
const publicUrlArgDeclarations = publicUrlArgs.map((arg) => `ARG ${arg}=""`).join("\n");
|
|
3691
|
+
const publicUrlEnvDeclarations = publicUrlArgs.map((arg) => `ENV ${arg}=$${arg}`).join("\n");
|
|
4154
3692
|
return `# syntax=docker/dockerfile:1
|
|
4155
|
-
#
|
|
4156
|
-
FROM ${baseImage} AS deps
|
|
3693
|
+
# Next.js standalone Dockerfile with turbo prune optimization
|
|
4157
3694
|
|
|
4158
|
-
|
|
4159
|
-
|
|
4160
|
-
${packageManager === "pnpm" ? `# Copy lockfile first for better caching
|
|
4161
|
-
COPY ${pm.lockfile} ./
|
|
3695
|
+
# Stage 1: Prune monorepo
|
|
3696
|
+
FROM ${baseImage} AS pruner
|
|
4162
3697
|
|
|
4163
|
-
|
|
4164
|
-
RUN --mount=type=cache,id=${pm.cacheId},target=${pm.cacheTarget} \\
|
|
4165
|
-
${pm.fetch}
|
|
3698
|
+
WORKDIR /app
|
|
4166
3699
|
|
|
4167
|
-
|
|
4168
|
-
COPY package.json ./
|
|
3700
|
+
${installPm}
|
|
4169
3701
|
|
|
4170
|
-
|
|
4171
|
-
RUN --mount=type=cache,id=${pm.cacheId},target=${pm.cacheTarget} \\
|
|
4172
|
-
${pm.installCmd}` : `# Copy package files
|
|
4173
|
-
COPY package.json ${pm.lockfile} ./
|
|
3702
|
+
COPY . .
|
|
4174
3703
|
|
|
4175
|
-
#
|
|
4176
|
-
RUN
|
|
4177
|
-
${pm.installCmd}`}
|
|
3704
|
+
# Prune to only include necessary packages
|
|
3705
|
+
RUN ${turboCmd} prune ${turboPackage} --docker
|
|
4178
3706
|
|
|
4179
|
-
# Stage 2:
|
|
4180
|
-
FROM
|
|
3707
|
+
# Stage 2: Install dependencies
|
|
3708
|
+
FROM ${baseImage} AS deps
|
|
4181
3709
|
|
|
4182
3710
|
WORKDIR /app
|
|
4183
3711
|
|
|
4184
|
-
|
|
4185
|
-
COPY . .
|
|
3712
|
+
${installPm}
|
|
4186
3713
|
|
|
4187
|
-
#
|
|
4188
|
-
|
|
4189
|
-
|
|
4190
|
-
# for one whose CLI is a sibling package: \`turbo prune\` honours .gitignore, so a
|
|
4191
|
-
# workspace package's \`dist\` never reaches the image and its bin has nothing to
|
|
4192
|
-
# import. \`--if-present\` is what makes the same Dockerfile correct for both.
|
|
4193
|
-
RUN ${pm.run} --if-present build
|
|
3714
|
+
# Copy pruned lockfile and package.jsons
|
|
3715
|
+
COPY --from=pruner /app/out/${pm.lockfile} ./
|
|
3716
|
+
COPY --from=pruner /app/out/json/ ./
|
|
4194
3717
|
|
|
4195
|
-
#
|
|
4196
|
-
RUN
|
|
3718
|
+
# Install dependencies
|
|
3719
|
+
RUN --mount=type=cache,id=${pm.cacheId},target=${pm.cacheTarget} \\
|
|
3720
|
+
${turboInstallCmd}
|
|
4197
3721
|
|
|
4198
|
-
# Stage 3:
|
|
4199
|
-
FROM
|
|
3722
|
+
# Stage 3: Build
|
|
3723
|
+
FROM deps AS builder
|
|
4200
3724
|
|
|
4201
3725
|
WORKDIR /app
|
|
4202
3726
|
|
|
4203
|
-
#
|
|
4204
|
-
|
|
3727
|
+
# Build-time args for public API URLs (populated by gkm deploy)
|
|
3728
|
+
# These get baked into the Next.js build as public environment variables
|
|
3729
|
+
${publicUrlArgDeclarations}
|
|
4205
3730
|
|
|
4206
|
-
#
|
|
4207
|
-
|
|
4208
|
-
adduser --system --uid 1001 hono
|
|
3731
|
+
# Convert ARGs to ENVs for Next.js build
|
|
3732
|
+
${publicUrlEnvDeclarations}
|
|
4209
3733
|
|
|
4210
|
-
# Copy
|
|
4211
|
-
COPY --from=
|
|
3734
|
+
# Copy pruned source
|
|
3735
|
+
COPY --from=pruner /app/out/full/ ./
|
|
4212
3736
|
|
|
4213
|
-
#
|
|
3737
|
+
# Copy workspace root configs for turbo builds (turbo prune doesn't include root configs)
|
|
3738
|
+
# Using wildcard to make it optional for single-app projects
|
|
3739
|
+
COPY --from=pruner /app/tsconfig.* ./
|
|
3740
|
+
|
|
3741
|
+
# Ensure public directory exists (may be empty for scaffolded projects)
|
|
3742
|
+
RUN mkdir -p ${appPath}/public
|
|
3743
|
+
|
|
3744
|
+
# Set Next.js to produce standalone output
|
|
3745
|
+
ENV NEXT_TELEMETRY_DISABLED=1
|
|
3746
|
+
|
|
3747
|
+
# Build the application
|
|
3748
|
+
RUN ${turboCmd} run build --filter=${turboPackage}
|
|
3749
|
+
|
|
3750
|
+
# Stage 4: Production
|
|
3751
|
+
FROM ${baseImage} AS runner
|
|
3752
|
+
|
|
3753
|
+
WORKDIR /app
|
|
3754
|
+
|
|
3755
|
+
# Install tini for proper signal handling
|
|
3756
|
+
RUN apk add --no-cache tini
|
|
3757
|
+
|
|
3758
|
+
# Create non-root user
|
|
3759
|
+
RUN addgroup --system --gid 1001 nodejs && \\
|
|
3760
|
+
adduser --system --uid 1001 nextjs
|
|
3761
|
+
|
|
3762
|
+
# Set environment
|
|
4214
3763
|
ENV NODE_ENV=production
|
|
3764
|
+
ENV NEXT_TELEMETRY_DISABLED=1
|
|
4215
3765
|
ENV PORT=${port}
|
|
3766
|
+
ENV HOSTNAME="0.0.0.0"
|
|
4216
3767
|
|
|
4217
|
-
#
|
|
4218
|
-
|
|
4219
|
-
|
|
3768
|
+
# Copy static files and standalone output
|
|
3769
|
+
COPY --from=builder --chown=nextjs:nodejs /app/${appPath}/.next/standalone ./
|
|
3770
|
+
COPY --from=builder --chown=nextjs:nodejs /app/${appPath}/.next/static ./${appPath}/.next/static
|
|
3771
|
+
COPY --from=builder --chown=nextjs:nodejs /app/${appPath}/public ./${appPath}/public
|
|
4220
3772
|
|
|
4221
|
-
|
|
4222
|
-
USER hono
|
|
3773
|
+
USER nextjs
|
|
4223
3774
|
|
|
4224
3775
|
EXPOSE ${port}
|
|
4225
3776
|
|
|
4226
|
-
# Use tini as entrypoint to handle PID 1 responsibilities
|
|
4227
3777
|
ENTRYPOINT ["/sbin/tini", "--"]
|
|
4228
|
-
CMD ["node", "server.
|
|
3778
|
+
CMD ["node", "${appPath}/server.js"]
|
|
4229
3779
|
`;
|
|
4230
3780
|
}
|
|
4231
3781
|
/**
|
|
4232
|
-
* Generate a Dockerfile
|
|
4233
|
-
* Uses turbo prune
|
|
3782
|
+
* Generate a Dockerfile for backend apps in a workspace.
|
|
3783
|
+
* Uses turbo prune for monorepo optimization.
|
|
3784
|
+
* @internal Exported for testing
|
|
4234
3785
|
*/
|
|
4235
|
-
function
|
|
4236
|
-
const { baseImage, port,
|
|
3786
|
+
function generateBackendDockerfile(options) {
|
|
3787
|
+
const { baseImage, port, appPath, turboPackage, packageManager, healthCheckPath = "/health" } = options;
|
|
4237
3788
|
const pm = getPmConfig(packageManager);
|
|
4238
3789
|
const installPm = pm.install ? `RUN ${pm.install}` : "";
|
|
4239
3790
|
const turboInstallCmd = getTurboInstallCmd(packageManager);
|
|
4240
3791
|
return `# syntax=docker/dockerfile:1
|
|
3792
|
+
# Backend Dockerfile with turbo prune optimization
|
|
3793
|
+
|
|
4241
3794
|
# Stage 1: Prune monorepo
|
|
4242
3795
|
FROM ${baseImage} AS pruner
|
|
4243
3796
|
|
|
@@ -4261,7 +3814,7 @@ ${installPm}
|
|
|
4261
3814
|
COPY --from=pruner /app/out/${pm.lockfile} ./
|
|
4262
3815
|
COPY --from=pruner /app/out/json/ ./
|
|
4263
3816
|
|
|
4264
|
-
# Install dependencies
|
|
3817
|
+
# Install dependencies
|
|
4265
3818
|
RUN --mount=type=cache,id=${pm.cacheId},target=${pm.cacheTarget} \\
|
|
4266
3819
|
${turboInstallCmd}
|
|
4267
3820
|
|
|
@@ -4273,6 +3826,22 @@ WORKDIR /app
|
|
|
4273
3826
|
# Copy pruned source
|
|
4274
3827
|
COPY --from=pruner /app/out/full/ ./
|
|
4275
3828
|
|
|
3829
|
+
# Copy workspace root configs for turbo builds (turbo prune doesn't include root configs)
|
|
3830
|
+
# Using wildcard to make it optional for single-app projects
|
|
3831
|
+
COPY --from=pruner /app/gkm.config.* ./
|
|
3832
|
+
COPY --from=pruner /app/tsconfig.* ./
|
|
3833
|
+
|
|
3834
|
+
# Write encrypted credentials for gkm build to embed.
|
|
3835
|
+
# A build secret (\`docker build --secret id=gkm_credentials,src=…\`), not an
|
|
3836
|
+
# ARG: an ARG is recorded in the image history. Two lines, ciphertext then IV;
|
|
3837
|
+
# absent when the app has no secrets.
|
|
3838
|
+
RUN --mount=type=secret,id=gkm_credentials,required=false \
|
|
3839
|
+
if [ -s /run/secrets/gkm_credentials ]; then \
|
|
3840
|
+
mkdir -p ${appPath}/.gkm && \
|
|
3841
|
+
sed -n 1p /run/secrets/gkm_credentials > ${appPath}/.gkm/credentials.enc && \
|
|
3842
|
+
sed -n 2p /run/secrets/gkm_credentials > ${appPath}/.gkm/credentials.iv; \
|
|
3843
|
+
fi
|
|
3844
|
+
|
|
4276
3845
|
# Build any workspace packages the app depends on.
|
|
4277
3846
|
#
|
|
4278
3847
|
# A no-op for a project whose dependencies come from a registry, and required
|
|
@@ -4282,7 +3851,7 @@ COPY --from=pruner /app/out/full/ ./
|
|
|
4282
3851
|
RUN ${pm.run} --if-present build
|
|
4283
3852
|
|
|
4284
3853
|
# Build production server using gkm
|
|
4285
|
-
RUN ${pm.exec} gkm build --provider server --production
|
|
3854
|
+
RUN cd ${appPath} && ${pm.exec} gkm build --provider server --production
|
|
4286
3855
|
|
|
4287
3856
|
# Stage 4: Production
|
|
4288
3857
|
FROM ${baseImage} AS runner
|
|
@@ -4294,163 +3863,36 @@ RUN apk add --no-cache tini
|
|
|
4294
3863
|
RUN addgroup --system --gid 1001 nodejs && \\
|
|
4295
3864
|
adduser --system --uid 1001 hono
|
|
4296
3865
|
|
|
4297
|
-
|
|
4298
|
-
|
|
4299
|
-
ENV NODE_ENV=production
|
|
4300
|
-
ENV PORT=${port}
|
|
4301
|
-
|
|
4302
|
-
HEALTHCHECK --interval=30s --timeout=10s --start-period=30s --retries=3 \\
|
|
4303
|
-
CMD wget -qO- http://localhost:${port}${healthCheckPath} > /dev/null 2>&1 || exit 1
|
|
4304
|
-
|
|
4305
|
-
USER hono
|
|
4306
|
-
|
|
4307
|
-
EXPOSE ${port}
|
|
4308
|
-
|
|
4309
|
-
ENTRYPOINT ["/sbin/tini", "--"]
|
|
4310
|
-
CMD ["node", "server.mjs"]
|
|
4311
|
-
`;
|
|
4312
|
-
}
|
|
4313
|
-
/**
|
|
4314
|
-
* Generate a slim Dockerfile for pre-built bundles
|
|
4315
|
-
*/
|
|
4316
|
-
function generateSlimDockerfile(options) {
|
|
4317
|
-
const { baseImage, port, healthCheckPath, bundle = ".gkm/server/dist/server.mjs" } = options;
|
|
4318
|
-
return `# Slim Dockerfile for pre-built production bundle
|
|
4319
|
-
FROM ${baseImage}
|
|
4320
|
-
|
|
4321
|
-
WORKDIR /app
|
|
4322
|
-
|
|
4323
|
-
# Install tini for proper signal handling as PID 1
|
|
4324
|
-
# Handles SIGTERM propagation and zombie process reaping
|
|
4325
|
-
RUN apk add --no-cache tini
|
|
4326
|
-
|
|
4327
|
-
# Create non-root user
|
|
4328
|
-
RUN addgroup --system --gid 1001 nodejs && \\
|
|
4329
|
-
adduser --system --uid 1001 hono
|
|
4330
|
-
|
|
4331
|
-
# Copy pre-built bundle
|
|
4332
|
-
COPY ${bundle} ./
|
|
3866
|
+
# Copy bundled server
|
|
3867
|
+
COPY --from=builder --chown=hono:nodejs /app/${appPath}/.gkm/server/dist/server.mjs ./
|
|
4333
3868
|
|
|
4334
|
-
# Environment
|
|
4335
3869
|
ENV NODE_ENV=production
|
|
4336
3870
|
ENV PORT=${port}
|
|
4337
3871
|
|
|
4338
|
-
# Health check
|
|
4339
3872
|
HEALTHCHECK --interval=30s --timeout=10s --start-period=30s --retries=3 \\
|
|
4340
3873
|
CMD wget -qO- http://localhost:${port}${healthCheckPath} > /dev/null 2>&1 || exit 1
|
|
4341
3874
|
|
|
4342
|
-
# Switch to non-root user
|
|
4343
3875
|
USER hono
|
|
4344
3876
|
|
|
4345
3877
|
EXPOSE ${port}
|
|
4346
3878
|
|
|
4347
|
-
# Use tini as entrypoint to handle PID 1 responsibilities
|
|
4348
3879
|
ENTRYPOINT ["/sbin/tini", "--"]
|
|
4349
3880
|
CMD ["node", "server.mjs"]
|
|
4350
3881
|
`;
|
|
4351
3882
|
}
|
|
4352
3883
|
/**
|
|
4353
|
-
* Generate .
|
|
4354
|
-
|
|
4355
|
-
|
|
4356
|
-
return `# Dependencies
|
|
4357
|
-
node_modules
|
|
4358
|
-
.pnpm-store
|
|
4359
|
-
|
|
4360
|
-
# Build output (except what we need)
|
|
4361
|
-
.gkm/aws*
|
|
4362
|
-
.gkm/server/*.ts
|
|
4363
|
-
!.gkm/server/dist
|
|
4364
|
-
|
|
4365
|
-
# The key gkm build --stage encrypted secrets with: runtime env only, never
|
|
4366
|
-
# the build context
|
|
4367
|
-
.gkm/server/master.key
|
|
4368
|
-
|
|
4369
|
-
# gkm compose's stacks: each app's env file holds its stage's secrets
|
|
4370
|
-
.gkm/compose
|
|
4371
|
-
|
|
4372
|
-
# IDE and editor
|
|
4373
|
-
.idea
|
|
4374
|
-
.vscode
|
|
4375
|
-
*.swp
|
|
4376
|
-
*.swo
|
|
4377
|
-
|
|
4378
|
-
# Git
|
|
4379
|
-
.git
|
|
4380
|
-
.gitignore
|
|
4381
|
-
|
|
4382
|
-
# Logs
|
|
4383
|
-
*.log
|
|
4384
|
-
npm-debug.log*
|
|
4385
|
-
pnpm-debug.log*
|
|
4386
|
-
|
|
4387
|
-
# Test files
|
|
4388
|
-
**/*.test.ts
|
|
4389
|
-
**/*.spec.ts
|
|
4390
|
-
**/__tests__
|
|
4391
|
-
coverage
|
|
4392
|
-
|
|
4393
|
-
# Documentation
|
|
4394
|
-
docs
|
|
4395
|
-
*.md
|
|
4396
|
-
!README.md
|
|
4397
|
-
|
|
4398
|
-
# Environment files (handle secrets separately)
|
|
4399
|
-
.env
|
|
4400
|
-
.env.*
|
|
4401
|
-
!.env.example
|
|
4402
|
-
|
|
4403
|
-
# Docker files (don't copy recursively)
|
|
4404
|
-
Dockerfile*
|
|
4405
|
-
docker-compose*
|
|
4406
|
-
.dockerignore
|
|
4407
|
-
`;
|
|
4408
|
-
}
|
|
4409
|
-
/**
|
|
4410
|
-
* Generate docker-entrypoint.sh for custom startup logic
|
|
4411
|
-
*/
|
|
4412
|
-
function generateDockerEntrypoint() {
|
|
4413
|
-
return `#!/bin/sh
|
|
4414
|
-
set -e
|
|
4415
|
-
|
|
4416
|
-
# Run any custom startup scripts here
|
|
4417
|
-
# Example: wait for database
|
|
4418
|
-
# until nc -z $DB_HOST $DB_PORT; do
|
|
4419
|
-
# echo "Waiting for database..."
|
|
4420
|
-
# sleep 1
|
|
4421
|
-
# done
|
|
4422
|
-
|
|
4423
|
-
# Execute the main command
|
|
4424
|
-
exec "$@"
|
|
4425
|
-
`;
|
|
4426
|
-
}
|
|
4427
|
-
/**
|
|
4428
|
-
* Resolve Docker configuration from GkmConfig with defaults
|
|
4429
|
-
*/
|
|
4430
|
-
function resolveDockerConfig(config) {
|
|
4431
|
-
const docker = config.docker ?? {};
|
|
4432
|
-
return {
|
|
4433
|
-
registry: docker.registry ?? "",
|
|
4434
|
-
imageName: docker.imageName ?? config.name ?? "api",
|
|
4435
|
-
baseImage: docker.baseImage ?? "node:22-alpine",
|
|
4436
|
-
port: docker.port ?? 3e3
|
|
4437
|
-
};
|
|
4438
|
-
}
|
|
4439
|
-
/**
|
|
4440
|
-
* Generate a Dockerfile for Next.js frontend apps using standalone output.
|
|
4441
|
-
* Uses turbo prune for monorepo optimization.
|
|
3884
|
+
* Generate a Dockerfile for apps with a custom entry point.
|
|
3885
|
+
* Uses esbuild to bundle the entry point into dist/index.mjs with all dependencies.
|
|
3886
|
+
* This is used for apps that don't use gkm routes (e.g., Better Auth servers).
|
|
4442
3887
|
* @internal Exported for testing
|
|
4443
3888
|
*/
|
|
4444
|
-
function
|
|
4445
|
-
const { baseImage, port, appPath, turboPackage, packageManager,
|
|
3889
|
+
function generateEntryDockerfile(options) {
|
|
3890
|
+
const { baseImage, port, appPath, entry, turboPackage, packageManager, healthCheckPath = "/health" } = options;
|
|
4446
3891
|
const pm = getPmConfig(packageManager);
|
|
4447
3892
|
const installPm = pm.install ? `RUN ${pm.install}` : "";
|
|
4448
3893
|
const turboInstallCmd = getTurboInstallCmd(packageManager);
|
|
4449
|
-
const turboCmd = packageManager === "pnpm" ? "pnpm dlx turbo" : "npx turbo";
|
|
4450
|
-
const publicUrlArgDeclarations = publicUrlArgs.map((arg) => `ARG ${arg}=""`).join("\n");
|
|
4451
|
-
const publicUrlEnvDeclarations = publicUrlArgs.map((arg) => `ENV ${arg}=$${arg}`).join("\n");
|
|
4452
3894
|
return `# syntax=docker/dockerfile:1
|
|
4453
|
-
#
|
|
3895
|
+
# Entry-based Dockerfile with turbo prune + tsdown bundling
|
|
4454
3896
|
|
|
4455
3897
|
# Stage 1: Prune monorepo
|
|
4456
3898
|
FROM ${baseImage} AS pruner
|
|
@@ -4462,7 +3904,7 @@ ${installPm}
|
|
|
4462
3904
|
COPY . .
|
|
4463
3905
|
|
|
4464
3906
|
# Prune to only include necessary packages
|
|
4465
|
-
RUN ${
|
|
3907
|
+
RUN ${packageManager === "pnpm" ? "pnpm dlx turbo" : "npx turbo"} prune ${turboPackage} --docker
|
|
4466
3908
|
|
|
4467
3909
|
# Stage 2: Install dependencies
|
|
4468
3910
|
FROM ${baseImage} AS deps
|
|
@@ -4479,18 +3921,11 @@ COPY --from=pruner /app/out/json/ ./
|
|
|
4479
3921
|
RUN --mount=type=cache,id=${pm.cacheId},target=${pm.cacheTarget} \\
|
|
4480
3922
|
${turboInstallCmd}
|
|
4481
3923
|
|
|
4482
|
-
# Stage 3: Build
|
|
3924
|
+
# Stage 3: Build with tsdown
|
|
4483
3925
|
FROM deps AS builder
|
|
4484
3926
|
|
|
4485
3927
|
WORKDIR /app
|
|
4486
3928
|
|
|
4487
|
-
# Build-time args for public API URLs (populated by gkm deploy)
|
|
4488
|
-
# These get baked into the Next.js build as public environment variables
|
|
4489
|
-
${publicUrlArgDeclarations}
|
|
4490
|
-
|
|
4491
|
-
# Convert ARGs to ENVs for Next.js build
|
|
4492
|
-
${publicUrlEnvDeclarations}
|
|
4493
|
-
|
|
4494
3929
|
# Copy pruned source
|
|
4495
3930
|
COPY --from=pruner /app/out/full/ ./
|
|
4496
3931
|
|
|
@@ -4498,222 +3933,27 @@ COPY --from=pruner /app/out/full/ ./
|
|
|
4498
3933
|
# Using wildcard to make it optional for single-app projects
|
|
4499
3934
|
COPY --from=pruner /app/tsconfig.* ./
|
|
4500
3935
|
|
|
4501
|
-
#
|
|
4502
|
-
|
|
4503
|
-
|
|
4504
|
-
#
|
|
4505
|
-
|
|
4506
|
-
|
|
4507
|
-
|
|
4508
|
-
|
|
4509
|
-
|
|
4510
|
-
|
|
4511
|
-
|
|
4512
|
-
|
|
4513
|
-
|
|
4514
|
-
|
|
4515
|
-
|
|
4516
|
-
|
|
4517
|
-
|
|
4518
|
-
|
|
4519
|
-
|
|
4520
|
-
|
|
4521
|
-
|
|
4522
|
-
# Set environment
|
|
4523
|
-
ENV NODE_ENV=production
|
|
4524
|
-
ENV NEXT_TELEMETRY_DISABLED=1
|
|
4525
|
-
ENV PORT=${port}
|
|
4526
|
-
ENV HOSTNAME="0.0.0.0"
|
|
4527
|
-
|
|
4528
|
-
# Copy static files and standalone output
|
|
4529
|
-
COPY --from=builder --chown=nextjs:nodejs /app/${appPath}/.next/standalone ./
|
|
4530
|
-
COPY --from=builder --chown=nextjs:nodejs /app/${appPath}/.next/static ./${appPath}/.next/static
|
|
4531
|
-
COPY --from=builder --chown=nextjs:nodejs /app/${appPath}/public ./${appPath}/public
|
|
4532
|
-
|
|
4533
|
-
USER nextjs
|
|
4534
|
-
|
|
4535
|
-
EXPOSE ${port}
|
|
4536
|
-
|
|
4537
|
-
ENTRYPOINT ["/sbin/tini", "--"]
|
|
4538
|
-
CMD ["node", "${appPath}/server.js"]
|
|
4539
|
-
`;
|
|
4540
|
-
}
|
|
4541
|
-
/**
|
|
4542
|
-
* Generate a Dockerfile for backend apps in a workspace.
|
|
4543
|
-
* Uses turbo prune for monorepo optimization.
|
|
4544
|
-
* @internal Exported for testing
|
|
4545
|
-
*/
|
|
4546
|
-
function generateBackendDockerfile(options) {
|
|
4547
|
-
const { baseImage, port, appPath, turboPackage, packageManager, healthCheckPath = "/health" } = options;
|
|
4548
|
-
const pm = getPmConfig(packageManager);
|
|
4549
|
-
const installPm = pm.install ? `RUN ${pm.install}` : "";
|
|
4550
|
-
const turboInstallCmd = getTurboInstallCmd(packageManager);
|
|
4551
|
-
return `# syntax=docker/dockerfile:1
|
|
4552
|
-
# Backend Dockerfile with turbo prune optimization
|
|
4553
|
-
|
|
4554
|
-
# Stage 1: Prune monorepo
|
|
4555
|
-
FROM ${baseImage} AS pruner
|
|
4556
|
-
|
|
4557
|
-
WORKDIR /app
|
|
4558
|
-
|
|
4559
|
-
${installPm}
|
|
4560
|
-
|
|
4561
|
-
COPY . .
|
|
4562
|
-
|
|
4563
|
-
# Prune to only include necessary packages
|
|
4564
|
-
RUN ${packageManager === "pnpm" ? "pnpm dlx turbo" : "npx turbo"} prune ${turboPackage} --docker
|
|
4565
|
-
|
|
4566
|
-
# Stage 2: Install dependencies
|
|
4567
|
-
FROM ${baseImage} AS deps
|
|
4568
|
-
|
|
4569
|
-
WORKDIR /app
|
|
4570
|
-
|
|
4571
|
-
${installPm}
|
|
4572
|
-
|
|
4573
|
-
# Copy pruned lockfile and package.jsons
|
|
4574
|
-
COPY --from=pruner /app/out/${pm.lockfile} ./
|
|
4575
|
-
COPY --from=pruner /app/out/json/ ./
|
|
4576
|
-
|
|
4577
|
-
# Install dependencies
|
|
4578
|
-
RUN --mount=type=cache,id=${pm.cacheId},target=${pm.cacheTarget} \\
|
|
4579
|
-
${turboInstallCmd}
|
|
4580
|
-
|
|
4581
|
-
# Stage 3: Build
|
|
4582
|
-
FROM deps AS builder
|
|
4583
|
-
|
|
4584
|
-
WORKDIR /app
|
|
4585
|
-
|
|
4586
|
-
# Copy pruned source
|
|
4587
|
-
COPY --from=pruner /app/out/full/ ./
|
|
4588
|
-
|
|
4589
|
-
# Copy workspace root configs for turbo builds (turbo prune doesn't include root configs)
|
|
4590
|
-
# Using wildcard to make it optional for single-app projects
|
|
4591
|
-
COPY --from=pruner /app/gkm.config.* ./
|
|
4592
|
-
COPY --from=pruner /app/tsconfig.* ./
|
|
4593
|
-
|
|
4594
|
-
# Write encrypted credentials for gkm build to embed.
|
|
4595
|
-
# A build secret (\`docker build --secret id=gkm_credentials,src=…\`), not an
|
|
4596
|
-
# ARG: an ARG is recorded in the image history. Two lines, ciphertext then IV;
|
|
4597
|
-
# absent when the app has no secrets.
|
|
4598
|
-
RUN --mount=type=secret,id=gkm_credentials,required=false \
|
|
4599
|
-
if [ -s /run/secrets/gkm_credentials ]; then \
|
|
4600
|
-
mkdir -p ${appPath}/.gkm && \
|
|
4601
|
-
sed -n 1p /run/secrets/gkm_credentials > ${appPath}/.gkm/credentials.enc && \
|
|
4602
|
-
sed -n 2p /run/secrets/gkm_credentials > ${appPath}/.gkm/credentials.iv; \
|
|
4603
|
-
fi
|
|
4604
|
-
|
|
4605
|
-
# Build any workspace packages the app depends on.
|
|
4606
|
-
#
|
|
4607
|
-
# A no-op for a project whose dependencies come from a registry, and required
|
|
4608
|
-
# for one whose CLI is a sibling package: \`turbo prune\` honours .gitignore, so a
|
|
4609
|
-
# workspace package's \`dist\` never reaches the image and its bin has nothing to
|
|
4610
|
-
# import. \`--if-present\` is what makes the same Dockerfile correct for both.
|
|
4611
|
-
RUN ${pm.run} --if-present build
|
|
4612
|
-
|
|
4613
|
-
# Build production server using gkm
|
|
4614
|
-
RUN cd ${appPath} && ${pm.exec} gkm build --provider server --production
|
|
4615
|
-
|
|
4616
|
-
# Stage 4: Production
|
|
4617
|
-
FROM ${baseImage} AS runner
|
|
4618
|
-
|
|
4619
|
-
WORKDIR /app
|
|
4620
|
-
|
|
4621
|
-
RUN apk add --no-cache tini
|
|
4622
|
-
|
|
4623
|
-
RUN addgroup --system --gid 1001 nodejs && \\
|
|
4624
|
-
adduser --system --uid 1001 hono
|
|
4625
|
-
|
|
4626
|
-
# Copy bundled server
|
|
4627
|
-
COPY --from=builder --chown=hono:nodejs /app/${appPath}/.gkm/server/dist/server.mjs ./
|
|
4628
|
-
|
|
4629
|
-
ENV NODE_ENV=production
|
|
4630
|
-
ENV PORT=${port}
|
|
4631
|
-
|
|
4632
|
-
HEALTHCHECK --interval=30s --timeout=10s --start-period=30s --retries=3 \\
|
|
4633
|
-
CMD wget -qO- http://localhost:${port}${healthCheckPath} > /dev/null 2>&1 || exit 1
|
|
4634
|
-
|
|
4635
|
-
USER hono
|
|
4636
|
-
|
|
4637
|
-
EXPOSE ${port}
|
|
4638
|
-
|
|
4639
|
-
ENTRYPOINT ["/sbin/tini", "--"]
|
|
4640
|
-
CMD ["node", "server.mjs"]
|
|
4641
|
-
`;
|
|
4642
|
-
}
|
|
4643
|
-
/**
|
|
4644
|
-
* Generate a Dockerfile for apps with a custom entry point.
|
|
4645
|
-
* Uses esbuild to bundle the entry point into dist/index.mjs with all dependencies.
|
|
4646
|
-
* This is used for apps that don't use gkm routes (e.g., Better Auth servers).
|
|
4647
|
-
* @internal Exported for testing
|
|
4648
|
-
*/
|
|
4649
|
-
function generateEntryDockerfile(options) {
|
|
4650
|
-
const { baseImage, port, appPath, entry, turboPackage, packageManager, healthCheckPath = "/health" } = options;
|
|
4651
|
-
const pm = getPmConfig(packageManager);
|
|
4652
|
-
const installPm = pm.install ? `RUN ${pm.install}` : "";
|
|
4653
|
-
const turboInstallCmd = getTurboInstallCmd(packageManager);
|
|
4654
|
-
return `# syntax=docker/dockerfile:1
|
|
4655
|
-
# Entry-based Dockerfile with turbo prune + tsdown bundling
|
|
4656
|
-
|
|
4657
|
-
# Stage 1: Prune monorepo
|
|
4658
|
-
FROM ${baseImage} AS pruner
|
|
4659
|
-
|
|
4660
|
-
WORKDIR /app
|
|
4661
|
-
|
|
4662
|
-
${installPm}
|
|
4663
|
-
|
|
4664
|
-
COPY . .
|
|
4665
|
-
|
|
4666
|
-
# Prune to only include necessary packages
|
|
4667
|
-
RUN ${packageManager === "pnpm" ? "pnpm dlx turbo" : "npx turbo"} prune ${turboPackage} --docker
|
|
4668
|
-
|
|
4669
|
-
# Stage 2: Install dependencies
|
|
4670
|
-
FROM ${baseImage} AS deps
|
|
4671
|
-
|
|
4672
|
-
WORKDIR /app
|
|
4673
|
-
|
|
4674
|
-
${installPm}
|
|
4675
|
-
|
|
4676
|
-
# Copy pruned lockfile and package.jsons
|
|
4677
|
-
COPY --from=pruner /app/out/${pm.lockfile} ./
|
|
4678
|
-
COPY --from=pruner /app/out/json/ ./
|
|
4679
|
-
|
|
4680
|
-
# Install dependencies
|
|
4681
|
-
RUN --mount=type=cache,id=${pm.cacheId},target=${pm.cacheTarget} \\
|
|
4682
|
-
${turboInstallCmd}
|
|
4683
|
-
|
|
4684
|
-
# Stage 3: Build with tsdown
|
|
4685
|
-
FROM deps AS builder
|
|
4686
|
-
|
|
4687
|
-
WORKDIR /app
|
|
4688
|
-
|
|
4689
|
-
# Copy pruned source
|
|
4690
|
-
COPY --from=pruner /app/out/full/ ./
|
|
4691
|
-
|
|
4692
|
-
# Copy workspace root configs for turbo builds (turbo prune doesn't include root configs)
|
|
4693
|
-
# Using wildcard to make it optional for single-app projects
|
|
4694
|
-
COPY --from=pruner /app/tsconfig.* ./
|
|
4695
|
-
|
|
4696
|
-
# Bundle entry point with esbuild (outputs to dist/index.mjs)
|
|
4697
|
-
# Creates a fully standalone bundle with all dependencies included
|
|
4698
|
-
# Use define to embed credentials if present.
|
|
4699
|
-
# They arrive as a build secret (\`docker build --secret id=gkm_credentials,src=…\`),
|
|
4700
|
-
# not an ARG: an ARG is recorded in the image history. Two lines, ciphertext
|
|
4701
|
-
# then IV; absent when the app has no secrets.
|
|
4702
|
-
RUN --mount=type=secret,id=gkm_credentials,required=false \
|
|
4703
|
-
cd ${appPath} && \
|
|
4704
|
-
if [ -s /run/secrets/gkm_credentials ]; then \
|
|
4705
|
-
CREDS=$(sed -n 1p /run/secrets/gkm_credentials) && \
|
|
4706
|
-
IV=$(sed -n 2p /run/secrets/gkm_credentials) && \
|
|
4707
|
-
npx esbuild ${entry} --bundle --platform=node --target=node22 --format=esm \
|
|
4708
|
-
--outfile=dist/index.mjs --packages=bundle \
|
|
4709
|
-
--banner:js='import { createRequire } from "module"; const require = createRequire(import.meta.url);' \
|
|
4710
|
-
--define:__GKM_ENCRYPTED_CREDENTIALS__="'\\"$CREDS\\"'" \
|
|
4711
|
-
--define:__GKM_CREDENTIALS_IV__="'\\"$IV\\"'"; \
|
|
4712
|
-
else \
|
|
4713
|
-
npx esbuild ${entry} --bundle --platform=node --target=node22 --format=esm \
|
|
4714
|
-
--outfile=dist/index.mjs --packages=bundle \
|
|
4715
|
-
--banner:js='import { createRequire } from "module"; const require = createRequire(import.meta.url);'; \
|
|
4716
|
-
fi
|
|
3936
|
+
# Bundle entry point with esbuild (outputs to dist/index.mjs)
|
|
3937
|
+
# Creates a fully standalone bundle with all dependencies included
|
|
3938
|
+
# Use define to embed credentials if present.
|
|
3939
|
+
# They arrive as a build secret (\`docker build --secret id=gkm_credentials,src=…\`),
|
|
3940
|
+
# not an ARG: an ARG is recorded in the image history. Two lines, ciphertext
|
|
3941
|
+
# then IV; absent when the app has no secrets.
|
|
3942
|
+
RUN --mount=type=secret,id=gkm_credentials,required=false \
|
|
3943
|
+
cd ${appPath} && \
|
|
3944
|
+
if [ -s /run/secrets/gkm_credentials ]; then \
|
|
3945
|
+
CREDS=$(sed -n 1p /run/secrets/gkm_credentials) && \
|
|
3946
|
+
IV=$(sed -n 2p /run/secrets/gkm_credentials) && \
|
|
3947
|
+
npx esbuild ${entry} --bundle --platform=node --target=node22 --format=esm \
|
|
3948
|
+
--outfile=dist/index.mjs --packages=bundle \
|
|
3949
|
+
--banner:js='import { createRequire } from "module"; const require = createRequire(import.meta.url);' \
|
|
3950
|
+
--define:__GKM_ENCRYPTED_CREDENTIALS__="'\\"$CREDS\\"'" \
|
|
3951
|
+
--define:__GKM_CREDENTIALS_IV__="'\\"$IV\\"'"; \
|
|
3952
|
+
else \
|
|
3953
|
+
npx esbuild ${entry} --bundle --platform=node --target=node22 --format=esm \
|
|
3954
|
+
--outfile=dist/index.mjs --packages=bundle \
|
|
3955
|
+
--banner:js='import { createRequire } from "module"; const require = createRequire(import.meta.url);'; \
|
|
3956
|
+
fi
|
|
4717
3957
|
|
|
4718
3958
|
# Stage 4: Production
|
|
4719
3959
|
FROM ${baseImage} AS runner
|
|
@@ -5167,1092 +4407,264 @@ function printRunInstructions(workspace) {
|
|
|
5167
4407
|
logger.log(` docker compose -f ${COMPOSE_PATH}${project} --profile apps up --build`);
|
|
5168
4408
|
}
|
|
5169
4409
|
//#endregion
|
|
5170
|
-
//#region src/deploy/
|
|
5171
|
-
var dokploy_api_exports = /* @__PURE__ */ require_rolldown_runtime.__exportAll({
|
|
5172
|
-
DEFAULT_DOKPLOY_TIMEOUT_MS: () => DEFAULT_DOKPLOY_TIMEOUT_MS,
|
|
5173
|
-
DokployApi: () => DokployApi,
|
|
5174
|
-
DokployApiError: () => DokployApiError,
|
|
5175
|
-
DokployRequestTimedOut: () => DokployRequestTimedOut
|
|
5176
|
-
});
|
|
5177
|
-
/** Per request, so a stalled server fails the deploy, not the CI job. */
|
|
5178
|
-
const DEFAULT_DOKPLOY_TIMEOUT_MS = 3e4;
|
|
5179
|
-
/**
|
|
5180
|
-
* Dokploy accepted the connection but did not answer in time.
|
|
5181
|
-
*
|
|
5182
|
-
* Not retried: a request that arrived and stalled may still be acted on, and
|
|
5183
|
-
* sending it again could repeat a deploy or a create the server is already
|
|
5184
|
-
* doing.
|
|
5185
|
-
*/
|
|
5186
|
-
var DokployRequestTimedOut = class extends Error {
|
|
5187
|
-
baseUrl;
|
|
5188
|
-
endpoint;
|
|
5189
|
-
timeoutMs;
|
|
5190
|
-
constructor(baseUrl, endpoint, timeoutMs) {
|
|
5191
|
-
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.`);
|
|
5192
|
-
this.baseUrl = baseUrl;
|
|
5193
|
-
this.endpoint = endpoint;
|
|
5194
|
-
this.timeoutMs = timeoutMs;
|
|
5195
|
-
this.name = "DokployRequestTimedOut";
|
|
5196
|
-
}
|
|
5197
|
-
};
|
|
4410
|
+
//#region src/deploy/generated.ts
|
|
5198
4411
|
/**
|
|
5199
|
-
*
|
|
5200
|
-
*
|
|
4412
|
+
* The values a deployed stage generates once and keeps: what nothing can
|
|
4413
|
+
* provision and nobody else issues.
|
|
4414
|
+
*
|
|
4415
|
+
* - **A seed**, which salts every password a deploy derives — a database's
|
|
4416
|
+
* master and roles, a bucket's root user. Derived from repo facts alone
|
|
4417
|
+
* (project, stage, construct id), those passwords were computable by anyone
|
|
4418
|
+
* who could read the repo.
|
|
4419
|
+
* - **Each `secret` construct's value** — an auth server's signing secret —
|
|
4420
|
+
* random, rather than derived.
|
|
4421
|
+
* - **Each `encryption` construct's keyring** — one random key and an index
|
|
4422
|
+
* key. Later keys are added by `gkm encryption:rotate`, never by a deploy.
|
|
4423
|
+
*
|
|
4424
|
+
* Both are generated on the first deploy that needs them and written to the
|
|
4425
|
+
* stage's store, so every later deploy reads the same values: sessions survive
|
|
4426
|
+
* a redeploy, and a role's password does not move. A value set by hand with
|
|
4427
|
+
* `gkm secrets:set` is kept, never replaced.
|
|
5201
4428
|
*/
|
|
5202
|
-
function pause(ms, signal) {
|
|
5203
|
-
return new Promise((resolve, reject) => {
|
|
5204
|
-
if (signal?.aborted) {
|
|
5205
|
-
reject(signal.reason);
|
|
5206
|
-
return;
|
|
5207
|
-
}
|
|
5208
|
-
const onAbort = () => {
|
|
5209
|
-
clearTimeout(timer);
|
|
5210
|
-
reject(signal?.reason);
|
|
5211
|
-
};
|
|
5212
|
-
const timer = setTimeout(() => {
|
|
5213
|
-
signal?.removeEventListener("abort", onAbort);
|
|
5214
|
-
resolve();
|
|
5215
|
-
}, ms);
|
|
5216
|
-
signal?.addEventListener("abort", onAbort, { once: true });
|
|
5217
|
-
});
|
|
5218
|
-
}
|
|
5219
|
-
var DokployApiError = class extends Error {
|
|
5220
|
-
status;
|
|
5221
|
-
statusText;
|
|
5222
|
-
issues;
|
|
5223
|
-
constructor(message, status, statusText, issues) {
|
|
5224
|
-
super(message);
|
|
5225
|
-
this.status = status;
|
|
5226
|
-
this.statusText = statusText;
|
|
5227
|
-
this.issues = issues;
|
|
5228
|
-
this.name = "DokployApiError";
|
|
5229
|
-
}
|
|
5230
|
-
};
|
|
5231
4429
|
/**
|
|
5232
|
-
*
|
|
4430
|
+
* The stage's secrets with its seed and every declared `secret`'s value
|
|
4431
|
+
* present, generating what is missing. Pure: the caller writes the result
|
|
4432
|
+
* back to the stage's store when `generated` is not empty.
|
|
5233
4433
|
*/
|
|
5234
|
-
|
|
5235
|
-
|
|
5236
|
-
|
|
5237
|
-
|
|
5238
|
-
|
|
5239
|
-
|
|
5240
|
-
|
|
5241
|
-
|
|
5242
|
-
|
|
5243
|
-
this.signal = options.signal;
|
|
5244
|
-
}
|
|
5245
|
-
/**
|
|
5246
|
-
* Make a GET request to the Dokploy API
|
|
5247
|
-
*/
|
|
5248
|
-
async get(endpoint) {
|
|
5249
|
-
return this.request("GET", endpoint);
|
|
4434
|
+
function withGeneratedSecrets(secrets, manifest) {
|
|
4435
|
+
const generated = [];
|
|
4436
|
+
const custom = { ...secrets.custom };
|
|
4437
|
+
for (const declaration of Object.values(manifest)) {
|
|
4438
|
+
if (declaration?.kind !== "secret" && declaration?.kind !== "encryption") continue;
|
|
4439
|
+
const key = declaration.provides?.[0];
|
|
4440
|
+
if (!key || custom[key]) continue;
|
|
4441
|
+
custom[key] = declaration.kind === "encryption" ? (0, _geekmidas_constructs_encryption.generateKeyring)() : random();
|
|
4442
|
+
generated.push(key);
|
|
5250
4443
|
}
|
|
5251
|
-
|
|
5252
|
-
|
|
5253
|
-
|
|
5254
|
-
|
|
5255
|
-
|
|
5256
|
-
|
|
5257
|
-
|
|
5258
|
-
|
|
5259
|
-
|
|
5260
|
-
|
|
5261
|
-
|
|
5262
|
-
|
|
5263
|
-
|
|
5264
|
-
|
|
5265
|
-
|
|
5266
|
-
|
|
5267
|
-
|
|
5268
|
-
|
|
5269
|
-
|
|
5270
|
-
|
|
5271
|
-
|
|
5272
|
-
|
|
5273
|
-
|
|
5274
|
-
|
|
5275
|
-
|
|
5276
|
-
|
|
5277
|
-
|
|
5278
|
-
|
|
5279
|
-
|
|
5280
|
-
|
|
5281
|
-
|
|
5282
|
-
|
|
5283
|
-
if (attempt === 3) break;
|
|
5284
|
-
await pause(attempt * 750, this.signal);
|
|
5285
|
-
}
|
|
5286
|
-
}
|
|
5287
|
-
if (!response || !timeout) {
|
|
5288
|
-
const detail = lastError instanceof Error ? lastError.message : String(lastError);
|
|
5289
|
-
throw new DokployApiError(`Could not reach Dokploy at ${this.baseUrl} (${endpoint}): ${detail}`, 0, "Network error");
|
|
5290
|
-
}
|
|
5291
|
-
try {
|
|
5292
|
-
return await this.read(response);
|
|
5293
|
-
} catch (error) {
|
|
5294
|
-
throw this.stopped(timeout, endpoint) ?? error;
|
|
5295
|
-
}
|
|
5296
|
-
}
|
|
5297
|
-
/**
|
|
5298
|
-
* What a request cut short should reject with: the caller's own reason
|
|
5299
|
-
* when they aborted, `DokployRequestTimedOut` when the deadline passed,
|
|
5300
|
-
* nothing when it was neither.
|
|
5301
|
-
*/
|
|
5302
|
-
stopped(timeout, endpoint) {
|
|
5303
|
-
if (this.signal?.aborted) return this.signal.reason;
|
|
5304
|
-
if (timeout.aborted) return new DokployRequestTimedOut(this.baseUrl, endpoint, this.timeoutMs);
|
|
5305
|
-
}
|
|
5306
|
-
/** Turns a response into its JSON body, or a `DokployApiError`. */
|
|
5307
|
-
async read(response) {
|
|
5308
|
-
if (!response.ok) {
|
|
5309
|
-
let errorMessage = `Dokploy API error: ${response.status} ${response.statusText}`;
|
|
5310
|
-
let issues;
|
|
5311
|
-
try {
|
|
5312
|
-
const errorBody = await response.json();
|
|
5313
|
-
if (errorBody.message) errorMessage = `Dokploy API error: ${errorBody.message}`;
|
|
5314
|
-
if (errorBody.issues?.length) {
|
|
5315
|
-
issues = errorBody.issues;
|
|
5316
|
-
errorMessage += `\n Issues: ${errorBody.issues.map((i) => i.message).join(", ")}`;
|
|
5317
|
-
}
|
|
5318
|
-
} catch {}
|
|
5319
|
-
throw new DokployApiError(errorMessage, response.status, response.statusText, issues);
|
|
5320
|
-
}
|
|
5321
|
-
const text = await response.text();
|
|
5322
|
-
if (!text || text.trim() === "") return;
|
|
5323
|
-
return JSON.parse(text);
|
|
5324
|
-
}
|
|
5325
|
-
/**
|
|
5326
|
-
* Every external port published on this server, across every project.
|
|
5327
|
-
*
|
|
5328
|
-
* Dokploy knows what it has bound; guessing does not. A port that looks free
|
|
5329
|
-
* from here can be held by a service in a project this deploy has never
|
|
5330
|
-
* heard of — which surfaces as `Port 5432 is already in use by container
|
|
5331
|
-
* "…"`, naming something the caller cannot see.
|
|
5332
|
-
*
|
|
5333
|
-
* `project.all` is a summary, so each project is fetched: the per-service
|
|
5334
|
-
* `externalPort` only appears on `project.one`.
|
|
5335
|
-
*/
|
|
5336
|
-
async publishedPorts() {
|
|
5337
|
-
const ports = /* @__PURE__ */ new Set();
|
|
5338
|
-
const projects = await this.listProjects();
|
|
5339
|
-
for (const project of projects) {
|
|
5340
|
-
const details = await this.getProject(project.projectId);
|
|
5341
|
-
for (const environment of details.environments ?? []) {
|
|
5342
|
-
const resources = [
|
|
5343
|
-
...environment.postgres ?? [],
|
|
5344
|
-
...environment.redis ?? [],
|
|
5345
|
-
...environment.applications ?? []
|
|
5346
|
-
];
|
|
5347
|
-
for (const resource of resources) if (typeof resource.externalPort === "number") ports.add(resource.externalPort);
|
|
5348
|
-
}
|
|
5349
|
-
}
|
|
5350
|
-
return ports;
|
|
5351
|
-
}
|
|
5352
|
-
/**
|
|
5353
|
-
* Validate the API token by making a test request
|
|
5354
|
-
*/
|
|
5355
|
-
async validateToken() {
|
|
5356
|
-
try {
|
|
5357
|
-
await this.get("project.all");
|
|
5358
|
-
return true;
|
|
5359
|
-
} catch {
|
|
5360
|
-
return false;
|
|
5361
|
-
}
|
|
5362
|
-
}
|
|
5363
|
-
/**
|
|
5364
|
-
* List all projects
|
|
5365
|
-
*/
|
|
5366
|
-
async listProjects() {
|
|
5367
|
-
return this.get("project.all");
|
|
5368
|
-
}
|
|
5369
|
-
/**
|
|
5370
|
-
* Get a single project by ID
|
|
5371
|
-
*/
|
|
5372
|
-
async getProject(projectId) {
|
|
5373
|
-
return this.get(`project.one?projectId=${projectId}`);
|
|
5374
|
-
}
|
|
5375
|
-
/**
|
|
5376
|
-
* Create a new project
|
|
5377
|
-
*/
|
|
5378
|
-
async createProject(name, description) {
|
|
5379
|
-
return this.post("project.create", {
|
|
5380
|
-
name,
|
|
5381
|
-
description: description ?? `Created by gkm CLI`
|
|
5382
|
-
});
|
|
5383
|
-
}
|
|
5384
|
-
/**
|
|
5385
|
-
* Change a project's name or description.
|
|
5386
|
-
*
|
|
5387
|
-
* The name is sent with the description because `project.update` validates
|
|
5388
|
-
* the whole project on some Dokploy versions, and a missing name fails it.
|
|
5389
|
-
*/
|
|
5390
|
-
async updateProject(projectId, updates) {
|
|
5391
|
-
await this.post("project.update", {
|
|
5392
|
-
projectId,
|
|
5393
|
-
...updates
|
|
5394
|
-
});
|
|
5395
|
-
}
|
|
5396
|
-
/**
|
|
5397
|
-
* Delete a project and all its resources
|
|
5398
|
-
*/
|
|
5399
|
-
async deleteProject(projectId) {
|
|
5400
|
-
await this.post("project.remove", { projectId });
|
|
5401
|
-
}
|
|
5402
|
-
/**
|
|
5403
|
-
* Create an environment in a project
|
|
5404
|
-
*/
|
|
5405
|
-
async createEnvironment(projectId, name, description) {
|
|
5406
|
-
return this.post("environment.create", {
|
|
5407
|
-
projectId,
|
|
5408
|
-
name,
|
|
5409
|
-
description: description ?? `${name} environment`
|
|
5410
|
-
});
|
|
5411
|
-
}
|
|
5412
|
-
/**
|
|
5413
|
-
* List all applications in a project
|
|
5414
|
-
*/
|
|
5415
|
-
async listApplications(projectId) {
|
|
5416
|
-
return this.listInProject(projectId, (env) => env.applications);
|
|
5417
|
-
}
|
|
5418
|
-
/**
|
|
5419
|
-
* Everything of one kind in a project, across its environments.
|
|
5420
|
-
*
|
|
5421
|
-
* From `project.one`, because the per-kind `*.all?projectId=` endpoints
|
|
5422
|
-
* return **404** on current Dokploy: resources live under environments now.
|
|
5423
|
-
* Those calls used to be wrapped in `catch { return [] }`, which turned "I
|
|
5424
|
-
* could not ask" into "there are none" — and for a find-or-create that is
|
|
5425
|
-
* the worst available answer. Every deploy created a second application, a
|
|
5426
|
-
* second Postgres and a second Redis, silently, for as long as the project
|
|
5427
|
-
* existed.
|
|
5428
|
-
*
|
|
5429
|
-
* A failure here is not swallowed: creating a duplicate is worse than
|
|
5430
|
-
* stopping.
|
|
5431
|
-
*/
|
|
5432
|
-
async listInProject(projectId, pick) {
|
|
5433
|
-
return ((await this.getProject(projectId)).environments ?? []).flatMap((environment) => (pick(environment) ?? []).map((resource) => ({
|
|
5434
|
-
...resource,
|
|
5435
|
-
environmentId: environment.environmentId
|
|
5436
|
-
})));
|
|
5437
|
-
}
|
|
5438
|
-
/**
|
|
5439
|
-
* Find an application by name in a project
|
|
5440
|
-
*/
|
|
5441
|
-
async findApplicationByName(projectId, name, environmentId) {
|
|
5442
|
-
const applications = await this.listApplications(projectId);
|
|
5443
|
-
const normalizedName = name.toLowerCase().replace(/[^a-z0-9-]/g, "-");
|
|
5444
|
-
return applications.find((app) => (!environmentId || app.environmentId === environmentId) && (app.name === name || app.appName === normalizedName));
|
|
5445
|
-
}
|
|
5446
|
-
/**
|
|
5447
|
-
* Create a new application
|
|
5448
|
-
*/
|
|
5449
|
-
async createApplication(name, projectId, environmentId) {
|
|
5450
|
-
return this.post("application.create", {
|
|
5451
|
-
name,
|
|
5452
|
-
projectId,
|
|
5453
|
-
environmentId,
|
|
5454
|
-
appName: name.toLowerCase().replace(/[^a-z0-9-]/g, "-")
|
|
5455
|
-
});
|
|
5456
|
-
}
|
|
5457
|
-
/**
|
|
5458
|
-
* Find or create an application by name
|
|
5459
|
-
*/
|
|
5460
|
-
async findOrCreateApplication(name, projectId, environmentId) {
|
|
5461
|
-
const existing = await this.findApplicationByName(projectId, name, environmentId);
|
|
5462
|
-
if (existing) return {
|
|
5463
|
-
application: existing,
|
|
5464
|
-
created: false
|
|
5465
|
-
};
|
|
5466
|
-
return {
|
|
5467
|
-
application: await this.createApplication(name, projectId, environmentId),
|
|
5468
|
-
created: true
|
|
5469
|
-
};
|
|
5470
|
-
}
|
|
5471
|
-
/**
|
|
5472
|
-
* Get an application by ID
|
|
5473
|
-
*/
|
|
5474
|
-
async getApplication(applicationId) {
|
|
5475
|
-
try {
|
|
5476
|
-
return await this.get(`application.one?applicationId=${applicationId}`);
|
|
5477
|
-
} catch {
|
|
5478
|
-
return null;
|
|
5479
|
-
}
|
|
5480
|
-
}
|
|
5481
|
-
/**
|
|
5482
|
-
* Update an application
|
|
5483
|
-
*/
|
|
5484
|
-
async updateApplication(applicationId, updates) {
|
|
5485
|
-
await this.post("application.update", {
|
|
5486
|
-
applicationId,
|
|
5487
|
-
...updates
|
|
5488
|
-
});
|
|
5489
|
-
}
|
|
5490
|
-
/**
|
|
5491
|
-
* Save environment variables for an application
|
|
5492
|
-
*/
|
|
5493
|
-
async saveApplicationEnv(applicationId, env, options) {
|
|
5494
|
-
await this.post("application.saveEnvironment", {
|
|
5495
|
-
applicationId,
|
|
5496
|
-
env,
|
|
5497
|
-
buildArgs: options?.buildArgs ?? null,
|
|
5498
|
-
buildSecrets: options?.buildSecrets ?? null,
|
|
5499
|
-
createEnvFile: false
|
|
5500
|
-
});
|
|
5501
|
-
}
|
|
5502
|
-
/**
|
|
5503
|
-
* Configure application to use Docker provider (pull from registry)
|
|
5504
|
-
*
|
|
5505
|
-
* For private registries, either:
|
|
5506
|
-
* - Use `registryId` if the registry is configured in Dokploy
|
|
5507
|
-
* - Or provide `username`, `password`, and `registryUrl` directly
|
|
5508
|
-
*/
|
|
5509
|
-
async saveDockerProvider(applicationId, dockerImage, options) {
|
|
5510
|
-
await this.post("application.saveDockerProvider", {
|
|
5511
|
-
applicationId,
|
|
5512
|
-
dockerImage,
|
|
5513
|
-
...options,
|
|
5514
|
-
username: options?.username ?? null,
|
|
5515
|
-
password: options?.password ?? null,
|
|
5516
|
-
registryUrl: options?.registryUrl ?? null
|
|
5517
|
-
});
|
|
5518
|
-
}
|
|
5519
|
-
/**
|
|
5520
|
-
* Deploy an application
|
|
5521
|
-
*/
|
|
5522
|
-
async deployApplication(applicationId) {
|
|
5523
|
-
await this.post("application.deploy", { applicationId });
|
|
5524
|
-
}
|
|
5525
|
-
/**
|
|
5526
|
-
* Delete an application
|
|
5527
|
-
*/
|
|
5528
|
-
async deleteApplication(applicationId) {
|
|
5529
|
-
await this.post("application.remove", { applicationId });
|
|
5530
|
-
}
|
|
5531
|
-
/**
|
|
5532
|
-
* List all registries
|
|
5533
|
-
*/
|
|
5534
|
-
async listRegistries() {
|
|
5535
|
-
return this.get("registry.all");
|
|
5536
|
-
}
|
|
5537
|
-
/**
|
|
5538
|
-
* Create a new registry
|
|
5539
|
-
*/
|
|
5540
|
-
async createRegistry(registryName, registryUrl, username, password, options) {
|
|
5541
|
-
return this.post("registry.create", {
|
|
5542
|
-
registryName,
|
|
5543
|
-
registryUrl,
|
|
5544
|
-
username,
|
|
5545
|
-
password,
|
|
5546
|
-
imagePrefix: options?.imagePrefix
|
|
5547
|
-
});
|
|
5548
|
-
}
|
|
5549
|
-
/**
|
|
5550
|
-
* Get a registry by ID
|
|
5551
|
-
*/
|
|
5552
|
-
async getRegistry(registryId) {
|
|
5553
|
-
return this.get(`registry.one?registryId=${registryId}`);
|
|
5554
|
-
}
|
|
5555
|
-
/**
|
|
5556
|
-
* Update a registry
|
|
5557
|
-
*/
|
|
5558
|
-
async updateRegistry(registryId, updates) {
|
|
5559
|
-
await this.post("registry.update", {
|
|
5560
|
-
registryId,
|
|
5561
|
-
...updates
|
|
5562
|
-
});
|
|
5563
|
-
}
|
|
5564
|
-
/**
|
|
5565
|
-
* Delete a registry
|
|
5566
|
-
*/
|
|
5567
|
-
async deleteRegistry(registryId) {
|
|
5568
|
-
await this.post("registry.remove", { registryId });
|
|
5569
|
-
}
|
|
5570
|
-
/**
|
|
5571
|
-
* List all Postgres databases in a project
|
|
5572
|
-
*/
|
|
5573
|
-
async listPostgres(projectId) {
|
|
5574
|
-
return this.listInProject(projectId, (env) => env.postgres);
|
|
5575
|
-
}
|
|
5576
|
-
/**
|
|
5577
|
-
* Find a Postgres database by name in a project
|
|
5578
|
-
*/
|
|
5579
|
-
async findPostgresByName(projectId, name) {
|
|
5580
|
-
const databases = await this.listPostgres(projectId);
|
|
5581
|
-
const normalizedName = name.toLowerCase().replace(/[^a-z0-9-]/g, "-");
|
|
5582
|
-
const match = databases.find((db) => db.name === name || db.appName === normalizedName);
|
|
5583
|
-
if (!match) return void 0;
|
|
5584
|
-
return this.getPostgres(match.postgresId);
|
|
5585
|
-
}
|
|
5586
|
-
/**
|
|
5587
|
-
* Create a new Postgres database
|
|
5588
|
-
*/
|
|
5589
|
-
async createPostgres(name, projectId, environmentId, options) {
|
|
5590
|
-
return this.post("postgres.create", {
|
|
5591
|
-
name,
|
|
5592
|
-
projectId,
|
|
5593
|
-
environmentId,
|
|
5594
|
-
appName: options?.appName ?? name.toLowerCase().replace(/[^a-z0-9-]/g, "-"),
|
|
5595
|
-
databaseName: options?.databaseName,
|
|
5596
|
-
databaseUser: options?.databaseUser ?? "postgres",
|
|
5597
|
-
databasePassword: options?.databasePassword,
|
|
5598
|
-
dockerImage: options?.dockerImage ?? "postgres:18",
|
|
5599
|
-
description: options?.description ?? `Postgres database for ${name}`
|
|
5600
|
-
});
|
|
5601
|
-
}
|
|
5602
|
-
/**
|
|
5603
|
-
* Find or create a Postgres database by name
|
|
5604
|
-
*/
|
|
5605
|
-
async findOrCreatePostgres(name, projectId, environmentId, options) {
|
|
5606
|
-
const existing = await this.findPostgresByName(projectId, name);
|
|
5607
|
-
if (existing) return {
|
|
5608
|
-
postgres: existing,
|
|
5609
|
-
created: false
|
|
5610
|
-
};
|
|
5611
|
-
return {
|
|
5612
|
-
postgres: await this.createPostgres(name, projectId, environmentId, options),
|
|
5613
|
-
created: true
|
|
5614
|
-
};
|
|
5615
|
-
}
|
|
5616
|
-
/**
|
|
5617
|
-
* Get a Postgres database by ID
|
|
5618
|
-
*/
|
|
5619
|
-
async getPostgres(postgresId) {
|
|
5620
|
-
return this.get(`postgres.one?postgresId=${postgresId}`);
|
|
5621
|
-
}
|
|
5622
|
-
/**
|
|
5623
|
-
* Deploy a Postgres database
|
|
5624
|
-
*/
|
|
5625
|
-
async deployPostgres(postgresId) {
|
|
5626
|
-
await this.post("postgres.deploy", { postgresId });
|
|
5627
|
-
}
|
|
5628
|
-
/**
|
|
5629
|
-
* Save environment variables for Postgres
|
|
5630
|
-
*/
|
|
5631
|
-
async savePostgresEnv(postgresId, env) {
|
|
5632
|
-
await this.post("postgres.saveEnvironment", {
|
|
5633
|
-
postgresId,
|
|
5634
|
-
env
|
|
5635
|
-
});
|
|
5636
|
-
}
|
|
5637
|
-
/**
|
|
5638
|
-
* Set external port for Postgres (for external access)
|
|
5639
|
-
*/
|
|
5640
|
-
async savePostgresExternalPort(postgresId, externalPort) {
|
|
5641
|
-
await this.post("postgres.saveExternalPort", {
|
|
5642
|
-
postgresId,
|
|
5643
|
-
externalPort
|
|
5644
|
-
});
|
|
5645
|
-
}
|
|
5646
|
-
/**
|
|
5647
|
-
* Update Postgres configuration
|
|
5648
|
-
*/
|
|
5649
|
-
async updatePostgres(postgresId, updates) {
|
|
5650
|
-
await this.post("postgres.update", {
|
|
5651
|
-
postgresId,
|
|
5652
|
-
...updates
|
|
5653
|
-
});
|
|
5654
|
-
}
|
|
5655
|
-
/**
|
|
5656
|
-
* Delete a Postgres database
|
|
5657
|
-
*/
|
|
5658
|
-
async deletePostgres(postgresId) {
|
|
5659
|
-
await this.post("postgres.remove", { postgresId });
|
|
5660
|
-
}
|
|
5661
|
-
/**
|
|
5662
|
-
* The compose stacks in a project.
|
|
5663
|
-
*
|
|
5664
|
-
* Dokploy has first-class primitives for Postgres and Redis and none for
|
|
5665
|
-
* MinIO, so an object store is a compose stack — the one kind whose contents
|
|
5666
|
-
* this target writes rather than configures.
|
|
5667
|
-
*/
|
|
5668
|
-
async listCompose(projectId) {
|
|
5669
|
-
return this.listInProject(projectId, (env) => env.compose);
|
|
5670
|
-
}
|
|
5671
|
-
/** A compose stack by name, hydrated — `project.one` returns summaries. */
|
|
5672
|
-
async findComposeByName(projectId, name) {
|
|
5673
|
-
const summary = (await this.listCompose(projectId)).find((compose) => compose.name === name);
|
|
5674
|
-
return summary ? this.getCompose(summary.composeId) : void 0;
|
|
5675
|
-
}
|
|
5676
|
-
async getCompose(composeId) {
|
|
5677
|
-
return this.get(`compose.one?composeId=${composeId}`);
|
|
5678
|
-
}
|
|
5679
|
-
/**
|
|
5680
|
-
* A compose stack carrying the file this target wrote.
|
|
5681
|
-
*
|
|
5682
|
-
* Created empty and then updated, because `compose.create` takes no file:
|
|
5683
|
-
* it defaults to `sourceType: 'github'` and expects a repository. `raw` is
|
|
5684
|
-
* what makes the file ours.
|
|
5685
|
-
*/
|
|
5686
|
-
async findOrCreateCompose(name, projectId, environmentId, composeFile) {
|
|
5687
|
-
const existing = await this.findComposeByName(projectId, name);
|
|
5688
|
-
const compose = existing ?? await this.post("compose.create", {
|
|
5689
|
-
name,
|
|
5690
|
-
environmentId,
|
|
5691
|
-
composeType: "docker-compose"
|
|
5692
|
-
});
|
|
5693
|
-
await this.post("compose.update", {
|
|
5694
|
-
composeId: compose.composeId,
|
|
5695
|
-
sourceType: "raw",
|
|
5696
|
-
composeFile
|
|
5697
|
-
});
|
|
5698
|
-
return {
|
|
5699
|
-
compose,
|
|
5700
|
-
created: !existing
|
|
5701
|
-
};
|
|
5702
|
-
}
|
|
5703
|
-
async deployCompose(composeId) {
|
|
5704
|
-
await this.post("compose.deploy", { composeId });
|
|
5705
|
-
}
|
|
5706
|
-
async deleteCompose(composeId) {
|
|
5707
|
-
await this.post("compose.delete", {
|
|
5708
|
-
composeId,
|
|
5709
|
-
deleteVolumes: false
|
|
5710
|
-
});
|
|
5711
|
-
}
|
|
5712
|
-
/**
|
|
5713
|
-
* List all Redis instances in a project
|
|
5714
|
-
*/
|
|
5715
|
-
async listRedis(projectId) {
|
|
5716
|
-
return this.listInProject(projectId, (env) => env.redis);
|
|
5717
|
-
}
|
|
5718
|
-
/**
|
|
5719
|
-
* Find a Redis instance by name in a project
|
|
5720
|
-
*/
|
|
5721
|
-
async findRedisByName(projectId, name) {
|
|
5722
|
-
const instances = await this.listRedis(projectId);
|
|
5723
|
-
const normalizedName = name.toLowerCase().replace(/[^a-z0-9-]/g, "-");
|
|
5724
|
-
const match = instances.find((redis) => redis.name === name || redis.appName === normalizedName);
|
|
5725
|
-
return match ? this.getRedis(match.redisId) : void 0;
|
|
5726
|
-
}
|
|
5727
|
-
/**
|
|
5728
|
-
* Create a new Redis instance
|
|
5729
|
-
*/
|
|
5730
|
-
async createRedis(name, projectId, environmentId, options) {
|
|
5731
|
-
return this.post("redis.create", {
|
|
5732
|
-
name,
|
|
5733
|
-
projectId,
|
|
5734
|
-
environmentId,
|
|
5735
|
-
appName: options?.appName ?? name.toLowerCase().replace(/[^a-z0-9-]/g, "-"),
|
|
5736
|
-
databasePassword: options?.databasePassword,
|
|
5737
|
-
dockerImage: options?.dockerImage ?? "redis:8",
|
|
5738
|
-
description: options?.description ?? `Redis instance for ${name}`
|
|
5739
|
-
});
|
|
5740
|
-
}
|
|
5741
|
-
/**
|
|
5742
|
-
* Find or create a Redis instance by name
|
|
5743
|
-
*/
|
|
5744
|
-
async findOrCreateRedis(name, projectId, environmentId, options) {
|
|
5745
|
-
const existing = await this.findRedisByName(projectId, name);
|
|
5746
|
-
if (existing) return {
|
|
5747
|
-
redis: existing,
|
|
5748
|
-
created: false
|
|
5749
|
-
};
|
|
5750
|
-
return {
|
|
5751
|
-
redis: await this.createRedis(name, projectId, environmentId, options),
|
|
5752
|
-
created: true
|
|
5753
|
-
};
|
|
5754
|
-
}
|
|
5755
|
-
/**
|
|
5756
|
-
* Get a Redis instance by ID
|
|
5757
|
-
*/
|
|
5758
|
-
async getRedis(redisId) {
|
|
5759
|
-
return this.get(`redis.one?redisId=${redisId}`);
|
|
5760
|
-
}
|
|
5761
|
-
/**
|
|
5762
|
-
* Deploy a Redis instance
|
|
5763
|
-
*/
|
|
5764
|
-
async deployRedis(redisId) {
|
|
5765
|
-
await this.post("redis.deploy", { redisId });
|
|
5766
|
-
}
|
|
5767
|
-
/**
|
|
5768
|
-
* Save environment variables for Redis
|
|
5769
|
-
*/
|
|
5770
|
-
async saveRedisEnv(redisId, env) {
|
|
5771
|
-
await this.post("redis.saveEnvironment", {
|
|
5772
|
-
redisId,
|
|
5773
|
-
env
|
|
5774
|
-
});
|
|
5775
|
-
}
|
|
5776
|
-
/**
|
|
5777
|
-
* Set external port for Redis (for external access)
|
|
5778
|
-
*/
|
|
5779
|
-
async saveRedisExternalPort(redisId, externalPort) {
|
|
5780
|
-
await this.post("redis.saveExternalPort", {
|
|
5781
|
-
redisId,
|
|
5782
|
-
externalPort
|
|
5783
|
-
});
|
|
5784
|
-
}
|
|
5785
|
-
/**
|
|
5786
|
-
* Update Redis configuration
|
|
5787
|
-
*/
|
|
5788
|
-
async updateRedis(redisId, updates) {
|
|
5789
|
-
await this.post("redis.update", {
|
|
5790
|
-
redisId,
|
|
5791
|
-
...updates
|
|
5792
|
-
});
|
|
5793
|
-
}
|
|
5794
|
-
/**
|
|
5795
|
-
* Delete a Redis instance
|
|
5796
|
-
*/
|
|
5797
|
-
async deleteRedis(redisId) {
|
|
5798
|
-
await this.post("redis.remove", { redisId });
|
|
5799
|
-
}
|
|
5800
|
-
/**
|
|
5801
|
-
* Create a new domain for an application
|
|
5802
|
-
*/
|
|
5803
|
-
async createDomain(options) {
|
|
5804
|
-
return this.post("domain.create", options);
|
|
5805
|
-
}
|
|
5806
|
-
/**
|
|
5807
|
-
* Update an existing domain
|
|
5808
|
-
*/
|
|
5809
|
-
async updateDomain(domainId, updates) {
|
|
5810
|
-
await this.post("domain.update", {
|
|
5811
|
-
domainId,
|
|
5812
|
-
...updates
|
|
5813
|
-
});
|
|
5814
|
-
}
|
|
5815
|
-
/**
|
|
5816
|
-
* Delete a domain
|
|
5817
|
-
*/
|
|
5818
|
-
async deleteDomain(domainId) {
|
|
5819
|
-
await this.post("domain.delete", { domainId });
|
|
5820
|
-
}
|
|
5821
|
-
/**
|
|
5822
|
-
* Get a domain by ID
|
|
5823
|
-
*/
|
|
5824
|
-
async getDomain(domainId) {
|
|
5825
|
-
return this.get(`domain.one?domainId=${domainId}`);
|
|
5826
|
-
}
|
|
5827
|
-
/**
|
|
5828
|
-
* Get all domains for an application
|
|
5829
|
-
*/
|
|
5830
|
-
async getDomainsByApplicationId(applicationId) {
|
|
5831
|
-
return this.get(`domain.byApplicationId?applicationId=${applicationId}`);
|
|
5832
|
-
}
|
|
5833
|
-
/**
|
|
5834
|
-
* Validate a domain and trigger SSL certificate generation
|
|
5835
|
-
*
|
|
5836
|
-
* This should be called after DNS records are created and propagated.
|
|
5837
|
-
* It triggers Let's Encrypt certificate generation for HTTPS domains.
|
|
5838
|
-
*
|
|
5839
|
-
* @param domain - The domain hostname to validate (e.g., 'api.example.com')
|
|
5840
|
-
*/
|
|
5841
|
-
async validateDomain(domain) {
|
|
5842
|
-
return this.post("domain.validateDomain", { domain });
|
|
5843
|
-
}
|
|
5844
|
-
/**
|
|
5845
|
-
* Auto-generate a domain name for an application
|
|
5846
|
-
*/
|
|
5847
|
-
async generateDomain(appName, serverId) {
|
|
5848
|
-
return this.post("domain.generateDomain", {
|
|
5849
|
-
appName,
|
|
5850
|
-
serverId
|
|
5851
|
-
});
|
|
5852
|
-
}
|
|
5853
|
-
/**
|
|
5854
|
-
* List all backup destinations
|
|
5855
|
-
*/
|
|
5856
|
-
async listDestinations() {
|
|
5857
|
-
return this.get("destination.all");
|
|
5858
|
-
}
|
|
5859
|
-
/**
|
|
5860
|
-
* Get a destination by ID
|
|
5861
|
-
*/
|
|
5862
|
-
async getDestination(destinationId) {
|
|
5863
|
-
return this.get(`destination.one?destinationId=${destinationId}`);
|
|
5864
|
-
}
|
|
5865
|
-
/**
|
|
5866
|
-
* Create a new S3 backup destination
|
|
5867
|
-
*/
|
|
5868
|
-
async createDestination(options) {
|
|
5869
|
-
return this.post("destination.create", { ...options });
|
|
5870
|
-
}
|
|
5871
|
-
/**
|
|
5872
|
-
* Find a destination by name
|
|
5873
|
-
*/
|
|
5874
|
-
async findDestinationByName(name) {
|
|
5875
|
-
return (await this.listDestinations()).find((d) => d.name === name);
|
|
5876
|
-
}
|
|
5877
|
-
/**
|
|
5878
|
-
* Find or create a destination by name
|
|
5879
|
-
*/
|
|
5880
|
-
async findOrCreateDestination(name, options) {
|
|
5881
|
-
const existing = await this.findDestinationByName(name);
|
|
5882
|
-
if (existing) return {
|
|
5883
|
-
destination: existing,
|
|
5884
|
-
created: false
|
|
5885
|
-
};
|
|
5886
|
-
return {
|
|
5887
|
-
destination: await this.createDestination({
|
|
5888
|
-
name,
|
|
5889
|
-
...options
|
|
5890
|
-
}),
|
|
5891
|
-
created: true
|
|
5892
|
-
};
|
|
5893
|
-
}
|
|
5894
|
-
/**
|
|
5895
|
-
* Update a destination
|
|
5896
|
-
*/
|
|
5897
|
-
async updateDestination(destinationId, updates) {
|
|
5898
|
-
await this.post("destination.update", {
|
|
5899
|
-
destinationId,
|
|
5900
|
-
...updates
|
|
5901
|
-
});
|
|
5902
|
-
}
|
|
5903
|
-
/**
|
|
5904
|
-
* Delete a destination
|
|
5905
|
-
*/
|
|
5906
|
-
async deleteDestination(destinationId) {
|
|
5907
|
-
await this.post("destination.remove", { destinationId });
|
|
5908
|
-
}
|
|
5909
|
-
/**
|
|
5910
|
-
* Test connection to a destination
|
|
5911
|
-
*/
|
|
5912
|
-
async testDestinationConnection(destinationId) {
|
|
5913
|
-
return this.post("destination.testConnection", { destinationId });
|
|
4444
|
+
const seed = secrets.seed ?? random();
|
|
4445
|
+
if (!secrets.seed) generated.push("seed");
|
|
4446
|
+
return {
|
|
4447
|
+
secrets: generated.length ? {
|
|
4448
|
+
...secrets,
|
|
4449
|
+
seed,
|
|
4450
|
+
custom,
|
|
4451
|
+
updatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
4452
|
+
} : secrets,
|
|
4453
|
+
generated
|
|
4454
|
+
};
|
|
4455
|
+
}
|
|
4456
|
+
/** 256 bits, URL-safe. */
|
|
4457
|
+
function random() {
|
|
4458
|
+
return (0, node_crypto.randomBytes)(32).toString("base64url");
|
|
4459
|
+
}
|
|
4460
|
+
//#endregion
|
|
4461
|
+
//#region src/deploy/journal.ts
|
|
4462
|
+
var DeployJournal = class DeployJournal {
|
|
4463
|
+
store;
|
|
4464
|
+
stage;
|
|
4465
|
+
state;
|
|
4466
|
+
resources;
|
|
4467
|
+
version;
|
|
4468
|
+
existed;
|
|
4469
|
+
constructor(store, stage, state, resources, version, existed) {
|
|
4470
|
+
this.store = store;
|
|
4471
|
+
this.stage = stage;
|
|
4472
|
+
this.state = state;
|
|
4473
|
+
this.resources = resources;
|
|
4474
|
+
this.version = version;
|
|
4475
|
+
this.existed = existed;
|
|
5914
4476
|
}
|
|
5915
4477
|
/**
|
|
5916
|
-
*
|
|
4478
|
+
* The stage's journal — its stored state, or `initial` written as the
|
|
4479
|
+
* stage's first state, so there is somewhere to record the very first
|
|
4480
|
+
* resource before it is created.
|
|
5917
4481
|
*/
|
|
5918
|
-
async
|
|
5919
|
-
|
|
5920
|
-
|
|
5921
|
-
|
|
4482
|
+
static async open(store, stage, initial) {
|
|
4483
|
+
const stored = await store.read(stage);
|
|
4484
|
+
if (stored) return new DeployJournal(store, stage, stored.state, stored.resources, stored.version, true);
|
|
4485
|
+
const state = initial();
|
|
4486
|
+
const version = await store.write(stage, state, { expectedVersion: null });
|
|
4487
|
+
return new DeployJournal(store, stage, state, {}, version, false);
|
|
4488
|
+
}
|
|
4489
|
+
record(key) {
|
|
4490
|
+
return this.resources[key];
|
|
4491
|
+
}
|
|
4492
|
+
/** The keys a previous run marked `pending` and never saw created. */
|
|
4493
|
+
unfinished() {
|
|
4494
|
+
return Object.values(this.resources).filter((record) => record.status === "pending").map((record) => record.key);
|
|
4495
|
+
}
|
|
4496
|
+
/** Records that `entry` is about to be created. */
|
|
4497
|
+
async pending(entry) {
|
|
4498
|
+
await this.put({
|
|
4499
|
+
...entry,
|
|
4500
|
+
status: "pending"
|
|
5922
4501
|
});
|
|
5923
4502
|
}
|
|
5924
|
-
/**
|
|
5925
|
-
|
|
5926
|
-
|
|
5927
|
-
|
|
5928
|
-
|
|
5929
|
-
|
|
5930
|
-
|
|
5931
|
-
|
|
5932
|
-
*/
|
|
5933
|
-
async getBackup(backupId) {
|
|
5934
|
-
return this.get(`backup.one?backupId=${backupId}`);
|
|
5935
|
-
}
|
|
5936
|
-
/**
|
|
5937
|
-
* Update a backup schedule
|
|
5938
|
-
*/
|
|
5939
|
-
async updateBackup(backupId, updates) {
|
|
5940
|
-
await this.post("backup.update", {
|
|
5941
|
-
backupId,
|
|
5942
|
-
...updates
|
|
4503
|
+
/** Records `entry` as existing with `id`; a no-op if it already is. */
|
|
4504
|
+
async ready(entry, id) {
|
|
4505
|
+
const current = this.resources[entry.key];
|
|
4506
|
+
if (current?.status === "ready" && current.id === id) return;
|
|
4507
|
+
await this.put({
|
|
4508
|
+
...entry,
|
|
4509
|
+
status: "ready",
|
|
4510
|
+
id
|
|
5943
4511
|
});
|
|
5944
4512
|
}
|
|
5945
4513
|
/**
|
|
5946
|
-
*
|
|
4514
|
+
* The resource `entry` names: by its recorded id, else by looking it up,
|
|
4515
|
+
* else created — with `pending` written before the create and `ready`
|
|
4516
|
+
* after, so a run that dies in between leaves a record the next run
|
|
4517
|
+
* resolves by looking the resource up rather than creating another.
|
|
5947
4518
|
*/
|
|
5948
|
-
async
|
|
5949
|
-
|
|
4519
|
+
async ensure(entry, steps) {
|
|
4520
|
+
const record = this.resources[entry.key];
|
|
4521
|
+
let staleId;
|
|
4522
|
+
if (record?.status === "ready" && record.id && steps.get) {
|
|
4523
|
+
const known = await steps.get(record.id);
|
|
4524
|
+
if (known) {
|
|
4525
|
+
await this.ready(entry, steps.id(known));
|
|
4526
|
+
return {
|
|
4527
|
+
resource: known,
|
|
4528
|
+
via: "recorded"
|
|
4529
|
+
};
|
|
4530
|
+
}
|
|
4531
|
+
staleId = record.id;
|
|
4532
|
+
}
|
|
4533
|
+
const existing = await steps.find();
|
|
4534
|
+
if (existing) {
|
|
4535
|
+
await this.ready(entry, steps.id(existing));
|
|
4536
|
+
return {
|
|
4537
|
+
resource: existing,
|
|
4538
|
+
via: record?.status === "pending" ? "resumed" : "found",
|
|
4539
|
+
...staleId ? { staleId } : {}
|
|
4540
|
+
};
|
|
4541
|
+
}
|
|
4542
|
+
await this.pending(entry);
|
|
4543
|
+
const created = await steps.create();
|
|
4544
|
+
await this.ready(entry, steps.id(created));
|
|
4545
|
+
return {
|
|
4546
|
+
resource: created,
|
|
4547
|
+
via: "created",
|
|
4548
|
+
...staleId ? { staleId } : {}
|
|
4549
|
+
};
|
|
5950
4550
|
}
|
|
5951
|
-
/**
|
|
5952
|
-
|
|
5953
|
-
|
|
5954
|
-
|
|
5955
|
-
|
|
4551
|
+
/** Writes the stage's state, keeping its resource records. */
|
|
4552
|
+
async save() {
|
|
4553
|
+
this.state.lastDeployedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
4554
|
+
this.version = await this.store.write(this.stage, this.state, { expectedVersion: this.version });
|
|
4555
|
+
}
|
|
4556
|
+
async put(record) {
|
|
4557
|
+
this.version = await this.store.putResource(this.stage, record, { expectedVersion: this.version });
|
|
4558
|
+
this.resources = {
|
|
4559
|
+
...this.resources,
|
|
4560
|
+
[record.key]: {
|
|
4561
|
+
...record,
|
|
4562
|
+
updatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
4563
|
+
}
|
|
4564
|
+
};
|
|
5956
4565
|
}
|
|
5957
4566
|
};
|
|
5958
4567
|
//#endregion
|
|
5959
|
-
//#region src/deploy/
|
|
5960
|
-
const __filename$1 = (0, node_url.fileURLToPath)(require("url").pathToFileURL(__filename).href);
|
|
5961
|
-
const __dirname$1 = (0, node_path.dirname)(__filename$1);
|
|
4568
|
+
//#region src/deploy/state.ts
|
|
5962
4569
|
/**
|
|
5963
|
-
*
|
|
5964
|
-
* Handles both dev (.ts with tsx) and production (.mjs from dist).
|
|
5965
|
-
*
|
|
5966
|
-
* In production: sniffer.ts is bundled into dist/index.mjs, but sniffer helper
|
|
5967
|
-
* files are output to dist/deploy/ as standalone modules for subprocess loading.
|
|
4570
|
+
* Deploy state management for Dokploy deployments
|
|
5968
4571
|
*
|
|
5969
|
-
*
|
|
4572
|
+
* Stores resource IDs (applications, services) per stage to avoid
|
|
4573
|
+
* re-creating resources on subsequent deploys.
|
|
5970
4574
|
*/
|
|
5971
|
-
function resolveSnifferFile(baseName) {
|
|
5972
|
-
const deployMjsPath = (0, node_path.resolve)(__dirname$1, "deploy", `${baseName}.mjs`);
|
|
5973
|
-
if ((0, node_fs.existsSync)(deployMjsPath)) return deployMjsPath;
|
|
5974
|
-
const mjsPath = (0, node_path.resolve)(__dirname$1, `${baseName}.mjs`);
|
|
5975
|
-
if ((0, node_fs.existsSync)(mjsPath)) return mjsPath;
|
|
5976
|
-
const tsPath = (0, node_path.resolve)(__dirname$1, `${baseName}.ts`);
|
|
5977
|
-
if ((0, node_fs.existsSync)(tsPath)) return tsPath;
|
|
5978
|
-
return tsPath;
|
|
5979
|
-
}
|
|
5980
4575
|
/**
|
|
5981
|
-
*
|
|
5982
|
-
* nothing of the deploy's: the deploy's Dokploy token, registry login and AWS
|
|
5983
|
-
* keys stay where they are. A worker that outlives the timeout is killed, and
|
|
5984
|
-
* comes back as the error the caller reports.
|
|
4576
|
+
* Create a new empty state for a stage
|
|
5985
4577
|
*/
|
|
5986
|
-
|
|
5987
|
-
|
|
5988
|
-
|
|
5989
|
-
|
|
5990
|
-
|
|
5991
|
-
|
|
5992
|
-
|
|
5993
|
-
|
|
5994
|
-
|
|
5995
|
-
|
|
5996
|
-
stdout: result.stdout,
|
|
5997
|
-
stderr: result.stderr,
|
|
5998
|
-
code: result.exitCode
|
|
5999
|
-
};
|
|
6000
|
-
} catch (error) {
|
|
6001
|
-
return { error: error instanceof Error ? error : new Error(String(error)) };
|
|
6002
|
-
}
|
|
4578
|
+
function createEmptyState(stage, projectId, environmentId) {
|
|
4579
|
+
return {
|
|
4580
|
+
provider: "dokploy",
|
|
4581
|
+
stage,
|
|
4582
|
+
projectId,
|
|
4583
|
+
environmentId,
|
|
4584
|
+
applications: {},
|
|
4585
|
+
services: {},
|
|
4586
|
+
lastDeployedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
4587
|
+
};
|
|
6003
4588
|
}
|
|
6004
|
-
/**
|
|
6005
|
-
|
|
6006
|
-
|
|
6007
|
-
|
|
6008
|
-
|
|
6009
|
-
} catch {
|
|
6010
|
-
return;
|
|
6011
|
-
}
|
|
4589
|
+
/**
|
|
4590
|
+
* Set application ID in state (mutates state)
|
|
4591
|
+
*/
|
|
4592
|
+
function setApplicationId(state, appName, applicationId) {
|
|
4593
|
+
state.applications[appName] = applicationId;
|
|
6012
4594
|
}
|
|
6013
4595
|
/**
|
|
6014
|
-
*
|
|
6015
|
-
*
|
|
6016
|
-
* Detection strategy (in order):
|
|
6017
|
-
* 1. Frontend apps: Returns empty (no server secrets)
|
|
6018
|
-
* 2. Apps with `requiredEnv`: Uses explicit list from config
|
|
6019
|
-
* 3. Entry apps: Imports entry file in subprocess to capture config.parse() calls
|
|
6020
|
-
* 4. Route-based apps: Loads route files and calls getEnvironment() on each construct
|
|
6021
|
-
* 5. Apps with `envParser` (no routes): Runs SnifferEnvironmentParser to detect usage
|
|
6022
|
-
* 6. Apps with neither: Returns empty
|
|
6023
|
-
*
|
|
6024
|
-
* This function handles "fire and forget" async operations gracefully,
|
|
6025
|
-
* capturing errors and unhandled rejections without failing the build.
|
|
6026
|
-
*
|
|
6027
|
-
* @param app - The normalized app configuration
|
|
6028
|
-
* @param appName - The name of the app
|
|
6029
|
-
* @param workspacePath - Absolute path to the workspace root
|
|
6030
|
-
* @param options - Optional configuration for sniffing behavior
|
|
6031
|
-
* @returns The sniffed environment with required variables
|
|
4596
|
+
* Record the image an app was deployed with (mutates state)
|
|
6032
4597
|
*/
|
|
6033
|
-
|
|
6034
|
-
|
|
6035
|
-
|
|
6036
|
-
|
|
6037
|
-
...options.timeoutMs ? { timeoutMs: options.timeoutMs } : {}
|
|
6038
|
-
};
|
|
6039
|
-
if (app.type === "web" || app.type === "mobile") {
|
|
6040
|
-
const publicPrefix = require_workspace.getPublicEnvPrefix(app.framework);
|
|
6041
|
-
const depVars = publicPrefix ? (app.dependencies ?? []).map((dep) => `${publicPrefix}${dep.toUpperCase()}_URL`) : [];
|
|
6042
|
-
if (app.config) {
|
|
6043
|
-
const sniffedVars = [];
|
|
6044
|
-
const sniffedOptional = [];
|
|
6045
|
-
const configPaths = [];
|
|
6046
|
-
if (app.config.client) configPaths.push(app.config.client);
|
|
6047
|
-
if (app.config.server) configPaths.push(app.config.server);
|
|
6048
|
-
for (const configPath of configPaths) {
|
|
6049
|
-
const result = await sniffEntryFile(configPath, app.path, workspacePath, run);
|
|
6050
|
-
if (logWarnings && result.error) require_discover.output.warn(`[sniffer] ${appName}: Config file "${configPath}" threw error during sniffing (env vars still captured): ${result.error.message}`);
|
|
6051
|
-
sniffedVars.push(...result.envVars);
|
|
6052
|
-
sniffedOptional.push(...result.optionalEnvVars);
|
|
6053
|
-
}
|
|
6054
|
-
const optionalEnvVars = [...new Set(sniffedOptional)];
|
|
6055
|
-
return {
|
|
6056
|
-
appName,
|
|
6057
|
-
requiredEnvVars: applyMarkOptional([.../* @__PURE__ */ new Set([...depVars, ...sniffedVars])], optionalEnvVars, markOptional),
|
|
6058
|
-
optionalEnvVars
|
|
6059
|
-
};
|
|
6060
|
-
}
|
|
6061
|
-
return {
|
|
6062
|
-
appName,
|
|
6063
|
-
requiredEnvVars: depVars,
|
|
6064
|
-
optionalEnvVars: []
|
|
6065
|
-
};
|
|
6066
|
-
}
|
|
6067
|
-
if (app.entry) {
|
|
6068
|
-
const result = await sniffEntryFile(app.entry, app.path, workspacePath, run);
|
|
6069
|
-
if (logWarnings && result.error) require_discover.output.warn(`[sniffer] ${appName}: Entry file threw error during sniffing (env vars still captured): ${result.error.message}`);
|
|
6070
|
-
return {
|
|
6071
|
-
appName,
|
|
6072
|
-
requiredEnvVars: applyMarkOptional(result.envVars, result.optionalEnvVars, markOptional),
|
|
6073
|
-
optionalEnvVars: result.optionalEnvVars
|
|
6074
|
-
};
|
|
6075
|
-
}
|
|
6076
|
-
if (app.routes) {
|
|
6077
|
-
const result = await sniffRouteFiles(normalizeRoutes(app.routes), app.path, workspacePath, run);
|
|
6078
|
-
if (logWarnings && result.error) require_discover.output.warn(`[sniffer] ${appName}: Route sniffing threw error (env vars still captured): ${result.error.message}`);
|
|
6079
|
-
return {
|
|
6080
|
-
appName,
|
|
6081
|
-
requiredEnvVars: applyMarkOptional(result.envVars, result.optionalEnvVars, markOptional),
|
|
6082
|
-
optionalEnvVars: result.optionalEnvVars
|
|
6083
|
-
};
|
|
6084
|
-
}
|
|
6085
|
-
if (app.envParser) {
|
|
6086
|
-
const result = await sniffEnvParser(app.envParser, app.path, workspacePath, run);
|
|
6087
|
-
if (logWarnings) {
|
|
6088
|
-
if (result.error) require_discover.output.warn(`[sniffer] ${appName}: envParser threw error during sniffing (env vars still captured): ${result.error.message}`);
|
|
6089
|
-
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(", ")}`);
|
|
6090
|
-
}
|
|
6091
|
-
return {
|
|
6092
|
-
appName,
|
|
6093
|
-
requiredEnvVars: applyMarkOptional(result.envVars, result.optionalEnvVars, markOptional),
|
|
6094
|
-
optionalEnvVars: result.optionalEnvVars
|
|
6095
|
-
};
|
|
6096
|
-
}
|
|
6097
|
-
return {
|
|
6098
|
-
appName,
|
|
6099
|
-
requiredEnvVars: [],
|
|
6100
|
-
optionalEnvVars: []
|
|
4598
|
+
function setDeployedImage(state, appName, image) {
|
|
4599
|
+
state.images = {
|
|
4600
|
+
...state.images,
|
|
4601
|
+
[appName]: image
|
|
6101
4602
|
};
|
|
6102
4603
|
}
|
|
6103
4604
|
/**
|
|
6104
|
-
*
|
|
6105
|
-
* Only modifies the list when `markOptional` is true and there are optional vars.
|
|
4605
|
+
* Get a generated secret for an app
|
|
6106
4606
|
*/
|
|
6107
|
-
function
|
|
6108
|
-
|
|
6109
|
-
const optionalSet = new Set(optionalEnvVars);
|
|
6110
|
-
return envVars.map((v) => optionalSet.has(v) ? `${v}?` : v);
|
|
4607
|
+
function getGeneratedSecret(state, appName, secretName) {
|
|
4608
|
+
return state?.generatedSecrets?.[appName]?.[secretName];
|
|
6111
4609
|
}
|
|
6112
4610
|
/**
|
|
6113
|
-
*
|
|
6114
|
-
*
|
|
6115
|
-
* Entry apps call `config.parse()` at module load time. To capture which
|
|
6116
|
-
* env vars are accessed, we:
|
|
6117
|
-
* 1. Spawn a subprocess with a module loader hook
|
|
6118
|
-
* 2. The loader intercepts `@geekmidas/envkit` and replaces EnvironmentParser
|
|
6119
|
-
* with SnifferEnvironmentParser
|
|
6120
|
-
* 3. Import the entry file (triggers config.parse())
|
|
6121
|
-
* 4. Capture and return the accessed env var names
|
|
6122
|
-
*
|
|
6123
|
-
* This approach provides process isolation - each app is sniffed in its own
|
|
6124
|
-
* subprocess, preventing module cache pollution.
|
|
6125
|
-
*
|
|
6126
|
-
* @param entryPath - Relative path to the entry file (e.g., './src/index.ts')
|
|
6127
|
-
* @param appPath - The app's path relative to workspace (e.g., 'apps/auth')
|
|
6128
|
-
* @param workspacePath - Absolute path to workspace root
|
|
6129
|
-
* @returns EntrySniffResult with env vars and optional error
|
|
4611
|
+
* Set a generated secret for an app (mutates state)
|
|
6130
4612
|
*/
|
|
6131
|
-
|
|
6132
|
-
|
|
6133
|
-
|
|
6134
|
-
|
|
6135
|
-
const ran = await runSniffer(run, workspacePath, (0, node_path.resolve)(workspacePath, appPath), require_discover.nodeWithTsx(workerPath, [fullEntryPath], [loaderPath]));
|
|
6136
|
-
if ("error" in ran) return {
|
|
6137
|
-
envVars: [],
|
|
6138
|
-
optionalEnvVars: [],
|
|
6139
|
-
error: ran.error
|
|
6140
|
-
};
|
|
6141
|
-
const answer = snifferAnswer(ran.stdout);
|
|
6142
|
-
if (answer) return {
|
|
6143
|
-
envVars: answer.envVars || [],
|
|
6144
|
-
optionalEnvVars: answer.optionalEnvVars || [],
|
|
6145
|
-
error: answer.error ? new Error(answer.error) : void 0
|
|
6146
|
-
};
|
|
6147
|
-
return {
|
|
6148
|
-
envVars: [],
|
|
6149
|
-
optionalEnvVars: [],
|
|
6150
|
-
error: /* @__PURE__ */ new Error(`Failed to sniff entry file (exit code ${ran.code}): ${ran.stderr || ran.stdout || "No output"}`)
|
|
6151
|
-
};
|
|
4613
|
+
function setGeneratedSecret(state, appName, secretName, value) {
|
|
4614
|
+
if (!state.generatedSecrets) state.generatedSecrets = {};
|
|
4615
|
+
if (!state.generatedSecrets[appName]) state.generatedSecrets[appName] = {};
|
|
4616
|
+
state.generatedSecrets[appName][secretName] = value;
|
|
6152
4617
|
}
|
|
6153
4618
|
/**
|
|
6154
|
-
*
|
|
6155
|
-
*
|
|
6156
|
-
* Route-based apps have endpoints, functions, crons, and subscribers that
|
|
6157
|
-
* use services. Each service's register() method accesses environment variables.
|
|
6158
|
-
*
|
|
6159
|
-
* This runs in a subprocess with tsx loader to properly handle TypeScript
|
|
6160
|
-
* compilation and path alias resolution (e.g., `src/...` imports).
|
|
6161
|
-
*
|
|
6162
|
-
* @param routes - Glob pattern(s) for route files
|
|
6163
|
-
* @param appPath - The app's path relative to workspace (e.g., 'apps/api')
|
|
6164
|
-
* @param workspacePath - Absolute path to workspace root
|
|
6165
|
-
* @returns EntrySniffResult with env vars and optional error
|
|
4619
|
+
* Set DNS verification record for a hostname (mutates state)
|
|
6166
4620
|
*/
|
|
6167
|
-
|
|
6168
|
-
|
|
6169
|
-
|
|
6170
|
-
|
|
6171
|
-
|
|
6172
|
-
envVars: [],
|
|
6173
|
-
optionalEnvVars: [],
|
|
6174
|
-
error: /* @__PURE__ */ new Error("No route patterns provided")
|
|
6175
|
-
};
|
|
6176
|
-
const ran = await runSniffer(run, workspacePath, fullAppPath, require_discover.nodeWithTsx(workerPath, [fullAppPath, pattern]));
|
|
6177
|
-
if ("error" in ran) return {
|
|
6178
|
-
envVars: [],
|
|
6179
|
-
optionalEnvVars: [],
|
|
6180
|
-
error: ran.error
|
|
6181
|
-
};
|
|
6182
|
-
if (ran.stderr) ran.stderr.split("\n").filter((line) => line.trim()).forEach((line) => require_discover.output.warn(line));
|
|
6183
|
-
const answer = snifferAnswer(ran.stdout);
|
|
6184
|
-
if (answer) return {
|
|
6185
|
-
envVars: answer.envVars || [],
|
|
6186
|
-
optionalEnvVars: answer.optionalEnvVars || [],
|
|
6187
|
-
error: answer.error ? new Error(answer.error) : void 0
|
|
6188
|
-
};
|
|
6189
|
-
return {
|
|
6190
|
-
envVars: [],
|
|
6191
|
-
optionalEnvVars: [],
|
|
6192
|
-
error: /* @__PURE__ */ new Error(`Failed to sniff route files (exit code ${ran.code}): ${ran.stderr || ran.stdout || "No output"}`)
|
|
4621
|
+
function setDnsVerification(state, hostname, serverIp) {
|
|
4622
|
+
if (!state.dnsVerified) state.dnsVerified = {};
|
|
4623
|
+
state.dnsVerified[hostname] = {
|
|
4624
|
+
serverIp,
|
|
4625
|
+
verifiedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
6193
4626
|
};
|
|
6194
4627
|
}
|
|
6195
4628
|
/**
|
|
6196
|
-
*
|
|
6197
|
-
* which environment variables it accesses.
|
|
6198
|
-
*
|
|
6199
|
-
* In a subprocess, like the entry and route sniffers: the module is the
|
|
6200
|
-
* project's code, and it was imported into the deploy itself before — with
|
|
6201
|
-
* every credential the deploy held in reach. Fire-and-forget rejections are
|
|
6202
|
-
* collected there with the shared `sniffWithFireAndForget`.
|
|
6203
|
-
*
|
|
6204
|
-
* @param envParserPath - The envParser config (e.g., './src/config/env#envParser')
|
|
6205
|
-
* @param appPath - The app's path relative to workspace
|
|
6206
|
-
* @param workspacePath - Absolute path to workspace root
|
|
6207
|
-
* @returns SniffResult with env vars and any errors encountered
|
|
4629
|
+
* Check if a hostname is already verified with the given IP
|
|
6208
4630
|
*/
|
|
6209
|
-
|
|
6210
|
-
|
|
6211
|
-
|
|
6212
|
-
|
|
6213
|
-
|
|
6214
|
-
|
|
6215
|
-
|
|
6216
|
-
|
|
6217
|
-
|
|
6218
|
-
|
|
6219
|
-
|
|
6220
|
-
|
|
6221
|
-
|
|
6222
|
-
|
|
6223
|
-
|
|
6224
|
-
|
|
6225
|
-
|
|
6226
|
-
|
|
6227
|
-
if (!answer) return {
|
|
6228
|
-
envVars: [],
|
|
6229
|
-
optionalEnvVars: [],
|
|
6230
|
-
unhandledRejections: [],
|
|
6231
|
-
error: /* @__PURE__ */ new Error(`Failed to sniff envParser (exit code ${ran.code}): ${ran.stderr || ran.stdout || "No output"}`)
|
|
6232
|
-
};
|
|
6233
|
-
for (const warning of answer.warnings ?? []) require_discover.output.warn(warning);
|
|
6234
|
-
return {
|
|
6235
|
-
envVars: answer.envVars ?? [],
|
|
6236
|
-
optionalEnvVars: answer.optionalEnvVars ?? [],
|
|
6237
|
-
unhandledRejections: (answer.unhandledRejections ?? []).map((message) => new Error(message)),
|
|
6238
|
-
...answer.error ? { error: new Error(answer.error) } : {}
|
|
4631
|
+
function isDnsVerified(state, hostname, serverIp) {
|
|
4632
|
+
return (state?.dnsVerified?.[hostname])?.serverIp === serverIp;
|
|
4633
|
+
}
|
|
4634
|
+
/**
|
|
4635
|
+
* Get the key for a DNS record in state
|
|
4636
|
+
*/
|
|
4637
|
+
function getDnsRecordKey(name, type) {
|
|
4638
|
+
return `${name}:${type}`;
|
|
4639
|
+
}
|
|
4640
|
+
/**
|
|
4641
|
+
* Set a created DNS record in state (mutates state)
|
|
4642
|
+
*/
|
|
4643
|
+
function setDnsRecord(state, record) {
|
|
4644
|
+
if (!state.dnsRecords) state.dnsRecords = {};
|
|
4645
|
+
const key = getDnsRecordKey(record.name, record.type);
|
|
4646
|
+
state.dnsRecords[key] = {
|
|
4647
|
+
...record,
|
|
4648
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
6239
4649
|
};
|
|
6240
4650
|
}
|
|
6241
4651
|
/**
|
|
6242
|
-
*
|
|
6243
|
-
*
|
|
6244
|
-
* @param apps - Map of app name to app config
|
|
6245
|
-
* @param workspacePath - Absolute path to workspace root
|
|
6246
|
-
* @param options - Optional configuration for sniffing behavior
|
|
6247
|
-
* @returns Map of app name to sniffed environment
|
|
4652
|
+
* Get backup state from state
|
|
6248
4653
|
*/
|
|
6249
|
-
|
|
6250
|
-
|
|
6251
|
-
|
|
6252
|
-
|
|
6253
|
-
|
|
6254
|
-
|
|
6255
|
-
|
|
4654
|
+
function getBackupState(state) {
|
|
4655
|
+
return state?.backups;
|
|
4656
|
+
}
|
|
4657
|
+
/**
|
|
4658
|
+
* Set backup state (mutates state)
|
|
4659
|
+
*/
|
|
4660
|
+
function setBackupState(state, backupState) {
|
|
4661
|
+
state.backups = backupState;
|
|
4662
|
+
}
|
|
4663
|
+
/**
|
|
4664
|
+
* Set postgres backup ID in state (mutates state)
|
|
4665
|
+
*/
|
|
4666
|
+
function setPostgresBackupId(state, backupId) {
|
|
4667
|
+
if (state.backups) state.backups.postgresBackupId = backupId;
|
|
6256
4668
|
}
|
|
6257
4669
|
//#endregion
|
|
6258
4670
|
Object.defineProperty(exports, "DEFAULT_IMAGES", {
|
|
@@ -6261,34 +4673,22 @@ Object.defineProperty(exports, "DEFAULT_IMAGES", {
|
|
|
6261
4673
|
return DEFAULT_IMAGES;
|
|
6262
4674
|
}
|
|
6263
4675
|
});
|
|
6264
|
-
Object.defineProperty(exports, "
|
|
6265
|
-
enumerable: true,
|
|
6266
|
-
get: function() {
|
|
6267
|
-
return DockerBuildFailed;
|
|
6268
|
-
}
|
|
6269
|
-
});
|
|
6270
|
-
Object.defineProperty(exports, "DockerPushFailed", {
|
|
6271
|
-
enumerable: true,
|
|
6272
|
-
get: function() {
|
|
6273
|
-
return DockerPushFailed;
|
|
6274
|
-
}
|
|
6275
|
-
});
|
|
6276
|
-
Object.defineProperty(exports, "DocumentStateStore", {
|
|
4676
|
+
Object.defineProperty(exports, "DeployJournal", {
|
|
6277
4677
|
enumerable: true,
|
|
6278
4678
|
get: function() {
|
|
6279
|
-
return
|
|
4679
|
+
return DeployJournal;
|
|
6280
4680
|
}
|
|
6281
4681
|
});
|
|
6282
|
-
Object.defineProperty(exports, "
|
|
4682
|
+
Object.defineProperty(exports, "DockerBuildFailed", {
|
|
6283
4683
|
enumerable: true,
|
|
6284
4684
|
get: function() {
|
|
6285
|
-
return
|
|
4685
|
+
return DockerBuildFailed;
|
|
6286
4686
|
}
|
|
6287
4687
|
});
|
|
6288
|
-
Object.defineProperty(exports, "
|
|
4688
|
+
Object.defineProperty(exports, "DockerPushFailed", {
|
|
6289
4689
|
enumerable: true,
|
|
6290
4690
|
get: function() {
|
|
6291
|
-
return
|
|
4691
|
+
return DockerPushFailed;
|
|
6292
4692
|
}
|
|
6293
4693
|
});
|
|
6294
4694
|
Object.defineProperty(exports, "FAKE_ENV", {
|
|
@@ -6303,36 +4703,6 @@ Object.defineProperty(exports, "LOCAL_CA_PATH", {
|
|
|
6303
4703
|
return LOCAL_CA_PATH;
|
|
6304
4704
|
}
|
|
6305
4705
|
});
|
|
6306
|
-
Object.defineProperty(exports, "StageStateMissing", {
|
|
6307
|
-
enumerable: true,
|
|
6308
|
-
get: function() {
|
|
6309
|
-
return StageStateMissing;
|
|
6310
|
-
}
|
|
6311
|
-
});
|
|
6312
|
-
Object.defineProperty(exports, "StateLocked", {
|
|
6313
|
-
enumerable: true,
|
|
6314
|
-
get: function() {
|
|
6315
|
-
return StateLocked;
|
|
6316
|
-
}
|
|
6317
|
-
});
|
|
6318
|
-
Object.defineProperty(exports, "StateStoreBusy", {
|
|
6319
|
-
enumerable: true,
|
|
6320
|
-
get: function() {
|
|
6321
|
-
return StateStoreBusy;
|
|
6322
|
-
}
|
|
6323
|
-
});
|
|
6324
|
-
Object.defineProperty(exports, "StateStoreWithoutLocking", {
|
|
6325
|
-
enumerable: true,
|
|
6326
|
-
get: function() {
|
|
6327
|
-
return StateStoreWithoutLocking;
|
|
6328
|
-
}
|
|
6329
|
-
});
|
|
6330
|
-
Object.defineProperty(exports, "StateVersionConflict", {
|
|
6331
|
-
enumerable: true,
|
|
6332
|
-
get: function() {
|
|
6333
|
-
return StateVersionConflict;
|
|
6334
|
-
}
|
|
6335
|
-
});
|
|
6336
4706
|
Object.defineProperty(exports, "appEnvKeys", {
|
|
6337
4707
|
enumerable: true,
|
|
6338
4708
|
get: function() {
|
|
@@ -6363,12 +4733,6 @@ Object.defineProperty(exports, "cacheBackendFor", {
|
|
|
6363
4733
|
return cacheBackendFor;
|
|
6364
4734
|
}
|
|
6365
4735
|
});
|
|
6366
|
-
Object.defineProperty(exports, "canPrompt", {
|
|
6367
|
-
enumerable: true,
|
|
6368
|
-
get: function() {
|
|
6369
|
-
return canPrompt;
|
|
6370
|
-
}
|
|
6371
|
-
});
|
|
6372
4736
|
Object.defineProperty(exports, "composeFor", {
|
|
6373
4737
|
enumerable: true,
|
|
6374
4738
|
get: function() {
|
|
@@ -6381,12 +4745,6 @@ Object.defineProperty(exports, "constructGlobs", {
|
|
|
6381
4745
|
return constructGlobs;
|
|
6382
4746
|
}
|
|
6383
4747
|
});
|
|
6384
|
-
Object.defineProperty(exports, "contentVersion", {
|
|
6385
|
-
enumerable: true,
|
|
6386
|
-
get: function() {
|
|
6387
|
-
return contentVersion;
|
|
6388
|
-
}
|
|
6389
|
-
});
|
|
6390
4748
|
Object.defineProperty(exports, "createCredentialsPreload", {
|
|
6391
4749
|
enumerable: true,
|
|
6392
4750
|
get: function() {
|
|
@@ -6405,12 +4763,6 @@ Object.defineProperty(exports, "createEntryWrapper", {
|
|
|
6405
4763
|
return createEntryWrapper;
|
|
6406
4764
|
}
|
|
6407
4765
|
});
|
|
6408
|
-
Object.defineProperty(exports, "createStateStore", {
|
|
6409
|
-
enumerable: true,
|
|
6410
|
-
get: function() {
|
|
6411
|
-
return createStateStore;
|
|
6412
|
-
}
|
|
6413
|
-
});
|
|
6414
4766
|
Object.defineProperty(exports, "derivedContainers", {
|
|
6415
4767
|
enumerable: true,
|
|
6416
4768
|
get: function() {
|
|
@@ -6435,18 +4787,6 @@ Object.defineProperty(exports, "dockerCommand", {
|
|
|
6435
4787
|
return dockerCommand;
|
|
6436
4788
|
}
|
|
6437
4789
|
});
|
|
6438
|
-
Object.defineProperty(exports, "encryptSecrets", {
|
|
6439
|
-
enumerable: true,
|
|
6440
|
-
get: function() {
|
|
6441
|
-
return encryptSecrets;
|
|
6442
|
-
}
|
|
6443
|
-
});
|
|
6444
|
-
Object.defineProperty(exports, "encryption_exports", {
|
|
6445
|
-
enumerable: true,
|
|
6446
|
-
get: function() {
|
|
6447
|
-
return encryption_exports;
|
|
6448
|
-
}
|
|
6449
|
-
});
|
|
6450
4790
|
Object.defineProperty(exports, "envFor", {
|
|
6451
4791
|
enumerable: true,
|
|
6452
4792
|
get: function() {
|
|
@@ -6525,12 +4865,6 @@ Object.defineProperty(exports, "isPortAvailable", {
|
|
|
6525
4865
|
return isPortAvailable;
|
|
6526
4866
|
}
|
|
6527
4867
|
});
|
|
6528
|
-
Object.defineProperty(exports, "keyFingerprint", {
|
|
6529
|
-
enumerable: true,
|
|
6530
|
-
get: function() {
|
|
6531
|
-
return keyFingerprint;
|
|
6532
|
-
}
|
|
6533
|
-
});
|
|
6534
4868
|
Object.defineProperty(exports, "loadEnvFiles", {
|
|
6535
4869
|
enumerable: true,
|
|
6536
4870
|
get: function() {
|
|
@@ -6549,30 +4883,12 @@ Object.defineProperty(exports, "localRolePassword", {
|
|
|
6549
4883
|
return localRolePassword;
|
|
6550
4884
|
}
|
|
6551
4885
|
});
|
|
6552
|
-
Object.defineProperty(exports, "loginCommand", {
|
|
6553
|
-
enumerable: true,
|
|
6554
|
-
get: function() {
|
|
6555
|
-
return loginCommand;
|
|
6556
|
-
}
|
|
6557
|
-
});
|
|
6558
|
-
Object.defineProperty(exports, "logoutCommand", {
|
|
6559
|
-
enumerable: true,
|
|
6560
|
-
get: function() {
|
|
6561
|
-
return logoutCommand;
|
|
6562
|
-
}
|
|
6563
|
-
});
|
|
6564
4886
|
Object.defineProperty(exports, "networkEnv", {
|
|
6565
4887
|
enumerable: true,
|
|
6566
4888
|
get: function() {
|
|
6567
4889
|
return networkEnv;
|
|
6568
4890
|
}
|
|
6569
4891
|
});
|
|
6570
|
-
Object.defineProperty(exports, "newLockHolder", {
|
|
6571
|
-
enumerable: true,
|
|
6572
|
-
get: function() {
|
|
6573
|
-
return newLockHolder;
|
|
6574
|
-
}
|
|
6575
|
-
});
|
|
6576
4892
|
Object.defineProperty(exports, "pgClient", {
|
|
6577
4893
|
enumerable: true,
|
|
6578
4894
|
get: function() {
|
|
@@ -6615,12 +4931,6 @@ Object.defineProperty(exports, "primaryPortKey", {
|
|
|
6615
4931
|
return primaryPortKey;
|
|
6616
4932
|
}
|
|
6617
4933
|
});
|
|
6618
|
-
Object.defineProperty(exports, "prompt", {
|
|
6619
|
-
enumerable: true,
|
|
6620
|
-
get: function() {
|
|
6621
|
-
return prompt;
|
|
6622
|
-
}
|
|
6623
|
-
});
|
|
6624
4934
|
Object.defineProperty(exports, "providerOf", {
|
|
6625
4935
|
enumerable: true,
|
|
6626
4936
|
get: function() {
|
|
@@ -6699,42 +5009,18 @@ Object.defineProperty(exports, "siteDockerfile", {
|
|
|
6699
5009
|
return siteDockerfile;
|
|
6700
5010
|
}
|
|
6701
5011
|
});
|
|
6702
|
-
Object.defineProperty(exports, "sniffAllApps", {
|
|
6703
|
-
enumerable: true,
|
|
6704
|
-
get: function() {
|
|
6705
|
-
return sniffAllApps;
|
|
6706
|
-
}
|
|
6707
|
-
});
|
|
6708
|
-
Object.defineProperty(exports, "sniffAppEnvironment", {
|
|
6709
|
-
enumerable: true,
|
|
6710
|
-
get: function() {
|
|
6711
|
-
return sniffAppEnvironment;
|
|
6712
|
-
}
|
|
6713
|
-
});
|
|
6714
5012
|
Object.defineProperty(exports, "storageBackendFor", {
|
|
6715
5013
|
enumerable: true,
|
|
6716
5014
|
get: function() {
|
|
6717
5015
|
return storageBackendFor;
|
|
6718
5016
|
}
|
|
6719
5017
|
});
|
|
6720
|
-
Object.defineProperty(exports, "validateDokployToken", {
|
|
6721
|
-
enumerable: true,
|
|
6722
|
-
get: function() {
|
|
6723
|
-
return validateDokployToken;
|
|
6724
|
-
}
|
|
6725
|
-
});
|
|
6726
5018
|
Object.defineProperty(exports, "validateImageRef", {
|
|
6727
5019
|
enumerable: true,
|
|
6728
5020
|
get: function() {
|
|
6729
5021
|
return validateImageRef;
|
|
6730
5022
|
}
|
|
6731
5023
|
});
|
|
6732
|
-
Object.defineProperty(exports, "whoamiCommand", {
|
|
6733
|
-
enumerable: true,
|
|
6734
|
-
get: function() {
|
|
6735
|
-
return whoamiCommand;
|
|
6736
|
-
}
|
|
6737
|
-
});
|
|
6738
5024
|
Object.defineProperty(exports, "withGeneratedSecrets", {
|
|
6739
5025
|
enumerable: true,
|
|
6740
5026
|
get: function() {
|
|
@@ -6748,4 +5034,4 @@ Object.defineProperty(exports, "workspace_exports", {
|
|
|
6748
5034
|
}
|
|
6749
5035
|
});
|
|
6750
5036
|
|
|
6751
|
-
//# sourceMappingURL=
|
|
5037
|
+
//# sourceMappingURL=state-DF26e_lo.cjs.map
|