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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (91) hide show
  1. package/dist/{HostingerProvider-cL-6oQ5r.mjs → HostingerProvider-YQY3rMpV.mjs} +2 -2
  2. package/dist/{HostingerProvider-cL-6oQ5r.mjs.map → HostingerProvider-YQY3rMpV.mjs.map} +1 -1
  3. package/dist/{LegacyStateStore-ByRWQ9s1.mjs → LegacyStateStore-CB4ch3Dl.mjs} +2 -2
  4. package/dist/{LegacyStateStore-ByRWQ9s1.mjs.map → LegacyStateStore-CB4ch3Dl.mjs.map} +1 -1
  5. package/dist/{LegacyStateStore-C_OFs1gN.cjs → LegacyStateStore-CQJqvmXw.cjs} +7 -7
  6. package/dist/{LegacyStateStore-C_OFs1gN.cjs.map → LegacyStateStore-CQJqvmXw.cjs.map} +1 -1
  7. package/dist/{S3StateStore-FhzyENrN.mjs → S3StateStore-CXTenDTW.mjs} +2 -2
  8. package/dist/{S3StateStore-FhzyENrN.mjs.map → S3StateStore-CXTenDTW.mjs.map} +1 -1
  9. package/dist/{S3StateStore-C7eBae0G.cjs → S3StateStore-DLS4rEc0.cjs} +5 -5
  10. package/dist/{S3StateStore-C7eBae0G.cjs.map → S3StateStore-DLS4rEc0.cjs.map} +1 -1
  11. package/dist/{SSMStateStore-DC43R7zG.cjs → SSMStateStore-EqsTBIOs.cjs} +9 -9
  12. package/dist/{SSMStateStore-DC43R7zG.cjs.map → SSMStateStore-EqsTBIOs.cjs.map} +1 -1
  13. package/dist/{SSMStateStore-BjZZm-nV.mjs → SSMStateStore-hnP5deFJ.mjs} +2 -2
  14. package/dist/{SSMStateStore-BjZZm-nV.mjs.map → SSMStateStore-hnP5deFJ.mjs.map} +1 -1
  15. package/dist/StateStore-B4u0wx2t.cjs +412 -0
  16. package/dist/StateStore-B4u0wx2t.cjs.map +1 -0
  17. package/dist/StateStore-CYUfGYcx.mjs +359 -0
  18. package/dist/StateStore-CYUfGYcx.mjs.map +1 -0
  19. package/dist/{bundler-URImKj-A.mjs → bundler-ouDdNvk9.mjs} +2 -2
  20. package/dist/{bundler-URImKj-A.mjs.map → bundler-ouDdNvk9.mjs.map} +1 -1
  21. package/dist/{config-CSOK73pY.mjs → config-EuzFEkcH.mjs} +2 -2
  22. package/dist/config-EuzFEkcH.mjs.map +1 -0
  23. package/dist/{config-DdIu1O_I.cjs → config-WMOB9bLL.cjs} +2 -2
  24. package/dist/config-WMOB9bLL.cjs.map +1 -0
  25. package/dist/config.cjs +2 -2
  26. package/dist/config.d.cts +65 -3
  27. package/dist/config.d.cts.map +1 -1
  28. package/dist/config.d.mts +65 -3
  29. package/dist/config.d.mts.map +1 -1
  30. package/dist/config.mjs +2 -2
  31. package/dist/{encryption-BspqzZIb.cjs → encryption-BP8zfxEJ.cjs} +3 -3
  32. package/dist/{encryption-BspqzZIb.cjs.map → encryption-BP8zfxEJ.cjs.map} +1 -1
  33. package/dist/{encryption-Dlnl5XEU.mjs → encryption-BW7Gh51G.mjs} +3 -3
  34. package/dist/{encryption-Dlnl5XEU.mjs.map → encryption-BW7Gh51G.mjs.map} +1 -1
  35. package/dist/{github-mQorSS36.cjs → github-DHpDMBT7.cjs} +3 -3
  36. package/dist/{github-mQorSS36.cjs.map → github-DHpDMBT7.cjs.map} +1 -1
  37. package/dist/{github-BdF9tsvl.mjs → github-T_6sK3Rz.mjs} +3 -3
  38. package/dist/{github-BdF9tsvl.mjs.map → github-T_6sK3Rz.mjs.map} +1 -1
  39. package/dist/{index-BorYE_uO.d.cts → index-B5TJ-mB6.d.mts} +2 -2
  40. package/dist/{index-BorYE_uO.d.cts.map → index-B5TJ-mB6.d.mts.map} +1 -1
  41. package/dist/{index-C3JmluyO.d.mts → index-DV6j4Ydq.d.cts} +2 -2
  42. package/dist/{index-C3JmluyO.d.mts.map → index-DV6j4Ydq.d.cts.map} +1 -1
  43. package/dist/index.cjs +1210 -701
  44. package/dist/index.cjs.map +1 -1
  45. package/dist/index.mjs +1215 -704
  46. package/dist/index.mjs.map +1 -1
  47. package/dist/{migrate-CXVnh1J3.mjs → migrate-BrFREmgQ.mjs} +4 -4
  48. package/dist/{migrate-CXVnh1J3.mjs.map → migrate-BrFREmgQ.mjs.map} +1 -1
  49. package/dist/{migrate-DzYqA-e3.cjs → migrate-CThJfVHR.cjs} +4 -4
  50. package/dist/{migrate-DzYqA-e3.cjs.map → migrate-CThJfVHR.cjs.map} +1 -1
  51. package/dist/openapi.cjs +1 -1
  52. package/dist/openapi.d.cts +1 -1
  53. package/dist/openapi.d.mts +1 -1
  54. package/dist/openapi.mjs +1 -1
  55. package/dist/ready-DbDHneLR.mjs.map +1 -1
  56. package/dist/ready-mR96orkJ.cjs.map +1 -1
  57. package/dist/reconcile/public.d.cts +1 -1
  58. package/dist/reconcile/public.d.mts +1 -1
  59. package/dist/{types-D7j6QWeX.d.mts → types-CipIIm8f.d.mts} +34 -2
  60. package/dist/{types-D7j6QWeX.d.mts.map → types-CipIIm8f.d.mts.map} +1 -1
  61. package/dist/{types-JV4XOY01.d.cts → types-D9b-IYdW.d.cts} +34 -2
  62. package/dist/{types-JV4XOY01.d.cts.map → types-D9b-IYdW.d.cts.map} +1 -1
  63. package/dist/workspace/index.cjs +1 -1
  64. package/dist/workspace/index.d.cts +2 -2
  65. package/dist/workspace/index.d.mts +2 -2
  66. package/dist/workspace/index.mjs +1 -1
  67. package/dist/{workspace-Bah36xSG.cjs → workspace-B64Xk1Mb.cjs} +14 -4
  68. package/dist/workspace-B64Xk1Mb.cjs.map +1 -0
  69. package/dist/{workspace-D-f58jsP.mjs → workspace-BRZ14o-o.mjs} +9 -6
  70. package/dist/{workspace-D-f58jsP.mjs.map → workspace-BRZ14o-o.mjs.map} +1 -1
  71. package/dist/{workspace-BvNXAXQI.mjs → workspace-Bp1xVaOz.mjs} +14 -4
  72. package/dist/workspace-Bp1xVaOz.mjs.map +1 -0
  73. package/dist/{workspace-B14L2TrY.cjs → workspace-Dbn07ILl.cjs} +7 -4
  74. package/dist/{workspace-B14L2TrY.cjs.map → workspace-Dbn07ILl.cjs.map} +1 -1
  75. package/package.json +13 -13
  76. package/dist/LocalStateProvider-7HR6SS2t.cjs +0 -48
  77. package/dist/LocalStateProvider-7HR6SS2t.cjs.map +0 -1
  78. package/dist/LocalStateProvider-D7QyJoLM.mjs +0 -48
  79. package/dist/LocalStateProvider-D7QyJoLM.mjs.map +0 -1
  80. package/dist/LocalStateStore-BupdV-VN.mjs +0 -163
  81. package/dist/LocalStateStore-BupdV-VN.mjs.map +0 -1
  82. package/dist/LocalStateStore-DDDU0HkI.cjs +0 -163
  83. package/dist/LocalStateStore-DDDU0HkI.cjs.map +0 -1
  84. package/dist/SSMStateProvider-B0Hu1MKZ.mjs +0 -75
  85. package/dist/SSMStateProvider-B0Hu1MKZ.mjs.map +0 -1
  86. package/dist/SSMStateProvider-VrQ-yeqd.cjs +0 -74
  87. package/dist/SSMStateProvider-VrQ-yeqd.cjs.map +0 -1
  88. package/dist/config-CSOK73pY.mjs.map +0 -1
  89. package/dist/config-DdIu1O_I.cjs.map +0 -1
  90. package/dist/workspace-Bah36xSG.cjs.map +0 -1
  91. package/dist/workspace-BvNXAXQI.mjs.map +0 -1
package/dist/index.mjs CHANGED
@@ -1,29 +1,31 @@
1
1
  #!/usr/bin/env -S npx tsx
2
2
  import { n as __require, t as __exportAll } from "./rolldown-runtime-BhDjJH2R.mjs";
3
3
  import { i as ConstructGenerator, t as discover } from "./discover-Cf2jAcgF.mjs";
4
- import { d as parseModuleConfig, f as appKey, l as loadWorkspaceConfig, n as NotABackendApp, o as loadAppConfig, p as derivedApps, r as NotInAnApp, s as loadConfig, t as ConfigNotFound, u as loadWorkspaceSettings } from "./config-CSOK73pY.mjs";
5
- import { D as assertDeployedStage, E as TEST_STAGE, O as deployedProblems, T as InvalidStages, a as getAppBuildOrder, b as getDeployTargetError, k as stageProblems, m as stripPublicPrefix, o as getAppGkmConfig, p as getPublicEnvPrefix, t as allConstructGlobs, x as isDeployTargetSupported } from "./workspace-BvNXAXQI.mjs";
4
+ import { d as parseModuleConfig, f as appKey, l as loadWorkspaceConfig, n as NotABackendApp, o as loadAppConfig, p as derivedApps, r as NotInAnApp, s as loadConfig, t as ConfigNotFound, u as loadWorkspaceSettings } from "./config-EuzFEkcH.mjs";
5
+ import { D as assertDeployedStage, E as TEST_STAGE, O as deployedProblems, T as InvalidStages, a as getAppBuildOrder, b as getDeployTargetError, k as stageProblems, m as stripPublicPrefix, o as getAppGkmConfig, p as getPublicEnvPrefix, t as allConstructGlobs, x as isDeployTargetSupported } from "./workspace-Bp1xVaOz.mjs";
6
6
  import { a as withCustomSecret, i as toEmbeddableSecrets, n as maskPassword, t as initStageSecrets } from "./storage-BzaUxmqe.mjs";
7
7
  import { a as getKeyPath, i as FileSecretsStore, n as secretsStoreFor } from "./store-DeGATCS-.mjs";
8
- import { C as loadEnvFiles, S as isPortAvailable, T as prepareEntryCredentials, _ as fakeKey, a as reconcileWorkspace, b as createEntryWrapper, c as LOCAL_CA_PATH, d as postgresDatabaseNames, f as quoteIdentifier, g as describeServices, h as rootSite, i as derivedContainers, l as forgetState, m as resourceName, n as backendsOf$1, p as pgClient, r as constructGlobs, s as COMPOSE_PATH, t as FAKE_ENV, u as applyPostgres, v as primaryPortKey, w as loadSecretsForApp, x as findAvailablePort, y as createCredentialsPreload } from "./workspace-D-f58jsP.mjs";
8
+ import { C as loadEnvFiles, S as isPortAvailable, T as prepareEntryCredentials, _ as fakeKey, a as reconcileWorkspace, b as createEntryWrapper, c as LOCAL_CA_PATH, d as postgresDatabaseNames, f as quoteIdentifier, g as describeServices, h as rootSite, i as derivedContainers, l as forgetState, m as resourceName, n as backendsOf$1, p as pgClient, r as constructGlobs, s as COMPOSE_PATH, t as FAKE_ENV, u as applyPostgres, v as primaryPortKey, w as loadSecretsForApp, x as findAvailablePort, y as createCredentialsPreload } from "./workspace-BRZ14o-o.mjs";
9
9
  import { i as telemetryFor, n as EndpointGenerator, r as runtimeFor } from "./OpenApiTsGenerator-BRWD1Wit.mjs";
10
10
  import { generateOpenApi, openapiCommand, resolveOpenApiConfig } from "./openapi.mjs";
11
11
  import { a as writeTestHarness, c as driversFor, i as TEST_MANIFEST_ENV, n as TEST_READY_FILE, o as SubscriberGenerator, s as cacheBackendsIn, t as TEST_READY_ENV } from "./ready-DbDHneLR.mjs";
12
12
  import { i as storageBackendFor, n as eventsBackendFor, r as providerOf, t as cacheBackendFor } from "./backends-CjuCiE8h.mjs";
13
13
  import { n as withCompute, r as withRoutes, t as manifestModule } from "./emit-C1e8qD08.mjs";
14
+ import { c as createStateStore, i as StateStoreBusy, o as StateVersionConflict, r as StateLocked, s as contentVersion, t as DocumentStateStore } from "./StateStore-CYUfGYcx.mjs";
14
15
  import { createRequire } from "node:module";
15
16
  import { Command } from "commander";
16
17
  import { stdin, stdout } from "node:process";
17
18
  import * as readline from "node:readline/promises";
18
- import { copyFileSync, existsSync, mkdirSync, readFileSync, unlinkSync, writeFileSync } from "node:fs";
19
- import { chmod, mkdir, mkdtemp, readFile, rm, writeFile } from "node:fs/promises";
20
- import { homedir, hostname, tmpdir, userInfo } from "node:os";
19
+ import { copyFileSync, existsSync, linkSync, mkdirSync, readFileSync, readdirSync, renameSync, unlinkSync, writeFileSync } from "node:fs";
20
+ import { chmod, mkdir, mkdtemp, open, readFile, rename, rm, stat, unlink, writeFile } from "node:fs/promises";
21
+ import { homedir, tmpdir } from "node:os";
21
22
  import { basename, dirname, isAbsolute, join, parse, relative, resolve } from "node:path";
22
23
  import { cacheTable, canonicalId, cloudName, cookieDomain, dependentsOf, externalApiUrl, kebabCase, migrationFolder, mobileOrigins, provideKey, provisionOrder, schemeBase, scopedName, serviceKey } from "@geekmidas/manifest";
23
24
  import fg from "fast-glob";
24
25
  import { execFile, execFileSync, execSync, spawn } from "node:child_process";
25
26
  import chokidar from "chokidar";
26
- import { createCipheriv, createHash, randomBytes, randomUUID } from "node:crypto";
27
+ import { connect } from "node:net";
28
+ import { createCipheriv, createHash, randomBytes, randomUUID, timingSafeEqual } from "node:crypto";
27
29
  import { fileURLToPath, pathToFileURL } from "node:url";
28
30
  import { generateKeyring } from "@geekmidas/constructs/encryption";
29
31
  import { ownerRole, readerRole, roleStatements } from "@geekmidas/db/pg/roles";
@@ -32,9 +34,11 @@ import { parse as parse$1, parseDocument } from "yaml";
32
34
  import { promisify } from "node:util";
33
35
  import { cacheTableStatements } from "@geekmidas/cache/postgres";
34
36
  import prompts from "prompts";
35
- import { createServer } from "http";
37
+ import { createServer as createServer$1 } from "node:http";
38
+ import { Readable } from "node:stream";
39
+ import { createServer as createServer$2 } from "http";
36
40
  import { Http2ServerRequest } from "http2";
37
- import { Readable } from "stream";
41
+ import { Readable as Readable$1 } from "stream";
38
42
  import crypto from "crypto";
