@geekmidas/cli 10.0.0-alpha.57 → 10.0.0-alpha.59
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{HostingerProvider-cL-6oQ5r.mjs → HostingerProvider-YQY3rMpV.mjs} +2 -2
- package/dist/{HostingerProvider-cL-6oQ5r.mjs.map → HostingerProvider-YQY3rMpV.mjs.map} +1 -1
- package/dist/{LegacyStateStore-ByRWQ9s1.mjs → LegacyStateStore-CB4ch3Dl.mjs} +2 -2
- package/dist/{LegacyStateStore-ByRWQ9s1.mjs.map → LegacyStateStore-CB4ch3Dl.mjs.map} +1 -1
- package/dist/{LegacyStateStore-C_OFs1gN.cjs → LegacyStateStore-CQJqvmXw.cjs} +7 -7
- package/dist/{LegacyStateStore-C_OFs1gN.cjs.map → LegacyStateStore-CQJqvmXw.cjs.map} +1 -1
- package/dist/{OpenApiTsGenerator-BLRQ7Iu9.cjs → OpenApiTsGenerator-BCWPRZwD.cjs} +153 -4
- package/dist/OpenApiTsGenerator-BCWPRZwD.cjs.map +1 -0
- package/dist/{OpenApiTsGenerator-NThANByQ.mjs → OpenApiTsGenerator-BRWD1Wit.mjs} +149 -6
- package/dist/OpenApiTsGenerator-BRWD1Wit.mjs.map +1 -0
- package/dist/{S3StateStore-FhzyENrN.mjs → S3StateStore-CXTenDTW.mjs} +2 -2
- package/dist/{S3StateStore-FhzyENrN.mjs.map → S3StateStore-CXTenDTW.mjs.map} +1 -1
- package/dist/{S3StateStore-C7eBae0G.cjs → S3StateStore-DLS4rEc0.cjs} +5 -5
- package/dist/{S3StateStore-C7eBae0G.cjs.map → S3StateStore-DLS4rEc0.cjs.map} +1 -1
- package/dist/{SSMStateStore-C7KcDSTb.cjs → SSMStateStore-EqsTBIOs.cjs} +10 -8
- package/dist/SSMStateStore-EqsTBIOs.cjs.map +1 -0
- package/dist/{SSMStateStore-7hPWW7v7.mjs → SSMStateStore-hnP5deFJ.mjs} +4 -2
- package/dist/SSMStateStore-hnP5deFJ.mjs.map +1 -0
- package/dist/StateStore-B4u0wx2t.cjs +412 -0
- package/dist/StateStore-B4u0wx2t.cjs.map +1 -0
- package/dist/StateStore-CYUfGYcx.mjs +359 -0
- package/dist/StateStore-CYUfGYcx.mjs.map +1 -0
- package/dist/{backends-BcHL7Gy_.mjs → backends-CjuCiE8h.mjs} +2 -2
- package/dist/{backends-BcHL7Gy_.mjs.map → backends-CjuCiE8h.mjs.map} +1 -1
- package/dist/{backends-_gQdAzBK.cjs → backends-v3xDuhO0.cjs} +2 -2
- package/dist/{backends-_gQdAzBK.cjs.map → backends-v3xDuhO0.cjs.map} +1 -1
- package/dist/{bundler-URImKj-A.mjs → bundler-ouDdNvk9.mjs} +2 -2
- package/dist/{bundler-URImKj-A.mjs.map → bundler-ouDdNvk9.mjs.map} +1 -1
- package/dist/{config-20V08LZp.mjs → config-EuzFEkcH.mjs} +2 -2
- package/dist/config-EuzFEkcH.mjs.map +1 -0
- package/dist/{config-CP6wsLHA.cjs → config-WMOB9bLL.cjs} +2 -2
- package/dist/config-WMOB9bLL.cjs.map +1 -0
- package/dist/config.cjs +2 -2
- package/dist/config.d.cts +65 -3
- package/dist/config.d.cts.map +1 -1
- package/dist/config.d.mts +65 -3
- package/dist/config.d.mts.map +1 -1
- package/dist/config.mjs +2 -2
- package/dist/{encryption-10Srqiix.cjs → encryption-BP8zfxEJ.cjs} +4 -4
- package/dist/{encryption-10Srqiix.cjs.map → encryption-BP8zfxEJ.cjs.map} +1 -1
- package/dist/{encryption-KQTYFEzU.mjs → encryption-BW7Gh51G.mjs} +4 -4
- package/dist/{encryption-KQTYFEzU.mjs.map → encryption-BW7Gh51G.mjs.map} +1 -1
- package/dist/{github-DtRUkHN8.cjs → github-DHpDMBT7.cjs} +3 -3
- package/dist/{github-DtRUkHN8.cjs.map → github-DHpDMBT7.cjs.map} +1 -1
- package/dist/{github-y7OEj59M.mjs → github-T_6sK3Rz.mjs} +3 -3
- package/dist/{github-y7OEj59M.mjs.map → github-T_6sK3Rz.mjs.map} +1 -1
- package/dist/{index-DMcjpvd-.d.cts → index-B5TJ-mB6.d.mts} +2 -2
- package/dist/{index-DMcjpvd-.d.cts.map → index-B5TJ-mB6.d.mts.map} +1 -1
- package/dist/{index-RM92lx8A.d.mts → index-DV6j4Ydq.d.cts} +2 -2
- package/dist/{index-RM92lx8A.d.mts.map → index-DV6j4Ydq.d.cts.map} +1 -1
- package/dist/index.cjs +1633 -771
- package/dist/index.cjs.map +1 -1
- package/dist/index.mjs +1648 -784
- package/dist/index.mjs.map +1 -1
- package/dist/{migrate-CS3DkO6y.mjs → migrate-BrFREmgQ.mjs} +4 -4
- package/dist/{migrate-CS3DkO6y.mjs.map → migrate-BrFREmgQ.mjs.map} +1 -1
- package/dist/{migrate-DQ54y2WD.cjs → migrate-CThJfVHR.cjs} +4 -4
- package/dist/{migrate-DQ54y2WD.cjs.map → migrate-CThJfVHR.cjs.map} +1 -1
- package/dist/openapi.cjs +2 -2
- package/dist/openapi.d.cts +1 -1
- package/dist/openapi.d.mts +1 -1
- package/dist/openapi.mjs +2 -2
- package/dist/{ready-BSpUZnNo.mjs → ready-DbDHneLR.mjs} +2 -2
- package/dist/ready-DbDHneLR.mjs.map +1 -0
- package/dist/{ready-MUhVJDHe.cjs → ready-mR96orkJ.cjs} +2 -2
- package/dist/ready-mR96orkJ.cjs.map +1 -0
- package/dist/reconcile/public.d.cts +1 -1
- package/dist/reconcile/public.d.mts +1 -1
- package/dist/{types-BIyIQUR-.d.cts → types-CipIIm8f.d.mts} +65 -3
- package/dist/types-CipIIm8f.d.mts.map +1 -0
- package/dist/{types-CmQl6zaP.d.mts → types-D9b-IYdW.d.cts} +66 -2
- package/dist/types-D9b-IYdW.d.cts.map +1 -0
- package/dist/vitest.cjs +1 -1
- package/dist/vitest.mjs +1 -1
- package/dist/workspace/index.cjs +1 -1
- package/dist/workspace/index.d.cts +2 -2
- package/dist/workspace/index.d.mts +2 -2
- package/dist/workspace/index.mjs +1 -1
- package/dist/{workspace-BpdXIeWV.cjs → workspace-B64Xk1Mb.cjs} +16 -4
- package/dist/workspace-B64Xk1Mb.cjs.map +1 -0
- package/dist/{workspace-CgWcE_Yr.mjs → workspace-BRZ14o-o.mjs} +11 -8
- package/dist/{workspace-CgWcE_Yr.mjs.map → workspace-BRZ14o-o.mjs.map} +1 -1
- package/dist/{workspace-C2mFV53i.mjs → workspace-Bp1xVaOz.mjs} +16 -4
- package/dist/workspace-Bp1xVaOz.mjs.map +1 -0
- package/dist/{workspace-BbPJVCto.cjs → workspace-Dbn07ILl.cjs} +9 -6
- package/dist/{workspace-BbPJVCto.cjs.map → workspace-Dbn07ILl.cjs.map} +1 -1
- package/package.json +13 -13
- package/dist/LocalStateProvider-7HR6SS2t.cjs +0 -48
- package/dist/LocalStateProvider-7HR6SS2t.cjs.map +0 -1
- package/dist/LocalStateProvider-D7QyJoLM.mjs +0 -48
- package/dist/LocalStateProvider-D7QyJoLM.mjs.map +0 -1
- package/dist/LocalStateStore-BupdV-VN.mjs +0 -163
- package/dist/LocalStateStore-BupdV-VN.mjs.map +0 -1
- package/dist/LocalStateStore-DDDU0HkI.cjs +0 -163
- package/dist/LocalStateStore-DDDU0HkI.cjs.map +0 -1
- package/dist/OpenApiTsGenerator-BLRQ7Iu9.cjs.map +0 -1
- package/dist/OpenApiTsGenerator-NThANByQ.mjs.map +0 -1
- package/dist/SSMStateProvider-B0Hu1MKZ.mjs +0 -75
- package/dist/SSMStateProvider-B0Hu1MKZ.mjs.map +0 -1
- package/dist/SSMStateProvider-VrQ-yeqd.cjs +0 -74
- package/dist/SSMStateProvider-VrQ-yeqd.cjs.map +0 -1
- package/dist/SSMStateStore-7hPWW7v7.mjs.map +0 -1
- package/dist/SSMStateStore-C7KcDSTb.cjs.map +0 -1
- package/dist/config-20V08LZp.mjs.map +0 -1
- package/dist/config-CP6wsLHA.cjs.map +0 -1
- package/dist/ready-BSpUZnNo.mjs.map +0 -1
- package/dist/ready-MUhVJDHe.cjs.map +0 -1
- package/dist/types-BIyIQUR-.d.cts.map +0 -1
- package/dist/types-CmQl6zaP.d.mts.map +0 -1
- package/dist/workspace-BpdXIeWV.cjs.map +0 -1
- package/dist/workspace-C2mFV53i.mjs.map +0 -1
package/dist/index.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-
|
|
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-
|
|
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-
|
|
9
|
-
import { n as EndpointGenerator, r as runtimeFor } from "./OpenApiTsGenerator-
|
|
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
|
+
import { i as telemetryFor, n as EndpointGenerator, r as runtimeFor } from "./OpenApiTsGenerator-BRWD1Wit.mjs";
|
|
10
10
|
import { generateOpenApi, openapiCommand, resolveOpenApiConfig } from "./openapi.mjs";
|
|
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-
|
|
12
|
-
import { i as storageBackendFor, n as eventsBackendFor, r as providerOf, t as cacheBackendFor } from "./backends-
|
|
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
|
+
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,
|
|
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 {
|
|
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.
|
|
49
|
+
var version = "10.0.0-alpha.59";
|
|
46
50
|
//#endregion
|
|
47
51
|
//#region src/auth/credentials.ts
|
|
48
52
|
/**
|
|
@@ -136,15 +140,6 @@ async function getDokployToken(options) {
|
|
|
136
140
|
return null;
|
|
137
141
|
}
|
|
138
142
|
/**
|
|
139
|
-
* Store Dokploy registry ID
|
|
140
|
-
*/
|
|
141
|
-
async function storeDokployRegistryId(registryId, options) {
|
|
142
|
-
const credentials = await readCredentials(options);
|
|
143
|
-
if (!credentials.dokploy) throw new Error("Dokploy credentials not found. Run \"gkm login --service dokploy\" first.");
|
|
144
|
-
credentials.dokploy.registryId = registryId;
|
|
145
|
-
await writeCredentials(credentials, options);
|
|
146
|
-
}
|
|
147
|
-
/**
|
|
148
143
|
* Get Dokploy registry ID from stored credentials
|
|
149
144
|
*/
|
|
150
145
|
async function getDokployRegistryId(options) {
|
|
@@ -765,6 +760,621 @@ async function assignAppPorts(workspace, names, deps) {
|
|
|
765
760
|
running
|
|
766
761
|
};
|
|
767
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
|
+
}
|
|
768
1378
|
//#endregion
|
|
769
1379
|
//#region ../../node_modules/.pnpm/@hono+node-server@1.19.5_hono@4.13.8/node_modules/@hono/node-server/dist/index.mjs
|
|
770
1380
|
var RequestError = class extends Error {
|
|
@@ -818,7 +1428,7 @@ var newRequestFromIncoming = (method, url, headers, incoming, abortController) =
|
|
|
818
1428
|
let reader;
|
|
819
1429
|
init.body = new ReadableStream({ async pull(controller) {
|
|
820
1430
|
try {
|
|
821
|
-
reader ||= Readable.toWeb(incoming).getReader();
|
|
1431
|
+
reader ||= Readable$1.toWeb(incoming).getReader();
|
|
822
1432
|
const { done, value } = await reader.read();
|
|
823
1433
|
if (done) controller.close();
|
|
824
1434
|
else controller.enqueue(value);
|
|
@@ -826,7 +1436,7 @@ var newRequestFromIncoming = (method, url, headers, incoming, abortController) =
|
|
|
826
1436
|
controller.error(error);
|
|
827
1437
|
}
|
|
828
1438
|
} });
|
|
829
|
-
} else init.body = Readable.toWeb(incoming);
|
|
1439
|
+
} else init.body = Readable$1.toWeb(incoming);
|
|
830
1440
|
}
|
|
831
1441
|
return new Request(url, init);
|
|
832
1442
|
};
|
|
@@ -1197,7 +1807,7 @@ var createAdaptorServer = (options) => {
|
|
|
1197
1807
|
overrideGlobalObjects: options.overrideGlobalObjects,
|
|
1198
1808
|
autoCleanupIncoming: options.autoCleanupIncoming
|
|
1199
1809
|
});
|
|
1200
|
-
return (options.createServer || createServer)(options.serverOptions || {}, requestListener);
|
|
1810
|
+
return (options.createServer || createServer$2)(options.serverOptions || {}, requestListener);
|
|
1201
1811
|
};
|
|
1202
1812
|
var serve = (options, listeningListener) => {
|
|
1203
1813
|
const server = createAdaptorServer(options);
|
|
@@ -1430,6 +2040,7 @@ async function devCommand(options) {
|
|
|
1430
2040
|
const runtime = config.runtime ?? "node";
|
|
1431
2041
|
let secretsJsonPath;
|
|
1432
2042
|
let publicUrl;
|
|
2043
|
+
let manifest;
|
|
1433
2044
|
const appSecrets = await loadSecretsForApp(workspace ? await secretsStoreFor(workspace, config.stages.local) : new FileSecretsStore(secretsRoot), config.stages.local, workspaceAppName);
|
|
1434
2045
|
if (workspace) {
|
|
1435
2046
|
const reconciled = await reconcileWorkspace(workspace, {
|
|
@@ -1444,6 +2055,7 @@ async function devCommand(options) {
|
|
|
1444
2055
|
Object.assign(appSecrets, reconciled.env);
|
|
1445
2056
|
const own = reconciled.plan.resources.find((r) => (r.kind === "rest-api" || r.kind === "site") && appKey(r.id) === workspaceAppName);
|
|
1446
2057
|
publicUrl = own ? reconciled.env[own.envKey] : void 0;
|
|
2058
|
+
manifest = reconciled.manifest;
|
|
1447
2059
|
}
|
|
1448
2060
|
if (Object.keys(appSecrets).length > 0) {
|
|
1449
2061
|
const secretsDir = join(secretsRoot, ".gkm");
|
|
@@ -1456,6 +2068,38 @@ async function devCommand(options) {
|
|
|
1456
2068
|
devServer.label = workspaceAppName ?? "server";
|
|
1457
2069
|
devServer.publicUrl = publicUrl;
|
|
1458
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
|
+
}));
|
|
1459
2103
|
const hooksFile = (config.hooks?.server?.split("#"))?.[0];
|
|
1460
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(/^\.\//, ""));
|
|
1461
2105
|
const resolvedFiles = await fg(normalizedPatterns, {
|
|
@@ -1496,7 +2140,11 @@ async function devCommand(options) {
|
|
|
1496
2140
|
if (isShuttingDown) return;
|
|
1497
2141
|
isShuttingDown = true;
|
|
1498
2142
|
logger$9.log("\n🛑 Shutting down...");
|
|
1499
|
-
Promise.all([
|
|
2143
|
+
Promise.all([
|
|
2144
|
+
watcher.close(),
|
|
2145
|
+
devServer.stop(),
|
|
2146
|
+
discovery.leave()
|
|
2147
|
+
]).catch((err) => {
|
|
1500
2148
|
logger$9.error("Error during shutdown:", err);
|
|
1501
2149
|
}).finally(() => {
|
|
1502
2150
|
process.exit(0);
|
|
@@ -1740,9 +2388,9 @@ async function workspaceDevCommand(configured, options) {
|
|
|
1740
2388
|
for (const { id, port } of fakes) logger$9.log(`🎭 ${id} fake: http://localhost:${port}`);
|
|
1741
2389
|
logger$9.log(options.migrate || options.seed ? "🗄️ Migrating the databases" : "🗄️ Checking for pending migrations");
|
|
1742
2390
|
if (options.migrate || options.seed) {
|
|
1743
|
-
const { prepareDevDatabases } = await import("./migrate-
|
|
2391
|
+
const { prepareDevDatabases } = await import("./migrate-BrFREmgQ.mjs");
|
|
1744
2392
|
await prepareDevDatabases(workspace, reconciled.env, { seed: !!options.seed });
|
|
1745
|
-
} else await import("./migrate-
|
|
2393
|
+
} else await import("./migrate-BrFREmgQ.mjs").then(({ reportPendingMigrations }) => reportPendingMigrations(workspace, reconciled.env)).catch((error) => {
|
|
1746
2394
|
logger$9.log(`⚠️ Could not check for pending migrations: ${error instanceof Error ? error.message : String(error)}`);
|
|
1747
2395
|
});
|
|
1748
2396
|
const appUrls = {};
|
|
@@ -1797,12 +2445,39 @@ async function workspaceDevCommand(configured, options) {
|
|
|
1797
2445
|
break;
|
|
1798
2446
|
}
|
|
1799
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);
|
|
1800
2474
|
const turboEnv = {
|
|
1801
2475
|
...process.env,
|
|
1802
2476
|
...secretsEnv,
|
|
1803
2477
|
NODE_ENV: "development",
|
|
1804
2478
|
...configPath ? { GKM_CONFIG_PATH: configPath } : {},
|
|
1805
|
-
...options.fake ? { [FAKE_ENV]: "1" } : {}
|
|
2479
|
+
...options.fake ? { [FAKE_ENV]: "1" } : {},
|
|
2480
|
+
[DISCOVERY_QUIET_ENV]: "1"
|
|
1806
2481
|
};
|
|
1807
2482
|
logger$9.log(`🚀 Starting ${appCount} app(s) — each builds before it answers, and says so when it is ready`);
|
|
1808
2483
|
const turboProcess = spawn("pnpm", [
|
|
@@ -1822,6 +2497,7 @@ async function workspaceDevCommand(configured, options) {
|
|
|
1822
2497
|
isShuttingDown = true;
|
|
1823
2498
|
logger$9.log("\n🛑 Shutting down workspace...");
|
|
1824
2499
|
closeFakes(fakes);
|
|
2500
|
+
discovery.leave();
|
|
1825
2501
|
const pid = turboProcess.pid;
|
|
1826
2502
|
if (pid) try {
|
|
1827
2503
|
process.kill(-pid, "SIGTERM");
|
|
@@ -2062,6 +2738,13 @@ start({
|
|
|
2062
2738
|
});
|
|
2063
2739
|
`;
|
|
2064
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
|
+
}
|
|
2065
2748
|
/** Run `fn` with `console.log` muted — warnings and errors still print. */
|
|
2066
2749
|
async function quietly(fn) {
|
|
2067
2750
|
const log = console.log;
|
|
@@ -2159,6 +2842,12 @@ var DevServer = class {
|
|
|
2159
2842
|
}
|
|
2160
2843
|
/** What the ready line calls this app. */
|
|
2161
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
|
+
}
|
|
2162
2851
|
/**
|
|
2163
2852
|
* Where the app is reached — its HTTPS address behind the edge, when it has
|
|
2164
2853
|
* one. The local port is only what the edge forwards to.
|
|
@@ -2194,9 +2883,11 @@ var DevServer = class {
|
|
|
2194
2883
|
this.serverProcess.on("error", (error) => {
|
|
2195
2884
|
logger$9.error("❌ Server error:", error);
|
|
2196
2885
|
});
|
|
2197
|
-
this.serverProcess
|
|
2886
|
+
const child = this.serverProcess;
|
|
2887
|
+
child.on("exit", (code, signal) => {
|
|
2198
2888
|
if (code !== null && code !== 0 && signal !== "SIGTERM") logger$9.error(`❌ Server exited with code ${code}`);
|
|
2199
2889
|
this.isRunning = false;
|
|
2890
|
+
if (this.serverProcess === child) this.onStatus?.("stopped");
|
|
2200
2891
|
});
|
|
2201
2892
|
await new Promise((resolve) => setTimeout(resolve, 1e3));
|
|
2202
2893
|
if (this.isRunning) {
|
|
@@ -2210,6 +2901,7 @@ var DevServer = class {
|
|
|
2210
2901
|
...this.databaseApi ? [`db ${this.databaseApi}`] : []
|
|
2211
2902
|
];
|
|
2212
2903
|
if (tools.length > 0) logger$9.log(` ${tools.join(" · ")}`);
|
|
2904
|
+
this.onStatus?.("ready");
|
|
2213
2905
|
}
|
|
2214
2906
|
}
|
|
2215
2907
|
async stop() {
|
|
@@ -2235,6 +2927,7 @@ var DevServer = class {
|
|
|
2235
2927
|
}
|
|
2236
2928
|
async restart() {
|
|
2237
2929
|
const portToReuse = this.actualPort;
|
|
2930
|
+
this.onStatus?.("reloading");
|
|
2238
2931
|
await this.stop();
|
|
2239
2932
|
let attempts = 0;
|
|
2240
2933
|
while (attempts < 30) {
|
|
@@ -2768,14 +3461,34 @@ var CommandTimedOut = class extends Error {
|
|
|
2768
3461
|
* once `timeoutMs` passes (after the child is gone), and with the spawn error
|
|
2769
3462
|
* itself when the program cannot be started at all — `ENOENT` already names it.
|
|
2770
3463
|
*/
|
|
2771
|
-
function run(command, args, options = {}) {
|
|
3464
|
+
async function run(command, args, options = {}) {
|
|
3465
|
+
await spawnChild(command, args, options, false);
|
|
3466
|
+
}
|
|
3467
|
+
/**
|
|
3468
|
+
* {@link run}, resolving with what the program wrote to stdout — for a value
|
|
3469
|
+
* the CLI reads back, such as the digest `docker inspect` reports. stderr still
|
|
3470
|
+
* reaches the terminal, so a failure explains itself the same way.
|
|
3471
|
+
*/
|
|
3472
|
+
function runOutput(command, args, options = {}) {
|
|
3473
|
+
return spawnChild(command, args, options, true);
|
|
3474
|
+
}
|
|
3475
|
+
function spawnChild(command, args, options, capture) {
|
|
2772
3476
|
const { timeoutMs = DEFAULT_TIMEOUT_MS, ...spawnOptions } = options;
|
|
2773
3477
|
return new Promise((resolve, reject) => {
|
|
2774
3478
|
const child = spawn(command, [...args], {
|
|
2775
3479
|
stdio: "inherit",
|
|
2776
3480
|
...spawnOptions,
|
|
3481
|
+
...capture ? { stdio: [
|
|
3482
|
+
"ignore",
|
|
3483
|
+
"pipe",
|
|
3484
|
+
"inherit"
|
|
3485
|
+
] } : {},
|
|
2777
3486
|
shell: false
|
|
2778
3487
|
});
|
|
3488
|
+
let stdout = "";
|
|
3489
|
+
child.stdout?.on("data", (chunk) => {
|
|
3490
|
+
stdout += chunk.toString();
|
|
3491
|
+
});
|
|
2779
3492
|
let timedOut = false;
|
|
2780
3493
|
let killer;
|
|
2781
3494
|
const timer = setTimeout(() => {
|
|
@@ -2796,7 +3509,7 @@ function run(command, args, options = {}) {
|
|
|
2796
3509
|
child.on("close", (code, signal) => {
|
|
2797
3510
|
settle();
|
|
2798
3511
|
if (timedOut) reject(new CommandTimedOut(command, args, timeoutMs));
|
|
2799
|
-
else if (code === 0) resolve();
|
|
3512
|
+
else if (code === 0) resolve(stdout);
|
|
2800
3513
|
else reject(new CommandFailed(command, args, code, signal));
|
|
2801
3514
|
});
|
|
2802
3515
|
});
|
|
@@ -3103,7 +3816,8 @@ async function buildOneApp(input) {
|
|
|
3103
3816
|
hooks,
|
|
3104
3817
|
markOptional: options.markOptional ?? false,
|
|
3105
3818
|
skipBundle: options.skipBundle ?? false,
|
|
3106
|
-
stage: options.stage
|
|
3819
|
+
stage: options.stage,
|
|
3820
|
+
workspaceName: workspace.name
|
|
3107
3821
|
});
|
|
3108
3822
|
}
|
|
3109
3823
|
/**
|
|
@@ -3181,7 +3895,12 @@ async function buildApp(input) {
|
|
|
3181
3895
|
appRoot,
|
|
3182
3896
|
cache: cacheBackendsIn(declared, cacheBackend)
|
|
3183
3897
|
}),
|
|
3184
|
-
markOptional: input.markOptional ?? false
|
|
3898
|
+
markOptional: input.markOptional ?? false,
|
|
3899
|
+
...production && { telemetry: telemetryFor({
|
|
3900
|
+
appRoot,
|
|
3901
|
+
surfaceId: derived.surface?.id,
|
|
3902
|
+
workspaceName: input.workspaceName
|
|
3903
|
+
}) }
|
|
3185
3904
|
};
|
|
3186
3905
|
const endpointGenerator = new EndpointGenerator();
|
|
3187
3906
|
const functionGenerator = new FunctionGenerator();
|
|
@@ -3282,7 +4001,7 @@ async function buildForTarget(target, context, appRoot, workspaceRoot, endpointG
|
|
|
3282
4001
|
let masterKey;
|
|
3283
4002
|
if (context.production?.bundle && !skipBundle) {
|
|
3284
4003
|
logger$8.log(`\n📦 Bundling production server...`);
|
|
3285
|
-
const { bundleServer } = await import("./bundler-
|
|
4004
|
+
const { bundleServer } = await import("./bundler-ouDdNvk9.mjs");
|
|
3286
4005
|
const allConstructs = [
|
|
3287
4006
|
...endpoints.map((e) => e.construct),
|
|
3288
4007
|
...functions.map((f) => f.construct),
|
|
@@ -3590,7 +4309,7 @@ const PROVISIONERS = {
|
|
|
3590
4309
|
const name = resourceName(declaration.id, declaration.kind, context.stage);
|
|
3591
4310
|
const { postgres } = await context.api.findOrCreatePostgres(serviceName({
|
|
3592
4311
|
stage: context.stage,
|
|
3593
|
-
app: context.project
|
|
4312
|
+
app: context.scope ?? context.project
|
|
3594
4313
|
}, declaration.id), context.projectId, context.environmentId, {
|
|
3595
4314
|
databaseName: name,
|
|
3596
4315
|
databasePassword: derivedPassword(context, `${name}:master`)
|
|
@@ -3724,7 +4443,7 @@ const PROVISIONERS = {
|
|
|
3724
4443
|
if (backend !== "minio") throw new UnprovisionableBucket(declaration.id, backend);
|
|
3725
4444
|
const service = serviceName({
|
|
3726
4445
|
stage: context.stage,
|
|
3727
|
-
app: context.project
|
|
4446
|
+
app: context.scope ?? context.project
|
|
3728
4447
|
}, declaration.id);
|
|
3729
4448
|
const bucket = resourceName(declaration.id, declaration.kind, context.stage);
|
|
3730
4449
|
const user = `${bucket}-root`;
|
|
@@ -4185,6 +4904,7 @@ async function provisionDeclared(options) {
|
|
|
4185
4904
|
environmentId: options.environmentId,
|
|
4186
4905
|
stage: options.stage,
|
|
4187
4906
|
project: workspace.name,
|
|
4907
|
+
scope: options.scope ?? workspace.name,
|
|
4188
4908
|
cache: cacheBackendFor(providerOf(workspace)),
|
|
4189
4909
|
events: eventsBackendFor(providerOf(workspace)),
|
|
4190
4910
|
storage: storageBackendFor(providerOf(workspace)),
|
|
@@ -4283,18 +5003,21 @@ function createEmptyState(stage, projectId, environmentId) {
|
|
|
4283
5003
|
};
|
|
4284
5004
|
}
|
|
4285
5005
|
/**
|
|
4286
|
-
* Get application ID from state
|
|
4287
|
-
*/
|
|
4288
|
-
function getApplicationId(state, appName) {
|
|
4289
|
-
return state?.applications[appName];
|
|
4290
|
-
}
|
|
4291
|
-
/**
|
|
4292
5006
|
* Set application ID in state (mutates state)
|
|
4293
5007
|
*/
|
|
4294
5008
|
function setApplicationId(state, appName, applicationId) {
|
|
4295
5009
|
state.applications[appName] = applicationId;
|
|
4296
5010
|
}
|
|
4297
5011
|
/**
|
|
5012
|
+
* Record the image an app was deployed with (mutates state)
|
|
5013
|
+
*/
|
|
5014
|
+
function setDeployedImage(state, appName, image) {
|
|
5015
|
+
state.images = {
|
|
5016
|
+
...state.images,
|
|
5017
|
+
[appName]: image
|
|
5018
|
+
};
|
|
5019
|
+
}
|
|
5020
|
+
/**
|
|
4298
5021
|
* Get a generated secret for an app
|
|
4299
5022
|
*/
|
|
4300
5023
|
function getGeneratedSecret(state, appName, secretName) {
|
|
@@ -4381,7 +5104,7 @@ async function createDnsProvider(options) {
|
|
|
4381
5104
|
if (isDnsProvider(config.provider)) return config.provider;
|
|
4382
5105
|
const provider = config.provider;
|
|
4383
5106
|
if (provider === "hostinger") {
|
|
4384
|
-
const { HostingerProvider } = await import("./HostingerProvider-
|
|
5107
|
+
const { HostingerProvider } = await import("./HostingerProvider-YQY3rMpV.mjs");
|
|
4385
5108
|
return new HostingerProvider();
|
|
4386
5109
|
}
|
|
4387
5110
|
if (provider === "route53") {
|
|
@@ -6072,6 +6795,33 @@ async function pushImage(imageRef) {
|
|
|
6072
6795
|
}
|
|
6073
6796
|
}
|
|
6074
6797
|
/**
|
|
6798
|
+
* The digest the registry gave a pushed image, `sha256:…`.
|
|
6799
|
+
*
|
|
6800
|
+
* A tag can be pushed over — by the next deploy, or by anything else that
|
|
6801
|
+
* shares the repository — and the digest is what says which image a stage
|
|
6802
|
+
* actually ran. `RepoDigests` has one entry per repository the image was
|
|
6803
|
+
* pushed to, so the one for this ref's repository is picked by name.
|
|
6804
|
+
*
|
|
6805
|
+
* @internal Exported for testing
|
|
6806
|
+
*/
|
|
6807
|
+
async function pushedDigest(imageRef) {
|
|
6808
|
+
const repository = imageRef.split("@")[0].replace(/:[\w][\w.-]*$/, "");
|
|
6809
|
+
const output = await runOutput("docker", [
|
|
6810
|
+
"image",
|
|
6811
|
+
"inspect",
|
|
6812
|
+
"--format={{json .RepoDigests}}",
|
|
6813
|
+
imageRef
|
|
6814
|
+
], { cwd: process.cwd() });
|
|
6815
|
+
let digests;
|
|
6816
|
+
try {
|
|
6817
|
+
digests = JSON.parse(output.trim() || "[]");
|
|
6818
|
+
} catch {
|
|
6819
|
+
return;
|
|
6820
|
+
}
|
|
6821
|
+
if (!Array.isArray(digests)) return void 0;
|
|
6822
|
+
return digests.find((entry) => typeof entry === "string" && entry.startsWith(`${repository}@`))?.slice(repository.length + 1);
|
|
6823
|
+
}
|
|
6824
|
+
/**
|
|
6075
6825
|
* Deploy using Docker (build and optionally push image)
|
|
6076
6826
|
*/
|
|
6077
6827
|
async function deployDocker(options) {
|
|
@@ -6079,9 +6829,13 @@ async function deployDocker(options) {
|
|
|
6079
6829
|
const imageName = config.imageName;
|
|
6080
6830
|
const imageRef = validateImageRef(getImageRef(config.registry, imageName, tag));
|
|
6081
6831
|
await buildImage(imageRef, buildArgs, credentials);
|
|
6832
|
+
let digest;
|
|
6082
6833
|
if (!skipPush) {
|
|
6083
6834
|
if (!config.registry) logger$5.warn("\n⚠️ No registry configured. Use --skip-push or configure docker.registry in gkm.config.ts");
|
|
6084
|
-
else
|
|
6835
|
+
else {
|
|
6836
|
+
await pushImage(imageRef);
|
|
6837
|
+
digest = await pushedDigest(imageRef);
|
|
6838
|
+
}
|
|
6085
6839
|
}
|
|
6086
6840
|
logger$5.log("\n✅ Docker deployment ready!");
|
|
6087
6841
|
logger$5.log(`\n📋 Deployment details:`);
|
|
@@ -6090,25 +6844,10 @@ async function deployDocker(options) {
|
|
|
6090
6844
|
if (masterKey) logger$5.log(`\n🔐 Run the container with GKM_MASTER_KEY set to the key with fingerprint ${keyFingerprint(masterKey)}.`);
|
|
6091
6845
|
return {
|
|
6092
6846
|
imageRef,
|
|
6847
|
+
...digest ? { digest } : {},
|
|
6093
6848
|
masterKey
|
|
6094
6849
|
};
|
|
6095
6850
|
}
|
|
6096
|
-
/**
|
|
6097
|
-
* What one application is called on a provider.
|
|
6098
|
-
*
|
|
6099
|
-
* The same rule the constructs use, through the same `scopedName`: the
|
|
6100
|
-
* application beside `production-kitchen-sink-database` is
|
|
6101
|
-
* `production-kitchen-sink-api`, not `api`. Both deploy paths call this — the
|
|
6102
|
-
* workspace one named its applications by the bare app key, so a project held
|
|
6103
|
-
* an `api` and a `web` that every stage would collide on.
|
|
6104
|
-
*
|
|
6105
|
-
* The app id is dropped when it repeats the project, so a project named for its
|
|
6106
|
-
* one application is `production-kitchen-sink` rather than
|
|
6107
|
-
* `production-kitchen-sink-kitchen-sink`.
|
|
6108
|
-
*/
|
|
6109
|
-
function applicationName(stage, project, app) {
|
|
6110
|
-
return app === project ? `${stage}-${project}`.toLowerCase() : scopedName([stage, project], app);
|
|
6111
|
-
}
|
|
6112
6851
|
//#endregion
|
|
6113
6852
|
//#region src/deploy/dokploy-api.ts
|
|
6114
6853
|
var dokploy_api_exports = /* @__PURE__ */ __exportAll({
|
|
@@ -6325,6 +7064,18 @@ var DokployApi = class {
|
|
|
6325
7064
|
});
|
|
6326
7065
|
}
|
|
6327
7066
|
/**
|
|
7067
|
+
* Change a project's name or description.
|
|
7068
|
+
*
|
|
7069
|
+
* The name is sent with the description because `project.update` validates
|
|
7070
|
+
* the whole project on some Dokploy versions, and a missing name fails it.
|
|
7071
|
+
*/
|
|
7072
|
+
async updateProject(projectId, updates) {
|
|
7073
|
+
await this.post("project.update", {
|
|
7074
|
+
projectId,
|
|
7075
|
+
...updates
|
|
7076
|
+
});
|
|
7077
|
+
}
|
|
7078
|
+
/**
|
|
6328
7079
|
* Delete a project and all its resources
|
|
6329
7080
|
*/
|
|
6330
7081
|
async deleteProject(projectId) {
|
|
@@ -7024,71 +7775,423 @@ function random() {
|
|
|
7024
7775
|
return randomBytes(32).toString("base64url");
|
|
7025
7776
|
}
|
|
7026
7777
|
//#endregion
|
|
7027
|
-
//#region src/deploy/
|
|
7778
|
+
//#region src/deploy/identity.ts
|
|
7779
|
+
/**
|
|
7780
|
+
* Who a deploy is, on a target it shares with other workspaces.
|
|
7781
|
+
*
|
|
7782
|
+
* A Dokploy server is one flat namespace of projects, images and Docker
|
|
7783
|
+
* service names. Deploys used to identify everything they made by the
|
|
7784
|
+
* workspace name alone, so two workspaces called `shop` — or `Shop` and
|
|
7785
|
+
* `shop` — deploying to one server landed in the same project, pushed over
|
|
7786
|
+
* each other's images and redeployed each other's applications. A
|
|
7787
|
+
* `DeployIdentity` is what a deploy names and claims its resources by instead.
|
|
7788
|
+
*/
|
|
7789
|
+
/** A namespace that cannot be part of a Dokploy name or an image path. */
|
|
7790
|
+
var DeployNamespaceInvalid = class extends Error {
|
|
7791
|
+
namespace;
|
|
7792
|
+
constructor(namespace) {
|
|
7793
|
+
super(`deploy.namespace '${namespace}' is not usable as a name: use lowercase letters, digits and single '-' between them (e.g. 'acme' or 'acme-platform') in gkm.config.ts.`);
|
|
7794
|
+
this.namespace = namespace;
|
|
7795
|
+
this.name = "DeployNamespaceInvalid";
|
|
7796
|
+
}
|
|
7797
|
+
};
|
|
7798
|
+
/** A workspace name with nothing a target can name a resource by. */
|
|
7799
|
+
var DeployProjectNameInvalid = class extends Error {
|
|
7800
|
+
workspaceName;
|
|
7801
|
+
constructor(workspaceName) {
|
|
7802
|
+
super(`The workspace name '${workspaceName}' has no letters or digits to name deployed resources by. Set "name" in gkm.config.ts.`);
|
|
7803
|
+
this.workspaceName = workspaceName;
|
|
7804
|
+
this.name = "DeployProjectNameInvalid";
|
|
7805
|
+
}
|
|
7806
|
+
};
|
|
7807
|
+
const NAME = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
|
|
7028
7808
|
/**
|
|
7029
|
-
*
|
|
7809
|
+
* Lowercase `[a-z0-9-]`, runs of anything else collapsed to one `-`: valid as
|
|
7810
|
+
* a Docker image path component, a Docker service name and a Dokploy name.
|
|
7030
7811
|
*/
|
|
7031
|
-
function
|
|
7032
|
-
return
|
|
7812
|
+
function slug$1(value) {
|
|
7813
|
+
return value.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
|
|
7814
|
+
}
|
|
7815
|
+
/** The namespace a workspace is in when it does not name one. */
|
|
7816
|
+
function defaultNamespace(workspaceName) {
|
|
7817
|
+
return slug$1(kebabCase(workspaceName));
|
|
7818
|
+
}
|
|
7819
|
+
function deployIdentity(workspace, stage) {
|
|
7820
|
+
const project = slug$1(workspace.name);
|
|
7821
|
+
if (!project) throw new DeployProjectNameInvalid(workspace.name);
|
|
7822
|
+
const fallback = defaultNamespace(workspace.name);
|
|
7823
|
+
const namespace = workspace.deploy?.namespace ?? fallback;
|
|
7824
|
+
if (!NAME.test(namespace)) throw new DeployNamespaceInvalid(namespace);
|
|
7825
|
+
return {
|
|
7826
|
+
namespace,
|
|
7827
|
+
project,
|
|
7828
|
+
stage,
|
|
7829
|
+
key: `${namespace}/${project}`,
|
|
7830
|
+
scope: namespace === fallback ? project : `${namespace}-${project}`
|
|
7831
|
+
};
|
|
7033
7832
|
}
|
|
7034
7833
|
/**
|
|
7035
|
-
*
|
|
7036
|
-
*
|
|
7037
|
-
*
|
|
7038
|
-
* - 'ssm': CachedStateProvider with SSM as source of truth
|
|
7039
|
-
* - 's3': S3StateStore behind the provider interface
|
|
7040
|
-
* - Custom: Use provided StateProvider implementation
|
|
7041
|
-
*
|
|
7042
|
-
* Deploy moves to `createStateStore` (locks, versions, per-resource records);
|
|
7043
|
-
* this stays for the call sites that read and write whole states.
|
|
7834
|
+
* The token a Dokploy project's description carries to say which identity
|
|
7835
|
+
* created it. A project with the right name and no marker belongs to someone
|
|
7836
|
+
* else, and a deploy never adopts it.
|
|
7044
7837
|
*/
|
|
7045
|
-
|
|
7046
|
-
|
|
7047
|
-
|
|
7048
|
-
|
|
7049
|
-
|
|
7050
|
-
|
|
7051
|
-
|
|
7052
|
-
|
|
7053
|
-
|
|
7054
|
-
|
|
7055
|
-
|
|
7056
|
-
|
|
7057
|
-
|
|
7058
|
-
|
|
7059
|
-
|
|
7060
|
-
return new LocalStateProvider(workspaceRoot);
|
|
7061
|
-
}
|
|
7062
|
-
if (provider === "ssm") {
|
|
7063
|
-
if (!workspaceName) {
|
|
7064
|
-
const { StateStoreNeedsWorkspaceName } = await Promise.resolve().then(() => StateStore_exports);
|
|
7065
|
-
throw new StateStoreNeedsWorkspaceName("ssm");
|
|
7066
|
-
}
|
|
7067
|
-
const { LocalStateProvider } = await import("./LocalStateProvider-D7QyJoLM.mjs");
|
|
7068
|
-
const { SSMStateProvider } = await import("./SSMStateProvider-B0Hu1MKZ.mjs");
|
|
7069
|
-
const { CachedStateProvider } = await Promise.resolve().then(() => CachedStateProvider_exports);
|
|
7070
|
-
const ssmConfig = config;
|
|
7071
|
-
const local = new LocalStateProvider(workspaceRoot);
|
|
7072
|
-
return new CachedStateProvider(SSMStateProvider.create({
|
|
7073
|
-
workspaceName,
|
|
7074
|
-
region: ssmConfig.region,
|
|
7075
|
-
profile: ssmConfig.profile
|
|
7076
|
-
}), local);
|
|
7077
|
-
}
|
|
7078
|
-
if (provider === "s3") {
|
|
7079
|
-
const { createStateStore, StateStoreProvider } = await Promise.resolve().then(() => StateStore_exports);
|
|
7080
|
-
return new StateStoreProvider(await createStateStore(options));
|
|
7081
|
-
}
|
|
7082
|
-
const { UnknownStateProvider } = await Promise.resolve().then(() => StateStore_exports);
|
|
7083
|
-
throw new UnknownStateProvider(config);
|
|
7838
|
+
function ownershipMarker(identity) {
|
|
7839
|
+
return `gkm:${identity.key}`;
|
|
7840
|
+
}
|
|
7841
|
+
/** Whether a project description carries `identity`'s marker. */
|
|
7842
|
+
function isOwnedBy(description, identity) {
|
|
7843
|
+
const marker = ownershipMarker(identity);
|
|
7844
|
+
return (description ?? "").split(/\s+/).includes(marker);
|
|
7845
|
+
}
|
|
7846
|
+
/** Any `gkm:` marker a description carries — whoever's it is. */
|
|
7847
|
+
function markerOf(description) {
|
|
7848
|
+
return (description ?? "").split(/\s+/).find((t) => t.startsWith("gkm:"));
|
|
7849
|
+
}
|
|
7850
|
+
/** What the Dokploy project is called. */
|
|
7851
|
+
function projectName(identity) {
|
|
7852
|
+
return identity.scope;
|
|
7084
7853
|
}
|
|
7085
|
-
//#endregion
|
|
7086
|
-
//#region src/deploy/secrets.ts
|
|
7087
7854
|
/**
|
|
7088
|
-
*
|
|
7855
|
+
* The repository an app's image is pushed to, below the registry:
|
|
7856
|
+
* `<namespace>/<project>-<app>`.
|
|
7089
7857
|
*
|
|
7090
|
-
*
|
|
7091
|
-
*
|
|
7858
|
+
* The namespace is a path segment of its own so that a registry that maps
|
|
7859
|
+
* paths to owners (an organisation's packages, a Harbor project) can grant it
|
|
7860
|
+
* as one; the app is joined to the project so the two read as one name.
|
|
7861
|
+
*/
|
|
7862
|
+
function imageName(identity, app) {
|
|
7863
|
+
return `${identity.namespace}/${identity.project}-${slug$1(kebabCase(app))}`;
|
|
7864
|
+
}
|
|
7865
|
+
/** The full image ref an app is built, pushed and deployed as. */
|
|
7866
|
+
function imageRef(identity, app, registry, tag) {
|
|
7867
|
+
const name = imageName(identity, app);
|
|
7868
|
+
return registry ? `${registry.replace(/\/+$/, "")}/${name}:${tag}` : `${name}:${tag}`;
|
|
7869
|
+
}
|
|
7870
|
+
/**
|
|
7871
|
+
* What one application is called on the target.
|
|
7872
|
+
*
|
|
7873
|
+
* The same rule the constructs use, through the same `scopedName`, scoped by
|
|
7874
|
+
* the stage and the identity: the application beside
|
|
7875
|
+
* `production-kitchen-sink-database` is `production-kitchen-sink-api`. A
|
|
7876
|
+
* Dokploy application's name is also its Docker service name, which is unique
|
|
7877
|
+
* on the whole server, so the namespace has to be in it too — `acme`'s and
|
|
7878
|
+
* `globex`'s `shop` would otherwise both run `production-shop-api`.
|
|
7879
|
+
*
|
|
7880
|
+
* The app id is dropped when it repeats the project, so a project named for its
|
|
7881
|
+
* one application is `production-kitchen-sink` rather than
|
|
7882
|
+
* `production-kitchen-sink-kitchen-sink`.
|
|
7883
|
+
*/
|
|
7884
|
+
function applicationName(identity, app) {
|
|
7885
|
+
return slug$1(app) === identity.project ? `${identity.stage}-${identity.scope}` : scopedName([identity.stage, identity.scope], app);
|
|
7886
|
+
}
|
|
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
|
|
7995
|
+
//#region src/deploy/ownership.ts
|
|
7996
|
+
/**
|
|
7997
|
+
* Which Dokploy project a deploy is allowed to deploy into.
|
|
7998
|
+
*
|
|
7999
|
+
* Deploy used to take any project whose name matched the workspace's, in any
|
|
8000
|
+
* case — so a second workspace called `shop` (or `Shop`) on the same server
|
|
8001
|
+
* deployed into the first one's project, over its images and applications. A
|
|
8002
|
+
* project is now claimed by an ownership marker in its description, and a
|
|
8003
|
+
* project with the right name and no marker is somebody else's.
|
|
8004
|
+
*/
|
|
8005
|
+
/**
|
|
8006
|
+
* A Dokploy project with the name this deploy would use exists, and nothing
|
|
8007
|
+
* says this workspace created it.
|
|
8008
|
+
*/
|
|
8009
|
+
var ProjectNotOwned = class extends Error {
|
|
8010
|
+
projectName;
|
|
8011
|
+
projectId;
|
|
8012
|
+
identity;
|
|
8013
|
+
ownedBy;
|
|
8014
|
+
constructor(projectName, projectId, identity, ownedBy) {
|
|
8015
|
+
super(`The Dokploy project '${projectName}' (${projectId}) was not created by this workspace (${identity})${ownedBy ? ` — it is marked '${ownedBy}'` : ""}, so deploy will not use it. Set deploy.namespace in gkm.config.ts to deploy under a name of your own, or, if this project really is this workspace's, add 'gkm:${identity}' to its description in Dokploy.`);
|
|
8016
|
+
this.projectName = projectName;
|
|
8017
|
+
this.projectId = projectId;
|
|
8018
|
+
this.identity = identity;
|
|
8019
|
+
this.ownedBy = ownedBy;
|
|
8020
|
+
this.name = "ProjectNotOwned";
|
|
8021
|
+
}
|
|
8022
|
+
};
|
|
8023
|
+
/** A description with the marker added, keeping what was there. */
|
|
8024
|
+
function withMarker(description, marker) {
|
|
8025
|
+
const existing = (description ?? "").trim();
|
|
8026
|
+
return existing ? `${existing}\n${marker}` : marker;
|
|
8027
|
+
}
|
|
8028
|
+
/** `project.one`, or `null` when Dokploy no longer has the project. */
|
|
8029
|
+
async function projectById(api, projectId) {
|
|
8030
|
+
try {
|
|
8031
|
+
return await api.getProject(projectId) ?? null;
|
|
8032
|
+
} catch (error) {
|
|
8033
|
+
if (error instanceof DokployApiError) return null;
|
|
8034
|
+
throw error;
|
|
8035
|
+
}
|
|
8036
|
+
}
|
|
8037
|
+
/**
|
|
8038
|
+
* The project this deploy deploys into:
|
|
8039
|
+
*
|
|
8040
|
+
* 1. The one the stage's state names, if Dokploy still has it. State written
|
|
8041
|
+
* before markers existed is trusted — it holds an id this workspace was
|
|
8042
|
+
* given when it created the project — and the marker is written now, so
|
|
8043
|
+
* the project is claimed for the next deploy that has no state.
|
|
8044
|
+
* 2. One with this name *and* this identity's marker.
|
|
8045
|
+
* 3. A new one, marked.
|
|
8046
|
+
*
|
|
8047
|
+
* A project that matches by name (in any case) but carries no marker, or
|
|
8048
|
+
* another identity's, raises `ProjectNotOwned` — it is never adopted.
|
|
8049
|
+
*/
|
|
8050
|
+
async function resolveProject(api, identity, stateProjectId, log = () => {}, hooks = {}) {
|
|
8051
|
+
const name = projectName(identity);
|
|
8052
|
+
const marker = ownershipMarker(identity);
|
|
8053
|
+
if (stateProjectId) {
|
|
8054
|
+
const known = await projectById(api, stateProjectId);
|
|
8055
|
+
if (known) {
|
|
8056
|
+
const claimed = markerOf(known.description);
|
|
8057
|
+
if (claimed && claimed !== marker) throw new ProjectNotOwned(known.name, known.projectId, identity.key, claimed);
|
|
8058
|
+
if (!claimed) {
|
|
8059
|
+
await api.updateProject(known.projectId, {
|
|
8060
|
+
name: known.name,
|
|
8061
|
+
description: withMarker(known.description, marker)
|
|
8062
|
+
});
|
|
8063
|
+
log(` Claimed project ${known.name} for ${identity.key}`);
|
|
8064
|
+
}
|
|
8065
|
+
return {
|
|
8066
|
+
projectId: known.projectId,
|
|
8067
|
+
name: known.name,
|
|
8068
|
+
environments: known.environments ?? [],
|
|
8069
|
+
via: "state"
|
|
8070
|
+
};
|
|
8071
|
+
}
|
|
8072
|
+
}
|
|
8073
|
+
const projects = await api.listProjects();
|
|
8074
|
+
const owned = projects.find((p) => p.name.toLowerCase() === name.toLowerCase() && isOwnedBy(p.description, identity));
|
|
8075
|
+
if (owned) {
|
|
8076
|
+
const details = await api.getProject(owned.projectId);
|
|
8077
|
+
return {
|
|
8078
|
+
projectId: owned.projectId,
|
|
8079
|
+
name: owned.name,
|
|
8080
|
+
environments: details.environments ?? [],
|
|
8081
|
+
via: "marker"
|
|
8082
|
+
};
|
|
8083
|
+
}
|
|
8084
|
+
const clash = projects.find((p) => p.name.toLowerCase() === name.toLowerCase());
|
|
8085
|
+
if (clash) throw new ProjectNotOwned(clash.name, clash.projectId, identity.key, markerOf(clash.description));
|
|
8086
|
+
await hooks.beforeCreate?.();
|
|
8087
|
+
const created = await api.createProject(name, withMarker("Deployed by gkm.", marker));
|
|
8088
|
+
return {
|
|
8089
|
+
projectId: created.project.projectId,
|
|
8090
|
+
name: created.project.name,
|
|
8091
|
+
environments: [created.environment],
|
|
8092
|
+
via: "created"
|
|
8093
|
+
};
|
|
8094
|
+
}
|
|
8095
|
+
//#endregion
|
|
8096
|
+
//#region src/deploy/registry.ts
|
|
8097
|
+
/**
|
|
8098
|
+
* Which Dokploy registry a stage's images are pulled through.
|
|
8099
|
+
*
|
|
8100
|
+
* It used to be one id stored per machine (`~/.gkm/credentials.json`), and
|
|
8101
|
+
* when that was empty, the first registry Dokploy listed — so an image could be
|
|
8102
|
+
* deployed with credentials for a registry nobody chose, and two workspaces
|
|
8103
|
+
* deployed from one machine shared whichever was stored last. The id is now
|
|
8104
|
+
* kept in the stage's state, and found only from what the workspace
|
|
8105
|
+
* configured.
|
|
8106
|
+
*/
|
|
8107
|
+
/** There is nowhere to push images to, so Dokploy has nothing to pull. */
|
|
8108
|
+
var RegistryNotConfigured = class extends Error {
|
|
8109
|
+
stage;
|
|
8110
|
+
constructor(stage) {
|
|
8111
|
+
super(`Deploying '${stage}' needs a container registry: Dokploy pulls each app's image from one. Set deploy.dokploy.registry in gkm.config.ts (e.g. 'ghcr.io/acme'), and deploy.dokploy.registryId if Dokploy already holds that registry's credentials.`);
|
|
8112
|
+
this.stage = stage;
|
|
8113
|
+
this.name = "RegistryNotConfigured";
|
|
8114
|
+
}
|
|
8115
|
+
};
|
|
8116
|
+
/** `deploy.dokploy.registryId` names a registry Dokploy does not have. */
|
|
8117
|
+
var RegistryNotFound = class extends Error {
|
|
8118
|
+
registryId;
|
|
8119
|
+
constructor(registryId) {
|
|
8120
|
+
super(`deploy.dokploy.registryId '${registryId}' is not a registry on this Dokploy server. Find the right id under Settings → Docker Registry in Dokploy and set it in gkm.config.ts, or remove it to have deploy find the registry by its URL.`);
|
|
8121
|
+
this.registryId = registryId;
|
|
8122
|
+
this.name = "RegistryNotFound";
|
|
8123
|
+
}
|
|
8124
|
+
};
|
|
8125
|
+
/** More than one Dokploy registry serves the configured registry. */
|
|
8126
|
+
var RegistryAmbiguous = class extends Error {
|
|
8127
|
+
registry;
|
|
8128
|
+
registryIds;
|
|
8129
|
+
constructor(registry, registryIds) {
|
|
8130
|
+
super(`Dokploy has ${registryIds.length} registries for '${registry}' (${registryIds.join(", ")}). Set deploy.dokploy.registryId in gkm.config.ts to the one this workspace pushes with.`);
|
|
8131
|
+
this.registry = registry;
|
|
8132
|
+
this.registryIds = registryIds;
|
|
8133
|
+
this.name = "RegistryAmbiguous";
|
|
8134
|
+
}
|
|
8135
|
+
};
|
|
8136
|
+
/** `https://ghcr.io/acme/` → `{ host: 'ghcr.io', path: 'acme' }`. */
|
|
8137
|
+
function parts(url) {
|
|
8138
|
+
const [host = "", ...path] = url.replace(/^[a-z]+:\/\//i, "").replace(/\/+$/, "").toLowerCase().split("/");
|
|
8139
|
+
return {
|
|
8140
|
+
host,
|
|
8141
|
+
path: path.join("/")
|
|
8142
|
+
};
|
|
8143
|
+
}
|
|
8144
|
+
/**
|
|
8145
|
+
* The Dokploy registries that serve `registry`: the same host, and — where
|
|
8146
|
+
* Dokploy's own entry names one — the same path.
|
|
8147
|
+
*/
|
|
8148
|
+
function serving(registries, registry) {
|
|
8149
|
+
const wanted = parts(registry);
|
|
8150
|
+
const sameHost = registries.filter((r) => parts(r.registryUrl ?? "").host === wanted.host);
|
|
8151
|
+
if (sameHost.length <= 1) return sameHost;
|
|
8152
|
+
return sameHost.filter((r) => {
|
|
8153
|
+
return (parts(r.registryUrl ?? "").path || (r.imagePrefix ?? "").toLowerCase()) === wanted.path;
|
|
8154
|
+
});
|
|
8155
|
+
}
|
|
8156
|
+
/** `registry.one`, or `null` when Dokploy says it has no such registry. */
|
|
8157
|
+
async function registryById(api, registryId) {
|
|
8158
|
+
try {
|
|
8159
|
+
return await api.getRegistry(registryId) ?? null;
|
|
8160
|
+
} catch (error) {
|
|
8161
|
+
if (error instanceof DokployApiError) return null;
|
|
8162
|
+
throw error;
|
|
8163
|
+
}
|
|
8164
|
+
}
|
|
8165
|
+
/**
|
|
8166
|
+
* The registry id for a stage, in order: the configured id, the one in the
|
|
8167
|
+
* stage's state, the one Dokploy has for the configured registry URL, and
|
|
8168
|
+
* otherwise a new one. Never a registry the workspace did not point at.
|
|
8169
|
+
*/
|
|
8170
|
+
async function resolveRegistry(api, options) {
|
|
8171
|
+
const { stage, registry, configuredId, stateId, log = () => {} } = options;
|
|
8172
|
+
if (!registry) throw new RegistryNotConfigured(stage);
|
|
8173
|
+
if (configuredId) {
|
|
8174
|
+
const configured = await registryById(api, configuredId);
|
|
8175
|
+
if (!configured) throw new RegistryNotFound(configuredId);
|
|
8176
|
+
return configured;
|
|
8177
|
+
}
|
|
8178
|
+
if (stateId) {
|
|
8179
|
+
const known = await registryById(api, stateId);
|
|
8180
|
+
if (known) return known;
|
|
8181
|
+
log(` ⚠ The stage's registry ${stateId} no longer exists in Dokploy`);
|
|
8182
|
+
}
|
|
8183
|
+
const matches = serving(await api.listRegistries(), registry);
|
|
8184
|
+
if (matches.length > 1) throw new RegistryAmbiguous(registry, matches.map((r) => r.registryId));
|
|
8185
|
+
if (matches[0]) return matches[0];
|
|
8186
|
+
return options.create(registry);
|
|
8187
|
+
}
|
|
8188
|
+
//#endregion
|
|
8189
|
+
//#region src/deploy/secrets.ts
|
|
8190
|
+
/**
|
|
8191
|
+
* Filter secrets to only include the env vars that an app requires.
|
|
8192
|
+
*
|
|
8193
|
+
* @param stageSecrets - All secrets for the stage
|
|
8194
|
+
* @param sniffedEnv - The sniffed environment requirements for the app
|
|
7092
8195
|
* @returns Filtered secrets with found/missing tracking
|
|
7093
8196
|
*/
|
|
7094
8197
|
function filterSecretsForApp(stageSecrets, sniffedEnv) {
|
|
@@ -7796,6 +8899,7 @@ async function workspaceDeployCommand(configured, options) {
|
|
|
7796
8899
|
apps: units
|
|
7797
8900
|
} : configured;
|
|
7798
8901
|
if (provider !== "dokploy") throw new Error(`Workspace deployment only supports Dokploy. Got: ${provider}`);
|
|
8902
|
+
const identity = deployIdentity(workspace, stage);
|
|
7799
8903
|
logger$4.log(`\n🚀 Deploying workspace "${workspace.name}" to Dokploy...`);
|
|
7800
8904
|
logger$4.log(` Stage: ${stage}`);
|
|
7801
8905
|
const imageTag = tag ?? generateTag(stage);
|
|
@@ -7822,6 +8926,28 @@ async function workspaceDeployCommand(configured, options) {
|
|
|
7822
8926
|
});
|
|
7823
8927
|
if (dokployApps.length === 0) throw new Error("No apps to deploy. All selected apps have unsupported deploy targets.");
|
|
7824
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 }) {
|
|
7825
8951
|
logger$4.log("\n🔐 Loading secrets and analyzing environment requirements...");
|
|
7826
8952
|
const secretsStore = await secretsStoreFor(workspace, stage);
|
|
7827
8953
|
const stored = await secretsStore.read(stage);
|
|
@@ -7862,79 +8988,71 @@ async function workspaceDeployCommand(configured, options) {
|
|
|
7862
8988
|
baseUrl: creds.endpoint,
|
|
7863
8989
|
token: creds.token
|
|
7864
8990
|
});
|
|
8991
|
+
logger$4.log("\n📋 Loading deploy state...");
|
|
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`);
|
|
7865
8998
|
logger$4.log("\n📁 Setting up Dokploy project...");
|
|
7866
|
-
|
|
7867
|
-
|
|
7868
|
-
|
|
7869
|
-
|
|
7870
|
-
|
|
7871
|
-
|
|
7872
|
-
|
|
7873
|
-
environmentId = matchingEnv.environmentId;
|
|
7874
|
-
logger$4.log(` Using environment: ${matchingEnv.name}`);
|
|
7875
|
-
} else {
|
|
7876
|
-
logger$4.log(` Creating "${stage}" environment...`);
|
|
7877
|
-
environmentId = (await api.createEnvironment(project.projectId, stage)).environmentId;
|
|
7878
|
-
logger$4.log(` ✓ Created environment: ${stage}`);
|
|
8999
|
+
logger$4.log(` Identity: ${identity.key}`);
|
|
9000
|
+
const projectEntry = {
|
|
9001
|
+
key: "project",
|
|
9002
|
+
type: "project",
|
|
9003
|
+
data: {
|
|
9004
|
+
name: projectName(identity),
|
|
9005
|
+
identity: identity.key
|
|
7879
9006
|
}
|
|
7880
|
-
}
|
|
7881
|
-
|
|
7882
|
-
|
|
7883
|
-
|
|
7884
|
-
|
|
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);
|
|
9010
|
+
if (project.via === "created") logger$4.log(` ✓ Created project: ${project.projectId}`);
|
|
9011
|
+
else logger$4.log(` Found existing project: ${project.name}`);
|
|
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: () => {
|
|
7885
9020
|
logger$4.log(` Creating "${stage}" environment...`);
|
|
7886
|
-
|
|
7887
|
-
}
|
|
7888
|
-
|
|
7889
|
-
}
|
|
7890
|
-
logger$4.log("\n📋 Loading deploy state...");
|
|
7891
|
-
const stateProvider = await createStateProvider({
|
|
7892
|
-
config: workspace.state,
|
|
7893
|
-
workspaceRoot: workspace.root,
|
|
7894
|
-
workspaceName: workspace.name
|
|
9021
|
+
return api.createEnvironment(project.projectId, stage);
|
|
9022
|
+
},
|
|
9023
|
+
id: (e) => e.environmentId
|
|
7895
9024
|
});
|
|
7896
|
-
|
|
7897
|
-
if (
|
|
7898
|
-
|
|
7899
|
-
|
|
7900
|
-
|
|
7901
|
-
|
|
7902
|
-
|
|
7903
|
-
|
|
7904
|
-
|
|
7905
|
-
|
|
7906
|
-
}
|
|
7907
|
-
} else {
|
|
7908
|
-
logger$4.log(` Creating new state for stage "${stage}"`);
|
|
7909
|
-
state = createEmptyState(stage, project.projectId, environmentId);
|
|
7910
|
-
}
|
|
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;
|
|
9034
|
+
state.identity = identity.key;
|
|
7911
9035
|
logger$4.log("\n🐳 Checking registry...");
|
|
7912
|
-
let registryId = await getDokployRegistryId();
|
|
7913
9036
|
const registry = workspace.deploy.dokploy?.registry;
|
|
7914
|
-
|
|
7915
|
-
|
|
7916
|
-
|
|
7917
|
-
|
|
7918
|
-
|
|
7919
|
-
|
|
7920
|
-
|
|
7921
|
-
|
|
7922
|
-
if (!registryId) {
|
|
7923
|
-
const registries = await api.listRegistries();
|
|
7924
|
-
if (registries.length > 0) {
|
|
7925
|
-
registryId = registries[0].registryId;
|
|
7926
|
-
await storeDokployRegistryId(registryId);
|
|
7927
|
-
logger$4.log(` Using registry: ${registries[0].registryName}`);
|
|
7928
|
-
} else if (registry) {
|
|
7929
|
-
logger$4.log(" No registries found in Dokploy. Let's create one.");
|
|
7930
|
-
logger$4.log(` Registry URL: ${registry}`);
|
|
9037
|
+
const dokployRegistry = await resolveRegistry(api, {
|
|
9038
|
+
stage,
|
|
9039
|
+
registry,
|
|
9040
|
+
configuredId: workspace.deploy.dokploy?.registryId,
|
|
9041
|
+
stateId: state.registryId,
|
|
9042
|
+
log: (message) => logger$4.log(message),
|
|
9043
|
+
create: async (url) => {
|
|
9044
|
+
logger$4.log(` Dokploy has no registry for ${url}. Let's create one.`);
|
|
7931
9045
|
const username = await prompt("Registry username: ");
|
|
7932
9046
|
const password = await prompt("Registry password/token: ", true);
|
|
7933
|
-
|
|
7934
|
-
|
|
7935
|
-
|
|
7936
|
-
}
|
|
7937
|
-
}
|
|
9047
|
+
const created = await api.createRegistry("Default Registry", url, username, password);
|
|
9048
|
+
logger$4.log(` ✓ Registry created: ${created.registryId}`);
|
|
9049
|
+
return created;
|
|
9050
|
+
}
|
|
9051
|
+
});
|
|
9052
|
+
const registryId = dokployRegistry.registryId;
|
|
9053
|
+
state.registryId = registryId;
|
|
9054
|
+
logger$4.log(` Using registry: ${dokployRegistry.registryName}`);
|
|
9055
|
+
await journal.save();
|
|
7938
9056
|
const skippedMobileApps = appsToDeployNames.filter((name) => workspace.apps[name].type === "mobile");
|
|
7939
9057
|
if (skippedMobileApps.length > 0) logger$4.log(`\n📱 Skipping ${skippedMobileApps.length} mobile app(s) — deploy via framework toolchain: ${skippedMobileApps.join(", ")}`);
|
|
7940
9058
|
const backendApps = appsToDeployNames.filter((name) => workspace.apps[name].type === "backend");
|
|
@@ -7955,6 +9073,7 @@ async function workspaceDeployCommand(configured, options) {
|
|
|
7955
9073
|
projectId: project.projectId,
|
|
7956
9074
|
environmentId,
|
|
7957
9075
|
stage,
|
|
9076
|
+
scope: identity.scope,
|
|
7958
9077
|
appUrls,
|
|
7959
9078
|
supplied: stageSecrets.custom,
|
|
7960
9079
|
seed: stageSecrets.seed,
|
|
@@ -7980,7 +9099,7 @@ async function workspaceDeployCommand(configured, options) {
|
|
|
7980
9099
|
const backupState = await provisionBackupDestination({
|
|
7981
9100
|
api,
|
|
7982
9101
|
projectId: project.projectId,
|
|
7983
|
-
projectName:
|
|
9102
|
+
projectName: identity.scope,
|
|
7984
9103
|
stage,
|
|
7985
9104
|
config: workspace.deploy.backups,
|
|
7986
9105
|
existingState: getBackupState(state),
|
|
@@ -7991,7 +9110,7 @@ async function workspaceDeployCommand(configured, options) {
|
|
|
7991
9110
|
const backupSchedule = workspace.deploy.backups.schedule ?? "0 2 * * *";
|
|
7992
9111
|
const backupRetention = workspace.deploy.backups.retention ?? 30;
|
|
7993
9112
|
logger$4.log(" Creating postgres backup schedule...");
|
|
7994
|
-
|
|
9113
|
+
setPostgresBackupId(state, (await api.createPostgresBackup({
|
|
7995
9114
|
schedule: backupSchedule,
|
|
7996
9115
|
prefix: `${stage}/postgres`,
|
|
7997
9116
|
destinationId: backupState.destinationId,
|
|
@@ -7999,8 +9118,7 @@ async function workspaceDeployCommand(configured, options) {
|
|
|
7999
9118
|
postgresId: backupCluster.postgresId,
|
|
8000
9119
|
enabled: true,
|
|
8001
9120
|
keepLatestCount: backupRetention
|
|
8002
|
-
});
|
|
8003
|
-
setPostgresBackupId(state, backup.backupId);
|
|
9121
|
+
})).backupId);
|
|
8004
9122
|
logger$4.log(` ✓ Postgres backup schedule created (${backupSchedule})`);
|
|
8005
9123
|
} else logger$4.log(" ✓ Using existing postgres backup schedule");
|
|
8006
9124
|
}
|
|
@@ -8021,42 +9139,31 @@ async function workspaceDeployCommand(configured, options) {
|
|
|
8021
9139
|
const app = workspace.apps[appName];
|
|
8022
9140
|
logger$4.log(`\n ⚙️ Deploying ${appName}...`);
|
|
8023
9141
|
try {
|
|
8024
|
-
const
|
|
8025
|
-
let application = null;
|
|
8026
|
-
const cachedAppId = getApplicationId(state, appName);
|
|
8027
|
-
if (cachedAppId) {
|
|
8028
|
-
logger$4.log(` Using cached ID: ${cachedAppId}`);
|
|
8029
|
-
application = await api.getApplication(cachedAppId);
|
|
8030
|
-
if (application) logger$4.log(` ✓ Application found: ${application.applicationId}`);
|
|
8031
|
-
else logger$4.log(` ⚠ Cached ID invalid, will create new`);
|
|
8032
|
-
}
|
|
8033
|
-
if (!application) {
|
|
8034
|
-
const result = await api.findOrCreateApplication(dokployAppName, project.projectId, environmentId);
|
|
8035
|
-
application = result.application;
|
|
8036
|
-
if (result.created) logger$4.log(` Created application: ${application.applicationId}`);
|
|
8037
|
-
else logger$4.log(` Found existing application: ${application.applicationId}`);
|
|
8038
|
-
}
|
|
8039
|
-
setApplicationId(state, appName, application.applicationId);
|
|
9142
|
+
const application = await ensureApplication(api, journal, appName, applicationName(identity, appName), project.projectId, environmentId);
|
|
8040
9143
|
const appSecrets = encryptedSecrets.get(appName);
|
|
8041
9144
|
const credentials = appSecrets && appSecrets.secretCount > 0 ? {
|
|
8042
9145
|
encrypted: appSecrets.payload.encrypted,
|
|
8043
9146
|
iv: appSecrets.payload.iv
|
|
8044
9147
|
} : void 0;
|
|
8045
9148
|
if (appSecrets && credentials) logger$4.log(` Encrypted ${appSecrets.secretCount} secrets`);
|
|
8046
|
-
const
|
|
8047
|
-
|
|
8048
|
-
|
|
8049
|
-
await deployDocker({
|
|
9149
|
+
const imageRef$1 = imageRef(identity, appName, registry, imageTag);
|
|
9150
|
+
logger$4.log(` Building Docker image: ${imageRef$1}`);
|
|
9151
|
+
const built = await deployDocker({
|
|
8050
9152
|
stage,
|
|
8051
9153
|
tag: imageTag,
|
|
8052
9154
|
skipPush: false,
|
|
8053
9155
|
config: {
|
|
8054
9156
|
registry,
|
|
8055
|
-
imageName,
|
|
9157
|
+
imageName: imageName(identity, appName),
|
|
8056
9158
|
appName
|
|
8057
9159
|
},
|
|
8058
9160
|
credentials
|
|
8059
9161
|
});
|
|
9162
|
+
setDeployedImage(state, appName, {
|
|
9163
|
+
ref: built.imageRef ?? imageRef$1,
|
|
9164
|
+
...built.digest ? { digest: built.digest } : {}
|
|
9165
|
+
});
|
|
9166
|
+
await journal.save();
|
|
8060
9167
|
const backendHost = resolveHost(appName, app, stage, workspace.deploy?.domains, false);
|
|
8061
9168
|
const dependencyUrls = {};
|
|
8062
9169
|
if (app.dependencies) {
|
|
@@ -8087,40 +9194,21 @@ async function workspaceDeployCommand(configured, options) {
|
|
|
8087
9194
|
};
|
|
8088
9195
|
const envVars = Object.entries(withDeclared).map(([key, value]) => `${key}=${value}`);
|
|
8089
9196
|
if (Object.keys(withDeclared).length > 0) logger$4.log(` Resolved ${Object.keys(withDeclared).length} env vars: ${Object.keys(withDeclared).sort().join(", ")}`);
|
|
8090
|
-
await api.saveDockerProvider(application.applicationId, imageRef, { registryId });
|
|
9197
|
+
await api.saveDockerProvider(application.applicationId, imageRef$1, { registryId });
|
|
8091
9198
|
await api.saveApplicationEnv(application.applicationId, envVars.join("\n"));
|
|
8092
9199
|
logger$4.log(` Deploying to Dokploy...`);
|
|
8093
9200
|
await api.deployApplication(application.applicationId);
|
|
8094
|
-
const
|
|
8095
|
-
|
|
8096
|
-
|
|
8097
|
-
|
|
8098
|
-
publicUrls[appName] = `https://${backendHost}`;
|
|
8099
|
-
logger$4.log(` ✓ Domain: https://${backendHost} (existing)`);
|
|
8100
|
-
} else try {
|
|
8101
|
-
const domain = await api.createDomain({
|
|
8102
|
-
host: backendHost,
|
|
8103
|
-
port: app.port,
|
|
8104
|
-
https: true,
|
|
8105
|
-
certificateType: "letsencrypt",
|
|
8106
|
-
applicationId: application.applicationId
|
|
8107
|
-
});
|
|
8108
|
-
appHostnames.set(appName, backendHost);
|
|
8109
|
-
appDomainIds.set(appName, domain.domainId);
|
|
8110
|
-
publicUrls[appName] = `https://${backendHost}`;
|
|
8111
|
-
logger$4.log(` ✓ Domain: https://${backendHost} (created)`);
|
|
8112
|
-
} catch (domainError) {
|
|
8113
|
-
const message = domainError instanceof Error ? domainError.message : "Unknown error";
|
|
8114
|
-
logger$4.log(` ⚠ Domain creation failed: ${message}`);
|
|
8115
|
-
appHostnames.set(appName, backendHost);
|
|
8116
|
-
publicUrls[appName] = `https://${backendHost}`;
|
|
8117
|
-
}
|
|
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}`;
|
|
8118
9205
|
results.push({
|
|
8119
9206
|
appName,
|
|
8120
9207
|
type: app.type,
|
|
8121
9208
|
success: true,
|
|
8122
9209
|
applicationId: application.applicationId,
|
|
8123
|
-
imageRef
|
|
9210
|
+
imageRef: imageRef$1,
|
|
9211
|
+
...built.digest ? { digest: built.digest } : {}
|
|
8124
9212
|
});
|
|
8125
9213
|
logger$4.log(` ✓ ${appName} deployed successfully`);
|
|
8126
9214
|
} catch (error) {
|
|
@@ -8142,22 +9230,7 @@ async function workspaceDeployCommand(configured, options) {
|
|
|
8142
9230
|
const app = workspace.apps[appName];
|
|
8143
9231
|
logger$4.log(`\n 🌐 Deploying ${appName}...`);
|
|
8144
9232
|
try {
|
|
8145
|
-
const
|
|
8146
|
-
let application = null;
|
|
8147
|
-
const cachedAppId = getApplicationId(state, appName);
|
|
8148
|
-
if (cachedAppId) {
|
|
8149
|
-
logger$4.log(` Using cached ID: ${cachedAppId}`);
|
|
8150
|
-
application = await api.getApplication(cachedAppId);
|
|
8151
|
-
if (application) logger$4.log(` ✓ Application found: ${application.applicationId}`);
|
|
8152
|
-
else logger$4.log(` ⚠ Cached ID invalid, will create new`);
|
|
8153
|
-
}
|
|
8154
|
-
if (!application) {
|
|
8155
|
-
const result = await api.findOrCreateApplication(dokployAppName, project.projectId, environmentId);
|
|
8156
|
-
application = result.application;
|
|
8157
|
-
if (result.created) logger$4.log(` Created application: ${application.applicationId}`);
|
|
8158
|
-
else logger$4.log(` Found existing application: ${application.applicationId}`);
|
|
8159
|
-
}
|
|
8160
|
-
setApplicationId(state, appName, application.applicationId);
|
|
9233
|
+
const application = await ensureApplication(api, journal, appName, applicationName(identity, appName), project.projectId, environmentId);
|
|
8161
9234
|
const dependencyUrls = {};
|
|
8162
9235
|
if (app.dependencies) {
|
|
8163
9236
|
for (const dep of app.dependencies) if (publicUrls[dep]) dependencyUrls[dep] = publicUrls[dep];
|
|
@@ -8187,61 +9260,46 @@ async function workspaceDeployCommand(configured, options) {
|
|
|
8187
9260
|
}
|
|
8188
9261
|
}
|
|
8189
9262
|
if (buildArgs.length > 0) logger$4.log(` Build args: ${publicUrlArgNames.join(", ")}`);
|
|
8190
|
-
const
|
|
8191
|
-
|
|
8192
|
-
|
|
8193
|
-
await deployDocker({
|
|
9263
|
+
const imageRef$2 = imageRef(identity, appName, registry, imageTag);
|
|
9264
|
+
logger$4.log(` Building Docker image: ${imageRef$2}`);
|
|
9265
|
+
const built = await deployDocker({
|
|
8194
9266
|
stage,
|
|
8195
9267
|
tag: imageTag,
|
|
8196
9268
|
skipPush: false,
|
|
8197
9269
|
config: {
|
|
8198
9270
|
registry,
|
|
8199
|
-
imageName,
|
|
9271
|
+
imageName: imageName(identity, appName),
|
|
8200
9272
|
appName
|
|
8201
9273
|
},
|
|
8202
9274
|
buildArgs,
|
|
8203
9275
|
publicUrlArgs: publicUrlArgNames
|
|
8204
9276
|
});
|
|
9277
|
+
setDeployedImage(state, appName, {
|
|
9278
|
+
ref: built.imageRef ?? imageRef$2,
|
|
9279
|
+
...built.digest ? { digest: built.digest } : {}
|
|
9280
|
+
});
|
|
9281
|
+
await journal.save();
|
|
8205
9282
|
const envVars = [
|
|
8206
9283
|
`NODE_ENV=production`,
|
|
8207
9284
|
`PORT=${app.port}`,
|
|
8208
9285
|
`STAGE=${stage}`
|
|
8209
9286
|
];
|
|
8210
9287
|
for (const [key, value] of Object.entries(resolved)) envVars.push(`${key}=${value}`);
|
|
8211
|
-
await api.saveDockerProvider(application.applicationId, imageRef, { registryId });
|
|
9288
|
+
await api.saveDockerProvider(application.applicationId, imageRef$2, { registryId });
|
|
8212
9289
|
await api.saveApplicationEnv(application.applicationId, envVars.join("\n"));
|
|
8213
9290
|
logger$4.log(` Deploying to Dokploy...`);
|
|
8214
9291
|
await api.deployApplication(application.applicationId);
|
|
8215
|
-
const
|
|
8216
|
-
|
|
8217
|
-
|
|
8218
|
-
|
|
8219
|
-
publicUrls[appName] = `https://${frontendHost}`;
|
|
8220
|
-
logger$4.log(` ✓ Domain: https://${frontendHost} (existing)`);
|
|
8221
|
-
} else try {
|
|
8222
|
-
const domain = await api.createDomain({
|
|
8223
|
-
host: frontendHost,
|
|
8224
|
-
port: app.port,
|
|
8225
|
-
https: true,
|
|
8226
|
-
certificateType: "letsencrypt",
|
|
8227
|
-
applicationId: application.applicationId
|
|
8228
|
-
});
|
|
8229
|
-
appHostnames.set(appName, frontendHost);
|
|
8230
|
-
appDomainIds.set(appName, domain.domainId);
|
|
8231
|
-
publicUrls[appName] = `https://${frontendHost}`;
|
|
8232
|
-
logger$4.log(` ✓ Domain: https://${frontendHost} (created)`);
|
|
8233
|
-
} catch (domainError) {
|
|
8234
|
-
const message = domainError instanceof Error ? domainError.message : "Unknown error";
|
|
8235
|
-
logger$4.log(` ⚠ Domain creation failed: ${message}`);
|
|
8236
|
-
appHostnames.set(appName, frontendHost);
|
|
8237
|
-
publicUrls[appName] = `https://${frontendHost}`;
|
|
8238
|
-
}
|
|
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}`;
|
|
8239
9296
|
results.push({
|
|
8240
9297
|
appName,
|
|
8241
9298
|
type: app.type,
|
|
8242
9299
|
success: true,
|
|
8243
9300
|
applicationId: application.applicationId,
|
|
8244
|
-
imageRef
|
|
9301
|
+
imageRef: imageRef$2,
|
|
9302
|
+
...built.digest ? { digest: built.digest } : {}
|
|
8245
9303
|
});
|
|
8246
9304
|
logger$4.log(` ✓ ${appName} deployed successfully`);
|
|
8247
9305
|
} catch (error) {
|
|
@@ -8257,14 +9315,14 @@ async function workspaceDeployCommand(configured, options) {
|
|
|
8257
9315
|
}
|
|
8258
9316
|
}
|
|
8259
9317
|
logger$4.log("\n📋 Saving deploy state...");
|
|
8260
|
-
await
|
|
9318
|
+
await journal.save();
|
|
8261
9319
|
logger$4.log(" ✓ State saved");
|
|
8262
9320
|
const dnsConfig = workspace.deploy.dns;
|
|
8263
9321
|
if (dnsConfig && appHostnames.size > 0) {
|
|
8264
9322
|
const dnsResult = await orchestrateDns(appHostnames, dnsConfig, creds.endpoint);
|
|
8265
9323
|
if (dnsResult?.serverIp && appHostnames.size > 0) {
|
|
8266
9324
|
await verifyDnsRecords(appHostnames, dnsResult.serverIp, state);
|
|
8267
|
-
await
|
|
9325
|
+
await journal.save();
|
|
8268
9326
|
}
|
|
8269
9327
|
if (dnsResult?.success && appHostnames.size > 0) {
|
|
8270
9328
|
logger$4.log("\n🔒 Validating domains for SSL certificates...");
|
|
@@ -8297,6 +9355,71 @@ async function workspaceDeployCommand(configured, options) {
|
|
|
8297
9355
|
};
|
|
8298
9356
|
}
|
|
8299
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
|
+
/**
|
|
8300
9423
|
* Main deploy command
|
|
8301
9424
|
*/
|
|
8302
9425
|
async function deployCommand(options) {
|
|
@@ -8433,453 +9556,160 @@ async function deployListCommand(options) {
|
|
|
8433
9556
|
}
|
|
8434
9557
|
}
|
|
8435
9558
|
//#endregion
|
|
8436
|
-
//#region src/deploy/
|
|
8437
|
-
var CachedStateProvider_exports = /* @__PURE__ */ __exportAll({ CachedStateProvider: () => CachedStateProvider });
|
|
8438
|
-
/**
|
|
8439
|
-
* Cached state provider that wraps a remote provider with local cache.
|
|
8440
|
-
*
|
|
8441
|
-
* @deprecated for deploys: a read prefers the local copy, so a stale one wins
|
|
8442
|
-
* over the remote without warning, and a write overwrites the remote blindly.
|
|
8443
|
-
* Deploy uses `createStateStore`, which reads SSM directly and writes
|
|
8444
|
-
* conditionally. Kept for `gkm state:pull | push | diff`.
|
|
8445
|
-
*/
|
|
8446
|
-
var CachedStateProvider = class {
|
|
8447
|
-
remote;
|
|
8448
|
-
local;
|
|
8449
|
-
constructor(remote, local) {
|
|
8450
|
-
this.remote = remote;
|
|
8451
|
-
this.local = local;
|
|
8452
|
-
}
|
|
8453
|
-
async read(stage) {
|
|
8454
|
-
const localState = await this.local.read(stage);
|
|
8455
|
-
if (localState) return localState;
|
|
8456
|
-
const remoteState = await this.remote.read(stage);
|
|
8457
|
-
if (remoteState) await this.local.write(stage, remoteState);
|
|
8458
|
-
return remoteState;
|
|
8459
|
-
}
|
|
8460
|
-
async write(stage, state) {
|
|
8461
|
-
await this.remote.write(stage, state);
|
|
8462
|
-
await this.local.write(stage, state);
|
|
8463
|
-
}
|
|
8464
|
-
/**
|
|
8465
|
-
* Force pull from remote to local.
|
|
8466
|
-
* Used by `gkm state pull` command.
|
|
8467
|
-
*/
|
|
8468
|
-
async pull(stage) {
|
|
8469
|
-
const remoteState = await this.remote.read(stage);
|
|
8470
|
-
if (remoteState) await this.local.write(stage, remoteState);
|
|
8471
|
-
return remoteState;
|
|
8472
|
-
}
|
|
8473
|
-
/**
|
|
8474
|
-
* Force push from local to remote.
|
|
8475
|
-
* Used by `gkm state push` command.
|
|
8476
|
-
*/
|
|
8477
|
-
async push(stage) {
|
|
8478
|
-
const localState = await this.local.read(stage);
|
|
8479
|
-
if (localState) await this.remote.write(stage, localState);
|
|
8480
|
-
return localState;
|
|
8481
|
-
}
|
|
8482
|
-
/**
|
|
8483
|
-
* Get both local and remote state for comparison.
|
|
8484
|
-
* Used by `gkm state diff` command.
|
|
8485
|
-
*/
|
|
8486
|
-
async diff(stage) {
|
|
8487
|
-
const [local, remote] = await Promise.all([this.local.read(stage), this.remote.read(stage)]);
|
|
8488
|
-
return {
|
|
8489
|
-
local,
|
|
8490
|
-
remote
|
|
8491
|
-
};
|
|
8492
|
-
}
|
|
8493
|
-
};
|
|
8494
|
-
//#endregion
|
|
8495
|
-
//#region src/deploy/StateStore.ts
|
|
9559
|
+
//#region src/deploy/LocalStateStore.ts
|
|
8496
9560
|
/**
|
|
8497
|
-
*
|
|
8498
|
-
*
|
|
8499
|
-
* `StateProvider` reads and writes a whole stage at once, with nothing to stop
|
|
8500
|
-
* two runs of the same stage from overwriting each other and nowhere to record
|
|
8501
|
-
* a resource the moment it exists. A `StateStore` adds the three things a
|
|
8502
|
-
* deploy needs to be safe against crashes and concurrent runs:
|
|
8503
|
-
*
|
|
8504
|
-
* - `lock(stage)`: one run per stage. A second gets `StateLocked`, naming the
|
|
8505
|
-
* holder, instead of racing the first.
|
|
8506
|
-
* - Versioned `read` and conditional `write`: a write names the version it was
|
|
8507
|
-
* based on, and raises `StateVersionConflict` when the stored state moved on
|
|
8508
|
-
* underneath it, so nothing is lost silently even without the lock.
|
|
8509
|
-
* - Per-resource records: `putResource` journals one resource at a time, so a
|
|
8510
|
-
* run that dies half way leaves behind the ids of everything it created.
|
|
9561
|
+
* Local filesystem state store.
|
|
8511
9562
|
*
|
|
8512
|
-
*
|
|
8513
|
-
*
|
|
8514
|
-
|
|
8515
|
-
*/
|
|
8516
|
-
|
|
8517
|
-
|
|
8518
|
-
|
|
8519
|
-
StageStateMissing: () => StageStateMissing,
|
|
8520
|
-
StateLocked: () => StateLocked,
|
|
8521
|
-
StateSchemaTooNew: () => StateSchemaTooNew,
|
|
8522
|
-
StateStoreBusy: () => StateStoreBusy,
|
|
8523
|
-
StateStoreNeedsWorkspaceName: () => StateStoreNeedsWorkspaceName,
|
|
8524
|
-
StateStoreProvider: () => StateStoreProvider,
|
|
8525
|
-
StateStoreWithoutLocking: () => StateStoreWithoutLocking,
|
|
8526
|
-
StateUnreadable: () => StateUnreadable,
|
|
8527
|
-
StateVersionConflict: () => StateVersionConflict,
|
|
8528
|
-
UnknownStateProvider: () => UnknownStateProvider,
|
|
8529
|
-
contentVersion: () => contentVersion,
|
|
8530
|
-
createStateStore: () => createStateStore,
|
|
8531
|
-
decodeDocument: () => decodeDocument,
|
|
8532
|
-
encodeDocument: () => encodeDocument,
|
|
8533
|
-
isStateStore: () => isStateStore,
|
|
8534
|
-
migrateV1: () => migrateV1,
|
|
8535
|
-
newLockHolder: () => newLockHolder
|
|
8536
|
-
});
|
|
8537
|
-
var StateLocked = class extends Error {
|
|
8538
|
-
stage;
|
|
8539
|
-
holder;
|
|
8540
|
-
location;
|
|
8541
|
-
constructor(stage, holder, location) {
|
|
8542
|
-
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";
|
|
8543
|
-
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}\`.`);
|
|
8544
|
-
this.stage = stage;
|
|
8545
|
-
this.holder = holder;
|
|
8546
|
-
this.location = location;
|
|
8547
|
-
this.name = "StateLocked";
|
|
8548
|
-
}
|
|
8549
|
-
};
|
|
8550
|
-
var StateVersionConflict = class extends Error {
|
|
8551
|
-
stage;
|
|
8552
|
-
expectedVersion;
|
|
8553
|
-
actualVersion;
|
|
8554
|
-
constructor(stage, expectedVersion, actualVersion) {
|
|
8555
|
-
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.`);
|
|
8556
|
-
this.stage = stage;
|
|
8557
|
-
this.expectedVersion = expectedVersion;
|
|
8558
|
-
this.actualVersion = actualVersion;
|
|
8559
|
-
this.name = "StateVersionConflict";
|
|
8560
|
-
}
|
|
8561
|
-
};
|
|
8562
|
-
var StageStateMissing = class extends Error {
|
|
8563
|
-
stage;
|
|
8564
|
-
constructor(stage) {
|
|
8565
|
-
super(`There is no deploy state for stage '${stage}' to record a resource in. Write the stage's state first (write with expectedVersion: null).`);
|
|
8566
|
-
this.stage = stage;
|
|
8567
|
-
this.name = "StageStateMissing";
|
|
8568
|
-
}
|
|
8569
|
-
};
|
|
8570
|
-
var StateUnreadable = class extends Error {
|
|
8571
|
-
stage;
|
|
8572
|
-
location;
|
|
8573
|
-
constructor(stage, location, cause) {
|
|
8574
|
-
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 });
|
|
8575
|
-
this.stage = stage;
|
|
8576
|
-
this.location = location;
|
|
8577
|
-
this.name = "StateUnreadable";
|
|
8578
|
-
}
|
|
8579
|
-
};
|
|
8580
|
-
var StateSchemaTooNew = class extends Error {
|
|
8581
|
-
stage;
|
|
8582
|
-
schemaVersion;
|
|
8583
|
-
constructor(stage, schemaVersion) {
|
|
8584
|
-
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.`);
|
|
8585
|
-
this.stage = stage;
|
|
8586
|
-
this.schemaVersion = schemaVersion;
|
|
8587
|
-
this.name = "StateSchemaTooNew";
|
|
8588
|
-
}
|
|
8589
|
-
};
|
|
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;
|
|
8590
9570
|
/**
|
|
8591
|
-
*
|
|
8592
|
-
*
|
|
8593
|
-
* 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.
|
|
8594
9573
|
*/
|
|
8595
|
-
|
|
8596
|
-
|
|
8597
|
-
|
|
8598
|
-
|
|
8599
|
-
|
|
8600
|
-
|
|
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;
|
|
8601
9583
|
}
|
|
8602
|
-
|
|
8603
|
-
|
|
8604
|
-
provider;
|
|
8605
|
-
constructor(provider) {
|
|
8606
|
-
super(`The '${provider}' state provider keys state by workspace name. Set "name" in gkm.config.ts.`);
|
|
8607
|
-
this.provider = provider;
|
|
8608
|
-
this.name = "StateStoreNeedsWorkspaceName";
|
|
9584
|
+
get dir() {
|
|
9585
|
+
return join(this.workspaceRoot, ".gkm");
|
|
8609
9586
|
}
|
|
8610
|
-
|
|
8611
|
-
|
|
8612
|
-
config;
|
|
8613
|
-
constructor(config) {
|
|
8614
|
-
super(`Unknown state provider ${JSON.stringify(config)}. Use 'local', 'ssm', 's3', or an object implementing StateStore or StateProvider.`);
|
|
8615
|
-
this.config = config;
|
|
8616
|
-
this.name = "UnknownStateProvider";
|
|
9587
|
+
statePath(stage) {
|
|
9588
|
+
return join(this.dir, `deploy-${stage}.json`);
|
|
8617
9589
|
}
|
|
8618
|
-
|
|
8619
|
-
|
|
8620
|
-
var StateStoreBusy = class extends Error {
|
|
8621
|
-
stage;
|
|
8622
|
-
location;
|
|
8623
|
-
constructor(stage, location) {
|
|
8624
|
-
super(`Timed out waiting to write deploy state for stage '${stage}' (${location}). If no other gkm process is running, remove ${location}.`);
|
|
8625
|
-
this.stage = stage;
|
|
8626
|
-
this.location = location;
|
|
8627
|
-
this.name = "StateStoreBusy";
|
|
9590
|
+
lockPath(stage) {
|
|
9591
|
+
return join(this.dir, `deploy-${stage}.lock`);
|
|
8628
9592
|
}
|
|
8629
|
-
|
|
8630
|
-
|
|
8631
|
-
let parsed;
|
|
8632
|
-
try {
|
|
8633
|
-
parsed = JSON.parse(body);
|
|
8634
|
-
} catch (error) {
|
|
8635
|
-
throw new StateUnreadable(stage, location, error);
|
|
9593
|
+
backupPath(stage) {
|
|
9594
|
+
return join(this.dir, `deploy-${stage}.v1.json`);
|
|
8636
9595
|
}
|
|
8637
|
-
|
|
8638
|
-
|
|
8639
|
-
if (schemaVersion === void 0) return {
|
|
8640
|
-
schemaVersion: 1,
|
|
8641
|
-
state: parsed
|
|
8642
|
-
};
|
|
8643
|
-
if (schemaVersion === 2) return {
|
|
8644
|
-
schemaVersion: 2,
|
|
8645
|
-
document: parsed
|
|
8646
|
-
};
|
|
8647
|
-
throw new StateSchemaTooNew(stage, schemaVersion);
|
|
8648
|
-
}
|
|
8649
|
-
function encodeDocument(document) {
|
|
8650
|
-
return JSON.stringify(document, null, 2);
|
|
8651
|
-
}
|
|
8652
|
-
/**
|
|
8653
|
-
* v1 state as a v2 document. The ids v1 kept are seeded as `ready` records,
|
|
8654
|
-
* so the first journaled deploy after an upgrade adopts them rather than
|
|
8655
|
-
* treating them as unknown.
|
|
8656
|
-
*/
|
|
8657
|
-
function migrateV1(stage, state) {
|
|
8658
|
-
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
8659
|
-
const resources = {};
|
|
8660
|
-
const seed = (type, name, id) => {
|
|
8661
|
-
if (!id) return;
|
|
8662
|
-
const key = name ? `${type}:${name}` : type;
|
|
8663
|
-
resources[key] = {
|
|
8664
|
-
key,
|
|
8665
|
-
type,
|
|
8666
|
-
id,
|
|
8667
|
-
status: "ready",
|
|
8668
|
-
updatedAt: now
|
|
8669
|
-
};
|
|
8670
|
-
};
|
|
8671
|
-
seed("project", null, state.projectId);
|
|
8672
|
-
seed("environment", null, state.environmentId);
|
|
8673
|
-
for (const [app, id] of Object.entries(state.applications ?? {})) seed("application", app, id);
|
|
8674
|
-
seed("postgres", null, state.services?.postgresId);
|
|
8675
|
-
seed("redis", null, state.services?.redisId);
|
|
8676
|
-
seed("backup-destination", null, state.backups?.destinationId);
|
|
8677
|
-
return {
|
|
8678
|
-
schemaVersion: 2,
|
|
8679
|
-
stage,
|
|
8680
|
-
serial: 1,
|
|
8681
|
-
state,
|
|
8682
|
-
resources,
|
|
8683
|
-
updatedAt: now
|
|
8684
|
-
};
|
|
8685
|
-
}
|
|
8686
|
-
function newLockHolder(options) {
|
|
8687
|
-
let owner = "unknown";
|
|
8688
|
-
try {
|
|
8689
|
-
owner = userInfo().username;
|
|
8690
|
-
} catch {}
|
|
8691
|
-
return {
|
|
8692
|
-
id: randomUUID(),
|
|
8693
|
-
owner,
|
|
8694
|
-
host: hostname(),
|
|
8695
|
-
pid: process.pid,
|
|
8696
|
-
acquiredAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
8697
|
-
operation: options?.operation
|
|
8698
|
-
};
|
|
8699
|
-
}
|
|
8700
|
-
function contentVersion(body) {
|
|
8701
|
-
return createHash("sha256").update(body).digest("hex").slice(0, 16);
|
|
8702
|
-
}
|
|
8703
|
-
/**
|
|
8704
|
-
* A store over a backend that can read and conditionally write one blob per
|
|
8705
|
-
* stage, and create a lock object only if it does not exist. Migration and
|
|
8706
|
-
* resource records are the same for every such backend, so they live here.
|
|
8707
|
-
*/
|
|
8708
|
-
var DocumentStateStore = class {
|
|
8709
|
-
async lock(stage, options) {
|
|
8710
|
-
const holder = newLockHolder(options);
|
|
8711
|
-
await this.createLock(stage, holder);
|
|
8712
|
-
let released = false;
|
|
8713
|
-
return {
|
|
8714
|
-
stage,
|
|
8715
|
-
holder,
|
|
8716
|
-
release: async () => {
|
|
8717
|
-
if (released) return;
|
|
8718
|
-
released = true;
|
|
8719
|
-
await this.removeLock(stage, holder);
|
|
8720
|
-
}
|
|
8721
|
-
};
|
|
9596
|
+
mutexPath(stage) {
|
|
9597
|
+
return join(this.dir, `deploy-${stage}.json.mutex`);
|
|
8722
9598
|
}
|
|
8723
|
-
|
|
8724
|
-
|
|
8725
|
-
await this.removeLock(stage, null);
|
|
8726
|
-
return holder;
|
|
9599
|
+
location(stage) {
|
|
9600
|
+
return this.statePath(stage);
|
|
8727
9601
|
}
|
|
8728
|
-
async
|
|
8729
|
-
|
|
8730
|
-
|
|
8731
|
-
|
|
8732
|
-
|
|
8733
|
-
|
|
8734
|
-
version: current.version
|
|
8735
|
-
};
|
|
8736
|
-
}
|
|
8737
|
-
async write(stage, state, options) {
|
|
8738
|
-
if (options.expectedVersion === null) {
|
|
8739
|
-
const document = {
|
|
8740
|
-
schemaVersion: 2,
|
|
8741
|
-
stage,
|
|
8742
|
-
serial: 1,
|
|
8743
|
-
state,
|
|
8744
|
-
resources: {},
|
|
8745
|
-
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)
|
|
8746
9608
|
};
|
|
8747
|
-
|
|
9609
|
+
} catch (error) {
|
|
9610
|
+
if (isCode(error, "ENOENT")) return null;
|
|
9611
|
+
throw error;
|
|
8748
9612
|
}
|
|
8749
|
-
return this.mutate(stage, options.expectedVersion, (document) => ({
|
|
8750
|
-
...document,
|
|
8751
|
-
state
|
|
8752
|
-
}));
|
|
8753
9613
|
}
|
|
8754
|
-
async
|
|
8755
|
-
|
|
8756
|
-
|
|
8757
|
-
|
|
8758
|
-
|
|
8759
|
-
|
|
8760
|
-
|
|
8761
|
-
|
|
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();
|
|
8762
9628
|
}
|
|
9629
|
+
await rename(temp, target);
|
|
9630
|
+
} catch (error) {
|
|
9631
|
+
await rm(temp, { force: true });
|
|
9632
|
+
throw error;
|
|
8763
9633
|
}
|
|
8764
|
-
|
|
8765
|
-
}
|
|
8766
|
-
async deleteResource(stage, key, options) {
|
|
8767
|
-
return this.mutate(stage, options?.expectedVersion, (document) => {
|
|
8768
|
-
const { [key]: _removed, ...resources } = document.resources;
|
|
8769
|
-
return {
|
|
8770
|
-
...document,
|
|
8771
|
-
resources
|
|
8772
|
-
};
|
|
9634
|
+
return contentVersion(body);
|
|
8773
9635
|
});
|
|
8774
9636
|
}
|
|
8775
|
-
async
|
|
8776
|
-
|
|
8777
|
-
|
|
8778
|
-
|
|
8779
|
-
|
|
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;
|
|
8780
9645
|
}
|
|
8781
|
-
if (expectedVersion !== void 0 && current.version !== expectedVersion) throw new StateVersionConflict(stage, expectedVersion, current.version);
|
|
8782
|
-
const next = change(current.document);
|
|
8783
|
-
next.serial = current.document.serial + 1;
|
|
8784
|
-
next.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
8785
|
-
return this.writeRaw(stage, encodeDocument(next), current.version);
|
|
8786
9646
|
}
|
|
8787
|
-
|
|
8788
|
-
|
|
8789
|
-
|
|
8790
|
-
* wrote) first, its result is read instead.
|
|
8791
|
-
*/
|
|
8792
|
-
async readDocument(stage, attempt = 0) {
|
|
8793
|
-
const raw = await this.readRaw(stage);
|
|
8794
|
-
if (!raw) return null;
|
|
8795
|
-
const decoded = decodeDocument(stage, raw.body, this.location(stage));
|
|
8796
|
-
if (decoded.schemaVersion === 2) return {
|
|
8797
|
-
document: decoded.document,
|
|
8798
|
-
version: raw.version
|
|
8799
|
-
};
|
|
8800
|
-
await this.writeV1Backup(stage, raw.body);
|
|
8801
|
-
const document = migrateV1(stage, decoded.state);
|
|
9647
|
+
async createLock(stage, holder) {
|
|
9648
|
+
await this.ensureDir();
|
|
9649
|
+
let handle;
|
|
8802
9650
|
try {
|
|
8803
|
-
|
|
8804
|
-
document,
|
|
8805
|
-
version: await this.writeRaw(stage, encodeDocument(document), raw.version)
|
|
8806
|
-
};
|
|
9651
|
+
handle = await open(this.lockPath(stage), "wx", FILE_MODE);
|
|
8807
9652
|
} catch (error) {
|
|
8808
|
-
if (error
|
|
9653
|
+
if (isCode(error, "EEXIST")) throw new StateLocked(stage, await this.readLock(stage), this.lockPath(stage));
|
|
8809
9654
|
throw error;
|
|
8810
9655
|
}
|
|
9656
|
+
try {
|
|
9657
|
+
await handle.writeFile(JSON.stringify(holder, null, 2));
|
|
9658
|
+
} finally {
|
|
9659
|
+
await handle.close();
|
|
9660
|
+
}
|
|
8811
9661
|
}
|
|
8812
|
-
|
|
8813
|
-
|
|
8814
|
-
|
|
8815
|
-
}
|
|
8816
|
-
|
|
8817
|
-
|
|
8818
|
-
* `createStateProvider` reads:
|
|
8819
|
-
*
|
|
8820
|
-
* - none / `'local'`: `LocalStateStore` under `.gkm/`
|
|
8821
|
-
* - `'ssm'`: `SSMStateStore`, with no local cache — SSM is read directly, so
|
|
8822
|
-
* a stale local copy can never win over it
|
|
8823
|
-
* - `'s3'`: `S3StateStore`
|
|
8824
|
-
* - a `StateStore` object: used as is
|
|
8825
|
-
* - a `StateProvider` object: wrapped by `LegacyStateStore`
|
|
8826
|
-
*/
|
|
8827
|
-
async function createStateStore(options) {
|
|
8828
|
-
const { config, workspaceRoot, workspaceName } = options;
|
|
8829
|
-
if (!config || config.provider === "local") {
|
|
8830
|
-
const { LocalStateStore } = await import("./LocalStateStore-BupdV-VN.mjs");
|
|
8831
|
-
return new LocalStateStore(workspaceRoot);
|
|
8832
|
-
}
|
|
8833
|
-
if (isStateStore(config.provider)) return config.provider;
|
|
8834
|
-
if (isStateProvider(config.provider)) {
|
|
8835
|
-
const { LegacyStateStore } = await import("./LegacyStateStore-ByRWQ9s1.mjs");
|
|
8836
|
-
return new LegacyStateStore(config.provider, { warn: options.warn });
|
|
8837
|
-
}
|
|
8838
|
-
if (config.provider === "ssm") {
|
|
8839
|
-
if (!workspaceName) throw new StateStoreNeedsWorkspaceName("ssm");
|
|
8840
|
-
const { SSMStateStore } = await import("./SSMStateStore-7hPWW7v7.mjs");
|
|
8841
|
-
const ssm = config;
|
|
8842
|
-
return SSMStateStore.create({
|
|
8843
|
-
workspaceName,
|
|
8844
|
-
region: ssm.region,
|
|
8845
|
-
profile: ssm.profile
|
|
8846
|
-
});
|
|
9662
|
+
async readLock(stage) {
|
|
9663
|
+
try {
|
|
9664
|
+
return JSON.parse(await readFile(this.lockPath(stage), "utf-8"));
|
|
9665
|
+
} catch {
|
|
9666
|
+
return null;
|
|
9667
|
+
}
|
|
8847
9668
|
}
|
|
8848
|
-
|
|
8849
|
-
if (
|
|
8850
|
-
|
|
8851
|
-
|
|
8852
|
-
|
|
8853
|
-
workspaceName,
|
|
8854
|
-
bucket: s3.bucket,
|
|
8855
|
-
prefix: s3.prefix,
|
|
8856
|
-
region: s3.region,
|
|
8857
|
-
profile: s3.profile
|
|
8858
|
-
});
|
|
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 });
|
|
8859
9674
|
}
|
|
8860
|
-
|
|
8861
|
-
|
|
8862
|
-
|
|
8863
|
-
|
|
8864
|
-
|
|
8865
|
-
* conditional on the version this provider last read.
|
|
8866
|
-
*/
|
|
8867
|
-
var StateStoreProvider = class {
|
|
8868
|
-
store;
|
|
8869
|
-
versions = /* @__PURE__ */ new Map();
|
|
8870
|
-
constructor(store) {
|
|
8871
|
-
this.store = store;
|
|
9675
|
+
async ensureDir() {
|
|
9676
|
+
await mkdir(this.dir, {
|
|
9677
|
+
recursive: true,
|
|
9678
|
+
mode: 448
|
|
9679
|
+
});
|
|
8872
9680
|
}
|
|
8873
|
-
|
|
8874
|
-
|
|
8875
|
-
|
|
8876
|
-
|
|
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
|
+
}
|
|
8877
9706
|
}
|
|
8878
|
-
async
|
|
8879
|
-
|
|
8880
|
-
|
|
8881
|
-
|
|
8882
|
-
|
|
9707
|
+
async isStale(path) {
|
|
9708
|
+
try {
|
|
9709
|
+
return Date.now() - (await stat(path)).mtimeMs > MUTEX_STALE_MS;
|
|
9710
|
+
} catch {
|
|
9711
|
+
return false;
|
|
9712
|
+
}
|
|
8883
9713
|
}
|
|
8884
9714
|
};
|
|
8885
9715
|
//#endregion
|
|
@@ -8887,7 +9717,12 @@ var StateStoreProvider = class {
|
|
|
8887
9717
|
/**
|
|
8888
9718
|
* State Management CLI Commands
|
|
8889
9719
|
*
|
|
8890
|
-
* Commands for managing deployment state across local and remote
|
|
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.
|
|
8891
9726
|
*/
|
|
8892
9727
|
/** What `state:show` prints in place of a secret. */
|
|
8893
9728
|
const MASKED = "********";
|
|
@@ -8913,26 +9748,12 @@ function maskStateSecrets(state) {
|
|
|
8913
9748
|
*/
|
|
8914
9749
|
async function statePullCommand(options) {
|
|
8915
9750
|
const { workspace } = await loadWorkspaceConfig();
|
|
8916
|
-
|
|
8917
|
-
console.error("No remote state provider configured.");
|
|
8918
|
-
console.error("Add a remote provider in gkm.config.ts:");
|
|
8919
|
-
console.error(" state: { provider: \"ssm\", region: \"us-east-1\" }");
|
|
8920
|
-
process.exit(1);
|
|
8921
|
-
}
|
|
8922
|
-
const provider = await createStateProvider({
|
|
8923
|
-
config: workspace.state,
|
|
8924
|
-
workspaceRoot: workspace.root,
|
|
8925
|
-
workspaceName: workspace.name
|
|
8926
|
-
});
|
|
8927
|
-
if (!(provider instanceof CachedStateProvider)) {
|
|
8928
|
-
console.error("State provider does not support pull operation.");
|
|
8929
|
-
process.exit(1);
|
|
8930
|
-
}
|
|
9751
|
+
const { local, remote } = await remoteAndLocal(workspace);
|
|
8931
9752
|
console.log(`Pulling state for stage: ${options.stage}...`);
|
|
8932
|
-
const
|
|
8933
|
-
if (
|
|
9753
|
+
const pulled = await copyUnderLock(remote, local, local, options.stage, "state:pull");
|
|
9754
|
+
if (pulled) {
|
|
8934
9755
|
console.log("State pulled successfully.");
|
|
8935
|
-
printStateSummary(state);
|
|
9756
|
+
printStateSummary(pulled.state);
|
|
8936
9757
|
} else console.log("No remote state found for this stage.");
|
|
8937
9758
|
}
|
|
8938
9759
|
/**
|
|
@@ -8941,26 +9762,12 @@ async function statePullCommand(options) {
|
|
|
8941
9762
|
*/
|
|
8942
9763
|
async function statePushCommand(options) {
|
|
8943
9764
|
const { workspace } = await loadWorkspaceConfig();
|
|
8944
|
-
|
|
8945
|
-
console.error("No remote state provider configured.");
|
|
8946
|
-
console.error("Add a remote provider in gkm.config.ts:");
|
|
8947
|
-
console.error(" state: { provider: \"ssm\", region: \"us-east-1\" }");
|
|
8948
|
-
process.exit(1);
|
|
8949
|
-
}
|
|
8950
|
-
const provider = await createStateProvider({
|
|
8951
|
-
config: workspace.state,
|
|
8952
|
-
workspaceRoot: workspace.root,
|
|
8953
|
-
workspaceName: workspace.name
|
|
8954
|
-
});
|
|
8955
|
-
if (!(provider instanceof CachedStateProvider)) {
|
|
8956
|
-
console.error("State provider does not support push operation.");
|
|
8957
|
-
process.exit(1);
|
|
8958
|
-
}
|
|
9765
|
+
const { local, remote } = await remoteAndLocal(workspace);
|
|
8959
9766
|
console.log(`Pushing state for stage: ${options.stage}...`);
|
|
8960
|
-
const
|
|
8961
|
-
if (
|
|
9767
|
+
const pushed = await copyUnderLock(local, remote, remote, options.stage, "state:push");
|
|
9768
|
+
if (pushed) {
|
|
8962
9769
|
console.log("State pushed successfully.");
|
|
8963
|
-
printStateSummary(state);
|
|
9770
|
+
printStateSummary(pushed.state);
|
|
8964
9771
|
} else console.log("No local state found for this stage.");
|
|
8965
9772
|
}
|
|
8966
9773
|
/**
|
|
@@ -8969,7 +9776,7 @@ async function statePushCommand(options) {
|
|
|
8969
9776
|
*/
|
|
8970
9777
|
async function stateShowCommand(options) {
|
|
8971
9778
|
const { workspace } = await loadWorkspaceConfig();
|
|
8972
|
-
const stored = await (await
|
|
9779
|
+
const stored = await (await createStateStore({
|
|
8973
9780
|
config: workspace.state,
|
|
8974
9781
|
workspaceRoot: workspace.root,
|
|
8975
9782
|
workspaceName: workspace.name
|
|
@@ -8978,9 +9785,12 @@ async function stateShowCommand(options) {
|
|
|
8978
9785
|
console.log(`No state found for stage: ${options.stage}`);
|
|
8979
9786
|
return;
|
|
8980
9787
|
}
|
|
8981
|
-
const state = maskStateSecrets(stored);
|
|
9788
|
+
const state = maskStateSecrets(stored.state);
|
|
8982
9789
|
if (options.json) console.log(JSON.stringify(state, null, 2));
|
|
8983
|
-
else
|
|
9790
|
+
else {
|
|
9791
|
+
printStateDetails(state);
|
|
9792
|
+
printUnfinished(stored.resources);
|
|
9793
|
+
}
|
|
8984
9794
|
}
|
|
8985
9795
|
/**
|
|
8986
9796
|
* Release a stage's deploy lock, whoever holds it.
|
|
@@ -9009,22 +9819,11 @@ async function stateUnlockCommand(options) {
|
|
|
9009
9819
|
*/
|
|
9010
9820
|
async function stateDiffCommand(options) {
|
|
9011
9821
|
const { workspace } = await loadWorkspaceConfig();
|
|
9012
|
-
|
|
9013
|
-
console.error("No remote state provider configured.");
|
|
9014
|
-
console.error("Diff requires a remote provider to compare against.");
|
|
9015
|
-
process.exit(1);
|
|
9016
|
-
}
|
|
9017
|
-
const provider = await createStateProvider({
|
|
9018
|
-
config: workspace.state,
|
|
9019
|
-
workspaceRoot: workspace.root,
|
|
9020
|
-
workspaceName: workspace.name
|
|
9021
|
-
});
|
|
9022
|
-
if (!(provider instanceof CachedStateProvider)) {
|
|
9023
|
-
console.error("State provider does not support diff operation.");
|
|
9024
|
-
process.exit(1);
|
|
9025
|
-
}
|
|
9822
|
+
const stores = await remoteAndLocal(workspace, ["Diff requires a remote provider to compare against."]);
|
|
9026
9823
|
console.log(`Comparing state for stage: ${options.stage}...\n`);
|
|
9027
|
-
const
|
|
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;
|
|
9028
9827
|
if (!local && !remote) {
|
|
9029
9828
|
console.log("No state found (local or remote).");
|
|
9030
9829
|
return;
|
|
@@ -9060,6 +9859,71 @@ async function stateDiffCommand(options) {
|
|
|
9060
9859
|
else console.log(` ${key}: ${localVal ?? "(none)"} (local) != ${remoteVal ?? "(none)"} (remote)`);
|
|
9061
9860
|
}
|
|
9062
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}`);
|
|
9063
9927
|
}
|
|
9064
9928
|
function printStateSummary(state) {
|
|
9065
9929
|
const appCount = Object.keys(state.applications).length;
|
|
@@ -10107,25 +10971,25 @@ function loadPackageJson() {
|
|
|
10107
10971
|
* Run: pnpm --filter @geekmidas/cli sync-versions
|
|
10108
10972
|
*/
|
|
10109
10973
|
const GEEKMIDAS_VERSIONS = {
|
|
10110
|
-
"@geekmidas/audit": "~10.0.0-alpha.
|
|
10111
|
-
"@geekmidas/auth": "~10.0.0-alpha.
|
|
10112
|
-
"@geekmidas/cache": "~10.0.0-alpha.
|
|
10113
|
-
"@geekmidas/client": "~10.0.0-alpha.
|
|
10114
|
-
"@geekmidas/cloud": "~10.0.0-alpha.
|
|
10115
|
-
"@geekmidas/constructs": "~10.0.0-alpha.
|
|
10116
|
-
"@geekmidas/db": "~10.0.0-alpha.
|
|
10117
|
-
"@geekmidas/emailkit": "~10.0.0-alpha.
|
|
10118
|
-
"@geekmidas/envkit": "~10.0.0-alpha.
|
|
10119
|
-
"@geekmidas/errors": "~10.0.0-alpha.
|
|
10120
|
-
"@geekmidas/events": "~10.0.0-alpha.
|
|
10121
|
-
"@geekmidas/logger": "~10.0.0-alpha.
|
|
10122
|
-
"@geekmidas/manifest": "~10.0.0-alpha.
|
|
10123
|
-
"@geekmidas/rate-limit": "~10.0.0-alpha.
|
|
10124
|
-
"@geekmidas/schema": "~10.0.0-alpha.
|
|
10125
|
-
"@geekmidas/services": "~10.0.0-alpha.
|
|
10126
|
-
"@geekmidas/storage": "~10.0.0-alpha.
|
|
10127
|
-
"@geekmidas/telescope": "~10.0.0-alpha.
|
|
10128
|
-
"@geekmidas/testkit": "~10.0.0-alpha.
|
|
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",
|
|
10129
10993
|
"@geekmidas/cli": `~${loadPackageJson().version}`
|
|
10130
10994
|
};
|
|
10131
10995
|
//#endregion
|
|
@@ -16007,7 +16871,7 @@ function testDatabases(reconciled) {
|
|
|
16007
16871
|
* starts without the reference data the app needs.
|
|
16008
16872
|
*/
|
|
16009
16873
|
async function migrateTestStage(workspace, env) {
|
|
16010
|
-
const { migrateAndSeed } = await import("./migrate-
|
|
16874
|
+
const { migrateAndSeed } = await import("./migrate-BrFREmgQ.mjs");
|
|
16011
16875
|
const { discover } = await import("./discover-Cf2jAcgF.mjs").then((n) => n.n);
|
|
16012
16876
|
const sources = {};
|
|
16013
16877
|
const manifest = await discover({
|
|
@@ -16438,7 +17302,7 @@ program.command("migrate").description("Apply each database construct's migratio
|
|
|
16438
17302
|
try {
|
|
16439
17303
|
const globalOptions = program.opts();
|
|
16440
17304
|
if (globalOptions.cwd) process.chdir(globalOptions.cwd);
|
|
16441
|
-
const { migrateCommand } = await import("./migrate-
|
|
17305
|
+
const { migrateCommand } = await import("./migrate-BrFREmgQ.mjs");
|
|
16442
17306
|
await migrateCommand({
|
|
16443
17307
|
...options,
|
|
16444
17308
|
...construct ? { construct } : {}
|
|
@@ -16452,7 +17316,7 @@ program.command("seed").description("Migrate, then run each database construct's
|
|
|
16452
17316
|
try {
|
|
16453
17317
|
const globalOptions = program.opts();
|
|
16454
17318
|
if (globalOptions.cwd) process.chdir(globalOptions.cwd);
|
|
16455
|
-
const { seedCommand } = await import("./migrate-
|
|
17319
|
+
const { seedCommand } = await import("./migrate-BrFREmgQ.mjs");
|
|
16456
17320
|
await seedCommand({
|
|
16457
17321
|
...options,
|
|
16458
17322
|
...construct ? { construct } : {}
|
|
@@ -16466,7 +17330,7 @@ program.command("migration").description("Write a construct's next migration: an
|
|
|
16466
17330
|
try {
|
|
16467
17331
|
const globalOptions = program.opts();
|
|
16468
17332
|
if (globalOptions.cwd) process.chdir(globalOptions.cwd);
|
|
16469
|
-
const { migrationCommand } = await import("./migrate-
|
|
17333
|
+
const { migrationCommand } = await import("./migrate-BrFREmgQ.mjs");
|
|
16470
17334
|
await migrationCommand({
|
|
16471
17335
|
construct,
|
|
16472
17336
|
...name ? { name } : {}
|
|
@@ -16564,7 +17428,7 @@ program.command("encryption:rotate").description("Add a new current key to an En
|
|
|
16564
17428
|
try {
|
|
16565
17429
|
const globalOptions = program.opts();
|
|
16566
17430
|
if (globalOptions.cwd) process.chdir(globalOptions.cwd);
|
|
16567
|
-
const { encryptionRotateCommand } = await import("./encryption-
|
|
17431
|
+
const { encryptionRotateCommand } = await import("./encryption-BW7Gh51G.mjs");
|
|
16568
17432
|
await encryptionRotateCommand(construct, options);
|
|
16569
17433
|
} catch (error) {
|
|
16570
17434
|
console.error(formatError(error));
|
|
@@ -16575,7 +17439,7 @@ program.command("encryption:retire").description("Remove an old key from an Encr
|
|
|
16575
17439
|
try {
|
|
16576
17440
|
const globalOptions = program.opts();
|
|
16577
17441
|
if (globalOptions.cwd) process.chdir(globalOptions.cwd);
|
|
16578
|
-
const { encryptionRetireCommand } = await import("./encryption-
|
|
17442
|
+
const { encryptionRetireCommand } = await import("./encryption-BW7Gh51G.mjs");
|
|
16579
17443
|
await encryptionRetireCommand(construct, key, options);
|
|
16580
17444
|
} catch (error) {
|
|
16581
17445
|
console.error(formatError(error));
|
|
@@ -16627,7 +17491,7 @@ program.command("secrets:reconcile").description("Backfill missing custom secret
|
|
|
16627
17491
|
console.error(`No secrets found for stage "${stage}". Run "gkm secrets:init --stage ${stage}" first.`);
|
|
16628
17492
|
process.exit(1);
|
|
16629
17493
|
}
|
|
16630
|
-
const { derivedContainers } = await import("./workspace-
|
|
17494
|
+
const { derivedContainers } = await import("./workspace-BRZ14o-o.mjs").then((n) => n.o);
|
|
16631
17495
|
const result = reconcileMissingSecrets(secrets, workspace, await derivedContainers(workspace, stage));
|
|
16632
17496
|
if (!result) {
|
|
16633
17497
|
console.log(`\n✓ Secrets for stage "${stage}" are up-to-date`);
|
|
@@ -16686,7 +17550,7 @@ program.command("deploy:github").description("Let GitHub Actions deploy a stage:
|
|
|
16686
17550
|
try {
|
|
16687
17551
|
const globalOptions = program.opts();
|
|
16688
17552
|
if (globalOptions.cwd) process.chdir(globalOptions.cwd);
|
|
16689
|
-
const { deployGithubCommand } = await import("./github-
|
|
17553
|
+
const { deployGithubCommand } = await import("./github-T_6sK3Rz.mjs");
|
|
16690
17554
|
await deployGithubCommand(options);
|
|
16691
17555
|
} catch (error) {
|
|
16692
17556
|
console.error(formatError(error));
|
|
@@ -16821,6 +17685,6 @@ program.command("upgrade").description("Upgrade @geekmidas/cli (with --all, ever
|
|
|
16821
17685
|
});
|
|
16822
17686
|
program.parse();
|
|
16823
17687
|
//#endregion
|
|
16824
|
-
export {
|
|
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 };
|
|
16825
17689
|
|
|
16826
17690
|
//# sourceMappingURL=index.mjs.map
|