@geekmidas/cli 10.0.0-alpha.58 → 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.
Files changed (91) hide show
  1. package/dist/{HostingerProvider-cL-6oQ5r.mjs → HostingerProvider-YQY3rMpV.mjs} +2 -2
  2. package/dist/{HostingerProvider-cL-6oQ5r.mjs.map → HostingerProvider-YQY3rMpV.mjs.map} +1 -1
  3. package/dist/{LegacyStateStore-ByRWQ9s1.mjs → LegacyStateStore-CB4ch3Dl.mjs} +2 -2
  4. package/dist/{LegacyStateStore-ByRWQ9s1.mjs.map → LegacyStateStore-CB4ch3Dl.mjs.map} +1 -1
  5. package/dist/{LegacyStateStore-C_OFs1gN.cjs → LegacyStateStore-CQJqvmXw.cjs} +7 -7
  6. package/dist/{LegacyStateStore-C_OFs1gN.cjs.map → LegacyStateStore-CQJqvmXw.cjs.map} +1 -1
  7. package/dist/{S3StateStore-FhzyENrN.mjs → S3StateStore-CXTenDTW.mjs} +2 -2
  8. package/dist/{S3StateStore-FhzyENrN.mjs.map → S3StateStore-CXTenDTW.mjs.map} +1 -1
  9. package/dist/{S3StateStore-C7eBae0G.cjs → S3StateStore-DLS4rEc0.cjs} +5 -5
  10. package/dist/{S3StateStore-C7eBae0G.cjs.map → S3StateStore-DLS4rEc0.cjs.map} +1 -1
  11. package/dist/{SSMStateStore-DC43R7zG.cjs → SSMStateStore-EqsTBIOs.cjs} +9 -9
  12. package/dist/{SSMStateStore-DC43R7zG.cjs.map → SSMStateStore-EqsTBIOs.cjs.map} +1 -1
  13. package/dist/{SSMStateStore-BjZZm-nV.mjs → SSMStateStore-hnP5deFJ.mjs} +2 -2
  14. package/dist/{SSMStateStore-BjZZm-nV.mjs.map → SSMStateStore-hnP5deFJ.mjs.map} +1 -1
  15. package/dist/StateStore-B4u0wx2t.cjs +412 -0
  16. package/dist/StateStore-B4u0wx2t.cjs.map +1 -0
  17. package/dist/StateStore-CYUfGYcx.mjs +359 -0
  18. package/dist/StateStore-CYUfGYcx.mjs.map +1 -0
  19. package/dist/{bundler-URImKj-A.mjs → bundler-ouDdNvk9.mjs} +2 -2
  20. package/dist/{bundler-URImKj-A.mjs.map → bundler-ouDdNvk9.mjs.map} +1 -1
  21. package/dist/{config-CSOK73pY.mjs → config-EuzFEkcH.mjs} +2 -2
  22. package/dist/config-EuzFEkcH.mjs.map +1 -0
  23. package/dist/{config-DdIu1O_I.cjs → config-WMOB9bLL.cjs} +2 -2
  24. package/dist/config-WMOB9bLL.cjs.map +1 -0
  25. package/dist/config.cjs +2 -2
  26. package/dist/config.d.cts +65 -3
  27. package/dist/config.d.cts.map +1 -1
  28. package/dist/config.d.mts +65 -3
  29. package/dist/config.d.mts.map +1 -1
  30. package/dist/config.mjs +2 -2
  31. package/dist/{encryption-BspqzZIb.cjs → encryption-BP8zfxEJ.cjs} +3 -3
  32. package/dist/{encryption-BspqzZIb.cjs.map → encryption-BP8zfxEJ.cjs.map} +1 -1
  33. package/dist/{encryption-Dlnl5XEU.mjs → encryption-BW7Gh51G.mjs} +3 -3
  34. package/dist/{encryption-Dlnl5XEU.mjs.map → encryption-BW7Gh51G.mjs.map} +1 -1
  35. package/dist/{github-mQorSS36.cjs → github-DHpDMBT7.cjs} +3 -3
  36. package/dist/{github-mQorSS36.cjs.map → github-DHpDMBT7.cjs.map} +1 -1
  37. package/dist/{github-BdF9tsvl.mjs → github-T_6sK3Rz.mjs} +3 -3
  38. package/dist/{github-BdF9tsvl.mjs.map → github-T_6sK3Rz.mjs.map} +1 -1
  39. package/dist/{index-BorYE_uO.d.cts → index-B5TJ-mB6.d.mts} +2 -2
  40. package/dist/{index-BorYE_uO.d.cts.map → index-B5TJ-mB6.d.mts.map} +1 -1
  41. package/dist/{index-C3JmluyO.d.mts → index-DV6j4Ydq.d.cts} +2 -2
  42. package/dist/{index-C3JmluyO.d.mts.map → index-DV6j4Ydq.d.cts.map} +1 -1
  43. package/dist/index.cjs +1210 -701
  44. package/dist/index.cjs.map +1 -1
  45. package/dist/index.mjs +1215 -704
  46. package/dist/index.mjs.map +1 -1
  47. package/dist/{migrate-CXVnh1J3.mjs → migrate-BrFREmgQ.mjs} +4 -4
  48. package/dist/{migrate-CXVnh1J3.mjs.map → migrate-BrFREmgQ.mjs.map} +1 -1
  49. package/dist/{migrate-DzYqA-e3.cjs → migrate-CThJfVHR.cjs} +4 -4
  50. package/dist/{migrate-DzYqA-e3.cjs.map → migrate-CThJfVHR.cjs.map} +1 -1
  51. package/dist/openapi.cjs +1 -1
  52. package/dist/openapi.d.cts +1 -1
  53. package/dist/openapi.d.mts +1 -1
  54. package/dist/openapi.mjs +1 -1
  55. package/dist/ready-DbDHneLR.mjs.map +1 -1
  56. package/dist/ready-mR96orkJ.cjs.map +1 -1
  57. package/dist/reconcile/public.d.cts +1 -1
  58. package/dist/reconcile/public.d.mts +1 -1
  59. package/dist/{types-D7j6QWeX.d.mts → types-CipIIm8f.d.mts} +34 -2
  60. package/dist/{types-D7j6QWeX.d.mts.map → types-CipIIm8f.d.mts.map} +1 -1
  61. package/dist/{types-JV4XOY01.d.cts → types-D9b-IYdW.d.cts} +34 -2
  62. package/dist/{types-JV4XOY01.d.cts.map → types-D9b-IYdW.d.cts.map} +1 -1
  63. package/dist/workspace/index.cjs +1 -1
  64. package/dist/workspace/index.d.cts +2 -2
  65. package/dist/workspace/index.d.mts +2 -2
  66. package/dist/workspace/index.mjs +1 -1
  67. package/dist/{workspace-Bah36xSG.cjs → workspace-B64Xk1Mb.cjs} +14 -4
  68. package/dist/workspace-B64Xk1Mb.cjs.map +1 -0
  69. package/dist/{workspace-D-f58jsP.mjs → workspace-BRZ14o-o.mjs} +9 -6
  70. package/dist/{workspace-D-f58jsP.mjs.map → workspace-BRZ14o-o.mjs.map} +1 -1
  71. package/dist/{workspace-BvNXAXQI.mjs → workspace-Bp1xVaOz.mjs} +14 -4
  72. package/dist/workspace-Bp1xVaOz.mjs.map +1 -0
  73. package/dist/{workspace-B14L2TrY.cjs → workspace-Dbn07ILl.cjs} +7 -4
  74. package/dist/{workspace-B14L2TrY.cjs.map → workspace-Dbn07ILl.cjs.map} +1 -1
  75. package/package.json +13 -13
  76. package/dist/LocalStateProvider-7HR6SS2t.cjs +0 -48
  77. package/dist/LocalStateProvider-7HR6SS2t.cjs.map +0 -1
  78. package/dist/LocalStateProvider-D7QyJoLM.mjs +0 -48
  79. package/dist/LocalStateProvider-D7QyJoLM.mjs.map +0 -1
  80. package/dist/LocalStateStore-BupdV-VN.mjs +0 -163
  81. package/dist/LocalStateStore-BupdV-VN.mjs.map +0 -1
  82. package/dist/LocalStateStore-DDDU0HkI.cjs +0 -163
  83. package/dist/LocalStateStore-DDDU0HkI.cjs.map +0 -1
  84. package/dist/SSMStateProvider-B0Hu1MKZ.mjs +0 -75
  85. package/dist/SSMStateProvider-B0Hu1MKZ.mjs.map +0 -1
  86. package/dist/SSMStateProvider-VrQ-yeqd.cjs +0 -74
  87. package/dist/SSMStateProvider-VrQ-yeqd.cjs.map +0 -1
  88. package/dist/config-CSOK73pY.mjs.map +0 -1
  89. package/dist/config-DdIu1O_I.cjs.map +0 -1
  90. package/dist/workspace-Bah36xSG.cjs.map +0 -1
  91. package/dist/workspace-BvNXAXQI.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-DdIu1O_I.cjs");