39
43
  import { Cron } from "@geekmidas/constructs/crons";
40
44
  import { Function } from "@geekmidas/constructs/functions";
@@ -42,7 +46,7 @@ import { Queue } from "@geekmidas/constructs/queue";
42
46
  import { Topic } from "@geekmidas/constructs/topic";
43
47
  import { lookup } from "node:dns/promises";
44
48
  //#region package.json
45
- var version = "10.0.0-alpha.58";
49
+ var version = "10.0.0-alpha.59";
46
50
  //#endregion
47
51
  //#region src/auth/credentials.ts
48
52
  /**
@@ -756,6 +760,621 @@ async function assignAppPorts(workspace, names, deps) {
756
760
  running
757
761
  };
758
762
  }
763
+ /** Overrides `dev.discoveryPort` — `0` serves on a port of its own. */
764
+ const DISCOVERY_PORT_ENV = "GKM_DISCOVERY_PORT";
765
+ /**
766
+ * Set by a workspace's `gkm dev` for the app sessions turbo starts under it:
767
+ * the workspace has printed the connect URL, so they do not repeat it.
768
+ */
769
+ const DISCOVERY_QUIET_ENV = "GKM_DISCOVERY_QUIET";
770
+ /** The route everything is served under. */
771
+ const BASE = "/__gkm";
772
+ /** `GKM_DISCOVERY_PORT` is set to something that is not a port. */
773
+ var InvalidDiscoveryPort = class extends Error {
774
+ value;
775
+ constructor(value) {
776
+ 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.`);
777
+ this.value = value;
778
+ this.name = "InvalidDiscoveryPort";
779
+ }
780
+ };
781
+ /** The port the endpoint uses: the env, then `dev.discoveryPort`, then 4983. */
782
+ function discoveryPort(config, env = process.env) {
783
+ const raw = env[DISCOVERY_PORT_ENV];
784
+ if (raw !== void 0 && raw !== "") {
785
+ const port = Number(raw);
786
+ if (!Number.isInteger(port) || port < 0 || port > 65535) throw new InvalidDiscoveryPort(raw);
787
+ return port;
788
+ }
789
+ return config?.discoveryPort ?? 4983;
790
+ }
791
+ /** The machine's registry, shared by every `gkm dev`. */
792
+ function discoveryRegistry(env = process.env) {
793
+ return env["GKM_DEV_REGISTRY"] || join(homedir(), ".gkm", "dev");
794
+ }
795
+ function sessionsDir(registry) {
796
+ return join(registry, "sessions");
797
+ }
798
+ /** Whether a process is alive. `EPERM` is someone else's — alive. */
799
+ function alive(pid) {
800
+ try {
801
+ process.kill(pid, 0);
802
+ return true;
803
+ } catch (error) {
804
+ return error.code === "EPERM";
805
+ }
806
+ }
807
+ /**
808
+ * Every live session, pruning the ones whose process is gone — a `gkm dev`
809
+ * killed outright never got to remove its own.
810
+ */
811
+ function liveSessions(registry) {
812
+ const dir = sessionsDir(registry);
813
+ let files;
814
+ try {
815
+ files = readdirSync(dir).filter((file) => file.endsWith(".json"));
816
+ } catch {
817
+ return [];
818
+ }
819
+ const records = [];
820
+ for (const file of files) {
821
+ const path = join(dir, file);
822
+ let record;
823
+ try {
824
+ record = JSON.parse(readFileSync(path, "utf8"));
825
+ } catch {
826
+ continue;
827
+ }
828
+ if (alive(record.pid)) {
829
+ records.push(record);
830
+ continue;
831
+ }
832
+ try {
833
+ unlinkSync(path);
834
+ } catch {}
835
+ }
836
+ return records;
837
+ }
838
+ /** Written whole, then renamed into place, so a reader never sees half. */
839
+ function writeRecord(registry, record) {
840
+ const path = join(sessionsDir(registry), `${record.id}.json`);
841
+ const partial = `${path}.${process.pid}.tmp`;
842
+ writeFileSync(partial, JSON.stringify(record), { mode: 384 });
843
+ renameSync(partial, path);
844
+ }
845
+ function removeRecord(registry, id) {
846
+ try {
847
+ unlinkSync(join(sessionsDir(registry), `${id}.json`));
848
+ } catch {}
849
+ }
850
+ /**
851
+ * The token every session prints and every host checks.
852
+ *
853
+ * Shared through the registry, so a URL printed by one session still works
854
+ * after another has taken over hosting. It lives as long as some session does:
855
+ * a session that finds none but itself starts a new one, so a token printed
856
+ * yesterday stops working once everything it was printed by has stopped.
857
+ */
858
+ function sessionToken(registry, self) {
859
+ const path = join(registry, "token");
860
+ if (liveSessions(registry).filter((r) => r.id !== self).length === 0) {
861
+ const stale = `${path}.${self}.stale`;
862
+ try {
863
+ renameSync(path, stale);
864
+ unlinkSync(stale);
865
+ } catch {}
866
+ }
867
+ const fresh = `${path}.${self}.new`;
868
+ writeFileSync(fresh, randomBytes(32).toString("base64url"), { mode: 384 });
869
+ try {
870
+ linkSync(fresh, path);
871
+ } catch {}
872
+ unlinkSync(fresh);
873
+ return readFileSync(path, "utf8").trim();
874
+ }
875
+ /** `<name>-<8 hex>`: readable, and two checkouts of a project stay apart. */
876
+ function workspaceId(name, root) {
877
+ return `${name.toLowerCase().replace(/^@/, "").replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "") || "workspace"}-${createHash("sha256").update(root).digest("hex").slice(0, 8)}`;
878
+ }
879
+ /** Whether anything accepts connections on a local port. */
880
+ function listening(port) {
881
+ return new Promise((resolve) => {
882
+ const socket = connect({
883
+ port,
884
+ host: "localhost"
885
+ });
886
+ const done = (open) => {
887
+ socket.destroy();
888
+ resolve(open);
889
+ };
890
+ socket.setTimeout(300, () => done(false));
891
+ socket.once("connect", () => done(true));
892
+ socket.once("error", () => done(false));
893
+ });
894
+ }
895
+ /** Which data APIs the forwarder matches by prefix, rather than exactly. */
896
+ const PREFIXED = /* @__PURE__ */ new Set(["telescope", "database"]);
897
+ /** Whether `path` on an app is one of its data APIs. */
898
+ function servesData(app, path) {
899
+ return app.dataApis.some((api) => PREFIXED.has(api.kind) ? path === api.path || path.startsWith(`${api.path}/`) : path === api.path);
900
+ }
901
+ /**
902
+ * Every workspace the sessions describe, one entry per root.
903
+ *
904
+ * An app listed only by its workspace's session — a web app turbo started, an
905
+ * API whose own `gkm dev` has not registered yet — is `ready` when its port
906
+ * answers and `starting` until then.
907
+ */
908
+ async function collect(records) {
909
+ const byRoot = /* @__PURE__ */ new Map();
910
+ for (const record of records) {
911
+ const group = byRoot.get(record.workspace.root) ?? [];
912
+ group.push(record);
913
+ byRoot.set(record.workspace.root, group);
914
+ }
915
+ const views = [];
916
+ for (const [root, group] of byRoot) {
917
+ group.sort((a, b) => Number(b.role === "workspace") - Number(a.role === "workspace") || a.startedAt.localeCompare(b.startedAt));
918
+ const lead = group[0];
919
+ const id = workspaceId(lead.workspace.name, root);
920
+ const apps = /* @__PURE__ */ new Map();
921
+ for (const record of group) for (const app of record.apps) {
922
+ const own = record.role === "app";
923
+ if (!own && apps.has(app.name)) continue;
924
+ apps.set(app.name, {
925
+ app,
926
+ pid: own ? record.pid : null
927
+ });
928
+ }
929
+ const discovered = await Promise.all([...apps.values()].map(async ({ app, pid }) => {
930
+ const url = `http://localhost:${app.port}`;
931
+ const status = pid === null ? await listening(app.port) ? "ready" : "starting" : app.status;
932
+ return {
933
+ name: app.name,
934
+ type: app.type,
935
+ port: app.port,
936
+ url,
937
+ publicUrl: app.publicUrl ?? null,
938
+ status,
939
+ reloads: app.reloads,
940
+ pid,
941
+ dataApis: app.dataApis.map((api) => ({
942
+ kind: api.kind,
943
+ path: api.path,
944
+ url: `${url}${api.path}`,
945
+ proxy: `${BASE}/workspaces/${id}/apps/${encodeURIComponent(app.name)}${api.path}`
946
+ }))
947
+ };
948
+ }));
949
+ discovered.sort((a, b) => a.name.localeCompare(b.name));
950
+ views.push({
951
+ id,
952
+ name: lead.workspace.name,
953
+ root,
954
+ stage: lead.workspace.stage,
955
+ manifest: group.find((r) => r.manifest)?.manifest ?? null,
956
+ apps: discovered,
957
+ allowedOrigins: [...new Set(group.flatMap((r) => r.allowedOrigins))]
958
+ });
959
+ }
960
+ return views.sort((a, b) => a.name.localeCompare(b.name) || a.root.localeCompare(b.root));
961
+ }
962
+ /** What changed between two views, as events. */
963
+ function changes(before, after) {
964
+ const out = [];
965
+ const previous = new Map(before.map((w) => [w.id, w]));
966
+ const next = new Map(after.map((w) => [w.id, w]));
967
+ for (const workspace of after) {
968
+ const was = previous.get(workspace.id);
969
+ if (!was) out.push({
970
+ workspace,
971
+ event: {
972
+ type: "workspace.started",
973
+ workspace: workspace.id
974
+ }
975
+ });
976
+ const wasApps = new Map((was?.apps ?? []).map((a) => [a.name, a]));
977
+ for (const app of workspace.apps) {
978
+ const old = wasApps.get(app.name);
979
+ 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;
980
+ if (type) out.push({
981
+ workspace,
982
+ event: {
983
+ type,
984
+ workspace: workspace.id,
985
+ app
986
+ }
987
+ });
988
+ }
989
+ for (const old of was?.apps ?? []) if (!workspace.apps.some((a) => a.name === old.name)) out.push({
990
+ workspace,
991
+ event: {
992
+ type: "app.stopped",
993
+ workspace: workspace.id,
994
+ app: old
995
+ }
996
+ });
997
+ }
998
+ for (const workspace of before) {
999
+ if (next.has(workspace.id)) continue;
1000
+ for (const app of workspace.apps) out.push({
1001
+ workspace,
1002
+ event: {
1003
+ type: "app.stopped",
1004
+ workspace: workspace.id,
1005
+ app
1006
+ }
1007
+ });
1008
+ out.push({
1009
+ workspace,
1010
+ event: {
1011
+ type: "workspace.stopped",
1012
+ workspace: workspace.id
1013
+ }
1014
+ });
1015
+ }
1016
+ return out;
1017
+ }
1018
+ /** A browser with no `Origin` — curl, a local process — sees everything. */
1019
+ function visibleTo(workspace, origin) {
1020
+ return origin === void 0 || workspace.allowedOrigins.includes(origin);
1021
+ }
1022
+ function publicView({ allowedOrigins: _, ...workspace }) {
1023
+ return workspace;
1024
+ }
1025
+ /** Headers an app's response is passed on with; nothing that sets state. */
1026
+ const FORWARDED_RESPONSE_HEADERS = [
1027
+ "content-type",
1028
+ "cache-control",
1029
+ "etag",
1030
+ "last-modified"
1031
+ ];
1032
+ /**
1033
+ * The HTTP side: serves the registry it reads, checking every request.
1034
+ * @internal Exported for testing
1035
+ */
1036
+ var DiscoveryHost = class {
1037
+ registry;
1038
+ token;
1039
+ pollMs;
1040
+ server;
1041
+ view = [];
1042
+ streams = /* @__PURE__ */ new Set();
1043
+ scanning;
1044
+ timers = [];
1045
+ sequence = 0;
1046
+ constructor(registry, token, pollMs) {
1047
+ this.registry = registry;
1048
+ this.token = token;
1049
+ this.pollMs = pollMs;
1050
+ }
1051
+ /** The port it is bound to, once it is. */
1052
+ get port() {
1053
+ return (this.server?.address())?.port;
1054
+ }
1055
+ /** Binds the port; `false` when another process already has it. */
1056
+ async listen(port) {
1057
+ const server = createServer$1((request, response) => {
1058
+ this.handle(request, response).catch(() => {
1059
+ if (!response.headersSent) reply(response, 500, { error: "DiscoveryFailed" });
1060
+ else response.destroy();
1061
+ });
1062
+ });
1063
+ if (!await new Promise((resolve) => {
1064
+ server.once("error", () => resolve(false));
1065
+ server.listen(port, "127.0.0.1", () => resolve(true));
1066
+ })) return false;
1067
+ server.unref();
1068
+ this.server = server;
1069
+ this.view = await this.scan();
1070
+ this.timers.push(setInterval(() => void this.scan(), this.pollMs).unref(), setInterval(() => {
1071
+ for (const { response } of this.streams) response.write(": ping\n\n");
1072
+ }, 15e3).unref());
1073
+ return true;
1074
+ }
1075
+ async close() {
1076
+ for (const timer of this.timers) clearInterval(timer);
1077
+ this.timers = [];
1078
+ for (const { response } of this.streams) response.end();
1079
+ this.streams.clear();
1080
+ const server = this.server;
1081
+ this.server = void 0;
1082
+ if (!server) return;
1083
+ await new Promise((resolve) => {
1084
+ server.close(() => resolve());
1085
+ server.closeAllConnections();
1086
+ });
1087
+ }
1088
+ /** Reads the registry, and tells every stream what changed. One at a time. */
1089
+ scan() {
1090
+ const run = (this.scanning ?? Promise.resolve()).then(async () => {
1091
+ const next = await collect(liveSessions(this.registry));
1092
+ for (const { workspace, event } of changes(this.view, next)) this.broadcast(workspace, event);
1093
+ this.view = next;
1094
+ return next;
1095
+ });
1096
+ this.scanning = run.catch(() => this.view);
1097
+ return run;
1098
+ }
1099
+ broadcast(workspace, event) {
1100
+ this.sequence += 1;
1101
+ for (const stream of this.streams) {
1102
+ if (!visibleTo(workspace, stream.origin)) continue;
1103
+ send(stream.response, event, this.sequence);
1104
+ }
1105
+ }
1106
+ snapshot(origin) {
1107
+ return {
1108
+ version: 1,
1109
+ host: {
1110
+ pid: process.pid,
1111
+ port: this.port ?? 0
1112
+ },
1113
+ events: `${BASE}/events`,
1114
+ workspaces: this.view.filter((w) => visibleTo(w, origin)).map(publicView)
1115
+ };
1116
+ }
1117
+ async handle(request, response) {
1118
+ const port = this.port;
1119
+ const host = request.headers.host;
1120
+ if (host !== `127.0.0.1:${port}` && host !== `localhost:${port}`) {
1121
+ reply(response, 403, {
1122
+ error: "HostNotAllowed",
1123
+ message: `Reach discovery at http://127.0.0.1:${port}.`
1124
+ });
1125
+ return;
1126
+ }
1127
+ const origin = request.headers.origin;
1128
+ const allowed = this.view.flatMap((w) => w.allowedOrigins);
1129
+ if (origin !== void 0 && !allowed.includes(origin)) {
1130
+ reply(response, 403, {
1131
+ error: "OriginNotAllowed",
1132
+ message: `Add ${origin} to dev.allowedOrigins in gkm.config.ts to let it read gkm dev.`
1133
+ });
1134
+ return;
1135
+ }
1136
+ if (origin !== void 0) {
1137
+ response.setHeader("access-control-allow-origin", origin);
1138
+ response.setHeader("vary", "Origin");
1139
+ }
1140
+ if (request.method === "OPTIONS") {
1141
+ response.setHeader("access-control-allow-methods", "GET, HEAD");
1142
+ response.setHeader("access-control-allow-headers", "Authorization");
1143
+ response.setHeader("access-control-max-age", "600");
1144
+ if (request.headers["access-control-request-private-network"]) response.setHeader("access-control-allow-private-network", "true");
1145
+ response.writeHead(204).end();
1146
+ return;
1147
+ }
1148
+ const url = new URL(request.url ?? "/", `http://${host}`);
1149
+ if (!this.authorized(request, url)) {
1150
+ reply(response, 401, {
1151
+ error: "TokenRequired",
1152
+ message: "Use the connect URL gkm dev printed, or send its token as `Authorization: Bearer <token>`."
1153
+ });
1154
+ return;
1155
+ }
1156
+ if (request.method !== "GET" && request.method !== "HEAD") {
1157
+ response.setHeader("allow", "GET, HEAD, OPTIONS");
1158
+ reply(response, 405, {
1159
+ error: "ReadOnly",
1160
+ message: "Discovery is read-only: GET and HEAD."
1161
+ });
1162
+ return;
1163
+ }
1164
+ const path = url.pathname.replace(/\/+$/, "") || "/";
1165
+ if (path === BASE) {
1166
+ await this.scan();
1167
+ reply(response, 200, this.snapshot(origin));
1168
+ return;
1169
+ }
1170
+ if (path === `${BASE}/events`) {
1171
+ await this.scan();
1172
+ this.stream(response, origin);
1173
+ response.once("close", () => {
1174
+ for (const stream of this.streams) if (stream.response === response) this.streams.delete(stream);
1175
+ });
1176
+ return;
1177
+ }
1178
+ const forwarded = path.match(/^\/__gkm\/workspaces\/([^/]+)\/apps\/([^/]+)(\/.*)$/);
1179
+ if (forwarded) {
1180
+ const [, id, name, rest] = forwarded;
1181
+ await this.forward(request, response, origin, id, decodeURIComponent(name), rest, url);
1182
+ return;
1183
+ }
1184
+ reply(response, 404, { error: "NotFound" });
1185
+ }
1186
+ authorized(request, url) {
1187
+ const header = request.headers.authorization;
1188
+ const given = header?.startsWith("Bearer ") ? header.slice(7).trim() : url.searchParams.get("token");
1189
+ if (!given) return false;
1190
+ const a = Buffer.from(given);
1191
+ const b = Buffer.from(this.token);
1192
+ return a.length === b.length && timingSafeEqual(a, b);
1193
+ }
1194
+ stream(response, origin) {
1195
+ response.writeHead(200, {
1196
+ "content-type": "text/event-stream",
1197
+ "cache-control": "no-cache",
1198
+ connection: "keep-alive"
1199
+ });
1200
+ response.write("retry: 1000\n\n");
1201
+ send(response, {
1202
+ type: "snapshot",
1203
+ snapshot: this.snapshot(origin)
1204
+ });
1205
+ this.streams.add({
1206
+ origin,
1207
+ response
1208
+ });
1209
+ }
1210
+ /** Passes a read of one data API on to the app that serves it. */
1211
+ async forward(request, response, origin, id, name, path, url) {
1212
+ const app = this.view.find((w) => w.id === id && visibleTo(w, origin))?.apps.find((a) => a.name === name);
1213
+ if (!app) {
1214
+ reply(response, 404, { error: "AppNotFound" });
1215
+ return;
1216
+ }
1217
+ if (!servesData(app, path)) {
1218
+ reply(response, 404, {
1219
+ error: "NotADataApi",
1220
+ message: `Only ${name}'s data APIs are forwarded: ${app.dataApis.map((a) => a.path).join(", ") || "it has none"}.`
1221
+ });
1222
+ return;
1223
+ }
1224
+ const search = new URLSearchParams(url.searchParams);
1225
+ search.delete("token");
1226
+ const query = search.size > 0 ? `?${search}` : "";
1227
+ const abort = new AbortController();
1228
+ response.once("close", () => abort.abort());
1229
+ let upstream;
1230
+ try {
1231
+ upstream = await fetch(`http://localhost:${app.port}${path}${query}`, {
1232
+ method: request.method,
1233
+ headers: { accept: request.headers.accept ?? "*/*" },
1234
+ signal: abort.signal
1235
+ });
1236
+ } catch {
1237
+ reply(response, 502, {
1238
+ error: "AppUnavailable",
1239
+ message: `${name} is not answering on port ${app.port}.`
1240
+ });
1241
+ return;
1242
+ }
1243
+ const headers = {};
1244
+ for (const key of FORWARDED_RESPONSE_HEADERS) {
1245
+ const value = upstream.headers.get(key);
1246
+ if (value) headers[key] = value;
1247
+ }
1248
+ response.writeHead(upstream.status, headers);
1249
+ if (!upstream.body || request.method === "HEAD") {
1250
+ response.end();
1251
+ return;
1252
+ }
1253
+ Readable.fromWeb(upstream.body).on("error", () => response.destroy()).pipe(response);
1254
+ }
1255
+ };
1256
+ function reply(response, status, body) {
1257
+ response.writeHead(status, { "content-type": "application/json" });
1258
+ response.end(JSON.stringify(body));
1259
+ }
1260
+ function send(response, event, id) {
1261
+ response.write(`${id === void 0 ? "" : `id: ${id}\n`}event: ${event.type}\ndata: ${JSON.stringify(event)}\n\n`);
1262
+ }
1263
+ /**
1264
+ * One `gkm dev`'s membership: its record in the registry, and — while it holds
1265
+ * the port — the endpoint itself.
1266
+ */
1267
+ var DiscoverySession = class DiscoverySession {
1268
+ registry;
1269
+ port;
1270
+ record;
1271
+ token;
1272
+ pollMs;
1273
+ host;
1274
+ election;
1275
+ left = false;
1276
+ cleanup = () => removeRecord(this.registry, this.record.id);
1277
+ constructor(registry, port, record, token, pollMs) {
1278
+ this.registry = registry;
1279
+ this.port = port;
1280
+ this.record = record;
1281
+ this.token = token;
1282
+ this.pollMs = pollMs;
1283
+ }
1284
+ static async join(options) {
1285
+ const registry = options.registry ?? discoveryRegistry();
1286
+ mkdirSync(sessionsDir(registry), {
1287
+ recursive: true,
1288
+ mode: 448
1289
+ });
1290
+ const now = (/* @__PURE__ */ new Date()).toISOString();
1291
+ const record = {
1292
+ id: `${process.pid}-${randomBytes(4).toString("hex")}`,
1293
+ pid: process.pid,
1294
+ role: options.role,
1295
+ startedAt: now,
1296
+ updatedAt: now,
1297
+ workspace: options.workspace,
1298
+ allowedOrigins: [...options.allowedOrigins ?? []],
1299
+ ...options.manifest ? { manifest: options.manifest } : {},
1300
+ apps: options.apps
1301
+ };
1302
+ writeRecord(registry, record);
1303
+ const token = sessionToken(registry, record.id);
1304
+ const session = new DiscoverySession(registry, options.port ?? 4983, record, token, options.pollMs ?? 500);
1305
+ process.once("exit", session.cleanup);
1306
+ await session.elect();
1307
+ if (!session.host && session.port !== 0) session.election = setInterval(() => void session.elect(), options.electionMs ?? 1e3).unref();
1308
+ return session;
1309
+ }
1310
+ /** Whether this session is the one serving the endpoint. */
1311
+ get hosting() {
1312
+ return this.host !== void 0;
1313
+ }
1314
+ /** The port clients reach — this host's own, when it chose one. */
1315
+ get endpointPort() {
1316
+ return this.host?.port ?? this.port;
1317
+ }
1318
+ /** Jupyter-style: the endpoint, with the token that opens it. */
1319
+ get connectUrl() {
1320
+ return `http://127.0.0.1:${this.endpointPort}${BASE}?token=${this.token}`;
1321
+ }
1322
+ /** Changes one of this session's apps, and tells the endpoint. */
1323
+ updateApp(name, change) {
1324
+ if (this.left) return;
1325
+ this.record.apps = this.record.apps.map((app) => app.name === name ? change(app) : app);
1326
+ this.record.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
1327
+ writeRecord(this.registry, this.record);
1328
+ this.host?.scan();
1329
+ }
1330
+ /** Leaves the registry, and the port to whichever session takes it next. */
1331
+ async leave() {
1332
+ if (this.left) return;
1333
+ this.left = true;
1334
+ if (this.election) clearInterval(this.election);
1335
+ process.removeListener("exit", this.cleanup);
1336
+ this.cleanup();
1337
+ await this.host?.close();
1338
+ this.host = void 0;
1339
+ if (liveSessions(this.registry).length === 0) try {
1340
+ unlinkSync(join(this.registry, "token"));
1341
+ } catch {}
1342
+ }
1343
+ async elect() {
1344
+ if (this.host || this.left) return;
1345
+ const host = new DiscoveryHost(this.registry, this.token, this.pollMs);
1346
+ if (!await host.listen(this.port)) return;
1347
+ if (this.left) {
1348
+ await host.close();
1349
+ return;
1350
+ }
1351
+ this.host = host;
1352
+ if (this.election) clearInterval(this.election);
1353
+ this.election = void 0;
1354
+ }
1355
+ };
1356
+ /** Joins the machine's discovery; see {@link DiscoverySession}. */
1357
+ function joinDiscovery(options) {
1358
+ return DiscoverySession.join(options);
1359
+ }
1360
+ /** The data APIs a generated dev app mounts, from what `gkm dev` built. */
1361
+ function dataApisOf(options) {
1362
+ if (options.selfServing) return [];
1363
+ return [
1364
+ ...options.telescopePath ? [{
1365
+ kind: "telescope",
1366
+ path: `${options.telescopePath}/api`
1367
+ }] : [],
1368
+ ...options.databaseApi ? [{
1369
+ kind: "database",
1370
+ path: options.databaseApi
1371
+ }] : [],
1372
+ ...options.openApi ? [{
1373
+ kind: "openapi",
1374
+ path: "/__docs"
1375
+ }] : []
1376
+ ];
1377
+ }
759
1378
  //#endregion
