@geekmidas/cli 10.0.0-alpha.57 → 10.0.0-alpha.59

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