@geekmidas/cli 10.0.0-alpha.57 → 10.0.0-alpha.59
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/dist/{HostingerProvider-cL-6oQ5r.mjs → HostingerProvider-YQY3rMpV.mjs} +2 -2
- package/dist/{HostingerProvider-cL-6oQ5r.mjs.map → HostingerProvider-YQY3rMpV.mjs.map} +1 -1
- package/dist/{LegacyStateStore-ByRWQ9s1.mjs → LegacyStateStore-CB4ch3Dl.mjs} +2 -2
- package/dist/{LegacyStateStore-ByRWQ9s1.mjs.map → LegacyStateStore-CB4ch3Dl.mjs.map} +1 -1
- package/dist/{LegacyStateStore-C_OFs1gN.cjs → LegacyStateStore-CQJqvmXw.cjs} +7 -7
- package/dist/{LegacyStateStore-C_OFs1gN.cjs.map → LegacyStateStore-CQJqvmXw.cjs.map} +1 -1
- package/dist/{OpenApiTsGenerator-BLRQ7Iu9.cjs → OpenApiTsGenerator-BCWPRZwD.cjs} +153 -4
- package/dist/OpenApiTsGenerator-BCWPRZwD.cjs.map +1 -0
- package/dist/{OpenApiTsGenerator-NThANByQ.mjs → OpenApiTsGenerator-BRWD1Wit.mjs} +149 -6
- package/dist/OpenApiTsGenerator-BRWD1Wit.mjs.map +1 -0
- package/dist/{S3StateStore-FhzyENrN.mjs → S3StateStore-CXTenDTW.mjs} +2 -2
- package/dist/{S3StateStore-FhzyENrN.mjs.map → S3StateStore-CXTenDTW.mjs.map} +1 -1
- package/dist/{S3StateStore-C7eBae0G.cjs → S3StateStore-DLS4rEc0.cjs} +5 -5
- package/dist/{S3StateStore-C7eBae0G.cjs.map → S3StateStore-DLS4rEc0.cjs.map} +1 -1
- package/dist/{SSMStateStore-C7KcDSTb.cjs → SSMStateStore-EqsTBIOs.cjs} +10 -8
- package/dist/SSMStateStore-EqsTBIOs.cjs.map +1 -0
- package/dist/{SSMStateStore-7hPWW7v7.mjs → SSMStateStore-hnP5deFJ.mjs} +4 -2
- package/dist/SSMStateStore-hnP5deFJ.mjs.map +1 -0
- package/dist/StateStore-B4u0wx2t.cjs +412 -0
- package/dist/StateStore-B4u0wx2t.cjs.map +1 -0
- package/dist/StateStore-CYUfGYcx.mjs +359 -0
- package/dist/StateStore-CYUfGYcx.mjs.map +1 -0
- package/dist/{backends-BcHL7Gy_.mjs → backends-CjuCiE8h.mjs} +2 -2
- package/dist/{backends-BcHL7Gy_.mjs.map → backends-CjuCiE8h.mjs.map} +1 -1
- package/dist/{backends-_gQdAzBK.cjs → backends-v3xDuhO0.cjs} +2 -2
- package/dist/{backends-_gQdAzBK.cjs.map → backends-v3xDuhO0.cjs.map} +1 -1
- package/dist/{bundler-URImKj-A.mjs → bundler-ouDdNvk9.mjs} +2 -2
- package/dist/{bundler-URImKj-A.mjs.map → bundler-ouDdNvk9.mjs.map} +1 -1
- package/dist/{config-20V08LZp.mjs → config-EuzFEkcH.mjs} +2 -2
- package/dist/config-EuzFEkcH.mjs.map +1 -0
- package/dist/{config-CP6wsLHA.cjs → config-WMOB9bLL.cjs} +2 -2
- package/dist/config-WMOB9bLL.cjs.map +1 -0
- package/dist/config.cjs +2 -2
- package/dist/config.d.cts +65 -3
- package/dist/config.d.cts.map +1 -1
- package/dist/config.d.mts +65 -3
- package/dist/config.d.mts.map +1 -1
- package/dist/config.mjs +2 -2
- package/dist/{encryption-10Srqiix.cjs → encryption-BP8zfxEJ.cjs} +4 -4
- package/dist/{encryption-10Srqiix.cjs.map → encryption-BP8zfxEJ.cjs.map} +1 -1
- package/dist/{encryption-KQTYFEzU.mjs → encryption-BW7Gh51G.mjs} +4 -4
- package/dist/{encryption-KQTYFEzU.mjs.map → encryption-BW7Gh51G.mjs.map} +1 -1
- package/dist/{github-DtRUkHN8.cjs → github-DHpDMBT7.cjs} +3 -3
- package/dist/{github-DtRUkHN8.cjs.map → github-DHpDMBT7.cjs.map} +1 -1
- package/dist/{github-y7OEj59M.mjs → github-T_6sK3Rz.mjs} +3 -3
- package/dist/{github-y7OEj59M.mjs.map → github-T_6sK3Rz.mjs.map} +1 -1
- package/dist/{index-DMcjpvd-.d.cts → index-B5TJ-mB6.d.mts} +2 -2
- package/dist/{index-DMcjpvd-.d.cts.map → index-B5TJ-mB6.d.mts.map} +1 -1
- package/dist/{index-RM92lx8A.d.mts → index-DV6j4Ydq.d.cts} +2 -2
- package/dist/{index-RM92lx8A.d.mts.map → index-DV6j4Ydq.d.cts.map} +1 -1
- package/dist/index.cjs +1633 -771
- package/dist/index.cjs.map +1 -1
- package/dist/index.mjs +1648 -784
- package/dist/index.mjs.map +1 -1
- package/dist/{migrate-CS3DkO6y.mjs → migrate-BrFREmgQ.mjs} +4 -4
- package/dist/{migrate-CS3DkO6y.mjs.map → migrate-BrFREmgQ.mjs.map} +1 -1
- package/dist/{migrate-DQ54y2WD.cjs → migrate-CThJfVHR.cjs} +4 -4
- package/dist/{migrate-DQ54y2WD.cjs.map → migrate-CThJfVHR.cjs.map} +1 -1
- package/dist/openapi.cjs +2 -2
- package/dist/openapi.d.cts +1 -1
- package/dist/openapi.d.mts +1 -1
- package/dist/openapi.mjs +2 -2
- package/dist/{ready-BSpUZnNo.mjs → ready-DbDHneLR.mjs} +2 -2
- package/dist/ready-DbDHneLR.mjs.map +1 -0
- package/dist/{ready-MUhVJDHe.cjs → ready-mR96orkJ.cjs} +2 -2
- package/dist/ready-mR96orkJ.cjs.map +1 -0
- package/dist/reconcile/public.d.cts +1 -1
- package/dist/reconcile/public.d.mts +1 -1
- package/dist/{types-BIyIQUR-.d.cts → types-CipIIm8f.d.mts} +65 -3
- package/dist/types-CipIIm8f.d.mts.map +1 -0
- package/dist/{types-CmQl6zaP.d.mts → types-D9b-IYdW.d.cts} +66 -2
- package/dist/types-D9b-IYdW.d.cts.map +1 -0
- package/dist/vitest.cjs +1 -1
- package/dist/vitest.mjs +1 -1
- package/dist/workspace/index.cjs +1 -1
- package/dist/workspace/index.d.cts +2 -2
- package/dist/workspace/index.d.mts +2 -2
- package/dist/workspace/index.mjs +1 -1
- package/dist/{workspace-BpdXIeWV.cjs → workspace-B64Xk1Mb.cjs} +16 -4
- package/dist/workspace-B64Xk1Mb.cjs.map +1 -0
- package/dist/{workspace-CgWcE_Yr.mjs → workspace-BRZ14o-o.mjs} +11 -8
- package/dist/{workspace-CgWcE_Yr.mjs.map → workspace-BRZ14o-o.mjs.map} +1 -1
- package/dist/{workspace-C2mFV53i.mjs → workspace-Bp1xVaOz.mjs} +16 -4
- package/dist/workspace-Bp1xVaOz.mjs.map +1 -0
- package/dist/{workspace-BbPJVCto.cjs → workspace-Dbn07ILl.cjs} +9 -6
- package/dist/{workspace-BbPJVCto.cjs.map → workspace-Dbn07ILl.cjs.map} +1 -1
- package/package.json +13 -13
- package/dist/LocalStateProvider-7HR6SS2t.cjs +0 -48
- package/dist/LocalStateProvider-7HR6SS2t.cjs.map +0 -1
- package/dist/LocalStateProvider-D7QyJoLM.mjs +0 -48
- package/dist/LocalStateProvider-D7QyJoLM.mjs.map +0 -1
- package/dist/LocalStateStore-BupdV-VN.mjs +0 -163
- package/dist/LocalStateStore-BupdV-VN.mjs.map +0 -1
- package/dist/LocalStateStore-DDDU0HkI.cjs +0 -163
- package/dist/LocalStateStore-DDDU0HkI.cjs.map +0 -1
- package/dist/OpenApiTsGenerator-BLRQ7Iu9.cjs.map +0 -1
- package/dist/OpenApiTsGenerator-NThANByQ.mjs.map +0 -1
- package/dist/SSMStateProvider-B0Hu1MKZ.mjs +0 -75
- package/dist/SSMStateProvider-B0Hu1MKZ.mjs.map +0 -1
- package/dist/SSMStateProvider-VrQ-yeqd.cjs +0 -74
- package/dist/SSMStateProvider-VrQ-yeqd.cjs.map +0 -1
- package/dist/SSMStateStore-7hPWW7v7.mjs.map +0 -1
- package/dist/SSMStateStore-C7KcDSTb.cjs.map +0 -1
- package/dist/config-20V08LZp.mjs.map +0 -1
- package/dist/config-CP6wsLHA.cjs.map +0 -1
- package/dist/ready-BSpUZnNo.mjs.map +0 -1
- package/dist/ready-MUhVJDHe.cjs.map +0 -1
- package/dist/types-BIyIQUR-.d.cts.map +0 -1
- package/dist/types-CmQl6zaP.d.mts.map +0 -1
- package/dist/workspace-BpdXIeWV.cjs.map +0 -1
- package/dist/workspace-C2mFV53i.mjs.map +0 -1
package/dist/index.cjs
CHANGED
|
@@ -2,16 +2,17 @@
|
|
|
2
2
|
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
3
3
|
const require_rolldown_runtime = require("./rolldown-runtime-C0BPl7ul.cjs");
|
|
4
4
|
const require_discover = require("./discover-DXNviAh-.cjs");
|
|
5
|
-
const require_config = require("./config-
|
|
6
|
-
const require_workspace = require("./workspace-
|
|
5
|
+
const require_config = require("./config-WMOB9bLL.cjs");
|
|
6
|
+
const require_workspace = require("./workspace-B64Xk1Mb.cjs");
|
|
7
7
|
const require_storage = require("./storage-CSGoC3bK.cjs");
|
|
8
8
|
const require_store = require("./store-DQgxT_WR.cjs");
|
|
9
|
-
const require_workspace$1 = require("./workspace-
|
|
10
|
-
const require_OpenApiTsGenerator = require("./OpenApiTsGenerator-
|
|
9
|
+
const require_workspace$1 = require("./workspace-Dbn07ILl.cjs");
|
|
10
|
+
const require_OpenApiTsGenerator = require("./OpenApiTsGenerator-BCWPRZwD.cjs");
|
|
11
11
|
const require_openapi = require("./openapi.cjs");
|
|
12
|
-
const require_ready = require("./ready-
|
|
13
|
-
const require_backends = require("./backends-
|
|
12
|
+
const require_ready = require("./ready-mR96orkJ.cjs");
|
|
13
|
+
const require_backends = require("./backends-v3xDuhO0.cjs");
|
|
14
14
|
const require_emit = require("./emit-Dmc1nvdS.cjs");
|
|
15
|
+
const require_StateStore = require("./StateStore-B4u0wx2t.cjs");
|
|
15
16
|
let commander = require("commander");
|
|
16
17
|
let node_process = require("node:process");
|
|
17
18
|
let node_readline_promises = require("node:readline/promises");
|
|
@@ -27,6 +28,7 @@ let node_child_process = require("node:child_process");
|
|
|
27
28
|
let node_module = require("node:module");
|
|
28
29
|
let chokidar = require("chokidar");
|
|
29
30
|
chokidar = require_rolldown_runtime.__toESM(chokidar, 1);
|
|
31
|
+
let node_net = require("node:net");
|
|
30
32
|
let node_crypto = require("node:crypto");
|
|
31
33
|
let node_url = require("node:url");
|
|
32
34
|
let _geekmidas_constructs_encryption = require("@geekmidas/constructs/encryption");
|
|
@@ -37,6 +39,8 @@ let node_util = require("node:util");
|
|
|
37
39
|
let _geekmidas_cache_postgres = require("@geekmidas/cache/postgres");
|
|
38
40
|
let prompts = require("prompts");
|
|
39
41
|
prompts = require_rolldown_runtime.__toESM(prompts, 1);
|
|
42
|
+
let node_http = require("node:http");
|
|
43
|
+
let node_stream = require("node:stream");
|
|
40
44
|
let http = require("http");
|
|
41
45
|
let http2 = require("http2");
|
|
42
46
|
let stream = require("stream");
|
|
@@ -48,7 +52,7 @@ let _geekmidas_constructs_queue = require("@geekmidas/constructs/queue");
|
|
|
48
52
|
let _geekmidas_constructs_topic = require("@geekmidas/constructs/topic");
|
|
49
53
|
let node_dns_promises = require("node:dns/promises");
|
|
50
54
|
//#region package.json
|
|
51
|
-
var version = "10.0.0-alpha.
|
|
55
|
+
var version = "10.0.0-alpha.59";
|
|
52
56
|
//#endregion
|
|
53
57
|
//#region src/auth/credentials.ts
|
|
54
58
|
/**
|
|
@@ -142,15 +146,6 @@ async function getDokployToken(options) {
|
|
|
142
146
|
return null;
|
|
143
147
|
}
|
|
144
148
|
/**
|
|
145
|
-
* Store Dokploy registry ID
|
|
146
|
-
*/
|
|
147
|
-
async function storeDokployRegistryId(registryId, options) {
|
|
148
|
-
const credentials = await readCredentials(options);
|
|
149
|
-
if (!credentials.dokploy) throw new Error("Dokploy credentials not found. Run \"gkm login --service dokploy\" first.");
|
|
150
|
-
credentials.dokploy.registryId = registryId;
|
|
151
|
-
await writeCredentials(credentials, options);
|
|
152
|
-
}
|
|
153
|
-
/**
|
|
154
149
|
* Get Dokploy registry ID from stored credentials
|
|
155
150
|
*/
|
|
156
151
|
async function getDokployRegistryId(options) {
|
|
@@ -771,6 +766,621 @@ async function assignAppPorts(workspace, names, deps) {
|
|
|
771
766
|
running
|
|
772
767
|
};
|
|
773
768
|
}
|
|
769
|
+
/** Overrides `dev.discoveryPort` — `0` serves on a port of its own. */
|
|
770
|
+
const DISCOVERY_PORT_ENV = "GKM_DISCOVERY_PORT";
|
|
771
|
+
/**
|
|
772
|
+
* Set by a workspace's `gkm dev` for the app sessions turbo starts under it:
|
|
773
|
+
* the workspace has printed the connect URL, so they do not repeat it.
|
|
774
|
+
*/
|
|
775
|
+
const DISCOVERY_QUIET_ENV = "GKM_DISCOVERY_QUIET";
|
|
776
|
+
/** The route everything is served under. */
|
|
777
|
+
const BASE = "/__gkm";
|
|
778
|
+
/** `GKM_DISCOVERY_PORT` is set to something that is not a port. */
|
|
779
|
+
var InvalidDiscoveryPort = class extends Error {
|
|
780
|
+
value;
|
|
781
|
+
constructor(value) {
|
|
782
|
+
super(`${DISCOVERY_PORT_ENV}=${value} is not a port. Set it to a number from 0 to 65535 (0 serves discovery on a free port of its own), or unset it to use dev.discoveryPort.`);
|
|
783
|
+
this.value = value;
|
|
784
|
+
this.name = "InvalidDiscoveryPort";
|
|
785
|
+
}
|
|
786
|
+
};
|
|
787
|
+
/** The port the endpoint uses: the env, then `dev.discoveryPort`, then 4983. */
|
|
788
|
+
function discoveryPort(config, env = process.env) {
|
|
789
|
+
const raw = env[DISCOVERY_PORT_ENV];
|
|
790
|
+
if (raw !== void 0 && raw !== "") {
|
|
791
|
+
const port = Number(raw);
|
|
792
|
+
if (!Number.isInteger(port) || port < 0 || port > 65535) throw new InvalidDiscoveryPort(raw);
|
|
793
|
+
return port;
|
|
794
|
+
}
|
|
795
|
+
return config?.discoveryPort ?? 4983;
|
|
796
|
+
}
|
|
797
|
+
/** The machine's registry, shared by every `gkm dev`. */
|
|
798
|
+
function discoveryRegistry(env = process.env) {
|
|
799
|
+
return env["GKM_DEV_REGISTRY"] || (0, node_path.join)((0, node_os.homedir)(), ".gkm", "dev");
|
|
800
|
+
}
|
|
801
|
+
function sessionsDir(registry) {
|
|
802
|
+
return (0, node_path.join)(registry, "sessions");
|
|
803
|
+
}
|
|
804
|
+
/** Whether a process is alive. `EPERM` is someone else's — alive. */
|
|
805
|
+
function alive(pid) {
|
|
806
|
+
try {
|
|
807
|
+
process.kill(pid, 0);
|
|
808
|
+
return true;
|
|
809
|
+
} catch (error) {
|
|
810
|
+
return error.code === "EPERM";
|
|
811
|
+
}
|
|
812
|
+
}
|
|
813
|
+
/**
|
|
814
|
+
* Every live session, pruning the ones whose process is gone — a `gkm dev`
|
|
815
|
+
* killed outright never got to remove its own.
|
|
816
|
+
*/
|
|
817
|
+
function liveSessions(registry) {
|
|
818
|
+
const dir = sessionsDir(registry);
|
|
819
|
+
let files;
|
|
820
|
+
try {
|
|
821
|
+
files = (0, node_fs.readdirSync)(dir).filter((file) => file.endsWith(".json"));
|
|
822
|
+
} catch {
|
|
823
|
+
return [];
|
|
824
|
+
}
|
|
825
|
+
const records = [];
|
|
826
|
+
for (const file of files) {
|
|
827
|
+
const path = (0, node_path.join)(dir, file);
|
|
828
|
+
let record;
|
|
829
|
+
try {
|
|
830
|
+
record = JSON.parse((0, node_fs.readFileSync)(path, "utf8"));
|
|
831
|
+
} catch {
|
|
832
|
+
continue;
|
|
833
|
+
}
|
|
834
|
+
if (alive(record.pid)) {
|
|
835
|
+
records.push(record);
|
|
836
|
+
continue;
|
|
837
|
+
}
|
|
838
|
+
try {
|
|
839
|
+
(0, node_fs.unlinkSync)(path);
|
|
840
|
+
} catch {}
|
|
841
|
+
}
|
|
842
|
+
return records;
|
|
843
|
+
}
|
|
844
|
+
/** Written whole, then renamed into place, so a reader never sees half. */
|
|
845
|
+
function writeRecord(registry, record) {
|
|
846
|
+
const path = (0, node_path.join)(sessionsDir(registry), `${record.id}.json`);
|
|
847
|
+
const partial = `${path}.${process.pid}.tmp`;
|
|
848
|
+
(0, node_fs.writeFileSync)(partial, JSON.stringify(record), { mode: 384 });
|
|
849
|
+
(0, node_fs.renameSync)(partial, path);
|
|
850
|
+
}
|
|
851
|
+
function removeRecord(registry, id) {
|
|
852
|
+
try {
|
|
853
|
+
(0, node_fs.unlinkSync)((0, node_path.join)(sessionsDir(registry), `${id}.json`));
|
|
854
|
+
} catch {}
|
|
855
|
+
}
|
|
856
|
+
/**
|
|
857
|
+
* The token every session prints and every host checks.
|
|
858
|
+
*
|
|
859
|
+
* Shared through the registry, so a URL printed by one session still works
|
|
860
|
+
* after another has taken over hosting. It lives as long as some session does:
|
|
861
|
+
* a session that finds none but itself starts a new one, so a token printed
|
|
862
|
+
* yesterday stops working once everything it was printed by has stopped.
|
|
863
|
+
*/
|
|
864
|
+
function sessionToken(registry, self) {
|
|
865
|
+
const path = (0, node_path.join)(registry, "token");
|
|
866
|
+
if (liveSessions(registry).filter((r) => r.id !== self).length === 0) {
|
|
867
|
+
const stale = `${path}.${self}.stale`;
|
|
868
|
+
try {
|
|
869
|
+
(0, node_fs.renameSync)(path, stale);
|
|
870
|
+
(0, node_fs.unlinkSync)(stale);
|
|
871
|
+
} catch {}
|
|
872
|
+
}
|
|
873
|
+
const fresh = `${path}.${self}.new`;
|
|
874
|
+
(0, node_fs.writeFileSync)(fresh, (0, node_crypto.randomBytes)(32).toString("base64url"), { mode: 384 });
|
|
875
|
+
try {
|
|
876
|
+
(0, node_fs.linkSync)(fresh, path);
|
|
877
|
+
} catch {}
|
|
878
|
+
(0, node_fs.unlinkSync)(fresh);
|
|
879
|
+
return (0, node_fs.readFileSync)(path, "utf8").trim();
|
|
880
|
+
}
|
|
881
|
+
/** `<name>-<8 hex>`: readable, and two checkouts of a project stay apart. */
|
|
882
|
+
function workspaceId(name, root) {
|
|
883
|
+
return `${name.toLowerCase().replace(/^@/, "").replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "") || "workspace"}-${(0, node_crypto.createHash)("sha256").update(root).digest("hex").slice(0, 8)}`;
|
|
884
|
+
}
|
|
885
|
+
/** Whether anything accepts connections on a local port. */
|
|
886
|
+
function listening(port) {
|
|
887
|
+
return new Promise((resolve) => {
|
|
888
|
+
const socket = (0, node_net.connect)({
|
|
889
|
+
port,
|
|
890
|
+
host: "localhost"
|
|
891
|
+
});
|
|
892
|
+
const done = (open) => {
|
|
893
|
+
socket.destroy();
|
|
894
|
+
resolve(open);
|
|
895
|
+
};
|
|
896
|
+
socket.setTimeout(300, () => done(false));
|
|
897
|
+
socket.once("connect", () => done(true));
|
|
898
|
+
socket.once("error", () => done(false));
|
|
899
|
+
});
|
|
900
|
+
}
|
|
901
|
+
/** Which data APIs the forwarder matches by prefix, rather than exactly. */
|
|
902
|
+
const PREFIXED = /* @__PURE__ */ new Set(["telescope", "database"]);
|
|
903
|
+
/** Whether `path` on an app is one of its data APIs. */
|
|
904
|
+
function servesData(app, path) {
|
|
905
|
+
return app.dataApis.some((api) => PREFIXED.has(api.kind) ? path === api.path || path.startsWith(`${api.path}/`) : path === api.path);
|
|
906
|
+
}
|
|
907
|
+
/**
|
|
908
|
+
* Every workspace the sessions describe, one entry per root.
|
|
909
|
+
*
|
|
910
|
+
* An app listed only by its workspace's session — a web app turbo started, an
|
|
911
|
+
* API whose own `gkm dev` has not registered yet — is `ready` when its port
|
|
912
|
+
* answers and `starting` until then.
|
|
913
|
+
*/
|
|
914
|
+
async function collect(records) {
|
|
915
|
+
const byRoot = /* @__PURE__ */ new Map();
|
|
916
|
+
for (const record of records) {
|
|
917
|
+
const group = byRoot.get(record.workspace.root) ?? [];
|
|
918
|
+
group.push(record);
|
|
919
|
+
byRoot.set(record.workspace.root, group);
|
|
920
|
+
}
|
|
921
|
+
const views = [];
|
|
922
|
+
for (const [root, group] of byRoot) {
|
|
923
|
+
group.sort((a, b) => Number(b.role === "workspace") - Number(a.role === "workspace") || a.startedAt.localeCompare(b.startedAt));
|
|
924
|
+
const lead = group[0];
|
|
925
|
+
const id = workspaceId(lead.workspace.name, root);
|
|
926
|
+
const apps = /* @__PURE__ */ new Map();
|
|
927
|
+
for (const record of group) for (const app of record.apps) {
|
|
928
|
+
const own = record.role === "app";
|
|
929
|
+
if (!own && apps.has(app.name)) continue;
|
|
930
|
+
apps.set(app.name, {
|
|
931
|
+
app,
|
|
932
|
+
pid: own ? record.pid : null
|
|
933
|
+
});
|
|
934
|
+
}
|
|
935
|
+
const discovered = await Promise.all([...apps.values()].map(async ({ app, pid }) => {
|
|
936
|
+
const url = `http://localhost:${app.port}`;
|
|
937
|
+
const status = pid === null ? await listening(app.port) ? "ready" : "starting" : app.status;
|
|
938
|
+
return {
|
|
939
|
+
name: app.name,
|
|
940
|
+
type: app.type,
|
|
941
|
+
port: app.port,
|
|
942
|
+
url,
|
|
943
|
+
publicUrl: app.publicUrl ?? null,
|
|
944
|
+
status,
|
|
945
|
+
reloads: app.reloads,
|
|
946
|
+
pid,
|
|
947
|
+
dataApis: app.dataApis.map((api) => ({
|
|
948
|
+
kind: api.kind,
|
|
949
|
+
path: api.path,
|
|
950
|
+
url: `${url}${api.path}`,
|
|
951
|
+
proxy: `${BASE}/workspaces/${id}/apps/${encodeURIComponent(app.name)}${api.path}`
|
|
952
|
+
}))
|
|
953
|
+
};
|
|
954
|
+
}));
|
|
955
|
+
discovered.sort((a, b) => a.name.localeCompare(b.name));
|
|
956
|
+
views.push({
|
|
957
|
+
id,
|
|
958
|
+
name: lead.workspace.name,
|
|
959
|
+
root,
|
|
960
|
+
stage: lead.workspace.stage,
|
|
961
|
+
manifest: group.find((r) => r.manifest)?.manifest ?? null,
|
|
962
|
+
apps: discovered,
|
|
963
|
+
allowedOrigins: [...new Set(group.flatMap((r) => r.allowedOrigins))]
|
|
964
|
+
});
|
|
965
|
+
}
|
|
966
|
+
return views.sort((a, b) => a.name.localeCompare(b.name) || a.root.localeCompare(b.root));
|
|
967
|
+
}
|
|
968
|
+
/** What changed between two views, as events. */
|
|
969
|
+
function changes(before, after) {
|
|
970
|
+
const out = [];
|
|
971
|
+
const previous = new Map(before.map((w) => [w.id, w]));
|
|
972
|
+
const next = new Map(after.map((w) => [w.id, w]));
|
|
973
|
+
for (const workspace of after) {
|
|
974
|
+
const was = previous.get(workspace.id);
|
|
975
|
+
if (!was) out.push({
|
|
976
|
+
workspace,
|
|
977
|
+
event: {
|
|
978
|
+
type: "workspace.started",
|
|
979
|
+
workspace: workspace.id
|
|
980
|
+
}
|
|
981
|
+
});
|
|
982
|
+
const wasApps = new Map((was?.apps ?? []).map((a) => [a.name, a]));
|
|
983
|
+
for (const app of workspace.apps) {
|
|
984
|
+
const old = wasApps.get(app.name);
|
|
985
|
+
const type = !old ? "app.started" : app.reloads > old.reloads ? "app.reloaded" : app.status !== old.status || app.port !== old.port || app.dataApis.length !== old.dataApis.length ? "app.status" : void 0;
|
|
986
|
+
if (type) out.push({
|
|
987
|
+
workspace,
|
|
988
|
+
event: {
|
|
989
|
+
type,
|
|
990
|
+
workspace: workspace.id,
|
|
991
|
+
app
|
|
992
|
+
}
|
|
993
|
+
});
|
|
994
|
+
}
|
|
995
|
+
for (const old of was?.apps ?? []) if (!workspace.apps.some((a) => a.name === old.name)) out.push({
|
|
996
|
+
workspace,
|
|
997
|
+
event: {
|
|
998
|
+
type: "app.stopped",
|
|
999
|
+
workspace: workspace.id,
|
|
1000
|
+
app: old
|
|
1001
|
+
}
|
|
1002
|
+
});
|
|
1003
|
+
}
|
|
1004
|
+
for (const workspace of before) {
|
|
1005
|
+
if (next.has(workspace.id)) continue;
|
|
1006
|
+
for (const app of workspace.apps) out.push({
|
|
1007
|
+
workspace,
|
|
1008
|
+
event: {
|
|
1009
|
+
type: "app.stopped",
|
|
1010
|
+
workspace: workspace.id,
|
|
1011
|
+
app
|
|
1012
|
+
}
|
|
1013
|
+
});
|
|
1014
|
+
out.push({
|
|
1015
|
+
workspace,
|
|
1016
|
+
event: {
|
|
1017
|
+
type: "workspace.stopped",
|
|
1018
|
+
workspace: workspace.id
|
|
1019
|
+
}
|
|
1020
|
+
});
|
|
1021
|
+
}
|
|
1022
|
+
return out;
|
|
1023
|
+
}
|
|
1024
|
+
/** A browser with no `Origin` — curl, a local process — sees everything. */
|
|
1025
|
+
function visibleTo(workspace, origin) {
|
|
1026
|
+
return origin === void 0 || workspace.allowedOrigins.includes(origin);
|
|
1027
|
+
}
|
|
1028
|
+
function publicView({ allowedOrigins: _, ...workspace }) {
|
|
1029
|
+
return workspace;
|
|
1030
|
+
}
|
|
1031
|
+
/** Headers an app's response is passed on with; nothing that sets state. */
|
|
1032
|
+
const FORWARDED_RESPONSE_HEADERS = [
|
|
1033
|
+
"content-type",
|
|
1034
|
+
"cache-control",
|
|
1035
|
+
"etag",
|
|
1036
|
+
"last-modified"
|
|
1037
|
+
];
|
|
1038
|
+
/**
|
|
1039
|
+
* The HTTP side: serves the registry it reads, checking every request.
|
|
1040
|
+
* @internal Exported for testing
|
|
1041
|
+
*/
|
|
1042
|
+
var DiscoveryHost = class {
|
|
1043
|
+
registry;
|
|
1044
|
+
token;
|
|
1045
|
+
pollMs;
|
|
1046
|
+
server;
|
|
1047
|
+
view = [];
|
|
1048
|
+
streams = /* @__PURE__ */ new Set();
|
|
1049
|
+
scanning;
|
|
1050
|
+
timers = [];
|
|
1051
|
+
sequence = 0;
|
|
1052
|
+
constructor(registry, token, pollMs) {
|
|
1053
|
+
this.registry = registry;
|
|
1054
|
+
this.token = token;
|
|
1055
|
+
this.pollMs = pollMs;
|
|
1056
|
+
}
|
|
1057
|
+
/** The port it is bound to, once it is. */
|
|
1058
|
+
get port() {
|
|
1059
|
+
return (this.server?.address())?.port;
|
|
1060
|
+
}
|
|
1061
|
+
/** Binds the port; `false` when another process already has it. */
|
|
1062
|
+
async listen(port) {
|
|
1063
|
+
const server = (0, node_http.createServer)((request, response) => {
|
|
1064
|
+
this.handle(request, response).catch(() => {
|
|
1065
|
+
if (!response.headersSent) reply(response, 500, { error: "DiscoveryFailed" });
|
|
1066
|
+
else response.destroy();
|
|
1067
|
+
});
|
|
1068
|
+
});
|
|
1069
|
+
if (!await new Promise((resolve) => {
|
|
1070
|
+
server.once("error", () => resolve(false));
|
|
1071
|
+
server.listen(port, "127.0.0.1", () => resolve(true));
|
|
1072
|
+
})) return false;
|
|
1073
|
+
server.unref();
|
|
1074
|
+
this.server = server;
|
|
1075
|
+
this.view = await this.scan();
|
|
1076
|
+
this.timers.push(setInterval(() => void this.scan(), this.pollMs).unref(), setInterval(() => {
|
|
1077
|
+
for (const { response } of this.streams) response.write(": ping\n\n");
|
|
1078
|
+
}, 15e3).unref());
|
|
1079
|
+
return true;
|
|
1080
|
+
}
|
|
1081
|
+
async close() {
|
|
1082
|
+
for (const timer of this.timers) clearInterval(timer);
|
|
1083
|
+
this.timers = [];
|
|
1084
|
+
for (const { response } of this.streams) response.end();
|
|
1085
|
+
this.streams.clear();
|
|
1086
|
+
const server = this.server;
|
|
1087
|
+
this.server = void 0;
|
|
1088
|
+
if (!server) return;
|
|
1089
|
+
await new Promise((resolve) => {
|
|
1090
|
+
server.close(() => resolve());
|
|
1091
|
+
server.closeAllConnections();
|
|
1092
|
+
});
|
|
1093
|
+
}
|
|
1094
|
+
/** Reads the registry, and tells every stream what changed. One at a time. */
|
|
1095
|
+
scan() {
|
|
1096
|
+
const run = (this.scanning ?? Promise.resolve()).then(async () => {
|
|
1097
|
+
const next = await collect(liveSessions(this.registry));
|
|
1098
|
+
for (const { workspace, event } of changes(this.view, next)) this.broadcast(workspace, event);
|
|
1099
|
+
this.view = next;
|
|
1100
|
+
return next;
|
|
1101
|
+
});
|
|
1102
|
+
this.scanning = run.catch(() => this.view);
|
|
1103
|
+
return run;
|
|
1104
|
+
}
|
|
1105
|
+
broadcast(workspace, event) {
|
|
1106
|
+
this.sequence += 1;
|
|
1107
|
+
for (const stream of this.streams) {
|
|
1108
|
+
if (!visibleTo(workspace, stream.origin)) continue;
|
|
1109
|
+
send(stream.response, event, this.sequence);
|
|
1110
|
+
}
|
|
1111
|
+
}
|
|
1112
|
+
snapshot(origin) {
|
|
1113
|
+
return {
|
|
1114
|
+
version: 1,
|
|
1115
|
+
host: {
|
|
1116
|
+
pid: process.pid,
|
|
1117
|
+
port: this.port ?? 0
|
|
1118
|
+
},
|
|
1119
|
+
events: `${BASE}/events`,
|
|
1120
|
+
workspaces: this.view.filter((w) => visibleTo(w, origin)).map(publicView)
|
|
1121
|
+
};
|
|
1122
|
+
}
|
|
1123
|
+
async handle(request, response) {
|
|
1124
|
+
const port = this.port;
|
|
1125
|
+
const host = request.headers.host;
|
|
1126
|
+
if (host !== `127.0.0.1:${port}` && host !== `localhost:${port}`) {
|
|
1127
|
+
reply(response, 403, {
|
|
1128
|
+
error: "HostNotAllowed",
|
|
1129
|
+
message: `Reach discovery at http://127.0.0.1:${port}.`
|
|
1130
|
+
});
|
|
1131
|
+
return;
|
|
1132
|
+
}
|
|
1133
|
+
const origin = request.headers.origin;
|
|
1134
|
+
const allowed = this.view.flatMap((w) => w.allowedOrigins);
|
|
1135
|
+
if (origin !== void 0 && !allowed.includes(origin)) {
|
|
1136
|
+
reply(response, 403, {
|
|
1137
|
+
error: "OriginNotAllowed",
|
|
1138
|
+
message: `Add ${origin} to dev.allowedOrigins in gkm.config.ts to let it read gkm dev.`
|
|
1139
|
+
});
|
|
1140
|
+
return;
|
|
1141
|
+
}
|
|
1142
|
+
if (origin !== void 0) {
|
|
1143
|
+
response.setHeader("access-control-allow-origin", origin);
|
|
1144
|
+
response.setHeader("vary", "Origin");
|
|
1145
|
+
}
|
|
1146
|
+
if (request.method === "OPTIONS") {
|
|
1147
|
+
response.setHeader("access-control-allow-methods", "GET, HEAD");
|
|
1148
|
+
response.setHeader("access-control-allow-headers", "Authorization");
|
|
1149
|
+
response.setHeader("access-control-max-age", "600");
|
|
1150
|
+
if (request.headers["access-control-request-private-network"]) response.setHeader("access-control-allow-private-network", "true");
|
|
1151
|
+
response.writeHead(204).end();
|
|
1152
|
+
return;
|
|
1153
|
+
}
|
|
1154
|
+
const url = new URL(request.url ?? "/", `http://${host}`);
|
|
1155
|
+
if (!this.authorized(request, url)) {
|
|
1156
|
+
reply(response, 401, {
|
|
1157
|
+
error: "TokenRequired",
|
|
1158
|
+
message: "Use the connect URL gkm dev printed, or send its token as `Authorization: Bearer <token>`."
|
|
1159
|
+
});
|
|
1160
|
+
return;
|
|
1161
|
+
}
|
|
1162
|
+
if (request.method !== "GET" && request.method !== "HEAD") {
|
|
1163
|
+
response.setHeader("allow", "GET, HEAD, OPTIONS");
|
|
1164
|
+
reply(response, 405, {
|
|
1165
|
+
error: "ReadOnly",
|
|
1166
|
+
message: "Discovery is read-only: GET and HEAD."
|
|
1167
|
+
});
|
|
1168
|
+
return;
|
|
1169
|
+
}
|
|
1170
|
+
const path = url.pathname.replace(/\/+$/, "") || "/";
|
|
1171
|
+
if (path === BASE) {
|
|
1172
|
+
await this.scan();
|
|
1173
|
+
reply(response, 200, this.snapshot(origin));
|
|
1174
|
+
return;
|
|
1175
|
+
}
|
|
1176
|
+
if (path === `${BASE}/events`) {
|
|
1177
|
+
await this.scan();
|
|
1178
|
+
this.stream(response, origin);
|
|
1179
|
+
response.once("close", () => {
|
|
1180
|
+
for (const stream of this.streams) if (stream.response === response) this.streams.delete(stream);
|
|
1181
|
+
});
|
|
1182
|
+
return;
|
|
1183
|
+
}
|
|
1184
|
+
const forwarded = path.match(/^\/__gkm\/workspaces\/([^/]+)\/apps\/([^/]+)(\/.*)$/);
|
|
1185
|
+
if (forwarded) {
|
|
1186
|
+
const [, id, name, rest] = forwarded;
|
|
1187
|
+
await this.forward(request, response, origin, id, decodeURIComponent(name), rest, url);
|
|
1188
|
+
return;
|
|
1189
|
+
}
|
|
1190
|
+
reply(response, 404, { error: "NotFound" });
|
|
1191
|
+
}
|
|
1192
|
+
authorized(request, url) {
|
|
1193
|
+
const header = request.headers.authorization;
|
|
1194
|
+
const given = header?.startsWith("Bearer ") ? header.slice(7).trim() : url.searchParams.get("token");
|
|
1195
|
+
if (!given) return false;
|
|
1196
|
+
const a = Buffer.from(given);
|
|
1197
|
+
const b = Buffer.from(this.token);
|
|
1198
|
+
return a.length === b.length && (0, node_crypto.timingSafeEqual)(a, b);
|
|
1199
|
+
}
|
|
1200
|
+
stream(response, origin) {
|
|
1201
|
+
response.writeHead(200, {
|
|
1202
|
+
"content-type": "text/event-stream",
|
|
1203
|
+
"cache-control": "no-cache",
|
|
1204
|
+
connection: "keep-alive"
|
|
1205
|
+
});
|
|
1206
|
+
response.write("retry: 1000\n\n");
|
|
1207
|
+
send(response, {
|
|
1208
|
+
type: "snapshot",
|
|
1209
|
+
snapshot: this.snapshot(origin)
|
|
1210
|
+
});
|
|
1211
|
+
this.streams.add({
|
|
1212
|
+
origin,
|
|
1213
|
+
response
|
|
1214
|
+
});
|
|
1215
|
+
}
|
|
1216
|
+
/** Passes a read of one data API on to the app that serves it. */
|
|
1217
|
+
async forward(request, response, origin, id, name, path, url) {
|
|
1218
|
+
const app = this.view.find((w) => w.id === id && visibleTo(w, origin))?.apps.find((a) => a.name === name);
|
|
1219
|
+
if (!app) {
|
|
1220
|
+
reply(response, 404, { error: "AppNotFound" });
|
|
1221
|
+
return;
|
|
1222
|
+
}
|
|
1223
|
+
if (!servesData(app, path)) {
|
|
1224
|
+
reply(response, 404, {
|
|
1225
|
+
error: "NotADataApi",
|
|
1226
|
+
message: `Only ${name}'s data APIs are forwarded: ${app.dataApis.map((a) => a.path).join(", ") || "it has none"}.`
|
|
1227
|
+
});
|
|
1228
|
+
return;
|
|
1229
|
+
}
|
|
1230
|
+
const search = new URLSearchParams(url.searchParams);
|
|
1231
|
+
search.delete("token");
|
|
1232
|
+
const query = search.size > 0 ? `?${search}` : "";
|
|
1233
|
+
const abort = new AbortController();
|
|
1234
|
+
response.once("close", () => abort.abort());
|
|
1235
|
+
let upstream;
|
|
1236
|
+
try {
|
|
1237
|
+
upstream = await fetch(`http://localhost:${app.port}${path}${query}`, {
|
|
1238
|
+
method: request.method,
|
|
1239
|
+
headers: { accept: request.headers.accept ?? "*/*" },
|
|
1240
|
+
signal: abort.signal
|
|
1241
|
+
});
|
|
1242
|
+
} catch {
|
|
1243
|
+
reply(response, 502, {
|
|
1244
|
+
error: "AppUnavailable",
|
|
1245
|
+
message: `${name} is not answering on port ${app.port}.`
|
|
1246
|
+
});
|
|
1247
|
+
return;
|
|
1248
|
+
}
|
|
1249
|
+
const headers = {};
|
|
1250
|
+
for (const key of FORWARDED_RESPONSE_HEADERS) {
|
|
1251
|
+
const value = upstream.headers.get(key);
|
|
1252
|
+
if (value) headers[key] = value;
|
|
1253
|
+
}
|
|
1254
|
+
response.writeHead(upstream.status, headers);
|
|
1255
|
+
if (!upstream.body || request.method === "HEAD") {
|
|
1256
|
+
response.end();
|
|
1257
|
+
return;
|
|
1258
|
+
}
|
|
1259
|
+
node_stream.Readable.fromWeb(upstream.body).on("error", () => response.destroy()).pipe(response);
|
|
1260
|
+
}
|
|
1261
|
+
};
|
|
1262
|
+
function reply(response, status, body) {
|
|
1263
|
+
response.writeHead(status, { "content-type": "application/json" });
|
|
1264
|
+
response.end(JSON.stringify(body));
|
|
1265
|
+
}
|
|
1266
|
+
function send(response, event, id) {
|
|
1267
|
+
response.write(`${id === void 0 ? "" : `id: ${id}\n`}event: ${event.type}\ndata: ${JSON.stringify(event)}\n\n`);
|
|
1268
|
+
}
|
|
1269
|
+
/**
|
|
1270
|
+
* One `gkm dev`'s membership: its record in the registry, and — while it holds
|
|
1271
|
+
* the port — the endpoint itself.
|
|
1272
|
+
*/
|
|
1273
|
+
var DiscoverySession = class DiscoverySession {
|
|
1274
|
+
registry;
|
|
1275
|
+
port;
|
|
1276
|
+
record;
|
|
1277
|
+
token;
|
|
1278
|
+
pollMs;
|
|
1279
|
+
host;
|
|
1280
|
+
election;
|
|
1281
|
+
left = false;
|
|
1282
|
+
cleanup = () => removeRecord(this.registry, this.record.id);
|
|
1283
|
+
constructor(registry, port, record, token, pollMs) {
|
|
1284
|
+
this.registry = registry;
|
|
1285
|
+
this.port = port;
|
|
1286
|
+
this.record = record;
|
|
1287
|
+
this.token = token;
|
|
1288
|
+
this.pollMs = pollMs;
|
|
1289
|
+
}
|
|
1290
|
+
static async join(options) {
|
|
1291
|
+
const registry = options.registry ?? discoveryRegistry();
|
|
1292
|
+
(0, node_fs.mkdirSync)(sessionsDir(registry), {
|
|
1293
|
+
recursive: true,
|
|
1294
|
+
mode: 448
|
|
1295
|
+
});
|
|
1296
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
1297
|
+
const record = {
|
|
1298
|
+
id: `${process.pid}-${(0, node_crypto.randomBytes)(4).toString("hex")}`,
|
|
1299
|
+
pid: process.pid,
|
|
1300
|
+
role: options.role,
|
|
1301
|
+
startedAt: now,
|
|
1302
|
+
updatedAt: now,
|
|
1303
|
+
workspace: options.workspace,
|
|
1304
|
+
allowedOrigins: [...options.allowedOrigins ?? []],
|
|
1305
|
+
...options.manifest ? { manifest: options.manifest } : {},
|
|
1306
|
+
apps: options.apps
|
|
1307
|
+
};
|
|
1308
|
+
writeRecord(registry, record);
|
|
1309
|
+
const token = sessionToken(registry, record.id);
|
|
1310
|
+
const session = new DiscoverySession(registry, options.port ?? 4983, record, token, options.pollMs ?? 500);
|
|
1311
|
+
process.once("exit", session.cleanup);
|
|
1312
|
+
await session.elect();
|
|
1313
|
+
if (!session.host && session.port !== 0) session.election = setInterval(() => void session.elect(), options.electionMs ?? 1e3).unref();
|
|
1314
|
+
return session;
|
|
1315
|
+
}
|
|
1316
|
+
/** Whether this session is the one serving the endpoint. */
|
|
1317
|
+
get hosting() {
|
|
1318
|
+
return this.host !== void 0;
|
|
1319
|
+
}
|
|
1320
|
+
/** The port clients reach — this host's own, when it chose one. */
|
|
1321
|
+
get endpointPort() {
|
|
1322
|
+
return this.host?.port ?? this.port;
|
|
1323
|
+
}
|
|
1324
|
+
/** Jupyter-style: the endpoint, with the token that opens it. */
|
|
1325
|
+
get connectUrl() {
|
|
1326
|
+
return `http://127.0.0.1:${this.endpointPort}${BASE}?token=${this.token}`;
|
|
1327
|
+
}
|
|
1328
|
+
/** Changes one of this session's apps, and tells the endpoint. */
|
|
1329
|
+
updateApp(name, change) {
|
|
1330
|
+
if (this.left) return;
|
|
1331
|
+
this.record.apps = this.record.apps.map((app) => app.name === name ? change(app) : app);
|
|
1332
|
+
this.record.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
1333
|
+
writeRecord(this.registry, this.record);
|
|
1334
|
+
this.host?.scan();
|
|
1335
|
+
}
|
|
1336
|
+
/** Leaves the registry, and the port to whichever session takes it next. */
|
|
1337
|
+
async leave() {
|
|
1338
|
+
if (this.left) return;
|
|
1339
|
+
this.left = true;
|
|
1340
|
+
if (this.election) clearInterval(this.election);
|
|
1341
|
+
process.removeListener("exit", this.cleanup);
|
|
1342
|
+
this.cleanup();
|
|
1343
|
+
await this.host?.close();
|
|
1344
|
+
this.host = void 0;
|
|
1345
|
+
if (liveSessions(this.registry).length === 0) try {
|
|
1346
|
+
(0, node_fs.unlinkSync)((0, node_path.join)(this.registry, "token"));
|
|
1347
|
+
} catch {}
|
|
1348
|
+
}
|
|
1349
|
+
async elect() {
|
|
1350
|
+
if (this.host || this.left) return;
|
|
1351
|
+
const host = new DiscoveryHost(this.registry, this.token, this.pollMs);
|
|
1352
|
+
if (!await host.listen(this.port)) return;
|
|
1353
|
+
if (this.left) {
|
|
1354
|
+
await host.close();
|
|
1355
|
+
return;
|
|
1356
|
+
}
|
|
1357
|
+
this.host = host;
|
|
1358
|
+
if (this.election) clearInterval(this.election);
|
|
1359
|
+
this.election = void 0;
|
|
1360
|
+
}
|
|
1361
|
+
};
|
|
1362
|
+
/** Joins the machine's discovery; see {@link DiscoverySession}. */
|
|
1363
|
+
function joinDiscovery(options) {
|
|
1364
|
+
return DiscoverySession.join(options);
|
|
1365
|
+
}
|
|
1366
|
+
/** The data APIs a generated dev app mounts, from what `gkm dev` built. */
|
|
1367
|
+
function dataApisOf(options) {
|
|
1368
|
+
if (options.selfServing) return [];
|
|
1369
|
+
return [
|
|
1370
|
+
...options.telescopePath ? [{
|
|
1371
|
+
kind: "telescope",
|
|
1372
|
+
path: `${options.telescopePath}/api`
|
|
1373
|
+
}] : [],
|
|
1374
|
+
...options.databaseApi ? [{
|
|
1375
|
+
kind: "database",
|
|
1376
|
+
path: options.databaseApi
|
|
1377
|
+
}] : [],
|
|
1378
|
+
...options.openApi ? [{
|
|
1379
|
+
kind: "openapi",
|
|
1380
|
+
path: "/__docs"
|
|
1381
|
+
}] : []
|
|
1382
|
+
];
|
|
1383
|
+
}
|
|
774
1384
|
//#endregion
|
|
775
1385
|
//#region ../../node_modules/.pnpm/@hono+node-server@1.19.5_hono@4.13.8/node_modules/@hono/node-server/dist/index.mjs
|
|
776
1386
|
var RequestError = class extends Error {
|
|
@@ -1436,6 +2046,7 @@ async function devCommand(options) {
|
|
|
1436
2046
|
const runtime = config.runtime ?? "node";
|
|
1437
2047
|
let secretsJsonPath;
|
|
1438
2048
|
let publicUrl;
|
|
2049
|
+
let manifest;
|
|
1439
2050
|
const appSecrets = await require_workspace$1.loadSecretsForApp(workspace ? await require_store.secretsStoreFor(workspace, config.stages.local) : new require_store.FileSecretsStore(secretsRoot), config.stages.local, workspaceAppName);
|
|
1440
2051
|
if (workspace) {
|
|
1441
2052
|
const reconciled = await require_workspace$1.reconcileWorkspace(workspace, {
|
|
@@ -1450,6 +2061,7 @@ async function devCommand(options) {
|
|
|
1450
2061
|
Object.assign(appSecrets, reconciled.env);
|
|
1451
2062
|
const own = reconciled.plan.resources.find((r) => (r.kind === "rest-api" || r.kind === "site") && require_config.appKey(r.id) === workspaceAppName);
|
|
1452
2063
|
publicUrl = own ? reconciled.env[own.envKey] : void 0;
|
|
2064
|
+
manifest = reconciled.manifest;
|
|
1453
2065
|
}
|
|
1454
2066
|
if (Object.keys(appSecrets).length > 0) {
|
|
1455
2067
|
const secretsDir = (0, node_path.join)(secretsRoot, ".gkm");
|
|
@@ -1462,6 +2074,38 @@ async function devCommand(options) {
|
|
|
1462
2074
|
devServer.label = workspaceAppName ?? "server";
|
|
1463
2075
|
devServer.publicUrl = publicUrl;
|
|
1464
2076
|
await devServer.start();
|
|
2077
|
+
const discovery = await joinDiscovery({
|
|
2078
|
+
role: "app",
|
|
2079
|
+
port: discoveryPort(workspace.dev),
|
|
2080
|
+
workspace: {
|
|
2081
|
+
name: workspace.name,
|
|
2082
|
+
root: workspace.root,
|
|
2083
|
+
stage: workspace.stages.local
|
|
2084
|
+
},
|
|
2085
|
+
allowedOrigins: workspace.dev?.allowedOrigins ?? [],
|
|
2086
|
+
...manifest ? { manifest } : {},
|
|
2087
|
+
apps: [{
|
|
2088
|
+
name: workspaceAppName,
|
|
2089
|
+
type: "backend",
|
|
2090
|
+
port: devServer.port,
|
|
2091
|
+
...publicUrl ? { publicUrl } : {},
|
|
2092
|
+
status: "ready",
|
|
2093
|
+
reloads: 0,
|
|
2094
|
+
dataApis: dataApisOf({
|
|
2095
|
+
selfServing: devServer.selfServing,
|
|
2096
|
+
telescopePath: telescope?.path,
|
|
2097
|
+
databaseApi: initial.databaseApi,
|
|
2098
|
+
openApi: enableOpenApi
|
|
2099
|
+
})
|
|
2100
|
+
}]
|
|
2101
|
+
});
|
|
2102
|
+
if (!process.env["GKM_DISCOVERY_QUIET"]) announce(discovery);
|
|
2103
|
+
devServer.onStatus = (status) => discovery.updateApp(workspaceAppName, (app) => ({
|
|
2104
|
+
...app,
|
|
2105
|
+
status,
|
|
2106
|
+
port: devServer.port,
|
|
2107
|
+
reloads: app.status === "reloading" && status === "ready" ? app.reloads + 1 : app.reloads
|
|
2108
|
+
}));
|
|
1465
2109
|
const hooksFile = (config.hooks?.server?.split("#"))?.[0];
|
|
1466
2110
|
const normalizedPatterns = [config.constructs, ...hooksFile ? [hooksFile.endsWith(".ts") ? hooksFile : `${hooksFile}.ts`] : []].flat().filter((p) => typeof p === "string").map((p) => (0, node_path.isAbsolute)(p) ? (0, node_path.relative)(appRoot, p) : p.replace(/^\.\//, ""));
|
|
1467
2111
|
const resolvedFiles = await (0, fast_glob.default)(normalizedPatterns, {
|
|
@@ -1502,7 +2146,11 @@ async function devCommand(options) {
|
|
|
1502
2146
|
if (isShuttingDown) return;
|
|
1503
2147
|
isShuttingDown = true;
|
|
1504
2148
|
logger$9.log("\n🛑 Shutting down...");
|
|
1505
|
-
Promise.all([
|
|
2149
|
+
Promise.all([
|
|
2150
|
+
watcher.close(),
|
|
2151
|
+
devServer.stop(),
|
|
2152
|
+
discovery.leave()
|
|
2153
|
+
]).catch((err) => {
|
|
1506
2154
|
logger$9.error("Error during shutdown:", err);
|
|
1507
2155
|
}).finally(() => {
|
|
1508
2156
|
process.exit(0);
|
|
@@ -1746,9 +2394,9 @@ async function workspaceDevCommand(configured, options) {
|
|
|
1746
2394
|
for (const { id, port } of fakes) logger$9.log(`🎭 ${id} fake: http://localhost:${port}`);
|
|
1747
2395
|
logger$9.log(options.migrate || options.seed ? "🗄️ Migrating the databases" : "🗄️ Checking for pending migrations");
|
|
1748
2396
|
if (options.migrate || options.seed) {
|
|
1749
|
-
const { prepareDevDatabases } = await Promise.resolve().then(() => require("./migrate-
|
|
2397
|
+
const { prepareDevDatabases } = await Promise.resolve().then(() => require("./migrate-CThJfVHR.cjs"));
|
|
1750
2398
|
await prepareDevDatabases(workspace, reconciled.env, { seed: !!options.seed });
|
|
1751
|
-
} else await Promise.resolve().then(() => require("./migrate-
|
|
2399
|
+
} else await Promise.resolve().then(() => require("./migrate-CThJfVHR.cjs")).then(({ reportPendingMigrations }) => reportPendingMigrations(workspace, reconciled.env)).catch((error) => {
|
|
1752
2400
|
logger$9.log(`⚠️ Could not check for pending migrations: ${error instanceof Error ? error.message : String(error)}`);
|
|
1753
2401
|
});
|
|
1754
2402
|
const appUrls = {};
|
|
@@ -1803,12 +2451,39 @@ async function workspaceDevCommand(configured, options) {
|
|
|
1803
2451
|
break;
|
|
1804
2452
|
}
|
|
1805
2453
|
}
|
|
2454
|
+
const discovery = await joinDiscovery({
|
|
2455
|
+
role: "workspace",
|
|
2456
|
+
port: discoveryPort(workspace.dev),
|
|
2457
|
+
workspace: {
|
|
2458
|
+
name: workspace.name,
|
|
2459
|
+
root: workspace.root,
|
|
2460
|
+
stage: workspace.stages.local
|
|
2461
|
+
},
|
|
2462
|
+
allowedOrigins: workspace.dev?.allowedOrigins ?? [],
|
|
2463
|
+
...reconciled.manifest ? { manifest: reconciled.manifest } : {},
|
|
2464
|
+
apps: buildOrder.flatMap((name) => {
|
|
2465
|
+
const app = workspace.apps[name];
|
|
2466
|
+
if (!app) return [];
|
|
2467
|
+
const publicUrl = Object.entries(appUrls).find(([id]) => require_config.appKey(id) === name)?.[1];
|
|
2468
|
+
return [{
|
|
2469
|
+
name,
|
|
2470
|
+
type: app.type,
|
|
2471
|
+
port: app.port,
|
|
2472
|
+
...publicUrl ? { publicUrl } : {},
|
|
2473
|
+
status: "starting",
|
|
2474
|
+
reloads: 0,
|
|
2475
|
+
dataApis: []
|
|
2476
|
+
}];
|
|
2477
|
+
})
|
|
2478
|
+
});
|
|
2479
|
+
announce(discovery);
|
|
1806
2480
|
const turboEnv = {
|
|
1807
2481
|
...process.env,
|
|
1808
2482
|
...secretsEnv,
|
|
1809
2483
|
NODE_ENV: "development",
|
|
1810
2484
|
...configPath ? { GKM_CONFIG_PATH: configPath } : {},
|
|
1811
|
-
...options.fake ? { [require_workspace$1.FAKE_ENV]: "1" } : {}
|
|
2485
|
+
...options.fake ? { [require_workspace$1.FAKE_ENV]: "1" } : {},
|
|
2486
|
+
[DISCOVERY_QUIET_ENV]: "1"
|
|
1812
2487
|
};
|
|
1813
2488
|
logger$9.log(`🚀 Starting ${appCount} app(s) — each builds before it answers, and says so when it is ready`);
|
|
1814
2489
|
const turboProcess = (0, node_child_process.spawn)("pnpm", [
|
|
@@ -1828,6 +2503,7 @@ async function workspaceDevCommand(configured, options) {
|
|
|
1828
2503
|
isShuttingDown = true;
|
|
1829
2504
|
logger$9.log("\n🛑 Shutting down workspace...");
|
|
1830
2505
|
closeFakes(fakes);
|
|
2506
|
+
discovery.leave();
|
|
1831
2507
|
const pid = turboProcess.pid;
|
|
1832
2508
|
if (pid) try {
|
|
1833
2509
|
process.kill(-pid, "SIGTERM");
|
|
@@ -2068,6 +2744,13 @@ start({
|
|
|
2068
2744
|
});
|
|
2069
2745
|
`;
|
|
2070
2746
|
}
|
|
2747
|
+
/**
|
|
2748
|
+
* The discovery endpoint's connect URL, Jupyter-style: the token in it is what
|
|
2749
|
+
* lets a console read what this machine is running.
|
|
2750
|
+
*/
|
|
2751
|
+
function announce(discovery) {
|
|
2752
|
+
logger$9.log(`🧭 Discovery: ${discovery.connectUrl}`);
|
|
2753
|
+
}
|
|
2071
2754
|
/** Run `fn` with `console.log` muted — warnings and errors still print. */
|
|
2072
2755
|
async function quietly(fn) {
|
|
2073
2756
|
const log = console.log;
|
|
@@ -2165,6 +2848,12 @@ var DevServer = class {
|
|
|
2165
2848
|
}
|
|
2166
2849
|
/** What the ready line calls this app. */
|
|
2167
2850
|
label = "server";
|
|
2851
|
+
/** Told when the server is ready, reloading, or exits on its own. */
|
|
2852
|
+
onStatus;
|
|
2853
|
+
/** The port it is serving on. */
|
|
2854
|
+
get port() {
|
|
2855
|
+
return this.actualPort;
|
|
2856
|
+
}
|
|
2168
2857
|
/**
|
|
2169
2858
|
* Where the app is reached — its HTTPS address behind the edge, when it has
|
|
2170
2859
|
* one. The local port is only what the edge forwards to.
|
|
@@ -2200,9 +2889,11 @@ var DevServer = class {
|
|
|
2200
2889
|
this.serverProcess.on("error", (error) => {
|
|
2201
2890
|
logger$9.error("❌ Server error:", error);
|
|
2202
2891
|
});
|
|
2203
|
-
this.serverProcess
|
|
2892
|
+
const child = this.serverProcess;
|
|
2893
|
+
child.on("exit", (code, signal) => {
|
|
2204
2894
|
if (code !== null && code !== 0 && signal !== "SIGTERM") logger$9.error(`❌ Server exited with code ${code}`);
|
|
2205
2895
|
this.isRunning = false;
|
|
2896
|
+
if (this.serverProcess === child) this.onStatus?.("stopped");
|
|
2206
2897
|
});
|
|
2207
2898
|
await new Promise((resolve) => setTimeout(resolve, 1e3));
|
|
2208
2899
|
if (this.isRunning) {
|
|
@@ -2216,6 +2907,7 @@ var DevServer = class {
|
|
|
2216
2907
|
...this.databaseApi ? [`db ${this.databaseApi}`] : []
|
|
2217
2908
|
];
|
|
2218
2909
|
if (tools.length > 0) logger$9.log(` ${tools.join(" · ")}`);
|
|
2910
|
+
this.onStatus?.("ready");
|
|
2219
2911
|
}
|
|
2220
2912
|
}
|
|
2221
2913
|
async stop() {
|
|
@@ -2241,6 +2933,7 @@ var DevServer = class {
|
|
|
2241
2933
|
}
|
|
2242
2934
|
async restart() {
|
|
2243
2935
|
const portToReuse = this.actualPort;
|
|
2936
|
+
this.onStatus?.("reloading");
|
|
2244
2937
|
await this.stop();
|
|
2245
2938
|
let attempts = 0;
|
|
2246
2939
|
while (attempts < 30) {
|
|
@@ -2774,14 +3467,34 @@ var CommandTimedOut = class extends Error {
|
|
|
2774
3467
|
* once `timeoutMs` passes (after the child is gone), and with the spawn error
|
|
2775
3468
|
* itself when the program cannot be started at all — `ENOENT` already names it.
|
|
2776
3469
|
*/
|
|
2777
|
-
function run(command, args, options = {}) {
|
|
3470
|
+
async function run(command, args, options = {}) {
|
|
3471
|
+
await spawnChild(command, args, options, false);
|
|
3472
|
+
}
|
|
3473
|
+
/**
|
|
3474
|
+
* {@link run}, resolving with what the program wrote to stdout — for a value
|
|
3475
|
+
* the CLI reads back, such as the digest `docker inspect` reports. stderr still
|
|
3476
|
+
* reaches the terminal, so a failure explains itself the same way.
|
|
3477
|
+
*/
|
|
3478
|
+
function runOutput(command, args, options = {}) {
|
|
3479
|
+
return spawnChild(command, args, options, true);
|
|
3480
|
+
}
|
|
3481
|
+
function spawnChild(command, args, options, capture) {
|
|
2778
3482
|
const { timeoutMs = DEFAULT_TIMEOUT_MS, ...spawnOptions } = options;
|
|
2779
3483
|
return new Promise((resolve, reject) => {
|
|
2780
3484
|
const child = (0, node_child_process.spawn)(command, [...args], {
|
|
2781
3485
|
stdio: "inherit",
|
|
2782
3486
|
...spawnOptions,
|
|
3487
|
+
...capture ? { stdio: [
|
|
3488
|
+
"ignore",
|
|
3489
|
+
"pipe",
|
|
3490
|
+
"inherit"
|
|
3491
|
+
] } : {},
|
|
2783
3492
|
shell: false
|
|
2784
3493
|
});
|
|
3494
|
+
let stdout = "";
|
|
3495
|
+
child.stdout?.on("data", (chunk) => {
|
|
3496
|
+
stdout += chunk.toString();
|
|
3497
|
+
});
|
|
2785
3498
|
let timedOut = false;
|
|
2786
3499
|
let killer;
|
|
2787
3500
|
const timer = setTimeout(() => {
|
|
@@ -2802,7 +3515,7 @@ function run(command, args, options = {}) {
|
|
|
2802
3515
|
child.on("close", (code, signal) => {
|
|
2803
3516
|
settle();
|
|
2804
3517
|
if (timedOut) reject(new CommandTimedOut(command, args, timeoutMs));
|
|
2805
|
-
else if (code === 0) resolve();
|
|
3518
|
+
else if (code === 0) resolve(stdout);
|
|
2806
3519
|
else reject(new CommandFailed(command, args, code, signal));
|
|
2807
3520
|
});
|
|
2808
3521
|
});
|
|
@@ -3109,7 +3822,8 @@ async function buildOneApp(input) {
|
|
|
3109
3822
|
hooks,
|
|
3110
3823
|
markOptional: options.markOptional ?? false,
|
|
3111
3824
|
skipBundle: options.skipBundle ?? false,
|
|
3112
|
-
stage: options.stage
|
|
3825
|
+
stage: options.stage,
|
|
3826
|
+
workspaceName: workspace.name
|
|
3113
3827
|
});
|
|
3114
3828
|
}
|
|
3115
3829
|
/**
|
|
@@ -3187,7 +3901,12 @@ async function buildApp(input) {
|
|
|
3187
3901
|
appRoot,
|
|
3188
3902
|
cache: require_ready.cacheBackendsIn(declared, cacheBackend)
|
|
3189
3903
|
}),
|
|
3190
|
-
markOptional: input.markOptional ?? false
|
|
3904
|
+
markOptional: input.markOptional ?? false,
|
|
3905
|
+
...production && { telemetry: require_OpenApiTsGenerator.telemetryFor({
|
|
3906
|
+
appRoot,
|
|
3907
|
+
surfaceId: derived.surface?.id,
|
|
3908
|
+
workspaceName: input.workspaceName
|
|
3909
|
+
}) }
|
|
3191
3910
|
};
|
|
3192
3911
|
const endpointGenerator = new require_OpenApiTsGenerator.EndpointGenerator();
|
|
3193
3912
|
const functionGenerator = new FunctionGenerator();
|
|
@@ -3596,7 +4315,7 @@ const PROVISIONERS = {
|
|
|
3596
4315
|
const name = require_workspace$1.resourceName(declaration.id, declaration.kind, context.stage);
|
|
3597
4316
|
const { postgres } = await context.api.findOrCreatePostgres(serviceName({
|
|
3598
4317
|
stage: context.stage,
|
|
3599
|
-
app: context.project
|
|
4318
|
+
app: context.scope ?? context.project
|
|
3600
4319
|
}, declaration.id), context.projectId, context.environmentId, {
|
|
3601
4320
|
databaseName: name,
|
|
3602
4321
|
databasePassword: derivedPassword(context, `${name}:master`)
|
|
@@ -3730,7 +4449,7 @@ const PROVISIONERS = {
|
|
|
3730
4449
|
if (backend !== "minio") throw new UnprovisionableBucket(declaration.id, backend);
|
|
3731
4450
|
const service = serviceName({
|
|
3732
4451
|
stage: context.stage,
|
|
3733
|
-
app: context.project
|
|
4452
|
+
app: context.scope ?? context.project
|
|
3734
4453
|
}, declaration.id);
|
|
3735
4454
|
const bucket = require_workspace$1.resourceName(declaration.id, declaration.kind, context.stage);
|
|
3736
4455
|
const user = `${bucket}-root`;
|
|
@@ -4191,6 +4910,7 @@ async function provisionDeclared(options) {
|
|
|
4191
4910
|
environmentId: options.environmentId,
|
|
4192
4911
|
stage: options.stage,
|
|
4193
4912
|
project: workspace.name,
|
|
4913
|
+
scope: options.scope ?? workspace.name,
|
|
4194
4914
|
cache: require_backends.cacheBackendFor(require_backends.providerOf(workspace)),
|
|
4195
4915
|
events: require_backends.eventsBackendFor(require_backends.providerOf(workspace)),
|
|
4196
4916
|
storage: require_backends.storageBackendFor(require_backends.providerOf(workspace)),
|
|
@@ -4295,18 +5015,21 @@ function createEmptyState(stage, projectId, environmentId) {
|
|
|
4295
5015
|
};
|
|
4296
5016
|
}
|
|
4297
5017
|
/**
|
|
4298
|
-
* Get application ID from state
|
|
4299
|
-
*/
|
|
4300
|
-
function getApplicationId(state, appName) {
|
|
4301
|
-
return state?.applications[appName];
|
|
4302
|
-
}
|
|
4303
|
-
/**
|
|
4304
5018
|
* Set application ID in state (mutates state)
|
|
4305
5019
|
*/
|
|
4306
5020
|
function setApplicationId(state, appName, applicationId) {
|
|
4307
5021
|
state.applications[appName] = applicationId;
|
|
4308
5022
|
}
|
|
4309
5023
|
/**
|
|
5024
|
+
* Record the image an app was deployed with (mutates state)
|
|
5025
|
+
*/
|
|
5026
|
+
function setDeployedImage(state, appName, image) {
|
|
5027
|
+
state.images = {
|
|
5028
|
+
...state.images,
|
|
5029
|
+
[appName]: image
|
|
5030
|
+
};
|
|
5031
|
+
}
|
|
5032
|
+
/**
|
|
4310
5033
|
* Get a generated secret for an app
|
|
4311
5034
|
*/
|
|
4312
5035
|
function getGeneratedSecret(state, appName, secretName) {
|
|
@@ -6084,6 +6807,33 @@ async function pushImage(imageRef) {
|
|
|
6084
6807
|
}
|
|
6085
6808
|
}
|
|
6086
6809
|
/**
|
|
6810
|
+
* The digest the registry gave a pushed image, `sha256:…`.
|
|
6811
|
+
*
|
|
6812
|
+
* A tag can be pushed over — by the next deploy, or by anything else that
|
|
6813
|
+
* shares the repository — and the digest is what says which image a stage
|
|
6814
|
+
* actually ran. `RepoDigests` has one entry per repository the image was
|
|
6815
|
+
* pushed to, so the one for this ref's repository is picked by name.
|
|
6816
|
+
*
|
|
6817
|
+
* @internal Exported for testing
|
|
6818
|
+
*/
|
|
6819
|
+
async function pushedDigest(imageRef) {
|
|
6820
|
+
const repository = imageRef.split("@")[0].replace(/:[\w][\w.-]*$/, "");
|
|
6821
|
+
const output = await runOutput("docker", [
|
|
6822
|
+
"image",
|
|
6823
|
+
"inspect",
|
|
6824
|
+
"--format={{json .RepoDigests}}",
|
|
6825
|
+
imageRef
|
|
6826
|
+
], { cwd: process.cwd() });
|
|
6827
|
+
let digests;
|
|
6828
|
+
try {
|
|
6829
|
+
digests = JSON.parse(output.trim() || "[]");
|
|
6830
|
+
} catch {
|
|
6831
|
+
return;
|
|
6832
|
+
}
|
|
6833
|
+
if (!Array.isArray(digests)) return void 0;
|
|
6834
|
+
return digests.find((entry) => typeof entry === "string" && entry.startsWith(`${repository}@`))?.slice(repository.length + 1);
|
|
6835
|
+
}
|
|
6836
|
+
/**
|
|
6087
6837
|
* Deploy using Docker (build and optionally push image)
|
|
6088
6838
|
*/
|
|
6089
6839
|
async function deployDocker(options) {
|
|
@@ -6091,9 +6841,13 @@ async function deployDocker(options) {
|
|
|
6091
6841
|
const imageName = config.imageName;
|
|
6092
6842
|
const imageRef = validateImageRef(getImageRef(config.registry, imageName, tag));
|
|
6093
6843
|
await buildImage(imageRef, buildArgs, credentials);
|
|
6844
|
+
let digest;
|
|
6094
6845
|
if (!skipPush) {
|
|
6095
6846
|
if (!config.registry) logger$5.warn("\n⚠️ No registry configured. Use --skip-push or configure docker.registry in gkm.config.ts");
|
|
6096
|
-
else
|
|
6847
|
+
else {
|
|
6848
|
+
await pushImage(imageRef);
|
|
6849
|
+
digest = await pushedDigest(imageRef);
|
|
6850
|
+
}
|
|
6097
6851
|
}
|
|
6098
6852
|
logger$5.log("\n✅ Docker deployment ready!");
|
|
6099
6853
|
logger$5.log(`\n📋 Deployment details:`);
|
|
@@ -6102,25 +6856,10 @@ async function deployDocker(options) {
|
|
|
6102
6856
|
if (masterKey) logger$5.log(`\n🔐 Run the container with GKM_MASTER_KEY set to the key with fingerprint ${keyFingerprint(masterKey)}.`);
|
|
6103
6857
|
return {
|
|
6104
6858
|
imageRef,
|
|
6859
|
+
...digest ? { digest } : {},
|
|
6105
6860
|
masterKey
|
|
6106
6861
|
};
|
|
6107
6862
|
}
|
|
6108
|
-
/**
|
|
6109
|
-
* What one application is called on a provider.
|
|
6110
|
-
*
|
|
6111
|
-
* The same rule the constructs use, through the same `scopedName`: the
|
|
6112
|
-
* application beside `production-kitchen-sink-database` is
|
|
6113
|
-
* `production-kitchen-sink-api`, not `api`. Both deploy paths call this — the
|
|
6114
|
-
* workspace one named its applications by the bare app key, so a project held
|
|
6115
|
-
* an `api` and a `web` that every stage would collide on.
|
|
6116
|
-
*
|
|
6117
|
-
* The app id is dropped when it repeats the project, so a project named for its
|
|
6118
|
-
* one application is `production-kitchen-sink` rather than
|
|
6119
|
-
* `production-kitchen-sink-kitchen-sink`.
|
|
6120
|
-
*/
|
|
6121
|
-
function applicationName(stage, project, app) {
|
|
6122
|
-
return app === project ? `${stage}-${project}`.toLowerCase() : (0, _geekmidas_manifest.scopedName)([stage, project], app);
|
|
6123
|
-
}
|
|
6124
6863
|
//#endregion
|
|
6125
6864
|
//#region src/deploy/dokploy-api.ts
|
|
6126
6865
|
var dokploy_api_exports = /* @__PURE__ */ require_rolldown_runtime.__exportAll({
|
|
@@ -6337,6 +7076,18 @@ var DokployApi = class {
|
|
|
6337
7076
|
});
|
|
6338
7077
|
}
|
|
6339
7078
|
/**
|
|
7079
|
+
* Change a project's name or description.
|
|
7080
|
+
*
|
|
7081
|
+
* The name is sent with the description because `project.update` validates
|
|
7082
|
+
* the whole project on some Dokploy versions, and a missing name fails it.
|
|
7083
|
+
*/
|
|
7084
|
+
async updateProject(projectId, updates) {
|
|
7085
|
+
await this.post("project.update", {
|
|
7086
|
+
projectId,
|
|
7087
|
+
...updates
|
|
7088
|
+
});
|
|
7089
|
+
}
|
|
7090
|
+
/**
|
|
6340
7091
|
* Delete a project and all its resources
|
|
6341
7092
|
*/
|
|
6342
7093
|
async deleteProject(projectId) {
|
|
@@ -7036,63 +7787,415 @@ function random() {
|
|
|
7036
7787
|
return (0, node_crypto.randomBytes)(32).toString("base64url");
|
|
7037
7788
|
}
|
|
7038
7789
|
//#endregion
|
|
7039
|
-
//#region src/deploy/
|
|
7790
|
+
//#region src/deploy/identity.ts
|
|
7040
7791
|
/**
|
|
7041
|
-
*
|
|
7792
|
+
* Who a deploy is, on a target it shares with other workspaces.
|
|
7793
|
+
*
|
|
7794
|
+
* A Dokploy server is one flat namespace of projects, images and Docker
|
|
7795
|
+
* service names. Deploys used to identify everything they made by the
|
|
7796
|
+
* workspace name alone, so two workspaces called `shop` — or `Shop` and
|
|
7797
|
+
* `shop` — deploying to one server landed in the same project, pushed over
|
|
7798
|
+
* each other's images and redeployed each other's applications. A
|
|
7799
|
+
* `DeployIdentity` is what a deploy names and claims its resources by instead.
|
|
7800
|
+
*/
|
|
7801
|
+
/** A namespace that cannot be part of a Dokploy name or an image path. */
|
|
7802
|
+
var DeployNamespaceInvalid = class extends Error {
|
|
7803
|
+
namespace;
|
|
7804
|
+
constructor(namespace) {
|
|
7805
|
+
super(`deploy.namespace '${namespace}' is not usable as a name: use lowercase letters, digits and single '-' between them (e.g. 'acme' or 'acme-platform') in gkm.config.ts.`);
|
|
7806
|
+
this.namespace = namespace;
|
|
7807
|
+
this.name = "DeployNamespaceInvalid";
|
|
7808
|
+
}
|
|
7809
|
+
};
|
|
7810
|
+
/** A workspace name with nothing a target can name a resource by. */
|
|
7811
|
+
var DeployProjectNameInvalid = class extends Error {
|
|
7812
|
+
workspaceName;
|
|
7813
|
+
constructor(workspaceName) {
|
|
7814
|
+
super(`The workspace name '${workspaceName}' has no letters or digits to name deployed resources by. Set "name" in gkm.config.ts.`);
|
|
7815
|
+
this.workspaceName = workspaceName;
|
|
7816
|
+
this.name = "DeployProjectNameInvalid";
|
|
7817
|
+
}
|
|
7818
|
+
};
|
|
7819
|
+
const NAME = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
|
|
7820
|
+
/**
|
|
7821
|
+
* Lowercase `[a-z0-9-]`, runs of anything else collapsed to one `-`: valid as
|
|
7822
|
+
* a Docker image path component, a Docker service name and a Dokploy name.
|
|
7042
7823
|
*/
|
|
7043
|
-
function
|
|
7044
|
-
return
|
|
7824
|
+
function slug$1(value) {
|
|
7825
|
+
return value.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
|
|
7826
|
+
}
|
|
7827
|
+
/** The namespace a workspace is in when it does not name one. */
|
|
7828
|
+
function defaultNamespace(workspaceName) {
|
|
7829
|
+
return slug$1((0, _geekmidas_manifest.kebabCase)(workspaceName));
|
|
7830
|
+
}
|
|
7831
|
+
function deployIdentity(workspace, stage) {
|
|
7832
|
+
const project = slug$1(workspace.name);
|
|
7833
|
+
if (!project) throw new DeployProjectNameInvalid(workspace.name);
|
|
7834
|
+
const fallback = defaultNamespace(workspace.name);
|
|
7835
|
+
const namespace = workspace.deploy?.namespace ?? fallback;
|
|
7836
|
+
if (!NAME.test(namespace)) throw new DeployNamespaceInvalid(namespace);
|
|
7837
|
+
return {
|
|
7838
|
+
namespace,
|
|
7839
|
+
project,
|
|
7840
|
+
stage,
|
|
7841
|
+
key: `${namespace}/${project}`,
|
|
7842
|
+
scope: namespace === fallback ? project : `${namespace}-${project}`
|
|
7843
|
+
};
|
|
7045
7844
|
}
|
|
7046
7845
|
/**
|
|
7047
|
-
*
|
|
7846
|
+
* The token a Dokploy project's description carries to say which identity
|
|
7847
|
+
* created it. A project with the right name and no marker belongs to someone
|
|
7848
|
+
* else, and a deploy never adopts it.
|
|
7849
|
+
*/
|
|
7850
|
+
function ownershipMarker(identity) {
|
|
7851
|
+
return `gkm:${identity.key}`;
|
|
7852
|
+
}
|
|
7853
|
+
/** Whether a project description carries `identity`'s marker. */
|
|
7854
|
+
function isOwnedBy(description, identity) {
|
|
7855
|
+
const marker = ownershipMarker(identity);
|
|
7856
|
+
return (description ?? "").split(/\s+/).includes(marker);
|
|
7857
|
+
}
|
|
7858
|
+
/** Any `gkm:` marker a description carries — whoever's it is. */
|
|
7859
|
+
function markerOf(description) {
|
|
7860
|
+
return (description ?? "").split(/\s+/).find((t) => t.startsWith("gkm:"));
|
|
7861
|
+
}
|
|
7862
|
+
/** What the Dokploy project is called. */
|
|
7863
|
+
function projectName(identity) {
|
|
7864
|
+
return identity.scope;
|
|
7865
|
+
}
|
|
7866
|
+
/**
|
|
7867
|
+
* The repository an app's image is pushed to, below the registry:
|
|
7868
|
+
* `<namespace>/<project>-<app>`.
|
|
7048
7869
|
*
|
|
7049
|
-
*
|
|
7050
|
-
*
|
|
7051
|
-
*
|
|
7052
|
-
|
|
7870
|
+
* The namespace is a path segment of its own so that a registry that maps
|
|
7871
|
+
* paths to owners (an organisation's packages, a Harbor project) can grant it
|
|
7872
|
+
* as one; the app is joined to the project so the two read as one name.
|
|
7873
|
+
*/
|
|
7874
|
+
function imageName(identity, app) {
|
|
7875
|
+
return `${identity.namespace}/${identity.project}-${slug$1((0, _geekmidas_manifest.kebabCase)(app))}`;
|
|
7876
|
+
}
|
|
7877
|
+
/** The full image ref an app is built, pushed and deployed as. */
|
|
7878
|
+
function imageRef(identity, app, registry, tag) {
|
|
7879
|
+
const name = imageName(identity, app);
|
|
7880
|
+
return registry ? `${registry.replace(/\/+$/, "")}/${name}:${tag}` : `${name}:${tag}`;
|
|
7881
|
+
}
|
|
7882
|
+
/**
|
|
7883
|
+
* What one application is called on the target.
|
|
7884
|
+
*
|
|
7885
|
+
* The same rule the constructs use, through the same `scopedName`, scoped by
|
|
7886
|
+
* the stage and the identity: the application beside
|
|
7887
|
+
* `production-kitchen-sink-database` is `production-kitchen-sink-api`. A
|
|
7888
|
+
* Dokploy application's name is also its Docker service name, which is unique
|
|
7889
|
+
* on the whole server, so the namespace has to be in it too — `acme`'s and
|
|
7890
|
+
* `globex`'s `shop` would otherwise both run `production-shop-api`.
|
|
7053
7891
|
*
|
|
7054
|
-
*
|
|
7055
|
-
*
|
|
7892
|
+
* The app id is dropped when it repeats the project, so a project named for its
|
|
7893
|
+
* one application is `production-kitchen-sink` rather than
|
|
7894
|
+
* `production-kitchen-sink-kitchen-sink`.
|
|
7056
7895
|
*/
|
|
7057
|
-
|
|
7058
|
-
|
|
7059
|
-
|
|
7060
|
-
|
|
7061
|
-
|
|
7896
|
+
function applicationName(identity, app) {
|
|
7897
|
+
return slug$1(app) === identity.project ? `${identity.stage}-${identity.scope}` : (0, _geekmidas_manifest.scopedName)([identity.stage, identity.scope], app);
|
|
7898
|
+
}
|
|
7899
|
+
//#endregion
|
|
7900
|
+
//#region src/deploy/journal.ts
|
|
7901
|
+
var DeployJournal = class DeployJournal {
|
|
7902
|
+
store;
|
|
7903
|
+
stage;
|
|
7904
|
+
state;
|
|
7905
|
+
resources;
|
|
7906
|
+
version;
|
|
7907
|
+
existed;
|
|
7908
|
+
constructor(store, stage, state, resources, version, existed) {
|
|
7909
|
+
this.store = store;
|
|
7910
|
+
this.stage = stage;
|
|
7911
|
+
this.state = state;
|
|
7912
|
+
this.resources = resources;
|
|
7913
|
+
this.version = version;
|
|
7914
|
+
this.existed = existed;
|
|
7062
7915
|
}
|
|
7063
|
-
|
|
7064
|
-
|
|
7065
|
-
|
|
7066
|
-
|
|
7916
|
+
/**
|
|
7917
|
+
* The stage's journal — its stored state, or `initial` written as the
|
|
7918
|
+
* stage's first state, so there is somewhere to record the very first
|
|
7919
|
+
* resource before it is created.
|
|
7920
|
+
*/
|
|
7921
|
+
static async open(store, stage, initial) {
|
|
7922
|
+
const stored = await store.read(stage);
|
|
7923
|
+
if (stored) return new DeployJournal(store, stage, stored.state, stored.resources, stored.version, true);
|
|
7924
|
+
const state = initial();
|
|
7925
|
+
const version = await store.write(stage, state, { expectedVersion: null });
|
|
7926
|
+
return new DeployJournal(store, stage, state, {}, version, false);
|
|
7927
|
+
}
|
|
7928
|
+
record(key) {
|
|
7929
|
+
return this.resources[key];
|
|
7930
|
+
}
|
|
7931
|
+
/** The keys a previous run marked `pending` and never saw created. */
|
|
7932
|
+
unfinished() {
|
|
7933
|
+
return Object.values(this.resources).filter((record) => record.status === "pending").map((record) => record.key);
|
|
7934
|
+
}
|
|
7935
|
+
/** Records that `entry` is about to be created. */
|
|
7936
|
+
async pending(entry) {
|
|
7937
|
+
await this.put({
|
|
7938
|
+
...entry,
|
|
7939
|
+
status: "pending"
|
|
7940
|
+
});
|
|
7067
7941
|
}
|
|
7068
|
-
if
|
|
7069
|
-
|
|
7070
|
-
|
|
7071
|
-
|
|
7072
|
-
|
|
7073
|
-
|
|
7074
|
-
|
|
7075
|
-
|
|
7076
|
-
|
|
7077
|
-
|
|
7942
|
+
/** Records `entry` as existing with `id`; a no-op if it already is. */
|
|
7943
|
+
async ready(entry, id) {
|
|
7944
|
+
const current = this.resources[entry.key];
|
|
7945
|
+
if (current?.status === "ready" && current.id === id) return;
|
|
7946
|
+
await this.put({
|
|
7947
|
+
...entry,
|
|
7948
|
+
status: "ready",
|
|
7949
|
+
id
|
|
7950
|
+
});
|
|
7951
|
+
}
|
|
7952
|
+
/**
|
|
7953
|
+
* The resource `entry` names: by its recorded id, else by looking it up,
|
|
7954
|
+
* else created — with `pending` written before the create and `ready`
|
|
7955
|
+
* after, so a run that dies in between leaves a record the next run
|
|
7956
|
+
* resolves by looking the resource up rather than creating another.
|
|
7957
|
+
*/
|
|
7958
|
+
async ensure(entry, steps) {
|
|
7959
|
+
const record = this.resources[entry.key];
|
|
7960
|
+
let staleId;
|
|
7961
|
+
if (record?.status === "ready" && record.id && steps.get) {
|
|
7962
|
+
const known = await steps.get(record.id);
|
|
7963
|
+
if (known) {
|
|
7964
|
+
await this.ready(entry, steps.id(known));
|
|
7965
|
+
return {
|
|
7966
|
+
resource: known,
|
|
7967
|
+
via: "recorded"
|
|
7968
|
+
};
|
|
7969
|
+
}
|
|
7970
|
+
staleId = record.id;
|
|
7971
|
+
}
|
|
7972
|
+
const existing = await steps.find();
|
|
7973
|
+
if (existing) {
|
|
7974
|
+
await this.ready(entry, steps.id(existing));
|
|
7975
|
+
return {
|
|
7976
|
+
resource: existing,
|
|
7977
|
+
via: record?.status === "pending" ? "resumed" : "found",
|
|
7978
|
+
...staleId ? { staleId } : {}
|
|
7979
|
+
};
|
|
7980
|
+
}
|
|
7981
|
+
await this.pending(entry);
|
|
7982
|
+
const created = await steps.create();
|
|
7983
|
+
await this.ready(entry, steps.id(created));
|
|
7984
|
+
return {
|
|
7985
|
+
resource: created,
|
|
7986
|
+
via: "created",
|
|
7987
|
+
...staleId ? { staleId } : {}
|
|
7988
|
+
};
|
|
7989
|
+
}
|
|
7990
|
+
/** Writes the stage's state, keeping its resource records. */
|
|
7991
|
+
async save() {
|
|
7992
|
+
this.state.lastDeployedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
7993
|
+
this.version = await this.store.write(this.stage, this.state, { expectedVersion: this.version });
|
|
7994
|
+
}
|
|
7995
|
+
async put(record) {
|
|
7996
|
+
this.version = await this.store.putResource(this.stage, record, { expectedVersion: this.version });
|
|
7997
|
+
this.resources = {
|
|
7998
|
+
...this.resources,
|
|
7999
|
+
[record.key]: {
|
|
8000
|
+
...record,
|
|
8001
|
+
updatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
8002
|
+
}
|
|
8003
|
+
};
|
|
8004
|
+
}
|
|
8005
|
+
};
|
|
8006
|
+
//#endregion
|
|
8007
|
+
//#region src/deploy/ownership.ts
|
|
8008
|
+
/**
|
|
8009
|
+
* Which Dokploy project a deploy is allowed to deploy into.
|
|
8010
|
+
*
|
|
8011
|
+
* Deploy used to take any project whose name matched the workspace's, in any
|
|
8012
|
+
* case — so a second workspace called `shop` (or `Shop`) on the same server
|
|
8013
|
+
* deployed into the first one's project, over its images and applications. A
|
|
8014
|
+
* project is now claimed by an ownership marker in its description, and a
|
|
8015
|
+
* project with the right name and no marker is somebody else's.
|
|
8016
|
+
*/
|
|
8017
|
+
/**
|
|
8018
|
+
* A Dokploy project with the name this deploy would use exists, and nothing
|
|
8019
|
+
* says this workspace created it.
|
|
8020
|
+
*/
|
|
8021
|
+
var ProjectNotOwned = class extends Error {
|
|
8022
|
+
projectName;
|
|
8023
|
+
projectId;
|
|
8024
|
+
identity;
|
|
8025
|
+
ownedBy;
|
|
8026
|
+
constructor(projectName, projectId, identity, ownedBy) {
|
|
8027
|
+
super(`The Dokploy project '${projectName}' (${projectId}) was not created by this workspace (${identity})${ownedBy ? ` — it is marked '${ownedBy}'` : ""}, so deploy will not use it. Set deploy.namespace in gkm.config.ts to deploy under a name of your own, or, if this project really is this workspace's, add 'gkm:${identity}' to its description in Dokploy.`);
|
|
8028
|
+
this.projectName = projectName;
|
|
8029
|
+
this.projectId = projectId;
|
|
8030
|
+
this.identity = identity;
|
|
8031
|
+
this.ownedBy = ownedBy;
|
|
8032
|
+
this.name = "ProjectNotOwned";
|
|
8033
|
+
}
|
|
8034
|
+
};
|
|
8035
|
+
/** A description with the marker added, keeping what was there. */
|
|
8036
|
+
function withMarker(description, marker) {
|
|
8037
|
+
const existing = (description ?? "").trim();
|
|
8038
|
+
return existing ? `${existing}\n${marker}` : marker;
|
|
8039
|
+
}
|
|
8040
|
+
/** `project.one`, or `null` when Dokploy no longer has the project. */
|
|
8041
|
+
async function projectById(api, projectId) {
|
|
8042
|
+
try {
|
|
8043
|
+
return await api.getProject(projectId) ?? null;
|
|
8044
|
+
} catch (error) {
|
|
8045
|
+
if (error instanceof DokployApiError) return null;
|
|
8046
|
+
throw error;
|
|
8047
|
+
}
|
|
8048
|
+
}
|
|
8049
|
+
/**
|
|
8050
|
+
* The project this deploy deploys into:
|
|
8051
|
+
*
|
|
8052
|
+
* 1. The one the stage's state names, if Dokploy still has it. State written
|
|
8053
|
+
* before markers existed is trusted — it holds an id this workspace was
|
|
8054
|
+
* given when it created the project — and the marker is written now, so
|
|
8055
|
+
* the project is claimed for the next deploy that has no state.
|
|
8056
|
+
* 2. One with this name *and* this identity's marker.
|
|
8057
|
+
* 3. A new one, marked.
|
|
8058
|
+
*
|
|
8059
|
+
* A project that matches by name (in any case) but carries no marker, or
|
|
8060
|
+
* another identity's, raises `ProjectNotOwned` — it is never adopted.
|
|
8061
|
+
*/
|
|
8062
|
+
async function resolveProject(api, identity, stateProjectId, log = () => {}, hooks = {}) {
|
|
8063
|
+
const name = projectName(identity);
|
|
8064
|
+
const marker = ownershipMarker(identity);
|
|
8065
|
+
if (stateProjectId) {
|
|
8066
|
+
const known = await projectById(api, stateProjectId);
|
|
8067
|
+
if (known) {
|
|
8068
|
+
const claimed = markerOf(known.description);
|
|
8069
|
+
if (claimed && claimed !== marker) throw new ProjectNotOwned(known.name, known.projectId, identity.key, claimed);
|
|
8070
|
+
if (!claimed) {
|
|
8071
|
+
await api.updateProject(known.projectId, {
|
|
8072
|
+
name: known.name,
|
|
8073
|
+
description: withMarker(known.description, marker)
|
|
8074
|
+
});
|
|
8075
|
+
log(` Claimed project ${known.name} for ${identity.key}`);
|
|
8076
|
+
}
|
|
8077
|
+
return {
|
|
8078
|
+
projectId: known.projectId,
|
|
8079
|
+
name: known.name,
|
|
8080
|
+
environments: known.environments ?? [],
|
|
8081
|
+
via: "state"
|
|
8082
|
+
};
|
|
7078
8083
|
}
|
|
7079
|
-
|
|
7080
|
-
|
|
7081
|
-
|
|
7082
|
-
|
|
7083
|
-
const
|
|
7084
|
-
return
|
|
7085
|
-
|
|
7086
|
-
|
|
7087
|
-
|
|
7088
|
-
|
|
7089
|
-
|
|
7090
|
-
|
|
7091
|
-
|
|
7092
|
-
|
|
7093
|
-
|
|
7094
|
-
const
|
|
7095
|
-
|
|
8084
|
+
}
|
|
8085
|
+
const projects = await api.listProjects();
|
|
8086
|
+
const owned = projects.find((p) => p.name.toLowerCase() === name.toLowerCase() && isOwnedBy(p.description, identity));
|
|
8087
|
+
if (owned) {
|
|
8088
|
+
const details = await api.getProject(owned.projectId);
|
|
8089
|
+
return {
|
|
8090
|
+
projectId: owned.projectId,
|
|
8091
|
+
name: owned.name,
|
|
8092
|
+
environments: details.environments ?? [],
|
|
8093
|
+
via: "marker"
|
|
8094
|
+
};
|
|
8095
|
+
}
|
|
8096
|
+
const clash = projects.find((p) => p.name.toLowerCase() === name.toLowerCase());
|
|
8097
|
+
if (clash) throw new ProjectNotOwned(clash.name, clash.projectId, identity.key, markerOf(clash.description));
|
|
8098
|
+
await hooks.beforeCreate?.();
|
|
8099
|
+
const created = await api.createProject(name, withMarker("Deployed by gkm.", marker));
|
|
8100
|
+
return {
|
|
8101
|
+
projectId: created.project.projectId,
|
|
8102
|
+
name: created.project.name,
|
|
8103
|
+
environments: [created.environment],
|
|
8104
|
+
via: "created"
|
|
8105
|
+
};
|
|
8106
|
+
}
|
|
8107
|
+
//#endregion
|
|
8108
|
+
//#region src/deploy/registry.ts
|
|
8109
|
+
/**
|
|
8110
|
+
* Which Dokploy registry a stage's images are pulled through.
|
|
8111
|
+
*
|
|
8112
|
+
* It used to be one id stored per machine (`~/.gkm/credentials.json`), and
|
|
8113
|
+
* when that was empty, the first registry Dokploy listed — so an image could be
|
|
8114
|
+
* deployed with credentials for a registry nobody chose, and two workspaces
|
|
8115
|
+
* deployed from one machine shared whichever was stored last. The id is now
|
|
8116
|
+
* kept in the stage's state, and found only from what the workspace
|
|
8117
|
+
* configured.
|
|
8118
|
+
*/
|
|
8119
|
+
/** There is nowhere to push images to, so Dokploy has nothing to pull. */
|
|
8120
|
+
var RegistryNotConfigured = class extends Error {
|
|
8121
|
+
stage;
|
|
8122
|
+
constructor(stage) {
|
|
8123
|
+
super(`Deploying '${stage}' needs a container registry: Dokploy pulls each app's image from one. Set deploy.dokploy.registry in gkm.config.ts (e.g. 'ghcr.io/acme'), and deploy.dokploy.registryId if Dokploy already holds that registry's credentials.`);
|
|
8124
|
+
this.stage = stage;
|
|
8125
|
+
this.name = "RegistryNotConfigured";
|
|
8126
|
+
}
|
|
8127
|
+
};
|
|
8128
|
+
/** `deploy.dokploy.registryId` names a registry Dokploy does not have. */
|
|
8129
|
+
var RegistryNotFound = class extends Error {
|
|
8130
|
+
registryId;
|
|
8131
|
+
constructor(registryId) {
|
|
8132
|
+
super(`deploy.dokploy.registryId '${registryId}' is not a registry on this Dokploy server. Find the right id under Settings → Docker Registry in Dokploy and set it in gkm.config.ts, or remove it to have deploy find the registry by its URL.`);
|
|
8133
|
+
this.registryId = registryId;
|
|
8134
|
+
this.name = "RegistryNotFound";
|
|
8135
|
+
}
|
|
8136
|
+
};
|
|
8137
|
+
/** More than one Dokploy registry serves the configured registry. */
|
|
8138
|
+
var RegistryAmbiguous = class extends Error {
|
|
8139
|
+
registry;
|
|
8140
|
+
registryIds;
|
|
8141
|
+
constructor(registry, registryIds) {
|
|
8142
|
+
super(`Dokploy has ${registryIds.length} registries for '${registry}' (${registryIds.join(", ")}). Set deploy.dokploy.registryId in gkm.config.ts to the one this workspace pushes with.`);
|
|
8143
|
+
this.registry = registry;
|
|
8144
|
+
this.registryIds = registryIds;
|
|
8145
|
+
this.name = "RegistryAmbiguous";
|
|
8146
|
+
}
|
|
8147
|
+
};
|
|
8148
|
+
/** `https://ghcr.io/acme/` → `{ host: 'ghcr.io', path: 'acme' }`. */
|
|
8149
|
+
function parts(url) {
|
|
8150
|
+
const [host = "", ...path] = url.replace(/^[a-z]+:\/\//i, "").replace(/\/+$/, "").toLowerCase().split("/");
|
|
8151
|
+
return {
|
|
8152
|
+
host,
|
|
8153
|
+
path: path.join("/")
|
|
8154
|
+
};
|
|
8155
|
+
}
|
|
8156
|
+
/**
|
|
8157
|
+
* The Dokploy registries that serve `registry`: the same host, and — where
|
|
8158
|
+
* Dokploy's own entry names one — the same path.
|
|
8159
|
+
*/
|
|
8160
|
+
function serving(registries, registry) {
|
|
8161
|
+
const wanted = parts(registry);
|
|
8162
|
+
const sameHost = registries.filter((r) => parts(r.registryUrl ?? "").host === wanted.host);
|
|
8163
|
+
if (sameHost.length <= 1) return sameHost;
|
|
8164
|
+
return sameHost.filter((r) => {
|
|
8165
|
+
return (parts(r.registryUrl ?? "").path || (r.imagePrefix ?? "").toLowerCase()) === wanted.path;
|
|
8166
|
+
});
|
|
8167
|
+
}
|
|
8168
|
+
/** `registry.one`, or `null` when Dokploy says it has no such registry. */
|
|
8169
|
+
async function registryById(api, registryId) {
|
|
8170
|
+
try {
|
|
8171
|
+
return await api.getRegistry(registryId) ?? null;
|
|
8172
|
+
} catch (error) {
|
|
8173
|
+
if (error instanceof DokployApiError) return null;
|
|
8174
|
+
throw error;
|
|
8175
|
+
}
|
|
8176
|
+
}
|
|
8177
|
+
/**
|
|
8178
|
+
* The registry id for a stage, in order: the configured id, the one in the
|
|
8179
|
+
* stage's state, the one Dokploy has for the configured registry URL, and
|
|
8180
|
+
* otherwise a new one. Never a registry the workspace did not point at.
|
|
8181
|
+
*/
|
|
8182
|
+
async function resolveRegistry(api, options) {
|
|
8183
|
+
const { stage, registry, configuredId, stateId, log = () => {} } = options;
|
|
8184
|
+
if (!registry) throw new RegistryNotConfigured(stage);
|
|
8185
|
+
if (configuredId) {
|
|
8186
|
+
const configured = await registryById(api, configuredId);
|
|
8187
|
+
if (!configured) throw new RegistryNotFound(configuredId);
|
|
8188
|
+
return configured;
|
|
8189
|
+
}
|
|
8190
|
+
if (stateId) {
|
|
8191
|
+
const known = await registryById(api, stateId);
|
|
8192
|
+
if (known) return known;
|
|
8193
|
+
log(` ⚠ The stage's registry ${stateId} no longer exists in Dokploy`);
|
|
8194
|
+
}
|
|
8195
|
+
const matches = serving(await api.listRegistries(), registry);
|
|
8196
|
+
if (matches.length > 1) throw new RegistryAmbiguous(registry, matches.map((r) => r.registryId));
|
|
8197
|
+
if (matches[0]) return matches[0];
|
|
8198
|
+
return options.create(registry);
|
|
7096
8199
|
}
|
|
7097
8200
|
//#endregion
|
|
7098
8201
|
//#region src/deploy/secrets.ts
|
|
@@ -7808,6 +8911,7 @@ async function workspaceDeployCommand(configured, options) {
|
|
|
7808
8911
|
apps: units
|
|
7809
8912
|
} : configured;
|
|
7810
8913
|
if (provider !== "dokploy") throw new Error(`Workspace deployment only supports Dokploy. Got: ${provider}`);
|
|
8914
|
+
const identity = deployIdentity(workspace, stage);
|
|
7811
8915
|
logger$4.log(`\n🚀 Deploying workspace "${workspace.name}" to Dokploy...`);
|
|
7812
8916
|
logger$4.log(` Stage: ${stage}`);
|
|
7813
8917
|
const imageTag = tag ?? generateTag(stage);
|
|
@@ -7834,6 +8938,28 @@ async function workspaceDeployCommand(configured, options) {
|
|
|
7834
8938
|
});
|
|
7835
8939
|
if (dokployApps.length === 0) throw new Error("No apps to deploy. All selected apps have unsupported deploy targets.");
|
|
7836
8940
|
appsToDeployNames = dokployApps;
|
|
8941
|
+
const store = await require_StateStore.createStateStore({
|
|
8942
|
+
config: workspace.state,
|
|
8943
|
+
workspaceRoot: workspace.root,
|
|
8944
|
+
workspaceName: workspace.name
|
|
8945
|
+
});
|
|
8946
|
+
const lock = await store.lock(stage, { operation: "deploy" });
|
|
8947
|
+
try {
|
|
8948
|
+
return await deployLocked({
|
|
8949
|
+
workspace,
|
|
8950
|
+
manifest,
|
|
8951
|
+
stage,
|
|
8952
|
+
imageTag,
|
|
8953
|
+
appsToDeployNames,
|
|
8954
|
+
identity,
|
|
8955
|
+
store
|
|
8956
|
+
});
|
|
8957
|
+
} finally {
|
|
8958
|
+
await lock.release();
|
|
8959
|
+
}
|
|
8960
|
+
}
|
|
8961
|
+
/** Everything a deploy does while it holds the stage's lock. */
|
|
8962
|
+
async function deployLocked({ workspace, manifest, stage, imageTag, appsToDeployNames, identity, store }) {
|
|
7837
8963
|
logger$4.log("\n🔐 Loading secrets and analyzing environment requirements...");
|
|
7838
8964
|
const secretsStore = await require_store.secretsStoreFor(workspace, stage);
|
|
7839
8965
|
const stored = await secretsStore.read(stage);
|
|
@@ -7874,79 +9000,71 @@ async function workspaceDeployCommand(configured, options) {
|
|
|
7874
9000
|
baseUrl: creds.endpoint,
|
|
7875
9001
|
token: creds.token
|
|
7876
9002
|
});
|
|
9003
|
+
logger$4.log("\n📋 Loading deploy state...");
|
|
9004
|
+
const journal = await DeployJournal.open(store, stage, () => createEmptyState(stage, "", ""));
|
|
9005
|
+
const state = journal.state;
|
|
9006
|
+
if (journal.existed) logger$4.log(` Found existing state for stage "${stage}"`);
|
|
9007
|
+
else logger$4.log(` Creating new state for stage "${stage}"`);
|
|
9008
|
+
const unfinished = journal.unfinished();
|
|
9009
|
+
if (unfinished.length > 0) logger$4.log(` ⚠ A previous run stopped while creating: ${unfinished.join(", ")} — looking for them before creating anything`);
|
|
7877
9010
|
logger$4.log("\n📁 Setting up Dokploy project...");
|
|
7878
|
-
|
|
7879
|
-
|
|
7880
|
-
|
|
7881
|
-
|
|
7882
|
-
|
|
7883
|
-
|
|
7884
|
-
|
|
7885
|
-
environmentId = matchingEnv.environmentId;
|
|
7886
|
-
logger$4.log(` Using environment: ${matchingEnv.name}`);
|
|
7887
|
-
} else {
|
|
7888
|
-
logger$4.log(` Creating "${stage}" environment...`);
|
|
7889
|
-
environmentId = (await api.createEnvironment(project.projectId, stage)).environmentId;
|
|
7890
|
-
logger$4.log(` ✓ Created environment: ${stage}`);
|
|
9011
|
+
logger$4.log(` Identity: ${identity.key}`);
|
|
9012
|
+
const projectEntry = {
|
|
9013
|
+
key: "project",
|
|
9014
|
+
type: "project",
|
|
9015
|
+
data: {
|
|
9016
|
+
name: projectName(identity),
|
|
9017
|
+
identity: identity.key
|
|
7891
9018
|
}
|
|
7892
|
-
}
|
|
7893
|
-
|
|
7894
|
-
|
|
7895
|
-
|
|
7896
|
-
|
|
9019
|
+
};
|
|
9020
|
+
const project = await resolveProject(api, identity, state.projectId || void 0, (message) => logger$4.log(message), { beforeCreate: () => journal.pending(projectEntry) });
|
|
9021
|
+
await journal.ready(projectEntry, project.projectId);
|
|
9022
|
+
if (project.via === "created") logger$4.log(` ✓ Created project: ${project.projectId}`);
|
|
9023
|
+
else logger$4.log(` Found existing project: ${project.name}`);
|
|
9024
|
+
const environmentEntry = {
|
|
9025
|
+
key: "environment",
|
|
9026
|
+
type: "environment",
|
|
9027
|
+
data: { name: stage }
|
|
9028
|
+
};
|
|
9029
|
+
const environment = await journal.ensure(environmentEntry, {
|
|
9030
|
+
find: async () => project.environments.find((e) => e.name.toLowerCase() === stage.toLowerCase()),
|
|
9031
|
+
create: () => {
|
|
7897
9032
|
logger$4.log(` Creating "${stage}" environment...`);
|
|
7898
|
-
|
|
7899
|
-
}
|
|
7900
|
-
|
|
7901
|
-
}
|
|
7902
|
-
logger$4.log("\n📋 Loading deploy state...");
|
|
7903
|
-
const stateProvider = await createStateProvider({
|
|
7904
|
-
config: workspace.state,
|
|
7905
|
-
workspaceRoot: workspace.root,
|
|
7906
|
-
workspaceName: workspace.name
|
|
9033
|
+
return api.createEnvironment(project.projectId, stage);
|
|
9034
|
+
},
|
|
9035
|
+
id: (e) => e.environmentId
|
|
7907
9036
|
});
|
|
7908
|
-
|
|
7909
|
-
if (
|
|
7910
|
-
|
|
7911
|
-
|
|
7912
|
-
|
|
7913
|
-
|
|
7914
|
-
|
|
7915
|
-
|
|
7916
|
-
|
|
7917
|
-
|
|
7918
|
-
}
|
|
7919
|
-
} else {
|
|
7920
|
-
logger$4.log(` Creating new state for stage "${stage}"`);
|
|
7921
|
-
state = createEmptyState(stage, project.projectId, environmentId);
|
|
7922
|
-
}
|
|
9037
|
+
const environmentId = environment.resource.environmentId;
|
|
9038
|
+
if (environment.via === "created") logger$4.log(` ✓ Created environment: ${stage}`);
|
|
9039
|
+
else logger$4.log(` Using environment: ${environment.resource.name}`);
|
|
9040
|
+
if (journal.existed) {
|
|
9041
|
+
if (state.projectId && state.projectId !== project.projectId) logger$4.log(` ⚠ Project ID changed, updating state`);
|
|
9042
|
+
if (state.environmentId && state.environmentId !== environmentId) logger$4.log(` ⚠ Environment ID changed, updating state`);
|
|
9043
|
+
}
|
|
9044
|
+
state.projectId = project.projectId;
|
|
9045
|
+
state.environmentId = environmentId;
|
|
9046
|
+
state.identity = identity.key;
|
|
7923
9047
|
logger$4.log("\n🐳 Checking registry...");
|
|
7924
|
-
let registryId = await getDokployRegistryId();
|
|
7925
9048
|
const registry = workspace.deploy.dokploy?.registry;
|
|
7926
|
-
|
|
7927
|
-
|
|
7928
|
-
|
|
7929
|
-
|
|
7930
|
-
|
|
7931
|
-
|
|
7932
|
-
|
|
7933
|
-
|
|
7934
|
-
if (!registryId) {
|
|
7935
|
-
const registries = await api.listRegistries();
|
|
7936
|
-
if (registries.length > 0) {
|
|
7937
|
-
registryId = registries[0].registryId;
|
|
7938
|
-
await storeDokployRegistryId(registryId);
|
|
7939
|
-
logger$4.log(` Using registry: ${registries[0].registryName}`);
|
|
7940
|
-
} else if (registry) {
|
|
7941
|
-
logger$4.log(" No registries found in Dokploy. Let's create one.");
|
|
7942
|
-
logger$4.log(` Registry URL: ${registry}`);
|
|
9049
|
+
const dokployRegistry = await resolveRegistry(api, {
|
|
9050
|
+
stage,
|
|
9051
|
+
registry,
|
|
9052
|
+
configuredId: workspace.deploy.dokploy?.registryId,
|
|
9053
|
+
stateId: state.registryId,
|
|
9054
|
+
log: (message) => logger$4.log(message),
|
|
9055
|
+
create: async (url) => {
|
|
9056
|
+
logger$4.log(` Dokploy has no registry for ${url}. Let's create one.`);
|
|
7943
9057
|
const username = await prompt("Registry username: ");
|
|
7944
9058
|
const password = await prompt("Registry password/token: ", true);
|
|
7945
|
-
|
|
7946
|
-
|
|
7947
|
-
|
|
7948
|
-
}
|
|
7949
|
-
}
|
|
9059
|
+
const created = await api.createRegistry("Default Registry", url, username, password);
|
|
9060
|
+
logger$4.log(` ✓ Registry created: ${created.registryId}`);
|
|
9061
|
+
return created;
|
|
9062
|
+
}
|
|
9063
|
+
});
|
|
9064
|
+
const registryId = dokployRegistry.registryId;
|
|
9065
|
+
state.registryId = registryId;
|
|
9066
|
+
logger$4.log(` Using registry: ${dokployRegistry.registryName}`);
|
|
9067
|
+
await journal.save();
|
|
7950
9068
|
const skippedMobileApps = appsToDeployNames.filter((name) => workspace.apps[name].type === "mobile");
|
|
7951
9069
|
if (skippedMobileApps.length > 0) logger$4.log(`\n📱 Skipping ${skippedMobileApps.length} mobile app(s) — deploy via framework toolchain: ${skippedMobileApps.join(", ")}`);
|
|
7952
9070
|
const backendApps = appsToDeployNames.filter((name) => workspace.apps[name].type === "backend");
|
|
@@ -7967,6 +9085,7 @@ async function workspaceDeployCommand(configured, options) {
|
|
|
7967
9085
|
projectId: project.projectId,
|
|
7968
9086
|
environmentId,
|
|
7969
9087
|
stage,
|
|
9088
|
+
scope: identity.scope,
|
|
7970
9089
|
appUrls,
|
|
7971
9090
|
supplied: stageSecrets.custom,
|
|
7972
9091
|
seed: stageSecrets.seed,
|
|
@@ -7992,7 +9111,7 @@ async function workspaceDeployCommand(configured, options) {
|
|
|
7992
9111
|
const backupState = await provisionBackupDestination({
|
|
7993
9112
|
api,
|
|
7994
9113
|
projectId: project.projectId,
|
|
7995
|
-
projectName:
|
|
9114
|
+
projectName: identity.scope,
|
|
7996
9115
|
stage,
|
|
7997
9116
|
config: workspace.deploy.backups,
|
|
7998
9117
|
existingState: getBackupState(state),
|
|
@@ -8003,7 +9122,7 @@ async function workspaceDeployCommand(configured, options) {
|
|
|
8003
9122
|
const backupSchedule = workspace.deploy.backups.schedule ?? "0 2 * * *";
|
|
8004
9123
|
const backupRetention = workspace.deploy.backups.retention ?? 30;
|
|
8005
9124
|
logger$4.log(" Creating postgres backup schedule...");
|
|
8006
|
-
|
|
9125
|
+
setPostgresBackupId(state, (await api.createPostgresBackup({
|
|
8007
9126
|
schedule: backupSchedule,
|
|
8008
9127
|
prefix: `${stage}/postgres`,
|
|
8009
9128
|
destinationId: backupState.destinationId,
|
|
@@ -8011,8 +9130,7 @@ async function workspaceDeployCommand(configured, options) {
|
|
|
8011
9130
|
postgresId: backupCluster.postgresId,
|
|
8012
9131
|
enabled: true,
|
|
8013
9132
|
keepLatestCount: backupRetention
|
|
8014
|
-
});
|
|
8015
|
-
setPostgresBackupId(state, backup.backupId);
|
|
9133
|
+
})).backupId);
|
|
8016
9134
|
logger$4.log(` ✓ Postgres backup schedule created (${backupSchedule})`);
|
|
8017
9135
|
} else logger$4.log(" ✓ Using existing postgres backup schedule");
|
|
8018
9136
|
}
|
|
@@ -8033,42 +9151,31 @@ async function workspaceDeployCommand(configured, options) {
|
|
|
8033
9151
|
const app = workspace.apps[appName];
|
|
8034
9152
|
logger$4.log(`\n ⚙️ Deploying ${appName}...`);
|
|
8035
9153
|
try {
|
|
8036
|
-
const
|
|
8037
|
-
let application = null;
|
|
8038
|
-
const cachedAppId = getApplicationId(state, appName);
|
|
8039
|
-
if (cachedAppId) {
|
|
8040
|
-
logger$4.log(` Using cached ID: ${cachedAppId}`);
|
|
8041
|
-
application = await api.getApplication(cachedAppId);
|
|
8042
|
-
if (application) logger$4.log(` ✓ Application found: ${application.applicationId}`);
|
|
8043
|
-
else logger$4.log(` ⚠ Cached ID invalid, will create new`);
|
|
8044
|
-
}
|
|
8045
|
-
if (!application) {
|
|
8046
|
-
const result = await api.findOrCreateApplication(dokployAppName, project.projectId, environmentId);
|
|
8047
|
-
application = result.application;
|
|
8048
|
-
if (result.created) logger$4.log(` Created application: ${application.applicationId}`);
|
|
8049
|
-
else logger$4.log(` Found existing application: ${application.applicationId}`);
|
|
8050
|
-
}
|
|
8051
|
-
setApplicationId(state, appName, application.applicationId);
|
|
9154
|
+
const application = await ensureApplication(api, journal, appName, applicationName(identity, appName), project.projectId, environmentId);
|
|
8052
9155
|
const appSecrets = encryptedSecrets.get(appName);
|
|
8053
9156
|
const credentials = appSecrets && appSecrets.secretCount > 0 ? {
|
|
8054
9157
|
encrypted: appSecrets.payload.encrypted,
|
|
8055
9158
|
iv: appSecrets.payload.iv
|
|
8056
9159
|
} : void 0;
|
|
8057
9160
|
if (appSecrets && credentials) logger$4.log(` Encrypted ${appSecrets.secretCount} secrets`);
|
|
8058
|
-
const
|
|
8059
|
-
|
|
8060
|
-
|
|
8061
|
-
await deployDocker({
|
|
9161
|
+
const imageRef$1 = imageRef(identity, appName, registry, imageTag);
|
|
9162
|
+
logger$4.log(` Building Docker image: ${imageRef$1}`);
|
|
9163
|
+
const built = await deployDocker({
|
|
8062
9164
|
stage,
|
|
8063
9165
|
tag: imageTag,
|
|
8064
9166
|
skipPush: false,
|
|
8065
9167
|
config: {
|
|
8066
9168
|
registry,
|
|
8067
|
-
imageName,
|
|
9169
|
+
imageName: imageName(identity, appName),
|
|
8068
9170
|
appName
|
|
8069
9171
|
},
|
|
8070
9172
|
credentials
|
|
8071
9173
|
});
|
|
9174
|
+
setDeployedImage(state, appName, {
|
|
9175
|
+
ref: built.imageRef ?? imageRef$1,
|
|
9176
|
+
...built.digest ? { digest: built.digest } : {}
|
|
9177
|
+
});
|
|
9178
|
+
await journal.save();
|
|
8072
9179
|
const backendHost = resolveHost(appName, app, stage, workspace.deploy?.domains, false);
|
|
8073
9180
|
const dependencyUrls = {};
|
|
8074
9181
|
if (app.dependencies) {
|
|
@@ -8099,40 +9206,21 @@ async function workspaceDeployCommand(configured, options) {
|
|
|
8099
9206
|
};
|
|
8100
9207
|
const envVars = Object.entries(withDeclared).map(([key, value]) => `${key}=${value}`);
|
|
8101
9208
|
if (Object.keys(withDeclared).length > 0) logger$4.log(` Resolved ${Object.keys(withDeclared).length} env vars: ${Object.keys(withDeclared).sort().join(", ")}`);
|
|
8102
|
-
await api.saveDockerProvider(application.applicationId, imageRef, { registryId });
|
|
9209
|
+
await api.saveDockerProvider(application.applicationId, imageRef$1, { registryId });
|
|
8103
9210
|
await api.saveApplicationEnv(application.applicationId, envVars.join("\n"));
|
|
8104
9211
|
logger$4.log(` Deploying to Dokploy...`);
|
|
8105
9212
|
await api.deployApplication(application.applicationId);
|
|
8106
|
-
const
|
|
8107
|
-
|
|
8108
|
-
|
|
8109
|
-
|
|
8110
|
-
publicUrls[appName] = `https://${backendHost}`;
|
|
8111
|
-
logger$4.log(` ✓ Domain: https://${backendHost} (existing)`);
|
|
8112
|
-
} else try {
|
|
8113
|
-
const domain = await api.createDomain({
|
|
8114
|
-
host: backendHost,
|
|
8115
|
-
port: app.port,
|
|
8116
|
-
https: true,
|
|
8117
|
-
certificateType: "letsencrypt",
|
|
8118
|
-
applicationId: application.applicationId
|
|
8119
|
-
});
|
|
8120
|
-
appHostnames.set(appName, backendHost);
|
|
8121
|
-
appDomainIds.set(appName, domain.domainId);
|
|
8122
|
-
publicUrls[appName] = `https://${backendHost}`;
|
|
8123
|
-
logger$4.log(` ✓ Domain: https://${backendHost} (created)`);
|
|
8124
|
-
} catch (domainError) {
|
|
8125
|
-
const message = domainError instanceof Error ? domainError.message : "Unknown error";
|
|
8126
|
-
logger$4.log(` ⚠ Domain creation failed: ${message}`);
|
|
8127
|
-
appHostnames.set(appName, backendHost);
|
|
8128
|
-
publicUrls[appName] = `https://${backendHost}`;
|
|
8129
|
-
}
|
|
9213
|
+
const domainId = await ensureDomain(api, journal, backendHost, app.port, application.applicationId);
|
|
9214
|
+
appHostnames.set(appName, backendHost);
|
|
9215
|
+
if (domainId) appDomainIds.set(appName, domainId);
|
|
9216
|
+
publicUrls[appName] = `https://${backendHost}`;
|
|
8130
9217
|
results.push({
|
|
8131
9218
|
appName,
|
|
8132
9219
|
type: app.type,
|
|
8133
9220
|
success: true,
|
|
8134
9221
|
applicationId: application.applicationId,
|
|
8135
|
-
imageRef
|
|
9222
|
+
imageRef: imageRef$1,
|
|
9223
|
+
...built.digest ? { digest: built.digest } : {}
|
|
8136
9224
|
});
|
|
8137
9225
|
logger$4.log(` ✓ ${appName} deployed successfully`);
|
|
8138
9226
|
} catch (error) {
|
|
@@ -8154,22 +9242,7 @@ async function workspaceDeployCommand(configured, options) {
|
|
|
8154
9242
|
const app = workspace.apps[appName];
|
|
8155
9243
|
logger$4.log(`\n 🌐 Deploying ${appName}...`);
|
|
8156
9244
|
try {
|
|
8157
|
-
const
|
|
8158
|
-
let application = null;
|
|
8159
|
-
const cachedAppId = getApplicationId(state, appName);
|
|
8160
|
-
if (cachedAppId) {
|
|
8161
|
-
logger$4.log(` Using cached ID: ${cachedAppId}`);
|
|
8162
|
-
application = await api.getApplication(cachedAppId);
|
|
8163
|
-
if (application) logger$4.log(` ✓ Application found: ${application.applicationId}`);
|
|
8164
|
-
else logger$4.log(` ⚠ Cached ID invalid, will create new`);
|
|
8165
|
-
}
|
|
8166
|
-
if (!application) {
|
|
8167
|
-
const result = await api.findOrCreateApplication(dokployAppName, project.projectId, environmentId);
|
|
8168
|
-
application = result.application;
|
|
8169
|
-
if (result.created) logger$4.log(` Created application: ${application.applicationId}`);
|
|
8170
|
-
else logger$4.log(` Found existing application: ${application.applicationId}`);
|
|
8171
|
-
}
|
|
8172
|
-
setApplicationId(state, appName, application.applicationId);
|
|
9245
|
+
const application = await ensureApplication(api, journal, appName, applicationName(identity, appName), project.projectId, environmentId);
|
|
8173
9246
|
const dependencyUrls = {};
|
|
8174
9247
|
if (app.dependencies) {
|
|
8175
9248
|
for (const dep of app.dependencies) if (publicUrls[dep]) dependencyUrls[dep] = publicUrls[dep];
|
|
@@ -8199,61 +9272,46 @@ async function workspaceDeployCommand(configured, options) {
|
|
|
8199
9272
|
}
|
|
8200
9273
|
}
|
|
8201
9274
|
if (buildArgs.length > 0) logger$4.log(` Build args: ${publicUrlArgNames.join(", ")}`);
|
|
8202
|
-
const
|
|
8203
|
-
|
|
8204
|
-
|
|
8205
|
-
await deployDocker({
|
|
9275
|
+
const imageRef$2 = imageRef(identity, appName, registry, imageTag);
|
|
9276
|
+
logger$4.log(` Building Docker image: ${imageRef$2}`);
|
|
9277
|
+
const built = await deployDocker({
|
|
8206
9278
|
stage,
|
|
8207
9279
|
tag: imageTag,
|
|
8208
9280
|
skipPush: false,
|
|
8209
9281
|
config: {
|
|
8210
9282
|
registry,
|
|
8211
|
-
imageName,
|
|
9283
|
+
imageName: imageName(identity, appName),
|
|
8212
9284
|
appName
|
|
8213
9285
|
},
|
|
8214
9286
|
buildArgs,
|
|
8215
9287
|
publicUrlArgs: publicUrlArgNames
|
|
8216
9288
|
});
|
|
9289
|
+
setDeployedImage(state, appName, {
|
|
9290
|
+
ref: built.imageRef ?? imageRef$2,
|
|
9291
|
+
...built.digest ? { digest: built.digest } : {}
|
|
9292
|
+
});
|
|
9293
|
+
await journal.save();
|
|
8217
9294
|
const envVars = [
|
|
8218
9295
|
`NODE_ENV=production`,
|
|
8219
9296
|
`PORT=${app.port}`,
|
|
8220
9297
|
`STAGE=${stage}`
|
|
8221
9298
|
];
|
|
8222
9299
|
for (const [key, value] of Object.entries(resolved)) envVars.push(`${key}=${value}`);
|
|
8223
|
-
await api.saveDockerProvider(application.applicationId, imageRef, { registryId });
|
|
9300
|
+
await api.saveDockerProvider(application.applicationId, imageRef$2, { registryId });
|
|
8224
9301
|
await api.saveApplicationEnv(application.applicationId, envVars.join("\n"));
|
|
8225
9302
|
logger$4.log(` Deploying to Dokploy...`);
|
|
8226
9303
|
await api.deployApplication(application.applicationId);
|
|
8227
|
-
const
|
|
8228
|
-
|
|
8229
|
-
|
|
8230
|
-
|
|
8231
|
-
publicUrls[appName] = `https://${frontendHost}`;
|
|
8232
|
-
logger$4.log(` ✓ Domain: https://${frontendHost} (existing)`);
|
|
8233
|
-
} else try {
|
|
8234
|
-
const domain = await api.createDomain({
|
|
8235
|
-
host: frontendHost,
|
|
8236
|
-
port: app.port,
|
|
8237
|
-
https: true,
|
|
8238
|
-
certificateType: "letsencrypt",
|
|
8239
|
-
applicationId: application.applicationId
|
|
8240
|
-
});
|
|
8241
|
-
appHostnames.set(appName, frontendHost);
|
|
8242
|
-
appDomainIds.set(appName, domain.domainId);
|
|
8243
|
-
publicUrls[appName] = `https://${frontendHost}`;
|
|
8244
|
-
logger$4.log(` ✓ Domain: https://${frontendHost} (created)`);
|
|
8245
|
-
} catch (domainError) {
|
|
8246
|
-
const message = domainError instanceof Error ? domainError.message : "Unknown error";
|
|
8247
|
-
logger$4.log(` ⚠ Domain creation failed: ${message}`);
|
|
8248
|
-
appHostnames.set(appName, frontendHost);
|
|
8249
|
-
publicUrls[appName] = `https://${frontendHost}`;
|
|
8250
|
-
}
|
|
9304
|
+
const domainId = await ensureDomain(api, journal, frontendHost, app.port, application.applicationId);
|
|
9305
|
+
appHostnames.set(appName, frontendHost);
|
|
9306
|
+
if (domainId) appDomainIds.set(appName, domainId);
|
|
9307
|
+
publicUrls[appName] = `https://${frontendHost}`;
|
|
8251
9308
|
results.push({
|
|
8252
9309
|
appName,
|
|
8253
9310
|
type: app.type,
|
|
8254
9311
|
success: true,
|
|
8255
9312
|
applicationId: application.applicationId,
|
|
8256
|
-
imageRef
|
|
9313
|
+
imageRef: imageRef$2,
|
|
9314
|
+
...built.digest ? { digest: built.digest } : {}
|
|
8257
9315
|
});
|
|
8258
9316
|
logger$4.log(` ✓ ${appName} deployed successfully`);
|
|
8259
9317
|
} catch (error) {
|
|
@@ -8269,14 +9327,14 @@ async function workspaceDeployCommand(configured, options) {
|
|
|
8269
9327
|
}
|
|
8270
9328
|
}
|
|
8271
9329
|
logger$4.log("\n📋 Saving deploy state...");
|
|
8272
|
-
await
|
|
9330
|
+
await journal.save();
|
|
8273
9331
|
logger$4.log(" ✓ State saved");
|
|
8274
9332
|
const dnsConfig = workspace.deploy.dns;
|
|
8275
9333
|
if (dnsConfig && appHostnames.size > 0) {
|
|
8276
9334
|
const dnsResult = await orchestrateDns(appHostnames, dnsConfig, creds.endpoint);
|
|
8277
9335
|
if (dnsResult?.serverIp && appHostnames.size > 0) {
|
|
8278
9336
|
await verifyDnsRecords(appHostnames, dnsResult.serverIp, state);
|
|
8279
|
-
await
|
|
9337
|
+
await journal.save();
|
|
8280
9338
|
}
|
|
8281
9339
|
if (dnsResult?.success && appHostnames.size > 0) {
|
|
8282
9340
|
logger$4.log("\n🔒 Validating domains for SSL certificates...");
|
|
@@ -8309,6 +9367,71 @@ async function workspaceDeployCommand(configured, options) {
|
|
|
8309
9367
|
};
|
|
8310
9368
|
}
|
|
8311
9369
|
/**
|
|
9370
|
+
* An app's Dokploy application, through the journal: by the id recorded for
|
|
9371
|
+
* it, else by its name in the stage's environment, else created. Its id goes
|
|
9372
|
+
* into `state.applications` too, which the env resolver and `state:show` read.
|
|
9373
|
+
*/
|
|
9374
|
+
async function ensureApplication(api, journal, appName, dokployAppName, projectId, environmentId) {
|
|
9375
|
+
const recorded = journal.record(`application:${appName}`);
|
|
9376
|
+
if (recorded?.status === "ready" && recorded.id) logger$4.log(` Using cached ID: ${recorded.id}`);
|
|
9377
|
+
const { resource, via, staleId } = await journal.ensure({
|
|
9378
|
+
key: `application:${appName}`,
|
|
9379
|
+
type: "application",
|
|
9380
|
+
data: { name: dokployAppName }
|
|
9381
|
+
}, {
|
|
9382
|
+
get: (id) => api.getApplication(id),
|
|
9383
|
+
find: () => api.findApplicationByName(projectId, dokployAppName, environmentId),
|
|
9384
|
+
create: () => api.createApplication(dokployAppName, projectId, environmentId),
|
|
9385
|
+
id: (application) => application.applicationId
|
|
9386
|
+
});
|
|
9387
|
+
if (staleId) logger$4.log(` ⚠ Cached ID invalid, will create new`);
|
|
9388
|
+
logger$4.log(` ${{
|
|
9389
|
+
recorded: "✓ Application found",
|
|
9390
|
+
resumed: "Resumed application a stopped run created",
|
|
9391
|
+
found: "Found existing application",
|
|
9392
|
+
created: "Created application"
|
|
9393
|
+
}[via]}: ${resource.applicationId}`);
|
|
9394
|
+
setApplicationId(journal.state, appName, resource.applicationId);
|
|
9395
|
+
return resource;
|
|
9396
|
+
}
|
|
9397
|
+
/**
|
|
9398
|
+
* The domain `host` routes to an application, through the journal — found on
|
|
9399
|
+
* the application before it is created, so a re-run never adds a second.
|
|
9400
|
+
*
|
|
9401
|
+
* A domain that cannot be created is reported and skipped rather than
|
|
9402
|
+
* failing the app: the application is deployed and reachable on Dokploy's
|
|
9403
|
+
* own address, and DNS is fixed by hand more easily than a half deploy.
|
|
9404
|
+
*/
|
|
9405
|
+
async function ensureDomain(api, journal, host, port, applicationId) {
|
|
9406
|
+
try {
|
|
9407
|
+
const { resource, via } = await journal.ensure({
|
|
9408
|
+
key: `domain:${host}`,
|
|
9409
|
+
type: "domain",
|
|
9410
|
+
data: {
|
|
9411
|
+
host,
|
|
9412
|
+
applicationId
|
|
9413
|
+
}
|
|
9414
|
+
}, {
|
|
9415
|
+
find: async () => (await api.getDomainsByApplicationId(applicationId)).find((d) => d.host === host),
|
|
9416
|
+
create: () => api.createDomain({
|
|
9417
|
+
host,
|
|
9418
|
+
port,
|
|
9419
|
+
https: true,
|
|
9420
|
+
certificateType: "letsencrypt",
|
|
9421
|
+
applicationId
|
|
9422
|
+
}),
|
|
9423
|
+
id: (domain) => domain.domainId
|
|
9424
|
+
});
|
|
9425
|
+
logger$4.log(` ✓ Domain: https://${host} (${via === "created" ? "created" : "existing"})`);
|
|
9426
|
+
return resource.domainId;
|
|
9427
|
+
} catch (domainError) {
|
|
9428
|
+
if (domainError instanceof require_StateStore.StateVersionConflict || domainError instanceof require_StateStore.StateStoreBusy) throw domainError;
|
|
9429
|
+
const message = domainError instanceof Error ? domainError.message : "Unknown error";
|
|
9430
|
+
logger$4.log(` ⚠ Domain creation failed: ${message}`);
|
|
9431
|
+
return;
|
|
9432
|
+
}
|
|
9433
|
+
}
|
|
9434
|
+
/**
|
|
8312
9435
|
* Main deploy command
|
|
8313
9436
|
*/
|
|
8314
9437
|
async function deployCommand(options) {
|
|
@@ -8445,453 +9568,160 @@ async function deployListCommand(options) {
|
|
|
8445
9568
|
}
|
|
8446
9569
|
}
|
|
8447
9570
|
//#endregion
|
|
8448
|
-
//#region src/deploy/
|
|
8449
|
-
var CachedStateProvider_exports = /* @__PURE__ */ require_rolldown_runtime.__exportAll({ CachedStateProvider: () => CachedStateProvider });
|
|
8450
|
-
/**
|
|
8451
|
-
* Cached state provider that wraps a remote provider with local cache.
|
|
8452
|
-
*
|
|
8453
|
-
* @deprecated for deploys: a read prefers the local copy, so a stale one wins
|
|
8454
|
-
* over the remote without warning, and a write overwrites the remote blindly.
|
|
8455
|
-
* Deploy uses `createStateStore`, which reads SSM directly and writes
|
|
8456
|
-
* conditionally. Kept for `gkm state:pull | push | diff`.
|
|
8457
|
-
*/
|
|
8458
|
-
var CachedStateProvider = class {
|
|
8459
|
-
remote;
|
|
8460
|
-
local;
|
|
8461
|
-
constructor(remote, local) {
|
|
8462
|
-
this.remote = remote;
|
|
8463
|
-
this.local = local;
|
|
8464
|
-
}
|
|
8465
|
-
async read(stage) {
|
|
8466
|
-
const localState = await this.local.read(stage);
|
|
8467
|
-
if (localState) return localState;
|
|
8468
|
-
const remoteState = await this.remote.read(stage);
|
|
8469
|
-
if (remoteState) await this.local.write(stage, remoteState);
|
|
8470
|
-
return remoteState;
|
|
8471
|
-
}
|
|
8472
|
-
async write(stage, state) {
|
|
8473
|
-
await this.remote.write(stage, state);
|
|
8474
|
-
await this.local.write(stage, state);
|
|
8475
|
-
}
|
|
8476
|
-
/**
|
|
8477
|
-
* Force pull from remote to local.
|
|
8478
|
-
* Used by `gkm state pull` command.
|
|
8479
|
-
*/
|
|
8480
|
-
async pull(stage) {
|
|
8481
|
-
const remoteState = await this.remote.read(stage);
|
|
8482
|
-
if (remoteState) await this.local.write(stage, remoteState);
|
|
8483
|
-
return remoteState;
|
|
8484
|
-
}
|
|
8485
|
-
/**
|
|
8486
|
-
* Force push from local to remote.
|
|
8487
|
-
* Used by `gkm state push` command.
|
|
8488
|
-
*/
|
|
8489
|
-
async push(stage) {
|
|
8490
|
-
const localState = await this.local.read(stage);
|
|
8491
|
-
if (localState) await this.remote.write(stage, localState);
|
|
8492
|
-
return localState;
|
|
8493
|
-
}
|
|
8494
|
-
/**
|
|
8495
|
-
* Get both local and remote state for comparison.
|
|
8496
|
-
* Used by `gkm state diff` command.
|
|
8497
|
-
*/
|
|
8498
|
-
async diff(stage) {
|
|
8499
|
-
const [local, remote] = await Promise.all([this.local.read(stage), this.remote.read(stage)]);
|
|
8500
|
-
return {
|
|
8501
|
-
local,
|
|
8502
|
-
remote
|
|
8503
|
-
};
|
|
8504
|
-
}
|
|
8505
|
-
};
|
|
8506
|
-
//#endregion
|
|
8507
|
-
//#region src/deploy/StateStore.ts
|
|
9571
|
+
//#region src/deploy/LocalStateStore.ts
|
|
8508
9572
|
/**
|
|
8509
|
-
*
|
|
8510
|
-
*
|
|
8511
|
-
* `StateProvider` reads and writes a whole stage at once, with nothing to stop
|
|
8512
|
-
* two runs of the same stage from overwriting each other and nowhere to record
|
|
8513
|
-
* a resource the moment it exists. A `StateStore` adds the three things a
|
|
8514
|
-
* deploy needs to be safe against crashes and concurrent runs:
|
|
9573
|
+
* Local filesystem state store.
|
|
8515
9574
|
*
|
|
8516
|
-
*
|
|
8517
|
-
*
|
|
8518
|
-
|
|
8519
|
-
|
|
8520
|
-
|
|
8521
|
-
|
|
8522
|
-
|
|
8523
|
-
*
|
|
8524
|
-
* Built-in stores: `LocalStateStore`, `SSMStateStore`, `S3StateStore`. A
|
|
8525
|
-
* custom `StateProvider` is wrapped by `LegacyStateStore`, which keeps it
|
|
8526
|
-
* working but cannot lock.
|
|
8527
|
-
*/
|
|
8528
|
-
var StateStore_exports = /* @__PURE__ */ require_rolldown_runtime.__exportAll({
|
|
8529
|
-
DocumentStateStore: () => DocumentStateStore,
|
|
8530
|
-
STATE_SCHEMA_VERSION: () => 2,
|
|
8531
|
-
StageStateMissing: () => StageStateMissing,
|
|
8532
|
-
StateLocked: () => StateLocked,
|
|
8533
|
-
StateSchemaTooNew: () => StateSchemaTooNew,
|
|
8534
|
-
StateStoreBusy: () => StateStoreBusy,
|
|
8535
|
-
StateStoreNeedsWorkspaceName: () => StateStoreNeedsWorkspaceName,
|
|
8536
|
-
StateStoreProvider: () => StateStoreProvider,
|
|
8537
|
-
StateStoreWithoutLocking: () => StateStoreWithoutLocking,
|
|
8538
|
-
StateUnreadable: () => StateUnreadable,
|
|
8539
|
-
StateVersionConflict: () => StateVersionConflict,
|
|
8540
|
-
UnknownStateProvider: () => UnknownStateProvider,
|
|
8541
|
-
contentVersion: () => contentVersion,
|
|
8542
|
-
createStateStore: () => createStateStore,
|
|
8543
|
-
decodeDocument: () => decodeDocument,
|
|
8544
|
-
encodeDocument: () => encodeDocument,
|
|
8545
|
-
isStateStore: () => isStateStore,
|
|
8546
|
-
migrateV1: () => migrateV1,
|
|
8547
|
-
newLockHolder: () => newLockHolder
|
|
8548
|
-
});
|
|
8549
|
-
var StateLocked = class extends Error {
|
|
8550
|
-
stage;
|
|
8551
|
-
holder;
|
|
8552
|
-
location;
|
|
8553
|
-
constructor(stage, holder, location) {
|
|
8554
|
-
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";
|
|
8555
|
-
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}\`.`);
|
|
8556
|
-
this.stage = stage;
|
|
8557
|
-
this.holder = holder;
|
|
8558
|
-
this.location = location;
|
|
8559
|
-
this.name = "StateLocked";
|
|
8560
|
-
}
|
|
8561
|
-
};
|
|
8562
|
-
var StateVersionConflict = class extends Error {
|
|
8563
|
-
stage;
|
|
8564
|
-
expectedVersion;
|
|
8565
|
-
actualVersion;
|
|
8566
|
-
constructor(stage, expectedVersion, actualVersion) {
|
|
8567
|
-
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.`);
|
|
8568
|
-
this.stage = stage;
|
|
8569
|
-
this.expectedVersion = expectedVersion;
|
|
8570
|
-
this.actualVersion = actualVersion;
|
|
8571
|
-
this.name = "StateVersionConflict";
|
|
8572
|
-
}
|
|
8573
|
-
};
|
|
8574
|
-
var StageStateMissing = class extends Error {
|
|
8575
|
-
stage;
|
|
8576
|
-
constructor(stage) {
|
|
8577
|
-
super(`There is no deploy state for stage '${stage}' to record a resource in. Write the stage's state first (write with expectedVersion: null).`);
|
|
8578
|
-
this.stage = stage;
|
|
8579
|
-
this.name = "StageStateMissing";
|
|
8580
|
-
}
|
|
8581
|
-
};
|
|
8582
|
-
var StateUnreadable = class extends Error {
|
|
8583
|
-
stage;
|
|
8584
|
-
location;
|
|
8585
|
-
constructor(stage, location, cause) {
|
|
8586
|
-
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 });
|
|
8587
|
-
this.stage = stage;
|
|
8588
|
-
this.location = location;
|
|
8589
|
-
this.name = "StateUnreadable";
|
|
8590
|
-
}
|
|
8591
|
-
};
|
|
8592
|
-
var StateSchemaTooNew = class extends Error {
|
|
8593
|
-
stage;
|
|
8594
|
-
schemaVersion;
|
|
8595
|
-
constructor(stage, schemaVersion) {
|
|
8596
|
-
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.`);
|
|
8597
|
-
this.stage = stage;
|
|
8598
|
-
this.schemaVersion = schemaVersion;
|
|
8599
|
-
this.name = "StateSchemaTooNew";
|
|
8600
|
-
}
|
|
8601
|
-
};
|
|
9575
|
+
* `.gkm/deploy-<stage>.json` holds the state, `.gkm/deploy-<stage>.lock` the
|
|
9576
|
+
* lock. Every file is mode 0600: state holds database passwords and IAM keys.
|
|
9577
|
+
*/
|
|
9578
|
+
var LocalStateStore_exports = /* @__PURE__ */ require_rolldown_runtime.__exportAll({ LocalStateStore: () => LocalStateStore });
|
|
9579
|
+
const FILE_MODE = 384;
|
|
9580
|
+
/** How long a write waits for another process's write to finish. */
|
|
9581
|
+
const MUTEX_TIMEOUT_MS = 5e3;
|
|
8602
9582
|
/**
|
|
8603
|
-
*
|
|
8604
|
-
*
|
|
8605
|
-
* says so.
|
|
9583
|
+
* A write holds the mutex for milliseconds, so one this old was left by a
|
|
9584
|
+
* process that died mid-write and is safe to take over.
|
|
8606
9585
|
*/
|
|
8607
|
-
|
|
8608
|
-
|
|
8609
|
-
|
|
8610
|
-
|
|
8611
|
-
|
|
8612
|
-
|
|
8613
|
-
|
|
8614
|
-
|
|
8615
|
-
|
|
8616
|
-
provider;
|
|
8617
|
-
constructor(provider) {
|
|
8618
|
-
super(`The '${provider}' state provider keys state by workspace name. Set "name" in gkm.config.ts.`);
|
|
8619
|
-
this.provider = provider;
|
|
8620
|
-
this.name = "StateStoreNeedsWorkspaceName";
|
|
9586
|
+
const MUTEX_STALE_MS = 3e4;
|
|
9587
|
+
function isCode(error, code) {
|
|
9588
|
+
return error?.code === code;
|
|
9589
|
+
}
|
|
9590
|
+
var LocalStateStore = class extends require_StateStore.DocumentStateStore {
|
|
9591
|
+
workspaceRoot;
|
|
9592
|
+
constructor(workspaceRoot) {
|
|
9593
|
+
super();
|
|
9594
|
+
this.workspaceRoot = workspaceRoot;
|
|
8621
9595
|
}
|
|
8622
|
-
|
|
8623
|
-
|
|
8624
|
-
config;
|
|
8625
|
-
constructor(config) {
|
|
8626
|
-
super(`Unknown state provider ${JSON.stringify(config)}. Use 'local', 'ssm', 's3', or an object implementing StateStore or StateProvider.`);
|
|
8627
|
-
this.config = config;
|
|
8628
|
-
this.name = "UnknownStateProvider";
|
|
9596
|
+
get dir() {
|
|
9597
|
+
return (0, node_path.join)(this.workspaceRoot, ".gkm");
|
|
8629
9598
|
}
|
|
8630
|
-
|
|
8631
|
-
|
|
8632
|
-
var StateStoreBusy = class extends Error {
|
|
8633
|
-
stage;
|
|
8634
|
-
location;
|
|
8635
|
-
constructor(stage, location) {
|
|
8636
|
-
super(`Timed out waiting to write deploy state for stage '${stage}' (${location}). If no other gkm process is running, remove ${location}.`);
|
|
8637
|
-
this.stage = stage;
|
|
8638
|
-
this.location = location;
|
|
8639
|
-
this.name = "StateStoreBusy";
|
|
9599
|
+
statePath(stage) {
|
|
9600
|
+
return (0, node_path.join)(this.dir, `deploy-${stage}.json`);
|
|
8640
9601
|
}
|
|
8641
|
-
|
|
8642
|
-
|
|
8643
|
-
let parsed;
|
|
8644
|
-
try {
|
|
8645
|
-
parsed = JSON.parse(body);
|
|
8646
|
-
} catch (error) {
|
|
8647
|
-
throw new StateUnreadable(stage, location, error);
|
|
9602
|
+
lockPath(stage) {
|
|
9603
|
+
return (0, node_path.join)(this.dir, `deploy-${stage}.lock`);
|
|
8648
9604
|
}
|
|
8649
|
-
|
|
8650
|
-
|
|
8651
|
-
if (schemaVersion === void 0) return {
|
|
8652
|
-
schemaVersion: 1,
|
|
8653
|
-
state: parsed
|
|
8654
|
-
};
|
|
8655
|
-
if (schemaVersion === 2) return {
|
|
8656
|
-
schemaVersion: 2,
|
|
8657
|
-
document: parsed
|
|
8658
|
-
};
|
|
8659
|
-
throw new StateSchemaTooNew(stage, schemaVersion);
|
|
8660
|
-
}
|
|
8661
|
-
function encodeDocument(document) {
|
|
8662
|
-
return JSON.stringify(document, null, 2);
|
|
8663
|
-
}
|
|
8664
|
-
/**
|
|
8665
|
-
* v1 state as a v2 document. The ids v1 kept are seeded as `ready` records,
|
|
8666
|
-
* so the first journaled deploy after an upgrade adopts them rather than
|
|
8667
|
-
* treating them as unknown.
|
|
8668
|
-
*/
|
|
8669
|
-
function migrateV1(stage, state) {
|
|
8670
|
-
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
8671
|
-
const resources = {};
|
|
8672
|
-
const seed = (type, name, id) => {
|
|
8673
|
-
if (!id) return;
|
|
8674
|
-
const key = name ? `${type}:${name}` : type;
|
|
8675
|
-
resources[key] = {
|
|
8676
|
-
key,
|
|
8677
|
-
type,
|
|
8678
|
-
id,
|
|
8679
|
-
status: "ready",
|
|
8680
|
-
updatedAt: now
|
|
8681
|
-
};
|
|
8682
|
-
};
|
|
8683
|
-
seed("project", null, state.projectId);
|
|
8684
|
-
seed("environment", null, state.environmentId);
|
|
8685
|
-
for (const [app, id] of Object.entries(state.applications ?? {})) seed("application", app, id);
|
|
8686
|
-
seed("postgres", null, state.services?.postgresId);
|
|
8687
|
-
seed("redis", null, state.services?.redisId);
|
|
8688
|
-
seed("backup-destination", null, state.backups?.destinationId);
|
|
8689
|
-
return {
|
|
8690
|
-
schemaVersion: 2,
|
|
8691
|
-
stage,
|
|
8692
|
-
serial: 1,
|
|
8693
|
-
state,
|
|
8694
|
-
resources,
|
|
8695
|
-
updatedAt: now
|
|
8696
|
-
};
|
|
8697
|
-
}
|
|
8698
|
-
function newLockHolder(options) {
|
|
8699
|
-
let owner = "unknown";
|
|
8700
|
-
try {
|
|
8701
|
-
owner = (0, node_os.userInfo)().username;
|
|
8702
|
-
} catch {}
|
|
8703
|
-
return {
|
|
8704
|
-
id: (0, node_crypto.randomUUID)(),
|
|
8705
|
-
owner,
|
|
8706
|
-
host: (0, node_os.hostname)(),
|
|
8707
|
-
pid: process.pid,
|
|
8708
|
-
acquiredAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
8709
|
-
operation: options?.operation
|
|
8710
|
-
};
|
|
8711
|
-
}
|
|
8712
|
-
function contentVersion(body) {
|
|
8713
|
-
return (0, node_crypto.createHash)("sha256").update(body).digest("hex").slice(0, 16);
|
|
8714
|
-
}
|
|
8715
|
-
/**
|
|
8716
|
-
* A store over a backend that can read and conditionally write one blob per
|
|
8717
|
-
* stage, and create a lock object only if it does not exist. Migration and
|
|
8718
|
-
* resource records are the same for every such backend, so they live here.
|
|
8719
|
-
*/
|
|
8720
|
-
var DocumentStateStore = class {
|
|
8721
|
-
async lock(stage, options) {
|
|
8722
|
-
const holder = newLockHolder(options);
|
|
8723
|
-
await this.createLock(stage, holder);
|
|
8724
|
-
let released = false;
|
|
8725
|
-
return {
|
|
8726
|
-
stage,
|
|
8727
|
-
holder,
|
|
8728
|
-
release: async () => {
|
|
8729
|
-
if (released) return;
|
|
8730
|
-
released = true;
|
|
8731
|
-
await this.removeLock(stage, holder);
|
|
8732
|
-
}
|
|
8733
|
-
};
|
|
9605
|
+
backupPath(stage) {
|
|
9606
|
+
return (0, node_path.join)(this.dir, `deploy-${stage}.v1.json`);
|
|
8734
9607
|
}
|
|
8735
|
-
|
|
8736
|
-
|
|
8737
|
-
await this.removeLock(stage, null);
|
|
8738
|
-
return holder;
|
|
9608
|
+
mutexPath(stage) {
|
|
9609
|
+
return (0, node_path.join)(this.dir, `deploy-${stage}.json.mutex`);
|
|
8739
9610
|
}
|
|
8740
|
-
|
|
8741
|
-
|
|
8742
|
-
if (!current) return null;
|
|
8743
|
-
return {
|
|
8744
|
-
state: current.document.state,
|
|
8745
|
-
resources: current.document.resources,
|
|
8746
|
-
version: current.version
|
|
8747
|
-
};
|
|
9611
|
+
location(stage) {
|
|
9612
|
+
return this.statePath(stage);
|
|
8748
9613
|
}
|
|
8749
|
-
async
|
|
8750
|
-
|
|
8751
|
-
const
|
|
8752
|
-
|
|
8753
|
-
|
|
8754
|
-
|
|
8755
|
-
state,
|
|
8756
|
-
resources: {},
|
|
8757
|
-
updatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
9614
|
+
async readRaw(stage) {
|
|
9615
|
+
try {
|
|
9616
|
+
const body = await (0, node_fs_promises.readFile)(this.statePath(stage), "utf-8");
|
|
9617
|
+
return {
|
|
9618
|
+
body,
|
|
9619
|
+
version: require_StateStore.contentVersion(body)
|
|
8758
9620
|
};
|
|
8759
|
-
|
|
9621
|
+
} catch (error) {
|
|
9622
|
+
if (isCode(error, "ENOENT")) return null;
|
|
9623
|
+
throw error;
|
|
8760
9624
|
}
|
|
8761
|
-
return this.mutate(stage, options.expectedVersion, (document) => ({
|
|
8762
|
-
...document,
|
|
8763
|
-
state
|
|
8764
|
-
}));
|
|
8765
9625
|
}
|
|
8766
|
-
async
|
|
8767
|
-
|
|
8768
|
-
|
|
8769
|
-
|
|
8770
|
-
|
|
8771
|
-
|
|
8772
|
-
|
|
8773
|
-
|
|
9626
|
+
async writeRaw(stage, body, expectedVersion) {
|
|
9627
|
+
await this.ensureDir();
|
|
9628
|
+
return this.withMutex(stage, async () => {
|
|
9629
|
+
const actual = (await this.readRaw(stage))?.version ?? null;
|
|
9630
|
+
if (actual !== expectedVersion) throw new require_StateStore.StateVersionConflict(stage, expectedVersion, actual);
|
|
9631
|
+
const target = this.statePath(stage);
|
|
9632
|
+
const temp = `${target}.${(0, node_crypto.randomUUID)()}.tmp`;
|
|
9633
|
+
try {
|
|
9634
|
+
const handle = await (0, node_fs_promises.open)(temp, "wx", FILE_MODE);
|
|
9635
|
+
try {
|
|
9636
|
+
await handle.writeFile(body);
|
|
9637
|
+
await handle.sync();
|
|
9638
|
+
} finally {
|
|
9639
|
+
await handle.close();
|
|
8774
9640
|
}
|
|
9641
|
+
await (0, node_fs_promises.rename)(temp, target);
|
|
9642
|
+
} catch (error) {
|
|
9643
|
+
await (0, node_fs_promises.rm)(temp, { force: true });
|
|
9644
|
+
throw error;
|
|
8775
9645
|
}
|
|
8776
|
-
|
|
8777
|
-
}
|
|
8778
|
-
async deleteResource(stage, key, options) {
|
|
8779
|
-
return this.mutate(stage, options?.expectedVersion, (document) => {
|
|
8780
|
-
const { [key]: _removed, ...resources } = document.resources;
|
|
8781
|
-
return {
|
|
8782
|
-
...document,
|
|
8783
|
-
resources
|
|
8784
|
-
};
|
|
9646
|
+
return require_StateStore.contentVersion(body);
|
|
8785
9647
|
});
|
|
8786
9648
|
}
|
|
8787
|
-
async
|
|
8788
|
-
|
|
8789
|
-
|
|
8790
|
-
|
|
8791
|
-
|
|
9649
|
+
async writeV1Backup(stage, body) {
|
|
9650
|
+
try {
|
|
9651
|
+
await (0, node_fs_promises.writeFile)(this.backupPath(stage), body, {
|
|
9652
|
+
flag: "wx",
|
|
9653
|
+
mode: FILE_MODE
|
|
9654
|
+
});
|
|
9655
|
+
} catch (error) {
|
|
9656
|
+
if (!isCode(error, "EEXIST")) throw error;
|
|
8792
9657
|
}
|
|
8793
|
-
if (expectedVersion !== void 0 && current.version !== expectedVersion) throw new StateVersionConflict(stage, expectedVersion, current.version);
|
|
8794
|
-
const next = change(current.document);
|
|
8795
|
-
next.serial = current.document.serial + 1;
|
|
8796
|
-
next.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
8797
|
-
return this.writeRaw(stage, encodeDocument(next), current.version);
|
|
8798
9658
|
}
|
|
8799
|
-
|
|
8800
|
-
|
|
8801
|
-
|
|
8802
|
-
* wrote) first, its result is read instead.
|
|
8803
|
-
*/
|
|
8804
|
-
async readDocument(stage, attempt = 0) {
|
|
8805
|
-
const raw = await this.readRaw(stage);
|
|
8806
|
-
if (!raw) return null;
|
|
8807
|
-
const decoded = decodeDocument(stage, raw.body, this.location(stage));
|
|
8808
|
-
if (decoded.schemaVersion === 2) return {
|
|
8809
|
-
document: decoded.document,
|
|
8810
|
-
version: raw.version
|
|
8811
|
-
};
|
|
8812
|
-
await this.writeV1Backup(stage, raw.body);
|
|
8813
|
-
const document = migrateV1(stage, decoded.state);
|
|
9659
|
+
async createLock(stage, holder) {
|
|
9660
|
+
await this.ensureDir();
|
|
9661
|
+
let handle;
|
|
8814
9662
|
try {
|
|
8815
|
-
|
|
8816
|
-
document,
|
|
8817
|
-
version: await this.writeRaw(stage, encodeDocument(document), raw.version)
|
|
8818
|
-
};
|
|
9663
|
+
handle = await (0, node_fs_promises.open)(this.lockPath(stage), "wx", FILE_MODE);
|
|
8819
9664
|
} catch (error) {
|
|
8820
|
-
if (error
|
|
9665
|
+
if (isCode(error, "EEXIST")) throw new require_StateStore.StateLocked(stage, await this.readLock(stage), this.lockPath(stage));
|
|
8821
9666
|
throw error;
|
|
8822
9667
|
}
|
|
9668
|
+
try {
|
|
9669
|
+
await handle.writeFile(JSON.stringify(holder, null, 2));
|
|
9670
|
+
} finally {
|
|
9671
|
+
await handle.close();
|
|
9672
|
+
}
|
|
8823
9673
|
}
|
|
8824
|
-
|
|
8825
|
-
|
|
8826
|
-
|
|
8827
|
-
}
|
|
8828
|
-
|
|
8829
|
-
|
|
8830
|
-
* `createStateProvider` reads:
|
|
8831
|
-
*
|
|
8832
|
-
* - none / `'local'`: `LocalStateStore` under `.gkm/`
|
|
8833
|
-
* - `'ssm'`: `SSMStateStore`, with no local cache — SSM is read directly, so
|
|
8834
|
-
* a stale local copy can never win over it
|
|
8835
|
-
* - `'s3'`: `S3StateStore`
|
|
8836
|
-
* - a `StateStore` object: used as is
|
|
8837
|
-
* - a `StateProvider` object: wrapped by `LegacyStateStore`
|
|
8838
|
-
*/
|
|
8839
|
-
async function createStateStore(options) {
|
|
8840
|
-
const { config, workspaceRoot, workspaceName } = options;
|
|
8841
|
-
if (!config || config.provider === "local") {
|
|
8842
|
-
const { LocalStateStore } = await Promise.resolve().then(() => require("./LocalStateStore-DDDU0HkI.cjs"));
|
|
8843
|
-
return new LocalStateStore(workspaceRoot);
|
|
8844
|
-
}
|
|
8845
|
-
if (isStateStore(config.provider)) return config.provider;
|
|
8846
|
-
if (isStateProvider(config.provider)) {
|
|
8847
|
-
const { LegacyStateStore } = await Promise.resolve().then(() => require("./LegacyStateStore-C_OFs1gN.cjs"));
|
|
8848
|
-
return new LegacyStateStore(config.provider, { warn: options.warn });
|
|
8849
|
-
}
|
|
8850
|
-
if (config.provider === "ssm") {
|
|
8851
|
-
if (!workspaceName) throw new StateStoreNeedsWorkspaceName("ssm");
|
|
8852
|
-
const { SSMStateStore } = await Promise.resolve().then(() => require("./SSMStateStore-C7KcDSTb.cjs"));
|
|
8853
|
-
const ssm = config;
|
|
8854
|
-
return SSMStateStore.create({
|
|
8855
|
-
workspaceName,
|
|
8856
|
-
region: ssm.region,
|
|
8857
|
-
profile: ssm.profile
|
|
8858
|
-
});
|
|
9674
|
+
async readLock(stage) {
|
|
9675
|
+
try {
|
|
9676
|
+
return JSON.parse(await (0, node_fs_promises.readFile)(this.lockPath(stage), "utf-8"));
|
|
9677
|
+
} catch {
|
|
9678
|
+
return null;
|
|
9679
|
+
}
|
|
8859
9680
|
}
|
|
8860
|
-
|
|
8861
|
-
if (
|
|
8862
|
-
|
|
8863
|
-
|
|
8864
|
-
|
|
8865
|
-
workspaceName,
|
|
8866
|
-
bucket: s3.bucket,
|
|
8867
|
-
prefix: s3.prefix,
|
|
8868
|
-
region: s3.region,
|
|
8869
|
-
profile: s3.profile
|
|
8870
|
-
});
|
|
9681
|
+
async removeLock(stage, holder) {
|
|
9682
|
+
if (holder) {
|
|
9683
|
+
if ((await this.readLock(stage))?.id !== holder.id) return;
|
|
9684
|
+
}
|
|
9685
|
+
await (0, node_fs_promises.rm)(this.lockPath(stage), { force: true });
|
|
8871
9686
|
}
|
|
8872
|
-
|
|
8873
|
-
|
|
8874
|
-
|
|
8875
|
-
|
|
8876
|
-
|
|
8877
|
-
* conditional on the version this provider last read.
|
|
8878
|
-
*/
|
|
8879
|
-
var StateStoreProvider = class {
|
|
8880
|
-
store;
|
|
8881
|
-
versions = /* @__PURE__ */ new Map();
|
|
8882
|
-
constructor(store) {
|
|
8883
|
-
this.store = store;
|
|
9687
|
+
async ensureDir() {
|
|
9688
|
+
await (0, node_fs_promises.mkdir)(this.dir, {
|
|
9689
|
+
recursive: true,
|
|
9690
|
+
mode: 448
|
|
9691
|
+
});
|
|
8884
9692
|
}
|
|
8885
|
-
|
|
8886
|
-
|
|
8887
|
-
|
|
8888
|
-
|
|
9693
|
+
/**
|
|
9694
|
+
* Serialises the check-then-rename of a conditional write across
|
|
9695
|
+
* processes. The stage lock is for whole runs; this is for the instant of
|
|
9696
|
+
* a single write, so even writers that skip the lock never interleave.
|
|
9697
|
+
*/
|
|
9698
|
+
async withMutex(stage, fn) {
|
|
9699
|
+
const path = this.mutexPath(stage);
|
|
9700
|
+
const deadline = Date.now() + MUTEX_TIMEOUT_MS;
|
|
9701
|
+
for (;;) try {
|
|
9702
|
+
await (await (0, node_fs_promises.open)(path, "wx", FILE_MODE)).close();
|
|
9703
|
+
break;
|
|
9704
|
+
} catch (error) {
|
|
9705
|
+
if (!isCode(error, "EEXIST")) throw error;
|
|
9706
|
+
if (await this.isStale(path)) {
|
|
9707
|
+
await (0, node_fs_promises.unlink)(path).catch(() => {});
|
|
9708
|
+
continue;
|
|
9709
|
+
}
|
|
9710
|
+
if (Date.now() > deadline) throw new require_StateStore.StateStoreBusy(stage, path);
|
|
9711
|
+
await new Promise((resolve) => setTimeout(resolve, 10));
|
|
9712
|
+
}
|
|
9713
|
+
try {
|
|
9714
|
+
return await fn();
|
|
9715
|
+
} finally {
|
|
9716
|
+
await (0, node_fs_promises.rm)(path, { force: true });
|
|
9717
|
+
}
|
|
8889
9718
|
}
|
|
8890
|
-
async
|
|
8891
|
-
|
|
8892
|
-
|
|
8893
|
-
|
|
8894
|
-
|
|
9719
|
+
async isStale(path) {
|
|
9720
|
+
try {
|
|
9721
|
+
return Date.now() - (await (0, node_fs_promises.stat)(path)).mtimeMs > MUTEX_STALE_MS;
|
|
9722
|
+
} catch {
|
|
9723
|
+
return false;
|
|
9724
|
+
}
|
|
8895
9725
|
}
|
|
8896
9726
|
};
|
|
8897
9727
|
//#endregion
|
|
@@ -8899,7 +9729,12 @@ var StateStoreProvider = class {
|
|
|
8899
9729
|
/**
|
|
8900
9730
|
* State Management CLI Commands
|
|
8901
9731
|
*
|
|
8902
|
-
* Commands for managing deployment state across local and remote
|
|
9732
|
+
* Commands for managing deployment state across local and remote stores.
|
|
9733
|
+
*
|
|
9734
|
+
* Every command goes through a `StateStore`, the same one deploy writes
|
|
9735
|
+
* through: a v1 state is migrated on the way, resource records travel with
|
|
9736
|
+
* the state, and a copy is a conditional write — so `state:push` cannot
|
|
9737
|
+
* silently replace what a deploy wrote a moment earlier.
|
|
8903
9738
|
*/
|
|
8904
9739
|
/** What `state:show` prints in place of a secret. */
|
|
8905
9740
|
const MASKED = "********";
|
|
@@ -8925,26 +9760,12 @@ function maskStateSecrets(state) {
|
|
|
8925
9760
|
*/
|
|
8926
9761
|
async function statePullCommand(options) {
|
|
8927
9762
|
const { workspace } = await require_config.loadWorkspaceConfig();
|
|
8928
|
-
|
|
8929
|
-
console.error("No remote state provider configured.");
|
|
8930
|
-
console.error("Add a remote provider in gkm.config.ts:");
|
|
8931
|
-
console.error(" state: { provider: \"ssm\", region: \"us-east-1\" }");
|
|
8932
|
-
process.exit(1);
|
|
8933
|
-
}
|
|
8934
|
-
const provider = await createStateProvider({
|
|
8935
|
-
config: workspace.state,
|
|
8936
|
-
workspaceRoot: workspace.root,
|
|
8937
|
-
workspaceName: workspace.name
|
|
8938
|
-
});
|
|
8939
|
-
if (!(provider instanceof CachedStateProvider)) {
|
|
8940
|
-
console.error("State provider does not support pull operation.");
|
|
8941
|
-
process.exit(1);
|
|
8942
|
-
}
|
|
9763
|
+
const { local, remote } = await remoteAndLocal(workspace);
|
|
8943
9764
|
console.log(`Pulling state for stage: ${options.stage}...`);
|
|
8944
|
-
const
|
|
8945
|
-
if (
|
|
9765
|
+
const pulled = await copyUnderLock(remote, local, local, options.stage, "state:pull");
|
|
9766
|
+
if (pulled) {
|
|
8946
9767
|
console.log("State pulled successfully.");
|
|
8947
|
-
printStateSummary(state);
|
|
9768
|
+
printStateSummary(pulled.state);
|
|
8948
9769
|
} else console.log("No remote state found for this stage.");
|
|
8949
9770
|
}
|
|
8950
9771
|
/**
|
|
@@ -8953,26 +9774,12 @@ async function statePullCommand(options) {
|
|
|
8953
9774
|
*/
|
|
8954
9775
|
async function statePushCommand(options) {
|
|
8955
9776
|
const { workspace } = await require_config.loadWorkspaceConfig();
|
|
8956
|
-
|
|
8957
|
-
console.error("No remote state provider configured.");
|
|
8958
|
-
console.error("Add a remote provider in gkm.config.ts:");
|
|
8959
|
-
console.error(" state: { provider: \"ssm\", region: \"us-east-1\" }");
|
|
8960
|
-
process.exit(1);
|
|
8961
|
-
}
|
|
8962
|
-
const provider = await createStateProvider({
|
|
8963
|
-
config: workspace.state,
|
|
8964
|
-
workspaceRoot: workspace.root,
|
|
8965
|
-
workspaceName: workspace.name
|
|
8966
|
-
});
|
|
8967
|
-
if (!(provider instanceof CachedStateProvider)) {
|
|
8968
|
-
console.error("State provider does not support push operation.");
|
|
8969
|
-
process.exit(1);
|
|
8970
|
-
}
|
|
9777
|
+
const { local, remote } = await remoteAndLocal(workspace);
|
|
8971
9778
|
console.log(`Pushing state for stage: ${options.stage}...`);
|
|
8972
|
-
const
|
|
8973
|
-
if (
|
|
9779
|
+
const pushed = await copyUnderLock(local, remote, remote, options.stage, "state:push");
|
|
9780
|
+
if (pushed) {
|
|
8974
9781
|
console.log("State pushed successfully.");
|
|
8975
|
-
printStateSummary(state);
|
|
9782
|
+
printStateSummary(pushed.state);
|
|
8976
9783
|
} else console.log("No local state found for this stage.");
|
|
8977
9784
|
}
|
|
8978
9785
|
/**
|
|
@@ -8981,7 +9788,7 @@ async function statePushCommand(options) {
|
|
|
8981
9788
|
*/
|
|
8982
9789
|
async function stateShowCommand(options) {
|
|
8983
9790
|
const { workspace } = await require_config.loadWorkspaceConfig();
|
|
8984
|
-
const stored = await (await
|
|
9791
|
+
const stored = await (await require_StateStore.createStateStore({
|
|
8985
9792
|
config: workspace.state,
|
|
8986
9793
|
workspaceRoot: workspace.root,
|
|
8987
9794
|
workspaceName: workspace.name
|
|
@@ -8990,9 +9797,12 @@ async function stateShowCommand(options) {
|
|
|
8990
9797
|
console.log(`No state found for stage: ${options.stage}`);
|
|
8991
9798
|
return;
|
|
8992
9799
|
}
|
|
8993
|
-
const state = maskStateSecrets(stored);
|
|
9800
|
+
const state = maskStateSecrets(stored.state);
|
|
8994
9801
|
if (options.json) console.log(JSON.stringify(state, null, 2));
|
|
8995
|
-
else
|
|
9802
|
+
else {
|
|
9803
|
+
printStateDetails(state);
|
|
9804
|
+
printUnfinished(stored.resources);
|
|
9805
|
+
}
|
|
8996
9806
|
}
|
|
8997
9807
|
/**
|
|
8998
9808
|
* Release a stage's deploy lock, whoever holds it.
|
|
@@ -9004,7 +9814,7 @@ async function stateShowCommand(options) {
|
|
|
9004
9814
|
*/
|
|
9005
9815
|
async function stateUnlockCommand(options) {
|
|
9006
9816
|
const { workspace } = await require_config.loadWorkspaceConfig();
|
|
9007
|
-
const holder = await (await createStateStore({
|
|
9817
|
+
const holder = await (await require_StateStore.createStateStore({
|
|
9008
9818
|
config: workspace.state,
|
|
9009
9819
|
workspaceRoot: workspace.root,
|
|
9010
9820
|
workspaceName: workspace.name
|
|
@@ -9021,22 +9831,11 @@ async function stateUnlockCommand(options) {
|
|
|
9021
9831
|
*/
|
|
9022
9832
|
async function stateDiffCommand(options) {
|
|
9023
9833
|
const { workspace } = await require_config.loadWorkspaceConfig();
|
|
9024
|
-
|
|
9025
|
-
console.error("No remote state provider configured.");
|
|
9026
|
-
console.error("Diff requires a remote provider to compare against.");
|
|
9027
|
-
process.exit(1);
|
|
9028
|
-
}
|
|
9029
|
-
const provider = await createStateProvider({
|
|
9030
|
-
config: workspace.state,
|
|
9031
|
-
workspaceRoot: workspace.root,
|
|
9032
|
-
workspaceName: workspace.name
|
|
9033
|
-
});
|
|
9034
|
-
if (!(provider instanceof CachedStateProvider)) {
|
|
9035
|
-
console.error("State provider does not support diff operation.");
|
|
9036
|
-
process.exit(1);
|
|
9037
|
-
}
|
|
9834
|
+
const stores = await remoteAndLocal(workspace, ["Diff requires a remote provider to compare against."]);
|
|
9038
9835
|
console.log(`Comparing state for stage: ${options.stage}...\n`);
|
|
9039
|
-
const
|
|
9836
|
+
const [localStored, remoteStored] = await Promise.all([stores.local.read(options.stage), stores.remote.read(options.stage)]);
|
|
9837
|
+
const local = localStored?.state ?? null;
|
|
9838
|
+
const remote = remoteStored?.state ?? null;
|
|
9040
9839
|
if (!local && !remote) {
|
|
9041
9840
|
console.log("No state found (local or remote).");
|
|
9042
9841
|
return;
|
|
@@ -9072,6 +9871,71 @@ async function stateDiffCommand(options) {
|
|
|
9072
9871
|
else console.log(` ${key}: ${localVal ?? "(none)"} (local) != ${remoteVal ?? "(none)"} (remote)`);
|
|
9073
9872
|
}
|
|
9074
9873
|
}
|
|
9874
|
+
const localRecords = localStored?.resources ?? {};
|
|
9875
|
+
const remoteRecords = remoteStored?.resources ?? {};
|
|
9876
|
+
const differing = [.../* @__PURE__ */ new Set([...Object.keys(localRecords), ...Object.keys(remoteRecords)])].filter((key) => describeRecord(localRecords[key]) !== describeRecord(remoteRecords[key]));
|
|
9877
|
+
if (differing.length > 0) {
|
|
9878
|
+
console.log("\nResources:");
|
|
9879
|
+
for (const key of differing) console.log(` ${key}: ${describeRecord(localRecords[key])} (local) != ${describeRecord(remoteRecords[key])} (remote)`);
|
|
9880
|
+
}
|
|
9881
|
+
}
|
|
9882
|
+
/**
|
|
9883
|
+
* The stage's local store and its remote one — or, for a workspace that keeps
|
|
9884
|
+
* state only locally, the reason there is nothing to move it between.
|
|
9885
|
+
*/
|
|
9886
|
+
async function remoteAndLocal(workspace, hint = ["Add a remote provider in gkm.config.ts:", " state: { provider: \"ssm\", region: \"us-east-1\" }"]) {
|
|
9887
|
+
if (!workspace.state || workspace.state.provider === "local") {
|
|
9888
|
+
console.error("No remote state provider configured.");
|
|
9889
|
+
for (const line of hint) console.error(line);
|
|
9890
|
+
process.exit(1);
|
|
9891
|
+
}
|
|
9892
|
+
return {
|
|
9893
|
+
local: new LocalStateStore(workspace.root),
|
|
9894
|
+
remote: await require_StateStore.createStateStore({
|
|
9895
|
+
config: workspace.state,
|
|
9896
|
+
workspaceRoot: workspace.root,
|
|
9897
|
+
workspaceName: workspace.name
|
|
9898
|
+
})
|
|
9899
|
+
};
|
|
9900
|
+
}
|
|
9901
|
+
/**
|
|
9902
|
+
* Makes `to` hold exactly what `from` holds for `stage` — its state and its
|
|
9903
|
+
* resource records — while holding `locked`'s lock for the stage. Every
|
|
9904
|
+
* write is conditional on the version just read, so anything that changes
|
|
9905
|
+
* `to` mid-copy raises `StateVersionConflict` rather than being overwritten.
|
|
9906
|
+
*/
|
|
9907
|
+
async function copyUnderLock(from, to, locked, stage, operation) {
|
|
9908
|
+
const lock = await locked.lock(stage, { operation });
|
|
9909
|
+
try {
|
|
9910
|
+
return await copyStage(from, to, stage);
|
|
9911
|
+
} finally {
|
|
9912
|
+
await lock.release();
|
|
9913
|
+
}
|
|
9914
|
+
}
|
|
9915
|
+
/** @internal Exported for testing */
|
|
9916
|
+
async function copyStage(from, to, stage) {
|
|
9917
|
+
const source = await from.read(stage);
|
|
9918
|
+
if (!source) return null;
|
|
9919
|
+
const target = await to.read(stage);
|
|
9920
|
+
let version = await to.write(stage, source.state, { expectedVersion: target?.version ?? null });
|
|
9921
|
+
for (const key of Object.keys(target?.resources ?? {})) {
|
|
9922
|
+
if (key in source.resources) continue;
|
|
9923
|
+
version = await to.deleteResource(stage, key, { expectedVersion: version });
|
|
9924
|
+
}
|
|
9925
|
+
for (const { updatedAt: _stamped, ...record } of Object.values(source.resources)) version = await to.putResource(stage, record, { expectedVersion: version });
|
|
9926
|
+
return source;
|
|
9927
|
+
}
|
|
9928
|
+
function describeRecord(record) {
|
|
9929
|
+
if (!record) return "(none)";
|
|
9930
|
+
return record.id ? `${record.status} ${record.id}` : record.status;
|
|
9931
|
+
}
|
|
9932
|
+
/** Resources a run marked `pending` and never saw created. */
|
|
9933
|
+
function printUnfinished(resources) {
|
|
9934
|
+
const pending = Object.values(resources).filter((record) => record.status === "pending");
|
|
9935
|
+
if (pending.length === 0) return;
|
|
9936
|
+
console.log("");
|
|
9937
|
+
console.log("Unfinished (a deploy stopped while creating these):");
|
|
9938
|
+
for (const record of pending) console.log(` ${record.key}`);
|
|
9075
9939
|
}
|
|
9076
9940
|
function printStateSummary(state) {
|
|
9077
9941
|
const appCount = Object.keys(state.applications).length;
|
|
@@ -10119,25 +10983,25 @@ function loadPackageJson() {
|
|
|
10119
10983
|
* Run: pnpm --filter @geekmidas/cli sync-versions
|
|
10120
10984
|
*/
|
|
10121
10985
|
const GEEKMIDAS_VERSIONS = {
|
|
10122
|
-
"@geekmidas/audit": "~10.0.0-alpha.
|
|
10123
|
-
"@geekmidas/auth": "~10.0.0-alpha.
|
|
10124
|
-
"@geekmidas/cache": "~10.0.0-alpha.
|
|
10125
|
-
"@geekmidas/client": "~10.0.0-alpha.
|
|
10126
|
-
"@geekmidas/cloud": "~10.0.0-alpha.
|
|
10127
|
-
"@geekmidas/constructs": "~10.0.0-alpha.
|
|
10128
|
-
"@geekmidas/db": "~10.0.0-alpha.
|
|
10129
|
-
"@geekmidas/emailkit": "~10.0.0-alpha.
|
|
10130
|
-
"@geekmidas/envkit": "~10.0.0-alpha.
|
|
10131
|
-
"@geekmidas/errors": "~10.0.0-alpha.
|
|
10132
|
-
"@geekmidas/events": "~10.0.0-alpha.
|
|
10133
|
-
"@geekmidas/logger": "~10.0.0-alpha.
|
|
10134
|
-
"@geekmidas/manifest": "~10.0.0-alpha.
|
|
10135
|
-
"@geekmidas/rate-limit": "~10.0.0-alpha.
|
|
10136
|
-
"@geekmidas/schema": "~10.0.0-alpha.
|
|
10137
|
-
"@geekmidas/services": "~10.0.0-alpha.
|
|
10138
|
-
"@geekmidas/storage": "~10.0.0-alpha.
|
|
10139
|
-
"@geekmidas/telescope": "~10.0.0-alpha.
|
|
10140
|
-
"@geekmidas/testkit": "~10.0.0-alpha.
|
|
10986
|
+
"@geekmidas/audit": "~10.0.0-alpha.59",
|
|
10987
|
+
"@geekmidas/auth": "~10.0.0-alpha.59",
|
|
10988
|
+
"@geekmidas/cache": "~10.0.0-alpha.59",
|
|
10989
|
+
"@geekmidas/client": "~10.0.0-alpha.59",
|
|
10990
|
+
"@geekmidas/cloud": "~10.0.0-alpha.59",
|
|
10991
|
+
"@geekmidas/constructs": "~10.0.0-alpha.59",
|
|
10992
|
+
"@geekmidas/db": "~10.0.0-alpha.59",
|
|
10993
|
+
"@geekmidas/emailkit": "~10.0.0-alpha.59",
|
|
10994
|
+
"@geekmidas/envkit": "~10.0.0-alpha.59",
|
|
10995
|
+
"@geekmidas/errors": "~10.0.0-alpha.59",
|
|
10996
|
+
"@geekmidas/events": "~10.0.0-alpha.59",
|
|
10997
|
+
"@geekmidas/logger": "~10.0.0-alpha.59",
|
|
10998
|
+
"@geekmidas/manifest": "~10.0.0-alpha.59",
|
|
10999
|
+
"@geekmidas/rate-limit": "~10.0.0-alpha.59",
|
|
11000
|
+
"@geekmidas/schema": "~10.0.0-alpha.59",
|
|
11001
|
+
"@geekmidas/services": "~10.0.0-alpha.59",
|
|
11002
|
+
"@geekmidas/storage": "~10.0.0-alpha.59",
|
|
11003
|
+
"@geekmidas/telescope": "~10.0.0-alpha.59",
|
|
11004
|
+
"@geekmidas/testkit": "~10.0.0-alpha.59",
|
|
10141
11005
|
"@geekmidas/cli": `~${loadPackageJson().version}`
|
|
10142
11006
|
};
|
|
10143
11007
|
//#endregion
|
|
@@ -16019,7 +16883,7 @@ function testDatabases(reconciled) {
|
|
|
16019
16883
|
* starts without the reference data the app needs.
|
|
16020
16884
|
*/
|
|
16021
16885
|
async function migrateTestStage(workspace, env) {
|
|
16022
|
-
const { migrateAndSeed } = await Promise.resolve().then(() => require("./migrate-
|
|
16886
|
+
const { migrateAndSeed } = await Promise.resolve().then(() => require("./migrate-CThJfVHR.cjs"));
|
|
16023
16887
|
const { discover } = await Promise.resolve().then(() => require("./discover-DXNviAh-.cjs")).then((n) => n.discover_exports);
|
|
16024
16888
|
const sources = {};
|
|
16025
16889
|
const manifest = await discover({
|
|
@@ -16450,7 +17314,7 @@ program.command("migrate").description("Apply each database construct's migratio
|
|
|
16450
17314
|
try {
|
|
16451
17315
|
const globalOptions = program.opts();
|
|
16452
17316
|
if (globalOptions.cwd) process.chdir(globalOptions.cwd);
|
|
16453
|
-
const { migrateCommand } = await Promise.resolve().then(() => require("./migrate-
|
|
17317
|
+
const { migrateCommand } = await Promise.resolve().then(() => require("./migrate-CThJfVHR.cjs"));
|
|
16454
17318
|
await migrateCommand({
|
|
16455
17319
|
...options,
|
|
16456
17320
|
...construct ? { construct } : {}
|
|
@@ -16464,7 +17328,7 @@ program.command("seed").description("Migrate, then run each database construct's
|
|
|
16464
17328
|
try {
|
|
16465
17329
|
const globalOptions = program.opts();
|
|
16466
17330
|
if (globalOptions.cwd) process.chdir(globalOptions.cwd);
|
|
16467
|
-
const { seedCommand } = await Promise.resolve().then(() => require("./migrate-
|
|
17331
|
+
const { seedCommand } = await Promise.resolve().then(() => require("./migrate-CThJfVHR.cjs"));
|
|
16468
17332
|
await seedCommand({
|
|
16469
17333
|
...options,
|
|
16470
17334
|
...construct ? { construct } : {}
|
|
@@ -16478,7 +17342,7 @@ program.command("migration").description("Write a construct's next migration: an
|
|
|
16478
17342
|
try {
|
|
16479
17343
|
const globalOptions = program.opts();
|
|
16480
17344
|
if (globalOptions.cwd) process.chdir(globalOptions.cwd);
|
|
16481
|
-
const { migrationCommand } = await Promise.resolve().then(() => require("./migrate-
|
|
17345
|
+
const { migrationCommand } = await Promise.resolve().then(() => require("./migrate-CThJfVHR.cjs"));
|
|
16482
17346
|
await migrationCommand({
|
|
16483
17347
|
construct,
|
|
16484
17348
|
...name ? { name } : {}
|
|
@@ -16576,7 +17440,7 @@ program.command("encryption:rotate").description("Add a new current key to an En
|
|
|
16576
17440
|
try {
|
|
16577
17441
|
const globalOptions = program.opts();
|
|
16578
17442
|
if (globalOptions.cwd) process.chdir(globalOptions.cwd);
|
|
16579
|
-
const { encryptionRotateCommand } = await Promise.resolve().then(() => require("./encryption-
|
|
17443
|
+
const { encryptionRotateCommand } = await Promise.resolve().then(() => require("./encryption-BP8zfxEJ.cjs"));
|
|
16580
17444
|
await encryptionRotateCommand(construct, options);
|
|
16581
17445
|
} catch (error) {
|
|
16582
17446
|
console.error(formatError(error));
|
|
@@ -16587,7 +17451,7 @@ program.command("encryption:retire").description("Remove an old key from an Encr
|
|
|
16587
17451
|
try {
|
|
16588
17452
|
const globalOptions = program.opts();
|
|
16589
17453
|
if (globalOptions.cwd) process.chdir(globalOptions.cwd);
|
|
16590
|
-
const { encryptionRetireCommand } = await Promise.resolve().then(() => require("./encryption-
|
|
17454
|
+
const { encryptionRetireCommand } = await Promise.resolve().then(() => require("./encryption-BP8zfxEJ.cjs"));
|
|
16591
17455
|
await encryptionRetireCommand(construct, key, options);
|
|
16592
17456
|
} catch (error) {
|
|
16593
17457
|
console.error(formatError(error));
|
|
@@ -16639,7 +17503,7 @@ program.command("secrets:reconcile").description("Backfill missing custom secret
|
|
|
16639
17503
|
console.error(`No secrets found for stage "${stage}". Run "gkm secrets:init --stage ${stage}" first.`);
|
|
16640
17504
|
process.exit(1);
|
|
16641
17505
|
}
|
|
16642
|
-
const { derivedContainers } = await Promise.resolve().then(() => require("./workspace-
|
|
17506
|
+
const { derivedContainers } = await Promise.resolve().then(() => require("./workspace-Dbn07ILl.cjs")).then((n) => n.workspace_exports);
|
|
16643
17507
|
const result = reconcileMissingSecrets(secrets, workspace, await derivedContainers(workspace, stage));
|
|
16644
17508
|
if (!result) {
|
|
16645
17509
|
console.log(`\n✓ Secrets for stage "${stage}" are up-to-date`);
|
|
@@ -16698,7 +17562,7 @@ program.command("deploy:github").description("Let GitHub Actions deploy a stage:
|
|
|
16698
17562
|
try {
|
|
16699
17563
|
const globalOptions = program.opts();
|
|
16700
17564
|
if (globalOptions.cwd) process.chdir(globalOptions.cwd);
|
|
16701
|
-
const { deployGithubCommand } = await Promise.resolve().then(() => require("./github-
|
|
17565
|
+
const { deployGithubCommand } = await Promise.resolve().then(() => require("./github-DHpDMBT7.cjs"));
|
|
16702
17566
|
await deployGithubCommand(options);
|
|
16703
17567
|
} catch (error) {
|
|
16704
17568
|
console.error(formatError(error));
|
|
@@ -16833,19 +17697,18 @@ program.command("upgrade").description("Upgrade @geekmidas/cli (with --all, ever
|
|
|
16833
17697
|
});
|
|
16834
17698
|
program.parse();
|
|
16835
17699
|
//#endregion
|
|
16836
|
-
exports
|
|
16837
|
-
|
|
16838
|
-
|
|
16839
|
-
|
|
16840
|
-
|
|
16841
|
-
|
|
17700
|
+
Object.defineProperty(exports, "LocalStateStore_exports", {
|
|
17701
|
+
enumerable: true,
|
|
17702
|
+
get: function() {
|
|
17703
|
+
return LocalStateStore_exports;
|
|
17704
|
+
}
|
|
17705
|
+
});
|
|
16842
17706
|
Object.defineProperty(exports, "appPorts_exports", {
|
|
16843
17707
|
enumerable: true,
|
|
16844
17708
|
get: function() {
|
|
16845
17709
|
return appPorts_exports;
|
|
16846
17710
|
}
|
|
16847
17711
|
});
|
|
16848
|
-
exports.contentVersion = contentVersion;
|
|
16849
17712
|
Object.defineProperty(exports, "encryption_exports", {
|
|
16850
17713
|
enumerable: true,
|
|
16851
17714
|
get: function() {
|
|
@@ -16854,7 +17717,6 @@ Object.defineProperty(exports, "encryption_exports", {
|
|
|
16854
17717
|
});
|
|
16855
17718
|
exports.generateFullstackCustomSecrets = generateFullstackCustomSecrets;
|
|
16856
17719
|
exports.getHostingerToken = getHostingerToken;
|
|
16857
|
-
exports.newLockHolder = newLockHolder;
|
|
16858
17720
|
exports.slug = slug;
|
|
16859
17721
|
exports.stamp = stamp;
|
|
16860
17722
|
|