760
1379
  //#region ../../node_modules/.pnpm/@hono+node-server@1.19.5_hono@4.13.8/node_modules/@hono/node-server/dist/index.mjs
761
1380
  var RequestError = class extends Error {
@@ -809,7 +1428,7 @@ var newRequestFromIncoming = (method, url, headers, incoming, abortController) =
809
1428
  let reader;
810
1429
  init.body = new ReadableStream({ async pull(controller) {
811
1430
  try {
812
- reader ||= Readable.toWeb(incoming).getReader();
1431
+ reader ||= Readable$1.toWeb(incoming).getReader();
813
1432
  const { done, value } = await reader.read();
814
1433
  if (done) controller.close();
815
1434
  else controller.enqueue(value);
@@ -817,7 +1436,7 @@ var newRequestFromIncoming = (method, url, headers, incoming, abortController) =
817
1436
  controller.error(error);
818
1437
  }
819
1438
  } });
820
- } else init.body = Readable.toWeb(incoming);
1439
+ } else init.body = Readable$1.toWeb(incoming);
821
1440
  }
822
1441
  return new Request(url, init);
823
1442
  };
@@ -1188,7 +1807,7 @@ var createAdaptorServer = (options) => {
1188
1807
  overrideGlobalObjects: options.overrideGlobalObjects,
1189
1808
  autoCleanupIncoming: options.autoCleanupIncoming
1190
1809
  });
1191
- return (options.createServer || createServer)(options.serverOptions || {}, requestListener);
1810
+ return (options.createServer || createServer$2)(options.serverOptions || {}, requestListener);
1192
1811
  };