6
- const require_workspace = require("./workspace-Bah36xSG.cjs");
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-B14L2TrY.cjs");
9
+ const require_workspace$1 = require("./workspace-Dbn07ILl.cjs");
10
10
  const require_OpenApiTsGenerator = require("./OpenApiTsGenerator-BCWPRZwD.cjs");
11
11
  const require_openapi = require("./openapi.cjs");
12
12
  const require_ready = require("./ready-mR96orkJ.cjs");
13
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.58";
55
+ var version = "10.0.0-alpha.59";
52
56
  //#endregion
53
57
  //#region src/auth/credentials.ts
54
58
  /**
@@ -762,6 +766,621 @@ async function assignAppPorts(workspace, names, deps) {
762
766
  running
763
767
  };
764
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
+ }
765
1384
  //#endregion
766
1385
  //#region ../../node_modules/.pnpm/@hono+node-server@1.19.5_hono@4.13.8/node_modules/@hono/node-server/dist/index.mjs
767
1386
  var RequestError = class extends Error {
@@ -1427,6 +2046,7 @@ async function devCommand(options) {
1427
2046
  const runtime = config.runtime ?? "node";
1428
2047
  let secretsJsonPath;
1429
2048
  let publicUrl;
2049
+ let manifest;
1430
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);
1431
2051
  if (workspace) {
1432
2052
  const reconciled = await require_workspace$1.reconcileWorkspace(workspace, {
@@ -1441,6 +2061,7 @@ async function devCommand(options) {
1441
2061
  Object.assign(appSecrets, reconciled.env);
1442
2062
  const own = reconciled.plan.resources.find((r) => (r.kind === "rest-api" || r.kind === "site") && require_config.appKey(r.id) === workspaceAppName);
1443
2063
  publicUrl = own ? reconciled.env[own.envKey] : void 0;
2064
+ manifest = reconciled.manifest;
1444
2065
  }
1445
2066
  if (Object.keys(appSecrets).length > 0) {
1446
2067
  const secretsDir = (0, node_path.join)(secretsRoot, ".gkm");
@@ -1453,6 +2074,38 @@ async function devCommand(options) {
1453
2074
  devServer.label = workspaceAppName ?? "server";
1454
2075
  devServer.publicUrl = publicUrl;
1455
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
+ }));
1456
2109
  const hooksFile = (config.hooks?.server?.split("#"))?.[0];
1457
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(/^\.\//, ""));
1458
2111
  const resolvedFiles = await (0, fast_glob.default)(normalizedPatterns, {
@@ -1493,7 +2146,11 @@ async function devCommand(options) {
1493
2146
  if (isShuttingDown) return;
1494
2147
  isShuttingDown = true;
1495
2148
  logger$9.log("\n🛑 Shutting down...");
1496
- Promise.all([watcher.close(), devServer.stop()]).catch((err) => {
2149
+ Promise.all([
2150
+ watcher.close(),
2151
+ devServer.stop(),
2152
+ discovery.leave()
2153
+ ]).catch((err) => {
1497
2154
  logger$9.error("Error during shutdown:", err);
1498
2155
  }).finally(() => {
1499
2156
  process.exit(0);
@@ -1737,9 +2394,9 @@ async function workspaceDevCommand(configured, options) {
1737
2394
  for (const { id, port } of fakes) logger$9.log(`🎭 ${id} fake: http://localhost:${port}`);
1738
2395
  logger$9.log(options.migrate || options.seed ? "🗄️ Migrating the databases" : "🗄️ Checking for pending migrations");
1739
2396
  if (options.migrate || options.seed) {
1740
- const { prepareDevDatabases } = await Promise.resolve().then(() => require("./migrate-DzYqA-e3.cjs"));
2397
+ const { prepareDevDatabases } = await Promise.resolve().then(() => require("./migrate-CThJfVHR.cjs"));
1741
2398
  await prepareDevDatabases(workspace, reconciled.env, { seed: !!options.seed });
1742
- } else await Promise.resolve().then(() => require("./migrate-DzYqA-e3.cjs")).then(({ reportPendingMigrations }) => reportPendingMigrations(workspace, reconciled.env)).catch((error) => {
2399
+ } else await Promise.resolve().then(() => require("./migrate-CThJfVHR.cjs")).then(({ reportPendingMigrations }) => reportPendingMigrations(workspace, reconciled.env)).catch((error) => {
1743
2400
  logger$9.log(`⚠️ Could not check for pending migrations: ${error instanceof Error ? error.message : String(error)}`);
1744
2401
  });
1745
2402
  const appUrls = {};
@@ -1794,12 +2451,39 @@ async function workspaceDevCommand(configured, options) {
1794
2451
  break;
1795
2452
  }
1796
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);
1797
2480
  const turboEnv = {
1798
2481
  ...process.env,
1799
2482
  ...secretsEnv,
1800
2483
  NODE_ENV: "development",
1801
2484
  ...configPath ? { GKM_CONFIG_PATH: configPath } : {},
1802
- ...options.fake ? { [require_workspace$1.FAKE_ENV]: "1" } : {}
2485
+ ...options.fake ? { [require_workspace$1.FAKE_ENV]: "1" } : {},
2486
+ [DISCOVERY_QUIET_ENV]: "1"
1803
2487
  };
1804
2488
  logger$9.log(`🚀 Starting ${appCount} app(s) — each builds before it answers, and says so when it is ready`);
1805
2489
  const turboProcess = (0, node_child_process.spawn)("pnpm", [
@@ -1819,6 +2503,7 @@ async function workspaceDevCommand(configured, options) {
1819
2503
  isShuttingDown = true;
1820
2504
  logger$9.log("\n🛑 Shutting down workspace...");
1821
2505
  closeFakes(fakes);
2506
+ discovery.leave();
1822
2507
  const pid = turboProcess.pid;
1823
2508
  if (pid) try {
1824
2509
  process.kill(-pid, "SIGTERM");
@@ -2059,6 +2744,13 @@ start({
2059
2744
  });
2060
2745
  `;
2061
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
+ }
2062
2754
  /** Run `fn` with `console.log` muted — warnings and errors still print. */
2063
2755
  async function quietly(fn) {
2064
2756
  const log = console.log;
@@ -2156,6 +2848,12 @@ var DevServer = class {
2156
2848
  }
2157
2849
  /** What the ready line calls this app. */
2158
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
+ }
2159
2857
  /**
2160
2858
  * Where the app is reached — its HTTPS address behind the edge, when it has
2161
2859
  * one. The local port is only what the edge forwards to.
@@ -2191,9 +2889,11 @@ var DevServer = class {
2191
2889
  this.serverProcess.on("error", (error) => {
2192
2890
  logger$9.error("❌ Server error:", error);
2193
2891
  });
2194
- this.serverProcess.on("exit", (code, signal) => {
2892
+ const child = this.serverProcess;
2893
+ child.on("exit", (code, signal) => {
2195
2894
  if (code !== null && code !== 0 && signal !== "SIGTERM") logger$9.error(`❌ Server exited with code ${code}`);
2196
2895
  this.isRunning = false;
2896
+ if (this.serverProcess === child) this.onStatus?.("stopped");
2197
2897
  });
2198
2898
  await new Promise((resolve) => setTimeout(resolve, 1e3));
2199
2899
  if (this.isRunning) {
@@ -2207,6 +2907,7 @@ var DevServer = class {
2207
2907
  ...this.databaseApi ? [`db ${this.databaseApi}`] : []
2208
2908
  ];
2209
2909
  if (tools.length > 0) logger$9.log(` ${tools.join(" · ")}`);
2910
+ this.onStatus?.("ready");
2210
2911
  }
2211
2912
  }
2212
2913
  async stop() {
@@ -2232,6 +2933,7 @@ var DevServer = class {
2232
2933
  }
2233
2934
  async restart() {
2234
2935
  const portToReuse = this.actualPort;
2936
+ this.onStatus?.("reloading");
2235
2937
  await this.stop();
2236
2938
  let attempts = 0;
2237
2939
  while (attempts < 30) {
@@ -4313,12 +5015,6 @@ function createEmptyState(stage, projectId, environmentId) {
4313
5015
  };
4314
5016
  }
4315
5017
  /**
4316
- * Get application ID from state
4317
- */
4318
- function getApplicationId(state, appName) {
4319
- return state?.applications[appName];
4320
- }
4321
- /**
4322
5018
  * Set application ID in state (mutates state)
4323
5019
  */
4324
5020
  function setApplicationId(state, appName, applicationId) {
@@ -7201,6 +7897,113 @@ function applicationName(identity, app) {
7201
7897
  return slug$1(app) === identity.project ? `${identity.stage}-${identity.scope}` : (0, _geekmidas_manifest.scopedName)([identity.stage, identity.scope], app);
7202
7898
  }
7203
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;
7915
+ }
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
+ });
7941
+ }
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
7204
8007
  //#region src/deploy/ownership.ts
7205
8008
  /**
7206
8009
  * Which Dokploy project a deploy is allowed to deploy into.
@@ -7256,7 +8059,7 @@ async function projectById(api, projectId) {
7256
8059
  * A project that matches by name (in any case) but carries no marker, or
7257
8060
  * another identity's, raises `ProjectNotOwned` — it is never adopted.
7258
8061
  */
