@geekmidas/cli 10.0.0-alpha.59 → 10.0.0-alpha.60
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 +18 -1
- package/dist/{HostingerProvider-BZFGYYVA.cjs → HostingerProvider-BTZd2u0f.cjs} +3 -3
- package/dist/{HostingerProvider-BZFGYYVA.cjs.map → HostingerProvider-BTZd2u0f.cjs.map} +1 -1
- package/dist/{HostingerProvider-YQY3rMpV.mjs → HostingerProvider-DgzKa_Tj.mjs} +2 -2
- package/dist/{HostingerProvider-YQY3rMpV.mjs.map → HostingerProvider-DgzKa_Tj.mjs.map} +1 -1
- package/dist/{LegacyStateStore-CB4ch3Dl.mjs → LegacyStateStore-CToa2hga.mjs} +2 -2
- package/dist/{LegacyStateStore-CB4ch3Dl.mjs.map → LegacyStateStore-CToa2hga.mjs.map} +1 -1
- package/dist/{LegacyStateStore-CQJqvmXw.cjs → LegacyStateStore-CidlPT0N.cjs} +7 -7
- package/dist/{LegacyStateStore-CQJqvmXw.cjs.map → LegacyStateStore-CidlPT0N.cjs.map} +1 -1
- package/dist/LocalStateStore-BFLiOocU.mjs +165 -0
- package/dist/LocalStateStore-BFLiOocU.mjs.map +1 -0
- package/dist/LocalStateStore-Dpuqka1N.cjs +176 -0
- package/dist/LocalStateStore-Dpuqka1N.cjs.map +1 -0
- package/dist/{OpenApiTsGenerator-BCWPRZwD.cjs → OpenApiTsGenerator-BTuUumsV.cjs} +3 -3
- package/dist/{OpenApiTsGenerator-BCWPRZwD.cjs.map → OpenApiTsGenerator-BTuUumsV.cjs.map} +1 -1
- package/dist/{OpenApiTsGenerator-BRWD1Wit.mjs → OpenApiTsGenerator-CvVr1Ilz.mjs} +3 -3
- package/dist/{OpenApiTsGenerator-BRWD1Wit.mjs.map → OpenApiTsGenerator-CvVr1Ilz.mjs.map} +1 -1
- package/dist/{S3StateStore-CXTenDTW.mjs → S3StateStore-aaGkjcxO.mjs} +2 -2
- package/dist/{S3StateStore-CXTenDTW.mjs.map → S3StateStore-aaGkjcxO.mjs.map} +1 -1
- package/dist/{S3StateStore-DLS4rEc0.cjs → S3StateStore-cnm813Xn.cjs} +5 -5
- package/dist/{S3StateStore-DLS4rEc0.cjs.map → S3StateStore-cnm813Xn.cjs.map} +1 -1
- package/dist/{SSMStateStore-hnP5deFJ.mjs → SSMStateStore-BllaJKGo.mjs} +2 -2
- package/dist/{SSMStateStore-hnP5deFJ.mjs.map → SSMStateStore-BllaJKGo.mjs.map} +1 -1
- package/dist/{SSMStateStore-EqsTBIOs.cjs → SSMStateStore-DwaGprVX.cjs} +9 -9
- package/dist/{SSMStateStore-EqsTBIOs.cjs.map → SSMStateStore-DwaGprVX.cjs.map} +1 -1
- package/dist/appPorts-C9z9Pzn9.mjs +223 -0
- package/dist/appPorts-C9z9Pzn9.mjs.map +1 -0
- package/dist/appPorts-Cu79hJF6.cjs +264 -0
- package/dist/appPorts-Cu79hJF6.cjs.map +1 -0
- package/dist/{bundler-By6_sXiL.cjs → bundler-CzXZPSBJ.cjs} +4 -4
- package/dist/{bundler-By6_sXiL.cjs.map → bundler-CzXZPSBJ.cjs.map} +1 -1
- package/dist/{bundler-ouDdNvk9.mjs → bundler-DJVR4QlW.mjs} +4 -4
- package/dist/{bundler-ouDdNvk9.mjs.map → bundler-DJVR4QlW.mjs.map} +1 -1
- package/dist/cli-D_qTl20Z.cjs +69 -0
- package/dist/cli-D_qTl20Z.cjs.map +1 -0
- package/dist/cli-DzR1hpum.mjs +69 -0
- package/dist/cli-DzR1hpum.mjs.map +1 -0
- package/dist/{config-EuzFEkcH.mjs → config-C5-EEdmr.mjs} +4 -4
- package/dist/config-C5-EEdmr.mjs.map +1 -0
- package/dist/{config-WMOB9bLL.cjs → config-DXnkCEq0.cjs} +4 -4
- package/dist/config-DXnkCEq0.cjs.map +1 -0
- package/dist/config.cjs +1 -1
- package/dist/config.d.cts +2 -2
- package/dist/config.d.cts.map +1 -1
- package/dist/config.d.mts +2 -2
- package/dist/config.d.mts.map +1 -1
- package/dist/config.mjs +1 -1
- package/dist/deploy/public.cjs +22 -0
- package/dist/deploy/public.d.cts +459 -0
- package/dist/deploy/public.d.cts.map +1 -0
- package/dist/deploy/public.d.mts +459 -0
- package/dist/deploy/public.d.mts.map +1 -0
- package/dist/deploy/public.mjs +5 -0
- package/dist/deploy/sniffer-hooks.cjs +1 -1
- package/dist/deploy/sniffer-hooks.mjs +1 -1
- package/dist/deploy/sniffer-loader.cjs +1 -1
- package/dist/deploy/sniffer-loader.mjs +1 -1
- package/dist/deploy-0fBVeWO-.cjs +3518 -0
- package/dist/deploy-0fBVeWO-.cjs.map +1 -0
- package/dist/deploy-D6Q09OkA.mjs +3429 -0
- package/dist/deploy-D6Q09OkA.mjs.map +1 -0
- package/dist/{discover-DXNviAh-.cjs → discover-Bc-4eA_p.cjs} +55 -3
- package/dist/discover-Bc-4eA_p.cjs.map +1 -0
- package/dist/{discover-Cf2jAcgF.mjs → discover-FrYvzkeC.mjs} +44 -4
- package/dist/discover-FrYvzkeC.mjs.map +1 -0
- package/dist/{encryption-BP8zfxEJ.cjs → encryption-CML4UaPi.cjs} +5 -5
- package/dist/{encryption-BP8zfxEJ.cjs.map → encryption-CML4UaPi.cjs.map} +1 -1
- package/dist/{encryption-BW7Gh51G.mjs → encryption-aDA2AzVx.mjs} +4 -4
- package/dist/{encryption-BW7Gh51G.mjs.map → encryption-aDA2AzVx.mjs.map} +1 -1
- package/dist/fakes-C9RiT6XG.mjs +125 -0
- package/dist/fakes-C9RiT6XG.mjs.map +1 -0
- package/dist/fakes-zW-jNAbQ.cjs +148 -0
- package/dist/fakes-zW-jNAbQ.cjs.map +1 -0
- package/dist/{github-DHpDMBT7.cjs → github-Cq57poPj.cjs} +7 -7
- package/dist/github-Cq57poPj.cjs.map +1 -0
- package/dist/{github-T_6sK3Rz.mjs → github-DmCj2u95.mjs} +7 -7
- package/dist/github-DmCj2u95.mjs.map +1 -0
- package/dist/home-DOzBaWaL.cjs +33 -0
- package/dist/home-DOzBaWaL.cjs.map +1 -0
- package/dist/home-GI7pJl1M.mjs +22 -0
- package/dist/home-GI7pJl1M.mjs.map +1 -0
- package/dist/{index-DV6j4Ydq.d.cts → index-Da1-oa9W.d.mts} +2 -2
- package/dist/{index-B5TJ-mB6.d.mts.map → index-Da1-oa9W.d.mts.map} +1 -1
- package/dist/{index-B5TJ-mB6.d.mts → index-UiWCaGzU.d.cts} +2 -2
- package/dist/{index-DV6j4Ydq.d.cts.map → index-UiWCaGzU.d.cts.map} +1 -1
- package/dist/index.cjs +341 -6412
- package/dist/index.cjs.map +1 -1
- package/dist/index.mjs +289 -6335
- package/dist/index.mjs.map +1 -1
- package/dist/{migrate-CThJfVHR.cjs → migrate-BTqIflwc.cjs} +11 -11
- package/dist/{migrate-CThJfVHR.cjs.map → migrate-BTqIflwc.cjs.map} +1 -1
- package/dist/{migrate-BrFREmgQ.mjs → migrate-B_6KofUo.mjs} +6 -6
- package/dist/{migrate-BrFREmgQ.mjs.map → migrate-B_6KofUo.mjs.map} +1 -1
- package/dist/openapi.cjs +3 -3
- package/dist/openapi.d.cts +1 -1
- package/dist/openapi.d.mts +1 -1
- package/dist/openapi.mjs +3 -3
- package/dist/{ready-mR96orkJ.cjs → ready-CCLIAkwR.cjs} +7 -148
- package/dist/ready-CCLIAkwR.cjs.map +1 -0
- package/dist/{ready-DbDHneLR.mjs → ready-CIHpnUc5.mjs} +7 -124
- package/dist/ready-CIHpnUc5.mjs.map +1 -0
- package/dist/reconcile/public.cjs +1 -1
- package/dist/reconcile/public.d.cts +1 -1
- package/dist/reconcile/public.d.cts.map +1 -1
- package/dist/reconcile/public.d.mts +1 -1
- package/dist/reconcile/public.d.mts.map +1 -1
- package/dist/reconcile/public.mjs +1 -1
- package/dist/{workspace-BRZ14o-o.mjs → sniffer-BM8fpCas.mjs} +3317 -27
- package/dist/sniffer-BM8fpCas.mjs.map +1 -0
- package/dist/{workspace-Dbn07ILl.cjs → sniffer-DIv12ncU.cjs} +3616 -91
- package/dist/sniffer-DIv12ncU.cjs.map +1 -0
- package/dist/store-BS8A7NcA.mjs +415 -0
- package/dist/store-BS8A7NcA.mjs.map +1 -0
- package/dist/store-KvNQwYaB.cjs +510 -0
- package/dist/store-KvNQwYaB.cjs.map +1 -0
- package/dist/{types-CipIIm8f.d.mts → types-Bp1mVxG0.d.mts} +17 -4
- package/dist/types-Bp1mVxG0.d.mts.map +1 -0
- package/dist/{types-D9b-IYdW.d.cts → types-efNLu9xu.d.cts} +17 -4
- package/dist/types-efNLu9xu.d.cts.map +1 -0
- package/dist/vitest.cjs +2 -2
- package/dist/vitest.mjs +2 -2
- package/dist/workspace/index.d.cts +2 -2
- package/dist/workspace/index.d.mts +2 -2
- package/dist/workspace-B64Xk1Mb.cjs.map +1 -1
- package/dist/workspace-Bp1xVaOz.mjs.map +1 -1
- package/package.json +23 -13
- package/dist/StateStore-B4u0wx2t.cjs +0 -412
- package/dist/StateStore-B4u0wx2t.cjs.map +0 -1
- package/dist/StateStore-CYUfGYcx.mjs +0 -359
- package/dist/StateStore-CYUfGYcx.mjs.map +0 -1
- package/dist/backends-CjuCiE8h.mjs +0 -59
- package/dist/backends-CjuCiE8h.mjs.map +0 -1
- package/dist/backends-v3xDuhO0.cjs +0 -82
- package/dist/backends-v3xDuhO0.cjs.map +0 -1
- package/dist/config-EuzFEkcH.mjs.map +0 -1
- package/dist/config-WMOB9bLL.cjs.map +0 -1
- package/dist/discover-Cf2jAcgF.mjs.map +0 -1
- package/dist/discover-DXNviAh-.cjs.map +0 -1
- package/dist/github-DHpDMBT7.cjs.map +0 -1
- package/dist/github-T_6sK3Rz.mjs.map +0 -1
- package/dist/ready-DbDHneLR.mjs.map +0 -1
- package/dist/ready-mR96orkJ.cjs.map +0 -1
- package/dist/store-DQgxT_WR.cjs +0 -211
- package/dist/store-DQgxT_WR.cjs.map +0 -1
- package/dist/store-DeGATCS-.mjs +0 -182
- package/dist/store-DeGATCS-.mjs.map +0 -1
- package/dist/types-CipIIm8f.d.mts.map +0 -1
- package/dist/types-D9b-IYdW.d.cts.map +0 -1
- package/dist/workspace-BRZ14o-o.mjs.map +0 -1
- package/dist/workspace-Dbn07ILl.cjs.map +0 -1
|
@@ -1,20 +1,26 @@
|
|
|
1
1
|
const require_rolldown_runtime = require("./rolldown-runtime-C0BPl7ul.cjs");
|
|
2
|
-
const
|
|
3
|
-
const
|
|
2
|
+
const require_home = require("./home-DOzBaWaL.cjs");
|
|
3
|
+
const require_discover = require("./discover-Bc-4eA_p.cjs");
|
|
4
|
+
const require_config = require("./config-DXnkCEq0.cjs");
|
|
4
5
|
const require_workspace = require("./workspace-B64Xk1Mb.cjs");
|
|
5
6
|
const require_storage = require("./storage-CSGoC3bK.cjs");
|
|
6
|
-
const require_store = require("./store-
|
|
7
|
-
const
|
|
8
|
-
|
|
7
|
+
const require_store = require("./store-KvNQwYaB.cjs");
|
|
8
|
+
const require_fakes = require("./fakes-zW-jNAbQ.cjs");
|
|
9
|
+
let node_path = require("node:path");
|
|
10
|
+
let node_process = require("node:process");
|
|
11
|
+
let node_readline_promises = require("node:readline/promises");
|
|
12
|
+
node_readline_promises = require_rolldown_runtime.__toESM(node_readline_promises, 1);
|
|
9
13
|
let node_fs = require("node:fs");
|
|
10
14
|
let node_fs_promises = require("node:fs/promises");
|
|
11
15
|
let node_os = require("node:os");
|
|
12
|
-
let
|
|
16
|
+
let node_util = require("node:util");
|
|
13
17
|
let _geekmidas_manifest = require("@geekmidas/manifest");
|
|
14
18
|
let node_child_process = require("node:child_process");
|
|
19
|
+
let node_module = require("node:module");
|
|
15
20
|
let node_net = require("node:net");
|
|
16
21
|
let dotenv = require("dotenv");
|
|
17
22
|
let node_crypto = require("node:crypto");
|
|
23
|
+
let node_url = require("node:url");
|
|
18
24
|
let _geekmidas_constructs_encryption = require("@geekmidas/constructs/encryption");
|
|
19
25
|
let _geekmidas_db_pg_roles = require("@geekmidas/db/pg/roles");
|
|
20
26
|
let _geekmidas_events_sns_url = require("@geekmidas/events/sns/url");
|
|
@@ -26,10 +32,316 @@ let _aws_sdk_client_sns = require("@aws-sdk/client-sns");
|
|
|
26
32
|
let _aws_sdk_client_sqs = require("@aws-sdk/client-sqs");
|
|
27
33
|
let pg = require("pg");
|
|
28
34
|
let yaml = require("yaml");
|
|
29
|
-
let node_util = require("node:util");
|
|
30
35
|
let _geekmidas_cache_postgres = require("@geekmidas/cache/postgres");
|
|
36
|
+
//#region src/prompt.ts
|
|
37
|
+
/**
|
|
38
|
+
* Asking the person at the terminal.
|
|
39
|
+
*
|
|
40
|
+
* CLI layer only: a command's action may prompt, nothing it calls may. A
|
|
41
|
+
* library call (`deploy()`) that needs something it was not given raises a
|
|
42
|
+
* named error instead, and the command decides whether to ask.
|
|
43
|
+
*/
|
|
44
|
+
/** A prompt was needed with no terminal to ask at. */
|
|
45
|
+
var PromptNeedsTerminal = class extends Error {
|
|
46
|
+
question;
|
|
47
|
+
instead;
|
|
48
|
+
constructor(question, instead) {
|
|
49
|
+
super(`Interactive input required. ${instead}`);
|
|
50
|
+
this.question = question;
|
|
51
|
+
this.instead = instead;
|
|
52
|
+
this.name = "PromptNeedsTerminal";
|
|
53
|
+
}
|
|
54
|
+
};
|
|
55
|
+
/** Whether there is a person at a terminal to ask. */
|
|
56
|
+
function canPrompt() {
|
|
57
|
+
return process.stdin.isTTY === true;
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Ask `message` and resolve with the line typed — without echoing it when
|
|
61
|
+
* `hidden`. Ctrl+C at a hidden prompt exits, as it would anywhere else in a
|
|
62
|
+
* terminal program: raw mode has taken the signal away from the shell.
|
|
63
|
+
*/
|
|
64
|
+
async function prompt(message, options) {
|
|
65
|
+
if (!canPrompt()) throw new PromptNeedsTerminal(message, options.instead);
|
|
66
|
+
if (options.hidden) {
|
|
67
|
+
process.stdout.write(message);
|
|
68
|
+
return new Promise((resolve, reject) => {
|
|
69
|
+
let value = "";
|
|
70
|
+
const cleanup = () => {
|
|
71
|
+
process.stdin.setRawMode(false);
|
|
72
|
+
process.stdin.pause();
|
|
73
|
+
process.stdin.removeListener("data", onData);
|
|
74
|
+
process.stdin.removeListener("error", onError);
|
|
75
|
+
};
|
|
76
|
+
const onError = (err) => {
|
|
77
|
+
cleanup();
|
|
78
|
+
reject(err);
|
|
79
|
+
};
|
|
80
|
+
const onData = (char) => {
|
|
81
|
+
const c = char.toString();
|
|
82
|
+
if (c === "\n" || c === "\r") {
|
|
83
|
+
cleanup();
|
|
84
|
+
process.stdout.write("\n");
|
|
85
|
+
resolve(value);
|
|
86
|
+
} else if (c === "") {
|
|
87
|
+
cleanup();
|
|
88
|
+
process.stdout.write("\n");
|
|
89
|
+
process.exit(1);
|
|
90
|
+
} else if (c === "" || c === "\b") {
|
|
91
|
+
if (value.length > 0) value = value.slice(0, -1);
|
|
92
|
+
} else value += c;
|
|
93
|
+
};
|
|
94
|
+
process.stdin.setRawMode(true);
|
|
95
|
+
process.stdin.resume();
|
|
96
|
+
process.stdin.on("data", onData);
|
|
97
|
+
process.stdin.on("error", onError);
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
const rl = node_readline_promises.createInterface({
|
|
101
|
+
input: node_process.stdin,
|
|
102
|
+
output: node_process.stdout
|
|
103
|
+
});
|
|
104
|
+
try {
|
|
105
|
+
return await rl.question(message);
|
|
106
|
+
} finally {
|
|
107
|
+
rl.close();
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
//#endregion
|
|
111
|
+
//#region src/auth/credentials.ts
|
|
112
|
+
/**
|
|
113
|
+
* Get the path to the credentials directory
|
|
114
|
+
*/
|
|
115
|
+
function getCredentialsDir(options) {
|
|
116
|
+
if (options?.home) return options.home;
|
|
117
|
+
return options?.root ? (0, node_path.join)(options.root, ".gkm") : require_home.gkmHome();
|
|
118
|
+
}
|
|
119
|
+
/**
|
|
120
|
+
* Get the path to the credentials file
|
|
121
|
+
*/
|
|
122
|
+
function getCredentialsPath(options) {
|
|
123
|
+
return (0, node_path.join)(getCredentialsDir(options), "credentials.json");
|
|
124
|
+
}
|
|
125
|
+
/**
|
|
126
|
+
* Ensure the credentials directory exists
|
|
127
|
+
*/
|
|
128
|
+
function ensureCredentialsDir(options) {
|
|
129
|
+
const dir = getCredentialsDir(options);
|
|
130
|
+
if (!(0, node_fs.existsSync)(dir)) (0, node_fs.mkdirSync)(dir, {
|
|
131
|
+
recursive: true,
|
|
132
|
+
mode: 448
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
/**
|
|
136
|
+
* Read stored credentials from disk
|
|
137
|
+
*/
|
|
138
|
+
async function readCredentials(options) {
|
|
139
|
+
const path = getCredentialsPath(options);
|
|
140
|
+
if (!(0, node_fs.existsSync)(path)) return {};
|
|
141
|
+
try {
|
|
142
|
+
const content = await (0, node_fs_promises.readFile)(path, "utf-8");
|
|
143
|
+
return JSON.parse(content);
|
|
144
|
+
} catch {
|
|
145
|
+
return {};
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
/**
|
|
149
|
+
* Write credentials to disk
|
|
150
|
+
*/
|
|
151
|
+
async function writeCredentials(credentials, options) {
|
|
152
|
+
ensureCredentialsDir(options);
|
|
153
|
+
const path = getCredentialsPath(options);
|
|
154
|
+
await (0, node_fs_promises.writeFile)(path, JSON.stringify(credentials, null, 2), { mode: 384 });
|
|
155
|
+
}
|
|
156
|
+
/**
|
|
157
|
+
* Store Dokploy credentials
|
|
158
|
+
*/
|
|
159
|
+
async function storeDokployCredentials(token, endpoint, options) {
|
|
160
|
+
const credentials = await readCredentials(options);
|
|
161
|
+
credentials.dokploy = {
|
|
162
|
+
token,
|
|
163
|
+
endpoint,
|
|
164
|
+
storedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
165
|
+
};
|
|
166
|
+
await writeCredentials(credentials, options);
|
|
167
|
+
}
|
|
168
|
+
/**
|
|
169
|
+
* Get stored Dokploy credentials
|
|
170
|
+
*/
|
|
171
|
+
async function getDokployCredentials(options) {
|
|
172
|
+
const credentials = await readCredentials(options);
|
|
173
|
+
const token = process.env.DOKPLOY_API_TOKEN ?? credentials.dokploy?.token;
|
|
174
|
+
const endpoint = process.env.DOKPLOY_ENDPOINT ?? credentials.dokploy?.endpoint;
|
|
175
|
+
if (!token || !endpoint) return null;
|
|
176
|
+
return {
|
|
177
|
+
token,
|
|
178
|
+
endpoint: endpoint.replace(/\/$/, ""),
|
|
179
|
+
...credentials.dokploy?.registryId ? { registryId: credentials.dokploy.registryId } : {}
|
|
180
|
+
};
|
|
181
|
+
}
|
|
182
|
+
/**
|
|
183
|
+
* Remove Dokploy credentials
|
|
184
|
+
*/
|
|
185
|
+
async function removeDokployCredentials(options) {
|
|
186
|
+
const credentials = await readCredentials(options);
|
|
187
|
+
if (!credentials.dokploy) return false;
|
|
188
|
+
delete credentials.dokploy;
|
|
189
|
+
await writeCredentials(credentials, options);
|
|
190
|
+
return true;
|
|
191
|
+
}
|
|
192
|
+
/**
|
|
193
|
+
* Get Dokploy API token, checking stored credentials first, then environment
|
|
194
|
+
*/
|
|
195
|
+
async function getDokployToken(options) {
|
|
196
|
+
const envToken = process.env.DOKPLOY_API_TOKEN;
|
|
197
|
+
if (envToken) return envToken;
|
|
198
|
+
const stored = await getDokployCredentials(options);
|
|
199
|
+
if (stored) return stored.token;
|
|
200
|
+
return null;
|
|
201
|
+
}
|
|
202
|
+
/**
|
|
203
|
+
* Get Dokploy registry ID from stored credentials
|
|
204
|
+
*/
|
|
205
|
+
async function getDokployRegistryId(options) {
|
|
206
|
+
return (await getDokployCredentials(options))?.registryId ?? void 0;
|
|
207
|
+
}
|
|
208
|
+
/**
|
|
209
|
+
* Store Hostinger API token
|
|
210
|
+
*
|
|
211
|
+
* @param token - API token from hpanel.hostinger.com/profile/api
|
|
212
|
+
*/
|
|
213
|
+
async function storeHostingerToken(token, options) {
|
|
214
|
+
const credentials = await readCredentials(options);
|
|
215
|
+
credentials.hostinger = {
|
|
216
|
+
token,
|
|
217
|
+
storedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
218
|
+
};
|
|
219
|
+
await writeCredentials(credentials, options);
|
|
220
|
+
}
|
|
221
|
+
/**
|
|
222
|
+
* Get stored Hostinger API token
|
|
223
|
+
*
|
|
224
|
+
* Checks environment variable first (HOSTINGER_API_TOKEN),
|
|
225
|
+
* then falls back to stored credentials.
|
|
226
|
+
*/
|
|
227
|
+
async function getHostingerToken(options) {
|
|
228
|
+
const envToken = process.env.HOSTINGER_API_TOKEN;
|
|
229
|
+
if (envToken) return envToken;
|
|
230
|
+
return (await readCredentials(options)).hostinger?.token ?? null;
|
|
231
|
+
}
|
|
232
|
+
//#endregion
|
|
233
|
+
//#region src/auth/index.ts
|
|
234
|
+
const logger$2 = console;
|
|
235
|
+
/**
|
|
236
|
+
* Validate Dokploy token by making a test API call
|
|
237
|
+
*/
|
|
238
|
+
async function validateDokployToken(endpoint, token) {
|
|
239
|
+
const { DokployApi } = await Promise.resolve().then(() => dokploy_api_exports);
|
|
240
|
+
return new DokployApi({
|
|
241
|
+
baseUrl: endpoint,
|
|
242
|
+
token
|
|
243
|
+
}).validateToken();
|
|
244
|
+
}
|
|
245
|
+
/** What answers a login prompt without a terminal. */
|
|
246
|
+
const LOGIN_INSTEAD = "Please provide --token option.";
|
|
247
|
+
/**
|
|
248
|
+
* Login to a service
|
|
249
|
+
*/
|
|
250
|
+
async function loginCommand(options) {
|
|
251
|
+
const { provider, token: providedToken, endpoint: providedEndpoint } = options;
|
|
252
|
+
if (provider === "dokploy") {
|
|
253
|
+
logger$2.log("\n🔐 Logging in to Dokploy...\n");
|
|
254
|
+
let endpoint = providedEndpoint;
|
|
255
|
+
if (!endpoint) endpoint = await prompt("Dokploy URL (e.g., https://dokploy.example.com): ", { instead: LOGIN_INSTEAD });
|
|
256
|
+
endpoint = endpoint.replace(/\/$/, "");
|
|
257
|
+
try {
|
|
258
|
+
new URL(endpoint);
|
|
259
|
+
} catch {
|
|
260
|
+
logger$2.error("Invalid URL format");
|
|
261
|
+
process.exit(1);
|
|
262
|
+
}
|
|
263
|
+
let token = providedToken;
|
|
264
|
+
if (!token) {
|
|
265
|
+
logger$2.log(`\nGenerate a token at: ${endpoint}/settings/profile\n`);
|
|
266
|
+
token = await prompt("API Token: ", {
|
|
267
|
+
hidden: true,
|
|
268
|
+
instead: LOGIN_INSTEAD
|
|
269
|
+
});
|
|
270
|
+
}
|
|
271
|
+
if (!token) {
|
|
272
|
+
logger$2.error("Token is required");
|
|
273
|
+
process.exit(1);
|
|
274
|
+
}
|
|
275
|
+
logger$2.log("\nValidating credentials...");
|
|
276
|
+
if (!await validateDokployToken(endpoint, token)) {
|
|
277
|
+
logger$2.error("\n✗ Invalid credentials. Please check your token and try again.");
|
|
278
|
+
process.exit(1);
|
|
279
|
+
}
|
|
280
|
+
await storeDokployCredentials(token, endpoint);
|
|
281
|
+
logger$2.log("\n✓ Successfully logged in to Dokploy!");
|
|
282
|
+
logger$2.log(` Endpoint: ${endpoint}`);
|
|
283
|
+
logger$2.log(` Credentials stored in: ${getCredentialsPath()}`);
|
|
284
|
+
logger$2.log("\nYou can now use deploy commands without setting DOKPLOY_API_TOKEN.");
|
|
285
|
+
}
|
|
286
|
+
if (provider === "hostinger") {
|
|
287
|
+
logger$2.log("\n🔐 Logging in to Hostinger...\n");
|
|
288
|
+
let token = providedToken;
|
|
289
|
+
if (!token) {
|
|
290
|
+
logger$2.log("\nGenerate a token at: https://hpanel.hostinger.com/profile/api\n");
|
|
291
|
+
token = await prompt("API Token: ", {
|
|
292
|
+
hidden: true,
|
|
293
|
+
instead: LOGIN_INSTEAD
|
|
294
|
+
});
|
|
295
|
+
}
|
|
296
|
+
if (!token) {
|
|
297
|
+
logger$2.error("Token is required");
|
|
298
|
+
process.exit(1);
|
|
299
|
+
}
|
|
300
|
+
await storeHostingerToken(token);
|
|
301
|
+
logger$2.log("\n✓ Hostinger token stored.");
|
|
302
|
+
logger$2.log(` Credentials stored in: ${getCredentialsPath()}`);
|
|
303
|
+
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.");
|
|
304
|
+
logger$2.log("\nYou can now use it as a DNS provider without setting HOSTINGER_API_TOKEN.");
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
/**
|
|
308
|
+
* Logout from a service
|
|
309
|
+
*/
|
|
310
|
+
async function logoutCommand(options) {
|
|
311
|
+
const { provider = "dokploy" } = options;
|
|
312
|
+
if (provider === "all") {
|
|
313
|
+
if (await removeDokployCredentials()) logger$2.log("\n✓ Logged out from all services");
|
|
314
|
+
else logger$2.log("\nNo stored credentials found");
|
|
315
|
+
return;
|
|
316
|
+
}
|
|
317
|
+
if (provider === "dokploy") {
|
|
318
|
+
if (await removeDokployCredentials()) logger$2.log("\n✓ Logged out from Dokploy");
|
|
319
|
+
else logger$2.log("\nNo Dokploy credentials found");
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
/**
|
|
323
|
+
* Show current login status
|
|
324
|
+
*/
|
|
325
|
+
async function whoamiCommand() {
|
|
326
|
+
logger$2.log("\n📋 Current credentials:\n");
|
|
327
|
+
const dokploy = await getDokployCredentials();
|
|
328
|
+
if (dokploy) {
|
|
329
|
+
logger$2.log(" Dokploy:");
|
|
330
|
+
logger$2.log(` Endpoint: ${dokploy.endpoint}`);
|
|
331
|
+
logger$2.log(` Token: ${maskToken(dokploy.token)}`);
|
|
332
|
+
} else logger$2.log(" Dokploy: Not logged in");
|
|
333
|
+
logger$2.log(`\n Credentials file: ${getCredentialsPath()}`);
|
|
334
|
+
}
|
|
335
|
+
/**
|
|
336
|
+
* Mask a token for display
|
|
337
|
+
*/
|
|
338
|
+
function maskToken(token) {
|
|
339
|
+
if (token.length <= 8) return "****";
|
|
340
|
+
return `${token.slice(0, 4)}...${token.slice(-4)}`;
|
|
341
|
+
}
|
|
342
|
+
//#endregion
|
|
31
343
|
//#region src/credentials/index.ts
|
|
32
|
-
const logger = console;
|
|
344
|
+
const logger$1 = console;
|
|
33
345
|
/**
|
|
34
346
|
* Load environment files
|
|
35
347
|
* @internal Exported for testing
|
|
@@ -80,7 +392,7 @@ async function findAvailablePort(preferredPort, maxAttempts = 10) {
|
|
|
80
392
|
for (let i = 0; i < maxAttempts; i++) {
|
|
81
393
|
const port = preferredPort + i;
|
|
82
394
|
if (await isPortAvailable(port)) return port;
|
|
83
|
-
logger.log(`⚠️ Port ${port} is in use, trying ${port + 1}...`);
|
|
395
|
+
logger$1.log(`⚠️ Port ${port} is in use, trying ${port + 1}...`);
|
|
84
396
|
}
|
|
85
397
|
throw new Error(`Could not find an available port after trying ${maxAttempts} ports starting from ${preferredPort}`);
|
|
86
398
|
}
|
|
@@ -115,7 +427,7 @@ async function loadSecretsForApp(store, stage, appName) {
|
|
|
115
427
|
let secrets = {};
|
|
116
428
|
const stageSecrets = await store.read(stage);
|
|
117
429
|
if (stageSecrets) {
|
|
118
|
-
logger.log(`🔐 Loading secrets from stage: ${stage}`);
|
|
430
|
+
logger$1.log(`🔐 Loading secrets from stage: ${stage}`);
|
|
119
431
|
secrets = require_storage.toEmbeddableSecrets(stageSecrets);
|
|
120
432
|
}
|
|
121
433
|
if (Object.keys(secrets).length === 0) return {};
|
|
@@ -216,17 +528,17 @@ async function prepareEntryCredentials(options) {
|
|
|
216
528
|
secretsRoot = workspace?.root ?? findSecretsRoot(cwd);
|
|
217
529
|
}
|
|
218
530
|
if (appName && workspace && options.explicitPort === void 0) {
|
|
219
|
-
const { assignAppPorts, describeHolder, holderOf, withAppPorts } = await Promise.resolve().then(() => require("./
|
|
531
|
+
const { assignAppPorts, describeHolder, holderOf, withAppPorts } = await Promise.resolve().then(() => require("./appPorts-Cu79hJF6.cjs")).then((n) => n.appPorts_exports);
|
|
220
532
|
const assigned = await assignAppPorts(workspace, [appName], {
|
|
221
533
|
free: isPortAvailable,
|
|
222
534
|
holder: holderOf
|
|
223
535
|
});
|
|
224
|
-
for (const { app, from, to, holder } of assigned.moved) logger.log(`↪️ ${app}: ${from} is held by ${describeHolder(holder)}, which is not this workspace's — using ${to}`);
|
|
536
|
+
for (const { app, from, to, holder } of assigned.moved) logger$1.log(`↪️ ${app}: ${from} is held by ${describeHolder(holder)}, which is not this workspace's — using ${to}`);
|
|
225
537
|
workspace = withAppPorts(workspace, assigned.ports);
|
|
226
538
|
workspaceAppPort = assigned.ports[appName] ?? workspaceAppPort;
|
|
227
539
|
}
|
|
228
540
|
if (appName) {
|
|
229
|
-
const { APP_TAG_ENV, appTag } = await Promise.resolve().then(() => require("./
|
|
541
|
+
const { APP_TAG_ENV, appTag } = await Promise.resolve().then(() => require("./appPorts-Cu79hJF6.cjs")).then((n) => n.appPorts_exports);
|
|
230
542
|
process.env[APP_TAG_ENV] = appTag(secretsRoot, appName);
|
|
231
543
|
}
|
|
232
544
|
const resolvedPort = options.explicitPort ?? workspaceAppPort ?? 3e3;
|
|
@@ -245,7 +557,7 @@ async function prepareEntryCredentials(options) {
|
|
|
245
557
|
});
|
|
246
558
|
Object.assign(credentials, reconciled.env);
|
|
247
559
|
declaredKeys.push(...Object.keys(reconciled.env));
|
|
248
|
-
if (Object.keys(reconciled.env).length > 0) logger.log(`🔌 Resolved ${Object.keys(reconciled.env).length} declared URL(s)`);
|
|
560
|
+
if (Object.keys(reconciled.env).length > 0) logger$1.log(`🔌 Resolved ${Object.keys(reconciled.env).length} declared URL(s)`);
|
|
249
561
|
}
|
|
250
562
|
const secretsDir = (0, node_path.join)(secretsRoot, ".gkm");
|
|
251
563
|
await (0, node_fs_promises.mkdir)(secretsDir, { recursive: true });
|
|
@@ -264,6 +576,56 @@ async function prepareEntryCredentials(options) {
|
|
|
264
576
|
};
|
|
265
577
|
}
|
|
266
578
|
//#endregion
|
|
579
|
+
//#region src/workspace/client-generator.ts
|
|
580
|
+
/**
|
|
581
|
+
* Normalize routes to an array of patterns.
|
|
582
|
+
* Handles a glob or a list of them.
|
|
583
|
+
* @internal Exported for use in dev command
|
|
584
|
+
*/
|
|
585
|
+
function normalizeRoutes(routes) {
|
|
586
|
+
if (!routes) return [];
|
|
587
|
+
return Array.isArray(routes) ? routes : [routes];
|
|
588
|
+
}
|
|
589
|
+
//#endregion
|
|
590
|
+
//#region src/owningTsconfigJsx.ts
|
|
591
|
+
const REGISTER = "owning-tsconfig-jsx-register.mjs";
|
|
592
|
+
/** The CLI's `bin/` was not found above the module looking for it. */
|
|
593
|
+
var OwningTsconfigJsxHookMissing = class extends Error {
|
|
594
|
+
from;
|
|
595
|
+
constructor(from) {
|
|
596
|
+
super(`Could not find bin/${REGISTER} in any directory above '${from}'. It ships with @geekmidas/cli beside bin/gkm.mjs; reinstall the package if it is missing.`);
|
|
597
|
+
this.from = from;
|
|
598
|
+
this.name = "OwningTsconfigJsxHookMissing";
|
|
599
|
+
}
|
|
600
|
+
};
|
|
601
|
+
/**
|
|
602
|
+
* The CLI's `bin/owning-tsconfig-jsx-register.mjs`, as a file URL. Searched
|
|
603
|
+
* upward because this module runs from `src/` in the repo and from a chunk in
|
|
604
|
+
* `dist/` once built, and `bin/` sits beside both.
|
|
605
|
+
*/
|
|
606
|
+
function registerUrl() {
|
|
607
|
+
const from = (0, node_path.dirname)((0, node_url.fileURLToPath)(require("url").pathToFileURL(__filename).href));
|
|
608
|
+
for (let dir = from;; dir = (0, node_path.dirname)(dir)) {
|
|
609
|
+
const candidate = (0, node_path.join)(dir, "bin", REGISTER);
|
|
610
|
+
if ((0, node_fs.existsSync)(candidate)) return (0, node_url.pathToFileURL)(candidate).href;
|
|
611
|
+
if ((0, node_path.dirname)(dir) === dir) throw new OwningTsconfigJsxHookMissing(from);
|
|
612
|
+
}
|
|
613
|
+
}
|
|
614
|
+
/**
|
|
615
|
+
* `NODE_OPTIONS` for a process that runs app code through tsx, with the hook
|
|
616
|
+
* that compiles each `.tsx` with its own tsconfig's JSX settings (see
|
|
617
|
+
* `bin/owning-tsconfig-jsx.mjs`) put in front of every other `--import`, so it
|
|
618
|
+
* is registered before tsx and reads the source on tsx's behalf.
|
|
619
|
+
*
|
|
620
|
+
* `bin/gkm.mjs` already starts the CLI this way, and its children inherit it;
|
|
621
|
+
* this is for those that set `NODE_OPTIONS` themselves, or whose CLI was
|
|
622
|
+
* started with tsx already imported.
|
|
623
|
+
*/
|
|
624
|
+
function withOwningTsconfigJsx(nodeOptions = "") {
|
|
625
|
+
if (nodeOptions.includes(REGISTER)) return nodeOptions;
|
|
626
|
+
return [`--import=${registerUrl()}`, nodeOptions].filter(Boolean).join(" ");
|
|
627
|
+
}
|
|
628
|
+
//#endregion
|
|
267
629
|
//#region src/reconcile/containers.ts
|
|
268
630
|
/**
|
|
269
631
|
* What gkm knows about each container it can derive.
|
|
@@ -440,6 +802,61 @@ function volumeOf(container) {
|
|
|
440
802
|
}[container];
|
|
441
803
|
}
|
|
442
804
|
//#endregion
|
|
805
|
+
//#region src/workspace/backends.ts
|
|
806
|
+
/**
|
|
807
|
+
* Which backend each declared kind resolves to — decided by where the project
|
|
808
|
+
* deploys, and by nothing else.
|
|
809
|
+
*
|
|
810
|
+
* These used to be read out of a `services:` block, where one key answered two
|
|
811
|
+
* questions: *does this exist* and *what backs it*. The first is the manifest's
|
|
812
|
+
* — a declared `Cache` is why there is a cache — and the second is the deploy
|
|
813
|
+
* target's, because the right cache for a Lambda is not the right cache for a
|
|
814
|
+
* box already running Postgres. With both answered elsewhere there was nothing
|
|
815
|
+
* left for the block to say, so it is gone and these read the target.
|
|
816
|
+
*
|
|
817
|
+
* Mail has no answer here at all: every provider speaks SMTP, so the provider
|
|
818
|
+
* is whichever one the stage's mail URL points at.
|
|
819
|
+
*/
|
|
820
|
+
/**
|
|
821
|
+
* Which family of defaults a workspace's deploy target belongs to.
|
|
822
|
+
*
|
|
823
|
+
* One question, asked once: **does this target run containers the project
|
|
824
|
+
* controls?** Dokploy and a bare server do, so a bucket or a cache can live
|
|
825
|
+
* beside the app; AWS, Vercel and Cloudflare do not, so the default has to be
|
|
826
|
+
* something managed. That is the whole of the distinction, and it is why
|
|
827
|
+
* `MainProvider` is the right axis rather than the deploy target's own name —
|
|
828
|
+
* three targets share one answer.
|
|
829
|
+
*
|
|
830
|
+
* An AWS deploy names `deploy.default: 'sst'`, because it goes through SST.
|
|
831
|
+
*/
|
|
832
|
+
function providerOf(workspace) {
|
|
833
|
+
const target = workspace.deploy?.default;
|
|
834
|
+
return target === "dokploy" || target === "server" ? "server" : "aws";
|
|
835
|
+
}
|
|
836
|
+
/**
|
|
837
|
+
* The cache backend a target implies.
|
|
838
|
+
*
|
|
839
|
+
* The same answer locally and deployed: a local `gkm dev` for an AWS app still
|
|
840
|
+
* uses the Upstash protocol, because a backend that differed between the two
|
|
841
|
+
* would be worse than a slower one.
|
|
842
|
+
*/
|
|
843
|
+
function cacheBackendFor(on) {
|
|
844
|
+
return require_fakes.DEFAULT_CACHE[on];
|
|
845
|
+
}
|
|
846
|
+
/** Where a declared bucket lives on a target. */
|
|
847
|
+
function storageBackendFor(on) {
|
|
848
|
+
return require_fakes.DEFAULT_STORAGE[on];
|
|
849
|
+
}
|
|
850
|
+
/**
|
|
851
|
+
* Which broker carries declared queues and topics on a target.
|
|
852
|
+
*
|
|
853
|
+
* Only consulted when something declares one; a project with no `Topic` or
|
|
854
|
+
* `Queue` has no events at all, whatever the target.
|
|
855
|
+
*/
|
|
856
|
+
function eventsBackendFor(on) {
|
|
857
|
+
return require_fakes.DEFAULT_EVENTS[on];
|
|
858
|
+
}
|
|
859
|
+
//#endregion
|
|
443
860
|
//#region src/reconcile/caddyfile.ts
|
|
444
861
|
/**
|
|
445
862
|
* The kinds the edge serves.
|
|
@@ -841,7 +1258,7 @@ function resourceName(id, kind, stage, localStage) {
|
|
|
841
1258
|
* broker the project selected, which is why the backend is an argument here
|
|
842
1259
|
* rather than a second pass that adds containers after the fact.
|
|
843
1260
|
*/
|
|
844
|
-
function containerFor(kind, events = DEFAULT_EVENTS, cache =
|
|
1261
|
+
function containerFor(kind, events = DEFAULT_EVENTS, cache = require_fakes.DEFAULT_CACHE.aws, derived = false, edge = true) {
|
|
845
1262
|
if (EVENT_KINDS[kind]) return EVENT_CONTAINERS[events] ?? CONTAINERS.database;
|
|
846
1263
|
if (kind === "file-server") return edge ? "caddy" : CONTAINERS.objects;
|
|
847
1264
|
if (kind === "cache") {
|
|
@@ -862,7 +1279,7 @@ function planFor(manifest, stage, order, options = {}) {
|
|
|
862
1279
|
const resources = [];
|
|
863
1280
|
const fakes = {};
|
|
864
1281
|
const events = options.events ?? "pgboss";
|
|
865
|
-
const cache = options.cache ??
|
|
1282
|
+
const cache = options.cache ?? require_fakes.DEFAULT_CACHE.aws;
|
|
866
1283
|
for (const id of order) {
|
|
867
1284
|
const declaration = manifest[id];
|
|
868
1285
|
if (!declaration) continue;
|
|
@@ -1980,7 +2397,7 @@ var UnassignedPort = class extends Error {
|
|
|
1980
2397
|
* Every call is scoped to the generated compose file. Nothing here can touch a
|
|
1981
2398
|
* container this project did not generate.
|
|
1982
2399
|
*/
|
|
1983
|
-
const run = (0, node_util.promisify)(node_child_process.execFile);
|
|
2400
|
+
const run$1 = (0, node_util.promisify)(node_child_process.execFile);
|
|
1984
2401
|
/**
|
|
1985
2402
|
* The generated file, then the project's own if it has one.
|
|
1986
2403
|
*
|
|
@@ -2023,7 +2440,7 @@ function shown(args, services) {
|
|
|
2023
2440
|
const dockerCli = {
|
|
2024
2441
|
async publishedPort(composePath, service, inside) {
|
|
2025
2442
|
try {
|
|
2026
|
-
const { stdout } = await run("docker", [
|
|
2443
|
+
const { stdout } = await run$1("docker", [
|
|
2027
2444
|
"compose",
|
|
2028
2445
|
...composeFiles(composePath),
|
|
2029
2446
|
"port",
|
|
@@ -2048,11 +2465,11 @@ const dockerCli = {
|
|
|
2048
2465
|
...services
|
|
2049
2466
|
];
|
|
2050
2467
|
if (options.show) return shown(args, services);
|
|
2051
|
-
await run("docker", args);
|
|
2468
|
+
await run$1("docker", args);
|
|
2052
2469
|
},
|
|
2053
2470
|
async copyOut(composePath, service, from, to) {
|
|
2054
2471
|
await (0, node_fs_promises.mkdir)((0, node_path.dirname)(to), { recursive: true });
|
|
2055
|
-
await run("docker", [
|
|
2472
|
+
await run$1("docker", [
|
|
2056
2473
|
"compose",
|
|
2057
2474
|
...composeFiles(composePath),
|
|
2058
2475
|
"cp",
|
|
@@ -2061,7 +2478,7 @@ const dockerCli = {
|
|
|
2061
2478
|
]);
|
|
2062
2479
|
},
|
|
2063
2480
|
async reload(composePath, service) {
|
|
2064
|
-
await run("docker", [
|
|
2481
|
+
await run$1("docker", [
|
|
2065
2482
|
"compose",
|
|
2066
2483
|
...composeFiles(composePath),
|
|
2067
2484
|
"exec",
|
|
@@ -2076,7 +2493,7 @@ const dockerCli = {
|
|
|
2076
2493
|
async healthy(composePath, services) {
|
|
2077
2494
|
if (services.length === 0) return true;
|
|
2078
2495
|
try {
|
|
2079
|
-
const { stdout } = await run("docker", [
|
|
2496
|
+
const { stdout } = await run$1("docker", [
|
|
2080
2497
|
"compose",
|
|
2081
2498
|
...composeFiles(composePath),
|
|
2082
2499
|
"ps",
|
|
@@ -2908,10 +3325,10 @@ function constructGlobs(workspace) {
|
|
|
2908
3325
|
}
|
|
2909
3326
|
/** The backends a workspace's target implies, as the plan takes them. */
|
|
2910
3327
|
function backendsOf(workspace) {
|
|
2911
|
-
const on =
|
|
3328
|
+
const on = providerOf(workspace);
|
|
2912
3329
|
return {
|
|
2913
|
-
events:
|
|
2914
|
-
cache:
|
|
3330
|
+
events: eventsBackendFor(on),
|
|
3331
|
+
cache: cacheBackendFor(on)
|
|
2915
3332
|
};
|
|
2916
3333
|
}
|
|
2917
3334
|
/**
|
|
@@ -2961,7 +3378,7 @@ async function reconcileWorkspace(workspace, options) {
|
|
|
2961
3378
|
runnables
|
|
2962
3379
|
});
|
|
2963
3380
|
const manifest = options.manifest ?? discovered;
|
|
2964
|
-
const fakes = fakesApply(options.stage, options.fake) ? await
|
|
3381
|
+
const fakes = fakesApply(options.stage, options.fake) ? await require_fakes.readFakes(workspace.root, manifest) : {};
|
|
2965
3382
|
const result = await reconcile({
|
|
2966
3383
|
root: workspace.root,
|
|
2967
3384
|
project: workspace.name,
|
|
@@ -3028,90 +3445,3102 @@ function localAddress(port) {
|
|
|
3028
3445
|
return `http://localhost:${port}`;
|
|
3029
3446
|
}
|
|
3030
3447
|
//#endregion
|
|
3031
|
-
|
|
3032
|
-
|
|
3033
|
-
|
|
3034
|
-
|
|
3448
|
+
//#region src/run.ts
|
|
3449
|
+
/**
|
|
3450
|
+
* Start a program with an argument array, never a shell string.
|
|
3451
|
+
*
|
|
3452
|
+
* `gkm build`, `gkm docker` and `gkm deploy` pass values that come from the
|
|
3453
|
+
* workspace — image refs, tags, build args, package names — to `docker` and
|
|
3454
|
+
* `turbo`. Joined into a string and handed to a shell, a package named
|
|
3455
|
+
* `x;curl …|sh` or a tag holding `$(…)` became part of the command line, which
|
|
3456
|
+
* matters most where the CLI builds a repository it didn't write: a CI job for a
|
|
3457
|
+
* fork, a shared build runner. Here every value reaches the program as one
|
|
3458
|
+
* `argv` element, whatever it contains.
|
|
3459
|
+
*/
|
|
3460
|
+
/** Long enough for a cold image build; short enough that a hung one ends. */
|
|
3461
|
+
const DEFAULT_TIMEOUT_MS = 18e5;
|
|
3462
|
+
/** How long a timed-out child has to exit on SIGTERM before it is killed. */
|
|
3463
|
+
const KILL_GRACE_MS = 5e3;
|
|
3464
|
+
/** The command line as a person would read it — for messages, never to run. */
|
|
3465
|
+
function display(command, args) {
|
|
3466
|
+
return [command, ...args].join(" ");
|
|
3467
|
+
}
|
|
3468
|
+
/** The program ran and exited unsuccessfully; its own output said why. */
|
|
3469
|
+
var CommandFailed = class extends Error {
|
|
3470
|
+
command;
|
|
3471
|
+
args;
|
|
3472
|
+
exitCode;
|
|
3473
|
+
signal;
|
|
3474
|
+
constructor(command, args, exitCode, signal) {
|
|
3475
|
+
super(`\`${display(command, args)}\` ${exitCode === null ? `was stopped by ${signal ?? "a signal"}` : `exited with code ${exitCode}`}. Its output above says why; run it by hand to see it again.`);
|
|
3476
|
+
this.command = command;
|
|
3477
|
+
this.args = args;
|
|
3478
|
+
this.exitCode = exitCode;
|
|
3479
|
+
this.signal = signal;
|
|
3480
|
+
this.name = "CommandFailed";
|
|
3035
3481
|
}
|
|
3036
|
-
}
|
|
3037
|
-
|
|
3038
|
-
|
|
3039
|
-
|
|
3040
|
-
|
|
3482
|
+
};
|
|
3483
|
+
/** The program ran past its timeout and was killed. */
|
|
3484
|
+
var CommandTimedOut = class extends Error {
|
|
3485
|
+
command;
|
|
3486
|
+
args;
|
|
3487
|
+
timeoutMs;
|
|
3488
|
+
constructor(command, args, timeoutMs) {
|
|
3489
|
+
super(`\`${display(command, args)}\` was still running after ${Math.round(timeoutMs / 1e3)}s and was stopped. Check what it was waiting on — a registry, the Docker daemon, a prompt — and run it again.`);
|
|
3490
|
+
this.command = command;
|
|
3491
|
+
this.args = args;
|
|
3492
|
+
this.timeoutMs = timeoutMs;
|
|
3493
|
+
this.name = "CommandTimedOut";
|
|
3041
3494
|
}
|
|
3495
|
+
};
|
|
3496
|
+
/**
|
|
3497
|
+
* Run `command` with `args`, resolving when it exits 0.
|
|
3498
|
+
*
|
|
3499
|
+
* Rejects with {@link CommandFailed} on any other exit, {@link CommandTimedOut}
|
|
3500
|
+
* once `timeoutMs` passes (after the child is gone), and with the spawn error
|
|
3501
|
+
* itself when the program cannot be started at all — `ENOENT` already names it.
|
|
3502
|
+
*/
|
|
3503
|
+
async function run(command, args, options = {}) {
|
|
3504
|
+
await spawnChild(command, args, options, false);
|
|
3505
|
+
}
|
|
3506
|
+
/**
|
|
3507
|
+
* {@link run}, resolving with what the program wrote to stdout — for a value
|
|
3508
|
+
* the CLI reads back, such as the digest `docker inspect` reports. stderr still
|
|
3509
|
+
* reaches the terminal, so a failure explains itself the same way.
|
|
3510
|
+
*/
|
|
3511
|
+
function runOutput(command, args, options = {}) {
|
|
3512
|
+
return spawnChild(command, args, options, true);
|
|
3513
|
+
}
|
|
3514
|
+
function spawnChild(command, args, options, capture) {
|
|
3515
|
+
const { timeoutMs = DEFAULT_TIMEOUT_MS, ...spawnOptions } = options;
|
|
3516
|
+
return new Promise((resolve, reject) => {
|
|
3517
|
+
const child = (0, node_child_process.spawn)(command, [...args], {
|
|
3518
|
+
stdio: "inherit",
|
|
3519
|
+
...spawnOptions,
|
|
3520
|
+
...capture ? { stdio: [
|
|
3521
|
+
"ignore",
|
|
3522
|
+
"pipe",
|
|
3523
|
+
"inherit"
|
|
3524
|
+
] } : {},
|
|
3525
|
+
shell: false
|
|
3526
|
+
});
|
|
3527
|
+
let stdout = "";
|
|
3528
|
+
child.stdout?.on("data", (chunk) => {
|
|
3529
|
+
stdout += chunk.toString();
|
|
3530
|
+
});
|
|
3531
|
+
let timedOut = false;
|
|
3532
|
+
let killer;
|
|
3533
|
+
const timer = setTimeout(() => {
|
|
3534
|
+
timedOut = true;
|
|
3535
|
+
child.kill("SIGTERM");
|
|
3536
|
+
killer = setTimeout(() => child.kill("SIGKILL"), KILL_GRACE_MS);
|
|
3537
|
+
killer.unref?.();
|
|
3538
|
+
}, timeoutMs);
|
|
3539
|
+
timer.unref?.();
|
|
3540
|
+
const settle = () => {
|
|
3541
|
+
clearTimeout(timer);
|
|
3542
|
+
if (killer) clearTimeout(killer);
|
|
3543
|
+
};
|
|
3544
|
+
child.on("error", (error) => {
|
|
3545
|
+
settle();
|
|
3546
|
+
reject(spawnOptions.signal?.aborted ? spawnOptions.signal.reason : error);
|
|
3547
|
+
});
|
|
3548
|
+
child.on("close", (code, signal) => {
|
|
3549
|
+
settle();
|
|
3550
|
+
if (spawnOptions.signal?.aborted) reject(spawnOptions.signal.reason);
|
|
3551
|
+
else if (timedOut) reject(new CommandTimedOut(command, args, timeoutMs));
|
|
3552
|
+
else if (code === 0) resolve(stdout);
|
|
3553
|
+
else reject(new CommandFailed(command, args, code, signal));
|
|
3554
|
+
});
|
|
3555
|
+
});
|
|
3556
|
+
}
|
|
3557
|
+
//#endregion
|
|
3558
|
+
//#region src/secrets/encryption.ts
|
|
3559
|
+
var encryption_exports = /* @__PURE__ */ require_rolldown_runtime.__exportAll({
|
|
3560
|
+
encryptSecrets: () => encryptSecrets,
|
|
3561
|
+
generateDefineOptions: () => generateDefineOptions,
|
|
3562
|
+
keyFingerprint: () => keyFingerprint
|
|
3042
3563
|
});
|
|
3043
|
-
|
|
3044
|
-
|
|
3045
|
-
|
|
3046
|
-
|
|
3047
|
-
|
|
3564
|
+
/** AES-256-GCM configuration */
|
|
3565
|
+
const ALGORITHM = "aes-256-gcm";
|
|
3566
|
+
const KEY_LENGTH = 32;
|
|
3567
|
+
const IV_LENGTH = 12;
|
|
3568
|
+
/**
|
|
3569
|
+
* Encrypt secrets for embedding in a bundle.
|
|
3570
|
+
* Uses AES-256-GCM with a randomly generated ephemeral key.
|
|
3571
|
+
*
|
|
3572
|
+
* @param secrets - Key-value pairs to encrypt
|
|
3573
|
+
* @returns Encrypted payload with ephemeral master key
|
|
3574
|
+
*/
|
|
3575
|
+
function encryptSecrets(secrets) {
|
|
3576
|
+
const masterKey = (0, node_crypto.randomBytes)(KEY_LENGTH);
|
|
3577
|
+
const iv = (0, node_crypto.randomBytes)(IV_LENGTH);
|
|
3578
|
+
const plaintext = JSON.stringify(secrets);
|
|
3579
|
+
const cipher = (0, node_crypto.createCipheriv)(ALGORITHM, masterKey, iv);
|
|
3580
|
+
const ciphertext = Buffer.concat([cipher.update(plaintext, "utf-8"), cipher.final()]);
|
|
3581
|
+
const authTag = cipher.getAuthTag();
|
|
3582
|
+
return {
|
|
3583
|
+
encrypted: Buffer.concat([ciphertext, authTag]).toString("base64"),
|
|
3584
|
+
iv: iv.toString("hex"),
|
|
3585
|
+
masterKey: masterKey.toString("hex")
|
|
3586
|
+
};
|
|
3587
|
+
}
|
|
3588
|
+
/**
|
|
3589
|
+
* Generate the define options for tsdown/esbuild.
|
|
3590
|
+
* These will be injected at build time.
|
|
3591
|
+
*/
|
|
3592
|
+
function generateDefineOptions(payload) {
|
|
3593
|
+
return {
|
|
3594
|
+
__GKM_ENCRYPTED_CREDENTIALS__: JSON.stringify(payload.encrypted),
|
|
3595
|
+
__GKM_CREDENTIALS_IV__: JSON.stringify(payload.iv)
|
|
3596
|
+
};
|
|
3597
|
+
}
|
|
3598
|
+
/**
|
|
3599
|
+
* A short, one-way name for a master key: the first 8 hex characters of its
|
|
3600
|
+
* SHA-256.
|
|
3601
|
+
*
|
|
3602
|
+
* Printed where the key itself used to be, so a person can tell which key a
|
|
3603
|
+
* build used — and match it against the one they hold — without the output
|
|
3604
|
+
* carrying a value that decrypts every secret of the stage.
|
|
3605
|
+
*/
|
|
3606
|
+
function keyFingerprint(masterKey) {
|
|
3607
|
+
return (0, node_crypto.createHash)("sha256").update(masterKey).digest("hex").slice(0, 8);
|
|
3608
|
+
}
|
|
3609
|
+
//#endregion
|
|
3610
|
+
//#region src/deploy/dokploy-api.ts
|
|
3611
|
+
var dokploy_api_exports = /* @__PURE__ */ require_rolldown_runtime.__exportAll({
|
|
3612
|
+
DEFAULT_DOKPLOY_TIMEOUT_MS: () => DEFAULT_DOKPLOY_TIMEOUT_MS,
|
|
3613
|
+
DokployApi: () => DokployApi,
|
|
3614
|
+
DokployApiError: () => DokployApiError,
|
|
3615
|
+
DokployRequestTimedOut: () => DokployRequestTimedOut
|
|
3048
3616
|
});
|
|
3049
|
-
|
|
3050
|
-
|
|
3051
|
-
|
|
3052
|
-
|
|
3617
|
+
/** Per request, so a stalled server fails the deploy, not the CI job. */
|
|
3618
|
+
const DEFAULT_DOKPLOY_TIMEOUT_MS = 3e4;
|
|
3619
|
+
/**
|
|
3620
|
+
* Dokploy accepted the connection but did not answer in time.
|
|
3621
|
+
*
|
|
3622
|
+
* Not retried: a request that arrived and stalled may still be acted on, and
|
|
3623
|
+
* sending it again could repeat a deploy or a create the server is already
|
|
3624
|
+
* doing.
|
|
3625
|
+
*/
|
|
3626
|
+
var DokployRequestTimedOut = class extends Error {
|
|
3627
|
+
baseUrl;
|
|
3628
|
+
endpoint;
|
|
3629
|
+
timeoutMs;
|
|
3630
|
+
constructor(baseUrl, endpoint, timeoutMs) {
|
|
3631
|
+
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.`);
|
|
3632
|
+
this.baseUrl = baseUrl;
|
|
3633
|
+
this.endpoint = endpoint;
|
|
3634
|
+
this.timeoutMs = timeoutMs;
|
|
3635
|
+
this.name = "DokployRequestTimedOut";
|
|
3053
3636
|
}
|
|
3054
|
-
}
|
|
3055
|
-
|
|
3056
|
-
|
|
3057
|
-
|
|
3058
|
-
|
|
3637
|
+
};
|
|
3638
|
+
/**
|
|
3639
|
+
* Waits `ms` between retries, or rejects with the signal's reason the moment
|
|
3640
|
+
* it aborts — a caller who gave up should not sit through the backoff.
|
|
3641
|
+
*/
|
|
3642
|
+
function pause(ms, signal) {
|
|
3643
|
+
return new Promise((resolve, reject) => {
|
|
3644
|
+
if (signal?.aborted) {
|
|
3645
|
+
reject(signal.reason);
|
|
3646
|
+
return;
|
|
3647
|
+
}
|
|
3648
|
+
const onAbort = () => {
|
|
3649
|
+
clearTimeout(timer);
|
|
3650
|
+
reject(signal?.reason);
|
|
3651
|
+
};
|
|
3652
|
+
const timer = setTimeout(() => {
|
|
3653
|
+
signal?.removeEventListener("abort", onAbort);
|
|
3654
|
+
resolve();
|
|
3655
|
+
}, ms);
|
|
3656
|
+
signal?.addEventListener("abort", onAbort, { once: true });
|
|
3657
|
+
});
|
|
3658
|
+
}
|
|
3659
|
+
var DokployApiError = class extends Error {
|
|
3660
|
+
status;
|
|
3661
|
+
statusText;
|
|
3662
|
+
issues;
|
|
3663
|
+
constructor(message, status, statusText, issues) {
|
|
3664
|
+
super(message);
|
|
3665
|
+
this.status = status;
|
|
3666
|
+
this.statusText = statusText;
|
|
3667
|
+
this.issues = issues;
|
|
3668
|
+
this.name = "DokployApiError";
|
|
3059
3669
|
}
|
|
3060
|
-
}
|
|
3061
|
-
|
|
3062
|
-
|
|
3063
|
-
|
|
3064
|
-
|
|
3670
|
+
};
|
|
3671
|
+
/**
|
|
3672
|
+
* Dokploy API client
|
|
3673
|
+
*/
|
|
3674
|
+
var DokployApi = class {
|
|
3675
|
+
baseUrl;
|
|
3676
|
+
token;
|
|
3677
|
+
timeoutMs;
|
|
3678
|
+
signal;
|
|
3679
|
+
constructor(options) {
|
|
3680
|
+
this.baseUrl = options.baseUrl.replace(/\/$/, "");
|
|
3681
|
+
this.token = options.token;
|
|
3682
|
+
this.timeoutMs = options.timeoutMs ?? 3e4;
|
|
3683
|
+
this.signal = options.signal;
|
|
3065
3684
|
}
|
|
3066
|
-
|
|
3067
|
-
|
|
3068
|
-
|
|
3069
|
-
get
|
|
3070
|
-
return
|
|
3685
|
+
/**
|
|
3686
|
+
* Make a GET request to the Dokploy API
|
|
3687
|
+
*/
|
|
3688
|
+
async get(endpoint) {
|
|
3689
|
+
return this.request("GET", endpoint);
|
|
3071
3690
|
}
|
|
3072
|
-
|
|
3073
|
-
|
|
3074
|
-
|
|
3075
|
-
|
|
3076
|
-
return
|
|
3691
|
+
/**
|
|
3692
|
+
* Make a POST request to the Dokploy API
|
|
3693
|
+
*/
|
|
3694
|
+
async post(endpoint, body) {
|
|
3695
|
+
return this.request("POST", endpoint, body);
|
|
3077
3696
|
}
|
|
3078
|
-
|
|
3079
|
-
|
|
3080
|
-
|
|
3081
|
-
|
|
3082
|
-
|
|
3697
|
+
/**
|
|
3698
|
+
* Make a request to the Dokploy API
|
|
3699
|
+
*/
|
|
3700
|
+
async request(method, endpoint, body) {
|
|
3701
|
+
const url = `${this.baseUrl}/api/${endpoint}`;
|
|
3702
|
+
let response;
|
|
3703
|
+
let timeout;
|
|
3704
|
+
let lastError;
|
|
3705
|
+
for (let attempt = 1; attempt <= 3; attempt++) {
|
|
3706
|
+
this.signal?.throwIfAborted();
|
|
3707
|
+
timeout = AbortSignal.timeout(this.timeoutMs);
|
|
3708
|
+
try {
|
|
3709
|
+
response = await fetch(url, {
|
|
3710
|
+
method,
|
|
3711
|
+
headers: {
|
|
3712
|
+
"Content-Type": "application/json",
|
|
3713
|
+
"x-api-key": this.token
|
|
3714
|
+
},
|
|
3715
|
+
body: body ? JSON.stringify(body) : void 0,
|
|
3716
|
+
signal: this.signal ? AbortSignal.any([this.signal, timeout]) : timeout
|
|
3717
|
+
});
|
|
3718
|
+
break;
|
|
3719
|
+
} catch (error) {
|
|
3720
|
+
const stopped = this.stopped(timeout, endpoint);
|
|
3721
|
+
if (stopped) throw stopped;
|
|
3722
|
+
lastError = error;
|
|
3723
|
+
if (attempt === 3) break;
|
|
3724
|
+
await pause(attempt * 750, this.signal);
|
|
3725
|
+
}
|
|
3726
|
+
}
|
|
3727
|
+
if (!response || !timeout) {
|
|
3728
|
+
const detail = lastError instanceof Error ? lastError.message : String(lastError);
|
|
3729
|
+
throw new DokployApiError(`Could not reach Dokploy at ${this.baseUrl} (${endpoint}): ${detail}`, 0, "Network error");
|
|
3730
|
+
}
|
|
3731
|
+
try {
|
|
3732
|
+
return await this.read(response);
|
|
3733
|
+
} catch (error) {
|
|
3734
|
+
throw this.stopped(timeout, endpoint) ?? error;
|
|
3735
|
+
}
|
|
3083
3736
|
}
|
|
3084
|
-
|
|
3085
|
-
|
|
3086
|
-
|
|
3087
|
-
|
|
3088
|
-
|
|
3737
|
+
/**
|
|
3738
|
+
* What a request cut short should reject with: the caller's own reason
|
|
3739
|
+
* when they aborted, `DokployRequestTimedOut` when the deadline passed,
|
|
3740
|
+
* nothing when it was neither.
|
|
3741
|
+
*/
|
|
3742
|
+
stopped(timeout, endpoint) {
|
|
3743
|
+
if (this.signal?.aborted) return this.signal.reason;
|
|
3744
|
+
if (timeout.aborted) return new DokployRequestTimedOut(this.baseUrl, endpoint, this.timeoutMs);
|
|
3089
3745
|
}
|
|
3090
|
-
|
|
3091
|
-
|
|
3092
|
-
|
|
3093
|
-
|
|
3094
|
-
|
|
3746
|
+
/** Turns a response into its JSON body, or a `DokployApiError`. */
|
|
3747
|
+
async read(response) {
|
|
3748
|
+
if (!response.ok) {
|
|
3749
|
+
let errorMessage = `Dokploy API error: ${response.status} ${response.statusText}`;
|
|
3750
|
+
let issues;
|
|
3751
|
+
try {
|
|
3752
|
+
const errorBody = await response.json();
|
|
3753
|
+
if (errorBody.message) errorMessage = `Dokploy API error: ${errorBody.message}`;
|
|
3754
|
+
if (errorBody.issues?.length) {
|
|
3755
|
+
issues = errorBody.issues;
|
|
3756
|
+
errorMessage += `\n Issues: ${errorBody.issues.map((i) => i.message).join(", ")}`;
|
|
3757
|
+
}
|
|
3758
|
+
} catch {}
|
|
3759
|
+
throw new DokployApiError(errorMessage, response.status, response.statusText, issues);
|
|
3760
|
+
}
|
|
3761
|
+
const text = await response.text();
|
|
3762
|
+
if (!text || text.trim() === "") return;
|
|
3763
|
+
return JSON.parse(text);
|
|
3095
3764
|
}
|
|
3096
|
-
|
|
3097
|
-
|
|
3098
|
-
|
|
3099
|
-
|
|
3100
|
-
|
|
3765
|
+
/**
|
|
3766
|
+
* Every external port published on this server, across every project.
|
|
3767
|
+
*
|
|
3768
|
+
* Dokploy knows what it has bound; guessing does not. A port that looks free
|
|
3769
|
+
* from here can be held by a service in a project this deploy has never
|
|
3770
|
+
* heard of — which surfaces as `Port 5432 is already in use by container
|
|
3771
|
+
* "…"`, naming something the caller cannot see.
|
|
3772
|
+
*
|
|
3773
|
+
* `project.all` is a summary, so each project is fetched: the per-service
|
|
3774
|
+
* `externalPort` only appears on `project.one`.
|
|
3775
|
+
*/
|
|
3776
|
+
async publishedPorts() {
|
|
3777
|
+
const ports = /* @__PURE__ */ new Set();
|
|
3778
|
+
const projects = await this.listProjects();
|
|
3779
|
+
for (const project of projects) {
|
|
3780
|
+
const details = await this.getProject(project.projectId);
|
|
3781
|
+
for (const environment of details.environments ?? []) {
|
|
3782
|
+
const resources = [
|
|
3783
|
+
...environment.postgres ?? [],
|
|
3784
|
+
...environment.redis ?? [],
|
|
3785
|
+
...environment.applications ?? []
|
|
3786
|
+
];
|
|
3787
|
+
for (const resource of resources) if (typeof resource.externalPort === "number") ports.add(resource.externalPort);
|
|
3788
|
+
}
|
|
3789
|
+
}
|
|
3790
|
+
return ports;
|
|
3101
3791
|
}
|
|
3102
|
-
|
|
3103
|
-
|
|
3104
|
-
|
|
3105
|
-
|
|
3792
|
+
/**
|
|
3793
|
+
* Validate the API token by making a test request
|
|
3794
|
+
*/
|
|
3795
|
+
async validateToken() {
|
|
3796
|
+
try {
|
|
3797
|
+
await this.get("project.all");
|
|
3798
|
+
return true;
|
|
3799
|
+
} catch {
|
|
3800
|
+
return false;
|
|
3801
|
+
}
|
|
3802
|
+
}
|
|
3803
|
+
/**
|
|
3804
|
+
* List all projects
|
|
3805
|
+
*/
|
|
3806
|
+
async listProjects() {
|
|
3807
|
+
return this.get("project.all");
|
|
3808
|
+
}
|
|
3809
|
+
/**
|
|
3810
|
+
* Get a single project by ID
|
|
3811
|
+
*/
|
|
3812
|
+
async getProject(projectId) {
|
|
3813
|
+
return this.get(`project.one?projectId=${projectId}`);
|
|
3814
|
+
}
|
|
3815
|
+
/**
|
|
3816
|
+
* Create a new project
|
|
3817
|
+
*/
|
|
3818
|
+
async createProject(name, description) {
|
|
3819
|
+
return this.post("project.create", {
|
|
3820
|
+
name,
|
|
3821
|
+
description: description ?? `Created by gkm CLI`
|
|
3822
|
+
});
|
|
3823
|
+
}
|
|
3824
|
+
/**
|
|
3825
|
+
* Change a project's name or description.
|
|
3826
|
+
*
|
|
3827
|
+
* The name is sent with the description because `project.update` validates
|
|
3828
|
+
* the whole project on some Dokploy versions, and a missing name fails it.
|
|
3829
|
+
*/
|
|
3830
|
+
async updateProject(projectId, updates) {
|
|
3831
|
+
await this.post("project.update", {
|
|
3832
|
+
projectId,
|
|
3833
|
+
...updates
|
|
3834
|
+
});
|
|
3835
|
+
}
|
|
3836
|
+
/**
|
|
3837
|
+
* Delete a project and all its resources
|
|
3838
|
+
*/
|
|
3839
|
+
async deleteProject(projectId) {
|
|
3840
|
+
await this.post("project.remove", { projectId });
|
|
3841
|
+
}
|
|
3842
|
+
/**
|
|
3843
|
+
* Create an environment in a project
|
|
3844
|
+
*/
|
|
3845
|
+
async createEnvironment(projectId, name, description) {
|
|
3846
|
+
return this.post("environment.create", {
|
|
3847
|
+
projectId,
|
|
3848
|
+
name,
|
|
3849
|
+
description: description ?? `${name} environment`
|
|
3850
|
+
});
|
|
3851
|
+
}
|
|
3852
|
+
/**
|
|
3853
|
+
* List all applications in a project
|
|
3854
|
+
*/
|
|
3855
|
+
async listApplications(projectId) {
|
|
3856
|
+
return this.listInProject(projectId, (env) => env.applications);
|
|
3857
|
+
}
|
|
3858
|
+
/**
|
|
3859
|
+
* Everything of one kind in a project, across its environments.
|
|
3860
|
+
*
|
|
3861
|
+
* From `project.one`, because the per-kind `*.all?projectId=` endpoints
|
|
3862
|
+
* return **404** on current Dokploy: resources live under environments now.
|
|
3863
|
+
* Those calls used to be wrapped in `catch { return [] }`, which turned "I
|
|
3864
|
+
* could not ask" into "there are none" — and for a find-or-create that is
|
|
3865
|
+
* the worst available answer. Every deploy created a second application, a
|
|
3866
|
+
* second Postgres and a second Redis, silently, for as long as the project
|
|
3867
|
+
* existed.
|
|
3868
|
+
*
|
|
3869
|
+
* A failure here is not swallowed: creating a duplicate is worse than
|
|
3870
|
+
* stopping.
|
|
3871
|
+
*/
|
|
3872
|
+
async listInProject(projectId, pick) {
|
|
3873
|
+
return ((await this.getProject(projectId)).environments ?? []).flatMap((environment) => (pick(environment) ?? []).map((resource) => ({
|
|
3874
|
+
...resource,
|
|
3875
|
+
environmentId: environment.environmentId
|
|
3876
|
+
})));
|
|
3877
|
+
}
|
|
3878
|
+
/**
|
|
3879
|
+
* Find an application by name in a project
|
|
3880
|
+
*/
|
|
3881
|
+
async findApplicationByName(projectId, name, environmentId) {
|
|
3882
|
+
const applications = await this.listApplications(projectId);
|
|
3883
|
+
const normalizedName = name.toLowerCase().replace(/[^a-z0-9-]/g, "-");
|
|
3884
|
+
return applications.find((app) => (!environmentId || app.environmentId === environmentId) && (app.name === name || app.appName === normalizedName));
|
|
3885
|
+
}
|
|
3886
|
+
/**
|
|
3887
|
+
* Create a new application
|
|
3888
|
+
*/
|
|
3889
|
+
async createApplication(name, projectId, environmentId) {
|
|
3890
|
+
return this.post("application.create", {
|
|
3891
|
+
name,
|
|
3892
|
+
projectId,
|
|
3893
|
+
environmentId,
|
|
3894
|
+
appName: name.toLowerCase().replace(/[^a-z0-9-]/g, "-")
|
|
3895
|
+
});
|
|
3896
|
+
}
|
|
3897
|
+
/**
|
|
3898
|
+
* Find or create an application by name
|
|
3899
|
+
*/
|
|
3900
|
+
async findOrCreateApplication(name, projectId, environmentId) {
|
|
3901
|
+
const existing = await this.findApplicationByName(projectId, name, environmentId);
|
|
3902
|
+
if (existing) return {
|
|
3903
|
+
application: existing,
|
|
3904
|
+
created: false
|
|
3905
|
+
};
|
|
3906
|
+
return {
|
|
3907
|
+
application: await this.createApplication(name, projectId, environmentId),
|
|
3908
|
+
created: true
|
|
3909
|
+
};
|
|
3910
|
+
}
|
|
3911
|
+
/**
|
|
3912
|
+
* Get an application by ID
|
|
3913
|
+
*/
|
|
3914
|
+
async getApplication(applicationId) {
|
|
3915
|
+
try {
|
|
3916
|
+
return await this.get(`application.one?applicationId=${applicationId}`);
|
|
3917
|
+
} catch {
|
|
3918
|
+
return null;
|
|
3919
|
+
}
|
|
3920
|
+
}
|
|
3921
|
+
/**
|
|
3922
|
+
* Update an application
|
|
3923
|
+
*/
|
|
3924
|
+
async updateApplication(applicationId, updates) {
|
|
3925
|
+
await this.post("application.update", {
|
|
3926
|
+
applicationId,
|
|
3927
|
+
...updates
|
|
3928
|
+
});
|
|
3929
|
+
}
|
|
3930
|
+
/**
|
|
3931
|
+
* Save environment variables for an application
|
|
3932
|
+
*/
|
|
3933
|
+
async saveApplicationEnv(applicationId, env, options) {
|
|
3934
|
+
await this.post("application.saveEnvironment", {
|
|
3935
|
+
applicationId,
|
|
3936
|
+
env,
|
|
3937
|
+
buildArgs: options?.buildArgs ?? null,
|
|
3938
|
+
buildSecrets: options?.buildSecrets ?? null,
|
|
3939
|
+
createEnvFile: false
|
|
3940
|
+
});
|
|
3941
|
+
}
|
|
3942
|
+
/**
|
|
3943
|
+
* Configure application to use Docker provider (pull from registry)
|
|
3944
|
+
*
|
|
3945
|
+
* For private registries, either:
|
|
3946
|
+
* - Use `registryId` if the registry is configured in Dokploy
|
|
3947
|
+
* - Or provide `username`, `password`, and `registryUrl` directly
|
|
3948
|
+
*/
|
|
3949
|
+
async saveDockerProvider(applicationId, dockerImage, options) {
|
|
3950
|
+
await this.post("application.saveDockerProvider", {
|
|
3951
|
+
applicationId,
|
|
3952
|
+
dockerImage,
|
|
3953
|
+
...options,
|
|
3954
|
+
username: options?.username ?? null,
|
|
3955
|
+
password: options?.password ?? null,
|
|
3956
|
+
registryUrl: options?.registryUrl ?? null
|
|
3957
|
+
});
|
|
3958
|
+
}
|
|
3959
|
+
/**
|
|
3960
|
+
* Deploy an application
|
|
3961
|
+
*/
|
|
3962
|
+
async deployApplication(applicationId) {
|
|
3963
|
+
await this.post("application.deploy", { applicationId });
|
|
3964
|
+
}
|
|
3965
|
+
/**
|
|
3966
|
+
* Delete an application
|
|
3967
|
+
*/
|
|
3968
|
+
async deleteApplication(applicationId) {
|
|
3969
|
+
await this.post("application.remove", { applicationId });
|
|
3970
|
+
}
|
|
3971
|
+
/**
|
|
3972
|
+
* List all registries
|
|
3973
|
+
*/
|
|
3974
|
+
async listRegistries() {
|
|
3975
|
+
return this.get("registry.all");
|
|
3976
|
+
}
|
|
3977
|
+
/**
|
|
3978
|
+
* Create a new registry
|
|
3979
|
+
*/
|
|
3980
|
+
async createRegistry(registryName, registryUrl, username, password, options) {
|
|
3981
|
+
return this.post("registry.create", {
|
|
3982
|
+
registryName,
|
|
3983
|
+
registryUrl,
|
|
3984
|
+
username,
|
|
3985
|
+
password,
|
|
3986
|
+
imagePrefix: options?.imagePrefix
|
|
3987
|
+
});
|
|
3988
|
+
}
|
|
3989
|
+
/**
|
|
3990
|
+
* Get a registry by ID
|
|
3991
|
+
*/
|
|
3992
|
+
async getRegistry(registryId) {
|
|
3993
|
+
return this.get(`registry.one?registryId=${registryId}`);
|
|
3994
|
+
}
|
|
3995
|
+
/**
|
|
3996
|
+
* Update a registry
|
|
3997
|
+
*/
|
|
3998
|
+
async updateRegistry(registryId, updates) {
|
|
3999
|
+
await this.post("registry.update", {
|
|
4000
|
+
registryId,
|
|
4001
|
+
...updates
|
|
4002
|
+
});
|
|
4003
|
+
}
|
|
4004
|
+
/**
|
|
4005
|
+
* Delete a registry
|
|
4006
|
+
*/
|
|
4007
|
+
async deleteRegistry(registryId) {
|
|
4008
|
+
await this.post("registry.remove", { registryId });
|
|
4009
|
+
}
|
|
4010
|
+
/**
|
|
4011
|
+
* List all Postgres databases in a project
|
|
4012
|
+
*/
|
|
4013
|
+
async listPostgres(projectId) {
|
|
4014
|
+
return this.listInProject(projectId, (env) => env.postgres);
|
|
4015
|
+
}
|
|
4016
|
+
/**
|
|
4017
|
+
* Find a Postgres database by name in a project
|
|
4018
|
+
*/
|
|
4019
|
+
async findPostgresByName(projectId, name) {
|
|
4020
|
+
const databases = await this.listPostgres(projectId);
|
|
4021
|
+
const normalizedName = name.toLowerCase().replace(/[^a-z0-9-]/g, "-");
|
|
4022
|
+
const match = databases.find((db) => db.name === name || db.appName === normalizedName);
|
|
4023
|
+
if (!match) return void 0;
|
|
4024
|
+
return this.getPostgres(match.postgresId);
|
|
4025
|
+
}
|
|
4026
|
+
/**
|
|
4027
|
+
* Create a new Postgres database
|
|
4028
|
+
*/
|
|
4029
|
+
async createPostgres(name, projectId, environmentId, options) {
|
|
4030
|
+
return this.post("postgres.create", {
|
|
4031
|
+
name,
|
|
4032
|
+
projectId,
|
|
4033
|
+
environmentId,
|
|
4034
|
+
appName: options?.appName ?? name.toLowerCase().replace(/[^a-z0-9-]/g, "-"),
|
|
4035
|
+
databaseName: options?.databaseName,
|
|
4036
|
+
databaseUser: options?.databaseUser ?? "postgres",
|
|
4037
|
+
databasePassword: options?.databasePassword,
|
|
4038
|
+
dockerImage: options?.dockerImage ?? "postgres:18",
|
|
4039
|
+
description: options?.description ?? `Postgres database for ${name}`
|
|
4040
|
+
});
|
|
4041
|
+
}
|
|
4042
|
+
/**
|
|
4043
|
+
* Find or create a Postgres database by name
|
|
4044
|
+
*/
|
|
4045
|
+
async findOrCreatePostgres(name, projectId, environmentId, options) {
|
|
4046
|
+
const existing = await this.findPostgresByName(projectId, name);
|
|
4047
|
+
if (existing) return {
|
|
4048
|
+
postgres: existing,
|
|
4049
|
+
created: false
|
|
4050
|
+
};
|
|
4051
|
+
return {
|
|
4052
|
+
postgres: await this.createPostgres(name, projectId, environmentId, options),
|
|
4053
|
+
created: true
|
|
4054
|
+
};
|
|
4055
|
+
}
|
|
4056
|
+
/**
|
|
4057
|
+
* Get a Postgres database by ID
|
|
4058
|
+
*/
|
|
4059
|
+
async getPostgres(postgresId) {
|
|
4060
|
+
return this.get(`postgres.one?postgresId=${postgresId}`);
|
|
4061
|
+
}
|
|
4062
|
+
/**
|
|
4063
|
+
* Deploy a Postgres database
|
|
4064
|
+
*/
|
|
4065
|
+
async deployPostgres(postgresId) {
|
|
4066
|
+
await this.post("postgres.deploy", { postgresId });
|
|
4067
|
+
}
|
|
4068
|
+
/**
|
|
4069
|
+
* Save environment variables for Postgres
|
|
4070
|
+
*/
|
|
4071
|
+
async savePostgresEnv(postgresId, env) {
|
|
4072
|
+
await this.post("postgres.saveEnvironment", {
|
|
4073
|
+
postgresId,
|
|
4074
|
+
env
|
|
4075
|
+
});
|
|
4076
|
+
}
|
|
4077
|
+
/**
|
|
4078
|
+
* Set external port for Postgres (for external access)
|
|
4079
|
+
*/
|
|
4080
|
+
async savePostgresExternalPort(postgresId, externalPort) {
|
|
4081
|
+
await this.post("postgres.saveExternalPort", {
|
|
4082
|
+
postgresId,
|
|
4083
|
+
externalPort
|
|
4084
|
+
});
|
|
4085
|
+
}
|
|
4086
|
+
/**
|
|
4087
|
+
* Update Postgres configuration
|
|
4088
|
+
*/
|
|
4089
|
+
async updatePostgres(postgresId, updates) {
|
|
4090
|
+
await this.post("postgres.update", {
|
|
4091
|
+
postgresId,
|
|
4092
|
+
...updates
|
|
4093
|
+
});
|
|
4094
|
+
}
|
|
4095
|
+
/**
|
|
4096
|
+
* Delete a Postgres database
|
|
4097
|
+
*/
|
|
4098
|
+
async deletePostgres(postgresId) {
|
|
4099
|
+
await this.post("postgres.remove", { postgresId });
|
|
4100
|
+
}
|
|
4101
|
+
/**
|
|
4102
|
+
* The compose stacks in a project.
|
|
4103
|
+
*
|
|
4104
|
+
* Dokploy has first-class primitives for Postgres and Redis and none for
|
|
4105
|
+
* MinIO, so an object store is a compose stack — the one kind whose contents
|
|
4106
|
+
* this target writes rather than configures.
|
|
4107
|
+
*/
|
|
4108
|
+
async listCompose(projectId) {
|
|
4109
|
+
return this.listInProject(projectId, (env) => env.compose);
|
|
4110
|
+
}
|
|
4111
|
+
/** A compose stack by name, hydrated — `project.one` returns summaries. */
|
|
4112
|
+
async findComposeByName(projectId, name) {
|
|
4113
|
+
const summary = (await this.listCompose(projectId)).find((compose) => compose.name === name);
|
|
4114
|
+
return summary ? this.getCompose(summary.composeId) : void 0;
|
|
4115
|
+
}
|
|
4116
|
+
async getCompose(composeId) {
|
|
4117
|
+
return this.get(`compose.one?composeId=${composeId}`);
|
|
4118
|
+
}
|
|
4119
|
+
/**
|
|
4120
|
+
* A compose stack carrying the file this target wrote.
|
|
4121
|
+
*
|
|
4122
|
+
* Created empty and then updated, because `compose.create` takes no file:
|
|
4123
|
+
* it defaults to `sourceType: 'github'` and expects a repository. `raw` is
|
|
4124
|
+
* what makes the file ours.
|
|
4125
|
+
*/
|
|
4126
|
+
async findOrCreateCompose(name, projectId, environmentId, composeFile) {
|
|
4127
|
+
const existing = await this.findComposeByName(projectId, name);
|
|
4128
|
+
const compose = existing ?? await this.post("compose.create", {
|
|
4129
|
+
name,
|
|
4130
|
+
environmentId,
|
|
4131
|
+
composeType: "docker-compose"
|
|
4132
|
+
});
|
|
4133
|
+
await this.post("compose.update", {
|
|
4134
|
+
composeId: compose.composeId,
|
|
4135
|
+
sourceType: "raw",
|
|
4136
|
+
composeFile
|
|
4137
|
+
});
|
|
4138
|
+
return {
|
|
4139
|
+
compose,
|
|
4140
|
+
created: !existing
|
|
4141
|
+
};
|
|
4142
|
+
}
|
|
4143
|
+
async deployCompose(composeId) {
|
|
4144
|
+
await this.post("compose.deploy", { composeId });
|
|
4145
|
+
}
|
|
4146
|
+
async deleteCompose(composeId) {
|
|
4147
|
+
await this.post("compose.delete", {
|
|
4148
|
+
composeId,
|
|
4149
|
+
deleteVolumes: false
|
|
4150
|
+
});
|
|
4151
|
+
}
|
|
4152
|
+
/**
|
|
4153
|
+
* List all Redis instances in a project
|
|
4154
|
+
*/
|
|
4155
|
+
async listRedis(projectId) {
|
|
4156
|
+
return this.listInProject(projectId, (env) => env.redis);
|
|
4157
|
+
}
|
|
4158
|
+
/**
|
|
4159
|
+
* Find a Redis instance by name in a project
|
|
4160
|
+
*/
|
|
4161
|
+
async findRedisByName(projectId, name) {
|
|
4162
|
+
const instances = await this.listRedis(projectId);
|
|
4163
|
+
const normalizedName = name.toLowerCase().replace(/[^a-z0-9-]/g, "-");
|
|
4164
|
+
const match = instances.find((redis) => redis.name === name || redis.appName === normalizedName);
|
|
4165
|
+
return match ? this.getRedis(match.redisId) : void 0;
|
|
4166
|
+
}
|
|
4167
|
+
/**
|
|
4168
|
+
* Create a new Redis instance
|
|
4169
|
+
*/
|
|
4170
|
+
async createRedis(name, projectId, environmentId, options) {
|
|
4171
|
+
return this.post("redis.create", {
|
|
4172
|
+
name,
|
|
4173
|
+
projectId,
|
|
4174
|
+
environmentId,
|
|
4175
|
+
appName: options?.appName ?? name.toLowerCase().replace(/[^a-z0-9-]/g, "-"),
|
|
4176
|
+
databasePassword: options?.databasePassword,
|
|
4177
|
+
dockerImage: options?.dockerImage ?? "redis:8",
|
|
4178
|
+
description: options?.description ?? `Redis instance for ${name}`
|
|
4179
|
+
});
|
|
4180
|
+
}
|
|
4181
|
+
/**
|
|
4182
|
+
* Find or create a Redis instance by name
|
|
4183
|
+
*/
|
|
4184
|
+
async findOrCreateRedis(name, projectId, environmentId, options) {
|
|
4185
|
+
const existing = await this.findRedisByName(projectId, name);
|
|
4186
|
+
if (existing) return {
|
|
4187
|
+
redis: existing,
|
|
4188
|
+
created: false
|
|
4189
|
+
};
|
|
4190
|
+
return {
|
|
4191
|
+
redis: await this.createRedis(name, projectId, environmentId, options),
|
|
4192
|
+
created: true
|
|
4193
|
+
};
|
|
4194
|
+
}
|
|
4195
|
+
/**
|
|
4196
|
+
* Get a Redis instance by ID
|
|
4197
|
+
*/
|
|
4198
|
+
async getRedis(redisId) {
|
|
4199
|
+
return this.get(`redis.one?redisId=${redisId}`);
|
|
4200
|
+
}
|
|
4201
|
+
/**
|
|
4202
|
+
* Deploy a Redis instance
|
|
4203
|
+
*/
|
|
4204
|
+
async deployRedis(redisId) {
|
|
4205
|
+
await this.post("redis.deploy", { redisId });
|
|
4206
|
+
}
|
|
4207
|
+
/**
|
|
4208
|
+
* Save environment variables for Redis
|
|
4209
|
+
*/
|
|
4210
|
+
async saveRedisEnv(redisId, env) {
|
|
4211
|
+
await this.post("redis.saveEnvironment", {
|
|
4212
|
+
redisId,
|
|
4213
|
+
env
|
|
4214
|
+
});
|
|
4215
|
+
}
|
|
4216
|
+
/**
|
|
4217
|
+
* Set external port for Redis (for external access)
|
|
4218
|
+
*/
|
|
4219
|
+
async saveRedisExternalPort(redisId, externalPort) {
|
|
4220
|
+
await this.post("redis.saveExternalPort", {
|
|
4221
|
+
redisId,
|
|
4222
|
+
externalPort
|
|
4223
|
+
});
|
|
4224
|
+
}
|
|
4225
|
+
/**
|
|
4226
|
+
* Update Redis configuration
|
|
4227
|
+
*/
|
|
4228
|
+
async updateRedis(redisId, updates) {
|
|
4229
|
+
await this.post("redis.update", {
|
|
4230
|
+
redisId,
|
|
4231
|
+
...updates
|
|
4232
|
+
});
|
|
4233
|
+
}
|
|
4234
|
+
/**
|
|
4235
|
+
* Delete a Redis instance
|
|
4236
|
+
*/
|
|
4237
|
+
async deleteRedis(redisId) {
|
|
4238
|
+
await this.post("redis.remove", { redisId });
|
|
4239
|
+
}
|
|
4240
|
+
/**
|
|
4241
|
+
* Create a new domain for an application
|
|
4242
|
+
*/
|
|
4243
|
+
async createDomain(options) {
|
|
4244
|
+
return this.post("domain.create", options);
|
|
4245
|
+
}
|
|
4246
|
+
/**
|
|
4247
|
+
* Update an existing domain
|
|
4248
|
+
*/
|
|
4249
|
+
async updateDomain(domainId, updates) {
|
|
4250
|
+
await this.post("domain.update", {
|
|
4251
|
+
domainId,
|
|
4252
|
+
...updates
|
|
4253
|
+
});
|
|
4254
|
+
}
|
|
4255
|
+
/**
|
|
4256
|
+
* Delete a domain
|
|
4257
|
+
*/
|
|
4258
|
+
async deleteDomain(domainId) {
|
|
4259
|
+
await this.post("domain.delete", { domainId });
|
|
4260
|
+
}
|
|
4261
|
+
/**
|
|
4262
|
+
* Get a domain by ID
|
|
4263
|
+
*/
|
|
4264
|
+
async getDomain(domainId) {
|
|
4265
|
+
return this.get(`domain.one?domainId=${domainId}`);
|
|
4266
|
+
}
|
|
4267
|
+
/**
|
|
4268
|
+
* Get all domains for an application
|
|
4269
|
+
*/
|
|
4270
|
+
async getDomainsByApplicationId(applicationId) {
|
|
4271
|
+
return this.get(`domain.byApplicationId?applicationId=${applicationId}`);
|
|
4272
|
+
}
|
|
4273
|
+
/**
|
|
4274
|
+
* Validate a domain and trigger SSL certificate generation
|
|
4275
|
+
*
|
|
4276
|
+
* This should be called after DNS records are created and propagated.
|
|
4277
|
+
* It triggers Let's Encrypt certificate generation for HTTPS domains.
|
|
4278
|
+
*
|
|
4279
|
+
* @param domain - The domain hostname to validate (e.g., 'api.example.com')
|
|
4280
|
+
*/
|
|
4281
|
+
async validateDomain(domain) {
|
|
4282
|
+
return this.post("domain.validateDomain", { domain });
|
|
4283
|
+
}
|
|
4284
|
+
/**
|
|
4285
|
+
* Auto-generate a domain name for an application
|
|
4286
|
+
*/
|
|
4287
|
+
async generateDomain(appName, serverId) {
|
|
4288
|
+
return this.post("domain.generateDomain", {
|
|
4289
|
+
appName,
|
|
4290
|
+
serverId
|
|
4291
|
+
});
|
|
4292
|
+
}
|
|
4293
|
+
/**
|
|
4294
|
+
* List all backup destinations
|
|
4295
|
+
*/
|
|
4296
|
+
async listDestinations() {
|
|
4297
|
+
return this.get("destination.all");
|
|
4298
|
+
}
|
|
4299
|
+
/**
|
|
4300
|
+
* Get a destination by ID
|
|
4301
|
+
*/
|
|
4302
|
+
async getDestination(destinationId) {
|
|
4303
|
+
return this.get(`destination.one?destinationId=${destinationId}`);
|
|
4304
|
+
}
|
|
4305
|
+
/**
|
|
4306
|
+
* Create a new S3 backup destination
|
|
4307
|
+
*/
|
|
4308
|
+
async createDestination(options) {
|
|
4309
|
+
return this.post("destination.create", { ...options });
|
|
4310
|
+
}
|
|
4311
|
+
/**
|
|
4312
|
+
* Find a destination by name
|
|
4313
|
+
*/
|
|
4314
|
+
async findDestinationByName(name) {
|
|
4315
|
+
return (await this.listDestinations()).find((d) => d.name === name);
|
|
4316
|
+
}
|
|
4317
|
+
/**
|
|
4318
|
+
* Find or create a destination by name
|
|
4319
|
+
*/
|
|
4320
|
+
async findOrCreateDestination(name, options) {
|
|
4321
|
+
const existing = await this.findDestinationByName(name);
|
|
4322
|
+
if (existing) return {
|
|
4323
|
+
destination: existing,
|
|
4324
|
+
created: false
|
|
4325
|
+
};
|
|
4326
|
+
return {
|
|
4327
|
+
destination: await this.createDestination({
|
|
4328
|
+
name,
|
|
4329
|
+
...options
|
|
4330
|
+
}),
|
|
4331
|
+
created: true
|
|
4332
|
+
};
|
|
4333
|
+
}
|
|
4334
|
+
/**
|
|
4335
|
+
* Update a destination
|
|
4336
|
+
*/
|
|
4337
|
+
async updateDestination(destinationId, updates) {
|
|
4338
|
+
await this.post("destination.update", {
|
|
4339
|
+
destinationId,
|
|
4340
|
+
...updates
|
|
4341
|
+
});
|
|
4342
|
+
}
|
|
4343
|
+
/**
|
|
4344
|
+
* Delete a destination
|
|
4345
|
+
*/
|
|
4346
|
+
async deleteDestination(destinationId) {
|
|
4347
|
+
await this.post("destination.remove", { destinationId });
|
|
4348
|
+
}
|
|
4349
|
+
/**
|
|
4350
|
+
* Test connection to a destination
|
|
4351
|
+
*/
|
|
4352
|
+
async testDestinationConnection(destinationId) {
|
|
4353
|
+
return this.post("destination.testConnection", { destinationId });
|
|
4354
|
+
}
|
|
4355
|
+
/**
|
|
4356
|
+
* Create a backup schedule for postgres
|
|
4357
|
+
*/
|
|
4358
|
+
async createPostgresBackup(options) {
|
|
4359
|
+
return this.post("backup.create", {
|
|
4360
|
+
...options,
|
|
4361
|
+
databaseType: "postgres"
|
|
4362
|
+
});
|
|
4363
|
+
}
|
|
4364
|
+
/**
|
|
4365
|
+
* List backups for a postgres database
|
|
4366
|
+
*/
|
|
4367
|
+
async listPostgresBackups(postgresId) {
|
|
4368
|
+
return this.get(`backup.all?postgresId=${postgresId}&databaseType=postgres`);
|
|
4369
|
+
}
|
|
4370
|
+
/**
|
|
4371
|
+
* Get a backup by ID
|
|
4372
|
+
*/
|
|
4373
|
+
async getBackup(backupId) {
|
|
4374
|
+
return this.get(`backup.one?backupId=${backupId}`);
|
|
4375
|
+
}
|
|
4376
|
+
/**
|
|
4377
|
+
* Update a backup schedule
|
|
4378
|
+
*/
|
|
4379
|
+
async updateBackup(backupId, updates) {
|
|
4380
|
+
await this.post("backup.update", {
|
|
4381
|
+
backupId,
|
|
4382
|
+
...updates
|
|
4383
|
+
});
|
|
4384
|
+
}
|
|
4385
|
+
/**
|
|
4386
|
+
* Delete a backup schedule
|
|
4387
|
+
*/
|
|
4388
|
+
async deleteBackup(backupId) {
|
|
4389
|
+
await this.post("backup.remove", { backupId });
|
|
4390
|
+
}
|
|
4391
|
+
/**
|
|
4392
|
+
* Trigger a manual backup run
|
|
4393
|
+
*/
|
|
4394
|
+
async runBackupManually(backupId) {
|
|
4395
|
+
await this.post("backup.manualBackup", { backupId });
|
|
4396
|
+
}
|
|
4397
|
+
};
|
|
4398
|
+
//#endregion
|
|
4399
|
+
//#region src/deploy/StateProvider.ts
|
|
4400
|
+
/**
|
|
4401
|
+
* Check if value is a StateProvider implementation.
|
|
4402
|
+
*/
|
|
4403
|
+
function isStateProvider(value) {
|
|
4404
|
+
return typeof value === "object" && value !== null && typeof value.read === "function" && typeof value.write === "function";
|
|
4405
|
+
}
|
|
4406
|
+
//#endregion
|
|
4407
|
+
//#region src/deploy/StateStore.ts
|
|
4408
|
+
/**
|
|
4409
|
+
* Deploy state store.
|
|
4410
|
+
*
|
|
4411
|
+
* `StateProvider` reads and writes a whole stage at once, with nothing to stop
|
|
4412
|
+
* two runs of the same stage from overwriting each other and nowhere to record
|
|
4413
|
+
* a resource the moment it exists. A `StateStore` adds the three things a
|
|
4414
|
+
* deploy needs to be safe against crashes and concurrent runs:
|
|
4415
|
+
*
|
|
4416
|
+
* - `lock(stage)`: one run per stage. A second gets `StateLocked`, naming the
|
|
4417
|
+
* holder, instead of racing the first.
|
|
4418
|
+
* - Versioned `read` and conditional `write`: a write names the version it was
|
|
4419
|
+
* based on, and raises `StateVersionConflict` when the stored state moved on
|
|
4420
|
+
* underneath it, so nothing is lost silently even without the lock.
|
|
4421
|
+
* - Per-resource records: `putResource` journals one resource at a time, so a
|
|
4422
|
+
* run that dies half way leaves behind the ids of everything it created.
|
|
4423
|
+
*
|
|
4424
|
+
* Built-in stores: `LocalStateStore`, `SSMStateStore`, `S3StateStore`. A
|
|
4425
|
+
* custom `StateProvider` is wrapped by `LegacyStateStore`, which keeps it
|
|
4426
|
+
* working but cannot lock.
|
|
4427
|
+
*/
|
|
4428
|
+
var StateLocked = class extends Error {
|
|
4429
|
+
stage;
|
|
4430
|
+
holder;
|
|
4431
|
+
location;
|
|
4432
|
+
constructor(stage, holder, location) {
|
|
4433
|
+
const by = holder ? `${holder.owner}@${holder.host} (pid ${holder.pid}) since ${holder.acquiredAt}${holder.operation ? ` for '${holder.operation}'` : ""}` : "a run that did not record who it was";
|
|
4434
|
+
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}\`.`);
|
|
4435
|
+
this.stage = stage;
|
|
4436
|
+
this.holder = holder;
|
|
4437
|
+
this.location = location;
|
|
4438
|
+
this.name = "StateLocked";
|
|
4439
|
+
}
|
|
4440
|
+
};
|
|
4441
|
+
var StateVersionConflict = class extends Error {
|
|
4442
|
+
stage;
|
|
4443
|
+
expectedVersion;
|
|
4444
|
+
actualVersion;
|
|
4445
|
+
constructor(stage, expectedVersion, actualVersion) {
|
|
4446
|
+
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.`);
|
|
4447
|
+
this.stage = stage;
|
|
4448
|
+
this.expectedVersion = expectedVersion;
|
|
4449
|
+
this.actualVersion = actualVersion;
|
|
4450
|
+
this.name = "StateVersionConflict";
|
|
4451
|
+
}
|
|
4452
|
+
};
|
|
4453
|
+
var StageStateMissing = class extends Error {
|
|
4454
|
+
stage;
|
|
4455
|
+
constructor(stage) {
|
|
4456
|
+
super(`There is no deploy state for stage '${stage}' to record a resource in. Write the stage's state first (write with expectedVersion: null).`);
|
|
4457
|
+
this.stage = stage;
|
|
4458
|
+
this.name = "StageStateMissing";
|
|
4459
|
+
}
|
|
4460
|
+
};
|
|
4461
|
+
var StateUnreadable = class extends Error {
|
|
4462
|
+
stage;
|
|
4463
|
+
location;
|
|
4464
|
+
constructor(stage, location, cause) {
|
|
4465
|
+
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 });
|
|
4466
|
+
this.stage = stage;
|
|
4467
|
+
this.location = location;
|
|
4468
|
+
this.name = "StateUnreadable";
|
|
4469
|
+
}
|
|
4470
|
+
};
|
|
4471
|
+
var StateSchemaTooNew = class extends Error {
|
|
4472
|
+
stage;
|
|
4473
|
+
schemaVersion;
|
|
4474
|
+
constructor(stage, schemaVersion) {
|
|
4475
|
+
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.`);
|
|
4476
|
+
this.stage = stage;
|
|
4477
|
+
this.schemaVersion = schemaVersion;
|
|
4478
|
+
this.name = "StateSchemaTooNew";
|
|
4479
|
+
}
|
|
4480
|
+
};
|
|
4481
|
+
/**
|
|
4482
|
+
* Not thrown: emitted (once per store) when a custom `StateProvider` stands
|
|
4483
|
+
* in for a store, so a deploy that cannot be protected from a concurrent one
|
|
4484
|
+
* says so.
|
|
4485
|
+
*/
|
|
4486
|
+
var StateStoreWithoutLocking = class extends Error {
|
|
4487
|
+
provider;
|
|
4488
|
+
constructor(provider) {
|
|
4489
|
+
super(`The custom state provider '${provider}' has no lock and no versions, so two deploys of the same stage can still overwrite each other. Implement StateStore (lock, versioned read, conditional write) or use the 'local', 'ssm' or 's3' state provider.`);
|
|
4490
|
+
this.provider = provider;
|
|
4491
|
+
this.name = "StateStoreWithoutLocking";
|
|
4492
|
+
}
|
|
4493
|
+
};
|
|
4494
|
+
var StateStoreNeedsWorkspaceName = class extends Error {
|
|
4495
|
+
provider;
|
|
4496
|
+
constructor(provider) {
|
|
4497
|
+
super(`The '${provider}' state provider keys state by workspace name. Set "name" in gkm.config.ts.`);
|
|
4498
|
+
this.provider = provider;
|
|
4499
|
+
this.name = "StateStoreNeedsWorkspaceName";
|
|
4500
|
+
}
|
|
4501
|
+
};
|
|
4502
|
+
var UnknownStateProvider = class extends Error {
|
|
4503
|
+
config;
|
|
4504
|
+
constructor(config) {
|
|
4505
|
+
super(`Unknown state provider ${JSON.stringify(config)}. Use 'local', 'ssm', 's3', or an object implementing StateStore or StateProvider.`);
|
|
4506
|
+
this.config = config;
|
|
4507
|
+
this.name = "UnknownStateProvider";
|
|
4508
|
+
}
|
|
4509
|
+
};
|
|
4510
|
+
/** A lock or write that could not get at the state backend in time. */
|
|
4511
|
+
var StateStoreBusy = class extends Error {
|
|
4512
|
+
stage;
|
|
4513
|
+
location;
|
|
4514
|
+
constructor(stage, location) {
|
|
4515
|
+
super(`Timed out waiting to write deploy state for stage '${stage}' (${location}). If no other gkm process is running, remove ${location}.`);
|
|
4516
|
+
this.stage = stage;
|
|
4517
|
+
this.location = location;
|
|
4518
|
+
this.name = "StateStoreBusy";
|
|
4519
|
+
}
|
|
4520
|
+
};
|
|
4521
|
+
function decodeDocument(stage, body, location) {
|
|
4522
|
+
let parsed;
|
|
4523
|
+
try {
|
|
4524
|
+
parsed = JSON.parse(body);
|
|
4525
|
+
} catch (error) {
|
|
4526
|
+
throw new StateUnreadable(stage, location, error);
|
|
4527
|
+
}
|
|
4528
|
+
if (typeof parsed !== "object" || parsed === null) throw new StateUnreadable(stage, location, parsed);
|
|
4529
|
+
const schemaVersion = parsed.schemaVersion;
|
|
4530
|
+
if (schemaVersion === void 0) return {
|
|
4531
|
+
schemaVersion: 1,
|
|
4532
|
+
state: parsed
|
|
4533
|
+
};
|
|
4534
|
+
if (schemaVersion === 2) return {
|
|
4535
|
+
schemaVersion: 2,
|
|
4536
|
+
document: parsed
|
|
4537
|
+
};
|
|
4538
|
+
throw new StateSchemaTooNew(stage, schemaVersion);
|
|
4539
|
+
}
|
|
4540
|
+
function encodeDocument(document) {
|
|
4541
|
+
return JSON.stringify(document, null, 2);
|
|
4542
|
+
}
|
|
4543
|
+
/**
|
|
4544
|
+
* v1 state as a v2 document. The ids v1 kept are seeded as `ready` records,
|
|
4545
|
+
* so the first journaled deploy after an upgrade adopts them rather than
|
|
4546
|
+
* treating them as unknown.
|
|
4547
|
+
*/
|
|
4548
|
+
function migrateV1(stage, state) {
|
|
4549
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
4550
|
+
const resources = {};
|
|
4551
|
+
const seed = (type, name, id) => {
|
|
4552
|
+
if (!id) return;
|
|
4553
|
+
const key = name ? `${type}:${name}` : type;
|
|
4554
|
+
resources[key] = {
|
|
4555
|
+
key,
|
|
4556
|
+
type,
|
|
4557
|
+
id,
|
|
4558
|
+
status: "ready",
|
|
4559
|
+
updatedAt: now
|
|
4560
|
+
};
|
|
4561
|
+
};
|
|
4562
|
+
seed("project", null, state.projectId);
|
|
4563
|
+
seed("environment", null, state.environmentId);
|
|
4564
|
+
for (const [app, id] of Object.entries(state.applications ?? {})) seed("application", app, id);
|
|
4565
|
+
seed("postgres", null, state.services?.postgresId);
|
|
4566
|
+
seed("redis", null, state.services?.redisId);
|
|
4567
|
+
seed("backup-destination", null, state.backups?.destinationId);
|
|
4568
|
+
return {
|
|
4569
|
+
schemaVersion: 2,
|
|
4570
|
+
stage,
|
|
4571
|
+
serial: 1,
|
|
4572
|
+
state,
|
|
4573
|
+
resources,
|
|
4574
|
+
updatedAt: now
|
|
4575
|
+
};
|
|
4576
|
+
}
|
|
4577
|
+
function newLockHolder(options) {
|
|
4578
|
+
let owner = "unknown";
|
|
4579
|
+
try {
|
|
4580
|
+
owner = (0, node_os.userInfo)().username;
|
|
4581
|
+
} catch {}
|
|
4582
|
+
return {
|
|
4583
|
+
id: (0, node_crypto.randomUUID)(),
|
|
4584
|
+
owner,
|
|
4585
|
+
host: (0, node_os.hostname)(),
|
|
4586
|
+
pid: process.pid,
|
|
4587
|
+
acquiredAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
4588
|
+
operation: options?.operation
|
|
4589
|
+
};
|
|
4590
|
+
}
|
|
4591
|
+
function contentVersion(body) {
|
|
4592
|
+
return (0, node_crypto.createHash)("sha256").update(body).digest("hex").slice(0, 16);
|
|
4593
|
+
}
|
|
4594
|
+
/**
|
|
4595
|
+
* A store over a backend that can read and conditionally write one blob per
|
|
4596
|
+
* stage, and create a lock object only if it does not exist. Migration and
|
|
4597
|
+
* resource records are the same for every such backend, so they live here.
|
|
4598
|
+
*/
|
|
4599
|
+
var DocumentStateStore = class {
|
|
4600
|
+
async lock(stage, options) {
|
|
4601
|
+
const holder = newLockHolder(options);
|
|
4602
|
+
await this.createLock(stage, holder);
|
|
4603
|
+
let released = false;
|
|
4604
|
+
return {
|
|
4605
|
+
stage,
|
|
4606
|
+
holder,
|
|
4607
|
+
release: async () => {
|
|
4608
|
+
if (released) return;
|
|
4609
|
+
released = true;
|
|
4610
|
+
await this.removeLock(stage, holder);
|
|
4611
|
+
}
|
|
4612
|
+
};
|
|
4613
|
+
}
|
|
4614
|
+
async forceUnlock(stage) {
|
|
4615
|
+
const holder = await this.readLock(stage);
|
|
4616
|
+
await this.removeLock(stage, null);
|
|
4617
|
+
return holder;
|
|
4618
|
+
}
|
|
4619
|
+
async read(stage) {
|
|
4620
|
+
const current = await this.readDocument(stage);
|
|
4621
|
+
if (!current) return null;
|
|
4622
|
+
return {
|
|
4623
|
+
state: current.document.state,
|
|
4624
|
+
resources: current.document.resources,
|
|
4625
|
+
version: current.version
|
|
4626
|
+
};
|
|
4627
|
+
}
|
|
4628
|
+
async write(stage, state, options) {
|
|
4629
|
+
if (options.expectedVersion === null) {
|
|
4630
|
+
const document = {
|
|
4631
|
+
schemaVersion: 2,
|
|
4632
|
+
stage,
|
|
4633
|
+
serial: 1,
|
|
4634
|
+
state,
|
|
4635
|
+
resources: {},
|
|
4636
|
+
updatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
4637
|
+
};
|
|
4638
|
+
return this.writeRaw(stage, encodeDocument(document), null);
|
|
4639
|
+
}
|
|
4640
|
+
return this.mutate(stage, options.expectedVersion, (document) => ({
|
|
4641
|
+
...document,
|
|
4642
|
+
state
|
|
4643
|
+
}));
|
|
4644
|
+
}
|
|
4645
|
+
async putResource(stage, record, options) {
|
|
4646
|
+
return this.mutate(stage, options?.expectedVersion, (document) => ({
|
|
4647
|
+
...document,
|
|
4648
|
+
resources: {
|
|
4649
|
+
...document.resources,
|
|
4650
|
+
[record.key]: {
|
|
4651
|
+
...record,
|
|
4652
|
+
updatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
4653
|
+
}
|
|
4654
|
+
}
|
|
4655
|
+
}));
|
|
4656
|
+
}
|
|
4657
|
+
async deleteResource(stage, key, options) {
|
|
4658
|
+
return this.mutate(stage, options?.expectedVersion, (document) => {
|
|
4659
|
+
const { [key]: _removed, ...resources } = document.resources;
|
|
4660
|
+
return {
|
|
4661
|
+
...document,
|
|
4662
|
+
resources
|
|
4663
|
+
};
|
|
4664
|
+
});
|
|
4665
|
+
}
|
|
4666
|
+
async mutate(stage, expectedVersion, change) {
|
|
4667
|
+
const current = await this.readDocument(stage);
|
|
4668
|
+
if (!current) {
|
|
4669
|
+
if (expectedVersion !== void 0) throw new StateVersionConflict(stage, expectedVersion, null);
|
|
4670
|
+
throw new StageStateMissing(stage);
|
|
4671
|
+
}
|
|
4672
|
+
if (expectedVersion !== void 0 && current.version !== expectedVersion) throw new StateVersionConflict(stage, expectedVersion, current.version);
|
|
4673
|
+
const next = change(current.document);
|
|
4674
|
+
next.serial = current.document.serial + 1;
|
|
4675
|
+
next.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
4676
|
+
return this.writeRaw(stage, encodeDocument(next), current.version);
|
|
4677
|
+
}
|
|
4678
|
+
/**
|
|
4679
|
+
* Reads the stage's document, migrating v1 in place on the way. The
|
|
4680
|
+
* migration is itself a conditional write: if another run migrated (or
|
|
4681
|
+
* wrote) first, its result is read instead.
|
|
4682
|
+
*/
|
|
4683
|
+
async readDocument(stage, attempt = 0) {
|
|
4684
|
+
const raw = await this.readRaw(stage);
|
|
4685
|
+
if (!raw) return null;
|
|
4686
|
+
const decoded = decodeDocument(stage, raw.body, this.location(stage));
|
|
4687
|
+
if (decoded.schemaVersion === 2) return {
|
|
4688
|
+
document: decoded.document,
|
|
4689
|
+
version: raw.version
|
|
4690
|
+
};
|
|
4691
|
+
await this.writeV1Backup(stage, raw.body);
|
|
4692
|
+
const document = migrateV1(stage, decoded.state);
|
|
4693
|
+
try {
|
|
4694
|
+
return {
|
|
4695
|
+
document,
|
|
4696
|
+
version: await this.writeRaw(stage, encodeDocument(document), raw.version)
|
|
4697
|
+
};
|
|
4698
|
+
} catch (error) {
|
|
4699
|
+
if (error instanceof StateVersionConflict && attempt < 2) return this.readDocument(stage, attempt + 1);
|
|
4700
|
+
throw error;
|
|
4701
|
+
}
|
|
4702
|
+
}
|
|
4703
|
+
};
|
|
4704
|
+
function isStateStore(value) {
|
|
4705
|
+
return typeof value === "object" && value !== null && typeof value.lock === "function" && typeof value.read === "function" && typeof value.write === "function" && typeof value.putResource === "function";
|
|
4706
|
+
}
|
|
4707
|
+
/**
|
|
4708
|
+
* The store for a workspace's `state` config:
|
|
4709
|
+
*
|
|
4710
|
+
* - none / `'local'`: `LocalStateStore` under `.gkm/`
|
|
4711
|
+
* - `'ssm'`: `SSMStateStore`, with no local cache — SSM is read directly, so
|
|
4712
|
+
* a stale local copy can never win over it
|
|
4713
|
+
* - `'s3'`: `S3StateStore`
|
|
4714
|
+
* - a `StateStore` object: used as is
|
|
4715
|
+
* - a `StateProvider` object: wrapped by `LegacyStateStore`
|
|
4716
|
+
*/
|
|
4717
|
+
async function createStateStore(options) {
|
|
4718
|
+
const { config, workspaceRoot, workspaceName } = options;
|
|
4719
|
+
if (!config || config.provider === "local") {
|
|
4720
|
+
const { LocalStateStore } = await Promise.resolve().then(() => require("./LocalStateStore-Dpuqka1N.cjs")).then((n) => n.LocalStateStore_exports);
|
|
4721
|
+
return new LocalStateStore(workspaceRoot);
|
|
4722
|
+
}
|
|
4723
|
+
if (isStateStore(config.provider)) return config.provider;
|
|
4724
|
+
if (isStateProvider(config.provider)) {
|
|
4725
|
+
const { LegacyStateStore } = await Promise.resolve().then(() => require("./LegacyStateStore-CidlPT0N.cjs"));
|
|
4726
|
+
return new LegacyStateStore(config.provider, { warn: options.warn });
|
|
4727
|
+
}
|
|
4728
|
+
if (config.provider === "ssm") {
|
|
4729
|
+
if (!workspaceName) throw new StateStoreNeedsWorkspaceName("ssm");
|
|
4730
|
+
const { SSMStateStore } = await Promise.resolve().then(() => require("./SSMStateStore-DwaGprVX.cjs"));
|
|
4731
|
+
const ssm = config;
|
|
4732
|
+
return SSMStateStore.create({
|
|
4733
|
+
workspaceName,
|
|
4734
|
+
region: ssm.region,
|
|
4735
|
+
profile: ssm.profile
|
|
4736
|
+
});
|
|
4737
|
+
}
|
|
4738
|
+
if (config.provider === "s3") {
|
|
4739
|
+
if (!workspaceName) throw new StateStoreNeedsWorkspaceName("s3");
|
|
4740
|
+
const { S3StateStore } = await Promise.resolve().then(() => require("./S3StateStore-cnm813Xn.cjs"));
|
|
4741
|
+
const s3 = config;
|
|
4742
|
+
return S3StateStore.create({
|
|
4743
|
+
workspaceName,
|
|
4744
|
+
bucket: s3.bucket,
|
|
4745
|
+
prefix: s3.prefix,
|
|
4746
|
+
region: s3.region,
|
|
4747
|
+
profile: s3.profile
|
|
4748
|
+
});
|
|
4749
|
+
}
|
|
4750
|
+
throw new UnknownStateProvider(config);
|
|
4751
|
+
}
|
|
4752
|
+
//#endregion
|
|
4753
|
+
//#region src/deploy/domain.ts
|
|
4754
|
+
/** A stage was deployed that `deploy.domains` gives no domain. */
|
|
4755
|
+
var NoDomainForStage = class extends Error {
|
|
4756
|
+
stage;
|
|
4757
|
+
constructor(stage) {
|
|
4758
|
+
super(`No domain for stage "${stage}". Add it to gkm.config.ts: deploy: { domains: { ${stage}: 'example.com' } }.`);
|
|
4759
|
+
this.stage = stage;
|
|
4760
|
+
this.name = "NoDomainForStage";
|
|
4761
|
+
}
|
|
4762
|
+
};
|
|
4763
|
+
/**
|
|
4764
|
+
* The hostname an app answers on for a stage.
|
|
4765
|
+
*
|
|
4766
|
+
* 1. An explicit `app.domain` — a string, or one per stage — wins.
|
|
4767
|
+
* 2. Otherwise from the stage's base domain in `deploy.domains`: the root site
|
|
4768
|
+
* answers on the domain itself, and every other app on
|
|
4769
|
+
* `{subdomain}.{domain}` — its declaration's `subdomain`, or its own name.
|
|
4770
|
+
*
|
|
4771
|
+
* @throws {NoDomainForStage} when `deploy.domains` names no domain for it
|
|
4772
|
+
*/
|
|
4773
|
+
function resolveHost(appName, app, stage, domains, isMainFrontend) {
|
|
4774
|
+
if (app.domain) {
|
|
4775
|
+
if (typeof app.domain === "string") return app.domain;
|
|
4776
|
+
if (app.domain[stage]) return app.domain[stage];
|
|
4777
|
+
}
|
|
4778
|
+
const baseDomain = domains?.[stage];
|
|
4779
|
+
if (!baseDomain) throw new NoDomainForStage(stage);
|
|
4780
|
+
if (isMainFrontend) return baseDomain;
|
|
4781
|
+
return `${app.subdomain ?? appName}.${baseDomain}`;
|
|
4782
|
+
}
|
|
4783
|
+
/**
|
|
4784
|
+
* Whether this app is the site the base domain points at — see `rootSite`,
|
|
4785
|
+
* which the local edge asks too.
|
|
4786
|
+
*
|
|
4787
|
+
* @throws {AmbiguousRootSite} when several sites could be the root and none says it is
|
|
4788
|
+
*/
|
|
4789
|
+
function isMainFrontendApp(appName, app, allApps) {
|
|
4790
|
+
if (app.type !== "web") return false;
|
|
4791
|
+
return rootSite(Object.entries(allApps).filter(([, a]) => a.type === "web").map(([name, a]) => ({
|
|
4792
|
+
name,
|
|
4793
|
+
root: a.root
|
|
4794
|
+
}))) === appName;
|
|
4795
|
+
}
|
|
4796
|
+
/**
|
|
4797
|
+
* Get public URL arg names from app dependencies.
|
|
4798
|
+
*
|
|
4799
|
+
* @param app - The web/mobile app configuration
|
|
4800
|
+
* @returns Array of arg names like 'NEXT_PUBLIC_API_URL', or [] for frameworks
|
|
4801
|
+
* without a public prefix (e.g. Remix).
|
|
4802
|
+
*/
|
|
4803
|
+
function getPublicUrlArgNames(app) {
|
|
4804
|
+
const prefix = require_workspace.getPublicEnvPrefix(app.framework);
|
|
4805
|
+
if (!prefix) return [];
|
|
4806
|
+
return app.dependencies.map((dep) => `${prefix}${dep.toUpperCase()}_URL`);
|
|
4807
|
+
}
|
|
4808
|
+
//#endregion
|
|
4809
|
+
//#region src/docker/imageRef.ts
|
|
4810
|
+
/**
|
|
4811
|
+
* An image reference, checked before it reaches `docker`.
|
|
4812
|
+
*
|
|
4813
|
+
* The parts come from the workspace — a registry in `gkm.config.ts`, an image
|
|
4814
|
+
* name from the project, a tag from `--tag` — and an argument array already
|
|
4815
|
+
* keeps them out of a shell. What it cannot do is stop `docker` reading a value
|
|
4816
|
+
* that starts with `-` as one of its own flags, so a ref is held to Docker's
|
|
4817
|
+
* own grammar (distribution/reference) and anything else is refused by name
|
|
4818
|
+
* before a build starts.
|
|
4819
|
+
*/
|
|
4820
|
+
const DOMAIN_COMPONENT = "(?:[a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9-]*[a-zA-Z0-9])";
|
|
4821
|
+
const DOMAIN = `${DOMAIN_COMPONENT}(?:\\.${DOMAIN_COMPONENT})*(?::[0-9]+)?`;
|
|
4822
|
+
const PATH_COMPONENT = "[a-z0-9]+(?:(?:[._]|__|-+)[a-z0-9]+)*";
|
|
4823
|
+
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,})?$`);
|
|
4824
|
+
/** Docker refuses a repository name longer than this. */
|
|
4825
|
+
const MAX_NAME_LENGTH = 255;
|
|
4826
|
+
/** A value that is not an image reference was about to be built or pushed. */
|
|
4827
|
+
var ImageRefInvalid = class extends Error {
|
|
4828
|
+
ref;
|
|
4829
|
+
constructor(ref) {
|
|
4830
|
+
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.`);
|
|
4831
|
+
this.ref = ref;
|
|
4832
|
+
this.name = "ImageRefInvalid";
|
|
4833
|
+
}
|
|
4834
|
+
};
|
|
4835
|
+
/** Throw {@link ImageRefInvalid} unless `ref` is a Docker image reference. */
|
|
4836
|
+
function validateImageRef(ref) {
|
|
4837
|
+
const name = ref.split("@")[0].replace(/:[\w][\w.-]*$/, "");
|
|
4838
|
+
if (!REFERENCE.test(ref) || name.length > MAX_NAME_LENGTH) throw new ImageRefInvalid(ref);
|
|
4839
|
+
return ref;
|
|
4840
|
+
}
|
|
4841
|
+
//#endregion
|
|
4842
|
+
//#region src/docker/templates.ts
|
|
4843
|
+
const LOCKFILES = [
|
|
4844
|
+
["pnpm-lock.yaml", "pnpm"],
|
|
4845
|
+
["bun.lockb", "bun"],
|
|
4846
|
+
["yarn.lock", "yarn"],
|
|
4847
|
+
["package-lock.json", "npm"]
|
|
4848
|
+
];
|
|
4849
|
+
/**
|
|
4850
|
+
* Detect package manager from lockfiles
|
|
4851
|
+
* Walks up the directory tree to find lockfile (for monorepos)
|
|
4852
|
+
*/
|
|
4853
|
+
function detectPackageManager(cwd = process.cwd()) {
|
|
4854
|
+
let dir = cwd;
|
|
4855
|
+
const root = (0, node_path.parse)(dir).root;
|
|
4856
|
+
while (dir !== root) {
|
|
4857
|
+
for (const [lockfile, pm] of LOCKFILES) if ((0, node_fs.existsSync)((0, node_path.join)(dir, lockfile))) return pm;
|
|
4858
|
+
dir = (0, node_path.dirname)(dir);
|
|
4859
|
+
}
|
|
4860
|
+
for (const [lockfile, pm] of LOCKFILES) if ((0, node_fs.existsSync)((0, node_path.join)(root, lockfile))) return pm;
|
|
4861
|
+
return "pnpm";
|
|
4862
|
+
}
|
|
4863
|
+
/**
|
|
4864
|
+
* Find the lockfile path by walking up the directory tree
|
|
4865
|
+
* Returns the full path to the lockfile, or null if not found
|
|
4866
|
+
*/
|
|
4867
|
+
function findLockfilePath(cwd = process.cwd()) {
|
|
4868
|
+
let dir = cwd;
|
|
4869
|
+
const root = (0, node_path.parse)(dir).root;
|
|
4870
|
+
while (dir !== root) {
|
|
4871
|
+
for (const [lockfile] of LOCKFILES) {
|
|
4872
|
+
const lockfilePath = (0, node_path.join)(dir, lockfile);
|
|
4873
|
+
if ((0, node_fs.existsSync)(lockfilePath)) return lockfilePath;
|
|
4874
|
+
}
|
|
4875
|
+
dir = (0, node_path.dirname)(dir);
|
|
4876
|
+
}
|
|
4877
|
+
for (const [lockfile] of LOCKFILES) {
|
|
4878
|
+
const lockfilePath = (0, node_path.join)(root, lockfile);
|
|
4879
|
+
if ((0, node_fs.existsSync)(lockfilePath)) return lockfilePath;
|
|
4880
|
+
}
|
|
4881
|
+
return null;
|
|
4882
|
+
}
|
|
4883
|
+
/**
|
|
4884
|
+
* Check if we're in a monorepo (lockfile is in a parent directory)
|
|
4885
|
+
*/
|
|
4886
|
+
function isMonorepo(cwd = process.cwd()) {
|
|
4887
|
+
const lockfilePath = findLockfilePath(cwd);
|
|
4888
|
+
if (!lockfilePath) return false;
|
|
4889
|
+
return (0, node_path.dirname)(lockfilePath) !== cwd;
|
|
4890
|
+
}
|
|
4891
|
+
/**
|
|
4892
|
+
* Check if turbo.json exists (walks up directory tree)
|
|
4893
|
+
*/
|
|
4894
|
+
function hasTurboConfig(cwd = process.cwd()) {
|
|
4895
|
+
let dir = cwd;
|
|
4896
|
+
const root = (0, node_path.parse)(dir).root;
|
|
4897
|
+
while (dir !== root) {
|
|
4898
|
+
if ((0, node_fs.existsSync)((0, node_path.join)(dir, "turbo.json"))) return true;
|
|
4899
|
+
dir = (0, node_path.dirname)(dir);
|
|
4900
|
+
}
|
|
4901
|
+
return (0, node_fs.existsSync)((0, node_path.join)(root, "turbo.json"));
|
|
4902
|
+
}
|
|
4903
|
+
/**
|
|
4904
|
+
* Get install command for turbo builds (without frozen lockfile)
|
|
4905
|
+
* Turbo prune creates a subset that may not perfectly match the lockfile
|
|
4906
|
+
*/
|
|
4907
|
+
function getTurboInstallCmd(pm) {
|
|
4908
|
+
return {
|
|
4909
|
+
pnpm: "pnpm install",
|
|
4910
|
+
npm: "npm install",
|
|
4911
|
+
yarn: "yarn install",
|
|
4912
|
+
bun: "bun install"
|
|
4913
|
+
}[pm];
|
|
4914
|
+
}
|
|
4915
|
+
/**
|
|
4916
|
+
* Get package manager specific commands and paths
|
|
4917
|
+
*/
|
|
4918
|
+
function getPmConfig(pm) {
|
|
4919
|
+
return {
|
|
4920
|
+
pnpm: {
|
|
4921
|
+
install: "corepack enable && corepack prepare pnpm@latest --activate",
|
|
4922
|
+
lockfile: "pnpm-lock.yaml",
|
|
4923
|
+
fetch: "pnpm fetch",
|
|
4924
|
+
installCmd: "pnpm install --frozen-lockfile --offline",
|
|
4925
|
+
cacheTarget: "/root/.local/share/pnpm/store",
|
|
4926
|
+
cacheId: "pnpm",
|
|
4927
|
+
run: "pnpm",
|
|
4928
|
+
exec: "pnpm exec",
|
|
4929
|
+
dlx: "pnpm dlx",
|
|
4930
|
+
addGlobal: "pnpm add -g"
|
|
4931
|
+
},
|
|
4932
|
+
npm: {
|
|
4933
|
+
install: "",
|
|
4934
|
+
lockfile: "package-lock.json",
|
|
4935
|
+
fetch: "",
|
|
4936
|
+
installCmd: "npm ci",
|
|
4937
|
+
cacheTarget: "/root/.npm",
|
|
4938
|
+
cacheId: "npm",
|
|
4939
|
+
run: "npm run",
|
|
4940
|
+
exec: "npx",
|
|
4941
|
+
dlx: "npx",
|
|
4942
|
+
addGlobal: "npm install -g"
|
|
4943
|
+
},
|
|
4944
|
+
yarn: {
|
|
4945
|
+
install: "corepack enable && corepack prepare yarn@stable --activate",
|
|
4946
|
+
lockfile: "yarn.lock",
|
|
4947
|
+
fetch: "",
|
|
4948
|
+
installCmd: "yarn install --frozen-lockfile",
|
|
4949
|
+
cacheTarget: "/root/.yarn/cache",
|
|
4950
|
+
cacheId: "yarn",
|
|
4951
|
+
run: "yarn",
|
|
4952
|
+
exec: "yarn exec",
|
|
4953
|
+
dlx: "yarn dlx",
|
|
4954
|
+
addGlobal: "yarn global add"
|
|
4955
|
+
},
|
|
4956
|
+
bun: {
|
|
4957
|
+
install: "npm install -g bun",
|
|
4958
|
+
lockfile: "bun.lockb",
|
|
4959
|
+
fetch: "",
|
|
4960
|
+
installCmd: "bun install --frozen-lockfile",
|
|
4961
|
+
cacheTarget: "/root/.bun/install/cache",
|
|
4962
|
+
cacheId: "bun",
|
|
4963
|
+
run: "bun run",
|
|
4964
|
+
exec: "bunx",
|
|
4965
|
+
dlx: "bunx",
|
|
4966
|
+
addGlobal: "bun add -g"
|
|
4967
|
+
}
|
|
4968
|
+
}[pm];
|
|
4969
|
+
}
|
|
4970
|
+
/**
|
|
4971
|
+
* Generate a multi-stage Dockerfile for building from source
|
|
4972
|
+
* Optimized for build speed with:
|
|
4973
|
+
* - BuildKit cache mounts for package manager store
|
|
4974
|
+
* - pnpm fetch for better layer caching (when using pnpm)
|
|
4975
|
+
* - Optional turbo prune for monorepos
|
|
4976
|
+
*/
|
|
4977
|
+
function generateMultiStageDockerfile(options) {
|
|
4978
|
+
const { baseImage, port, healthCheckPath, turbo, turboPackage, packageManager } = options;
|
|
4979
|
+
if (turbo) return generateTurboDockerfile({
|
|
4980
|
+
...options,
|
|
4981
|
+
turboPackage: turboPackage ?? "api"
|
|
4982
|
+
});
|
|
4983
|
+
const pm = getPmConfig(packageManager);
|
|
4984
|
+
return `# syntax=docker/dockerfile:1
|
|
4985
|
+
# Stage 1: Dependencies
|
|
4986
|
+
FROM ${baseImage} AS deps
|
|
4987
|
+
|
|
4988
|
+
WORKDIR /app
|
|
4989
|
+
${pm.install ? `\n# Install ${packageManager}\nRUN ${pm.install}\n` : ""}
|
|
4990
|
+
${packageManager === "pnpm" ? `# Copy lockfile first for better caching
|
|
4991
|
+
COPY ${pm.lockfile} ./
|
|
4992
|
+
|
|
4993
|
+
# Fetch dependencies (downloads to virtual store, cached separately)
|
|
4994
|
+
RUN --mount=type=cache,id=${pm.cacheId},target=${pm.cacheTarget} \\
|
|
4995
|
+
${pm.fetch}
|
|
4996
|
+
|
|
4997
|
+
# Copy package.json after fetch
|
|
4998
|
+
COPY package.json ./
|
|
4999
|
+
|
|
5000
|
+
# Install from cache (fast - no network needed)
|
|
5001
|
+
RUN --mount=type=cache,id=${pm.cacheId},target=${pm.cacheTarget} \\
|
|
5002
|
+
${pm.installCmd}` : `# Copy package files
|
|
5003
|
+
COPY package.json ${pm.lockfile} ./
|
|
5004
|
+
|
|
5005
|
+
# Install dependencies with cache
|
|
5006
|
+
RUN --mount=type=cache,id=${pm.cacheId},target=${pm.cacheTarget} \\
|
|
5007
|
+
${pm.installCmd}`}
|
|
5008
|
+
|
|
5009
|
+
# Stage 2: Build
|
|
5010
|
+
FROM deps AS builder
|
|
5011
|
+
|
|
5012
|
+
WORKDIR /app
|
|
5013
|
+
|
|
5014
|
+
# Copy source (deps already installed)
|
|
5015
|
+
COPY . .
|
|
5016
|
+
|
|
5017
|
+
# Build any workspace packages the app depends on.
|
|
5018
|
+
#
|
|
5019
|
+
# A no-op for a project whose dependencies come from a registry, and required
|
|
5020
|
+
# for one whose CLI is a sibling package: \`turbo prune\` honours .gitignore, so a
|
|
5021
|
+
# workspace package's \`dist\` never reaches the image and its bin has nothing to
|
|
5022
|
+
# import. \`--if-present\` is what makes the same Dockerfile correct for both.
|
|
5023
|
+
RUN ${pm.run} --if-present build
|
|
5024
|
+
|
|
5025
|
+
# Build production server using gkm
|
|
5026
|
+
RUN ${pm.exec} gkm build --provider server --production
|
|
5027
|
+
|
|
5028
|
+
# Stage 3: Production
|
|
5029
|
+
FROM ${baseImage} AS runner
|
|
5030
|
+
|
|
5031
|
+
WORKDIR /app
|
|
5032
|
+
|
|
5033
|
+
# Install tini for proper signal handling as PID 1
|
|
5034
|
+
RUN apk add --no-cache tini
|
|
5035
|
+
|
|
5036
|
+
# Create non-root user
|
|
5037
|
+
RUN addgroup --system --gid 1001 nodejs && \\
|
|
5038
|
+
adduser --system --uid 1001 hono
|
|
5039
|
+
|
|
5040
|
+
# Copy bundled server
|
|
5041
|
+
COPY --from=builder --chown=hono:nodejs /app/.gkm/server/dist/server.mjs ./
|
|
5042
|
+
|
|
5043
|
+
# Environment
|
|
5044
|
+
ENV NODE_ENV=production
|
|
5045
|
+
ENV PORT=${port}
|
|
5046
|
+
|
|
5047
|
+
# Health check
|
|
5048
|
+
HEALTHCHECK --interval=30s --timeout=10s --start-period=30s --retries=3 \\
|
|
5049
|
+
CMD wget -qO- http://localhost:${port}${healthCheckPath} > /dev/null 2>&1 || exit 1
|
|
5050
|
+
|
|
5051
|
+
# Switch to non-root user
|
|
5052
|
+
USER hono
|
|
5053
|
+
|
|
5054
|
+
EXPOSE ${port}
|
|
5055
|
+
|
|
5056
|
+
# Use tini as entrypoint to handle PID 1 responsibilities
|
|
5057
|
+
ENTRYPOINT ["/sbin/tini", "--"]
|
|
5058
|
+
CMD ["node", "server.mjs"]
|
|
5059
|
+
`;
|
|
5060
|
+
}
|
|
5061
|
+
/**
|
|
5062
|
+
* Generate a Dockerfile optimized for Turbo monorepos
|
|
5063
|
+
* Uses turbo prune to create minimal Docker context
|
|
5064
|
+
*/
|
|
5065
|
+
function generateTurboDockerfile(options) {
|
|
5066
|
+
const { baseImage, port, healthCheckPath, turboPackage, packageManager } = options;
|
|
5067
|
+
const pm = getPmConfig(packageManager);
|
|
5068
|
+
const installPm = pm.install ? `RUN ${pm.install}` : "";
|
|
5069
|
+
const turboInstallCmd = getTurboInstallCmd(packageManager);
|
|
5070
|
+
return `# syntax=docker/dockerfile:1
|
|
5071
|
+
# Stage 1: Prune monorepo
|
|
5072
|
+
FROM ${baseImage} AS pruner
|
|
5073
|
+
|
|
5074
|
+
WORKDIR /app
|
|
5075
|
+
|
|
5076
|
+
${installPm}
|
|
5077
|
+
|
|
5078
|
+
COPY . .
|
|
5079
|
+
|
|
5080
|
+
# Prune to only include necessary packages
|
|
5081
|
+
RUN ${packageManager === "pnpm" ? "pnpm dlx turbo" : "npx turbo"} prune ${turboPackage} --docker
|
|
5082
|
+
|
|
5083
|
+
# Stage 2: Install dependencies
|
|
5084
|
+
FROM ${baseImage} AS deps
|
|
5085
|
+
|
|
5086
|
+
WORKDIR /app
|
|
5087
|
+
|
|
5088
|
+
${installPm}
|
|
5089
|
+
|
|
5090
|
+
# Copy pruned lockfile and package.jsons
|
|
5091
|
+
COPY --from=pruner /app/out/${pm.lockfile} ./
|
|
5092
|
+
COPY --from=pruner /app/out/json/ ./
|
|
5093
|
+
|
|
5094
|
+
# Install dependencies (no frozen-lockfile since turbo prune creates a subset)
|
|
5095
|
+
RUN --mount=type=cache,id=${pm.cacheId},target=${pm.cacheTarget} \\
|
|
5096
|
+
${turboInstallCmd}
|
|
5097
|
+
|
|
5098
|
+
# Stage 3: Build
|
|
5099
|
+
FROM deps AS builder
|
|
5100
|
+
|
|
5101
|
+
WORKDIR /app
|
|
5102
|
+
|
|
5103
|
+
# Copy pruned source
|
|
5104
|
+
COPY --from=pruner /app/out/full/ ./
|
|
5105
|
+
|
|
5106
|
+
# Build any workspace packages the app depends on.
|
|
5107
|
+
#
|
|
5108
|
+
# A no-op for a project whose dependencies come from a registry, and required
|
|
5109
|
+
# for one whose CLI is a sibling package: \`turbo prune\` honours .gitignore, so a
|
|
5110
|
+
# workspace package's \`dist\` never reaches the image and its bin has nothing to
|
|
5111
|
+
# import. \`--if-present\` is what makes the same Dockerfile correct for both.
|
|
5112
|
+
RUN ${pm.run} --if-present build
|
|
5113
|
+
|
|
5114
|
+
# Build production server using gkm
|
|
5115
|
+
RUN ${pm.exec} gkm build --provider server --production
|
|
5116
|
+
|
|
5117
|
+
# Stage 4: Production
|
|
5118
|
+
FROM ${baseImage} AS runner
|
|
5119
|
+
|
|
5120
|
+
WORKDIR /app
|
|
5121
|
+
|
|
5122
|
+
RUN apk add --no-cache tini
|
|
5123
|
+
|
|
5124
|
+
RUN addgroup --system --gid 1001 nodejs && \\
|
|
5125
|
+
adduser --system --uid 1001 hono
|
|
5126
|
+
|
|
5127
|
+
COPY --from=builder --chown=hono:nodejs /app/.gkm/server/dist/server.mjs ./
|
|
5128
|
+
|
|
5129
|
+
ENV NODE_ENV=production
|
|
5130
|
+
ENV PORT=${port}
|
|
5131
|
+
|
|
5132
|
+
HEALTHCHECK --interval=30s --timeout=10s --start-period=30s --retries=3 \\
|
|
5133
|
+
CMD wget -qO- http://localhost:${port}${healthCheckPath} > /dev/null 2>&1 || exit 1
|
|
5134
|
+
|
|
5135
|
+
USER hono
|
|
5136
|
+
|
|
5137
|
+
EXPOSE ${port}
|
|
5138
|
+
|
|
5139
|
+
ENTRYPOINT ["/sbin/tini", "--"]
|
|
5140
|
+
CMD ["node", "server.mjs"]
|
|
5141
|
+
`;
|
|
5142
|
+
}
|
|
5143
|
+
/**
|
|
5144
|
+
* Generate a slim Dockerfile for pre-built bundles
|
|
5145
|
+
*/
|
|
5146
|
+
function generateSlimDockerfile(options) {
|
|
5147
|
+
const { baseImage, port, healthCheckPath } = options;
|
|
5148
|
+
return `# Slim Dockerfile for pre-built production bundle
|
|
5149
|
+
FROM ${baseImage}
|
|
5150
|
+
|
|
5151
|
+
WORKDIR /app
|
|
5152
|
+
|
|
5153
|
+
# Install tini for proper signal handling as PID 1
|
|
5154
|
+
# Handles SIGTERM propagation and zombie process reaping
|
|
5155
|
+
RUN apk add --no-cache tini
|
|
5156
|
+
|
|
5157
|
+
# Create non-root user
|
|
5158
|
+
RUN addgroup --system --gid 1001 nodejs && \\
|
|
5159
|
+
adduser --system --uid 1001 hono
|
|
5160
|
+
|
|
5161
|
+
# Copy pre-built bundle
|
|
5162
|
+
COPY .gkm/server/dist/server.mjs ./
|
|
5163
|
+
|
|
5164
|
+
# Environment
|
|
5165
|
+
ENV NODE_ENV=production
|
|
5166
|
+
ENV PORT=${port}
|
|
5167
|
+
|
|
5168
|
+
# Health check
|
|
5169
|
+
HEALTHCHECK --interval=30s --timeout=10s --start-period=30s --retries=3 \\
|
|
5170
|
+
CMD wget -qO- http://localhost:${port}${healthCheckPath} > /dev/null 2>&1 || exit 1
|
|
5171
|
+
|
|
5172
|
+
# Switch to non-root user
|
|
5173
|
+
USER hono
|
|
5174
|
+
|
|
5175
|
+
EXPOSE ${port}
|
|
5176
|
+
|
|
5177
|
+
# Use tini as entrypoint to handle PID 1 responsibilities
|
|
5178
|
+
ENTRYPOINT ["/sbin/tini", "--"]
|
|
5179
|
+
CMD ["node", "server.mjs"]
|
|
5180
|
+
`;
|
|
5181
|
+
}
|
|
5182
|
+
/**
|
|
5183
|
+
* Generate .dockerignore file
|
|
5184
|
+
*/
|
|
5185
|
+
function generateDockerignore() {
|
|
5186
|
+
return `# Dependencies
|
|
5187
|
+
node_modules
|
|
5188
|
+
.pnpm-store
|
|
5189
|
+
|
|
5190
|
+
# Build output (except what we need)
|
|
5191
|
+
.gkm/aws*
|
|
5192
|
+
.gkm/server/*.ts
|
|
5193
|
+
!.gkm/server/dist
|
|
5194
|
+
|
|
5195
|
+
# The key gkm build --stage encrypted secrets with: runtime env only, never
|
|
5196
|
+
# the build context
|
|
5197
|
+
.gkm/server/master.key
|
|
5198
|
+
|
|
5199
|
+
# IDE and editor
|
|
5200
|
+
.idea
|
|
5201
|
+
.vscode
|
|
5202
|
+
*.swp
|
|
5203
|
+
*.swo
|
|
5204
|
+
|
|
5205
|
+
# Git
|
|
5206
|
+
.git
|
|
5207
|
+
.gitignore
|
|
5208
|
+
|
|
5209
|
+
# Logs
|
|
5210
|
+
*.log
|
|
5211
|
+
npm-debug.log*
|
|
5212
|
+
pnpm-debug.log*
|
|
5213
|
+
|
|
5214
|
+
# Test files
|
|
5215
|
+
**/*.test.ts
|
|
5216
|
+
**/*.spec.ts
|
|
5217
|
+
**/__tests__
|
|
5218
|
+
coverage
|
|
5219
|
+
|
|
5220
|
+
# Documentation
|
|
5221
|
+
docs
|
|
5222
|
+
*.md
|
|
5223
|
+
!README.md
|
|
5224
|
+
|
|
5225
|
+
# Environment files (handle secrets separately)
|
|
5226
|
+
.env
|
|
5227
|
+
.env.*
|
|
5228
|
+
!.env.example
|
|
5229
|
+
|
|
5230
|
+
# Docker files (don't copy recursively)
|
|
5231
|
+
Dockerfile*
|
|
5232
|
+
docker-compose*
|
|
5233
|
+
.dockerignore
|
|
5234
|
+
`;
|
|
5235
|
+
}
|
|
5236
|
+
/**
|
|
5237
|
+
* Generate docker-entrypoint.sh for custom startup logic
|
|
5238
|
+
*/
|
|
5239
|
+
function generateDockerEntrypoint() {
|
|
5240
|
+
return `#!/bin/sh
|
|
5241
|
+
set -e
|
|
5242
|
+
|
|
5243
|
+
# Run any custom startup scripts here
|
|
5244
|
+
# Example: wait for database
|
|
5245
|
+
# until nc -z $DB_HOST $DB_PORT; do
|
|
5246
|
+
# echo "Waiting for database..."
|
|
5247
|
+
# sleep 1
|
|
5248
|
+
# done
|
|
5249
|
+
|
|
5250
|
+
# Execute the main command
|
|
5251
|
+
exec "$@"
|
|
5252
|
+
`;
|
|
5253
|
+
}
|
|
5254
|
+
/**
|
|
5255
|
+
* Resolve Docker configuration from GkmConfig with defaults
|
|
5256
|
+
*/
|
|
5257
|
+
function resolveDockerConfig(config) {
|
|
5258
|
+
const docker = config.docker ?? {};
|
|
5259
|
+
return {
|
|
5260
|
+
registry: docker.registry ?? "",
|
|
5261
|
+
imageName: docker.imageName ?? config.name ?? "api",
|
|
5262
|
+
baseImage: docker.baseImage ?? "node:22-alpine",
|
|
5263
|
+
port: docker.port ?? 3e3
|
|
5264
|
+
};
|
|
5265
|
+
}
|
|
5266
|
+
/**
|
|
5267
|
+
* Generate a Dockerfile for Next.js frontend apps using standalone output.
|
|
5268
|
+
* Uses turbo prune for monorepo optimization.
|
|
5269
|
+
* @internal Exported for testing
|
|
5270
|
+
*/
|
|
5271
|
+
function generateNextjsDockerfile(options) {
|
|
5272
|
+
const { baseImage, port, appPath, turboPackage, packageManager, publicUrlArgs = ["NEXT_PUBLIC_API_URL", "NEXT_PUBLIC_AUTH_URL"] } = options;
|
|
5273
|
+
const pm = getPmConfig(packageManager);
|
|
5274
|
+
const installPm = pm.install ? `RUN ${pm.install}` : "";
|
|
5275
|
+
const turboInstallCmd = getTurboInstallCmd(packageManager);
|
|
5276
|
+
const turboCmd = packageManager === "pnpm" ? "pnpm dlx turbo" : "npx turbo";
|
|
5277
|
+
const publicUrlArgDeclarations = publicUrlArgs.map((arg) => `ARG ${arg}=""`).join("\n");
|
|
5278
|
+
const publicUrlEnvDeclarations = publicUrlArgs.map((arg) => `ENV ${arg}=$${arg}`).join("\n");
|
|
5279
|
+
return `# syntax=docker/dockerfile:1
|
|
5280
|
+
# Next.js standalone Dockerfile with turbo prune optimization
|
|
5281
|
+
|
|
5282
|
+
# Stage 1: Prune monorepo
|
|
5283
|
+
FROM ${baseImage} AS pruner
|
|
5284
|
+
|
|
5285
|
+
WORKDIR /app
|
|
5286
|
+
|
|
5287
|
+
${installPm}
|
|
5288
|
+
|
|
5289
|
+
COPY . .
|
|
5290
|
+
|
|
5291
|
+
# Prune to only include necessary packages
|
|
5292
|
+
RUN ${turboCmd} prune ${turboPackage} --docker
|
|
5293
|
+
|
|
5294
|
+
# Stage 2: Install dependencies
|
|
5295
|
+
FROM ${baseImage} AS deps
|
|
5296
|
+
|
|
5297
|
+
WORKDIR /app
|
|
5298
|
+
|
|
5299
|
+
${installPm}
|
|
5300
|
+
|
|
5301
|
+
# Copy pruned lockfile and package.jsons
|
|
5302
|
+
COPY --from=pruner /app/out/${pm.lockfile} ./
|
|
5303
|
+
COPY --from=pruner /app/out/json/ ./
|
|
5304
|
+
|
|
5305
|
+
# Install dependencies
|
|
5306
|
+
RUN --mount=type=cache,id=${pm.cacheId},target=${pm.cacheTarget} \\
|
|
5307
|
+
${turboInstallCmd}
|
|
5308
|
+
|
|
5309
|
+
# Stage 3: Build
|
|
5310
|
+
FROM deps AS builder
|
|
5311
|
+
|
|
5312
|
+
WORKDIR /app
|
|
5313
|
+
|
|
5314
|
+
# Build-time args for public API URLs (populated by gkm deploy)
|
|
5315
|
+
# These get baked into the Next.js build as public environment variables
|
|
5316
|
+
${publicUrlArgDeclarations}
|
|
5317
|
+
|
|
5318
|
+
# Convert ARGs to ENVs for Next.js build
|
|
5319
|
+
${publicUrlEnvDeclarations}
|
|
5320
|
+
|
|
5321
|
+
# Copy pruned source
|
|
5322
|
+
COPY --from=pruner /app/out/full/ ./
|
|
5323
|
+
|
|
5324
|
+
# Copy workspace root configs for turbo builds (turbo prune doesn't include root configs)
|
|
5325
|
+
# Using wildcard to make it optional for single-app projects
|
|
5326
|
+
COPY --from=pruner /app/tsconfig.* ./
|
|
5327
|
+
|
|
5328
|
+
# Ensure public directory exists (may be empty for scaffolded projects)
|
|
5329
|
+
RUN mkdir -p ${appPath}/public
|
|
5330
|
+
|
|
5331
|
+
# Set Next.js to produce standalone output
|
|
5332
|
+
ENV NEXT_TELEMETRY_DISABLED=1
|
|
5333
|
+
|
|
5334
|
+
# Build the application
|
|
5335
|
+
RUN ${turboCmd} run build --filter=${turboPackage}
|
|
5336
|
+
|
|
5337
|
+
# Stage 4: Production
|
|
5338
|
+
FROM ${baseImage} AS runner
|
|
5339
|
+
|
|
5340
|
+
WORKDIR /app
|
|
5341
|
+
|
|
5342
|
+
# Install tini for proper signal handling
|
|
5343
|
+
RUN apk add --no-cache tini
|
|
5344
|
+
|
|
5345
|
+
# Create non-root user
|
|
5346
|
+
RUN addgroup --system --gid 1001 nodejs && \\
|
|
5347
|
+
adduser --system --uid 1001 nextjs
|
|
5348
|
+
|
|
5349
|
+
# Set environment
|
|
5350
|
+
ENV NODE_ENV=production
|
|
5351
|
+
ENV NEXT_TELEMETRY_DISABLED=1
|
|
5352
|
+
ENV PORT=${port}
|
|
5353
|
+
ENV HOSTNAME="0.0.0.0"
|
|
5354
|
+
|
|
5355
|
+
# Copy static files and standalone output
|
|
5356
|
+
COPY --from=builder --chown=nextjs:nodejs /app/${appPath}/.next/standalone ./
|
|
5357
|
+
COPY --from=builder --chown=nextjs:nodejs /app/${appPath}/.next/static ./${appPath}/.next/static
|
|
5358
|
+
COPY --from=builder --chown=nextjs:nodejs /app/${appPath}/public ./${appPath}/public
|
|
5359
|
+
|
|
5360
|
+
USER nextjs
|
|
5361
|
+
|
|
5362
|
+
EXPOSE ${port}
|
|
5363
|
+
|
|
5364
|
+
ENTRYPOINT ["/sbin/tini", "--"]
|
|
5365
|
+
CMD ["node", "${appPath}/server.js"]
|
|
5366
|
+
`;
|
|
5367
|
+
}
|
|
5368
|
+
/**
|
|
5369
|
+
* Generate a Dockerfile for backend apps in a workspace.
|
|
5370
|
+
* Uses turbo prune for monorepo optimization.
|
|
5371
|
+
* @internal Exported for testing
|
|
5372
|
+
*/
|
|
5373
|
+
function generateBackendDockerfile(options) {
|
|
5374
|
+
const { baseImage, port, appPath, turboPackage, packageManager, healthCheckPath = "/health" } = options;
|
|
5375
|
+
const pm = getPmConfig(packageManager);
|
|
5376
|
+
const installPm = pm.install ? `RUN ${pm.install}` : "";
|
|
5377
|
+
const turboInstallCmd = getTurboInstallCmd(packageManager);
|
|
5378
|
+
return `# syntax=docker/dockerfile:1
|
|
5379
|
+
# Backend Dockerfile with turbo prune optimization
|
|
5380
|
+
|
|
5381
|
+
# Stage 1: Prune monorepo
|
|
5382
|
+
FROM ${baseImage} AS pruner
|
|
5383
|
+
|
|
5384
|
+
WORKDIR /app
|
|
5385
|
+
|
|
5386
|
+
${installPm}
|
|
5387
|
+
|
|
5388
|
+
COPY . .
|
|
5389
|
+
|
|
5390
|
+
# Prune to only include necessary packages
|
|
5391
|
+
RUN ${packageManager === "pnpm" ? "pnpm dlx turbo" : "npx turbo"} prune ${turboPackage} --docker
|
|
5392
|
+
|
|
5393
|
+
# Stage 2: Install dependencies
|
|
5394
|
+
FROM ${baseImage} AS deps
|
|
5395
|
+
|
|
5396
|
+
WORKDIR /app
|
|
5397
|
+
|
|
5398
|
+
${installPm}
|
|
5399
|
+
|
|
5400
|
+
# Copy pruned lockfile and package.jsons
|
|
5401
|
+
COPY --from=pruner /app/out/${pm.lockfile} ./
|
|
5402
|
+
COPY --from=pruner /app/out/json/ ./
|
|
5403
|
+
|
|
5404
|
+
# Install dependencies
|
|
5405
|
+
RUN --mount=type=cache,id=${pm.cacheId},target=${pm.cacheTarget} \\
|
|
5406
|
+
${turboInstallCmd}
|
|
5407
|
+
|
|
5408
|
+
# Stage 3: Build
|
|
5409
|
+
FROM deps AS builder
|
|
5410
|
+
|
|
5411
|
+
WORKDIR /app
|
|
5412
|
+
|
|
5413
|
+
# Copy pruned source
|
|
5414
|
+
COPY --from=pruner /app/out/full/ ./
|
|
5415
|
+
|
|
5416
|
+
# Copy workspace root configs for turbo builds (turbo prune doesn't include root configs)
|
|
5417
|
+
# Using wildcard to make it optional for single-app projects
|
|
5418
|
+
COPY --from=pruner /app/gkm.config.* ./
|
|
5419
|
+
COPY --from=pruner /app/tsconfig.* ./
|
|
5420
|
+
|
|
5421
|
+
# Write encrypted credentials for gkm build to embed.
|
|
5422
|
+
# A build secret (\`docker build --secret id=gkm_credentials,src=…\`), not an
|
|
5423
|
+
# ARG: an ARG is recorded in the image history. Two lines, ciphertext then IV;
|
|
5424
|
+
# absent when the app has no secrets.
|
|
5425
|
+
RUN --mount=type=secret,id=gkm_credentials,required=false \
|
|
5426
|
+
if [ -s /run/secrets/gkm_credentials ]; then \
|
|
5427
|
+
mkdir -p ${appPath}/.gkm && \
|
|
5428
|
+
sed -n 1p /run/secrets/gkm_credentials > ${appPath}/.gkm/credentials.enc && \
|
|
5429
|
+
sed -n 2p /run/secrets/gkm_credentials > ${appPath}/.gkm/credentials.iv; \
|
|
5430
|
+
fi
|
|
5431
|
+
|
|
5432
|
+
# Build any workspace packages the app depends on.
|
|
5433
|
+
#
|
|
5434
|
+
# A no-op for a project whose dependencies come from a registry, and required
|
|
5435
|
+
# for one whose CLI is a sibling package: \`turbo prune\` honours .gitignore, so a
|
|
5436
|
+
# workspace package's \`dist\` never reaches the image and its bin has nothing to
|
|
5437
|
+
# import. \`--if-present\` is what makes the same Dockerfile correct for both.
|
|
5438
|
+
RUN ${pm.run} --if-present build
|
|
5439
|
+
|
|
5440
|
+
# Build production server using gkm
|
|
5441
|
+
RUN cd ${appPath} && ${pm.exec} gkm build --provider server --production
|
|
5442
|
+
|
|
5443
|
+
# Stage 4: Production
|
|
5444
|
+
FROM ${baseImage} AS runner
|
|
5445
|
+
|
|
5446
|
+
WORKDIR /app
|
|
5447
|
+
|
|
5448
|
+
RUN apk add --no-cache tini
|
|
5449
|
+
|
|
5450
|
+
RUN addgroup --system --gid 1001 nodejs && \\
|
|
5451
|
+
adduser --system --uid 1001 hono
|
|
5452
|
+
|
|
5453
|
+
# Copy bundled server
|
|
5454
|
+
COPY --from=builder --chown=hono:nodejs /app/${appPath}/.gkm/server/dist/server.mjs ./
|
|
5455
|
+
|
|
5456
|
+
ENV NODE_ENV=production
|
|
5457
|
+
ENV PORT=${port}
|
|
5458
|
+
|
|
5459
|
+
HEALTHCHECK --interval=30s --timeout=10s --start-period=30s --retries=3 \\
|
|
5460
|
+
CMD wget -qO- http://localhost:${port}${healthCheckPath} > /dev/null 2>&1 || exit 1
|
|
5461
|
+
|
|
5462
|
+
USER hono
|
|
5463
|
+
|
|
5464
|
+
EXPOSE ${port}
|
|
5465
|
+
|
|
5466
|
+
ENTRYPOINT ["/sbin/tini", "--"]
|
|
5467
|
+
CMD ["node", "server.mjs"]
|
|
5468
|
+
`;
|
|
5469
|
+
}
|
|
5470
|
+
/**
|
|
5471
|
+
* Generate a Dockerfile for apps with a custom entry point.
|
|
5472
|
+
* Uses esbuild to bundle the entry point into dist/index.mjs with all dependencies.
|
|
5473
|
+
* This is used for apps that don't use gkm routes (e.g., Better Auth servers).
|
|
5474
|
+
* @internal Exported for testing
|
|
5475
|
+
*/
|
|
5476
|
+
function generateEntryDockerfile(options) {
|
|
5477
|
+
const { baseImage, port, appPath, entry, turboPackage, packageManager, healthCheckPath = "/health" } = options;
|
|
5478
|
+
const pm = getPmConfig(packageManager);
|
|
5479
|
+
const installPm = pm.install ? `RUN ${pm.install}` : "";
|
|
5480
|
+
const turboInstallCmd = getTurboInstallCmd(packageManager);
|
|
5481
|
+
return `# syntax=docker/dockerfile:1
|
|
5482
|
+
# Entry-based Dockerfile with turbo prune + tsdown bundling
|
|
5483
|
+
|
|
5484
|
+
# Stage 1: Prune monorepo
|
|
5485
|
+
FROM ${baseImage} AS pruner
|
|
5486
|
+
|
|
5487
|
+
WORKDIR /app
|
|
5488
|
+
|
|
5489
|
+
${installPm}
|
|
5490
|
+
|
|
5491
|
+
COPY . .
|
|
5492
|
+
|
|
5493
|
+
# Prune to only include necessary packages
|
|
5494
|
+
RUN ${packageManager === "pnpm" ? "pnpm dlx turbo" : "npx turbo"} prune ${turboPackage} --docker
|
|
5495
|
+
|
|
5496
|
+
# Stage 2: Install dependencies
|
|
5497
|
+
FROM ${baseImage} AS deps
|
|
5498
|
+
|
|
5499
|
+
WORKDIR /app
|
|
5500
|
+
|
|
5501
|
+
${installPm}
|
|
5502
|
+
|
|
5503
|
+
# Copy pruned lockfile and package.jsons
|
|
5504
|
+
COPY --from=pruner /app/out/${pm.lockfile} ./
|
|
5505
|
+
COPY --from=pruner /app/out/json/ ./
|
|
5506
|
+
|
|
5507
|
+
# Install dependencies
|
|
5508
|
+
RUN --mount=type=cache,id=${pm.cacheId},target=${pm.cacheTarget} \\
|
|
5509
|
+
${turboInstallCmd}
|
|
5510
|
+
|
|
5511
|
+
# Stage 3: Build with tsdown
|
|
5512
|
+
FROM deps AS builder
|
|
5513
|
+
|
|
5514
|
+
WORKDIR /app
|
|
5515
|
+
|
|
5516
|
+
# Copy pruned source
|
|
5517
|
+
COPY --from=pruner /app/out/full/ ./
|
|
5518
|
+
|
|
5519
|
+
# Copy workspace root configs for turbo builds (turbo prune doesn't include root configs)
|
|
5520
|
+
# Using wildcard to make it optional for single-app projects
|
|
5521
|
+
COPY --from=pruner /app/tsconfig.* ./
|
|
5522
|
+
|
|
5523
|
+
# Bundle entry point with esbuild (outputs to dist/index.mjs)
|
|
5524
|
+
# Creates a fully standalone bundle with all dependencies included
|
|
5525
|
+
# Use define to embed credentials if present.
|
|
5526
|
+
# They arrive as a build secret (\`docker build --secret id=gkm_credentials,src=…\`),
|
|
5527
|
+
# not an ARG: an ARG is recorded in the image history. Two lines, ciphertext
|
|
5528
|
+
# then IV; absent when the app has no secrets.
|
|
5529
|
+
RUN --mount=type=secret,id=gkm_credentials,required=false \
|
|
5530
|
+
cd ${appPath} && \
|
|
5531
|
+
if [ -s /run/secrets/gkm_credentials ]; then \
|
|
5532
|
+
CREDS=$(sed -n 1p /run/secrets/gkm_credentials) && \
|
|
5533
|
+
IV=$(sed -n 2p /run/secrets/gkm_credentials) && \
|
|
5534
|
+
npx esbuild ${entry} --bundle --platform=node --target=node22 --format=esm \
|
|
5535
|
+
--outfile=dist/index.mjs --packages=bundle \
|
|
5536
|
+
--banner:js='import { createRequire } from "module"; const require = createRequire(import.meta.url);' \
|
|
5537
|
+
--define:__GKM_ENCRYPTED_CREDENTIALS__="'\\"$CREDS\\"'" \
|
|
5538
|
+
--define:__GKM_CREDENTIALS_IV__="'\\"$IV\\"'"; \
|
|
5539
|
+
else \
|
|
5540
|
+
npx esbuild ${entry} --bundle --platform=node --target=node22 --format=esm \
|
|
5541
|
+
--outfile=dist/index.mjs --packages=bundle \
|
|
5542
|
+
--banner:js='import { createRequire } from "module"; const require = createRequire(import.meta.url);'; \
|
|
5543
|
+
fi
|
|
5544
|
+
|
|
5545
|
+
# Stage 4: Production
|
|
5546
|
+
FROM ${baseImage} AS runner
|
|
5547
|
+
|
|
5548
|
+
WORKDIR /app
|
|
5549
|
+
|
|
5550
|
+
RUN apk add --no-cache tini
|
|
5551
|
+
|
|
5552
|
+
RUN addgroup --system --gid 1001 nodejs && \\
|
|
5553
|
+
adduser --system --uid 1001 app
|
|
5554
|
+
|
|
5555
|
+
# Copy bundled output only (no node_modules needed - fully bundled)
|
|
5556
|
+
COPY --from=builder --chown=app:nodejs /app/${appPath}/dist/index.mjs ./
|
|
5557
|
+
|
|
5558
|
+
ENV NODE_ENV=production
|
|
5559
|
+
ENV PORT=${port}
|
|
5560
|
+
|
|
5561
|
+
HEALTHCHECK --interval=30s --timeout=10s --start-period=30s --retries=3 \\
|
|
5562
|
+
CMD wget -qO- http://localhost:${port}${healthCheckPath} > /dev/null 2>&1 || exit 1
|
|
5563
|
+
|
|
5564
|
+
USER app
|
|
5565
|
+
|
|
5566
|
+
EXPOSE ${port}
|
|
5567
|
+
|
|
5568
|
+
ENTRYPOINT ["/sbin/tini", "--"]
|
|
5569
|
+
CMD ["node", "index.mjs"]
|
|
5570
|
+
`;
|
|
5571
|
+
}
|
|
5572
|
+
/**
|
|
5573
|
+
* Generate a Dockerfile for Node-based SSR web frameworks (TanStack Start,
|
|
5574
|
+
* Remix). Builds with turbo, then runs the framework's production start
|
|
5575
|
+
* script via `npm start`.
|
|
5576
|
+
*
|
|
5577
|
+
* Public URL build args are declared so framework bundlers (Vite, Vinxi)
|
|
5578
|
+
* can inline them at build time. Per-framework prefix is the caller's job.
|
|
5579
|
+
* @internal Exported for testing
|
|
5580
|
+
*/
|
|
5581
|
+
function generateNodeWebDockerfile(options) {
|
|
5582
|
+
const { baseImage, port, appPath, turboPackage, packageManager, publicUrlArgs = [] } = options;
|
|
5583
|
+
const pm = getPmConfig(packageManager);
|
|
5584
|
+
const installPm = pm.install ? `RUN ${pm.install}` : "";
|
|
5585
|
+
const turboInstallCmd = getTurboInstallCmd(packageManager);
|
|
5586
|
+
const turboCmd = packageManager === "pnpm" ? "pnpm dlx turbo" : "npx turbo";
|
|
5587
|
+
const startCmd = packageManager === "pnpm" ? "pnpm start" : `${pm.lockfile.startsWith("package-lock") ? "npm" : packageManager} start`;
|
|
5588
|
+
const argDecls = publicUrlArgs.map((a) => `ARG ${a}=""`).join("\n");
|
|
5589
|
+
const envDecls = publicUrlArgs.map((a) => `ENV ${a}=$${a}`).join("\n");
|
|
5590
|
+
return `# syntax=docker/dockerfile:1
|
|
5591
|
+
# Node SSR web Dockerfile (TanStack Start / Remix) with turbo prune
|
|
5592
|
+
|
|
5593
|
+
FROM ${baseImage} AS pruner
|
|
5594
|
+
WORKDIR /app
|
|
5595
|
+
${installPm}
|
|
5596
|
+
COPY . .
|
|
5597
|
+
RUN ${turboCmd} prune ${turboPackage} --docker
|
|
5598
|
+
|
|
5599
|
+
FROM ${baseImage} AS deps
|
|
5600
|
+
WORKDIR /app
|
|
5601
|
+
${installPm}
|
|
5602
|
+
COPY --from=pruner /app/out/${pm.lockfile} ./
|
|
5603
|
+
COPY --from=pruner /app/out/json/ ./
|
|
5604
|
+
RUN --mount=type=cache,id=${pm.cacheId},target=${pm.cacheTarget} \\
|
|
5605
|
+
${turboInstallCmd}
|
|
5606
|
+
|
|
5607
|
+
FROM deps AS builder
|
|
5608
|
+
WORKDIR /app
|
|
5609
|
+
${argDecls}
|
|
5610
|
+
${envDecls}
|
|
5611
|
+
COPY --from=pruner /app/out/full/ ./
|
|
5612
|
+
COPY --from=pruner /app/tsconfig.* ./
|
|
5613
|
+
RUN ${turboCmd} run build --filter=${turboPackage}
|
|
5614
|
+
|
|
5615
|
+
FROM ${baseImage} AS runner
|
|
5616
|
+
WORKDIR /app
|
|
5617
|
+
RUN apk add --no-cache tini
|
|
5618
|
+
RUN addgroup --system --gid 1001 nodejs && \\
|
|
5619
|
+
adduser --system --uid 1001 app
|
|
5620
|
+
|
|
5621
|
+
# Copy the whole built workspace — frameworks differ on output paths
|
|
5622
|
+
COPY --from=builder --chown=app:nodejs /app/ ./
|
|
5623
|
+
|
|
5624
|
+
ENV NODE_ENV=production
|
|
5625
|
+
ENV PORT=${port}
|
|
5626
|
+
ENV HOSTNAME="0.0.0.0"
|
|
5627
|
+
|
|
5628
|
+
USER app
|
|
5629
|
+
EXPOSE ${port}
|
|
5630
|
+
|
|
5631
|
+
ENTRYPOINT ["/sbin/tini", "--"]
|
|
5632
|
+
CMD ["sh", "-c", "cd ${appPath} && ${startCmd}"]
|
|
5633
|
+
`;
|
|
5634
|
+
}
|
|
5635
|
+
/**
|
|
5636
|
+
* Generate a Dockerfile for Vite SPA apps. Builds the static bundle with
|
|
5637
|
+
* turbo, then serves it via nginx.
|
|
5638
|
+
* @internal Exported for testing
|
|
5639
|
+
*/
|
|
5640
|
+
function generateViteStaticDockerfile(options) {
|
|
5641
|
+
const { baseImage, port, appPath, turboPackage, packageManager, publicUrlArgs = [] } = options;
|
|
5642
|
+
const pm = getPmConfig(packageManager);
|
|
5643
|
+
const installPm = pm.install ? `RUN ${pm.install}` : "";
|
|
5644
|
+
const turboInstallCmd = getTurboInstallCmd(packageManager);
|
|
5645
|
+
const turboCmd = packageManager === "pnpm" ? "pnpm dlx turbo" : "npx turbo";
|
|
5646
|
+
const argDecls = publicUrlArgs.map((a) => `ARG ${a}=""`).join("\n");
|
|
5647
|
+
const envDecls = publicUrlArgs.map((a) => `ENV ${a}=$${a}`).join("\n");
|
|
5648
|
+
return `# syntax=docker/dockerfile:1
|
|
5649
|
+
# Vite SPA Dockerfile — builds static assets, serves via nginx
|
|
5650
|
+
|
|
5651
|
+
FROM ${baseImage} AS pruner
|
|
5652
|
+
WORKDIR /app
|
|
5653
|
+
${installPm}
|
|
5654
|
+
COPY . .
|
|
5655
|
+
RUN ${turboCmd} prune ${turboPackage} --docker
|
|
5656
|
+
|
|
5657
|
+
FROM ${baseImage} AS deps
|
|
5658
|
+
WORKDIR /app
|
|
5659
|
+
${installPm}
|
|
5660
|
+
COPY --from=pruner /app/out/${pm.lockfile} ./
|
|
5661
|
+
COPY --from=pruner /app/out/json/ ./
|
|
5662
|
+
RUN --mount=type=cache,id=${pm.cacheId},target=${pm.cacheTarget} \\
|
|
5663
|
+
${turboInstallCmd}
|
|
5664
|
+
|
|
5665
|
+
FROM deps AS builder
|
|
5666
|
+
WORKDIR /app
|
|
5667
|
+
${argDecls}
|
|
5668
|
+
${envDecls}
|
|
5669
|
+
COPY --from=pruner /app/out/full/ ./
|
|
5670
|
+
COPY --from=pruner /app/tsconfig.* ./
|
|
5671
|
+
RUN ${turboCmd} run build --filter=${turboPackage}
|
|
5672
|
+
|
|
5673
|
+
FROM nginx:alpine AS runner
|
|
5674
|
+
COPY --from=builder /app/${appPath}/dist /usr/share/nginx/html
|
|
5675
|
+
RUN printf 'server {\\n listen ${port};\\n root /usr/share/nginx/html;\\n index index.html;\\n location / { try_files $uri $uri/ /index.html; }\\n}\\n' \\
|
|
5676
|
+
> /etc/nginx/conf.d/default.conf
|
|
5677
|
+
EXPOSE ${port}
|
|
5678
|
+
CMD ["nginx", "-g", "daemon off;"]
|
|
5679
|
+
`;
|
|
5680
|
+
}
|
|
5681
|
+
//#endregion
|
|
5682
|
+
//#region src/docker/index.ts
|
|
5683
|
+
const logger = require_discover.output;
|
|
5684
|
+
/** `docker build` failed; what docker printed above says why. */
|
|
5685
|
+
var DockerBuildFailed = class extends Error {
|
|
5686
|
+
imageRef;
|
|
5687
|
+
constructor(imageRef, cause) {
|
|
5688
|
+
super(`Failed to build Docker image: ${cause instanceof Error ? cause.message : String(cause)}`, { cause });
|
|
5689
|
+
this.imageRef = imageRef;
|
|
5690
|
+
this.name = "DockerBuildFailed";
|
|
5691
|
+
}
|
|
5692
|
+
};
|
|
5693
|
+
/** `docker push` failed — usually the registry login or its permissions. */
|
|
5694
|
+
var DockerPushFailed = class extends Error {
|
|
5695
|
+
imageRef;
|
|
5696
|
+
constructor(imageRef, cause) {
|
|
5697
|
+
super(`Failed to push Docker image: ${cause instanceof Error ? cause.message : String(cause)}`, { cause });
|
|
5698
|
+
this.imageRef = imageRef;
|
|
5699
|
+
this.name = "DockerPushFailed";
|
|
5700
|
+
}
|
|
5701
|
+
};
|
|
5702
|
+
/** `gkm docker --push` was asked for with nowhere to push to. */
|
|
5703
|
+
var PushNeedsRegistry = class extends Error {
|
|
5704
|
+
imageName;
|
|
5705
|
+
constructor(imageName) {
|
|
5706
|
+
super(`Registry is required to push Docker image '${imageName}'. Use --registry or configure docker.registry in gkm.config.ts`);
|
|
5707
|
+
this.imageName = imageName;
|
|
5708
|
+
this.name = "PushNeedsRegistry";
|
|
5709
|
+
}
|
|
5710
|
+
};
|
|
5711
|
+
/**
|
|
5712
|
+
* Docker command implementation
|
|
5713
|
+
* Generates Dockerfile, docker-compose.yml, and related files
|
|
5714
|
+
*
|
|
5715
|
+
* Default: Multi-stage Dockerfile that builds from source inside Docker
|
|
5716
|
+
* --slim: Slim Dockerfile that copies pre-built bundle (requires prior build)
|
|
5717
|
+
*/
|
|
5718
|
+
async function dockerCommand(options) {
|
|
5719
|
+
const cwd = options.cwd ?? process.cwd();
|
|
5720
|
+
const loadedConfig = await require_config.loadWorkspaceConfig(cwd);
|
|
5721
|
+
if (loadedConfig.type === "workspace") {
|
|
5722
|
+
logger.log("📦 Detected workspace configuration");
|
|
5723
|
+
return workspaceDockerCommand(loadedConfig.workspace);
|
|
5724
|
+
}
|
|
5725
|
+
const dockerConfig = resolveDockerConfig(await require_config.loadConfig(cwd));
|
|
5726
|
+
const healthCheckPath = "/health";
|
|
5727
|
+
const useSlim = options.slim === true;
|
|
5728
|
+
if (useSlim) {
|
|
5729
|
+
const distDir = (0, node_path.join)(cwd, ".gkm", "server", "dist");
|
|
5730
|
+
if (!(0, node_fs.existsSync)((0, node_path.join)(distDir, "server.mjs"))) throw new Error("Slim Dockerfile requires a pre-built bundle. Run `gkm build --provider server --production` first, or omit --slim to use multi-stage build.");
|
|
5731
|
+
}
|
|
5732
|
+
const dockerDir = (0, node_path.join)(cwd, ".gkm", "docker");
|
|
5733
|
+
await (0, node_fs_promises.mkdir)(dockerDir, { recursive: true });
|
|
5734
|
+
const packageManager = detectPackageManager(cwd);
|
|
5735
|
+
const inMonorepo = isMonorepo(cwd);
|
|
5736
|
+
const hasTurbo = hasTurboConfig(cwd);
|
|
5737
|
+
let useTurbo = options.turbo ?? false;
|
|
5738
|
+
if (inMonorepo && !useSlim) {
|
|
5739
|
+
if (hasTurbo) {
|
|
5740
|
+
useTurbo = true;
|
|
5741
|
+
logger.log(" Detected monorepo with turbo.json - using turbo prune");
|
|
5742
|
+
} else throw new Error("Monorepo detected but turbo.json not found.\n\nDocker builds in monorepos require Turborepo for proper dependency isolation.\n\nTo fix this:\n 1. Install turbo: pnpm add -Dw turbo\n 2. Create turbo.json in your monorepo root\n 3. Run this command again\n\nSee: https://turbo.build/repo/docs/guides/tools/docker");
|
|
5743
|
+
}
|
|
5744
|
+
let turboPackage = options.turboPackage ?? dockerConfig.imageName;
|
|
5745
|
+
if (useTurbo && !options.turboPackage) try {
|
|
5746
|
+
const pkg = require(`${cwd}/package.json`);
|
|
5747
|
+
if (pkg.name) {
|
|
5748
|
+
turboPackage = pkg.name;
|
|
5749
|
+
logger.log(` Turbo package: ${turboPackage}`);
|
|
5750
|
+
}
|
|
5751
|
+
} catch {}
|
|
5752
|
+
const templateOptions = {
|
|
5753
|
+
imageName: dockerConfig.imageName,
|
|
5754
|
+
baseImage: dockerConfig.baseImage,
|
|
5755
|
+
port: dockerConfig.port,
|
|
5756
|
+
healthCheckPath,
|
|
5757
|
+
prebuilt: useSlim,
|
|
5758
|
+
turbo: useTurbo,
|
|
5759
|
+
turboPackage,
|
|
5760
|
+
packageManager
|
|
5761
|
+
};
|
|
5762
|
+
const dockerfile = useSlim ? generateSlimDockerfile(templateOptions) : generateMultiStageDockerfile(templateOptions);
|
|
5763
|
+
const dockerMode = useSlim ? "slim" : useTurbo ? "turbo" : "multi-stage";
|
|
5764
|
+
const dockerfilePath = (0, node_path.join)(dockerDir, "Dockerfile");
|
|
5765
|
+
await (0, node_fs_promises.writeFile)(dockerfilePath, dockerfile);
|
|
5766
|
+
logger.log(`Generated: .gkm/docker/Dockerfile (${dockerMode}, ${packageManager})`);
|
|
5767
|
+
const composePath = await writeConstructsCompose(loadedConfig.workspace);
|
|
5768
|
+
const dockerignore = generateDockerignore();
|
|
5769
|
+
const dockerignorePath = (0, node_path.join)(cwd, ".dockerignore");
|
|
5770
|
+
await (0, node_fs_promises.writeFile)(dockerignorePath, dockerignore);
|
|
5771
|
+
logger.log("Generated: .dockerignore (project root)");
|
|
5772
|
+
const entrypoint = generateDockerEntrypoint();
|
|
5773
|
+
const entrypointPath = (0, node_path.join)(dockerDir, "docker-entrypoint.sh");
|
|
5774
|
+
await (0, node_fs_promises.writeFile)(entrypointPath, entrypoint);
|
|
5775
|
+
logger.log("Generated: .gkm/docker/docker-entrypoint.sh");
|
|
5776
|
+
const result = {
|
|
5777
|
+
dockerfile: dockerfilePath,
|
|
5778
|
+
dockerCompose: composePath,
|
|
5779
|
+
dockerignore: dockerignorePath,
|
|
5780
|
+
entrypoint: entrypointPath
|
|
5781
|
+
};
|
|
5782
|
+
if (options.build) await buildDockerImage(dockerConfig.imageName, options, cwd);
|
|
5783
|
+
if (options.push) await pushDockerImage(dockerConfig.imageName, options, cwd);
|
|
5784
|
+
return result;
|
|
5785
|
+
}
|
|
5786
|
+
/**
|
|
5787
|
+
* Ensure lockfile exists in the build context
|
|
5788
|
+
* For monorepos, copies from workspace root if needed
|
|
5789
|
+
* Returns cleanup function if file was copied
|
|
5790
|
+
*/
|
|
5791
|
+
function ensureLockfile(cwd) {
|
|
5792
|
+
const lockfilePath = findLockfilePath(cwd);
|
|
5793
|
+
if (!lockfilePath) {
|
|
5794
|
+
logger.warn("\n⚠️ No lockfile found. Docker build may fail or use stale dependencies.");
|
|
5795
|
+
return null;
|
|
5796
|
+
}
|
|
5797
|
+
const lockfileName = (0, node_path.basename)(lockfilePath);
|
|
5798
|
+
const localLockfile = (0, node_path.join)(cwd, lockfileName);
|
|
5799
|
+
if (lockfilePath === localLockfile) return null;
|
|
5800
|
+
logger.log(` Copying ${lockfileName} from monorepo root...`);
|
|
5801
|
+
(0, node_fs.copyFileSync)(lockfilePath, localLockfile);
|
|
5802
|
+
return () => {
|
|
5803
|
+
try {
|
|
5804
|
+
(0, node_fs.unlinkSync)(localLockfile);
|
|
5805
|
+
} catch {}
|
|
5806
|
+
};
|
|
5807
|
+
}
|
|
5808
|
+
/**
|
|
5809
|
+
* Build Docker image
|
|
5810
|
+
* Uses BuildKit for cache mount support
|
|
5811
|
+
*/
|
|
5812
|
+
async function buildDockerImage(imageName, options, cwd) {
|
|
5813
|
+
const tag = options.tag ?? "latest";
|
|
5814
|
+
const registry = options.registry;
|
|
5815
|
+
const fullImageName = validateImageRef(registry ? `${registry}/${imageName}:${tag}` : `${imageName}:${tag}`);
|
|
5816
|
+
logger.log(`\n🐳 Building Docker image: ${fullImageName}`);
|
|
5817
|
+
const cleanup = ensureLockfile(cwd);
|
|
5818
|
+
try {
|
|
5819
|
+
await run("docker", [
|
|
5820
|
+
"build",
|
|
5821
|
+
"--file=.gkm/docker/Dockerfile",
|
|
5822
|
+
`--tag=${fullImageName}`,
|
|
5823
|
+
"."
|
|
5824
|
+
], {
|
|
5825
|
+
cwd,
|
|
5826
|
+
env: {
|
|
5827
|
+
...process.env,
|
|
5828
|
+
DOCKER_BUILDKIT: "1"
|
|
5829
|
+
}
|
|
5830
|
+
});
|
|
5831
|
+
logger.log(`✅ Docker image built: ${fullImageName}`);
|
|
5832
|
+
} catch (error) {
|
|
5833
|
+
throw new DockerBuildFailed(fullImageName, error);
|
|
5834
|
+
} finally {
|
|
5835
|
+
cleanup?.();
|
|
5836
|
+
}
|
|
5837
|
+
}
|
|
5838
|
+
/**
|
|
5839
|
+
* Push Docker image to registry
|
|
5840
|
+
*/
|
|
5841
|
+
async function pushDockerImage(imageName, options, cwd) {
|
|
5842
|
+
const tag = options.tag ?? "latest";
|
|
5843
|
+
const registry = options.registry;
|
|
5844
|
+
if (!registry) throw new PushNeedsRegistry(imageName);
|
|
5845
|
+
const fullImageName = validateImageRef(`${registry}/${imageName}:${tag}`);
|
|
5846
|
+
logger.log(`\n🚀 Pushing Docker image: ${fullImageName}`);
|
|
5847
|
+
try {
|
|
5848
|
+
await run("docker", ["push", fullImageName], { cwd });
|
|
5849
|
+
logger.log(`✅ Docker image pushed: ${fullImageName}`);
|
|
5850
|
+
} catch (error) {
|
|
5851
|
+
throw new DockerPushFailed(fullImageName, error);
|
|
5852
|
+
}
|
|
5853
|
+
}
|
|
5854
|
+
/**
|
|
5855
|
+
* Get the package name from package.json in an app directory.
|
|
5856
|
+
*/
|
|
5857
|
+
function getAppPackageName(appPath) {
|
|
5858
|
+
try {
|
|
5859
|
+
const pkgPath = (0, node_path.join)(appPath, "package.json");
|
|
5860
|
+
if (!(0, node_fs.existsSync)(pkgPath)) return;
|
|
5861
|
+
const content = (0, node_fs.readFileSync)(pkgPath, "utf-8");
|
|
5862
|
+
return JSON.parse(content).name;
|
|
5863
|
+
} catch {
|
|
5864
|
+
return;
|
|
5865
|
+
}
|
|
5866
|
+
}
|
|
5867
|
+
/**
|
|
5868
|
+
* Generate Dockerfiles for all apps in a workspace.
|
|
5869
|
+
* @internal Exported for testing
|
|
5870
|
+
*/
|
|
5871
|
+
async function workspaceDockerCommand(workspace) {
|
|
5872
|
+
const results = [];
|
|
5873
|
+
const apps = Object.entries(workspace.apps);
|
|
5874
|
+
logger.log(`\n🐳 Generating Dockerfiles for workspace: ${workspace.name}`);
|
|
5875
|
+
const dockerDir = (0, node_path.join)(workspace.root, ".gkm", "docker");
|
|
5876
|
+
await (0, node_fs_promises.mkdir)(dockerDir, { recursive: true });
|
|
5877
|
+
const packageManager = detectPackageManager(workspace.root);
|
|
5878
|
+
logger.log(` Package manager: ${packageManager}`);
|
|
5879
|
+
for (const [appName, app] of apps) {
|
|
5880
|
+
if (app.type === "mobile") {
|
|
5881
|
+
logger.log(`\n 📱 Skipping Docker for ${appName} (mobile app — deploy via framework toolchain)`);
|
|
5882
|
+
continue;
|
|
5883
|
+
}
|
|
5884
|
+
const appPath = app.path;
|
|
5885
|
+
const turboPackage = getAppPackageName((0, node_path.join)(workspace.root, appPath)) ?? appName;
|
|
5886
|
+
const imageName = appName;
|
|
5887
|
+
const buildType = !!app.entry ? "entry" : app.type;
|
|
5888
|
+
logger.log(`\n 📄 Generating Dockerfile for ${appName} (${buildType})`);
|
|
5889
|
+
let dockerfile;
|
|
5890
|
+
if (app.type === "web") {
|
|
5891
|
+
const publicUrlArgs = getPublicUrlArgNames(app);
|
|
5892
|
+
const webOpts = {
|
|
5893
|
+
imageName,
|
|
5894
|
+
baseImage: "node:22-alpine",
|
|
5895
|
+
port: app.port,
|
|
5896
|
+
appPath,
|
|
5897
|
+
turboPackage,
|
|
5898
|
+
packageManager,
|
|
5899
|
+
publicUrlArgs
|
|
5900
|
+
};
|
|
5901
|
+
switch (app.framework) {
|
|
5902
|
+
case "vite":
|
|
5903
|
+
dockerfile = generateViteStaticDockerfile(webOpts);
|
|
5904
|
+
break;
|
|
5905
|
+
case "tanstack-start":
|
|
5906
|
+
case "remix":
|
|
5907
|
+
dockerfile = generateNodeWebDockerfile(webOpts);
|
|
5908
|
+
break;
|
|
5909
|
+
default: dockerfile = generateNextjsDockerfile(webOpts);
|
|
5910
|
+
}
|
|
5911
|
+
} else if (app.entry) dockerfile = generateEntryDockerfile({
|
|
5912
|
+
imageName,
|
|
5913
|
+
baseImage: "node:22-alpine",
|
|
5914
|
+
port: app.port,
|
|
5915
|
+
appPath,
|
|
5916
|
+
entry: app.entry,
|
|
5917
|
+
turboPackage,
|
|
5918
|
+
packageManager,
|
|
5919
|
+
healthCheckPath: "/health"
|
|
5920
|
+
});
|
|
5921
|
+
else dockerfile = generateBackendDockerfile({
|
|
5922
|
+
imageName,
|
|
5923
|
+
baseImage: "node:22-alpine",
|
|
5924
|
+
port: app.port,
|
|
5925
|
+
appPath,
|
|
5926
|
+
turboPackage,
|
|
5927
|
+
packageManager,
|
|
5928
|
+
healthCheckPath: "/health"
|
|
5929
|
+
});
|
|
5930
|
+
const dockerfilePath = (0, node_path.join)(dockerDir, `Dockerfile.${appName}`);
|
|
5931
|
+
await (0, node_fs_promises.writeFile)(dockerfilePath, dockerfile);
|
|
5932
|
+
logger.log(` Generated: .gkm/docker/Dockerfile.${appName}`);
|
|
5933
|
+
results.push({
|
|
5934
|
+
appName,
|
|
5935
|
+
type: app.type,
|
|
5936
|
+
dockerfile: dockerfilePath,
|
|
5937
|
+
imageName
|
|
5938
|
+
});
|
|
5939
|
+
}
|
|
5940
|
+
const dockerignore = generateDockerignore();
|
|
5941
|
+
const dockerignorePath = (0, node_path.join)(workspace.root, ".dockerignore");
|
|
5942
|
+
await (0, node_fs_promises.writeFile)(dockerignorePath, dockerignore);
|
|
5943
|
+
logger.log(`\n Generated: .dockerignore (workspace root)`);
|
|
5944
|
+
const composePath = await writeConstructsCompose(workspace);
|
|
5945
|
+
logger.log(`\n✅ Generated ${results.length} Dockerfile(s) + ${COMPOSE_PATH}`);
|
|
5946
|
+
logger.log("\n📋 Build commands:");
|
|
5947
|
+
for (const result of results) {
|
|
5948
|
+
const icon = result.type === "backend" ? "⚙️" : result.type === "mobile" ? "📱" : "🌐";
|
|
5949
|
+
logger.log(` ${icon} docker build -f .gkm/docker/Dockerfile.${result.appName} -t ${result.imageName} .`);
|
|
5950
|
+
}
|
|
5951
|
+
printRunInstructions(workspace);
|
|
5952
|
+
return {
|
|
5953
|
+
apps: results,
|
|
5954
|
+
dockerCompose: composePath,
|
|
5955
|
+
dockerignore: dockerignorePath
|
|
5956
|
+
};
|
|
5957
|
+
}
|
|
5958
|
+
/**
|
|
5959
|
+
* Write `docker-compose.constructs.yml` — the same file `gkm dev` writes, with
|
|
5960
|
+
* the apps beside the containers their constructs derive.
|
|
5961
|
+
*
|
|
5962
|
+
* Through reconcile rather than a second generator, so what `gkm docker`
|
|
5963
|
+
* describes and what `gkm dev` runs cannot disagree. Nothing is started.
|
|
5964
|
+
*/
|
|
5965
|
+
async function writeConstructsCompose(workspace) {
|
|
5966
|
+
await reconcileWorkspace(workspace, {
|
|
5967
|
+
stage: workspace.stages.local,
|
|
5968
|
+
start: false
|
|
5969
|
+
});
|
|
5970
|
+
logger.log(` Generated: ${COMPOSE_PATH} (project root)`);
|
|
5971
|
+
return (0, node_path.join)(workspace.root, COMPOSE_PATH);
|
|
5972
|
+
}
|
|
5973
|
+
function printRunInstructions(workspace) {
|
|
5974
|
+
const project = (0, node_fs.existsSync)((0, node_path.join)(workspace.root, "docker-compose.yml")) ? " -f docker-compose.yml" : "";
|
|
5975
|
+
logger.log("\n📋 Run everything (the apps sit behind the \"apps\" profile):");
|
|
5976
|
+
logger.log(" gkm setup # starts the containers and creates what they hold");
|
|
5977
|
+
logger.log(` docker compose -f ${COMPOSE_PATH}${project} --profile apps up --build`);
|
|
5978
|
+
}
|
|
5979
|
+
//#endregion
|
|
5980
|
+
//#region src/deploy/sniffer.ts
|
|
5981
|
+
const __filename$1 = (0, node_url.fileURLToPath)(require("url").pathToFileURL(__filename).href);
|
|
5982
|
+
const __dirname$1 = (0, node_path.dirname)(__filename$1);
|
|
5983
|
+
/**
|
|
5984
|
+
* Resolve the tsx package path from the CLI package's dependencies.
|
|
5985
|
+
* This ensures tsx is available regardless of whether the target project has it installed.
|
|
5986
|
+
*/
|
|
5987
|
+
function resolveTsxPath() {
|
|
5988
|
+
return (0, node_module.createRequire)(require("url").pathToFileURL(__filename).href).resolve("tsx");
|
|
5989
|
+
}
|
|
5990
|
+
/**
|
|
5991
|
+
* Resolve the path to a sniffer helper file.
|
|
5992
|
+
* Handles both dev (.ts with tsx) and production (.mjs from dist).
|
|
5993
|
+
*
|
|
5994
|
+
* In production: sniffer.ts is bundled into dist/index.mjs, but sniffer helper
|
|
5995
|
+
* files are output to dist/deploy/ as standalone modules for subprocess loading.
|
|
5996
|
+
*
|
|
5997
|
+
* In development: All files are in src/deploy/ and loaded via tsx.
|
|
5998
|
+
*/
|
|
5999
|
+
function resolveSnifferFile(baseName) {
|
|
6000
|
+
const deployMjsPath = (0, node_path.resolve)(__dirname$1, "deploy", `${baseName}.mjs`);
|
|
6001
|
+
if ((0, node_fs.existsSync)(deployMjsPath)) return deployMjsPath;
|
|
6002
|
+
const mjsPath = (0, node_path.resolve)(__dirname$1, `${baseName}.mjs`);
|
|
6003
|
+
if ((0, node_fs.existsSync)(mjsPath)) return mjsPath;
|
|
6004
|
+
const tsPath = (0, node_path.resolve)(__dirname$1, `${baseName}.ts`);
|
|
6005
|
+
if ((0, node_fs.existsSync)(tsPath)) return tsPath;
|
|
6006
|
+
return tsPath;
|
|
6007
|
+
}
|
|
6008
|
+
/**
|
|
6009
|
+
* Get required environment variables for an app.
|
|
6010
|
+
*
|
|
6011
|
+
* Detection strategy (in order):
|
|
6012
|
+
* 1. Frontend apps: Returns empty (no server secrets)
|
|
6013
|
+
* 2. Apps with `requiredEnv`: Uses explicit list from config
|
|
6014
|
+
* 3. Entry apps: Imports entry file in subprocess to capture config.parse() calls
|
|
6015
|
+
* 4. Route-based apps: Loads route files and calls getEnvironment() on each construct
|
|
6016
|
+
* 5. Apps with `envParser` (no routes): Runs SnifferEnvironmentParser to detect usage
|
|
6017
|
+
* 6. Apps with neither: Returns empty
|
|
6018
|
+
*
|
|
6019
|
+
* This function handles "fire and forget" async operations gracefully,
|
|
6020
|
+
* capturing errors and unhandled rejections without failing the build.
|
|
6021
|
+
*
|
|
6022
|
+
* @param app - The normalized app configuration
|
|
6023
|
+
* @param appName - The name of the app
|
|
6024
|
+
* @param workspacePath - Absolute path to the workspace root
|
|
6025
|
+
* @param options - Optional configuration for sniffing behavior
|
|
6026
|
+
* @returns The sniffed environment with required variables
|
|
6027
|
+
*/
|
|
6028
|
+
async function sniffAppEnvironment(app, appName, workspacePath, options = {}) {
|
|
6029
|
+
const { logWarnings = true, markOptional = false } = options;
|
|
6030
|
+
if (app.type === "web" || app.type === "mobile") {
|
|
6031
|
+
const publicPrefix = require_workspace.getPublicEnvPrefix(app.framework);
|
|
6032
|
+
const depVars = publicPrefix ? (app.dependencies ?? []).map((dep) => `${publicPrefix}${dep.toUpperCase()}_URL`) : [];
|
|
6033
|
+
if (app.config) {
|
|
6034
|
+
const sniffedVars = [];
|
|
6035
|
+
const sniffedOptional = [];
|
|
6036
|
+
const configPaths = [];
|
|
6037
|
+
if (app.config.client) configPaths.push(app.config.client);
|
|
6038
|
+
if (app.config.server) configPaths.push(app.config.server);
|
|
6039
|
+
for (const configPath of configPaths) {
|
|
6040
|
+
const result = await sniffEntryFile(configPath, app.path, workspacePath);
|
|
6041
|
+
if (logWarnings && result.error) require_discover.output.warn(`[sniffer] ${appName}: Config file "${configPath}" threw error during sniffing (env vars still captured): ${result.error.message}`);
|
|
6042
|
+
sniffedVars.push(...result.envVars);
|
|
6043
|
+
sniffedOptional.push(...result.optionalEnvVars);
|
|
6044
|
+
}
|
|
6045
|
+
const optionalEnvVars = [...new Set(sniffedOptional)];
|
|
6046
|
+
return {
|
|
6047
|
+
appName,
|
|
6048
|
+
requiredEnvVars: applyMarkOptional([.../* @__PURE__ */ new Set([...depVars, ...sniffedVars])], optionalEnvVars, markOptional),
|
|
6049
|
+
optionalEnvVars
|
|
6050
|
+
};
|
|
6051
|
+
}
|
|
6052
|
+
return {
|
|
6053
|
+
appName,
|
|
6054
|
+
requiredEnvVars: depVars,
|
|
6055
|
+
optionalEnvVars: []
|
|
6056
|
+
};
|
|
6057
|
+
}
|
|
6058
|
+
if (app.entry) {
|
|
6059
|
+
const result = await sniffEntryFile(app.entry, app.path, workspacePath);
|
|
6060
|
+
if (logWarnings && result.error) require_discover.output.warn(`[sniffer] ${appName}: Entry file threw error during sniffing (env vars still captured): ${result.error.message}`);
|
|
6061
|
+
return {
|
|
6062
|
+
appName,
|
|
6063
|
+
requiredEnvVars: applyMarkOptional(result.envVars, result.optionalEnvVars, markOptional),
|
|
6064
|
+
optionalEnvVars: result.optionalEnvVars
|
|
6065
|
+
};
|
|
6066
|
+
}
|
|
6067
|
+
if (app.routes) {
|
|
6068
|
+
const result = await sniffRouteFiles(normalizeRoutes(app.routes), app.path, workspacePath);
|
|
6069
|
+
if (logWarnings && result.error) require_discover.output.warn(`[sniffer] ${appName}: Route sniffing threw error (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.envParser) {
|
|
6077
|
+
const result = await sniffEnvParser(app.envParser, app.path, workspacePath);
|
|
6078
|
+
if (logWarnings) {
|
|
6079
|
+
if (result.error) require_discover.output.warn(`[sniffer] ${appName}: envParser threw error during sniffing (env vars still captured): ${result.error.message}`);
|
|
6080
|
+
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(", ")}`);
|
|
6081
|
+
}
|
|
6082
|
+
return {
|
|
6083
|
+
appName,
|
|
6084
|
+
requiredEnvVars: applyMarkOptional(result.envVars, result.optionalEnvVars, markOptional),
|
|
6085
|
+
optionalEnvVars: result.optionalEnvVars
|
|
6086
|
+
};
|
|
6087
|
+
}
|
|
6088
|
+
return {
|
|
6089
|
+
appName,
|
|
6090
|
+
requiredEnvVars: [],
|
|
6091
|
+
optionalEnvVars: []
|
|
6092
|
+
};
|
|
6093
|
+
}
|
|
6094
|
+
/**
|
|
6095
|
+
* Apply the `?` suffix to optional variables in an env var list.
|
|
6096
|
+
* Only modifies the list when `markOptional` is true and there are optional vars.
|
|
6097
|
+
*/
|
|
6098
|
+
function applyMarkOptional(envVars, optionalEnvVars, markOptional) {
|
|
6099
|
+
if (!markOptional || optionalEnvVars.length === 0) return envVars;
|
|
6100
|
+
const optionalSet = new Set(optionalEnvVars);
|
|
6101
|
+
return envVars.map((v) => optionalSet.has(v) ? `${v}?` : v);
|
|
6102
|
+
}
|
|
6103
|
+
/**
|
|
6104
|
+
* Sniff an entry file by importing it in a subprocess.
|
|
6105
|
+
*
|
|
6106
|
+
* Entry apps call `config.parse()` at module load time. To capture which
|
|
6107
|
+
* env vars are accessed, we:
|
|
6108
|
+
* 1. Spawn a subprocess with a module loader hook
|
|
6109
|
+
* 2. The loader intercepts `@geekmidas/envkit` and replaces EnvironmentParser
|
|
6110
|
+
* with SnifferEnvironmentParser
|
|
6111
|
+
* 3. Import the entry file (triggers config.parse())
|
|
6112
|
+
* 4. Capture and return the accessed env var names
|
|
6113
|
+
*
|
|
6114
|
+
* This approach provides process isolation - each app is sniffed in its own
|
|
6115
|
+
* subprocess, preventing module cache pollution.
|
|
6116
|
+
*
|
|
6117
|
+
* @param entryPath - Relative path to the entry file (e.g., './src/index.ts')
|
|
6118
|
+
* @param appPath - The app's path relative to workspace (e.g., 'apps/auth')
|
|
6119
|
+
* @param workspacePath - Absolute path to workspace root
|
|
6120
|
+
* @returns EntrySniffResult with env vars and optional error
|
|
6121
|
+
*/
|
|
6122
|
+
async function sniffEntryFile(entryPath, appPath, workspacePath) {
|
|
6123
|
+
const fullEntryPath = (0, node_path.resolve)(workspacePath, appPath, entryPath);
|
|
6124
|
+
const loaderPath = resolveSnifferFile("sniffer-loader");
|
|
6125
|
+
const workerPath = resolveSnifferFile("sniffer-worker");
|
|
6126
|
+
return new Promise((resolvePromise) => {
|
|
6127
|
+
const child = (0, node_child_process.spawn)("node", [
|
|
6128
|
+
"--import",
|
|
6129
|
+
loaderPath,
|
|
6130
|
+
workerPath,
|
|
6131
|
+
fullEntryPath
|
|
6132
|
+
], {
|
|
6133
|
+
cwd: (0, node_path.resolve)(workspacePath, appPath),
|
|
6134
|
+
stdio: [
|
|
6135
|
+
"ignore",
|
|
6136
|
+
"pipe",
|
|
6137
|
+
"pipe"
|
|
6138
|
+
],
|
|
6139
|
+
env: {
|
|
6140
|
+
...process.env,
|
|
6141
|
+
NODE_OPTIONS: withOwningTsconfigJsx("--import=tsx")
|
|
6142
|
+
}
|
|
6143
|
+
});
|
|
6144
|
+
let stdout = "";
|
|
6145
|
+
let stderr = "";
|
|
6146
|
+
child.stdout.on("data", (data) => {
|
|
6147
|
+
stdout += data.toString();
|
|
6148
|
+
});
|
|
6149
|
+
child.stderr.on("data", (data) => {
|
|
6150
|
+
stderr += data.toString();
|
|
6151
|
+
});
|
|
6152
|
+
child.on("close", (code) => {
|
|
6153
|
+
try {
|
|
6154
|
+
const jsonMatch = stdout.match(/\{[^{}]*"envVars"[^{}]*\}[^{]*$/);
|
|
6155
|
+
if (jsonMatch) {
|
|
6156
|
+
const result = JSON.parse(jsonMatch[0]);
|
|
6157
|
+
resolvePromise({
|
|
6158
|
+
envVars: result.envVars || [],
|
|
6159
|
+
optionalEnvVars: result.optionalEnvVars || [],
|
|
6160
|
+
error: result.error ? new Error(result.error) : void 0
|
|
6161
|
+
});
|
|
6162
|
+
return;
|
|
6163
|
+
}
|
|
6164
|
+
} catch {}
|
|
6165
|
+
resolvePromise({
|
|
6166
|
+
envVars: [],
|
|
6167
|
+
optionalEnvVars: [],
|
|
6168
|
+
error: /* @__PURE__ */ new Error(`Failed to sniff entry file (exit code ${code}): ${stderr || stdout || "No output"}`)
|
|
6169
|
+
});
|
|
6170
|
+
});
|
|
6171
|
+
child.on("error", (err) => {
|
|
6172
|
+
resolvePromise({
|
|
6173
|
+
envVars: [],
|
|
6174
|
+
optionalEnvVars: [],
|
|
6175
|
+
error: err
|
|
6176
|
+
});
|
|
6177
|
+
});
|
|
6178
|
+
});
|
|
6179
|
+
}
|
|
6180
|
+
/**
|
|
6181
|
+
* Sniff route files by loading constructs and calling getEnvironment().
|
|
6182
|
+
*
|
|
6183
|
+
* Route-based apps have endpoints, functions, crons, and subscribers that
|
|
6184
|
+
* use services. Each service's register() method accesses environment variables.
|
|
6185
|
+
*
|
|
6186
|
+
* This runs in a subprocess with tsx loader to properly handle TypeScript
|
|
6187
|
+
* compilation and path alias resolution (e.g., `src/...` imports).
|
|
6188
|
+
*
|
|
6189
|
+
* @param routes - Glob pattern(s) for route files
|
|
6190
|
+
* @param appPath - The app's path relative to workspace (e.g., 'apps/api')
|
|
6191
|
+
* @param workspacePath - Absolute path to workspace root
|
|
6192
|
+
* @returns EntrySniffResult with env vars and optional error
|
|
6193
|
+
*/
|
|
6194
|
+
async function sniffRouteFiles(routes, appPath, workspacePath) {
|
|
6195
|
+
const fullAppPath = (0, node_path.resolve)(workspacePath, appPath);
|
|
6196
|
+
const workerPath = resolveSnifferFile("sniffer-routes-worker");
|
|
6197
|
+
const tsxPath = resolveTsxPath();
|
|
6198
|
+
const pattern = (Array.isArray(routes) ? routes : [routes])[0];
|
|
6199
|
+
if (!pattern) return {
|
|
6200
|
+
envVars: [],
|
|
6201
|
+
optionalEnvVars: [],
|
|
6202
|
+
error: /* @__PURE__ */ new Error("No route patterns provided")
|
|
6203
|
+
};
|
|
6204
|
+
return new Promise((resolvePromise) => {
|
|
6205
|
+
const child = (0, node_child_process.spawn)("node", [
|
|
6206
|
+
"--import",
|
|
6207
|
+
tsxPath,
|
|
6208
|
+
workerPath,
|
|
6209
|
+
fullAppPath,
|
|
6210
|
+
pattern
|
|
6211
|
+
], {
|
|
6212
|
+
cwd: fullAppPath,
|
|
6213
|
+
stdio: [
|
|
6214
|
+
"ignore",
|
|
6215
|
+
"pipe",
|
|
6216
|
+
"pipe"
|
|
6217
|
+
],
|
|
6218
|
+
env: { ...process.env }
|
|
6219
|
+
});
|
|
6220
|
+
let stdout = "";
|
|
6221
|
+
let stderr = "";
|
|
6222
|
+
child.stdout.on("data", (data) => {
|
|
6223
|
+
stdout += data.toString();
|
|
6224
|
+
});
|
|
6225
|
+
child.stderr.on("data", (data) => {
|
|
6226
|
+
stderr += data.toString();
|
|
6227
|
+
});
|
|
6228
|
+
child.on("close", (code) => {
|
|
6229
|
+
if (stderr) stderr.split("\n").filter((line) => line.trim()).forEach((line) => require_discover.output.warn(line));
|
|
6230
|
+
try {
|
|
6231
|
+
const jsonMatch = stdout.match(/\{[^{}]*"envVars"[^{}]*\}[^{]*$/);
|
|
6232
|
+
if (jsonMatch) {
|
|
6233
|
+
const result = JSON.parse(jsonMatch[0]);
|
|
6234
|
+
resolvePromise({
|
|
6235
|
+
envVars: result.envVars || [],
|
|
6236
|
+
optionalEnvVars: result.optionalEnvVars || [],
|
|
6237
|
+
error: result.error ? new Error(result.error) : void 0
|
|
6238
|
+
});
|
|
6239
|
+
return;
|
|
6240
|
+
}
|
|
6241
|
+
} catch {}
|
|
6242
|
+
resolvePromise({
|
|
6243
|
+
envVars: [],
|
|
6244
|
+
optionalEnvVars: [],
|
|
6245
|
+
error: /* @__PURE__ */ new Error(`Failed to sniff route files (exit code ${code}): ${stderr || stdout || "No output"}`)
|
|
6246
|
+
});
|
|
6247
|
+
});
|
|
6248
|
+
child.on("error", (err) => {
|
|
6249
|
+
resolvePromise({
|
|
6250
|
+
envVars: [],
|
|
6251
|
+
optionalEnvVars: [],
|
|
6252
|
+
error: err
|
|
6253
|
+
});
|
|
6254
|
+
});
|
|
6255
|
+
});
|
|
6256
|
+
}
|
|
6257
|
+
/**
|
|
6258
|
+
* Run the SnifferEnvironmentParser on an envParser module to detect
|
|
6259
|
+
* which environment variables it accesses.
|
|
6260
|
+
*
|
|
6261
|
+
* This function handles "fire and forget" async operations by using
|
|
6262
|
+
* the shared sniffWithFireAndForget utility from @geekmidas/envkit.
|
|
6263
|
+
*
|
|
6264
|
+
* @param envParserPath - The envParser config (e.g., './src/config/env#envParser')
|
|
6265
|
+
* @param appPath - The app's path relative to workspace
|
|
6266
|
+
* @param workspacePath - Absolute path to workspace root
|
|
6267
|
+
* @returns SniffResult with env vars and any errors encountered
|
|
6268
|
+
*/
|
|
6269
|
+
async function sniffEnvParser(envParserPath, appPath, workspacePath) {
|
|
6270
|
+
const [modulePath, exportName = "default"] = envParserPath.split("#");
|
|
6271
|
+
if (!modulePath) return {
|
|
6272
|
+
envVars: [],
|
|
6273
|
+
optionalEnvVars: [],
|
|
6274
|
+
unhandledRejections: []
|
|
6275
|
+
};
|
|
6276
|
+
const fullPath = (0, node_path.resolve)(workspacePath, appPath, modulePath);
|
|
6277
|
+
let SnifferEnvironmentParser;
|
|
6278
|
+
let sniffWithFireAndForget;
|
|
6279
|
+
try {
|
|
6280
|
+
const envkitModule = await import("@geekmidas/envkit/sniffer");
|
|
6281
|
+
SnifferEnvironmentParser = envkitModule.SnifferEnvironmentParser;
|
|
6282
|
+
sniffWithFireAndForget = envkitModule.sniffWithFireAndForget;
|
|
6283
|
+
} catch (error) {
|
|
6284
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
6285
|
+
require_discover.output.warn(`[sniffer] Failed to import SnifferEnvironmentParser: ${message}`);
|
|
6286
|
+
return {
|
|
6287
|
+
envVars: [],
|
|
6288
|
+
optionalEnvVars: [],
|
|
6289
|
+
unhandledRejections: []
|
|
6290
|
+
};
|
|
6291
|
+
}
|
|
6292
|
+
const sniffer = new SnifferEnvironmentParser();
|
|
6293
|
+
return sniffWithFireAndForget(sniffer, async () => {
|
|
6294
|
+
const envParser = (await import((0, node_url.pathToFileURL)(fullPath).href))[exportName];
|
|
6295
|
+
if (typeof envParser !== "function") {
|
|
6296
|
+
require_discover.output.warn(`[sniffer] Export "${exportName}" from "${modulePath}" is not a function`);
|
|
6297
|
+
return;
|
|
6298
|
+
}
|
|
6299
|
+
const result = envParser(sniffer);
|
|
6300
|
+
if (result && typeof result.parse === "function") try {
|
|
6301
|
+
result.parse();
|
|
6302
|
+
} catch {}
|
|
6303
|
+
});
|
|
6304
|
+
}
|
|
6305
|
+
/**
|
|
6306
|
+
* Sniff environment requirements for multiple apps.
|
|
6307
|
+
*
|
|
6308
|
+
* @param apps - Map of app name to app config
|
|
6309
|
+
* @param workspacePath - Absolute path to workspace root
|
|
6310
|
+
* @param options - Optional configuration for sniffing behavior
|
|
6311
|
+
* @returns Map of app name to sniffed environment
|
|
6312
|
+
*/
|
|
6313
|
+
async function sniffAllApps(apps, workspacePath, options = {}) {
|
|
6314
|
+
const results = /* @__PURE__ */ new Map();
|
|
6315
|
+
for (const [appName, app] of Object.entries(apps)) {
|
|
6316
|
+
const sniffed = await sniffAppEnvironment(app, appName, workspacePath, options);
|
|
6317
|
+
results.set(appName, sniffed);
|
|
6318
|
+
}
|
|
6319
|
+
return results;
|
|
6320
|
+
}
|
|
6321
|
+
//#endregion
|
|
6322
|
+
Object.defineProperty(exports, "DockerBuildFailed", {
|
|
6323
|
+
enumerable: true,
|
|
6324
|
+
get: function() {
|
|
6325
|
+
return DockerBuildFailed;
|
|
6326
|
+
}
|
|
6327
|
+
});
|
|
6328
|
+
Object.defineProperty(exports, "DockerPushFailed", {
|
|
6329
|
+
enumerable: true,
|
|
6330
|
+
get: function() {
|
|
6331
|
+
return DockerPushFailed;
|
|
6332
|
+
}
|
|
6333
|
+
});
|
|
6334
|
+
Object.defineProperty(exports, "DocumentStateStore", {
|
|
6335
|
+
enumerable: true,
|
|
6336
|
+
get: function() {
|
|
6337
|
+
return DocumentStateStore;
|
|
6338
|
+
}
|
|
6339
|
+
});
|
|
6340
|
+
Object.defineProperty(exports, "DokployApi", {
|
|
6341
|
+
enumerable: true,
|
|
6342
|
+
get: function() {
|
|
6343
|
+
return DokployApi;
|
|
6344
|
+
}
|
|
6345
|
+
});
|
|
6346
|
+
Object.defineProperty(exports, "DokployApiError", {
|
|
6347
|
+
enumerable: true,
|
|
6348
|
+
get: function() {
|
|
6349
|
+
return DokployApiError;
|
|
6350
|
+
}
|
|
6351
|
+
});
|
|
6352
|
+
Object.defineProperty(exports, "FAKE_ENV", {
|
|
6353
|
+
enumerable: true,
|
|
6354
|
+
get: function() {
|
|
6355
|
+
return FAKE_ENV;
|
|
6356
|
+
}
|
|
6357
|
+
});
|
|
6358
|
+
Object.defineProperty(exports, "LOCAL_CA_PATH", {
|
|
6359
|
+
enumerable: true,
|
|
6360
|
+
get: function() {
|
|
6361
|
+
return LOCAL_CA_PATH;
|
|
6362
|
+
}
|
|
6363
|
+
});
|
|
6364
|
+
Object.defineProperty(exports, "StageStateMissing", {
|
|
6365
|
+
enumerable: true,
|
|
6366
|
+
get: function() {
|
|
6367
|
+
return StageStateMissing;
|
|
6368
|
+
}
|
|
6369
|
+
});
|
|
6370
|
+
Object.defineProperty(exports, "StateLocked", {
|
|
6371
|
+
enumerable: true,
|
|
6372
|
+
get: function() {
|
|
6373
|
+
return StateLocked;
|
|
6374
|
+
}
|
|
6375
|
+
});
|
|
6376
|
+
Object.defineProperty(exports, "StateStoreBusy", {
|
|
6377
|
+
enumerable: true,
|
|
6378
|
+
get: function() {
|
|
6379
|
+
return StateStoreBusy;
|
|
6380
|
+
}
|
|
6381
|
+
});
|
|
6382
|
+
Object.defineProperty(exports, "StateStoreWithoutLocking", {
|
|
6383
|
+
enumerable: true,
|
|
6384
|
+
get: function() {
|
|
6385
|
+
return StateStoreWithoutLocking;
|
|
6386
|
+
}
|
|
6387
|
+
});
|
|
6388
|
+
Object.defineProperty(exports, "StateVersionConflict", {
|
|
6389
|
+
enumerable: true,
|
|
6390
|
+
get: function() {
|
|
6391
|
+
return StateVersionConflict;
|
|
6392
|
+
}
|
|
6393
|
+
});
|
|
6394
|
+
Object.defineProperty(exports, "applyPostgres", {
|
|
6395
|
+
enumerable: true,
|
|
6396
|
+
get: function() {
|
|
6397
|
+
return applyPostgres;
|
|
6398
|
+
}
|
|
6399
|
+
});
|
|
6400
|
+
Object.defineProperty(exports, "backendsOf", {
|
|
6401
|
+
enumerable: true,
|
|
6402
|
+
get: function() {
|
|
6403
|
+
return backendsOf;
|
|
6404
|
+
}
|
|
6405
|
+
});
|
|
6406
|
+
Object.defineProperty(exports, "cacheBackendFor", {
|
|
6407
|
+
enumerable: true,
|
|
6408
|
+
get: function() {
|
|
6409
|
+
return cacheBackendFor;
|
|
6410
|
+
}
|
|
6411
|
+
});
|
|
6412
|
+
Object.defineProperty(exports, "canPrompt", {
|
|
6413
|
+
enumerable: true,
|
|
6414
|
+
get: function() {
|
|
6415
|
+
return canPrompt;
|
|
6416
|
+
}
|
|
6417
|
+
});
|
|
6418
|
+
Object.defineProperty(exports, "constructGlobs", {
|
|
6419
|
+
enumerable: true,
|
|
6420
|
+
get: function() {
|
|
6421
|
+
return constructGlobs;
|
|
6422
|
+
}
|
|
6423
|
+
});
|
|
6424
|
+
Object.defineProperty(exports, "contentVersion", {
|
|
6425
|
+
enumerable: true,
|
|
6426
|
+
get: function() {
|
|
6427
|
+
return contentVersion;
|
|
6428
|
+
}
|
|
6429
|
+
});
|
|
6430
|
+
Object.defineProperty(exports, "createCredentialsPreload", {
|
|
6431
|
+
enumerable: true,
|
|
6432
|
+
get: function() {
|
|
6433
|
+
return createCredentialsPreload;
|
|
6434
|
+
}
|
|
6435
|
+
});
|
|
6436
|
+
Object.defineProperty(exports, "createEntryWrapper", {
|
|
6437
|
+
enumerable: true,
|
|
6438
|
+
get: function() {
|
|
6439
|
+
return createEntryWrapper;
|
|
6440
|
+
}
|
|
6441
|
+
});
|
|
6442
|
+
Object.defineProperty(exports, "createStateStore", {
|
|
6443
|
+
enumerable: true,
|
|
6444
|
+
get: function() {
|
|
6445
|
+
return createStateStore;
|
|
6446
|
+
}
|
|
6447
|
+
});
|
|
6448
|
+
Object.defineProperty(exports, "derivedContainers", {
|
|
6449
|
+
enumerable: true,
|
|
6450
|
+
get: function() {
|
|
6451
|
+
return derivedContainers;
|
|
6452
|
+
}
|
|
6453
|
+
});
|
|
6454
|
+
Object.defineProperty(exports, "describeServices", {
|
|
6455
|
+
enumerable: true,
|
|
6456
|
+
get: function() {
|
|
6457
|
+
return describeServices;
|
|
6458
|
+
}
|
|
6459
|
+
});
|
|
6460
|
+
Object.defineProperty(exports, "dockerCommand", {
|
|
6461
|
+
enumerable: true,
|
|
6462
|
+
get: function() {
|
|
6463
|
+
return dockerCommand;
|
|
6464
|
+
}
|
|
6465
|
+
});
|
|
6466
|
+
Object.defineProperty(exports, "encryptSecrets", {
|
|
6467
|
+
enumerable: true,
|
|
6468
|
+
get: function() {
|
|
6469
|
+
return encryptSecrets;
|
|
6470
|
+
}
|
|
6471
|
+
});
|
|
6472
|
+
Object.defineProperty(exports, "encryption_exports", {
|
|
6473
|
+
enumerable: true,
|
|
6474
|
+
get: function() {
|
|
6475
|
+
return encryption_exports;
|
|
6476
|
+
}
|
|
6477
|
+
});
|
|
6478
|
+
Object.defineProperty(exports, "eventsBackendFor", {
|
|
6479
|
+
enumerable: true,
|
|
6480
|
+
get: function() {
|
|
6481
|
+
return eventsBackendFor;
|
|
6482
|
+
}
|
|
6483
|
+
});
|
|
6484
|
+
Object.defineProperty(exports, "fakeKey", {
|
|
6485
|
+
enumerable: true,
|
|
6486
|
+
get: function() {
|
|
6487
|
+
return fakeKey;
|
|
6488
|
+
}
|
|
6489
|
+
});
|
|
6490
|
+
Object.defineProperty(exports, "findAvailablePort", {
|
|
6491
|
+
enumerable: true,
|
|
6492
|
+
get: function() {
|
|
6493
|
+
return findAvailablePort;
|
|
6494
|
+
}
|
|
6495
|
+
});
|
|
6496
|
+
Object.defineProperty(exports, "forgetState", {
|
|
6497
|
+
enumerable: true,
|
|
6498
|
+
get: function() {
|
|
3106
6499
|
return forgetState;
|
|
3107
6500
|
}
|
|
3108
6501
|
});
|
|
6502
|
+
Object.defineProperty(exports, "getDokployCredentials", {
|
|
6503
|
+
enumerable: true,
|
|
6504
|
+
get: function() {
|
|
6505
|
+
return getDokployCredentials;
|
|
6506
|
+
}
|
|
6507
|
+
});
|
|
6508
|
+
Object.defineProperty(exports, "getDokployRegistryId", {
|
|
6509
|
+
enumerable: true,
|
|
6510
|
+
get: function() {
|
|
6511
|
+
return getDokployRegistryId;
|
|
6512
|
+
}
|
|
6513
|
+
});
|
|
6514
|
+
Object.defineProperty(exports, "getDokployToken", {
|
|
6515
|
+
enumerable: true,
|
|
6516
|
+
get: function() {
|
|
6517
|
+
return getDokployToken;
|
|
6518
|
+
}
|
|
6519
|
+
});
|
|
6520
|
+
Object.defineProperty(exports, "getHostingerToken", {
|
|
6521
|
+
enumerable: true,
|
|
6522
|
+
get: function() {
|
|
6523
|
+
return getHostingerToken;
|
|
6524
|
+
}
|
|
6525
|
+
});
|
|
6526
|
+
Object.defineProperty(exports, "isMainFrontendApp", {
|
|
6527
|
+
enumerable: true,
|
|
6528
|
+
get: function() {
|
|
6529
|
+
return isMainFrontendApp;
|
|
6530
|
+
}
|
|
6531
|
+
});
|
|
3109
6532
|
Object.defineProperty(exports, "isPortAvailable", {
|
|
3110
6533
|
enumerable: true,
|
|
3111
6534
|
get: function() {
|
|
3112
6535
|
return isPortAvailable;
|
|
3113
6536
|
}
|
|
3114
6537
|
});
|
|
6538
|
+
Object.defineProperty(exports, "keyFingerprint", {
|
|
6539
|
+
enumerable: true,
|
|
6540
|
+
get: function() {
|
|
6541
|
+
return keyFingerprint;
|
|
6542
|
+
}
|
|
6543
|
+
});
|
|
3115
6544
|
Object.defineProperty(exports, "loadEnvFiles", {
|
|
3116
6545
|
enumerable: true,
|
|
3117
6546
|
get: function() {
|
|
@@ -3124,6 +6553,24 @@ Object.defineProperty(exports, "loadSecretsForApp", {
|
|
|
3124
6553
|
return loadSecretsForApp;
|
|
3125
6554
|
}
|
|
3126
6555
|
});
|
|
6556
|
+
Object.defineProperty(exports, "loginCommand", {
|
|
6557
|
+
enumerable: true,
|
|
6558
|
+
get: function() {
|
|
6559
|
+
return loginCommand;
|
|
6560
|
+
}
|
|
6561
|
+
});
|
|
6562
|
+
Object.defineProperty(exports, "logoutCommand", {
|
|
6563
|
+
enumerable: true,
|
|
6564
|
+
get: function() {
|
|
6565
|
+
return logoutCommand;
|
|
6566
|
+
}
|
|
6567
|
+
});
|
|
6568
|
+
Object.defineProperty(exports, "newLockHolder", {
|
|
6569
|
+
enumerable: true,
|
|
6570
|
+
get: function() {
|
|
6571
|
+
return newLockHolder;
|
|
6572
|
+
}
|
|
6573
|
+
});
|
|
3127
6574
|
Object.defineProperty(exports, "pgClient", {
|
|
3128
6575
|
enumerable: true,
|
|
3129
6576
|
get: function() {
|
|
@@ -3148,28 +6595,106 @@ Object.defineProperty(exports, "primaryPortKey", {
|
|
|
3148
6595
|
return primaryPortKey;
|
|
3149
6596
|
}
|
|
3150
6597
|
});
|
|
6598
|
+
Object.defineProperty(exports, "prompt", {
|
|
6599
|
+
enumerable: true,
|
|
6600
|
+
get: function() {
|
|
6601
|
+
return prompt;
|
|
6602
|
+
}
|
|
6603
|
+
});
|
|
6604
|
+
Object.defineProperty(exports, "providerOf", {
|
|
6605
|
+
enumerable: true,
|
|
6606
|
+
get: function() {
|
|
6607
|
+
return providerOf;
|
|
6608
|
+
}
|
|
6609
|
+
});
|
|
3151
6610
|
Object.defineProperty(exports, "quoteIdentifier", {
|
|
3152
6611
|
enumerable: true,
|
|
3153
6612
|
get: function() {
|
|
3154
6613
|
return quoteIdentifier;
|
|
3155
6614
|
}
|
|
3156
6615
|
});
|
|
6616
|
+
Object.defineProperty(exports, "readCredentials", {
|
|
6617
|
+
enumerable: true,
|
|
6618
|
+
get: function() {
|
|
6619
|
+
return readCredentials;
|
|
6620
|
+
}
|
|
6621
|
+
});
|
|
3157
6622
|
Object.defineProperty(exports, "reconcileWorkspace", {
|
|
3158
6623
|
enumerable: true,
|
|
3159
6624
|
get: function() {
|
|
3160
6625
|
return reconcileWorkspace;
|
|
3161
6626
|
}
|
|
3162
6627
|
});
|
|
6628
|
+
Object.defineProperty(exports, "resolveHost", {
|
|
6629
|
+
enumerable: true,
|
|
6630
|
+
get: function() {
|
|
6631
|
+
return resolveHost;
|
|
6632
|
+
}
|
|
6633
|
+
});
|
|
3163
6634
|
Object.defineProperty(exports, "resourceName", {
|
|
3164
6635
|
enumerable: true,
|
|
3165
6636
|
get: function() {
|
|
3166
6637
|
return resourceName;
|
|
3167
6638
|
}
|
|
3168
6639
|
});
|
|
3169
|
-
Object.defineProperty(exports, "
|
|
6640
|
+
Object.defineProperty(exports, "run", {
|
|
6641
|
+
enumerable: true,
|
|
6642
|
+
get: function() {
|
|
6643
|
+
return run;
|
|
6644
|
+
}
|
|
6645
|
+
});
|
|
6646
|
+
Object.defineProperty(exports, "runOutput", {
|
|
6647
|
+
enumerable: true,
|
|
6648
|
+
get: function() {
|
|
6649
|
+
return runOutput;
|
|
6650
|
+
}
|
|
6651
|
+
});
|
|
6652
|
+
Object.defineProperty(exports, "sniffAllApps", {
|
|
6653
|
+
enumerable: true,
|
|
6654
|
+
get: function() {
|
|
6655
|
+
return sniffAllApps;
|
|
6656
|
+
}
|
|
6657
|
+
});
|
|
6658
|
+
Object.defineProperty(exports, "sniffAppEnvironment", {
|
|
6659
|
+
enumerable: true,
|
|
6660
|
+
get: function() {
|
|
6661
|
+
return sniffAppEnvironment;
|
|
6662
|
+
}
|
|
6663
|
+
});
|
|
6664
|
+
Object.defineProperty(exports, "storageBackendFor", {
|
|
6665
|
+
enumerable: true,
|
|
6666
|
+
get: function() {
|
|
6667
|
+
return storageBackendFor;
|
|
6668
|
+
}
|
|
6669
|
+
});
|
|
6670
|
+
Object.defineProperty(exports, "storeDokployCredentials", {
|
|
6671
|
+
enumerable: true,
|
|
6672
|
+
get: function() {
|
|
6673
|
+
return storeDokployCredentials;
|
|
6674
|
+
}
|
|
6675
|
+
});
|
|
6676
|
+
Object.defineProperty(exports, "validateDokployToken", {
|
|
6677
|
+
enumerable: true,
|
|
6678
|
+
get: function() {
|
|
6679
|
+
return validateDokployToken;
|
|
6680
|
+
}
|
|
6681
|
+
});
|
|
6682
|
+
Object.defineProperty(exports, "validateImageRef", {
|
|
6683
|
+
enumerable: true,
|
|
6684
|
+
get: function() {
|
|
6685
|
+
return validateImageRef;
|
|
6686
|
+
}
|
|
6687
|
+
});
|
|
6688
|
+
Object.defineProperty(exports, "whoamiCommand", {
|
|
6689
|
+
enumerable: true,
|
|
6690
|
+
get: function() {
|
|
6691
|
+
return whoamiCommand;
|
|
6692
|
+
}
|
|
6693
|
+
});
|
|
6694
|
+
Object.defineProperty(exports, "withOwningTsconfigJsx", {
|
|
3170
6695
|
enumerable: true,
|
|
3171
6696
|
get: function() {
|
|
3172
|
-
return
|
|
6697
|
+
return withOwningTsconfigJsx;
|
|
3173
6698
|
}
|
|
3174
6699
|
});
|
|
3175
6700
|
Object.defineProperty(exports, "workspace_exports", {
|
|
@@ -3179,4 +6704,4 @@ Object.defineProperty(exports, "workspace_exports", {
|
|
|
3179
6704
|
}
|
|
3180
6705
|
});
|
|
3181
6706
|
|
|
3182
|
-
//# sourceMappingURL=
|
|
6707
|
+
//# sourceMappingURL=sniffer-DIv12ncU.cjs.map
|