1193
1812
  var serve = (options, listeningListener) => {
1194
1813
  const server = createAdaptorServer(options);
@@ -1421,6 +2040,7 @@ async function devCommand(options) {
1421
2040
  const runtime = config.runtime ?? "node";
1422
2041
  let secretsJsonPath;
1423
2042
  let publicUrl;
2043
+ let manifest;
1424
2044
  const appSecrets = await loadSecretsForApp(workspace ? await secretsStoreFor(workspace, config.stages.local) : new FileSecretsStore(secretsRoot), config.stages.local, workspaceAppName);
1425
2045
  if (workspace) {
1426
2046
  const reconciled = await reconcileWorkspace(workspace, {
@@ -1435,6 +2055,7 @@ async function devCommand(options) {
1435
2055
  Object.assign(appSecrets, reconciled.env);
1436
2056
  const own = reconciled.plan.resources.find((r) => (r.kind === "rest-api" || r.kind === "site") && appKey(r.id) === workspaceAppName);
1437
2057
  publicUrl = own ? reconciled.env[own.envKey] : void 0;
2058
+ manifest = reconciled.manifest;
1438
2059
  }
1439
2060
  if (Object.keys(appSecrets).length > 0) {
1440
2061
  const secretsDir = join(secretsRoot, ".gkm");
@@ -1447,6 +2068,38 @@ async function devCommand(options) {
1447
2068
  devServer.label = workspaceAppName ?? "server";
1448
2069
  devServer.publicUrl = publicUrl;
1449
2070
  await devServer.start();
2071
+ const discovery = await joinDiscovery({
2072
+ role: "app",
2073
+ port: discoveryPort(workspace.dev),
2074
+ workspace: {
2075
+ name: workspace.name,
2076
+ root: workspace.root,
2077
+ stage: workspace.stages.local
2078
+ },
2079
+ allowedOrigins: workspace.dev?.allowedOrigins ?? [],
2080
+ ...manifest ? { manifest } : {},
2081
+ apps: [{
2082
+ name: workspaceAppName,
2083
+ type: "backend",
2084
+ port: devServer.port,
2085
+ ...publicUrl ? { publicUrl } : {},
2086
+ status: "ready",
2087
+ reloads: 0,
2088
+ dataApis: dataApisOf({
2089
+ selfServing: devServer.selfServing,
2090
+ telescopePath: telescope?.path,
2091
+ databaseApi: initial.databaseApi,
2092
+ openApi: enableOpenApi
2093
+ })
2094
+ }]
2095
+ });
2096
+ if (!process.env["GKM_DISCOVERY_QUIET"]) announce(discovery);
2097
+ devServer.onStatus = (status) => discovery.updateApp(workspaceAppName, (app) => ({
2098
+ ...app,
2099
+ status,
2100
+ port: devServer.port,
2101
+ reloads: app.status === "reloading" && status === "ready" ? app.reloads + 1 : app.reloads
2102
+ }));
1450
2103
  const hooksFile = (config.hooks?.server?.split("#"))?.[0];
1451
2104
  const normalizedPatterns = [config.constructs, ...hooksFile ? [hooksFile.endsWith(".ts") ? hooksFile : `${hooksFile}.ts`] : []].flat().filter((p) => typeof p === "string").map((p) => isAbsolute(p) ? relative(appRoot, p) : p.replace(/^\.\//, ""));
1452
2105
  const resolvedFiles = await fg(normalizedPatterns, {
@@ -1487,7 +2140,11 @@ async function devCommand(options) {
1487
2140
  if (isShuttingDown) return;
1488
2141
  isShuttingDown = true;
1489
2142
  logger$9.log("\n🛑 Shutting down...");
1490
- Promise.all([watcher.close(), devServer.stop()]).catch((err) => {
2143
+ Promise.all([
2144
+ watcher.close(),
2145
+ devServer.stop(),
2146
+ discovery.leave()
2147
+ ]).catch((err) => {
1491
2148
  logger$9.error("Error during shutdown:", err);
1492
2149
  }).finally(() => {
1493
2150
  process.exit(0);
@@ -1731,9 +2388,9 @@ async function workspaceDevCommand(configured, options) {
1731
2388
  for (const { id, port } of fakes) logger$9.log(`🎭 ${id} fake: http://localhost:${port}`);
1732
2389
  logger$9.log(options.migrate || options.seed ? "🗄️ Migrating the databases" : "🗄️ Checking for pending migrations");
1733
2390
  if (options.migrate || options.seed) {
1734
- const { prepareDevDatabases } = await import("./migrate-CXVnh1J3.mjs");
2391
+ const { prepareDevDatabases } = await import("./migrate-BrFREmgQ.mjs");
1735
2392
  await prepareDevDatabases(workspace, reconciled.env, { seed: !!options.seed });
1736
- } else await import("./migrate-CXVnh1J3.mjs").then(({ reportPendingMigrations }) => reportPendingMigrations(workspace, reconciled.env)).catch((error) => {
2393
+ } else await import("./migrate-BrFREmgQ.mjs").then(({ reportPendingMigrations }) => reportPendingMigrations(workspace, reconciled.env)).catch((error) => {
1737
2394
  logger$9.log(`⚠️ Could not check for pending migrations: ${error instanceof Error ? error.message : String(error)}`);
1738
2395
  });
1739
2396
  const appUrls = {};
@@ -1788,12 +2445,39 @@ async function workspaceDevCommand(configured, options) {
1788
2445
  break;
1789
2446
  }
1790
2447
  }
2448
+ const discovery = await joinDiscovery({
2449
+ role: "workspace",
2450
+ port: discoveryPort(workspace.dev),
2451
+ workspace: {
2452
+ name: workspace.name,
2453
+ root: workspace.root,
2454
+ stage: workspace.stages.local
2455
+ },
2456
+ allowedOrigins: workspace.dev?.allowedOrigins ?? [],
2457
+ ...reconciled.manifest ? { manifest: reconciled.manifest } : {},
2458
+ apps: buildOrder.flatMap((name) => {
2459
+ const app = workspace.apps[name];
2460
+ if (!app) return [];
2461
+ const publicUrl = Object.entries(appUrls).find(([id]) => appKey(id) === name)?.[1];
2462
+ return [{
2463
+ name,
2464
+ type: app.type,
2465
+ port: app.port,
2466
+ ...publicUrl ? { publicUrl } : {},
2467
+ status: "starting",
2468
+ reloads: 0,
2469
+ dataApis: []
2470
+ }];
2471
+ })
2472
+ });
2473
+ announce(discovery);
1791
2474
  const turboEnv = {
1792
2475
  ...process.env,
1793
2476
  ...secretsEnv,
1794
2477
  NODE_ENV: "development",
1795
2478
  ...configPath ? { GKM_CONFIG_PATH: configPath } : {},
1796
- ...options.fake ? { [FAKE_ENV]: "1" } : {}
2479
+ ...options.fake ? { [FAKE_ENV]: "1" } : {},
2480
+ [DISCOVERY_QUIET_ENV]: "1"
1797
2481
  };
1798
2482
  logger$9.log(`🚀 Starting ${appCount} app(s) — each builds before it answers, and says so when it is ready`);
1799
2483
  const turboProcess = spawn("pnpm", [
@@ -1813,6 +2497,7 @@ async function workspaceDevCommand(configured, options) {
1813
2497
  isShuttingDown = true;
1814
2498
  logger$9.log("\n🛑 Shutting down workspace...");
1815
2499
  closeFakes(fakes);
2500
+ discovery.leave();
1816
2501
  const pid = turboProcess.pid;
1817
2502
  if (pid) try {
1818
2503
  process.kill(-pid, "SIGTERM");
@@ -2053,6 +2738,13 @@ start({
2053
2738
  });
2054
2739
  `;
2055
2740
  }
2741
+ /**
2742
+ * The discovery endpoint's connect URL, Jupyter-style: the token in it is what
2743
+ * lets a console read what this machine is running.
2744
+ */
2745
+ function announce(discovery) {
2746
+ logger$9.log(`🧭 Discovery: ${discovery.connectUrl}`);
2747
+ }
2056
2748
  /** Run `fn` with `console.log` muted — warnings and errors still print. */
2057
2749
  async function quietly(fn) {
2058
2750
  const log = console.log;
@@ -2150,6 +2842,12 @@ var DevServer = class {
2150
2842
  }
2151
2843
  /** What the ready line calls this app. */
2152
2844
  label = "server";
2845
+ /** Told when the server is ready, reloading, or exits on its own. */
2846
+ onStatus;
2847
+ /** The port it is serving on. */
2848
+ get port() {
2849
+ return this.actualPort;
2850
+ }
2153
2851
  /**
2154
2852
  * Where the app is reached — its HTTPS address behind the edge, when it has
2155
2853
  * one. The local port is only what the edge forwards to.
@@ -2185,9 +2883,11 @@ var DevServer = class {
2185
2883
  this.serverProcess.on("error", (error) => {
2186
2884
  logger$9.error("❌ Server error:", error);
2187
2885
  });
2188
- this.serverProcess.on("exit", (code, signal) => {
2886
+ const child = this.serverProcess;
2887
+ child.on("exit", (code, signal) => {
2189
2888
  if (code !== null && code !== 0 && signal !== "SIGTERM") logger$9.error(`❌ Server exited with code ${code}`);
2190
2889
  this.isRunning = false;
2890
+ if (this.serverProcess === child) this.onStatus?.("stopped");
2191
2891
  });
2192
2892
  await new Promise((resolve) => setTimeout(resolve, 1e3));
2193
2893
  if (this.isRunning) {
@@ -2201,6 +2901,7 @@ var DevServer = class {
2201
2901
  ...this.databaseApi ? [`db ${this.databaseApi}`] : []
2202
2902
  ];
2203
2903
  if (tools.length > 0) logger$9.log(` ${tools.join(" · ")}`);
2904
+ this.onStatus?.("ready");
2204
2905
  }
2205
2906
  }
2206
2907
  async stop() {
@@ -2226,6 +2927,7 @@ var DevServer = class {
2226
2927
  }
2227
2928
  async restart() {
2228
2929
  const portToReuse = this.actualPort;
2930
+ this.onStatus?.("reloading");
2229
2931
  await this.stop();
2230
2932
  let attempts = 0;
2231
2933
  while (attempts < 30) {
@@ -3299,7 +4001,7 @@ async function buildForTarget(target, context, appRoot, workspaceRoot, endpointG
3299
4001
  let masterKey;
3300
4002
  if (context.production?.bundle && !skipBundle) {
3301
4003
  logger$8.log(`\n📦 Bundling production server...`);
3302
- const { bundleServer } = await import("./bundler-URImKj-A.mjs");
4004
+ const { bundleServer } = await import("./bundler-ouDdNvk9.mjs");
3303
4005
  const allConstructs = [
3304
4006
  ...endpoints.map((e) => e.construct),
3305
4007
  ...functions.map((f) => f.construct),
@@ -4301,12 +5003,6 @@ function createEmptyState(stage, projectId, environmentId) {
4301
5003
  };
4302
5004
  }
4303
5005
  /**
4304
- * Get application ID from state
4305
- */
4306
- function getApplicationId(state, appName) {
4307
- return state?.applications[appName];
4308
- }
4309
- /**
4310
5006
  * Set application ID in state (mutates state)
4311
5007
  */
4312
5008
  function setApplicationId(state, appName, applicationId) {
@@ -4408,7 +5104,7 @@ async function createDnsProvider(options) {
4408
5104
  if (isDnsProvider(config.provider)) return config.provider;
4409
5105
  const provider = config.provider;
4410
5106
  if (provider === "hostinger") {
4411
- const { HostingerProvider } = await import("./HostingerProvider-cL-6oQ5r.mjs");
5107
+ const { HostingerProvider } = await import("./HostingerProvider-YQY3rMpV.mjs");
4412
5108
  return new HostingerProvider();
4413
5109
  }
4414
5110
  if (provider === "route53") {
@@ -7189,6 +7885,113 @@ function applicationName(identity, app) {
7189
7885
  return slug$1(app) === identity.project ? `${identity.stage}-${identity.scope}` : scopedName([identity.stage, identity.scope], app);
7190
7886
  }
7191
7887
  //#endregion
7888
+ //#region src/deploy/journal.ts
7889
+ var DeployJournal = class DeployJournal {
7890
+ store;
7891
+ stage;
7892
+ state;
7893
+ resources;
7894
+ version;
7895
+ existed;
7896
+ constructor(store, stage, state, resources, version, existed) {
7897
+ this.store = store;
7898
+ this.stage = stage;
7899
+ this.state = state;
7900
+ this.resources = resources;
7901
+ this.version = version;
7902
+ this.existed = existed;
7903
+ }
7904
+ /**
7905
+ * The stage's journal — its stored state, or `initial` written as the
7906
+ * stage's first state, so there is somewhere to record the very first
7907
+ * resource before it is created.
7908
+ */
7909
+ static async open(store, stage, initial) {
7910
+ const stored = await store.read(stage);
7911
+ if (stored) return new DeployJournal(store, stage, stored.state, stored.resources, stored.version, true);
7912
+ const state = initial();
7913
+ const version = await store.write(stage, state, { expectedVersion: null });
7914
+ return new DeployJournal(store, stage, state, {}, version, false);
7915
+ }
7916
+ record(key) {
7917
+ return this.resources[key];
7918
+ }
7919
+ /** The keys a previous run marked `pending` and never saw created. */
7920
+ unfinished() {
7921
+ return Object.values(this.resources).filter((record) => record.status === "pending").map((record) => record.key);
7922
+ }
7923
+ /** Records that `entry` is about to be created. */
7924
+ async pending(entry) {
7925
+ await this.put({
7926
+ ...entry,
7927
+ status: "pending"
7928
+ });
7929
+ }
7930
+ /** Records `entry` as existing with `id`; a no-op if it already is. */
7931
+ async ready(entry, id) {
7932
+ const current = this.resources[entry.key];
7933
+ if (current?.status === "ready" && current.id === id) return;
7934
+ await this.put({
7935
+ ...entry,
7936
+ status: "ready",
7937
+ id
7938
+ });
7939
+ }
7940
+ /**
7941
+ * The resource `entry` names: by its recorded id, else by looking it up,
7942
+ * else created — with `pending` written before the create and `ready`
7943
+ * after, so a run that dies in between leaves a record the next run
7944
+ * resolves by looking the resource up rather than creating another.
7945
+ */
7946
+ async ensure(entry, steps) {
7947
+ const record = this.resources[entry.key];
7948
+ let staleId;
7949
+ if (record?.status === "ready" && record.id && steps.get) {
7950
+ const known = await steps.get(record.id);
7951
+ if (known) {
7952
+ await this.ready(entry, steps.id(known));
7953
+ return {
7954
+ resource: known,
7955
+ via: "recorded"
7956
+ };
7957
+ }
7958
+ staleId = record.id;
7959
+ }
7960
+ const existing = await steps.find();
7961
+ if (existing) {
7962
+ await this.ready(entry, steps.id(existing));
7963
+ return {
7964
+ resource: existing,
7965
+ via: record?.status === "pending" ? "resumed" : "found",
7966
+ ...staleId ? { staleId } : {}
7967
+ };
7968
+ }
7969
+ await this.pending(entry);
7970
+ const created = await steps.create();
7971
+ await this.ready(entry, steps.id(created));
7972
+ return {
7973
+ resource: created,
7974
+ via: "created",
7975
+ ...staleId ? { staleId } : {}
7976
+ };
7977
+ }
7978
+ /** Writes the stage's state, keeping its resource records. */
7979
+ async save() {
7980
+ this.state.lastDeployedAt = (/* @__PURE__ */ new Date()).toISOString();
7981
+ this.version = await this.store.write(this.stage, this.state, { expectedVersion: this.version });
7982
+ }
7983
+ async put(record) {
7984
+ this.version = await this.store.putResource(this.stage, record, { expectedVersion: this.version });
7985
+ this.resources = {
7986
+ ...this.resources,
7987
+ [record.key]: {
7988
+ ...record,
7989
+ updatedAt: (/* @__PURE__ */ new Date()).toISOString()
7990
+ }
7991
+ };
7992
+ }
7993
+ };
7994
+ //#endregion
7192
7995
  //#region src/deploy/ownership.ts
7193
7996
  /**
7194
7997
  * Which Dokploy project a deploy is allowed to deploy into.
@@ -7244,7 +8047,7 @@ async function projectById(api, projectId) {
7244
8047
  * A project that matches by name (in any case) but carries no marker, or
7245
8048
  * another identity's, raises `ProjectNotOwned` — it is never adopted.
7246
8049
  */
7247
- async function resolveProject(api, identity, stateProjectId, log = () => {}) {
8050
+ async function resolveProject(api, identity, stateProjectId, log = () => {}, hooks = {}) {
7248
8051
  const name = projectName(identity);
7249
8052
  const marker = ownershipMarker(identity);
7250
8053
  if (stateProjectId) {
@@ -7280,6 +8083,7 @@ async function resolveProject(api, identity, stateProjectId, log = () => {}) {
7280
8083
  }
7281
8084
  const clash = projects.find((p) => p.name.toLowerCase() === name.toLowerCase());
7282
8085
  if (clash) throw new ProjectNotOwned(clash.name, clash.projectId, identity.key, markerOf(clash.description));
8086
+ await hooks.beforeCreate?.();
7283
8087
  const created = await api.createProject(name, withMarker("Deployed by gkm.", marker));
7284
8088
  return {
7285
8089
  projectId: created.project.projectId,
@@ -7382,65 +8186,6 @@ async function resolveRegistry(api, options) {
7382
8186
  return options.create(registry);
7383
8187
  }
7384
8188
  //#endregion
7385
- //#region src/deploy/StateProvider.ts
7386
- /**
7387
- * Check if value is a StateProvider implementation.
7388
- */
7389
- function isStateProvider(value) {
7390
- return typeof value === "object" && value !== null && typeof value.read === "function" && typeof value.write === "function";
7391
- }
7392
- /**
7393
- * Create a state provider based on configuration.
7394
- *
7395
- * - 'local': LocalStateProvider (default)
7396
- * - 'ssm': CachedStateProvider with SSM as source of truth
7397
- * - 's3': S3StateStore behind the provider interface
7398
- * - Custom: Use provided StateProvider implementation
7399
- *
7400
- * Deploy moves to `createStateStore` (locks, versions, per-resource records);
7401
- * this stays for the call sites that read and write whole states.
7402
- */
7403
- async function createStateProvider(options) {
7404
- const { config, workspaceRoot, workspaceName } = options;
7405
- if (!config) {
7406
- const { LocalStateProvider } = await import("./LocalStateProvider-D7QyJoLM.mjs");
7407
- return new LocalStateProvider(workspaceRoot);
7408
- }
7409
- const { isStateStore } = await Promise.resolve().then(() => StateStore_exports);
7410
- if (isStateStore(config.provider)) {
7411
- const { StateStoreProvider } = await Promise.resolve().then(() => StateStore_exports);
7412
- return new StateStoreProvider(config.provider);
7413
- }
7414
- if (isStateProvider(config.provider)) return config.provider;
7415
- const provider = config.provider;
7416
- if (provider === "local") {
7417
- const { LocalStateProvider } = await import("./LocalStateProvider-D7QyJoLM.mjs");
7418
- return new LocalStateProvider(workspaceRoot);
7419
- }
7420
- if (provider === "ssm") {
7421
- if (!workspaceName) {
7422
- const { StateStoreNeedsWorkspaceName } = await Promise.resolve().then(() => StateStore_exports);
7423
- throw new StateStoreNeedsWorkspaceName("ssm");
7424
- }
7425
- const { LocalStateProvider } = await import("./LocalStateProvider-D7QyJoLM.mjs");
7426
- const { SSMStateProvider } = await import("./SSMStateProvider-B0Hu1MKZ.mjs");
7427
- const { CachedStateProvider } = await Promise.resolve().then(() => CachedStateProvider_exports);
7428
- const ssmConfig = config;
7429
- const local = new LocalStateProvider(workspaceRoot);
7430
- return new CachedStateProvider(SSMStateProvider.create({
7431
- workspaceName,
7432
- region: ssmConfig.region,
7433
- profile: ssmConfig.profile
7434
- }), local);
7435
- }
7436
- if (provider === "s3") {
7437
- const { createStateStore, StateStoreProvider } = await Promise.resolve().then(() => StateStore_exports);
7438
- return new StateStoreProvider(await createStateStore(options));
7439
- }
7440
- const { UnknownStateProvider } = await Promise.resolve().then(() => StateStore_exports);
7441
- throw new UnknownStateProvider(config);
7442
- }
7443
- //#endregion
7444
8189
  //#region src/deploy/secrets.ts
7445
8190
  /**
7446
8191
  * Filter secrets to only include the env vars that an app requires.
@@ -8181,6 +8926,28 @@ async function workspaceDeployCommand(configured, options) {
8181
8926
  });
8182
8927
  if (dokployApps.length === 0) throw new Error("No apps to deploy. All selected apps have unsupported deploy targets.");
8183
8928
  appsToDeployNames = dokployApps;
8929
+ const store = await createStateStore({
8930
+ config: workspace.state,
8931
+ workspaceRoot: workspace.root,
8932
+ workspaceName: workspace.name
8933
+ });
8934
+ const lock = await store.lock(stage, { operation: "deploy" });
8935
+ try {
8936
+ return await deployLocked({
8937
+ workspace,
8938
+ manifest,
8939
+ stage,
8940
+ imageTag,
8941
+ appsToDeployNames,
8942
+ identity,
8943
+ store
8944
+ });
8945
+ } finally {
8946
+ await lock.release();
8947
+ }
8948
+ }
8949
+ /** Everything a deploy does while it holds the stage's lock. */
8950
+ async function deployLocked({ workspace, manifest, stage, imageTag, appsToDeployNames, identity, store }) {
8184
8951
  logger$4.log("\n🔐 Loading secrets and analyzing environment requirements...");
8185
8952
  const secretsStore = await secretsStoreFor(workspace, stage);
8186
8953
  const stored = await secretsStore.read(stage);
@@ -8222,42 +8989,48 @@ async function workspaceDeployCommand(configured, options) {
8222
8989
  token: creds.token
8223
8990
  });
8224
8991
  logger$4.log("\n📋 Loading deploy state...");
8225
- const stateProvider = await createStateProvider({
8226
- config: workspace.state,
8227
- workspaceRoot: workspace.root,
8228
- workspaceName: workspace.name
8229
- });
8230
- const previous = await stateProvider.read(stage);
8992
+ const journal = await DeployJournal.open(store, stage, () => createEmptyState(stage, "", ""));
8993
+ const state = journal.state;
8994
+ if (journal.existed) logger$4.log(` Found existing state for stage "${stage}"`);
8995
+ else logger$4.log(` Creating new state for stage "${stage}"`);
8996
+ const unfinished = journal.unfinished();
8997
+ if (unfinished.length > 0) logger$4.log(` ⚠ A previous run stopped while creating: ${unfinished.join(", ")} — looking for them before creating anything`);
8231
8998
  logger$4.log("\n📁 Setting up Dokploy project...");
8232
8999
  logger$4.log(` Identity: ${identity.key}`);
8233
- const project = await resolveProject(api, identity, previous?.projectId, (message) => logger$4.log(message));
9000
+ const projectEntry = {
9001
+ key: "project",
9002
+ type: "project",
9003
+ data: {
9004
+ name: projectName(identity),
9005
+ identity: identity.key
9006
+ }
9007
+ };
9008
+ const project = await resolveProject(api, identity, state.projectId || void 0, (message) => logger$4.log(message), { beforeCreate: () => journal.pending(projectEntry) });
9009
+ await journal.ready(projectEntry, project.projectId);
8234
9010
  if (project.via === "created") logger$4.log(` ✓ Created project: ${project.projectId}`);
8235
9011
  else logger$4.log(` Found existing project: ${project.name}`);
8236
- let environmentId;
8237
- const matchingEnv = project.environments.find((e) => e.name.toLowerCase() === stage.toLowerCase());
8238
- if (matchingEnv) {
8239
- environmentId = matchingEnv.environmentId;
8240
- logger$4.log(` Using environment: ${matchingEnv.name}`);
8241
- } else {
8242
- logger$4.log(` Creating "${stage}" environment...`);
8243
- environmentId = (await api.createEnvironment(project.projectId, stage)).environmentId;
8244
- logger$4.log(` ✓ Created environment: ${stage}`);
8245
- }
8246
- let state = previous;
8247
- if (state) {
8248
- logger$4.log(` Found existing state for stage "${stage}"`);
8249
- if (state.projectId !== project.projectId) {
8250
- logger$4.log(` ⚠ Project ID changed, updating state`);
8251
- state.projectId = project.projectId;
8252
- }
8253
- if (state.environmentId !== environmentId) {
8254
- logger$4.log(` ⚠ Environment ID changed, updating state`);
8255
- state.environmentId = environmentId;
8256
- }
8257
- } else {
8258
- logger$4.log(` Creating new state for stage "${stage}"`);
8259
- state = createEmptyState(stage, project.projectId, environmentId);
8260
- }
9012
+ const environmentEntry = {
9013
+ key: "environment",
9014
+ type: "environment",
9015
+ data: { name: stage }
9016
+ };
9017
+ const environment = await journal.ensure(environmentEntry, {
9018
+ find: async () => project.environments.find((e) => e.name.toLowerCase() === stage.toLowerCase()),
9019
+ create: () => {
9020
+ logger$4.log(` Creating "${stage}" environment...`);
9021
+ return api.createEnvironment(project.projectId, stage);
9022
+ },
9023
+ id: (e) => e.environmentId
9024
+ });
9025
+ const environmentId = environment.resource.environmentId;
9026
+ if (environment.via === "created") logger$4.log(` ✓ Created environment: ${stage}`);
9027
+ else logger$4.log(` Using environment: ${environment.resource.name}`);
9028
+ if (journal.existed) {
9029
+ if (state.projectId && state.projectId !== project.projectId) logger$4.log(` ⚠ Project ID changed, updating state`);
9030
+ if (state.environmentId && state.environmentId !== environmentId) logger$4.log(` ⚠ Environment ID changed, updating state`);
9031
+ }
9032
+ state.projectId = project.projectId;
9033
+ state.environmentId = environmentId;
8261
9034
  state.identity = identity.key;
8262
9035
  logger$4.log("\n🐳 Checking registry...");
8263
9036
  const registry = workspace.deploy.dokploy?.registry;
@@ -8279,6 +9052,7 @@ async function workspaceDeployCommand(configured, options) {
8279
9052
  const registryId = dokployRegistry.registryId;
8280
9053
  state.registryId = registryId;
8281
9054
  logger$4.log(` Using registry: ${dokployRegistry.registryName}`);
9055
+ await journal.save();
8282
9056
  const skippedMobileApps = appsToDeployNames.filter((name) => workspace.apps[name].type === "mobile");
8283
9057
  if (skippedMobileApps.length > 0) logger$4.log(`\n📱 Skipping ${skippedMobileApps.length} mobile app(s) — deploy via framework toolchain: ${skippedMobileApps.join(", ")}`);
8284
9058
  const backendApps = appsToDeployNames.filter((name) => workspace.apps[name].type === "backend");
@@ -8336,7 +9110,7 @@ async function workspaceDeployCommand(configured, options) {
8336
9110
  const backupSchedule = workspace.deploy.backups.schedule ?? "0 2 * * *";
8337
9111
  const backupRetention = workspace.deploy.backups.retention ?? 30;
8338
9112
  logger$4.log(" Creating postgres backup schedule...");
8339
- const backup = await api.createPostgresBackup({
9113
+ setPostgresBackupId(state, (await api.createPostgresBackup({
8340
9114
  schedule: backupSchedule,
8341
9115
  prefix: `${stage}/postgres`,
8342
9116
  destinationId: backupState.destinationId,
@@ -8344,8 +9118,7 @@ async function workspaceDeployCommand(configured, options) {
8344
9118
  postgresId: backupCluster.postgresId,
8345
9119
  enabled: true,
8346
9120
  keepLatestCount: backupRetention
8347
- });
8348
- setPostgresBackupId(state, backup.backupId);
9121
+ })).backupId);
8349
9122
  logger$4.log(` ✓ Postgres backup schedule created (${backupSchedule})`);
8350
9123
  } else logger$4.log(" ✓ Using existing postgres backup schedule");
8351
9124
  }
@@ -8366,22 +9139,7 @@ async function workspaceDeployCommand(configured, options) {
8366
9139
  const app = workspace.apps[appName];
8367
9140
  logger$4.log(`\n ⚙️ Deploying ${appName}...`);
8368
9141
  try {
8369
- const dokployAppName = applicationName(identity, appName);
8370
- let application = null;
8371
- const cachedAppId = getApplicationId(state, appName);
8372
- if (cachedAppId) {
8373
- logger$4.log(` Using cached ID: ${cachedAppId}`);
8374
- application = await api.getApplication(cachedAppId);
8375
- if (application) logger$4.log(` ✓ Application found: ${application.applicationId}`);
8376
- else logger$4.log(` ⚠ Cached ID invalid, will create new`);
8377
- }
8378
- if (!application) {
8379
- const result = await api.findOrCreateApplication(dokployAppName, project.projectId, environmentId);
8380
- application = result.application;
8381
- if (result.created) logger$4.log(` Created application: ${application.applicationId}`);
8382
- else logger$4.log(` Found existing application: ${application.applicationId}`);
8383
- }
8384
- setApplicationId(state, appName, application.applicationId);
9142
+ const application = await ensureApplication(api, journal, appName, applicationName(identity, appName), project.projectId, environmentId);
8385
9143
  const appSecrets = encryptedSecrets.get(appName);
8386
9144
  const credentials = appSecrets && appSecrets.secretCount > 0 ? {
8387
9145
  encrypted: appSecrets.payload.encrypted,
@@ -8405,6 +9163,7 @@ async function workspaceDeployCommand(configured, options) {
8405
9163
  ref: built.imageRef ?? imageRef$1,
8406
9164
  ...built.digest ? { digest: built.digest } : {}
8407
9165
  });
9166
+ await journal.save();
8408
9167
  const backendHost = resolveHost(appName, app, stage, workspace.deploy?.domains, false);
8409
9168
  const dependencyUrls = {};
8410
9169
  if (app.dependencies) {
@@ -8439,30 +9198,10 @@ async function workspaceDeployCommand(configured, options) {
8439
9198
  await api.saveApplicationEnv(application.applicationId, envVars.join("\n"));
8440
9199
  logger$4.log(` Deploying to Dokploy...`);
8441
9200
  await api.deployApplication(application.applicationId);
8442
- const existingDomain = (await api.getDomainsByApplicationId(application.applicationId)).find((d) => d.host === backendHost);
8443
- if (existingDomain) {
8444
- appHostnames.set(appName, backendHost);
8445
- appDomainIds.set(appName, existingDomain.domainId);
8446
- publicUrls[appName] = `https://${backendHost}`;
8447
- logger$4.log(` ✓ Domain: https://${backendHost} (existing)`);
8448
- } else try {
8449
- const domain = await api.createDomain({
8450
- host: backendHost,
8451
- port: app.port,
8452
- https: true,
8453
- certificateType: "letsencrypt",
8454
- applicationId: application.applicationId
8455
- });
8456
- appHostnames.set(appName, backendHost);
8457
- appDomainIds.set(appName, domain.domainId);
8458
- publicUrls[appName] = `https://${backendHost}`;
8459
- logger$4.log(` ✓ Domain: https://${backendHost} (created)`);
8460
- } catch (domainError) {
8461
- const message = domainError instanceof Error ? domainError.message : "Unknown error";
8462
- logger$4.log(` ⚠ Domain creation failed: ${message}`);
8463
- appHostnames.set(appName, backendHost);
8464
- publicUrls[appName] = `https://${backendHost}`;
8465
- }
9201
+ const domainId = await ensureDomain(api, journal, backendHost, app.port, application.applicationId);
9202
+ appHostnames.set(appName, backendHost);
9203
+ if (domainId) appDomainIds.set(appName, domainId);
9204
+ publicUrls[appName] = `https://${backendHost}`;
8466
9205
  results.push({
8467
9206
  appName,
8468
9207
  type: app.type,
@@ -8491,22 +9230,7 @@ async function workspaceDeployCommand(configured, options) {
8491
9230
  const app = workspace.apps[appName];
8492
9231
  logger$4.log(`\n 🌐 Deploying ${appName}...`);
8493
9232
  try {
8494
- const dokployAppName = applicationName(identity, appName);
8495
- let application = null;
8496
- const cachedAppId = getApplicationId(state, appName);
8497
- if (cachedAppId) {
8498
- logger$4.log(` Using cached ID: ${cachedAppId}`);
8499
- application = await api.getApplication(cachedAppId);
8500
- if (application) logger$4.log(` ✓ Application found: ${application.applicationId}`);
8501
- else logger$4.log(` ⚠ Cached ID invalid, will create new`);
8502
- }
8503
- if (!application) {
8504
- const result = await api.findOrCreateApplication(dokployAppName, project.projectId, environmentId);
8505
- application = result.application;
8506
- if (result.created) logger$4.log(` Created application: ${application.applicationId}`);
8507
- else logger$4.log(` Found existing application: ${application.applicationId}`);
8508
- }
8509
- setApplicationId(state, appName, application.applicationId);
9233
+ const application = await ensureApplication(api, journal, appName, applicationName(identity, appName), project.projectId, environmentId);
8510
9234
  const dependencyUrls = {};
8511
9235
  if (app.dependencies) {
8512
9236
  for (const dep of app.dependencies) if (publicUrls[dep]) dependencyUrls[dep] = publicUrls[dep];
@@ -8554,6 +9278,7 @@ async function workspaceDeployCommand(configured, options) {
8554
9278
  ref: built.imageRef ?? imageRef$2,
8555
9279
  ...built.digest ? { digest: built.digest } : {}
8556
9280
  });
9281
+ await journal.save();
8557
9282
  const envVars = [
8558
9283
  `NODE_ENV=production`,
8559
9284
  `PORT=${app.port}`,
@@ -8564,30 +9289,10 @@ async function workspaceDeployCommand(configured, options) {
8564
9289
  await api.saveApplicationEnv(application.applicationId, envVars.join("\n"));
8565
9290
  logger$4.log(` Deploying to Dokploy...`);
8566
9291
  await api.deployApplication(application.applicationId);
8567
- const existingFrontendDomain = (await api.getDomainsByApplicationId(application.applicationId)).find((d) => d.host === frontendHost);
8568
- if (existingFrontendDomain) {
8569
- appHostnames.set(appName, frontendHost);
8570
- appDomainIds.set(appName, existingFrontendDomain.domainId);
8571
- publicUrls[appName] = `https://${frontendHost}`;
8572
- logger$4.log(` ✓ Domain: https://${frontendHost} (existing)`);
8573
- } else try {
8574
- const domain = await api.createDomain({
8575
- host: frontendHost,
8576
- port: app.port,
8577
- https: true,
8578
- certificateType: "letsencrypt",
8579
- applicationId: application.applicationId
8580
- });
8581
- appHostnames.set(appName, frontendHost);
8582
- appDomainIds.set(appName, domain.domainId);
8583
- publicUrls[appName] = `https://${frontendHost}`;
8584
- logger$4.log(` ✓ Domain: https://${frontendHost} (created)`);
8585
- } catch (domainError) {
8586
- const message = domainError instanceof Error ? domainError.message : "Unknown error";
8587
- logger$4.log(` ⚠ Domain creation failed: ${message}`);
8588
- appHostnames.set(appName, frontendHost);
8589
- publicUrls[appName] = `https://${frontendHost}`;
8590
- }
9292
+ const domainId = await ensureDomain(api, journal, frontendHost, app.port, application.applicationId);
9293
+ appHostnames.set(appName, frontendHost);
9294
+ if (domainId) appDomainIds.set(appName, domainId);
9295
+ publicUrls[appName] = `https://${frontendHost}`;
8591
9296
  results.push({
8592
9297
  appName,
8593
9298
  type: app.type,
@@ -8610,14 +9315,14 @@ async function workspaceDeployCommand(configured, options) {
8610
9315
  }
8611
9316
  }
8612
9317
  logger$4.log("\n📋 Saving deploy state...");
8613
- await stateProvider.write(stage, state);
9318
+ await journal.save();
8614
9319
  logger$4.log(" ✓ State saved");
8615
9320
  const dnsConfig = workspace.deploy.dns;
8616
9321
  if (dnsConfig && appHostnames.size > 0) {
8617
9322
  const dnsResult = await orchestrateDns(appHostnames, dnsConfig, creds.endpoint);
8618
9323
  if (dnsResult?.serverIp && appHostnames.size > 0) {
8619
9324
  await verifyDnsRecords(appHostnames, dnsResult.serverIp, state);
8620
- await stateProvider.write(stage, state);
9325
+ await journal.save();
8621
9326
  }
8622
9327
  if (dnsResult?.success && appHostnames.size > 0) {
8623
9328
  logger$4.log("\n🔒 Validating domains for SSL certificates...");
@@ -8650,6 +9355,71 @@ async function workspaceDeployCommand(configured, options) {
8650
9355
  };
8651
9356
  }
8652
9357
  /**
9358
+ * An app's Dokploy application, through the journal: by the id recorded for
9359
+ * it, else by its name in the stage's environment, else created. Its id goes
9360
+ * into `state.applications` too, which the env resolver and `state:show` read.
9361
+ */
9362
+ async function ensureApplication(api, journal, appName, dokployAppName, projectId, environmentId) {
9363
+ const recorded = journal.record(`application:${appName}`);
9364
+ if (recorded?.status === "ready" && recorded.id) logger$4.log(` Using cached ID: ${recorded.id}`);
9365
+ const { resource, via, staleId } = await journal.ensure({
9366
+ key: `application:${appName}`,
9367
+ type: "application",
9368
+ data: { name: dokployAppName }
9369
+ }, {
9370
+ get: (id) => api.getApplication(id),
9371
+ find: () => api.findApplicationByName(projectId, dokployAppName, environmentId),
9372
+ create: () => api.createApplication(dokployAppName, projectId, environmentId),
9373
+ id: (application) => application.applicationId
9374
+ });
9375
+ if (staleId) logger$4.log(` ⚠ Cached ID invalid, will create new`);
9376
+ logger$4.log(` ${{
9377
+ recorded: "✓ Application found",
9378
+ resumed: "Resumed application a stopped run created",
9379
+ found: "Found existing application",
9380
+ created: "Created application"
9381
+ }[via]}: ${resource.applicationId}`);
9382
+ setApplicationId(journal.state, appName, resource.applicationId);
9383
+ return resource;
9384
+ }
9385
+ /**
9386
+ * The domain `host` routes to an application, through the journal — found on
9387
+ * the application before it is created, so a re-run never adds a second.
9388
+ *
9389
+ * A domain that cannot be created is reported and skipped rather than
9390
+ * failing the app: the application is deployed and reachable on Dokploy's
9391
+ * own address, and DNS is fixed by hand more easily than a half deploy.
9392
+ */
9393
+ async function ensureDomain(api, journal, host, port, applicationId) {
9394
+ try {
9395
+ const { resource, via } = await journal.ensure({
9396
+ key: `domain:${host}`,
9397
+ type: "domain",
9398
+ data: {
9399
+ host,
9400
+ applicationId
9401
+ }
9402
+ }, {
9403
+ find: async () => (await api.getDomainsByApplicationId(applicationId)).find((d) => d.host === host),
9404
+ create: () => api.createDomain({
9405
+ host,
9406
+ port,
9407
+ https: true,
9408
+ certificateType: "letsencrypt",
9409
+ applicationId
9410
+ }),
9411
+ id: (domain) => domain.domainId
9412
+ });
9413
+ logger$4.log(` ✓ Domain: https://${host} (${via === "created" ? "created" : "existing"})`);
9414
+ return resource.domainId;
9415
+ } catch (domainError) {
9416
+ if (domainError instanceof StateVersionConflict || domainError instanceof StateStoreBusy) throw domainError;
9417
+ const message = domainError instanceof Error ? domainError.message : "Unknown error";
9418
+ logger$4.log(` ⚠ Domain creation failed: ${message}`);
9419
+ return;
9420
+ }
9421
+ }
9422
+ /**
8653
9423
  * Main deploy command
8654
9424
  */
8655
9425
  async function deployCommand(options) {
@@ -8786,453 +9556,160 @@ async function deployListCommand(options) {
8786
9556
  }
8787
9557
  }
8788
9558
  //#endregion
8789
- //#region src/deploy/CachedStateProvider.ts
8790
- var CachedStateProvider_exports = /* @__PURE__ */ __exportAll({ CachedStateProvider: () => CachedStateProvider });
8791
- /**
8792
- * Cached state provider that wraps a remote provider with local cache.
8793
- *
8794
- * @deprecated for deploys: a read prefers the local copy, so a stale one wins
8795
- * over the remote without warning, and a write overwrites the remote blindly.
8796
- * Deploy uses `createStateStore`, which reads SSM directly and writes
8797
- * conditionally. Kept for `gkm state:pull | push | diff`.
8798
- */
8799
- var CachedStateProvider = class {
8800
- remote;
8801
- local;
8802
- constructor(remote, local) {
8803
- this.remote = remote;
8804
- this.local = local;
8805
- }
8806
- async read(stage) {
8807
- const localState = await this.local.read(stage);
8808
- if (localState) return localState;
8809
- const remoteState = await this.remote.read(stage);
8810
- if (remoteState) await this.local.write(stage, remoteState);
8811
- return remoteState;
8812
- }
8813
- async write(stage, state) {
8814
- await this.remote.write(stage, state);
8815
- await this.local.write(stage, state);
8816
- }
8817
- /**
8818
- * Force pull from remote to local.
8819
- * Used by `gkm state pull` command.
8820
- */
8821
- async pull(stage) {
8822
- const remoteState = await this.remote.read(stage);
8823
- if (remoteState) await this.local.write(stage, remoteState);
8824
- return remoteState;
8825
- }
8826
- /**
8827
- * Force push from local to remote.
8828
- * Used by `gkm state push` command.
8829
- */
8830
- async push(stage) {
8831
- const localState = await this.local.read(stage);
8832
- if (localState) await this.remote.write(stage, localState);
8833
- return localState;
8834
- }
8835
- /**
8836
- * Get both local and remote state for comparison.
8837
- * Used by `gkm state diff` command.
8838
- */
8839
- async diff(stage) {
8840
- const [local, remote] = await Promise.all([this.local.read(stage), this.remote.read(stage)]);
8841
- return {
8842
- local,
8843
- remote
8844
- };
8845
- }
8846
- };
8847
- //#endregion
8848
- //#region src/deploy/StateStore.ts
9559
+ //#region src/deploy/LocalStateStore.ts
8849
9560
  /**
8850
- * Deploy state store.
8851
- *
8852
- * `StateProvider` reads and writes a whole stage at once, with nothing to stop
8853
- * two runs of the same stage from overwriting each other and nowhere to record
8854
- * a resource the moment it exists. A `StateStore` adds the three things a
8855
- * deploy needs to be safe against crashes and concurrent runs:
9561
+ * Local filesystem state store.
8856
9562
  *
8857
- * - `lock(stage)`: one run per stage. A second gets `StateLocked`, naming the
8858
- * holder, instead of racing the first.
8859
- * - Versioned `read` and conditional `write`: a write names the version it was
8860
- * based on, and raises `StateVersionConflict` when the stored state moved on
8861
- * underneath it, so nothing is lost silently even without the lock.
8862
- * - Per-resource records: `putResource` journals one resource at a time, so a
8863
- * run that dies half way leaves behind the ids of everything it created.
8864
- *
8865
- * Built-in stores: `LocalStateStore`, `SSMStateStore`, `S3StateStore`. A
8866
- * custom `StateProvider` is wrapped by `LegacyStateStore`, which keeps it
8867
- * working but cannot lock.
8868
- */
8869
- var StateStore_exports = /* @__PURE__ */ __exportAll({
8870
- DocumentStateStore: () => DocumentStateStore,
8871
- STATE_SCHEMA_VERSION: () => 2,
8872
- StageStateMissing: () => StageStateMissing,
8873
- StateLocked: () => StateLocked,
8874
- StateSchemaTooNew: () => StateSchemaTooNew,
8875
- StateStoreBusy: () => StateStoreBusy,
8876
- StateStoreNeedsWorkspaceName: () => StateStoreNeedsWorkspaceName,
8877
- StateStoreProvider: () => StateStoreProvider,
8878
- StateStoreWithoutLocking: () => StateStoreWithoutLocking,
8879
- StateUnreadable: () => StateUnreadable,
8880
- StateVersionConflict: () => StateVersionConflict,
8881
- UnknownStateProvider: () => UnknownStateProvider,
8882
- contentVersion: () => contentVersion,
8883
- createStateStore: () => createStateStore,
8884
- decodeDocument: () => decodeDocument,
8885
- encodeDocument: () => encodeDocument,
8886
- isStateStore: () => isStateStore,
8887
- migrateV1: () => migrateV1,
8888
- newLockHolder: () => newLockHolder
8889
- });
8890
- var StateLocked = class extends Error {
8891
- stage;
8892
- holder;
8893
- location;
8894
- constructor(stage, holder, location) {
8895
- 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";
8896
- 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}\`.`);
8897
- this.stage = stage;
8898
- this.holder = holder;
8899
- this.location = location;
8900
- this.name = "StateLocked";
8901
- }
8902
- };
8903
- var StateVersionConflict = class extends Error {
8904
- stage;
8905
- expectedVersion;
8906
- actualVersion;
8907
- constructor(stage, expectedVersion, actualVersion) {
8908
- 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.`);
8909
- this.stage = stage;
8910
- this.expectedVersion = expectedVersion;
8911
- this.actualVersion = actualVersion;
8912
- this.name = "StateVersionConflict";
8913
- }
8914
- };
8915
- var StageStateMissing = class extends Error {
8916
- stage;
8917
- constructor(stage) {
8918
- super(`There is no deploy state for stage '${stage}' to record a resource in. Write the stage's state first (write with expectedVersion: null).`);
8919
- this.stage = stage;
8920
- this.name = "StageStateMissing";
8921
- }
8922
- };
8923
- var StateUnreadable = class extends Error {
8924
- stage;
8925
- location;
8926
- constructor(stage, location, cause) {
8927
- 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 });
8928
- this.stage = stage;
8929
- this.location = location;
8930
- this.name = "StateUnreadable";
8931
- }
8932
- };
8933
- var StateSchemaTooNew = class extends Error {
8934
- stage;
8935
- schemaVersion;
8936
- constructor(stage, schemaVersion) {
8937
- 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.`);
8938
- this.stage = stage;
8939
- this.schemaVersion = schemaVersion;
8940
- this.name = "StateSchemaTooNew";
8941
- }
8942
- };
9563
+ * `.gkm/deploy-<stage>.json` holds the state, `.gkm/deploy-<stage>.lock` the
9564
+ * lock. Every file is mode 0600: state holds database passwords and IAM keys.
9565
+ */
9566
+ var LocalStateStore_exports = /* @__PURE__ */ __exportAll({ LocalStateStore: () => LocalStateStore });
9567
+ const FILE_MODE = 384;
9568
+ /** How long a write waits for another process's write to finish. */
9569
+ const MUTEX_TIMEOUT_MS = 5e3;
8943
9570
  /**
8944
- * Not thrown: emitted (once per store) when a custom `StateProvider` stands
8945
- * in for a store, so a deploy that cannot be protected from a concurrent one
8946
- * says so.
9571
+ * A write holds the mutex for milliseconds, so one this old was left by a
9572
+ * process that died mid-write and is safe to take over.
8947
9573
  */
8948
- var StateStoreWithoutLocking = class extends Error {
8949
- provider;
8950
- constructor(provider) {
8951
- 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.`);
8952
- this.provider = provider;
8953
- this.name = "StateStoreWithoutLocking";
9574
+ const MUTEX_STALE_MS = 3e4;
9575
+ function isCode(error, code) {
9576
+ return error?.code === code;
9577
+ }
9578
+ var LocalStateStore = class extends DocumentStateStore {
9579
+ workspaceRoot;
9580
+ constructor(workspaceRoot) {
9581
+ super();
9582
+ this.workspaceRoot = workspaceRoot;
8954
9583
  }
8955
- };
8956
- var StateStoreNeedsWorkspaceName = class extends Error {
8957
- provider;
8958
- constructor(provider) {
8959
- super(`The '${provider}' state provider keys state by workspace name. Set "name" in gkm.config.ts.`);
8960
- this.provider = provider;
8961
- this.name = "StateStoreNeedsWorkspaceName";
9584
+ get dir() {
9585
+ return join(this.workspaceRoot, ".gkm");
8962
9586
  }
8963
- };
8964
- var UnknownStateProvider = class extends Error {
8965
- config;
8966
- constructor(config) {
8967
- super(`Unknown state provider ${JSON.stringify(config)}. Use 'local', 'ssm', 's3', or an object implementing StateStore or StateProvider.`);
8968
- this.config = config;
8969
- this.name = "UnknownStateProvider";
9587
+ statePath(stage) {
9588
+ return join(this.dir, `deploy-${stage}.json`);
8970
9589
  }
8971
- };
8972
- /** A lock or write that could not get at the state backend in time. */
8973
- var StateStoreBusy = class extends Error {
8974
- stage;
8975
- location;
8976
- constructor(stage, location) {
8977
- super(`Timed out waiting to write deploy state for stage '${stage}' (${location}). If no other gkm process is running, remove ${location}.`);
8978
- this.stage = stage;
8979
- this.location = location;
8980
- this.name = "StateStoreBusy";
9590
+ lockPath(stage) {
9591
+ return join(this.dir, `deploy-${stage}.lock`);
8981
9592
  }
8982
- };
8983
- function decodeDocument(stage, body, location) {
8984
- let parsed;
8985
- try {
8986
- parsed = JSON.parse(body);
8987
- } catch (error) {
8988
- throw new StateUnreadable(stage, location, error);
9593
+ backupPath(stage) {
9594
+ return join(this.dir, `deploy-${stage}.v1.json`);
8989
9595
  }
8990
- if (typeof parsed !== "object" || parsed === null) throw new StateUnreadable(stage, location, parsed);
8991
- const schemaVersion = parsed.schemaVersion;
8992
- if (schemaVersion === void 0) return {
8993
- schemaVersion: 1,
8994
- state: parsed
8995
- };
8996
- if (schemaVersion === 2) return {
8997
- schemaVersion: 2,
8998
- document: parsed
8999
- };
9000
- throw new StateSchemaTooNew(stage, schemaVersion);
9001
- }
9002
- function encodeDocument(document) {
9003
- return JSON.stringify(document, null, 2);
9004
- }
9005
- /**
9006
- * v1 state as a v2 document. The ids v1 kept are seeded as `ready` records,
9007
- * so the first journaled deploy after an upgrade adopts them rather than
9008
- * treating them as unknown.
9009
- */
9010
- function migrateV1(stage, state) {
9011
- const now = (/* @__PURE__ */ new Date()).toISOString();
9012
- const resources = {};
9013
- const seed = (type, name, id) => {
9014
- if (!id) return;
9015
- const key = name ? `${type}:${name}` : type;
9016
- resources[key] = {
9017
- key,
9018
- type,
9019
- id,
9020
- status: "ready",
9021
- updatedAt: now
9022
- };
9023
- };
9024
- seed("project", null, state.projectId);
9025
- seed("environment", null, state.environmentId);
9026
- for (const [app, id] of Object.entries(state.applications ?? {})) seed("application", app, id);
9027
- seed("postgres", null, state.services?.postgresId);
9028
- seed("redis", null, state.services?.redisId);
9029
- seed("backup-destination", null, state.backups?.destinationId);
9030
- return {
9031
- schemaVersion: 2,
9032
- stage,
9033
- serial: 1,
9034
- state,
9035
- resources,
9036
- updatedAt: now
9037
- };
9038
- }
9039
- function newLockHolder(options) {
9040
- let owner = "unknown";
9041
- try {
9042
- owner = userInfo().username;
9043
- } catch {}
9044
- return {
9045
- id: randomUUID(),
9046
- owner,
9047
- host: hostname(),
9048
- pid: process.pid,
9049
- acquiredAt: (/* @__PURE__ */ new Date()).toISOString(),
9050
- operation: options?.operation
9051
- };
9052
- }
9053
- function contentVersion(body) {
9054
- return createHash("sha256").update(body).digest("hex").slice(0, 16);
9055
- }
9056
- /**
9057
- * A store over a backend that can read and conditionally write one blob per
9058
- * stage, and create a lock object only if it does not exist. Migration and
9059
- * resource records are the same for every such backend, so they live here.
9060
- */
9061
- var DocumentStateStore = class {
9062
- async lock(stage, options) {
9063
- const holder = newLockHolder(options);
9064
- await this.createLock(stage, holder);
9065
- let released = false;
9066
- return {
9067
- stage,
9068
- holder,
9069
- release: async () => {
9070
- if (released) return;
9071
- released = true;
9072
- await this.removeLock(stage, holder);
9073
- }
9074
- };
9596
+ mutexPath(stage) {
9597
+ return join(this.dir, `deploy-${stage}.json.mutex`);
9075
9598
  }
9076
- async forceUnlock(stage) {
9077
- const holder = await this.readLock(stage);
9078
- await this.removeLock(stage, null);
9079
- return holder;
9599
+ location(stage) {
9600
+ return this.statePath(stage);
9080
9601
  }
9081
- async read(stage) {
9082
- const current = await this.readDocument(stage);
9083
- if (!current) return null;
9084
- return {
9085
- state: current.document.state,
9086
- resources: current.document.resources,
9087
- version: current.version
9088
- };
9089
- }
9090
- async write(stage, state, options) {
9091
- if (options.expectedVersion === null) {
9092
- const document = {
9093
- schemaVersion: 2,
9094
- stage,
9095
- serial: 1,
9096
- state,
9097
- resources: {},
9098
- updatedAt: (/* @__PURE__ */ new Date()).toISOString()
9602
+ async readRaw(stage) {
9603
+ try {
9604
+ const body = await readFile(this.statePath(stage), "utf-8");
9605
+ return {
9606
+ body,
9607
+ version: contentVersion(body)
9099
9608
  };
9100
- return this.writeRaw(stage, encodeDocument(document), null);
9609
+ } catch (error) {
9610
+ if (isCode(error, "ENOENT")) return null;
9611
+ throw error;
9101
9612
  }
9102
- return this.mutate(stage, options.expectedVersion, (document) => ({
9103
- ...document,
9104
- state
9105
- }));
9106
9613
  }
9107
- async putResource(stage, record, options) {
9108
- return this.mutate(stage, options?.expectedVersion, (document) => ({
9109
- ...document,
9110
- resources: {
9111
- ...document.resources,
9112
- [record.key]: {
9113
- ...record,
9114
- updatedAt: (/* @__PURE__ */ new Date()).toISOString()
9614
+ async writeRaw(stage, body, expectedVersion) {
9615
+ await this.ensureDir();
9616
+ return this.withMutex(stage, async () => {
9617
+ const actual = (await this.readRaw(stage))?.version ?? null;
9618
+ if (actual !== expectedVersion) throw new StateVersionConflict(stage, expectedVersion, actual);
9619
+ const target = this.statePath(stage);
9620
+ const temp = `${target}.${randomUUID()}.tmp`;
9621
+ try {
9622
+ const handle = await open(temp, "wx", FILE_MODE);
9623
+ try {
9624
+ await handle.writeFile(body);
9625
+ await handle.sync();
9626
+ } finally {
9627
+ await handle.close();
9115
9628
  }
9629
+ await rename(temp, target);
9630
+ } catch (error) {
9631
+ await rm(temp, { force: true });
9632
+ throw error;
9116
9633
  }
9117
- }));
9118
- }
9119
- async deleteResource(stage, key, options) {
9120
- return this.mutate(stage, options?.expectedVersion, (document) => {
9121
- const { [key]: _removed, ...resources } = document.resources;
9122
- return {
9123
- ...document,
9124
- resources
9125
- };
9634
+ return contentVersion(body);
9126
9635
  });
9127
9636
  }
9128
- async mutate(stage, expectedVersion, change) {
9129
- const current = await this.readDocument(stage);
9130
- if (!current) {
9131
- if (expectedVersion !== void 0) throw new StateVersionConflict(stage, expectedVersion, null);
9132
- throw new StageStateMissing(stage);
9637
+ async writeV1Backup(stage, body) {
9638
+ try {
9639
+ await writeFile(this.backupPath(stage), body, {
9640
+ flag: "wx",
9641
+ mode: FILE_MODE
9642
+ });
9643
+ } catch (error) {
9644
+ if (!isCode(error, "EEXIST")) throw error;
9133
9645
  }
9134
- if (expectedVersion !== void 0 && current.version !== expectedVersion) throw new StateVersionConflict(stage, expectedVersion, current.version);
9135
- const next = change(current.document);
9136
- next.serial = current.document.serial + 1;
9137
- next.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
9138
- return this.writeRaw(stage, encodeDocument(next), current.version);
9139
9646
  }
9140
- /**
9141
- * Reads the stage's document, migrating v1 in place on the way. The
9142
- * migration is itself a conditional write: if another run migrated (or
9143
- * wrote) first, its result is read instead.
9144
- */
9145
- async readDocument(stage, attempt = 0) {
9146
- const raw = await this.readRaw(stage);
9147
- if (!raw) return null;
9148
- const decoded = decodeDocument(stage, raw.body, this.location(stage));
9149
- if (decoded.schemaVersion === 2) return {
9150
- document: decoded.document,
9151
- version: raw.version
9152
- };
9153
- await this.writeV1Backup(stage, raw.body);
9154
- const document = migrateV1(stage, decoded.state);
9647
+ async createLock(stage, holder) {
9648
+ await this.ensureDir();
9649
+ let handle;
9155
9650
  try {
9156
- return {
9157
- document,
9158
- version: await this.writeRaw(stage, encodeDocument(document), raw.version)
9159
- };
9651
+ handle = await open(this.lockPath(stage), "wx", FILE_MODE);
9160
9652
  } catch (error) {
9161
- if (error instanceof StateVersionConflict && attempt < 2) return this.readDocument(stage, attempt + 1);
9653
+ if (isCode(error, "EEXIST")) throw new StateLocked(stage, await this.readLock(stage), this.lockPath(stage));
9162
9654
  throw error;
9163
9655
  }
9656
+ try {
9657
+ await handle.writeFile(JSON.stringify(holder, null, 2));
9658
+ } finally {
9659
+ await handle.close();
9660
+ }
9164
9661
  }
9165
- };
9166
- function isStateStore(value) {
9167
- return typeof value === "object" && value !== null && typeof value.lock === "function" && typeof value.read === "function" && typeof value.write === "function" && typeof value.putResource === "function";
9168
- }
9169
- /**
9170
- * The store for a workspace's `state` config — the same config
9171
- * `createStateProvider` reads:
9172
- *
9173
- * - none / `'local'`: `LocalStateStore` under `.gkm/`
9174
- * - `'ssm'`: `SSMStateStore`, with no local cache — SSM is read directly, so
9175
- * a stale local copy can never win over it
9176
- * - `'s3'`: `S3StateStore`
9177
- * - a `StateStore` object: used as is
9178
- * - a `StateProvider` object: wrapped by `LegacyStateStore`
9179
- */
9180
- async function createStateStore(options) {
9181
- const { config, workspaceRoot, workspaceName } = options;
9182
- if (!config || config.provider === "local") {
9183
- const { LocalStateStore } = await import("./LocalStateStore-BupdV-VN.mjs");
9184
- return new LocalStateStore(workspaceRoot);
9185
- }
9186
- if (isStateStore(config.provider)) return config.provider;
9187
- if (isStateProvider(config.provider)) {
9188
- const { LegacyStateStore } = await import("./LegacyStateStore-ByRWQ9s1.mjs");
9189
- return new LegacyStateStore(config.provider, { warn: options.warn });
9190
- }
9191
- if (config.provider === "ssm") {
9192
- if (!workspaceName) throw new StateStoreNeedsWorkspaceName("ssm");
9193
- const { SSMStateStore } = await import("./SSMStateStore-BjZZm-nV.mjs");
9194
- const ssm = config;
9195
- return SSMStateStore.create({
9196
- workspaceName,
9197
- region: ssm.region,
9198
- profile: ssm.profile
9199
- });
9662
+ async readLock(stage) {
9663
+ try {
9664
+ return JSON.parse(await readFile(this.lockPath(stage), "utf-8"));
9665
+ } catch {
9666
+ return null;
9667
+ }
9200
9668
  }
9201
- if (config.provider === "s3") {
9202
- if (!workspaceName) throw new StateStoreNeedsWorkspaceName("s3");
9203
- const { S3StateStore } = await import("./S3StateStore-FhzyENrN.mjs");
9204
- const s3 = config;
9205
- return S3StateStore.create({
9206
- workspaceName,
9207
- bucket: s3.bucket,
9208
- prefix: s3.prefix,
9209
- region: s3.region,
9210
- profile: s3.profile
9211
- });
9669
+ async removeLock(stage, holder) {
9670
+ if (holder) {
9671
+ if ((await this.readLock(stage))?.id !== holder.id) return;
9672
+ }
9673
+ await rm(this.lockPath(stage), { force: true });
9212
9674
  }
9213
- throw new UnknownStateProvider(config);
9214
- }
9215
- /**
9216
- * A store behind the `StateProvider` interface, for the call sites that still
9217
- * read and write whole states (deploy, until it journals). A write is still
9218
- * conditional on the version this provider last read.
9219
- */
9220
- var StateStoreProvider = class {
9221
- store;
9222
- versions = /* @__PURE__ */ new Map();
9223
- constructor(store) {
9224
- this.store = store;
9675
+ async ensureDir() {
9676
+ await mkdir(this.dir, {
9677
+ recursive: true,
9678
+ mode: 448
9679
+ });
9225
9680
  }
9226
- async read(stage) {
9227
- const stored = await this.store.read(stage);
9228
- this.versions.set(stage, stored?.version ?? null);
9229
- return stored?.state ?? null;
9681
+ /**
9682
+ * Serialises the check-then-rename of a conditional write across
9683
+ * processes. The stage lock is for whole runs; this is for the instant of
9684
+ * a single write, so even writers that skip the lock never interleave.
9685
+ */
9686
+ async withMutex(stage, fn) {
9687
+ const path = this.mutexPath(stage);
9688
+ const deadline = Date.now() + MUTEX_TIMEOUT_MS;
9689
+ for (;;) try {
9690
+ await (await open(path, "wx", FILE_MODE)).close();
9691
+ break;
9692
+ } catch (error) {
9693
+ if (!isCode(error, "EEXIST")) throw error;
9694
+ if (await this.isStale(path)) {
9695
+ await unlink(path).catch(() => {});
9696
+ continue;
9697
+ }
9698
+ if (Date.now() > deadline) throw new StateStoreBusy(stage, path);
9699
+ await new Promise((resolve) => setTimeout(resolve, 10));
9700
+ }
9701
+ try {
9702
+ return await fn();
9703
+ } finally {
9704
+ await rm(path, { force: true });
9705
+ }
9230
9706
  }
9231
- async write(stage, state) {
9232
- const expectedVersion = this.versions.has(stage) ? this.versions.get(stage) ?? null : (await this.store.read(stage))?.version ?? null;
9233
- state.lastDeployedAt = (/* @__PURE__ */ new Date()).toISOString();
9234
- const version = await this.store.write(stage, state, { expectedVersion });
9235
- this.versions.set(stage, version);
9707
+ async isStale(path) {
9708
+ try {
9709
+ return Date.now() - (await stat(path)).mtimeMs > MUTEX_STALE_MS;
9710
+ } catch {
9711
+ return false;
9712
+ }
9236
9713
  }
9237
9714
  };
9238
9715
  //#endregion
@@ -9240,7 +9717,12 @@ var StateStoreProvider = class {
9240
9717
  /**
9241
9718
  * State Management CLI Commands
9242
9719
  *
9243
- * Commands for managing deployment state across local and remote providers.
9720
+ * Commands for managing deployment state across local and remote stores.
9721
+ *
9722
+ * Every command goes through a `StateStore`, the same one deploy writes
9723
+ * through: a v1 state is migrated on the way, resource records travel with
9724
+ * the state, and a copy is a conditional write — so `state:push` cannot
9725
+ * silently replace what a deploy wrote a moment earlier.
9244
9726
  */
9245
9727
  /** What `state:show` prints in place of a secret. */
9246
9728
  const MASKED = "********";
@@ -9266,26 +9748,12 @@ function maskStateSecrets(state) {
9266
9748
  */
9267
9749
  async function statePullCommand(options) {
9268
9750
  const { workspace } = await loadWorkspaceConfig();
9269
- if (!workspace.state || workspace.state.provider === "local") {
9270
- console.error("No remote state provider configured.");
9271
- console.error("Add a remote provider in gkm.config.ts:");
9272
- console.error(" state: { provider: \"ssm\", region: \"us-east-1\" }");
9273
- process.exit(1);
9274
- }
9275
- const provider = await createStateProvider({
9276
- config: workspace.state,
9277
- workspaceRoot: workspace.root,
9278
- workspaceName: workspace.name
9279
- });
9280
- if (!(provider instanceof CachedStateProvider)) {
9281
- console.error("State provider does not support pull operation.");
9282
- process.exit(1);
9283
- }
9751
+ const { local, remote } = await remoteAndLocal(workspace);
9284
9752
  console.log(`Pulling state for stage: ${options.stage}...`);
9285
- const state = await provider.pull(options.stage);
9286
- if (state) {
9753
+ const pulled = await copyUnderLock(remote, local, local, options.stage, "state:pull");
9754
+ if (pulled) {
9287
9755
  console.log("State pulled successfully.");
9288
- printStateSummary(state);
9756
+ printStateSummary(pulled.state);
9289
9757
  } else console.log("No remote state found for this stage.");
9290
9758
  }
9291
9759
  /**
@@ -9294,26 +9762,12 @@ async function statePullCommand(options) {
9294
9762
  */
9295
9763
  async function statePushCommand(options) {
9296
9764
  const { workspace } = await loadWorkspaceConfig();
9297
- if (!workspace.state || workspace.state.provider === "local") {
9298
- console.error("No remote state provider configured.");
9299
- console.error("Add a remote provider in gkm.config.ts:");
9300
- console.error(" state: { provider: \"ssm\", region: \"us-east-1\" }");
9301
- process.exit(1);
9302
- }
9303
- const provider = await createStateProvider({
9304
- config: workspace.state,
9305
- workspaceRoot: workspace.root,
9306
- workspaceName: workspace.name
9307
- });
9308
- if (!(provider instanceof CachedStateProvider)) {
9309
- console.error("State provider does not support push operation.");
9310
- process.exit(1);
9311
- }
9765
+ const { local, remote } = await remoteAndLocal(workspace);
9312
9766
  console.log(`Pushing state for stage: ${options.stage}...`);
9313
- const state = await provider.push(options.stage);
9314
- if (state) {
9767
+ const pushed = await copyUnderLock(local, remote, remote, options.stage, "state:push");
9768
+ if (pushed) {
9315
9769
  console.log("State pushed successfully.");
9316
- printStateSummary(state);
9770
+ printStateSummary(pushed.state);
9317
9771
  } else console.log("No local state found for this stage.");
9318
9772
  }
9319
9773
  /**
@@ -9322,7 +9776,7 @@ async function statePushCommand(options) {
9322
9776
  */
9323
9777
  async function stateShowCommand(options) {
9324
9778
  const { workspace } = await loadWorkspaceConfig();
9325
- const stored = await (await createStateProvider({
9779
+ const stored = await (await createStateStore({
9326
9780
  config: workspace.state,
9327
9781
  workspaceRoot: workspace.root,
9328
9782
  workspaceName: workspace.name
@@ -9331,9 +9785,12 @@ async function stateShowCommand(options) {
9331
9785
  console.log(`No state found for stage: ${options.stage}`);
9332
9786
  return;
9333
9787
  }
9334
- const state = maskStateSecrets(stored);
9788
+ const state = maskStateSecrets(stored.state);
9335
9789
  if (options.json) console.log(JSON.stringify(state, null, 2));
9336
- else printStateDetails(state);
9790
+ else {
9791
+ printStateDetails(state);
9792
+ printUnfinished(stored.resources);
9793
+ }
9337
9794
  }
9338
9795
  /**
9339
9796
  * Release a stage's deploy lock, whoever holds it.
@@ -9362,22 +9819,11 @@ async function stateUnlockCommand(options) {
9362
9819
  */
9363
9820
  async function stateDiffCommand(options) {
9364
9821
  const { workspace } = await loadWorkspaceConfig();
9365
- if (!workspace.state || workspace.state.provider === "local") {
9366
- console.error("No remote state provider configured.");
9367
- console.error("Diff requires a remote provider to compare against.");
9368
- process.exit(1);
9369
- }
9370
- const provider = await createStateProvider({
9371
- config: workspace.state,
9372
- workspaceRoot: workspace.root,
9373
- workspaceName: workspace.name
9374
- });
9375
- if (!(provider instanceof CachedStateProvider)) {
9376
- console.error("State provider does not support diff operation.");
9377
- process.exit(1);
9378
- }
9822
+ const stores = await remoteAndLocal(workspace, ["Diff requires a remote provider to compare against."]);
9379
9823
  console.log(`Comparing state for stage: ${options.stage}...\n`);
9380
- const { local, remote } = await provider.diff(options.stage);
9824
+ const [localStored, remoteStored] = await Promise.all([stores.local.read(options.stage), stores.remote.read(options.stage)]);
9825
+ const local = localStored?.state ?? null;
9826
+ const remote = remoteStored?.state ?? null;
9381
9827
  if (!local && !remote) {
9382
9828
  console.log("No state found (local or remote).");
9383
9829
  return;
@@ -9413,6 +9859,71 @@ async function stateDiffCommand(options) {
9413
9859
  else console.log(` ${key}: ${localVal ?? "(none)"} (local) != ${remoteVal ?? "(none)"} (remote)`);
9414
9860
  }
9415
9861
  }
9862
+ const localRecords = localStored?.resources ?? {};
9863
+ const remoteRecords = remoteStored?.resources ?? {};
9864
+ const differing = [.../* @__PURE__ */ new Set([...Object.keys(localRecords), ...Object.keys(remoteRecords)])].filter((key) => describeRecord(localRecords[key]) !== describeRecord(remoteRecords[key]));
9865
+ if (differing.length > 0) {
9866
+ console.log("\nResources:");
9867
+ for (const key of differing) console.log(` ${key}: ${describeRecord(localRecords[key])} (local) != ${describeRecord(remoteRecords[key])} (remote)`);
9868
+ }
9869
+ }
9870
+ /**
9871
+ * The stage's local store and its remote one — or, for a workspace that keeps
9872
+ * state only locally, the reason there is nothing to move it between.
9873
+ */
9874
+ async function remoteAndLocal(workspace, hint = ["Add a remote provider in gkm.config.ts:", " state: { provider: \"ssm\", region: \"us-east-1\" }"]) {
9875
+ if (!workspace.state || workspace.state.provider === "local") {
9876
+ console.error("No remote state provider configured.");
9877
+ for (const line of hint) console.error(line);
9878
+ process.exit(1);
9879
+ }
9880
+ return {
9881
+ local: new LocalStateStore(workspace.root),
9882
+ remote: await createStateStore({
9883
+ config: workspace.state,
9884
+ workspaceRoot: workspace.root,
9885
+ workspaceName: workspace.name
9886
+ })
9887
+ };
9888
+ }
9889
+ /**
9890
+ * Makes `to` hold exactly what `from` holds for `stage` — its state and its
9891
+ * resource records — while holding `locked`'s lock for the stage. Every
9892
+ * write is conditional on the version just read, so anything that changes
9893
+ * `to` mid-copy raises `StateVersionConflict` rather than being overwritten.
9894
+ */
9895
+ async function copyUnderLock(from, to, locked, stage, operation) {
9896
+ const lock = await locked.lock(stage, { operation });
9897
+ try {
9898
+ return await copyStage(from, to, stage);
9899
+ } finally {
9900
+ await lock.release();
9901
+ }
9902
+ }
9903
+ /** @internal Exported for testing */
9904
+ async function copyStage(from, to, stage) {
9905
+ const source = await from.read(stage);
9906
+ if (!source) return null;
9907
+ const target = await to.read(stage);
9908
+ let version = await to.write(stage, source.state, { expectedVersion: target?.version ?? null });
9909
+ for (const key of Object.keys(target?.resources ?? {})) {
9910
+ if (key in source.resources) continue;
9911
+ version = await to.deleteResource(stage, key, { expectedVersion: version });
9912
+ }
9913
+ for (const { updatedAt: _stamped, ...record } of Object.values(source.resources)) version = await to.putResource(stage, record, { expectedVersion: version });
9914
+ return source;
9915
+ }
9916
+ function describeRecord(record) {
9917
+ if (!record) return "(none)";
9918
+ return record.id ? `${record.status} ${record.id}` : record.status;
9919
+ }
9920
+ /** Resources a run marked `pending` and never saw created. */
9921
+ function printUnfinished(resources) {
9922
+ const pending = Object.values(resources).filter((record) => record.status === "pending");
9923
+ if (pending.length === 0) return;
9924
+ console.log("");
9925
+ console.log("Unfinished (a deploy stopped while creating these):");
9926
+ for (const record of pending) console.log(` ${record.key}`);
9416
9927
  }
9417
9928
  function printStateSummary(state) {
9418
9929
  const appCount = Object.keys(state.applications).length;
@@ -10460,25 +10971,25 @@ function loadPackageJson() {
10460
10971
  * Run: pnpm --filter @geekmidas/cli sync-versions
10461
10972
  */
10462
10973
  const GEEKMIDAS_VERSIONS = {
10463
- "@geekmidas/audit": "~10.0.0-alpha.58",
10464
- "@geekmidas/auth": "~10.0.0-alpha.58",
10465
- "@geekmidas/cache": "~10.0.0-alpha.58",
10466
- "@geekmidas/client": "~10.0.0-alpha.58",
10467
- "@geekmidas/cloud": "~10.0.0-alpha.58",
10468
- "@geekmidas/constructs": "~10.0.0-alpha.58",
10469
- "@geekmidas/db": "~10.0.0-alpha.58",
10470
- "@geekmidas/emailkit": "~10.0.0-alpha.58",
10471
- "@geekmidas/envkit": "~10.0.0-alpha.58",
10472
- "@geekmidas/errors": "~10.0.0-alpha.58",
10473
- "@geekmidas/events": "~10.0.0-alpha.58",
10474
- "@geekmidas/logger": "~10.0.0-alpha.58",
10475
- "@geekmidas/manifest": "~10.0.0-alpha.58",
10476
- "@geekmidas/rate-limit": "~10.0.0-alpha.58",
10477
- "@geekmidas/schema": "~10.0.0-alpha.58",
10478
- "@geekmidas/services": "~10.0.0-alpha.58",
10479
- "@geekmidas/storage": "~10.0.0-alpha.58",
10480
- "@geekmidas/telescope": "~10.0.0-alpha.58",
10481
- "@geekmidas/testkit": "~10.0.0-alpha.58",
10974
+ "@geekmidas/audit": "~10.0.0-alpha.59",
10975
+ "@geekmidas/auth": "~10.0.0-alpha.59",
10976
+ "@geekmidas/cache": "~10.0.0-alpha.59",
10977
+ "@geekmidas/client": "~10.0.0-alpha.59",
10978
+ "@geekmidas/cloud": "~10.0.0-alpha.59",
10979
+ "@geekmidas/constructs": "~10.0.0-alpha.59",
10980
+ "@geekmidas/db": "~10.0.0-alpha.59",
10981
+ "@geekmidas/emailkit": "~10.0.0-alpha.59",
10982
+ "@geekmidas/envkit": "~10.0.0-alpha.59",
10983
+ "@geekmidas/errors": "~10.0.0-alpha.59",
10984
+ "@geekmidas/events": "~10.0.0-alpha.59",
10985
+ "@geekmidas/logger": "~10.0.0-alpha.59",
10986
+ "@geekmidas/manifest": "~10.0.0-alpha.59",
10987
+ "@geekmidas/rate-limit": "~10.0.0-alpha.59",
10988
+ "@geekmidas/schema": "~10.0.0-alpha.59",
10989
+ "@geekmidas/services": "~10.0.0-alpha.59",
10990
+ "@geekmidas/storage": "~10.0.0-alpha.59",
10991
+ "@geekmidas/telescope": "~10.0.0-alpha.59",
10992
+ "@geekmidas/testkit": "~10.0.0-alpha.59",
10482
10993
  "@geekmidas/cli": `~${loadPackageJson().version}`
10483
10994
  };
10484
10995
  //#endregion
@@ -16360,7 +16871,7 @@ function testDatabases(reconciled) {
16360
16871
  * starts without the reference data the app needs.
16361
16872
  */
16362
16873
  async function migrateTestStage(workspace, env) {
16363
- const { migrateAndSeed } = await import("./migrate-CXVnh1J3.mjs");
16874
+ const { migrateAndSeed } = await import("./migrate-BrFREmgQ.mjs");
16364
16875
  const { discover } = await import("./discover-Cf2jAcgF.mjs").then((n) => n.n);
16365
16876
  const sources = {};
16366
16877
  const manifest = await discover({
@@ -16791,7 +17302,7 @@ program.command("migrate").description("Apply each database construct's migratio
16791
17302
  try {
16792
17303
  const globalOptions = program.opts();
16793
17304
  if (globalOptions.cwd) process.chdir(globalOptions.cwd);
16794
- const { migrateCommand } = await import("./migrate-CXVnh1J3.mjs");
17305
+ const { migrateCommand } = await import("./migrate-BrFREmgQ.mjs");
16795
17306
  await migrateCommand({
16796
17307
  ...options,
16797
17308
  ...construct ? { construct } : {}
@@ -16805,7 +17316,7 @@ program.command("seed").description("Migrate, then run each database construct's
16805
17316
  try {
16806
17317
  const globalOptions = program.opts();
16807
17318
  if (globalOptions.cwd) process.chdir(globalOptions.cwd);
16808
- const { seedCommand } = await import("./migrate-CXVnh1J3.mjs");
17319
+ const { seedCommand } = await import("./migrate-BrFREmgQ.mjs");
16809
17320
  await seedCommand({
16810
17321
  ...options,
16811
17322
  ...construct ? { construct } : {}
@@ -16819,7 +17330,7 @@ program.command("migration").description("Write a construct's next migration: an
16819
17330
  try {
16820
17331
  const globalOptions = program.opts();
16821
17332
  if (globalOptions.cwd) process.chdir(globalOptions.cwd);
16822
- const { migrationCommand } = await import("./migrate-CXVnh1J3.mjs");
17333
+ const { migrationCommand } = await import("./migrate-BrFREmgQ.mjs");
16823
17334
  await migrationCommand({
16824
17335
  construct,
16825
17336
  ...name ? { name } : {}
@@ -16917,7 +17428,7 @@ program.command("encryption:rotate").description("Add a new current key to an En
16917
17428
  try {
16918
17429
  const globalOptions = program.opts();
16919
17430
  if (globalOptions.cwd) process.chdir(globalOptions.cwd);
16920
- const { encryptionRotateCommand } = await import("./encryption-Dlnl5XEU.mjs");
17431
+ const { encryptionRotateCommand } = await import("./encryption-BW7Gh51G.mjs");
16921
17432
  await encryptionRotateCommand(construct, options);
16922
17433
  } catch (error) {
16923
17434
  console.error(formatError(error));
@@ -16928,7 +17439,7 @@ program.command("encryption:retire").description("Remove an old key from an Encr
16928
17439
  try {
16929
17440
  const globalOptions = program.opts();
16930
17441
  if (globalOptions.cwd) process.chdir(globalOptions.cwd);
16931
- const { encryptionRetireCommand } = await import("./encryption-Dlnl5XEU.mjs");
17442
+ const { encryptionRetireCommand } = await import("./encryption-BW7Gh51G.mjs");
16932
17443
  await encryptionRetireCommand(construct, key, options);
16933
17444
  } catch (error) {
16934
17445
  console.error(formatError(error));
@@ -16980,7 +17491,7 @@ program.command("secrets:reconcile").description("Backfill missing custom secret
16980
17491
  console.error(`No secrets found for stage "${stage}". Run "gkm secrets:init --stage ${stage}" first.`);
16981
17492
  process.exit(1);
16982
17493
  }
16983
- const { derivedContainers } = await import("./workspace-D-f58jsP.mjs").then((n) => n.o);
17494
+ const { derivedContainers } = await import("./workspace-BRZ14o-o.mjs").then((n) => n.o);
16984
17495
  const result = reconcileMissingSecrets(secrets, workspace, await derivedContainers(workspace, stage));
16985
17496
  if (!result) {
16986
17497
  console.log(`\n✓ Secrets for stage "${stage}" are up-to-date`);
@@ -17039,7 +17550,7 @@ program.command("deploy:github").description("Let GitHub Actions deploy a stage:
17039
17550
  try {
17040
17551
  const globalOptions = program.opts();
17041
17552
  if (globalOptions.cwd) process.chdir(globalOptions.cwd);
17042
- const { deployGithubCommand } = await import("./github-BdF9tsvl.mjs");
17553
+ const { deployGithubCommand } = await import("./github-T_6sK3Rz.mjs");
17043
17554
  await deployGithubCommand(options);
17044
17555
  } catch (error) {
17045
17556
  console.error(formatError(error));
@@ -17174,6 +17685,6 @@ program.command("upgrade").description("Upgrade @geekmidas/cli (with --all, ever
17174
17685
  });
17175
17686
  program.parse();
17176
17687
  //#endregion
17177
- export { StageStateMissing as a, StateStoreWithoutLocking as c, newLockHolder as d, encryption_exports as f, DocumentStateStore as i, StateVersionConflict as l, getHostingerToken as m, slug as n, StateLocked as o, appPorts_exports as p, stamp as r, StateStoreBusy as s, generateFullstackCustomSecrets as t, contentVersion as u };
17688
+ export { encryption_exports as a, LocalStateStore_exports as i, slug as n, appPorts_exports as o, stamp as r, getHostingerToken as s, generateFullstackCustomSecrets as t };
17178
17689
 
17179
17690
  //# sourceMappingURL=index.mjs.map