7259
- async function resolveProject(api, identity, stateProjectId, log = () => {}) {
8062
+ async function resolveProject(api, identity, stateProjectId, log = () => {}, hooks = {}) {
7260
8063
  const name = projectName(identity);
7261
8064
  const marker = ownershipMarker(identity);
7262
8065
  if (stateProjectId) {
@@ -7292,6 +8095,7 @@ async function resolveProject(api, identity, stateProjectId, log = () => {}) {
7292
8095
  }
7293
8096
  const clash = projects.find((p) => p.name.toLowerCase() === name.toLowerCase());
7294
8097
  if (clash) throw new ProjectNotOwned(clash.name, clash.projectId, identity.key, markerOf(clash.description));
8098
+ await hooks.beforeCreate?.();
7295
8099
  const created = await api.createProject(name, withMarker("Deployed by gkm.", marker));
7296
8100
  return {
7297
8101
  projectId: created.project.projectId,
@@ -7394,65 +8198,6 @@ async function resolveRegistry(api, options) {
7394
8198
  return options.create(registry);
7395
8199
  }
7396
8200
  //#endregion
7397
- //#region src/deploy/StateProvider.ts
7398
- /**
7399
- * Check if value is a StateProvider implementation.
7400
- */
7401
- function isStateProvider(value) {
7402
- return typeof value === "object" && value !== null && typeof value.read === "function" && typeof value.write === "function";
7403
- }
7404
- /**
7405
- * Create a state provider based on configuration.
7406
- *
7407
- * - 'local': LocalStateProvider (default)
7408
- * - 'ssm': CachedStateProvider with SSM as source of truth
7409
- * - 's3': S3StateStore behind the provider interface
7410
- * - Custom: Use provided StateProvider implementation
7411
- *
7412
- * Deploy moves to `createStateStore` (locks, versions, per-resource records);
7413
- * this stays for the call sites that read and write whole states.
7414
- */
7415
- async function createStateProvider(options) {
7416
- const { config, workspaceRoot, workspaceName } = options;
7417
- if (!config) {
7418
- const { LocalStateProvider } = await Promise.resolve().then(() => require("./LocalStateProvider-7HR6SS2t.cjs"));
7419
- return new LocalStateProvider(workspaceRoot);
7420
- }
7421
- const { isStateStore } = await Promise.resolve().then(() => StateStore_exports);
7422
- if (isStateStore(config.provider)) {
7423
- const { StateStoreProvider } = await Promise.resolve().then(() => StateStore_exports);
7424
- return new StateStoreProvider(config.provider);
7425
- }
7426
- if (isStateProvider(config.provider)) return config.provider;
7427
- const provider = config.provider;
7428
- if (provider === "local") {
7429
- const { LocalStateProvider } = await Promise.resolve().then(() => require("./LocalStateProvider-7HR6SS2t.cjs"));
7430
- return new LocalStateProvider(workspaceRoot);
7431
- }
7432
- if (provider === "ssm") {
7433
- if (!workspaceName) {
7434
- const { StateStoreNeedsWorkspaceName } = await Promise.resolve().then(() => StateStore_exports);
7435
- throw new StateStoreNeedsWorkspaceName("ssm");
7436
- }
7437
- const { LocalStateProvider } = await Promise.resolve().then(() => require("./LocalStateProvider-7HR6SS2t.cjs"));
7438
- const { SSMStateProvider } = await Promise.resolve().then(() => require("./SSMStateProvider-VrQ-yeqd.cjs"));
7439
- const { CachedStateProvider } = await Promise.resolve().then(() => CachedStateProvider_exports);
7440
- const ssmConfig = config;
7441
- const local = new LocalStateProvider(workspaceRoot);
7442
- return new CachedStateProvider(SSMStateProvider.create({
7443
- workspaceName,
7444
- region: ssmConfig.region,
7445
- profile: ssmConfig.profile
7446
- }), local);
7447
- }
7448
- if (provider === "s3") {
7449
- const { createStateStore, StateStoreProvider } = await Promise.resolve().then(() => StateStore_exports);
7450
- return new StateStoreProvider(await createStateStore(options));
7451
- }
7452
- const { UnknownStateProvider } = await Promise.resolve().then(() => StateStore_exports);
7453
- throw new UnknownStateProvider(config);
7454
- }
7455
- //#endregion
7456
8201
  //#region src/deploy/secrets.ts
7457
8202
  /**
7458
8203
  * Filter secrets to only include the env vars that an app requires.
@@ -8193,6 +8938,28 @@ async function workspaceDeployCommand(configured, options) {
8193
8938
  });
8194
8939
  if (dokployApps.length === 0) throw new Error("No apps to deploy. All selected apps have unsupported deploy targets.");
8195
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 }) {
8196
8963
  logger$4.log("\n🔐 Loading secrets and analyzing environment requirements...");
8197
8964
  const secretsStore = await require_store.secretsStoreFor(workspace, stage);
8198
8965
  const stored = await secretsStore.read(stage);
@@ -8234,42 +9001,48 @@ async function workspaceDeployCommand(configured, options) {
8234
9001
  token: creds.token
8235
9002
  });
8236
9003
  logger$4.log("\n📋 Loading deploy state...");
8237
- const stateProvider = await createStateProvider({
8238
- config: workspace.state,
8239
- workspaceRoot: workspace.root,
8240
- workspaceName: workspace.name
8241
- });
8242
- const previous = await stateProvider.read(stage);
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`);
8243
9010
  logger$4.log("\n📁 Setting up Dokploy project...");
8244
9011
  logger$4.log(` Identity: ${identity.key}`);
8245
- const project = await resolveProject(api, identity, previous?.projectId, (message) => logger$4.log(message));
9012
+ const projectEntry = {
9013
+ key: "project",
9014
+ type: "project",
9015
+ data: {
9016
+ name: projectName(identity),
9017
+ identity: identity.key
9018
+ }
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);
8246
9022
  if (project.via === "created") logger$4.log(` ✓ Created project: ${project.projectId}`);
8247
9023
  else logger$4.log(` Found existing project: ${project.name}`);
8248
- let environmentId;
8249
- const matchingEnv = project.environments.find((e) => e.name.toLowerCase() === stage.toLowerCase());
8250
- if (matchingEnv) {
8251
- environmentId = matchingEnv.environmentId;
8252
- logger$4.log(` Using environment: ${matchingEnv.name}`);
8253
- } else {
8254
- logger$4.log(` Creating "${stage}" environment...`);
8255
- environmentId = (await api.createEnvironment(project.projectId, stage)).environmentId;
8256
- logger$4.log(` ✓ Created environment: ${stage}`);
8257
- }
8258
- let state = previous;
8259
- if (state) {
8260
- logger$4.log(` Found existing state for stage "${stage}"`);
8261
- if (state.projectId !== project.projectId) {
8262
- logger$4.log(` ⚠ Project ID changed, updating state`);
8263
- state.projectId = project.projectId;
8264
- }
8265
- if (state.environmentId !== environmentId) {
8266
- logger$4.log(` ⚠ Environment ID changed, updating state`);
8267
- state.environmentId = environmentId;
8268
- }
8269
- } else {
8270
- logger$4.log(` Creating new state for stage "${stage}"`);
8271
- state = createEmptyState(stage, project.projectId, environmentId);
8272
- }
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: () => {
9032
+ logger$4.log(` Creating "${stage}" environment...`);
9033
+ return api.createEnvironment(project.projectId, stage);
9034
+ },
9035
+ id: (e) => e.environmentId
9036
+ });
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;
8273
9046
  state.identity = identity.key;
8274
9047
  logger$4.log("\n🐳 Checking registry...");
8275
9048
  const registry = workspace.deploy.dokploy?.registry;
@@ -8291,6 +9064,7 @@ async function workspaceDeployCommand(configured, options) {
8291
9064
  const registryId = dokployRegistry.registryId;
8292
9065
  state.registryId = registryId;
8293
9066
  logger$4.log(` Using registry: ${dokployRegistry.registryName}`);
9067
+ await journal.save();
8294
9068
  const skippedMobileApps = appsToDeployNames.filter((name) => workspace.apps[name].type === "mobile");
8295
9069
  if (skippedMobileApps.length > 0) logger$4.log(`\n📱 Skipping ${skippedMobileApps.length} mobile app(s) — deploy via framework toolchain: ${skippedMobileApps.join(", ")}`);
8296
9070
  const backendApps = appsToDeployNames.filter((name) => workspace.apps[name].type === "backend");
@@ -8348,7 +9122,7 @@ async function workspaceDeployCommand(configured, options) {
8348
9122
  const backupSchedule = workspace.deploy.backups.schedule ?? "0 2 * * *";
8349
9123
  const backupRetention = workspace.deploy.backups.retention ?? 30;
8350
9124
  logger$4.log(" Creating postgres backup schedule...");
8351
- const backup = await api.createPostgresBackup({
9125
+ setPostgresBackupId(state, (await api.createPostgresBackup({
8352
9126
  schedule: backupSchedule,
8353
9127
  prefix: `${stage}/postgres`,
8354
9128
  destinationId: backupState.destinationId,
@@ -8356,8 +9130,7 @@ async function workspaceDeployCommand(configured, options) {
8356
9130
  postgresId: backupCluster.postgresId,
8357
9131
  enabled: true,
8358
9132
  keepLatestCount: backupRetention
8359
- });
8360
- setPostgresBackupId(state, backup.backupId);
9133
+ })).backupId);
8361
9134
  logger$4.log(` ✓ Postgres backup schedule created (${backupSchedule})`);
8362
9135
  } else logger$4.log(" ✓ Using existing postgres backup schedule");
8363
9136
  }
@@ -8378,22 +9151,7 @@ async function workspaceDeployCommand(configured, options) {
8378
9151
  const app = workspace.apps[appName];
8379
9152
  logger$4.log(`\n ⚙️ Deploying ${appName}...`);
8380
9153
  try {
8381
- const dokployAppName = applicationName(identity, appName);
8382
- let application = null;
8383
- const cachedAppId = getApplicationId(state, appName);
8384
- if (cachedAppId) {
8385
- logger$4.log(` Using cached ID: ${cachedAppId}`);
8386
- application = await api.getApplication(cachedAppId);
8387
- if (application) logger$4.log(` ✓ Application found: ${application.applicationId}`);
8388
- else logger$4.log(` ⚠ Cached ID invalid, will create new`);
8389
- }
8390
- if (!application) {
8391
- const result = await api.findOrCreateApplication(dokployAppName, project.projectId, environmentId);
8392
- application = result.application;
8393
- if (result.created) logger$4.log(` Created application: ${application.applicationId}`);
8394
- else logger$4.log(` Found existing application: ${application.applicationId}`);
8395
- }
8396
- setApplicationId(state, appName, application.applicationId);
9154
+ const application = await ensureApplication(api, journal, appName, applicationName(identity, appName), project.projectId, environmentId);
8397
9155
  const appSecrets = encryptedSecrets.get(appName);
8398
9156
  const credentials = appSecrets && appSecrets.secretCount > 0 ? {
8399
9157
  encrypted: appSecrets.payload.encrypted,
@@ -8417,6 +9175,7 @@ async function workspaceDeployCommand(configured, options) {
8417
9175
  ref: built.imageRef ?? imageRef$1,
8418
9176
  ...built.digest ? { digest: built.digest } : {}
8419
9177
  });
9178
+ await journal.save();
8420
9179
  const backendHost = resolveHost(appName, app, stage, workspace.deploy?.domains, false);
8421
9180
  const dependencyUrls = {};
8422
9181
  if (app.dependencies) {
@@ -8451,30 +9210,10 @@ async function workspaceDeployCommand(configured, options) {
8451
9210
  await api.saveApplicationEnv(application.applicationId, envVars.join("\n"));
8452
9211
  logger$4.log(` Deploying to Dokploy...`);
8453
9212
  await api.deployApplication(application.applicationId);
8454
- const existingDomain = (await api.getDomainsByApplicationId(application.applicationId)).find((d) => d.host === backendHost);
8455
- if (existingDomain) {
8456
- appHostnames.set(appName, backendHost);
8457
- appDomainIds.set(appName, existingDomain.domainId);
8458
- publicUrls[appName] = `https://${backendHost}`;
8459
- logger$4.log(` ✓ Domain: https://${backendHost} (existing)`);
8460
- } else try {
8461
- const domain = await api.createDomain({
8462
- host: backendHost,
8463
- port: app.port,
8464
- https: true,
8465
- certificateType: "letsencrypt",
8466
- applicationId: application.applicationId
8467
- });
8468
- appHostnames.set(appName, backendHost);
8469
- appDomainIds.set(appName, domain.domainId);
8470
- publicUrls[appName] = `https://${backendHost}`;
8471
- logger$4.log(` ✓ Domain: https://${backendHost} (created)`);
8472
- } catch (domainError) {
8473
- const message = domainError instanceof Error ? domainError.message : "Unknown error";
8474
- logger$4.log(` ⚠ Domain creation failed: ${message}`);
8475
- appHostnames.set(appName, backendHost);
8476
- publicUrls[appName] = `https://${backendHost}`;
8477
- }
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}`;
8478
9217
  results.push({
8479
9218
  appName,
8480
9219
  type: app.type,
@@ -8503,22 +9242,7 @@ async function workspaceDeployCommand(configured, options) {
8503
9242
  const app = workspace.apps[appName];
8504
9243
  logger$4.log(`\n 🌐 Deploying ${appName}...`);
8505
9244
  try {
8506
- const dokployAppName = applicationName(identity, appName);
8507
- let application = null;
8508
- const cachedAppId = getApplicationId(state, appName);
8509
- if (cachedAppId) {
8510
- logger$4.log(` Using cached ID: ${cachedAppId}`);
8511
- application = await api.getApplication(cachedAppId);
8512
- if (application) logger$4.log(` ✓ Application found: ${application.applicationId}`);
8513
- else logger$4.log(` ⚠ Cached ID invalid, will create new`);
8514
- }
8515
- if (!application) {
8516
- const result = await api.findOrCreateApplication(dokployAppName, project.projectId, environmentId);
8517
- application = result.application;
8518
- if (result.created) logger$4.log(` Created application: ${application.applicationId}`);
8519
- else logger$4.log(` Found existing application: ${application.applicationId}`);
8520
- }
8521
- setApplicationId(state, appName, application.applicationId);
9245
+ const application = await ensureApplication(api, journal, appName, applicationName(identity, appName), project.projectId, environmentId);
8522
9246
  const dependencyUrls = {};
8523
9247
  if (app.dependencies) {
8524
9248
  for (const dep of app.dependencies) if (publicUrls[dep]) dependencyUrls[dep] = publicUrls[dep];
@@ -8566,6 +9290,7 @@ async function workspaceDeployCommand(configured, options) {
8566
9290
  ref: built.imageRef ?? imageRef$2,
8567
9291
  ...built.digest ? { digest: built.digest } : {}
8568
9292
  });
9293
+ await journal.save();
8569
9294
  const envVars = [
8570
9295
  `NODE_ENV=production`,
8571
9296
  `PORT=${app.port}`,
@@ -8576,30 +9301,10 @@ async function workspaceDeployCommand(configured, options) {
8576
9301
  await api.saveApplicationEnv(application.applicationId, envVars.join("\n"));
8577
9302
  logger$4.log(` Deploying to Dokploy...`);
8578
9303
  await api.deployApplication(application.applicationId);
8579
- const existingFrontendDomain = (await api.getDomainsByApplicationId(application.applicationId)).find((d) => d.host === frontendHost);
8580
- if (existingFrontendDomain) {
8581
- appHostnames.set(appName, frontendHost);
8582
- appDomainIds.set(appName, existingFrontendDomain.domainId);
8583
- publicUrls[appName] = `https://${frontendHost}`;
8584
- logger$4.log(` ✓ Domain: https://${frontendHost} (existing)`);
8585
- } else try {
8586
- const domain = await api.createDomain({
8587
- host: frontendHost,
8588
- port: app.port,
8589
- https: true,
8590
- certificateType: "letsencrypt",
8591
- applicationId: application.applicationId
8592
- });
8593
- appHostnames.set(appName, frontendHost);
8594
- appDomainIds.set(appName, domain.domainId);
8595
- publicUrls[appName] = `https://${frontendHost}`;
8596
- logger$4.log(` ✓ Domain: https://${frontendHost} (created)`);
8597
- } catch (domainError) {
8598
- const message = domainError instanceof Error ? domainError.message : "Unknown error";
8599
- logger$4.log(` ⚠ Domain creation failed: ${message}`);
8600
- appHostnames.set(appName, frontendHost);
8601
- publicUrls[appName] = `https://${frontendHost}`;
8602
- }
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}`;
8603
9308
  results.push({
8604
9309
  appName,
8605
9310
  type: app.type,
@@ -8622,14 +9327,14 @@ async function workspaceDeployCommand(configured, options) {
8622
9327
  }
8623
9328
  }
8624
9329
  logger$4.log("\n📋 Saving deploy state...");
8625
- await stateProvider.write(stage, state);
9330
+ await journal.save();
8626
9331
  logger$4.log(" ✓ State saved");
8627
9332
  const dnsConfig = workspace.deploy.dns;
8628
9333
  if (dnsConfig && appHostnames.size > 0) {
8629
9334
  const dnsResult = await orchestrateDns(appHostnames, dnsConfig, creds.endpoint);
8630
9335
  if (dnsResult?.serverIp && appHostnames.size > 0) {
8631
9336
  await verifyDnsRecords(appHostnames, dnsResult.serverIp, state);
8632
- await stateProvider.write(stage, state);
9337
+ await journal.save();
8633
9338
  }
8634
9339
  if (dnsResult?.success && appHostnames.size > 0) {
8635
9340
  logger$4.log("\n🔒 Validating domains for SSL certificates...");
@@ -8662,6 +9367,71 @@ async function workspaceDeployCommand(configured, options) {
8662
9367
  };
8663
9368
  }
8664
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
+ /**
8665
9435
  * Main deploy command
8666
9436
  */
8667
9437
  async function deployCommand(options) {
@@ -8798,453 +9568,160 @@ async function deployListCommand(options) {
8798
9568
  }
8799
9569
  }
8800
9570
  //#endregion
8801
- //#region src/deploy/CachedStateProvider.ts
8802
- var CachedStateProvider_exports = /* @__PURE__ */ require_rolldown_runtime.__exportAll({ CachedStateProvider: () => CachedStateProvider });
8803
- /**
8804
- * Cached state provider that wraps a remote provider with local cache.
8805
- *
8806
- * @deprecated for deploys: a read prefers the local copy, so a stale one wins
8807
- * over the remote without warning, and a write overwrites the remote blindly.
8808
- * Deploy uses `createStateStore`, which reads SSM directly and writes
8809
- * conditionally. Kept for `gkm state:pull | push | diff`.
8810
- */
8811
- var CachedStateProvider = class {
8812
- remote;
8813
- local;
8814
- constructor(remote, local) {
8815
- this.remote = remote;
8816
- this.local = local;
8817
- }
8818
- async read(stage) {
8819
- const localState = await this.local.read(stage);
8820
- if (localState) return localState;
8821
- const remoteState = await this.remote.read(stage);
8822
- if (remoteState) await this.local.write(stage, remoteState);
8823
- return remoteState;
8824
- }
8825
- async write(stage, state) {
8826
- await this.remote.write(stage, state);
8827
- await this.local.write(stage, state);
8828
- }
8829
- /**
8830
- * Force pull from remote to local.
8831
- * Used by `gkm state pull` command.
8832
- */
8833
- async pull(stage) {
8834
- const remoteState = await this.remote.read(stage);
8835
- if (remoteState) await this.local.write(stage, remoteState);
8836
- return remoteState;
8837
- }
8838
- /**
8839
- * Force push from local to remote.
8840
- * Used by `gkm state push` command.
8841
- */
8842
- async push(stage) {
8843
- const localState = await this.local.read(stage);
8844
- if (localState) await this.remote.write(stage, localState);
8845
- return localState;
8846
- }
8847
- /**
8848
- * Get both local and remote state for comparison.
8849
- * Used by `gkm state diff` command.
8850
- */
8851
- async diff(stage) {
8852
- const [local, remote] = await Promise.all([this.local.read(stage), this.remote.read(stage)]);
8853
- return {
8854
- local,
8855
- remote
8856
- };
8857
- }
8858
- };
8859
- //#endregion
8860
- //#region src/deploy/StateStore.ts
9571
+ //#region src/deploy/LocalStateStore.ts
8861
9572
  /**
8862
- * Deploy state store.
9573
+ * Local filesystem state store.
8863
9574
  *
8864
- * `StateProvider` reads and writes a whole stage at once, with nothing to stop
8865
- * two runs of the same stage from overwriting each other and nowhere to record
8866
- * a resource the moment it exists. A `StateStore` adds the three things a
8867
- * deploy needs to be safe against crashes and concurrent runs:
8868
- *
8869
- * - `lock(stage)`: one run per stage. A second gets `StateLocked`, naming the
8870
- * holder, instead of racing the first.
8871
- * - Versioned `read` and conditional `write`: a write names the version it was
8872
- * based on, and raises `StateVersionConflict` when the stored state moved on
8873
- * underneath it, so nothing is lost silently even without the lock.
8874
- * - Per-resource records: `putResource` journals one resource at a time, so a
8875
- * run that dies half way leaves behind the ids of everything it created.
8876
- *
8877
- * Built-in stores: `LocalStateStore`, `SSMStateStore`, `S3StateStore`. A
8878
- * custom `StateProvider` is wrapped by `LegacyStateStore`, which keeps it
8879
- * working but cannot lock.
8880
- */
8881
- var StateStore_exports = /* @__PURE__ */ require_rolldown_runtime.__exportAll({
8882
- DocumentStateStore: () => DocumentStateStore,
8883
- STATE_SCHEMA_VERSION: () => 2,
8884
- StageStateMissing: () => StageStateMissing,
8885
- StateLocked: () => StateLocked,
8886
- StateSchemaTooNew: () => StateSchemaTooNew,
8887
- StateStoreBusy: () => StateStoreBusy,
8888
- StateStoreNeedsWorkspaceName: () => StateStoreNeedsWorkspaceName,
8889
- StateStoreProvider: () => StateStoreProvider,
8890
- StateStoreWithoutLocking: () => StateStoreWithoutLocking,
8891
- StateUnreadable: () => StateUnreadable,
8892
- StateVersionConflict: () => StateVersionConflict,
8893
- UnknownStateProvider: () => UnknownStateProvider,
8894
- contentVersion: () => contentVersion,
8895
- createStateStore: () => createStateStore,
8896
- decodeDocument: () => decodeDocument,
8897
- encodeDocument: () => encodeDocument,
8898
- isStateStore: () => isStateStore,
8899
- migrateV1: () => migrateV1,
8900
- newLockHolder: () => newLockHolder
8901
- });
8902
- var StateLocked = class extends Error {
8903
- stage;
8904
- holder;
8905
- location;
8906
- constructor(stage, holder, location) {
8907
- 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";
8908
- 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}\`.`);
8909
- this.stage = stage;
8910
- this.holder = holder;
8911
- this.location = location;
8912
- this.name = "StateLocked";
8913
- }
8914
- };
8915
- var StateVersionConflict = class extends Error {
8916
- stage;
8917
- expectedVersion;
8918
- actualVersion;
8919
- constructor(stage, expectedVersion, actualVersion) {
8920
- 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.`);
8921
- this.stage = stage;
8922
- this.expectedVersion = expectedVersion;
8923
- this.actualVersion = actualVersion;
8924
- this.name = "StateVersionConflict";
8925
- }
8926
- };
8927
- var StageStateMissing = class extends Error {
8928
- stage;
8929
- constructor(stage) {
8930
- super(`There is no deploy state for stage '${stage}' to record a resource in. Write the stage's state first (write with expectedVersion: null).`);
8931
- this.stage = stage;
8932
- this.name = "StageStateMissing";
8933
- }
8934
- };
8935
- var StateUnreadable = class extends Error {
8936
- stage;
8937
- location;
8938
- constructor(stage, location, cause) {
8939
- 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 });
8940
- this.stage = stage;
8941
- this.location = location;
8942
- this.name = "StateUnreadable";
8943
- }
8944
- };
8945
- var StateSchemaTooNew = class extends Error {
8946
- stage;
8947
- schemaVersion;
8948
- constructor(stage, schemaVersion) {
8949
- 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.`);
8950
- this.stage = stage;
8951
- this.schemaVersion = schemaVersion;
8952
- this.name = "StateSchemaTooNew";
8953
- }
8954
- };
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;
8955
9582
  /**
8956
- * Not thrown: emitted (once per store) when a custom `StateProvider` stands
8957
- * in for a store, so a deploy that cannot be protected from a concurrent one
8958
- * 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.
8959
9585
  */
8960
- var StateStoreWithoutLocking = class extends Error {
8961
- provider;
8962
- constructor(provider) {
8963
- super(`The custom state provider '${provider}' has no lock and no versions, so two deploys of the same stage can still overwrite each other. Implement StateStore (lock, versioned read, conditional write) or use the 'local', 'ssm' or 's3' state provider.`);
8964
- this.provider = provider;
8965
- this.name = "StateStoreWithoutLocking";
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;
8966
9595
  }
8967
- };
8968
- var StateStoreNeedsWorkspaceName = class extends Error {
8969
- provider;
8970
- constructor(provider) {
8971
- super(`The '${provider}' state provider keys state by workspace name. Set "name" in gkm.config.ts.`);
8972
- this.provider = provider;
8973
- this.name = "StateStoreNeedsWorkspaceName";
9596
+ get dir() {
9597
+ return (0, node_path.join)(this.workspaceRoot, ".gkm");
8974
9598
  }
8975
- };
8976
- var UnknownStateProvider = class extends Error {
8977
- config;
8978
- constructor(config) {
8979
- super(`Unknown state provider ${JSON.stringify(config)}. Use 'local', 'ssm', 's3', or an object implementing StateStore or StateProvider.`);
8980
- this.config = config;
8981
- this.name = "UnknownStateProvider";
9599
+ statePath(stage) {
9600
+ return (0, node_path.join)(this.dir, `deploy-${stage}.json`);
8982
9601
  }
8983
- };
8984
- /** A lock or write that could not get at the state backend in time. */
8985
- var StateStoreBusy = class extends Error {
8986
- stage;
8987
- location;
8988
- constructor(stage, location) {
8989
- super(`Timed out waiting to write deploy state for stage '${stage}' (${location}). If no other gkm process is running, remove ${location}.`);
8990
- this.stage = stage;
8991
- this.location = location;
8992
- this.name = "StateStoreBusy";
9602
+ lockPath(stage) {
9603
+ return (0, node_path.join)(this.dir, `deploy-${stage}.lock`);
8993
9604
  }
8994
- };
8995
- function decodeDocument(stage, body, location) {
8996
- let parsed;
8997
- try {
8998
- parsed = JSON.parse(body);
8999
- } catch (error) {
9000
- throw new StateUnreadable(stage, location, error);
9605
+ backupPath(stage) {
9606
+ return (0, node_path.join)(this.dir, `deploy-${stage}.v1.json`);
9001
9607
  }
9002
- if (typeof parsed !== "object" || parsed === null) throw new StateUnreadable(stage, location, parsed);
9003
- const schemaVersion = parsed.schemaVersion;
9004
- if (schemaVersion === void 0) return {
9005
- schemaVersion: 1,
9006
- state: parsed
9007
- };
9008
- if (schemaVersion === 2) return {
9009
- schemaVersion: 2,
9010
- document: parsed
9011
- };
9012
- throw new StateSchemaTooNew(stage, schemaVersion);
9013
- }
9014
- function encodeDocument(document) {
9015
- return JSON.stringify(document, null, 2);
9016
- }
9017
- /**
9018
- * v1 state as a v2 document. The ids v1 kept are seeded as `ready` records,
9019
- * so the first journaled deploy after an upgrade adopts them rather than
9020
- * treating them as unknown.
9021
- */
9022
- function migrateV1(stage, state) {
9023
- const now = (/* @__PURE__ */ new Date()).toISOString();
9024
- const resources = {};
9025
- const seed = (type, name, id) => {
9026
- if (!id) return;
9027
- const key = name ? `${type}:${name}` : type;
9028
- resources[key] = {
9029
- key,
9030
- type,
9031
- id,
9032
- status: "ready",
9033
- updatedAt: now
9034
- };
9035
- };
9036
- seed("project", null, state.projectId);
9037
- seed("environment", null, state.environmentId);
9038
- for (const [app, id] of Object.entries(state.applications ?? {})) seed("application", app, id);
9039
- seed("postgres", null, state.services?.postgresId);
9040
- seed("redis", null, state.services?.redisId);
9041
- seed("backup-destination", null, state.backups?.destinationId);
9042
- return {
9043
- schemaVersion: 2,
9044
- stage,
9045
- serial: 1,
9046
- state,
9047
- resources,
9048
- updatedAt: now
9049
- };
9050
- }
9051
- function newLockHolder(options) {
9052
- let owner = "unknown";
9053
- try {
9054
- owner = (0, node_os.userInfo)().username;
9055
- } catch {}
9056
- return {
9057
- id: (0, node_crypto.randomUUID)(),
9058
- owner,
9059
- host: (0, node_os.hostname)(),
9060
- pid: process.pid,
9061
- acquiredAt: (/* @__PURE__ */ new Date()).toISOString(),
9062
- operation: options?.operation
9063
- };
9064
- }
9065
- function contentVersion(body) {
9066
- return (0, node_crypto.createHash)("sha256").update(body).digest("hex").slice(0, 16);
9067
- }
9068
- /**
9069
- * A store over a backend that can read and conditionally write one blob per
9070
- * stage, and create a lock object only if it does not exist. Migration and
9071
- * resource records are the same for every such backend, so they live here.
9072
- */
9073
- var DocumentStateStore = class {
9074
- async lock(stage, options) {
9075
- const holder = newLockHolder(options);
9076
- await this.createLock(stage, holder);
9077
- let released = false;
9078
- return {
9079
- stage,
9080
- holder,
9081
- release: async () => {
9082
- if (released) return;
9083
- released = true;
9084
- await this.removeLock(stage, holder);
9085
- }
9086
- };
9087
- }
9088
- async forceUnlock(stage) {
9089
- const holder = await this.readLock(stage);
9090
- await this.removeLock(stage, null);
9091
- return holder;
9608
+ mutexPath(stage) {
9609
+ return (0, node_path.join)(this.dir, `deploy-${stage}.json.mutex`);
9092
9610
  }
9093
- async read(stage) {
9094
- const current = await this.readDocument(stage);
9095
- if (!current) return null;
9096
- return {
9097
- state: current.document.state,
9098
- resources: current.document.resources,
9099
- version: current.version
9100
- };
9611
+ location(stage) {
9612
+ return this.statePath(stage);
9101
9613
  }
9102
- async write(stage, state, options) {
9103
- if (options.expectedVersion === null) {
9104
- const document = {
9105
- schemaVersion: 2,
9106
- stage,
9107
- serial: 1,
9108
- state,
9109
- resources: {},
9110
- 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)
9111
9620
  };
9112
- return this.writeRaw(stage, encodeDocument(document), null);
9621
+ } catch (error) {
9622
+ if (isCode(error, "ENOENT")) return null;
9623
+ throw error;
9113
9624
  }
9114
- return this.mutate(stage, options.expectedVersion, (document) => ({
9115
- ...document,
9116
- state
9117
- }));
9118
9625
  }
9119
- async putResource(stage, record, options) {
9120
- return this.mutate(stage, options?.expectedVersion, (document) => ({
9121
- ...document,
9122
- resources: {
9123
- ...document.resources,
9124
- [record.key]: {
9125
- ...record,
9126
- updatedAt: (/* @__PURE__ */ new Date()).toISOString()
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();
9127
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;
9128
9645
  }
9129
- }));
9130
- }
9131
- async deleteResource(stage, key, options) {
9132
- return this.mutate(stage, options?.expectedVersion, (document) => {
9133
- const { [key]: _removed, ...resources } = document.resources;
9134
- return {
9135
- ...document,
9136
- resources
9137
- };
9646
+ return require_StateStore.contentVersion(body);
9138
9647
  });
9139
9648
  }
9140
- async mutate(stage, expectedVersion, change) {
9141
- const current = await this.readDocument(stage);
9142
- if (!current) {
9143
- if (expectedVersion !== void 0) throw new StateVersionConflict(stage, expectedVersion, null);
9144
- throw new StageStateMissing(stage);
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;
9145
9657
  }
9146
- if (expectedVersion !== void 0 && current.version !== expectedVersion) throw new StateVersionConflict(stage, expectedVersion, current.version);
9147
- const next = change(current.document);
9148
- next.serial = current.document.serial + 1;
9149
- next.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
9150
- return this.writeRaw(stage, encodeDocument(next), current.version);
9151
9658
  }
9152
- /**
9153
- * Reads the stage's document, migrating v1 in place on the way. The
9154
- * migration is itself a conditional write: if another run migrated (or
9155
- * wrote) first, its result is read instead.
9156
- */
9157
- async readDocument(stage, attempt = 0) {
9158
- const raw = await this.readRaw(stage);
9159
- if (!raw) return null;
9160
- const decoded = decodeDocument(stage, raw.body, this.location(stage));
9161
- if (decoded.schemaVersion === 2) return {
9162
- document: decoded.document,
9163
- version: raw.version
9164
- };
9165
- await this.writeV1Backup(stage, raw.body);
9166
- const document = migrateV1(stage, decoded.state);
9659
+ async createLock(stage, holder) {
9660
+ await this.ensureDir();
9661
+ let handle;
9167
9662
  try {
9168
- return {
9169
- document,
9170
- version: await this.writeRaw(stage, encodeDocument(document), raw.version)
9171
- };
9663
+ handle = await (0, node_fs_promises.open)(this.lockPath(stage), "wx", FILE_MODE);
9172
9664
  } catch (error) {
9173
- if (error instanceof StateVersionConflict && attempt < 2) return this.readDocument(stage, attempt + 1);
9665
+ if (isCode(error, "EEXIST")) throw new require_StateStore.StateLocked(stage, await this.readLock(stage), this.lockPath(stage));
9174
9666
  throw error;
9175
9667
  }
9668
+ try {
9669
+ await handle.writeFile(JSON.stringify(holder, null, 2));
9670
+ } finally {
9671
+ await handle.close();
9672
+ }
9176
9673
  }
9177
- };
9178
- function isStateStore(value) {
9179
- return typeof value === "object" && value !== null && typeof value.lock === "function" && typeof value.read === "function" && typeof value.write === "function" && typeof value.putResource === "function";
9180
- }
9181
- /**
9182
- * The store for a workspace's `state` config — the same config
9183
- * `createStateProvider` reads:
9184
- *
9185
- * - none / `'local'`: `LocalStateStore` under `.gkm/`
9186
- * - `'ssm'`: `SSMStateStore`, with no local cache — SSM is read directly, so
9187
- * a stale local copy can never win over it
9188
- * - `'s3'`: `S3StateStore`
9189
- * - a `StateStore` object: used as is
9190
- * - a `StateProvider` object: wrapped by `LegacyStateStore`
9191
- */
9192
- async function createStateStore(options) {
9193
- const { config, workspaceRoot, workspaceName } = options;
9194
- if (!config || config.provider === "local") {
9195
- const { LocalStateStore } = await Promise.resolve().then(() => require("./LocalStateStore-DDDU0HkI.cjs"));
9196
- return new LocalStateStore(workspaceRoot);
9197
- }
9198
- if (isStateStore(config.provider)) return config.provider;
9199
- if (isStateProvider(config.provider)) {
9200
- const { LegacyStateStore } = await Promise.resolve().then(() => require("./LegacyStateStore-C_OFs1gN.cjs"));
9201
- return new LegacyStateStore(config.provider, { warn: options.warn });
9202
- }
9203
- if (config.provider === "ssm") {
9204
- if (!workspaceName) throw new StateStoreNeedsWorkspaceName("ssm");
9205
- const { SSMStateStore } = await Promise.resolve().then(() => require("./SSMStateStore-DC43R7zG.cjs"));
9206
- const ssm = config;
9207
- return SSMStateStore.create({
9208
- workspaceName,
9209
- region: ssm.region,
9210
- profile: ssm.profile
9211
- });
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
+ }
9212
9680
  }
9213
- if (config.provider === "s3") {
9214
- if (!workspaceName) throw new StateStoreNeedsWorkspaceName("s3");
9215
- const { S3StateStore } = await Promise.resolve().then(() => require("./S3StateStore-C7eBae0G.cjs"));
9216
- const s3 = config;
9217
- return S3StateStore.create({
9218
- workspaceName,
9219
- bucket: s3.bucket,
9220
- prefix: s3.prefix,
9221
- region: s3.region,
9222
- profile: s3.profile
9223
- });
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 });
9224
9686
  }
9225
- throw new UnknownStateProvider(config);
9226
- }
9227
- /**
9228
- * A store behind the `StateProvider` interface, for the call sites that still
9229
- * read and write whole states (deploy, until it journals). A write is still
9230
- * conditional on the version this provider last read.
9231
- */
9232
- var StateStoreProvider = class {
9233
- store;
9234
- versions = /* @__PURE__ */ new Map();
9235
- constructor(store) {
9236
- this.store = store;
9687
+ async ensureDir() {
9688
+ await (0, node_fs_promises.mkdir)(this.dir, {
9689
+ recursive: true,
9690
+ mode: 448
9691
+ });
9237
9692
  }
9238
- async read(stage) {
9239
- const stored = await this.store.read(stage);
9240
- this.versions.set(stage, stored?.version ?? null);
9241
- return stored?.state ?? null;
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
+ }
9242
9718
  }
9243
- async write(stage, state) {
9244
- const expectedVersion = this.versions.has(stage) ? this.versions.get(stage) ?? null : (await this.store.read(stage))?.version ?? null;
9245
- state.lastDeployedAt = (/* @__PURE__ */ new Date()).toISOString();
9246
- const version = await this.store.write(stage, state, { expectedVersion });
9247
- this.versions.set(stage, version);
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
+ }
9248
9725
  }
9249
9726
  };
9250
9727
  //#endregion
@@ -9252,7 +9729,12 @@ var StateStoreProvider = class {
9252
9729
  /**
9253
9730
  * State Management CLI Commands
9254
9731
  *
9255
- * Commands for managing deployment state across local and remote providers.
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.
9256
9738
  */
9257
9739
  /** What `state:show` prints in place of a secret. */
9258
9740
  const MASKED = "********";
@@ -9278,26 +9760,12 @@ function maskStateSecrets(state) {
9278
9760
  */
9279
9761
  async function statePullCommand(options) {
9280
9762
  const { workspace } = await require_config.loadWorkspaceConfig();
9281
- if (!workspace.state || workspace.state.provider === "local") {
9282
- console.error("No remote state provider configured.");
9283
- console.error("Add a remote provider in gkm.config.ts:");
9284
- console.error(" state: { provider: \"ssm\", region: \"us-east-1\" }");
9285
- process.exit(1);
9286
- }
9287
- const provider = await createStateProvider({
9288
- config: workspace.state,
9289
- workspaceRoot: workspace.root,
9290
- workspaceName: workspace.name
9291
- });
9292
- if (!(provider instanceof CachedStateProvider)) {
9293
- console.error("State provider does not support pull operation.");
9294
- process.exit(1);
9295
- }
9763
+ const { local, remote } = await remoteAndLocal(workspace);
9296
9764
  console.log(`Pulling state for stage: ${options.stage}...`);
9297
- const state = await provider.pull(options.stage);
9298
- if (state) {
9765
+ const pulled = await copyUnderLock(remote, local, local, options.stage, "state:pull");
9766
+ if (pulled) {
9299
9767
  console.log("State pulled successfully.");
9300
- printStateSummary(state);
9768
+ printStateSummary(pulled.state);
9301
9769
  } else console.log("No remote state found for this stage.");
9302
9770
  }
9303
9771
  /**
@@ -9306,26 +9774,12 @@ async function statePullCommand(options) {
9306
9774
  */
9307
9775
  async function statePushCommand(options) {
9308
9776
  const { workspace } = await require_config.loadWorkspaceConfig();
9309
- if (!workspace.state || workspace.state.provider === "local") {
9310
- console.error("No remote state provider configured.");
9311
- console.error("Add a remote provider in gkm.config.ts:");
9312
- console.error(" state: { provider: \"ssm\", region: \"us-east-1\" }");
9313
- process.exit(1);
9314
- }
9315
- const provider = await createStateProvider({
9316
- config: workspace.state,
9317
- workspaceRoot: workspace.root,
9318
- workspaceName: workspace.name
9319
- });
9320
- if (!(provider instanceof CachedStateProvider)) {
9321
- console.error("State provider does not support push operation.");
9322
- process.exit(1);
9323
- }
9777
+ const { local, remote } = await remoteAndLocal(workspace);
9324
9778
  console.log(`Pushing state for stage: ${options.stage}...`);
9325
- const state = await provider.push(options.stage);
9326
- if (state) {
9779
+ const pushed = await copyUnderLock(local, remote, remote, options.stage, "state:push");
9780
+ if (pushed) {
9327
9781
  console.log("State pushed successfully.");
9328
- printStateSummary(state);
9782
+ printStateSummary(pushed.state);
9329
9783
  } else console.log("No local state found for this stage.");
9330
9784
  }
9331
9785
  /**
@@ -9334,7 +9788,7 @@ async function statePushCommand(options) {
9334
9788
  */
9335
9789
  async function stateShowCommand(options) {
9336
9790
  const { workspace } = await require_config.loadWorkspaceConfig();
9337
- const stored = await (await createStateProvider({
9791
+ const stored = await (await require_StateStore.createStateStore({
9338
9792
  config: workspace.state,
9339
9793
  workspaceRoot: workspace.root,
9340
9794
  workspaceName: workspace.name
@@ -9343,9 +9797,12 @@ async function stateShowCommand(options) {
9343
9797
  console.log(`No state found for stage: ${options.stage}`);
9344
9798
  return;
9345
9799
  }
9346
- const state = maskStateSecrets(stored);
9800
+ const state = maskStateSecrets(stored.state);
9347
9801
  if (options.json) console.log(JSON.stringify(state, null, 2));
9348
- else printStateDetails(state);
9802
+ else {
9803
+ printStateDetails(state);
9804
+ printUnfinished(stored.resources);
9805
+ }
9349
9806
  }
9350
9807
  /**
9351
9808
  * Release a stage's deploy lock, whoever holds it.
@@ -9357,7 +9814,7 @@ async function stateShowCommand(options) {
9357
9814
  */
9358
9815
  async function stateUnlockCommand(options) {
9359
9816
  const { workspace } = await require_config.loadWorkspaceConfig();
9360
- const holder = await (await createStateStore({
9817
+ const holder = await (await require_StateStore.createStateStore({
9361
9818
  config: workspace.state,
9362
9819
  workspaceRoot: workspace.root,
9363
9820
  workspaceName: workspace.name
@@ -9374,22 +9831,11 @@ async function stateUnlockCommand(options) {
9374
9831
  */
9375
9832
  async function stateDiffCommand(options) {
9376
9833
  const { workspace } = await require_config.loadWorkspaceConfig();
9377
- if (!workspace.state || workspace.state.provider === "local") {
9378
- console.error("No remote state provider configured.");
9379
- console.error("Diff requires a remote provider to compare against.");
9380
- process.exit(1);
9381
- }
9382
- const provider = await createStateProvider({
9383
- config: workspace.state,
9384
- workspaceRoot: workspace.root,
9385
- workspaceName: workspace.name
9386
- });
9387
- if (!(provider instanceof CachedStateProvider)) {
9388
- console.error("State provider does not support diff operation.");
9389
- process.exit(1);
9390
- }
9834
+ const stores = await remoteAndLocal(workspace, ["Diff requires a remote provider to compare against."]);
9391
9835
  console.log(`Comparing state for stage: ${options.stage}...\n`);
9392
- const { local, remote } = await provider.diff(options.stage);
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;
9393
9839
  if (!local && !remote) {
9394
9840
  console.log("No state found (local or remote).");
9395
9841
  return;
@@ -9425,6 +9871,71 @@ async function stateDiffCommand(options) {
9425
9871
  else console.log(` ${key}: ${localVal ?? "(none)"} (local) != ${remoteVal ?? "(none)"} (remote)`);
9426
9872
  }
9427
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}`);
9428
9939
  }
