@geekmidas/cli 1.10.41 → 1.12.0
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/CHANGELOG.md +149 -0
- package/README.md +14 -0
- package/dist/{CachedStateProvider-37pG1HBx.cjs → CachedStateProvider-BdX7klRc.cjs} +1 -1
- package/dist/{CachedStateProvider-C8A-cCm5.mjs → CachedStateProvider-CzyFct42.mjs} +1 -1
- package/dist/{CachedStateProvider-C8A-cCm5.mjs.map → CachedStateProvider-CzyFct42.mjs.map} +1 -1
- package/dist/CachedStateProvider-GcGziXh1.mjs +3 -0
- package/dist/{CachedStateProvider-CxsXOKkg.cjs → CachedStateProvider-klLWZXWF.cjs} +1 -1
- package/dist/{CachedStateProvider-CxsXOKkg.cjs.map → CachedStateProvider-klLWZXWF.cjs.map} +1 -1
- package/dist/{HostingerProvider-ANWchdiK.mjs → HostingerProvider-B2ZYz7Yc.mjs} +1 -1
- package/dist/{HostingerProvider-ANWchdiK.mjs.map → HostingerProvider-B2ZYz7Yc.mjs.map} +1 -1
- package/dist/{HostingerProvider-5KYmwoK2.cjs → HostingerProvider-CrHfsgz9.cjs} +1 -1
- package/dist/{HostingerProvider-5KYmwoK2.cjs.map → HostingerProvider-CrHfsgz9.cjs.map} +1 -1
- package/dist/{LocalStateProvider-Dp0KkRcw.mjs → LocalStateProvider-CP49rjzt.mjs} +1 -1
- package/dist/{LocalStateProvider-Dp0KkRcw.mjs.map → LocalStateProvider-CP49rjzt.mjs.map} +1 -1
- package/dist/{LocalStateProvider-CLifRC0Y.cjs → LocalStateProvider-CalzNfNA.cjs} +1 -1
- package/dist/{LocalStateProvider-CLifRC0Y.cjs.map → LocalStateProvider-CalzNfNA.cjs.map} +1 -1
- package/dist/{Route53Provider-owQQ4pn6.cjs → Route53Provider-DwgXiLxt.cjs} +1 -1
- package/dist/{Route53Provider-owQQ4pn6.cjs.map → Route53Provider-DwgXiLxt.cjs.map} +1 -1
- package/dist/{Route53Provider-QoPgcXxn.mjs → Route53Provider-tlgBzrRQ.mjs} +1 -1
- package/dist/{Route53Provider-QoPgcXxn.mjs.map → Route53Provider-tlgBzrRQ.mjs.map} +1 -1
- package/dist/{SSMStateProvider-CksOTB8M.mjs → SSMStateProvider-BlrTiV5m.mjs} +1 -1
- package/dist/{SSMStateProvider-CksOTB8M.mjs.map → SSMStateProvider-BlrTiV5m.mjs.map} +1 -1
- package/dist/{SSMStateProvider-CT8tjl9o.cjs → SSMStateProvider-Bt6r8CTL.cjs} +1 -1
- package/dist/{SSMStateProvider-CT8tjl9o.cjs.map → SSMStateProvider-Bt6r8CTL.cjs.map} +1 -1
- package/dist/{backup-provisioner-C4noe75O.cjs → backup-provisioner-B3HBN-2k.cjs} +1 -1
- package/dist/{backup-provisioner-C4noe75O.cjs.map → backup-provisioner-B3HBN-2k.cjs.map} +1 -1
- package/dist/{backup-provisioner-BEXoHTuC.mjs → backup-provisioner-Xunq-p_8.mjs} +1 -1
- package/dist/{backup-provisioner-BEXoHTuC.mjs.map → backup-provisioner-Xunq-p_8.mjs.map} +1 -1
- package/dist/{bundler-c6QvGUBM.mjs → bundler-DkA4qzUD.mjs} +2 -2
- package/dist/{bundler-c6QvGUBM.mjs.map → bundler-DkA4qzUD.mjs.map} +1 -1
- package/dist/{bundler-BG6P249p.cjs → bundler-Dufq3uvI.cjs} +2 -2
- package/dist/{bundler-BG6P249p.cjs.map → bundler-Dufq3uvI.cjs.map} +1 -1
- package/dist/config.d.cts +2 -2
- package/dist/config.d.mts +2 -2
- package/dist/dokploy-api-BPYkG-ZZ.cjs +3 -0
- package/dist/dokploy-api-BiSzWM4v.mjs +3 -0
- package/dist/{dokploy-api-Dvyq-LsM.cjs → dokploy-api-CqOIKHme.cjs} +1 -1
- package/dist/{dokploy-api-Dvyq-LsM.cjs.map → dokploy-api-CqOIKHme.cjs.map} +1 -1
- package/dist/{dokploy-api-BIT0683o.mjs → dokploy-api-sfZFlWuE.mjs} +1 -1
- package/dist/{dokploy-api-BIT0683o.mjs.map → dokploy-api-sfZFlWuE.mjs.map} +1 -1
- package/dist/{encryption-CipsS0KW.cjs → encryption-BVS7Vmzs.cjs} +1 -1
- package/dist/encryption-Bd_m4q3g.mjs +3 -0
- package/dist/{encryption-DSkIlEZ6.mjs → encryption-CO5Q4Bhf.mjs} +1 -1
- package/dist/{encryption-DSkIlEZ6.mjs.map → encryption-CO5Q4Bhf.mjs.map} +1 -1
- package/dist/{encryption-DgKS-Dv9.cjs → encryption-MfXAiFTm.cjs} +1 -1
- package/dist/{encryption-DgKS-Dv9.cjs.map → encryption-MfXAiFTm.cjs.map} +1 -1
- package/dist/{index-BBvGMG_A.d.mts → index-Bw548Bta.d.mts} +2 -2
- package/dist/{index-Dz2a7xQU.d.cts.map → index-Bw548Bta.d.mts.map} +1 -1
- package/dist/{index-Dz2a7xQU.d.cts → index-DTpkeFb8.d.cts} +2 -2
- package/dist/{index-BBvGMG_A.d.mts.map → index-DTpkeFb8.d.cts.map} +1 -1
- package/dist/index.cjs +863 -1330
- package/dist/index.cjs.map +1 -1
- package/dist/index.mjs +990 -1457
- package/dist/index.mjs.map +1 -1
- package/dist/{openapi-DHp49UJV.cjs → openapi-BzNOC7_i.cjs} +14 -2
- package/dist/openapi-BzNOC7_i.cjs.map +1 -0
- package/dist/{openapi-DF9GyhyZ.mjs → openapi-Cfwi0fqF.mjs} +14 -2
- package/dist/openapi-Cfwi0fqF.mjs.map +1 -0
- package/dist/{openapi-react-query-C4UdILaI.mjs → openapi-react-query-C4iIvpPf.mjs} +1 -1
- package/dist/{openapi-react-query-C4UdILaI.mjs.map → openapi-react-query-C4iIvpPf.mjs.map} +1 -1
- package/dist/{openapi-react-query-DYbBq-WJ.cjs → openapi-react-query-CYWik72n.cjs} +1 -1
- package/dist/{openapi-react-query-DYbBq-WJ.cjs.map → openapi-react-query-CYWik72n.cjs.map} +1 -1
- package/dist/openapi-react-query.cjs +1 -1
- package/dist/openapi-react-query.mjs +1 -1
- package/dist/openapi.cjs +1 -1
- package/dist/openapi.d.cts +1 -1
- package/dist/openapi.d.mts +1 -1
- package/dist/openapi.mjs +1 -1
- package/dist/{reconcile-CYGmqXvq.mjs → reconcile-B2FoJE0a.mjs} +1 -1
- package/dist/{reconcile-CYGmqXvq.mjs.map → reconcile-B2FoJE0a.mjs.map} +1 -1
- package/dist/{reconcile-D93YG7G5.cjs → reconcile-BzqKJ2Rt.cjs} +1 -1
- package/dist/{reconcile-D93YG7G5.cjs.map → reconcile-BzqKJ2Rt.cjs.map} +1 -1
- package/dist/setup-DDXUXzzl.mjs +765 -0
- package/dist/setup-DDXUXzzl.mjs.map +1 -0
- package/dist/setup-DLwhfyt-.cjs +8 -0
- package/dist/setup-Dh-B7YTX.mjs +8 -0
- package/dist/setup-WZ-xqAQO.cjs +849 -0
- package/dist/setup-WZ-xqAQO.cjs.map +1 -0
- package/dist/{sync-COnAugP-.mjs → sync-B5UHXwRM.mjs} +1 -1
- package/dist/sync-BLA94rgC.cjs +4 -0
- package/dist/{sync-D_NowTkZ.mjs → sync-CSooqRQ8.mjs} +1 -1
- package/dist/{sync-D_NowTkZ.mjs.map → sync-CSooqRQ8.mjs.map} +1 -1
- package/dist/{sync-DGXXSk2v.cjs → sync-o_CtEbvR.cjs} +1 -1
- package/dist/{sync-DGXXSk2v.cjs.map → sync-o_CtEbvR.cjs.map} +1 -1
- package/dist/{types-DdHfUbxk.d.cts → types-B3GQUg3k.d.mts} +3 -1
- package/dist/{types-DdHfUbxk.d.cts.map → types-B3GQUg3k.d.mts.map} +1 -1
- package/dist/{types-OszPdw9m.d.mts → types-BWQDiooe.d.cts} +3 -1
- package/dist/{types-OszPdw9m.d.mts.map → types-BWQDiooe.d.cts.map} +1 -1
- package/dist/workspace/index.d.cts +2 -2
- package/dist/workspace/index.d.mts +2 -2
- package/package.json +6 -5
- package/src/build/__tests__/index-new.spec.ts +3 -1
- package/src/build/__tests__/manifests.spec.ts +2 -2
- package/src/build/index.ts +61 -21
- package/src/build/manifests.ts +62 -5
- package/src/dev/index.ts +25 -11
- package/src/generators/EndpointGenerator.ts +14 -2
- package/src/generators/QueueGenerator.ts +245 -0
- package/src/generators/SubscriberGenerator.ts +5 -0
- package/src/generators/TopicGenerator.ts +45 -0
- package/src/generators/__tests__/QueueGenerator.spec.ts +141 -0
- package/src/generators/index.ts +2 -0
- package/src/index.ts +4 -0
- package/src/init/versions.ts +6 -6
- package/src/setup/__tests__/auto-setup.spec.ts +147 -0
- package/src/setup/index.ts +46 -6
- package/src/test/index.ts +17 -0
- package/src/types.ts +14 -51
- package/dist/CachedStateProvider-CC0DyO09.mjs +0 -3
- package/dist/dokploy-api-BOx71j6F.cjs +0 -3
- package/dist/dokploy-api-BZGk-lkD.mjs +0 -3
- package/dist/encryption-Db2zvGqM.mjs +0 -3
- package/dist/openapi-DF9GyhyZ.mjs.map +0 -1
- package/dist/openapi-DHp49UJV.cjs.map +0 -1
- package/dist/sync-D1Pa30oV.cjs +0 -4
|
@@ -0,0 +1,849 @@
|
|
|
1
|
+
const require_chunk = require('./chunk-CUT6urMc.cjs');
|
|
2
|
+
const require_workspace = require('./workspace-CQTT7HJQ.cjs');
|
|
3
|
+
const require_config = require('./config-DujV8DbR.cjs');
|
|
4
|
+
const require_fullstack_secrets = require('./fullstack-secrets-BYzi6Hwl.cjs');
|
|
5
|
+
const require_storage = require('./storage-B1wvztiJ.cjs');
|
|
6
|
+
const require_sync = require('./sync-o_CtEbvR.cjs');
|
|
7
|
+
const node_fs = require_chunk.__toESM(require("node:fs"));
|
|
8
|
+
const node_path = require_chunk.__toESM(require("node:path"));
|
|
9
|
+
const node_fs_promises = require_chunk.__toESM(require("node:fs/promises"));
|
|
10
|
+
const node_child_process = require_chunk.__toESM(require("node:child_process"));
|
|
11
|
+
const node_net = require_chunk.__toESM(require("node:net"));
|
|
12
|
+
const dotenv = require_chunk.__toESM(require("dotenv"));
|
|
13
|
+
const yaml = require_chunk.__toESM(require("yaml"));
|
|
14
|
+
const prompts = require_chunk.__toESM(require("prompts"));
|
|
15
|
+
|
|
16
|
+
//#region src/credentials/index.ts
|
|
17
|
+
const logger$1 = console;
|
|
18
|
+
/**
|
|
19
|
+
* Load environment files
|
|
20
|
+
* @internal Exported for testing
|
|
21
|
+
*/
|
|
22
|
+
function loadEnvFiles(envConfig, cwd = process.cwd()) {
|
|
23
|
+
const loaded = [];
|
|
24
|
+
const missing = [];
|
|
25
|
+
const envFiles = envConfig ? Array.isArray(envConfig) ? envConfig : [envConfig] : [".env"];
|
|
26
|
+
for (const envFile of envFiles) {
|
|
27
|
+
const envPath = (0, node_path.resolve)(cwd, envFile);
|
|
28
|
+
if ((0, node_fs.existsSync)(envPath)) {
|
|
29
|
+
(0, dotenv.config)({
|
|
30
|
+
path: envPath,
|
|
31
|
+
override: true,
|
|
32
|
+
quiet: true
|
|
33
|
+
});
|
|
34
|
+
loaded.push(envFile);
|
|
35
|
+
} else if (envConfig) missing.push(envFile);
|
|
36
|
+
}
|
|
37
|
+
return {
|
|
38
|
+
loaded,
|
|
39
|
+
missing
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Check if a port is available
|
|
44
|
+
* @internal Exported for testing
|
|
45
|
+
*/
|
|
46
|
+
async function isPortAvailable(port) {
|
|
47
|
+
return new Promise((resolve$1) => {
|
|
48
|
+
const server = (0, node_net.createServer)();
|
|
49
|
+
server.once("error", (err) => {
|
|
50
|
+
if (err.code === "EADDRINUSE") resolve$1(false);
|
|
51
|
+
else resolve$1(false);
|
|
52
|
+
});
|
|
53
|
+
server.once("listening", () => {
|
|
54
|
+
server.close();
|
|
55
|
+
resolve$1(true);
|
|
56
|
+
});
|
|
57
|
+
server.listen(port);
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* Find an available port starting from the preferred port
|
|
62
|
+
* @internal Exported for testing
|
|
63
|
+
*/
|
|
64
|
+
async function findAvailablePort(preferredPort, maxAttempts = 10) {
|
|
65
|
+
for (let i = 0; i < maxAttempts; i++) {
|
|
66
|
+
const port = preferredPort + i;
|
|
67
|
+
if (await isPortAvailable(port)) return port;
|
|
68
|
+
logger$1.log(`⚠️ Port ${port} is in use, trying ${port + 1}...`);
|
|
69
|
+
}
|
|
70
|
+
throw new Error(`Could not find an available port after trying ${maxAttempts} ports starting from ${preferredPort}`);
|
|
71
|
+
}
|
|
72
|
+
const PORT_STATE_PATH = ".gkm/ports.json";
|
|
73
|
+
/**
|
|
74
|
+
* Parse docker-compose.yml and extract all port mappings that use env var interpolation.
|
|
75
|
+
* Entries like `'${POSTGRES_HOST_PORT:-5432}:5432'` are captured.
|
|
76
|
+
* Fixed port mappings like `'5050:80'` are skipped.
|
|
77
|
+
* @internal Exported for testing
|
|
78
|
+
*/
|
|
79
|
+
function parseComposePortMappings(composePath) {
|
|
80
|
+
if (!(0, node_fs.existsSync)(composePath)) return [];
|
|
81
|
+
const content = (0, node_fs.readFileSync)(composePath, "utf-8");
|
|
82
|
+
const compose = (0, yaml.parse)(content);
|
|
83
|
+
if (!compose?.services) return [];
|
|
84
|
+
const results = [];
|
|
85
|
+
for (const [serviceName, serviceConfig] of Object.entries(compose.services)) for (const portMapping of serviceConfig?.ports ?? []) {
|
|
86
|
+
const match = String(portMapping).match(/\$\{(\w+):-(\d+)\}:(\d+)/);
|
|
87
|
+
if (match?.[1] && match[2] && match[3]) results.push({
|
|
88
|
+
service: serviceName,
|
|
89
|
+
envVar: match[1],
|
|
90
|
+
defaultPort: Number(match[2]),
|
|
91
|
+
containerPort: Number(match[3])
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
return results;
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* Load saved port state from .gkm/ports.json.
|
|
98
|
+
* @internal Exported for testing
|
|
99
|
+
*/
|
|
100
|
+
async function loadPortState(workspaceRoot) {
|
|
101
|
+
try {
|
|
102
|
+
const raw = await (0, node_fs_promises.readFile)((0, node_path.join)(workspaceRoot, PORT_STATE_PATH), "utf-8");
|
|
103
|
+
return JSON.parse(raw);
|
|
104
|
+
} catch {
|
|
105
|
+
return {};
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
/**
|
|
109
|
+
* Save port state to .gkm/ports.json.
|
|
110
|
+
* @internal Exported for testing
|
|
111
|
+
*/
|
|
112
|
+
async function savePortState(workspaceRoot, ports) {
|
|
113
|
+
const dir = (0, node_path.join)(workspaceRoot, ".gkm");
|
|
114
|
+
await (0, node_fs_promises.mkdir)(dir, { recursive: true });
|
|
115
|
+
await (0, node_fs_promises.writeFile)((0, node_path.join)(workspaceRoot, PORT_STATE_PATH), `${JSON.stringify(ports, null, 2)}\n`);
|
|
116
|
+
}
|
|
117
|
+
/**
|
|
118
|
+
* Check if a project's own Docker container is running and return its host port.
|
|
119
|
+
* Uses `docker compose port` scoped to the project's compose file.
|
|
120
|
+
* @internal Exported for testing
|
|
121
|
+
*/
|
|
122
|
+
function getContainerHostPort(workspaceRoot, service, containerPort) {
|
|
123
|
+
try {
|
|
124
|
+
const result = (0, node_child_process.execSync)(`docker compose port ${service} ${containerPort}`, {
|
|
125
|
+
cwd: workspaceRoot,
|
|
126
|
+
stdio: "pipe"
|
|
127
|
+
}).toString().trim();
|
|
128
|
+
const match = result.match(/:(\d+)$/);
|
|
129
|
+
return match ? Number(match[1]) : null;
|
|
130
|
+
} catch {
|
|
131
|
+
return null;
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
/**
|
|
135
|
+
* Resolve host ports for Docker services by parsing docker-compose.yml.
|
|
136
|
+
* Priority: running container → saved state → find available port.
|
|
137
|
+
* Persists resolved ports to .gkm/ports.json.
|
|
138
|
+
* @internal Exported for testing
|
|
139
|
+
*/
|
|
140
|
+
async function resolveServicePorts(workspaceRoot) {
|
|
141
|
+
const composePath = (0, node_path.join)(workspaceRoot, "docker-compose.yml");
|
|
142
|
+
const mappings = parseComposePortMappings(composePath);
|
|
143
|
+
if (mappings.length === 0) return {
|
|
144
|
+
dockerEnv: {},
|
|
145
|
+
ports: {},
|
|
146
|
+
mappings: []
|
|
147
|
+
};
|
|
148
|
+
const savedState = await loadPortState(workspaceRoot);
|
|
149
|
+
const dockerEnv = {};
|
|
150
|
+
const ports = {};
|
|
151
|
+
const assignedPorts = /* @__PURE__ */ new Set();
|
|
152
|
+
logger$1.log("\n🔌 Resolving service ports...");
|
|
153
|
+
for (const mapping of mappings) {
|
|
154
|
+
const containerPort = getContainerHostPort(workspaceRoot, mapping.service, mapping.containerPort);
|
|
155
|
+
if (containerPort !== null) {
|
|
156
|
+
ports[mapping.envVar] = containerPort;
|
|
157
|
+
dockerEnv[mapping.envVar] = String(containerPort);
|
|
158
|
+
assignedPorts.add(containerPort);
|
|
159
|
+
logger$1.log(` 🔄 ${mapping.service}:${mapping.containerPort}: reusing existing container on port ${containerPort}`);
|
|
160
|
+
continue;
|
|
161
|
+
}
|
|
162
|
+
const savedPort = savedState[mapping.envVar];
|
|
163
|
+
if (savedPort && !assignedPorts.has(savedPort) && await isPortAvailable(savedPort)) {
|
|
164
|
+
ports[mapping.envVar] = savedPort;
|
|
165
|
+
dockerEnv[mapping.envVar] = String(savedPort);
|
|
166
|
+
assignedPorts.add(savedPort);
|
|
167
|
+
logger$1.log(` 💾 ${mapping.service}:${mapping.containerPort}: using saved port ${savedPort}`);
|
|
168
|
+
continue;
|
|
169
|
+
}
|
|
170
|
+
let resolvedPort = await findAvailablePort(mapping.defaultPort);
|
|
171
|
+
while (assignedPorts.has(resolvedPort)) resolvedPort = await findAvailablePort(resolvedPort + 1);
|
|
172
|
+
ports[mapping.envVar] = resolvedPort;
|
|
173
|
+
dockerEnv[mapping.envVar] = String(resolvedPort);
|
|
174
|
+
assignedPorts.add(resolvedPort);
|
|
175
|
+
if (resolvedPort !== mapping.defaultPort) logger$1.log(` ⚡ ${mapping.service}:${mapping.containerPort}: port ${mapping.defaultPort} occupied, using port ${resolvedPort}`);
|
|
176
|
+
else logger$1.log(` ✅ ${mapping.service}:${mapping.containerPort}: using default port ${resolvedPort}`);
|
|
177
|
+
}
|
|
178
|
+
await savePortState(workspaceRoot, ports);
|
|
179
|
+
return {
|
|
180
|
+
dockerEnv,
|
|
181
|
+
ports,
|
|
182
|
+
mappings
|
|
183
|
+
};
|
|
184
|
+
}
|
|
185
|
+
/**
|
|
186
|
+
* Replace a port in a URL string.
|
|
187
|
+
* Handles both `hostname:port` and `localhost:port` patterns.
|
|
188
|
+
* @internal Exported for testing
|
|
189
|
+
*/
|
|
190
|
+
function replacePortInUrl(url, oldPort, newPort) {
|
|
191
|
+
if (oldPort === newPort) return url;
|
|
192
|
+
let result = url.replace(new RegExp(`:${oldPort}(?=[/?#]|$)`, "g"), `:${newPort}`);
|
|
193
|
+
result = result.replace(new RegExp(`%3A${oldPort}(?=[%/?#&]|$)`, "gi"), `%3A${newPort}`);
|
|
194
|
+
return result;
|
|
195
|
+
}
|
|
196
|
+
/**
|
|
197
|
+
* Rewrite connection URLs and port vars in secrets with resolved ports.
|
|
198
|
+
* Uses the parsed compose mappings to determine which default ports to replace.
|
|
199
|
+
* Pure transform — does not modify secrets on disk.
|
|
200
|
+
* @internal Exported for testing
|
|
201
|
+
*/
|
|
202
|
+
function rewriteUrlsWithPorts(secrets, resolvedPorts) {
|
|
203
|
+
const { ports, mappings } = resolvedPorts;
|
|
204
|
+
const result = { ...secrets };
|
|
205
|
+
const portReplacements = [];
|
|
206
|
+
const serviceNames = /* @__PURE__ */ new Set();
|
|
207
|
+
for (const mapping of mappings) {
|
|
208
|
+
serviceNames.add(mapping.service);
|
|
209
|
+
const resolved = ports[mapping.envVar];
|
|
210
|
+
if (resolved !== void 0) portReplacements.push({
|
|
211
|
+
defaultPort: mapping.defaultPort,
|
|
212
|
+
resolvedPort: resolved
|
|
213
|
+
});
|
|
214
|
+
}
|
|
215
|
+
for (const [key, value] of Object.entries(result)) {
|
|
216
|
+
if (!key.endsWith("_HOST")) continue;
|
|
217
|
+
if (serviceNames.has(value)) result[key] = "localhost";
|
|
218
|
+
}
|
|
219
|
+
for (const [key, value] of Object.entries(result)) {
|
|
220
|
+
if (!key.endsWith("_PORT")) continue;
|
|
221
|
+
for (const { defaultPort, resolvedPort } of portReplacements) if (value === String(defaultPort)) result[key] = String(resolvedPort);
|
|
222
|
+
}
|
|
223
|
+
for (const [key, value] of Object.entries(result)) {
|
|
224
|
+
if (!key.endsWith("_URL") && !key.endsWith("_ENDPOINT") && !key.endsWith("_CONNECTION_STRING") && key !== "DATABASE_URL") continue;
|
|
225
|
+
let rewritten = value;
|
|
226
|
+
for (const name of serviceNames) rewritten = rewritten.replace(new RegExp(`@${name}:`, "g"), "@localhost:");
|
|
227
|
+
for (const { defaultPort, resolvedPort } of portReplacements) rewritten = replacePortInUrl(rewritten, defaultPort, resolvedPort);
|
|
228
|
+
result[key] = rewritten;
|
|
229
|
+
}
|
|
230
|
+
return result;
|
|
231
|
+
}
|
|
232
|
+
/**
|
|
233
|
+
* Build the environment variables to pass to `docker compose up`.
|
|
234
|
+
* Merges process.env, secrets, and port mappings so that Docker Compose
|
|
235
|
+
* can interpolate variables like ${POSTGRES_USER} correctly.
|
|
236
|
+
* @internal Exported for testing
|
|
237
|
+
*/
|
|
238
|
+
function buildDockerComposeEnv(secretsEnv, portEnv) {
|
|
239
|
+
return {
|
|
240
|
+
...process.env,
|
|
241
|
+
...secretsEnv,
|
|
242
|
+
...portEnv
|
|
243
|
+
};
|
|
244
|
+
}
|
|
245
|
+
/**
|
|
246
|
+
* Parse all service names from a docker-compose.yml file.
|
|
247
|
+
* @internal Exported for testing
|
|
248
|
+
*/
|
|
249
|
+
function parseComposeServiceNames(composePath) {
|
|
250
|
+
if (!(0, node_fs.existsSync)(composePath)) return [];
|
|
251
|
+
const content = (0, node_fs.readFileSync)(composePath, "utf-8");
|
|
252
|
+
const compose = (0, yaml.parse)(content);
|
|
253
|
+
return Object.keys(compose?.services ?? {});
|
|
254
|
+
}
|
|
255
|
+
/**
|
|
256
|
+
* Start docker-compose services for a single-app project (no workspace config).
|
|
257
|
+
* Starts all services defined in docker-compose.yml.
|
|
258
|
+
*/
|
|
259
|
+
async function startComposeServices(cwd, portEnv, secretsEnv) {
|
|
260
|
+
const composeFile = (0, node_path.join)(cwd, "docker-compose.yml");
|
|
261
|
+
if (!(0, node_fs.existsSync)(composeFile)) return;
|
|
262
|
+
const servicesToStart = parseComposeServiceNames(composeFile);
|
|
263
|
+
if (servicesToStart.length === 0) return;
|
|
264
|
+
logger$1.log(`🐳 Starting services: ${servicesToStart.join(", ")}`);
|
|
265
|
+
try {
|
|
266
|
+
(0, node_child_process.execSync)(`docker compose up -d ${servicesToStart.join(" ")}`, {
|
|
267
|
+
cwd,
|
|
268
|
+
stdio: "inherit",
|
|
269
|
+
env: buildDockerComposeEnv(secretsEnv, portEnv)
|
|
270
|
+
});
|
|
271
|
+
logger$1.log("✅ Services started");
|
|
272
|
+
} catch (error) {
|
|
273
|
+
logger$1.error("❌ Failed to start services:", error.message);
|
|
274
|
+
throw error;
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
/**
|
|
278
|
+
* Start docker-compose services for a workspace.
|
|
279
|
+
* Discovers all services from docker-compose.yml and starts everything
|
|
280
|
+
* except app services (which are managed by turbo).
|
|
281
|
+
* @internal Exported for testing
|
|
282
|
+
*/
|
|
283
|
+
async function startWorkspaceServices(workspace, portEnv, secretsEnv) {
|
|
284
|
+
const composeFile = (0, node_path.join)(workspace.root, "docker-compose.yml");
|
|
285
|
+
if (!(0, node_fs.existsSync)(composeFile)) return;
|
|
286
|
+
const allServices = parseComposeServiceNames(composeFile);
|
|
287
|
+
const appNames = new Set(Object.keys(workspace.apps));
|
|
288
|
+
const servicesToStart = allServices.filter((name) => !appNames.has(name));
|
|
289
|
+
if (servicesToStart.length === 0) return;
|
|
290
|
+
logger$1.log(`🐳 Starting services: ${servicesToStart.join(", ")}`);
|
|
291
|
+
try {
|
|
292
|
+
(0, node_child_process.execSync)(`docker compose up -d ${servicesToStart.join(" ")}`, {
|
|
293
|
+
cwd: workspace.root,
|
|
294
|
+
stdio: "inherit",
|
|
295
|
+
env: buildDockerComposeEnv(secretsEnv, portEnv)
|
|
296
|
+
});
|
|
297
|
+
logger$1.log("✅ Services started");
|
|
298
|
+
} catch (error) {
|
|
299
|
+
logger$1.error("❌ Failed to start services:", error.message);
|
|
300
|
+
throw error;
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
/**
|
|
304
|
+
* Load and flatten secrets for an app from encrypted storage.
|
|
305
|
+
* For workspace app: maps {APP}_DATABASE_URL → DATABASE_URL.
|
|
306
|
+
* @internal Exported for testing
|
|
307
|
+
*/
|
|
308
|
+
async function loadSecretsForApp(secretsRoot, appName, stages = ["dev", "development"]) {
|
|
309
|
+
let secrets = {};
|
|
310
|
+
for (const stage of stages) if (require_storage.secretsExist(stage, secretsRoot)) {
|
|
311
|
+
const stageSecrets = await require_storage.readStageSecrets(stage, secretsRoot);
|
|
312
|
+
if (stageSecrets) {
|
|
313
|
+
logger$1.log(`🔐 Loading secrets from stage: ${stage}`);
|
|
314
|
+
secrets = require_storage.toEmbeddableSecrets(stageSecrets);
|
|
315
|
+
break;
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
if (Object.keys(secrets).length === 0) return {};
|
|
319
|
+
if (!appName) return secrets;
|
|
320
|
+
const prefix = appName.toUpperCase();
|
|
321
|
+
const mapped = { ...secrets };
|
|
322
|
+
const appDbUrl = secrets[`${prefix}_DATABASE_URL`];
|
|
323
|
+
if (appDbUrl) mapped.DATABASE_URL = appDbUrl;
|
|
324
|
+
return mapped;
|
|
325
|
+
}
|
|
326
|
+
/**
|
|
327
|
+
* Walk up the directory tree to find the root containing .gkm/secrets/.
|
|
328
|
+
* @internal Exported for testing
|
|
329
|
+
*/
|
|
330
|
+
function findSecretsRoot(startDir) {
|
|
331
|
+
let dir = startDir;
|
|
332
|
+
while (dir !== "/") {
|
|
333
|
+
if ((0, node_fs.existsSync)((0, node_path.join)(dir, ".gkm", "secrets"))) return dir;
|
|
334
|
+
const parent = (0, node_path.dirname)(dir);
|
|
335
|
+
if (parent === dir) break;
|
|
336
|
+
dir = parent;
|
|
337
|
+
}
|
|
338
|
+
return startDir;
|
|
339
|
+
}
|
|
340
|
+
/**
|
|
341
|
+
* Generate the credentials injection code snippet.
|
|
342
|
+
* This is the common logic used by both entry wrapper and exec preload.
|
|
343
|
+
* @internal
|
|
344
|
+
*/
|
|
345
|
+
function generateCredentialsInjection(secretsJsonPath) {
|
|
346
|
+
return `import { existsSync, readFileSync } from 'node:fs';
|
|
347
|
+
|
|
348
|
+
// Inject dev secrets via globalThis and process.env
|
|
349
|
+
// Using globalThis.__gkm_credentials__ avoids CJS/ESM interop issues where
|
|
350
|
+
// Object.assign on the Credentials export only mutates one module copy.
|
|
351
|
+
const secretsPath = '${secretsJsonPath}';
|
|
352
|
+
if (existsSync(secretsPath)) {
|
|
353
|
+
const secrets = JSON.parse(readFileSync(secretsPath, 'utf-8'));
|
|
354
|
+
globalThis.__gkm_credentials__ = secrets;
|
|
355
|
+
Object.assign(process.env, secrets);
|
|
356
|
+
}
|
|
357
|
+
`;
|
|
358
|
+
}
|
|
359
|
+
/**
|
|
360
|
+
* Create a preload script that injects secrets into Credentials.
|
|
361
|
+
* Used by `gkm exec` to inject secrets before running any command.
|
|
362
|
+
* @internal Exported for testing
|
|
363
|
+
*/
|
|
364
|
+
async function createCredentialsPreload(preloadPath, secretsJsonPath) {
|
|
365
|
+
const content = `/**
|
|
366
|
+
* Credentials preload generated by 'gkm exec'
|
|
367
|
+
* This file is loaded via NODE_OPTIONS="--import <path>"
|
|
368
|
+
*/
|
|
369
|
+
${generateCredentialsInjection(secretsJsonPath)}`;
|
|
370
|
+
await (0, node_fs_promises.writeFile)(preloadPath, content);
|
|
371
|
+
}
|
|
372
|
+
/**
|
|
373
|
+
* Create a wrapper script that injects secrets before importing the entry file.
|
|
374
|
+
* @internal Exported for testing
|
|
375
|
+
*/
|
|
376
|
+
async function createEntryWrapper(wrapperPath, entryPath, secretsJsonPath) {
|
|
377
|
+
const credentialsInjection = secretsJsonPath ? `${generateCredentialsInjection(secretsJsonPath)}
|
|
378
|
+
` : "";
|
|
379
|
+
const content = `#!/usr/bin/env node
|
|
380
|
+
/**
|
|
381
|
+
* Entry wrapper generated by 'gkm dev --entry'
|
|
382
|
+
*/
|
|
383
|
+
${credentialsInjection}// Import and run the user's entry file (dynamic import ensures secrets load first)
|
|
384
|
+
await import('${entryPath}');
|
|
385
|
+
`;
|
|
386
|
+
await (0, node_fs_promises.writeFile)(wrapperPath, content);
|
|
387
|
+
}
|
|
388
|
+
/**
|
|
389
|
+
* Prepare credentials for dev/exec/test modes.
|
|
390
|
+
* Loads workspace config, secrets, resolves Docker ports, rewrites URLs,
|
|
391
|
+
* injects PORT, dependency URLs, and writes credentials JSON.
|
|
392
|
+
*
|
|
393
|
+
* @param options.resolveDockerPorts - How to resolve Docker ports:
|
|
394
|
+
* - `'full'` (default): probe running containers, saved state, then find available ports. Used by dev/test.
|
|
395
|
+
* - `'readonly'`: check running containers and saved state only, never probe for new ports. Used by exec.
|
|
396
|
+
* @param options.stages - Secret stages to try, in order. Default: ['dev', 'development'].
|
|
397
|
+
* @param options.startDocker - Start Docker Compose services after port resolution. Default: false.
|
|
398
|
+
* @param options.secretsFileName - Custom secrets JSON filename. Default: 'dev-secrets-{appName}.json' or 'dev-secrets.json'.
|
|
399
|
+
* @internal Exported for testing
|
|
400
|
+
*/
|
|
401
|
+
async function prepareEntryCredentials(options) {
|
|
402
|
+
const cwd = options.cwd ?? process.cwd();
|
|
403
|
+
const portMode = options.resolveDockerPorts ?? "full";
|
|
404
|
+
let workspaceAppPort;
|
|
405
|
+
let secretsRoot = cwd;
|
|
406
|
+
let appName;
|
|
407
|
+
let appInfo;
|
|
408
|
+
try {
|
|
409
|
+
appInfo = await require_config.loadWorkspaceAppInfo(cwd);
|
|
410
|
+
workspaceAppPort = appInfo.app.port;
|
|
411
|
+
secretsRoot = appInfo.workspaceRoot;
|
|
412
|
+
appName = appInfo.appName;
|
|
413
|
+
} catch {
|
|
414
|
+
secretsRoot = findSecretsRoot(cwd);
|
|
415
|
+
appName = require_config.getAppNameFromCwd(cwd) ?? void 0;
|
|
416
|
+
}
|
|
417
|
+
const resolvedPort = options.explicitPort ?? workspaceAppPort ?? 3e3;
|
|
418
|
+
const credentials = await loadSecretsForApp(secretsRoot, appName, options.stages);
|
|
419
|
+
credentials.PORT = String(resolvedPort);
|
|
420
|
+
const composePath = (0, node_path.join)(secretsRoot, "docker-compose.yml");
|
|
421
|
+
const mappings = parseComposePortMappings(composePath);
|
|
422
|
+
if (mappings.length > 0) {
|
|
423
|
+
let resolvedPorts;
|
|
424
|
+
if (portMode === "full") resolvedPorts = await resolveServicePorts(secretsRoot);
|
|
425
|
+
else {
|
|
426
|
+
const savedPorts = await loadPortState(secretsRoot);
|
|
427
|
+
const ports = {};
|
|
428
|
+
for (const mapping of mappings) {
|
|
429
|
+
const containerPort = getContainerHostPort(secretsRoot, mapping.service, mapping.containerPort);
|
|
430
|
+
if (containerPort !== null) ports[mapping.envVar] = containerPort;
|
|
431
|
+
else {
|
|
432
|
+
const saved = savedPorts[mapping.envVar];
|
|
433
|
+
if (saved !== void 0) ports[mapping.envVar] = saved;
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
resolvedPorts = {
|
|
437
|
+
dockerEnv: {},
|
|
438
|
+
ports,
|
|
439
|
+
mappings
|
|
440
|
+
};
|
|
441
|
+
}
|
|
442
|
+
if (options.startDocker) if (appInfo) await startWorkspaceServices(appInfo.workspace, resolvedPorts.dockerEnv, credentials);
|
|
443
|
+
else await startComposeServices(secretsRoot, resolvedPorts.dockerEnv, credentials);
|
|
444
|
+
if (Object.keys(resolvedPorts.ports).length > 0) {
|
|
445
|
+
const rewritten = rewriteUrlsWithPorts(credentials, resolvedPorts);
|
|
446
|
+
Object.assign(credentials, rewritten);
|
|
447
|
+
logger$1.log(`🔌 Applied ${Object.keys(resolvedPorts.ports).length} port mapping(s)`);
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
if (appInfo?.appName) {
|
|
451
|
+
const depEnv = require_workspace.getDependencyEnvVars(appInfo.workspace, appInfo.appName);
|
|
452
|
+
Object.assign(credentials, depEnv);
|
|
453
|
+
}
|
|
454
|
+
if (!credentials.EVENT_PUBLISHER_CONNECTION_STRING && !credentials.EVENT_SUBSCRIBER_CONNECTION_STRING && credentials.PGBOSS_DB_USER) {
|
|
455
|
+
const pgbossUrl = require_fullstack_secrets.generatePgBossUrl({
|
|
456
|
+
username: credentials.PGBOSS_DB_USER,
|
|
457
|
+
password: credentials.PGBOSS_DB_PASSWORD ?? "",
|
|
458
|
+
host: credentials.POSTGRES_HOST ?? "localhost",
|
|
459
|
+
port: Number(credentials.POSTGRES_PORT ?? "5432"),
|
|
460
|
+
database: credentials.POSTGRES_DB ?? "app"
|
|
461
|
+
});
|
|
462
|
+
credentials.EVENT_PUBLISHER_CONNECTION_STRING = pgbossUrl;
|
|
463
|
+
credentials.EVENT_SUBSCRIBER_CONNECTION_STRING = pgbossUrl;
|
|
464
|
+
}
|
|
465
|
+
const secretsDir = (0, node_path.join)(secretsRoot, ".gkm");
|
|
466
|
+
await (0, node_fs_promises.mkdir)(secretsDir, { recursive: true });
|
|
467
|
+
const secretsFileName = options.secretsFileName ?? (appName ? `dev-secrets-${appName}.json` : "dev-secrets.json");
|
|
468
|
+
const secretsJsonPath = (0, node_path.join)(secretsDir, secretsFileName);
|
|
469
|
+
await (0, node_fs_promises.writeFile)(secretsJsonPath, JSON.stringify(credentials, null, 2));
|
|
470
|
+
return {
|
|
471
|
+
credentials,
|
|
472
|
+
resolvedPort,
|
|
473
|
+
secretsJsonPath,
|
|
474
|
+
appName,
|
|
475
|
+
secretsRoot,
|
|
476
|
+
appInfo
|
|
477
|
+
};
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
//#endregion
|
|
481
|
+
//#region src/setup/index.ts
|
|
482
|
+
const logger = console;
|
|
483
|
+
/**
|
|
484
|
+
* Setup development environment.
|
|
485
|
+
*
|
|
486
|
+
* Orchestrates:
|
|
487
|
+
* 1. Load workspace config
|
|
488
|
+
* 2. Resolve secrets (local → SSM → generate fresh)
|
|
489
|
+
* 3. Write docker/.env from secrets
|
|
490
|
+
* 4. Start Docker services
|
|
491
|
+
*/
|
|
492
|
+
async function setupCommand(options = {}) {
|
|
493
|
+
const stage = options.stage ?? "development";
|
|
494
|
+
logger.log("\n🔧 Setting up development environment...\n");
|
|
495
|
+
let loadedConfig;
|
|
496
|
+
try {
|
|
497
|
+
loadedConfig = await require_config.loadWorkspaceConfig();
|
|
498
|
+
} catch {
|
|
499
|
+
logger.error("❌ No gkm.config.ts found. Run this command from a workspace root.");
|
|
500
|
+
process.exit(1);
|
|
501
|
+
}
|
|
502
|
+
const { workspace } = loadedConfig;
|
|
503
|
+
const isMultiApp = Object.keys(workspace.apps).length > 1;
|
|
504
|
+
logger.log(`📦 Workspace: ${workspace.name}`);
|
|
505
|
+
logger.log(`📱 Apps: ${Object.keys(workspace.apps).join(", ")}`);
|
|
506
|
+
logger.log(`🔑 Stage: ${stage}\n`);
|
|
507
|
+
const secrets = await resolveSecrets(stage, workspace, options);
|
|
508
|
+
if (!secrets) {
|
|
509
|
+
logger.error("❌ Failed to resolve secrets. Exiting.");
|
|
510
|
+
process.exit(1);
|
|
511
|
+
}
|
|
512
|
+
if (isMultiApp && workspace.services.db) {
|
|
513
|
+
await require_fullstack_secrets.writeDockerEnvFromSecrets(secrets, workspace.root);
|
|
514
|
+
logger.log("📄 Generated docker/.env with database passwords");
|
|
515
|
+
}
|
|
516
|
+
if (!options.skipDocker) {
|
|
517
|
+
const composeFile = (0, node_path.join)(workspace.root, "docker-compose.yml");
|
|
518
|
+
if ((0, node_fs.existsSync)(composeFile)) {
|
|
519
|
+
logger.log("");
|
|
520
|
+
const resolvedPorts = await resolveServicePorts(workspace.root);
|
|
521
|
+
await startWorkspaceServices(workspace, resolvedPorts.dockerEnv);
|
|
522
|
+
} else logger.log("⚠️ No docker-compose.yml found. Skipping Docker services.");
|
|
523
|
+
}
|
|
524
|
+
printSummary(workspace, stage);
|
|
525
|
+
}
|
|
526
|
+
/**
|
|
527
|
+
* Resolve secrets with priority:
|
|
528
|
+
* 1. Local secrets exist → use them (preserves manual additions)
|
|
529
|
+
* 2. SSM configured and has secrets → pull and use
|
|
530
|
+
* 3. Neither → generate fresh secrets
|
|
531
|
+
*
|
|
532
|
+
* --force skips checks 1 and 2 and always regenerates.
|
|
533
|
+
*/
|
|
534
|
+
async function resolveSecrets(stage, workspace, options) {
|
|
535
|
+
if (options.force) {
|
|
536
|
+
logger.log("🔐 Generating fresh secrets (--force)...");
|
|
537
|
+
return generateFreshSecrets(stage, workspace, options);
|
|
538
|
+
}
|
|
539
|
+
if (require_storage.secretsExist(stage, workspace.root)) {
|
|
540
|
+
logger.log("🔐 Using existing local secrets");
|
|
541
|
+
const secrets = await require_storage.readStageSecrets(stage, workspace.root);
|
|
542
|
+
if (secrets) {
|
|
543
|
+
const reconciled = reconcileSecrets(secrets, workspace);
|
|
544
|
+
if (reconciled) await require_storage.writeStageSecrets(reconciled, workspace.root);
|
|
545
|
+
return reconciled ?? secrets;
|
|
546
|
+
}
|
|
547
|
+
}
|
|
548
|
+
if (require_sync.isSSMConfigured(workspace)) {
|
|
549
|
+
logger.log("☁️ Checking for remote secrets in SSM...");
|
|
550
|
+
try {
|
|
551
|
+
const remoteSecrets = await require_sync.pullSecrets(stage, workspace);
|
|
552
|
+
if (remoteSecrets) {
|
|
553
|
+
logger.log("✅ Pulled secrets from SSM");
|
|
554
|
+
await require_storage.writeStageSecrets(remoteSecrets, workspace.root);
|
|
555
|
+
return remoteSecrets;
|
|
556
|
+
}
|
|
557
|
+
logger.log(" No remote secrets found");
|
|
558
|
+
} catch (error) {
|
|
559
|
+
logger.warn(`⚠️ Failed to pull from SSM: ${error.message}`);
|
|
560
|
+
}
|
|
561
|
+
}
|
|
562
|
+
logger.log("🔐 Generating fresh development secrets...");
|
|
563
|
+
return generateFreshSecrets(stage, workspace, options);
|
|
564
|
+
}
|
|
565
|
+
/**
|
|
566
|
+
* Reconcile existing secrets with expected workspace-derived keys.
|
|
567
|
+
* Adds missing keys (e.g. BETTER_AUTH_*) without overwriting existing values.
|
|
568
|
+
* Returns the updated secrets if changes were made, or null if no changes needed.
|
|
569
|
+
* @internal Exported for testing
|
|
570
|
+
*/
|
|
571
|
+
function reconcileSecrets(secrets, workspace) {
|
|
572
|
+
let changed = false;
|
|
573
|
+
let result = { ...secrets };
|
|
574
|
+
const serviceMap = [
|
|
575
|
+
{
|
|
576
|
+
key: "db",
|
|
577
|
+
name: "postgres"
|
|
578
|
+
},
|
|
579
|
+
{
|
|
580
|
+
key: "cache",
|
|
581
|
+
name: "redis"
|
|
582
|
+
},
|
|
583
|
+
{
|
|
584
|
+
key: "storage",
|
|
585
|
+
name: "minio"
|
|
586
|
+
},
|
|
587
|
+
{
|
|
588
|
+
key: "mail",
|
|
589
|
+
name: "mailpit"
|
|
590
|
+
}
|
|
591
|
+
];
|
|
592
|
+
for (const { key, name } of serviceMap) if (workspace.services[key] && !result.services[name]) {
|
|
593
|
+
const creds = require_fullstack_secrets.generateServiceCredentials(name);
|
|
594
|
+
if (name === "minio") {
|
|
595
|
+
creds.bucket = workspace.name;
|
|
596
|
+
creds.username = workspace.name;
|
|
597
|
+
}
|
|
598
|
+
result = {
|
|
599
|
+
...result,
|
|
600
|
+
services: {
|
|
601
|
+
...result.services,
|
|
602
|
+
[name]: creds
|
|
603
|
+
}
|
|
604
|
+
};
|
|
605
|
+
result.urls = require_fullstack_secrets.generateConnectionUrls(result.services, result.eventsBackend);
|
|
606
|
+
logger.log(` 🔄 Adding missing service credentials: ${name}`);
|
|
607
|
+
changed = true;
|
|
608
|
+
}
|
|
609
|
+
if (result.services.postgres && !result.services.pgboss) {
|
|
610
|
+
result = {
|
|
611
|
+
...result,
|
|
612
|
+
services: {
|
|
613
|
+
...result.services,
|
|
614
|
+
pgboss: {
|
|
615
|
+
host: result.services.postgres.host,
|
|
616
|
+
port: result.services.postgres.port,
|
|
617
|
+
username: "pgboss",
|
|
618
|
+
password: require_fullstack_secrets.generateSecurePassword(),
|
|
619
|
+
database: result.services.postgres.database ?? "app"
|
|
620
|
+
}
|
|
621
|
+
}
|
|
622
|
+
};
|
|
623
|
+
result.urls = require_fullstack_secrets.generateConnectionUrls(result.services, result.eventsBackend);
|
|
624
|
+
logger.log(" 🔄 Adding missing service credentials: pgboss");
|
|
625
|
+
changed = true;
|
|
626
|
+
}
|
|
627
|
+
const eventsBackend = workspace.services.events;
|
|
628
|
+
if (eventsBackend && result.eventsBackend !== eventsBackend) {
|
|
629
|
+
result.eventsBackend = eventsBackend;
|
|
630
|
+
if (eventsBackend === "sns" && !result.services.localstack) {
|
|
631
|
+
result = {
|
|
632
|
+
...result,
|
|
633
|
+
services: {
|
|
634
|
+
...result.services,
|
|
635
|
+
localstack: require_fullstack_secrets.generateLocalStackCredentials()
|
|
636
|
+
}
|
|
637
|
+
};
|
|
638
|
+
logger.log(" 🔄 Adding missing service credentials: localstack");
|
|
639
|
+
changed = true;
|
|
640
|
+
}
|
|
641
|
+
if (eventsBackend === "rabbitmq" && !result.services.rabbitmq) {
|
|
642
|
+
result = {
|
|
643
|
+
...result,
|
|
644
|
+
services: {
|
|
645
|
+
...result.services,
|
|
646
|
+
rabbitmq: require_fullstack_secrets.generateServiceCredentials("rabbitmq")
|
|
647
|
+
}
|
|
648
|
+
};
|
|
649
|
+
logger.log(" 🔄 Adding missing service credentials: rabbitmq");
|
|
650
|
+
changed = true;
|
|
651
|
+
}
|
|
652
|
+
result.urls = require_fullstack_secrets.generateConnectionUrls(result.services, eventsBackend);
|
|
653
|
+
changed = true;
|
|
654
|
+
}
|
|
655
|
+
const isMultiApp = Object.keys(workspace.apps).length > 1;
|
|
656
|
+
if (isMultiApp) {
|
|
657
|
+
const expected = require_fullstack_secrets.generateFullstackCustomSecrets(workspace);
|
|
658
|
+
const missing = {};
|
|
659
|
+
for (const [key, value] of Object.entries(expected)) if (!(key in result.custom)) missing[key] = value;
|
|
660
|
+
if (Object.keys(missing).length > 0) {
|
|
661
|
+
logger.log(` 🔄 Adding missing secrets: ${Object.keys(missing).join(", ")}`);
|
|
662
|
+
result = {
|
|
663
|
+
...result,
|
|
664
|
+
custom: {
|
|
665
|
+
...result.custom,
|
|
666
|
+
...missing
|
|
667
|
+
}
|
|
668
|
+
};
|
|
669
|
+
changed = true;
|
|
670
|
+
}
|
|
671
|
+
}
|
|
672
|
+
if (!changed) return null;
|
|
673
|
+
return {
|
|
674
|
+
...result,
|
|
675
|
+
updatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
676
|
+
};
|
|
677
|
+
}
|
|
678
|
+
/**
|
|
679
|
+
* Build a fresh StageSecrets object for a workspace: service credentials,
|
|
680
|
+
* connection URLs, and custom secrets. Pure — does not write to disk or touch
|
|
681
|
+
* SSM. Service passwords are freshly randomized, so the result must be the same
|
|
682
|
+
* one used to start the matching Docker containers.
|
|
683
|
+
* @internal Exported for reuse by `gkm test` auto-setup.
|
|
684
|
+
*/
|
|
685
|
+
function createFreshWorkspaceSecrets(stage, workspace) {
|
|
686
|
+
const serviceNames = [];
|
|
687
|
+
if (workspace.services.db) serviceNames.push("postgres");
|
|
688
|
+
if (workspace.services.cache) serviceNames.push("redis");
|
|
689
|
+
if (workspace.services.storage) serviceNames.push("minio");
|
|
690
|
+
if (workspace.services.mail) serviceNames.push("mailpit");
|
|
691
|
+
if (workspace.services.events === "sns") serviceNames.push("localstack");
|
|
692
|
+
if (workspace.services.events === "rabbitmq") serviceNames.push("rabbitmq");
|
|
693
|
+
const secrets = require_fullstack_secrets.createStageSecrets(stage, serviceNames, {
|
|
694
|
+
projectName: workspace.name,
|
|
695
|
+
eventsBackend: workspace.services.events
|
|
696
|
+
});
|
|
697
|
+
const isMultiApp = Object.keys(workspace.apps).length > 1;
|
|
698
|
+
if (isMultiApp) secrets.custom = require_fullstack_secrets.generateFullstackCustomSecrets(workspace);
|
|
699
|
+
else secrets.custom = {
|
|
700
|
+
NODE_ENV: "development",
|
|
701
|
+
PORT: "3000",
|
|
702
|
+
LOG_LEVEL: "debug",
|
|
703
|
+
JWT_SECRET: `dev-${Date.now()}-${Math.random().toString(36).slice(2)}`
|
|
704
|
+
};
|
|
705
|
+
return secrets;
|
|
706
|
+
}
|
|
707
|
+
/**
|
|
708
|
+
* Ensure a usable secret set exists for a stage, generating a fresh one from the
|
|
709
|
+
* workspace config when none is present. No-op if secrets already exist.
|
|
710
|
+
*
|
|
711
|
+
* Powers `gkm test --auto-setup` / `GKM_AUTO_SETUP`: CI can run without a
|
|
712
|
+
* committed secrets file or a shared encryption key — the stage is regenerated
|
|
713
|
+
* from the committed `gkm.config.ts`, and `writeStageSecrets` mints a local key.
|
|
714
|
+
*
|
|
715
|
+
* @returns true if fresh secrets were generated, false if existing ones were kept
|
|
716
|
+
*/
|
|
717
|
+
async function ensureStageSecrets(stage, cwd = process.cwd()) {
|
|
718
|
+
const { workspace } = await require_config.loadWorkspaceConfig(cwd);
|
|
719
|
+
if (require_storage.secretsExist(stage, workspace.root)) return false;
|
|
720
|
+
const secrets = createFreshWorkspaceSecrets(stage, workspace);
|
|
721
|
+
await require_storage.writeStageSecrets(secrets, workspace.root);
|
|
722
|
+
return true;
|
|
723
|
+
}
|
|
724
|
+
/**
|
|
725
|
+
* Generate fresh secrets for the workspace.
|
|
726
|
+
*/
|
|
727
|
+
async function generateFreshSecrets(stage, workspace, options) {
|
|
728
|
+
const secrets = createFreshWorkspaceSecrets(stage, workspace);
|
|
729
|
+
await require_storage.writeStageSecrets(secrets, workspace.root);
|
|
730
|
+
logger.log(` Secrets written to .gkm/secrets/${stage}.json`);
|
|
731
|
+
if (require_sync.isSSMConfigured(workspace) && !options.yes) {
|
|
732
|
+
const { shouldPush } = await (0, prompts.default)({
|
|
733
|
+
type: "confirm",
|
|
734
|
+
name: "shouldPush",
|
|
735
|
+
message: "Push secrets to SSM for team sharing?",
|
|
736
|
+
initial: true
|
|
737
|
+
});
|
|
738
|
+
if (shouldPush) try {
|
|
739
|
+
await require_sync.pushSecrets(stage, workspace);
|
|
740
|
+
logger.log("☁️ Secrets pushed to SSM");
|
|
741
|
+
} catch (error) {
|
|
742
|
+
logger.warn(`⚠️ Failed to push to SSM: ${error.message}`);
|
|
743
|
+
}
|
|
744
|
+
}
|
|
745
|
+
return secrets;
|
|
746
|
+
}
|
|
747
|
+
/**
|
|
748
|
+
* Print setup summary with next steps.
|
|
749
|
+
*/
|
|
750
|
+
function printSummary(workspace, stage) {
|
|
751
|
+
logger.log(`\n${"─".repeat(50)}`);
|
|
752
|
+
logger.log("\n✅ Development environment ready!\n");
|
|
753
|
+
logger.log("📋 Apps:");
|
|
754
|
+
for (const [name, app] of Object.entries(workspace.apps)) {
|
|
755
|
+
const icon = app.type === "web" ? "🌐" : app.type === "mobile" ? "📱" : "🔧";
|
|
756
|
+
logger.log(` ${icon} ${name} → http://localhost:${app.port}`);
|
|
757
|
+
}
|
|
758
|
+
logger.log("\n🚀 Next steps:");
|
|
759
|
+
logger.log(" gkm dev # Start all apps");
|
|
760
|
+
logger.log(` gkm secrets:show --stage ${stage} # View secrets`);
|
|
761
|
+
logger.log("");
|
|
762
|
+
}
|
|
763
|
+
|
|
764
|
+
//#endregion
|
|
765
|
+
Object.defineProperty(exports, 'createCredentialsPreload', {
|
|
766
|
+
enumerable: true,
|
|
767
|
+
get: function () {
|
|
768
|
+
return createCredentialsPreload;
|
|
769
|
+
}
|
|
770
|
+
});
|
|
771
|
+
Object.defineProperty(exports, 'createEntryWrapper', {
|
|
772
|
+
enumerable: true,
|
|
773
|
+
get: function () {
|
|
774
|
+
return createEntryWrapper;
|
|
775
|
+
}
|
|
776
|
+
});
|
|
777
|
+
Object.defineProperty(exports, 'createFreshWorkspaceSecrets', {
|
|
778
|
+
enumerable: true,
|
|
779
|
+
get: function () {
|
|
780
|
+
return createFreshWorkspaceSecrets;
|
|
781
|
+
}
|
|
782
|
+
});
|
|
783
|
+
Object.defineProperty(exports, 'ensureStageSecrets', {
|
|
784
|
+
enumerable: true,
|
|
785
|
+
get: function () {
|
|
786
|
+
return ensureStageSecrets;
|
|
787
|
+
}
|
|
788
|
+
});
|
|
789
|
+
Object.defineProperty(exports, 'findAvailablePort', {
|
|
790
|
+
enumerable: true,
|
|
791
|
+
get: function () {
|
|
792
|
+
return findAvailablePort;
|
|
793
|
+
}
|
|
794
|
+
});
|
|
795
|
+
Object.defineProperty(exports, 'isPortAvailable', {
|
|
796
|
+
enumerable: true,
|
|
797
|
+
get: function () {
|
|
798
|
+
return isPortAvailable;
|
|
799
|
+
}
|
|
800
|
+
});
|
|
801
|
+
Object.defineProperty(exports, 'loadEnvFiles', {
|
|
802
|
+
enumerable: true,
|
|
803
|
+
get: function () {
|
|
804
|
+
return loadEnvFiles;
|
|
805
|
+
}
|
|
806
|
+
});
|
|
807
|
+
Object.defineProperty(exports, 'loadSecretsForApp', {
|
|
808
|
+
enumerable: true,
|
|
809
|
+
get: function () {
|
|
810
|
+
return loadSecretsForApp;
|
|
811
|
+
}
|
|
812
|
+
});
|
|
813
|
+
Object.defineProperty(exports, 'prepareEntryCredentials', {
|
|
814
|
+
enumerable: true,
|
|
815
|
+
get: function () {
|
|
816
|
+
return prepareEntryCredentials;
|
|
817
|
+
}
|
|
818
|
+
});
|
|
819
|
+
Object.defineProperty(exports, 'reconcileSecrets', {
|
|
820
|
+
enumerable: true,
|
|
821
|
+
get: function () {
|
|
822
|
+
return reconcileSecrets;
|
|
823
|
+
}
|
|
824
|
+
});
|
|
825
|
+
Object.defineProperty(exports, 'resolveServicePorts', {
|
|
826
|
+
enumerable: true,
|
|
827
|
+
get: function () {
|
|
828
|
+
return resolveServicePorts;
|
|
829
|
+
}
|
|
830
|
+
});
|
|
831
|
+
Object.defineProperty(exports, 'rewriteUrlsWithPorts', {
|
|
832
|
+
enumerable: true,
|
|
833
|
+
get: function () {
|
|
834
|
+
return rewriteUrlsWithPorts;
|
|
835
|
+
}
|
|
836
|
+
});
|
|
837
|
+
Object.defineProperty(exports, 'setupCommand', {
|
|
838
|
+
enumerable: true,
|
|
839
|
+
get: function () {
|
|
840
|
+
return setupCommand;
|
|
841
|
+
}
|
|
842
|
+
});
|
|
843
|
+
Object.defineProperty(exports, 'startWorkspaceServices', {
|
|
844
|
+
enumerable: true,
|
|
845
|
+
get: function () {
|
|
846
|
+
return startWorkspaceServices;
|
|
847
|
+
}
|
|
848
|
+
});
|
|
849
|
+
//# sourceMappingURL=setup-WZ-xqAQO.cjs.map
|