9429
9940
  function printStateSummary(state) {
9430
9941
  const appCount = Object.keys(state.applications).length;
@@ -10472,25 +10983,25 @@ function loadPackageJson() {
10472
10983
  * Run: pnpm --filter @geekmidas/cli sync-versions
10473
10984
  */
10474
10985
  const GEEKMIDAS_VERSIONS = {
10475
- "@geekmidas/audit": "~10.0.0-alpha.58",
10476
- "@geekmidas/auth": "~10.0.0-alpha.58",
10477
- "@geekmidas/cache": "~10.0.0-alpha.58",
10478
- "@geekmidas/client": "~10.0.0-alpha.58",
10479
- "@geekmidas/cloud": "~10.0.0-alpha.58",
10480
- "@geekmidas/constructs": "~10.0.0-alpha.58",
10481
- "@geekmidas/db": "~10.0.0-alpha.58",
10482
- "@geekmidas/emailkit": "~10.0.0-alpha.58",
10483
- "@geekmidas/envkit": "~10.0.0-alpha.58",
10484
- "@geekmidas/errors": "~10.0.0-alpha.58",
10485
- "@geekmidas/events": "~10.0.0-alpha.58",
10486
- "@geekmidas/logger": "~10.0.0-alpha.58",
10487
- "@geekmidas/manifest": "~10.0.0-alpha.58",
10488
- "@geekmidas/rate-limit": "~10.0.0-alpha.58",
10489
- "@geekmidas/schema": "~10.0.0-alpha.58",
10490
- "@geekmidas/services": "~10.0.0-alpha.58",
10491
- "@geekmidas/storage": "~10.0.0-alpha.58",
10492
- "@geekmidas/telescope": "~10.0.0-alpha.58",
10493
- "@geekmidas/testkit": "~10.0.0-alpha.58",
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",
10494
11005
  "@geekmidas/cli": `~${loadPackageJson().version}`
10495
11006
  };
10496
11007
  //#endregion
@@ -16372,7 +16883,7 @@ function testDatabases(reconciled) {
16372
16883
  * starts without the reference data the app needs.
16373
16884
  */
16374
16885
  async function migrateTestStage(workspace, env) {
16375
- const { migrateAndSeed } = await Promise.resolve().then(() => require("./migrate-DzYqA-e3.cjs"));
16886
+ const { migrateAndSeed } = await Promise.resolve().then(() => require("./migrate-CThJfVHR.cjs"));
16376
16887
  const { discover } = await Promise.resolve().then(() => require("./discover-DXNviAh-.cjs")).then((n) => n.discover_exports);
16377
16888
  const sources = {};
16378
16889
  const manifest = await discover({
@@ -16803,7 +17314,7 @@ program.command("migrate").description("Apply each database construct's migratio
16803
17314
  try {
16804
17315
  const globalOptions = program.opts();
16805
17316
  if (globalOptions.cwd) process.chdir(globalOptions.cwd);
16806
- const { migrateCommand } = await Promise.resolve().then(() => require("./migrate-DzYqA-e3.cjs"));
17317
+ const { migrateCommand } = await Promise.resolve().then(() => require("./migrate-CThJfVHR.cjs"));
16807
17318
  await migrateCommand({
16808
17319
  ...options,
16809
17320
  ...construct ? { construct } : {}
@@ -16817,7 +17328,7 @@ program.command("seed").description("Migrate, then run each database construct's
16817
17328
  try {
16818
17329
  const globalOptions = program.opts();
16819
17330
  if (globalOptions.cwd) process.chdir(globalOptions.cwd);
16820
- const { seedCommand } = await Promise.resolve().then(() => require("./migrate-DzYqA-e3.cjs"));
17331
+ const { seedCommand } = await Promise.resolve().then(() => require("./migrate-CThJfVHR.cjs"));
16821
17332
  await seedCommand({
16822
17333
  ...options,
16823
17334
  ...construct ? { construct } : {}
@@ -16831,7 +17342,7 @@ program.command("migration").description("Write a construct's next migration: an
16831
17342
  try {
16832
17343
  const globalOptions = program.opts();
16833
17344
  if (globalOptions.cwd) process.chdir(globalOptions.cwd);
16834
- const { migrationCommand } = await Promise.resolve().then(() => require("./migrate-DzYqA-e3.cjs"));
17345
+ const { migrationCommand } = await Promise.resolve().then(() => require("./migrate-CThJfVHR.cjs"));
16835
17346
  await migrationCommand({
16836
17347
  construct,
16837
17348
  ...name ? { name } : {}
@@ -16929,7 +17440,7 @@ program.command("encryption:rotate").description("Add a new current key to an En
16929
17440
  try {
16930
17441
  const globalOptions = program.opts();
16931
17442
  if (globalOptions.cwd) process.chdir(globalOptions.cwd);
16932
- const { encryptionRotateCommand } = await Promise.resolve().then(() => require("./encryption-BspqzZIb.cjs"));
17443
+ const { encryptionRotateCommand } = await Promise.resolve().then(() => require("./encryption-BP8zfxEJ.cjs"));
16933
17444
  await encryptionRotateCommand(construct, options);
16934
17445
  } catch (error) {
16935
17446
  console.error(formatError(error));
@@ -16940,7 +17451,7 @@ program.command("encryption:retire").description("Remove an old key from an Encr
16940
17451
  try {
16941
17452
  const globalOptions = program.opts();
16942
17453
  if (globalOptions.cwd) process.chdir(globalOptions.cwd);
16943
- const { encryptionRetireCommand } = await Promise.resolve().then(() => require("./encryption-BspqzZIb.cjs"));
17454
+ const { encryptionRetireCommand } = await Promise.resolve().then(() => require("./encryption-BP8zfxEJ.cjs"));
16944
17455
  await encryptionRetireCommand(construct, key, options);
16945
17456
  } catch (error) {
16946
17457
  console.error(formatError(error));
@@ -16992,7 +17503,7 @@ program.command("secrets:reconcile").description("Backfill missing custom secret
16992
17503
  console.error(`No secrets found for stage "${stage}". Run "gkm secrets:init --stage ${stage}" first.`);
16993
17504
  process.exit(1);
16994
17505
  }
16995
- const { derivedContainers } = await Promise.resolve().then(() => require("./workspace-B14L2TrY.cjs")).then((n) => n.workspace_exports);
17506
+ const { derivedContainers } = await Promise.resolve().then(() => require("./workspace-Dbn07ILl.cjs")).then((n) => n.workspace_exports);
16996
17507
  const result = reconcileMissingSecrets(secrets, workspace, await derivedContainers(workspace, stage));
16997
17508
  if (!result) {
16998
17509
  console.log(`\n✓ Secrets for stage "${stage}" are up-to-date`);
@@ -17051,7 +17562,7 @@ program.command("deploy:github").description("Let GitHub Actions deploy a stage:
17051
17562
  try {
17052
17563
  const globalOptions = program.opts();
17053
17564
  if (globalOptions.cwd) process.chdir(globalOptions.cwd);
17054
- const { deployGithubCommand } = await Promise.resolve().then(() => require("./github-mQorSS36.cjs"));
17565
+ const { deployGithubCommand } = await Promise.resolve().then(() => require("./github-DHpDMBT7.cjs"));
17055
17566
  await deployGithubCommand(options);
17056
17567
  } catch (error) {
17057
17568
  console.error(formatError(error));
@@ -17186,19 +17697,18 @@ program.command("upgrade").description("Upgrade @geekmidas/cli (with --all, ever
17186
17697
  });
17187
17698
  program.parse();
17188
17699
  //#endregion
17189
- exports.DocumentStateStore = DocumentStateStore;
17190
- exports.StageStateMissing = StageStateMissing;
17191
- exports.StateLocked = StateLocked;
17192
- exports.StateStoreBusy = StateStoreBusy;
17193
- exports.StateStoreWithoutLocking = StateStoreWithoutLocking;
17194
- exports.StateVersionConflict = StateVersionConflict;
17700
+ Object.defineProperty(exports, "LocalStateStore_exports", {
17701
+ enumerable: true,
17702
+ get: function() {
17703
+ return LocalStateStore_exports;
17704
+ }
17705
+ });
17195
17706
  Object.defineProperty(exports, "appPorts_exports", {
17196
17707
  enumerable: true,
17197
17708
  get: function() {
17198
17709
  return appPorts_exports;
17199
17710
  }
17200
17711
  });
17201
- exports.contentVersion = contentVersion;
17202
17712
  Object.defineProperty(exports, "encryption_exports", {
17203
17713
  enumerable: true,
17204
17714
  get: function() {
@@ -17207,7 +17717,6 @@ Object.defineProperty(exports, "encryption_exports", {
17207
17717
  });
17208
17718
  exports.generateFullstackCustomSecrets = generateFullstackCustomSecrets;
17209
17719
  exports.getHostingerToken = getHostingerToken;
17210
- exports.newLockHolder = newLockHolder;
17211
17720
  exports.slug = slug;
17212
17721
  exports.stamp = stamp;
17213
17722