@geekmidas/cli 1.10.16 → 1.10.18
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 +12 -0
- package/dist/{bundler-B4AackW5.mjs → bundler-C5xkxnyr.mjs} +2 -2
- package/dist/{bundler-B4AackW5.mjs.map → bundler-C5xkxnyr.mjs.map} +1 -1
- package/dist/{bundler-BhhfkI9T.cjs → bundler-i-az1DZ2.cjs} +2 -2
- package/dist/{bundler-BhhfkI9T.cjs.map → bundler-i-az1DZ2.cjs.map} +1 -1
- package/dist/index.cjs +699 -712
- package/dist/index.cjs.map +1 -1
- package/dist/index.mjs +686 -699
- package/dist/index.mjs.map +1 -1
- package/dist/{openapi-BYxAWwok.cjs → openapi-CsCNpSf8.cjs} +1 -1
- package/dist/{openapi-BYxAWwok.cjs.map → openapi-CsCNpSf8.cjs.map} +1 -1
- package/dist/{openapi-DenF-okj.mjs → openapi-kvwpKbNe.mjs} +1 -1
- package/dist/{openapi-DenF-okj.mjs.map → openapi-kvwpKbNe.mjs.map} +1 -1
- package/dist/openapi.cjs +1 -1
- package/dist/openapi.mjs +1 -1
- package/dist/{storage-DOEtT2Hr.cjs → storage-ChVQI_G7.cjs} +1 -1
- package/dist/{storage-dbb9RyBl.mjs → storage-CpMNB77O.mjs} +1 -1
- package/dist/{storage-dbb9RyBl.mjs.map → storage-CpMNB77O.mjs.map} +1 -1
- package/dist/{storage-B1wvztiJ.cjs → storage-DLEb8Dkd.cjs} +1 -1
- package/dist/{storage-B1wvztiJ.cjs.map → storage-DLEb8Dkd.cjs.map} +1 -1
- package/dist/{storage-Cs4WBsc4.mjs → storage-mwbL7PhP.mjs} +1 -1
- package/dist/{sync-DGXXSk2v.cjs → sync-BWD_I5Ai.cjs} +2 -2
- package/dist/{sync-DGXXSk2v.cjs.map → sync-BWD_I5Ai.cjs.map} +1 -1
- package/dist/sync-ByaRPBxh.cjs +4 -0
- package/dist/{sync-COnAugP-.mjs → sync-CYBVB64f.mjs} +1 -1
- package/dist/{sync-D_NowTkZ.mjs → sync-lExOTa9t.mjs} +2 -2
- package/dist/{sync-D_NowTkZ.mjs.map → sync-lExOTa9t.mjs.map} +1 -1
- package/package.json +3 -3
- package/src/credentials/__tests__/fullDockerPorts.spec.ts +144 -0
- package/src/credentials/__tests__/helpers.ts +112 -0
- package/src/credentials/__tests__/prepareEntryCredentials.spec.ts +125 -0
- package/src/credentials/__tests__/readonlyDockerPorts.spec.ts +190 -0
- package/src/credentials/__tests__/workspaceCredentials.spec.ts +209 -0
- package/src/credentials/index.ts +826 -0
- package/src/dev/__tests__/index.spec.ts +68 -0
- package/src/dev/index.ts +44 -821
- package/src/exec/index.ts +120 -0
- package/src/init/versions.ts +1 -1
- package/src/setup/index.ts +4 -1
- package/src/test/index.ts +32 -109
- package/dist/sync-D1Pa30oV.cjs +0 -4
package/dist/index.cjs
CHANGED
|
@@ -3,14 +3,14 @@ const require_chunk = require('./chunk-CUT6urMc.cjs');
|
|
|
3
3
|
const require_workspace = require('./workspace-4SP3Gx4Y.cjs');
|
|
4
4
|
const require_config = require('./config-D3ORuiUs.cjs');
|
|
5
5
|
const require_credentials = require('./credentials-C8DWtnMY.cjs');
|
|
6
|
-
const
|
|
7
|
-
const
|
|
6
|
+
const require_storage = require('./storage-DLEb8Dkd.cjs');
|
|
7
|
+
const require_openapi = require('./openapi-CsCNpSf8.cjs');
|
|
8
8
|
const require_dokploy_api = require('./dokploy-api-DLgvEQlr.cjs');
|
|
9
9
|
const require_encryption = require('./encryption-BE0UOb8j.cjs');
|
|
10
10
|
const require_CachedStateProvider = require('./CachedStateProvider-D73dCqfH.cjs');
|
|
11
11
|
const require_fullstack_secrets = require('./fullstack-secrets-DOHBU4Rp.cjs');
|
|
12
12
|
const require_openapi_react_query = require('./openapi-react-query-DYbBq-WJ.cjs');
|
|
13
|
-
const require_sync = require('./sync-
|
|
13
|
+
const require_sync = require('./sync-BWD_I5Ai.cjs');
|
|
14
14
|
const node_fs = require_chunk.__toESM(require("node:fs"));
|
|
15
15
|
const node_path = require_chunk.__toESM(require("node:path"));
|
|
16
16
|
const commander = require_chunk.__toESM(require("commander"));
|
|
@@ -18,15 +18,15 @@ const node_process = require_chunk.__toESM(require("node:process"));
|
|
|
18
18
|
const node_readline_promises = require_chunk.__toESM(require("node:readline/promises"));
|
|
19
19
|
const node_fs_promises = require_chunk.__toESM(require("node:fs/promises"));
|
|
20
20
|
const node_child_process = require_chunk.__toESM(require("node:child_process"));
|
|
21
|
-
const node_net = require_chunk.__toESM(require("node:net"));
|
|
22
21
|
const chokidar = require_chunk.__toESM(require("chokidar"));
|
|
23
|
-
const dotenv = require_chunk.__toESM(require("dotenv"));
|
|
24
22
|
const fast_glob = require_chunk.__toESM(require("fast-glob"));
|
|
23
|
+
const node_net = require_chunk.__toESM(require("node:net"));
|
|
24
|
+
const dotenv = require_chunk.__toESM(require("dotenv"));
|
|
25
25
|
const yaml = require_chunk.__toESM(require("yaml"));
|
|
26
|
+
const node_crypto = require_chunk.__toESM(require("node:crypto"));
|
|
26
27
|
const __geekmidas_constructs_crons = require_chunk.__toESM(require("@geekmidas/constructs/crons"));
|
|
27
28
|
const __geekmidas_constructs_functions = require_chunk.__toESM(require("@geekmidas/constructs/functions"));
|
|
28
29
|
const __geekmidas_constructs_subscribers = require_chunk.__toESM(require("@geekmidas/constructs/subscribers"));
|
|
29
|
-
const node_crypto = require_chunk.__toESM(require("node:crypto"));
|
|
30
30
|
const pg = require_chunk.__toESM(require("pg"));
|
|
31
31
|
const node_dns_promises = require_chunk.__toESM(require("node:dns/promises"));
|
|
32
32
|
const node_module = require_chunk.__toESM(require("node:module"));
|
|
@@ -35,7 +35,7 @@ const prompts = require_chunk.__toESM(require("prompts"));
|
|
|
35
35
|
|
|
36
36
|
//#region package.json
|
|
37
37
|
var name = "@geekmidas/cli";
|
|
38
|
-
var version = "1.10.
|
|
38
|
+
var version = "1.10.17";
|
|
39
39
|
var description = "CLI tools for building Lambda handlers, server applications, and generating OpenAPI specs";
|
|
40
40
|
var private$1 = false;
|
|
41
41
|
var type = "module";
|
|
@@ -133,7 +133,7 @@ var package_default = {
|
|
|
133
133
|
|
|
134
134
|
//#endregion
|
|
135
135
|
//#region src/auth/index.ts
|
|
136
|
-
const logger$
|
|
136
|
+
const logger$14 = console;
|
|
137
137
|
/**
|
|
138
138
|
* Validate Dokploy token by making a test API call
|
|
139
139
|
*/
|
|
@@ -152,7 +152,7 @@ async function prompt$1(message, hidden = false) {
|
|
|
152
152
|
if (!process.stdin.isTTY) throw new Error("Interactive input required. Please provide --token option.");
|
|
153
153
|
if (hidden) {
|
|
154
154
|
process.stdout.write(message);
|
|
155
|
-
return new Promise((resolve$
|
|
155
|
+
return new Promise((resolve$4, reject) => {
|
|
156
156
|
let value = "";
|
|
157
157
|
const cleanup = () => {
|
|
158
158
|
process.stdin.setRawMode(false);
|
|
@@ -169,7 +169,7 @@ async function prompt$1(message, hidden = false) {
|
|
|
169
169
|
if (c === "\n" || c === "\r") {
|
|
170
170
|
cleanup();
|
|
171
171
|
process.stdout.write("\n");
|
|
172
|
-
resolve$
|
|
172
|
+
resolve$4(value);
|
|
173
173
|
} else if (c === "") {
|
|
174
174
|
cleanup();
|
|
175
175
|
process.stdout.write("\n");
|
|
@@ -201,36 +201,36 @@ async function prompt$1(message, hidden = false) {
|
|
|
201
201
|
async function loginCommand(options) {
|
|
202
202
|
const { service, token: providedToken, endpoint: providedEndpoint } = options;
|
|
203
203
|
if (service === "dokploy") {
|
|
204
|
-
logger$
|
|
204
|
+
logger$14.log("\n🔐 Logging in to Dokploy...\n");
|
|
205
205
|
let endpoint = providedEndpoint;
|
|
206
206
|
if (!endpoint) endpoint = await prompt$1("Dokploy URL (e.g., https://dokploy.example.com): ");
|
|
207
207
|
endpoint = endpoint.replace(/\/$/, "");
|
|
208
208
|
try {
|
|
209
209
|
new URL(endpoint);
|
|
210
210
|
} catch {
|
|
211
|
-
logger$
|
|
211
|
+
logger$14.error("Invalid URL format");
|
|
212
212
|
process.exit(1);
|
|
213
213
|
}
|
|
214
214
|
let token = providedToken;
|
|
215
215
|
if (!token) {
|
|
216
|
-
logger$
|
|
216
|
+
logger$14.log(`\nGenerate a token at: ${endpoint}/settings/profile\n`);
|
|
217
217
|
token = await prompt$1("API Token: ", true);
|
|
218
218
|
}
|
|
219
219
|
if (!token) {
|
|
220
|
-
logger$
|
|
220
|
+
logger$14.error("Token is required");
|
|
221
221
|
process.exit(1);
|
|
222
222
|
}
|
|
223
|
-
logger$
|
|
223
|
+
logger$14.log("\nValidating credentials...");
|
|
224
224
|
const isValid = await validateDokployToken(endpoint, token);
|
|
225
225
|
if (!isValid) {
|
|
226
|
-
logger$
|
|
226
|
+
logger$14.error("\n✗ Invalid credentials. Please check your token and try again.");
|
|
227
227
|
process.exit(1);
|
|
228
228
|
}
|
|
229
229
|
await require_credentials.storeDokployCredentials(token, endpoint);
|
|
230
|
-
logger$
|
|
231
|
-
logger$
|
|
232
|
-
logger$
|
|
233
|
-
logger$
|
|
230
|
+
logger$14.log("\n✓ Successfully logged in to Dokploy!");
|
|
231
|
+
logger$14.log(` Endpoint: ${endpoint}`);
|
|
232
|
+
logger$14.log(` Credentials stored in: ${require_credentials.getCredentialsPath()}`);
|
|
233
|
+
logger$14.log("\nYou can now use deploy commands without setting DOKPLOY_API_TOKEN.");
|
|
234
234
|
}
|
|
235
235
|
}
|
|
236
236
|
/**
|
|
@@ -240,28 +240,28 @@ async function logoutCommand(options) {
|
|
|
240
240
|
const { service = "dokploy" } = options;
|
|
241
241
|
if (service === "all") {
|
|
242
242
|
const dokployRemoved = await require_credentials.removeDokployCredentials();
|
|
243
|
-
if (dokployRemoved) logger$
|
|
244
|
-
else logger$
|
|
243
|
+
if (dokployRemoved) logger$14.log("\n✓ Logged out from all services");
|
|
244
|
+
else logger$14.log("\nNo stored credentials found");
|
|
245
245
|
return;
|
|
246
246
|
}
|
|
247
247
|
if (service === "dokploy") {
|
|
248
248
|
const removed = await require_credentials.removeDokployCredentials();
|
|
249
|
-
if (removed) logger$
|
|
250
|
-
else logger$
|
|
249
|
+
if (removed) logger$14.log("\n✓ Logged out from Dokploy");
|
|
250
|
+
else logger$14.log("\nNo Dokploy credentials found");
|
|
251
251
|
}
|
|
252
252
|
}
|
|
253
253
|
/**
|
|
254
254
|
* Show current login status
|
|
255
255
|
*/
|
|
256
256
|
async function whoamiCommand() {
|
|
257
|
-
logger$
|
|
257
|
+
logger$14.log("\n📋 Current credentials:\n");
|
|
258
258
|
const dokploy = await require_credentials.getDokployCredentials();
|
|
259
259
|
if (dokploy) {
|
|
260
|
-
logger$
|
|
261
|
-
logger$
|
|
262
|
-
logger$
|
|
263
|
-
} else logger$
|
|
264
|
-
logger$
|
|
260
|
+
logger$14.log(" Dokploy:");
|
|
261
|
+
logger$14.log(` Endpoint: ${dokploy.endpoint}`);
|
|
262
|
+
logger$14.log(` Token: ${maskToken(dokploy.token)}`);
|
|
263
|
+
} else logger$14.log(" Dokploy: Not logged in");
|
|
264
|
+
logger$14.log(`\n Credentials file: ${require_credentials.getCredentialsPath()}`);
|
|
265
265
|
}
|
|
266
266
|
/**
|
|
267
267
|
* Mask a token for display
|
|
@@ -343,134 +343,589 @@ function isEnabled(config) {
|
|
|
343
343
|
}
|
|
344
344
|
|
|
345
345
|
//#endregion
|
|
346
|
-
//#region src/
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
environment: await construct.getEnvironment()
|
|
346
|
+
//#region src/credentials/index.ts
|
|
347
|
+
const logger$13 = console;
|
|
348
|
+
/**
|
|
349
|
+
* Load environment files
|
|
350
|
+
* @internal Exported for testing
|
|
351
|
+
*/
|
|
352
|
+
function loadEnvFiles(envConfig, cwd = process.cwd()) {
|
|
353
|
+
const loaded = [];
|
|
354
|
+
const missing = [];
|
|
355
|
+
const envFiles = envConfig ? Array.isArray(envConfig) ? envConfig : [envConfig] : [".env"];
|
|
356
|
+
for (const envFile of envFiles) {
|
|
357
|
+
const envPath = (0, node_path.resolve)(cwd, envFile);
|
|
358
|
+
if ((0, node_fs.existsSync)(envPath)) {
|
|
359
|
+
(0, dotenv.config)({
|
|
360
|
+
path: envPath,
|
|
361
|
+
override: true,
|
|
362
|
+
quiet: true
|
|
364
363
|
});
|
|
365
|
-
|
|
366
|
-
}
|
|
367
|
-
return cronInfos;
|
|
368
|
-
}
|
|
369
|
-
isConstruct(value) {
|
|
370
|
-
return __geekmidas_constructs_crons.Cron.isCron(value);
|
|
371
|
-
}
|
|
372
|
-
async generateCronHandler(outputDir, sourceFile, exportName, context) {
|
|
373
|
-
const handlerFileName = `${exportName}.ts`;
|
|
374
|
-
const handlerPath = (0, node_path.join)(outputDir, handlerFileName);
|
|
375
|
-
const relativePath = (0, node_path.relative)((0, node_path.dirname)(handlerPath), sourceFile);
|
|
376
|
-
const importPath = relativePath.replace(/\.ts$/, ".js");
|
|
377
|
-
const relativeEnvParserPath = (0, node_path.relative)((0, node_path.dirname)(handlerPath), context.envParserPath);
|
|
378
|
-
const relativeLoggerPath = (0, node_path.relative)((0, node_path.dirname)(handlerPath), context.loggerPath);
|
|
379
|
-
const content = `import { AWSScheduledFunction } from '@geekmidas/constructs/crons';
|
|
380
|
-
import { ${exportName} } from '${importPath}';
|
|
381
|
-
import ${context.envParserImportPattern} from '${relativeEnvParserPath}';
|
|
382
|
-
import ${context.loggerImportPattern} from '${relativeLoggerPath}';
|
|
383
|
-
|
|
384
|
-
const adapter = new AWSScheduledFunction(envParser, ${exportName});
|
|
385
|
-
|
|
386
|
-
export const handler = adapter.handler;
|
|
387
|
-
`;
|
|
388
|
-
await (0, node_fs_promises.writeFile)(handlerPath, content);
|
|
389
|
-
return handlerPath;
|
|
364
|
+
loaded.push(envFile);
|
|
365
|
+
} else if (envConfig) missing.push(envFile);
|
|
390
366
|
}
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
367
|
+
return {
|
|
368
|
+
loaded,
|
|
369
|
+
missing
|
|
370
|
+
};
|
|
371
|
+
}
|
|
372
|
+
/**
|
|
373
|
+
* Check if a port is available
|
|
374
|
+
* @internal Exported for testing
|
|
375
|
+
*/
|
|
376
|
+
async function isPortAvailable(port) {
|
|
377
|
+
return new Promise((resolve$4) => {
|
|
378
|
+
const server = (0, node_net.createServer)();
|
|
379
|
+
server.once("error", (err) => {
|
|
380
|
+
if (err.code === "EADDRINUSE") resolve$4(false);
|
|
381
|
+
else resolve$4(false);
|
|
382
|
+
});
|
|
383
|
+
server.once("listening", () => {
|
|
384
|
+
server.close();
|
|
385
|
+
resolve$4(true);
|
|
386
|
+
});
|
|
387
|
+
server.listen(port);
|
|
388
|
+
});
|
|
389
|
+
}
|
|
390
|
+
/**
|
|
391
|
+
* Find an available port starting from the preferred port
|
|
392
|
+
* @internal Exported for testing
|
|
393
|
+
*/
|
|
394
|
+
async function findAvailablePort(preferredPort, maxAttempts = 10) {
|
|
395
|
+
for (let i = 0; i < maxAttempts; i++) {
|
|
396
|
+
const port = preferredPort + i;
|
|
397
|
+
if (await isPortAvailable(port)) return port;
|
|
398
|
+
logger$13.log(`⚠️ Port ${port} is in use, trying ${port + 1}...`);
|
|
398
399
|
}
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
400
|
+
throw new Error(`Could not find an available port after trying ${maxAttempts} ports starting from ${preferredPort}`);
|
|
401
|
+
}
|
|
402
|
+
const PORT_STATE_PATH = ".gkm/ports.json";
|
|
403
|
+
/**
|
|
404
|
+
* Parse docker-compose.yml and extract all port mappings that use env var interpolation.
|
|
405
|
+
* Entries like `'${POSTGRES_HOST_PORT:-5432}:5432'` are captured.
|
|
406
|
+
* Fixed port mappings like `'5050:80'` are skipped.
|
|
407
|
+
* @internal Exported for testing
|
|
408
|
+
*/
|
|
409
|
+
function parseComposePortMappings(composePath) {
|
|
410
|
+
if (!(0, node_fs.existsSync)(composePath)) return [];
|
|
411
|
+
const content = (0, node_fs.readFileSync)(composePath, "utf-8");
|
|
412
|
+
const compose = (0, yaml.parse)(content);
|
|
413
|
+
if (!compose?.services) return [];
|
|
414
|
+
const results = [];
|
|
415
|
+
for (const [serviceName, serviceConfig] of Object.entries(compose.services)) for (const portMapping of serviceConfig?.ports ?? []) {
|
|
416
|
+
const match = String(portMapping).match(/\$\{(\w+):-(\d+)\}:(\d+)/);
|
|
417
|
+
if (match?.[1] && match[2] && match[3]) results.push({
|
|
418
|
+
service: serviceName,
|
|
419
|
+
envVar: match[1],
|
|
420
|
+
defaultPort: Number(match[2]),
|
|
421
|
+
containerPort: Number(match[3])
|
|
422
|
+
});
|
|
418
423
|
}
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
const adapter = new AWSLambdaFunction(envParser, ${exportName});
|
|
432
|
-
|
|
433
|
-
export const handler = adapter.handler;
|
|
434
|
-
`;
|
|
435
|
-
await (0, node_fs_promises.writeFile)(handlerPath, content);
|
|
436
|
-
return handlerPath;
|
|
424
|
+
return results;
|
|
425
|
+
}
|
|
426
|
+
/**
|
|
427
|
+
* Load saved port state from .gkm/ports.json.
|
|
428
|
+
* @internal Exported for testing
|
|
429
|
+
*/
|
|
430
|
+
async function loadPortState(workspaceRoot) {
|
|
431
|
+
try {
|
|
432
|
+
const raw = await (0, node_fs_promises.readFile)((0, node_path.join)(workspaceRoot, PORT_STATE_PATH), "utf-8");
|
|
433
|
+
return JSON.parse(raw);
|
|
434
|
+
} catch {
|
|
435
|
+
return {};
|
|
437
436
|
}
|
|
438
|
-
}
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
437
|
+
}
|
|
438
|
+
/**
|
|
439
|
+
* Save port state to .gkm/ports.json.
|
|
440
|
+
* @internal Exported for testing
|
|
441
|
+
*/
|
|
442
|
+
async function savePortState(workspaceRoot, ports) {
|
|
443
|
+
const dir = (0, node_path.join)(workspaceRoot, ".gkm");
|
|
444
|
+
await (0, node_fs_promises.mkdir)(dir, { recursive: true });
|
|
445
|
+
await (0, node_fs_promises.writeFile)((0, node_path.join)(workspaceRoot, PORT_STATE_PATH), `${JSON.stringify(ports, null, 2)}\n`);
|
|
446
|
+
}
|
|
447
|
+
/**
|
|
448
|
+
* Check if a project's own Docker container is running and return its host port.
|
|
449
|
+
* Uses `docker compose port` scoped to the project's compose file.
|
|
450
|
+
* @internal Exported for testing
|
|
451
|
+
*/
|
|
452
|
+
function getContainerHostPort(workspaceRoot, service, containerPort) {
|
|
453
|
+
try {
|
|
454
|
+
const result = (0, node_child_process.execSync)(`docker compose port ${service} ${containerPort}`, {
|
|
455
|
+
cwd: workspaceRoot,
|
|
456
|
+
stdio: "pipe"
|
|
457
|
+
}).toString().trim();
|
|
458
|
+
const match = result.match(/:(\d+)$/);
|
|
459
|
+
return match ? Number(match[1]) : null;
|
|
460
|
+
} catch {
|
|
461
|
+
return null;
|
|
445
462
|
}
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
463
|
+
}
|
|
464
|
+
/**
|
|
465
|
+
* Resolve host ports for Docker services by parsing docker-compose.yml.
|
|
466
|
+
* Priority: running container → saved state → find available port.
|
|
467
|
+
* Persists resolved ports to .gkm/ports.json.
|
|
468
|
+
* @internal Exported for testing
|
|
469
|
+
*/
|
|
470
|
+
async function resolveServicePorts(workspaceRoot) {
|
|
471
|
+
const composePath = (0, node_path.join)(workspaceRoot, "docker-compose.yml");
|
|
472
|
+
const mappings = parseComposePortMappings(composePath);
|
|
473
|
+
if (mappings.length === 0) return {
|
|
474
|
+
dockerEnv: {},
|
|
475
|
+
ports: {},
|
|
476
|
+
mappings: []
|
|
477
|
+
};
|
|
478
|
+
const savedState = await loadPortState(workspaceRoot);
|
|
479
|
+
const dockerEnv = {};
|
|
480
|
+
const ports = {};
|
|
481
|
+
const assignedPorts = /* @__PURE__ */ new Set();
|
|
482
|
+
logger$13.log("\n🔌 Resolving service ports...");
|
|
483
|
+
for (const mapping of mappings) {
|
|
484
|
+
const containerPort = getContainerHostPort(workspaceRoot, mapping.service, mapping.containerPort);
|
|
485
|
+
if (containerPort !== null) {
|
|
486
|
+
ports[mapping.envVar] = containerPort;
|
|
487
|
+
dockerEnv[mapping.envVar] = String(containerPort);
|
|
488
|
+
assignedPorts.add(containerPort);
|
|
489
|
+
logger$13.log(` 🔄 ${mapping.service}:${mapping.containerPort}: reusing existing container on port ${containerPort}`);
|
|
490
|
+
continue;
|
|
454
491
|
}
|
|
455
|
-
|
|
456
|
-
if (
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
name: key,
|
|
463
|
-
handler: (0, node_path.relative)(process.cwd(), handlerFile).replace(/\.ts$/, ".handler"),
|
|
464
|
-
subscribedEvents: construct.subscribedEvents || [],
|
|
465
|
-
timeout: construct.timeout,
|
|
466
|
-
memorySize: construct.memorySize,
|
|
467
|
-
environment: await construct.getEnvironment()
|
|
468
|
-
});
|
|
469
|
-
logger$13.log(`Generated subscriber handler: ${key}`);
|
|
492
|
+
const savedPort = savedState[mapping.envVar];
|
|
493
|
+
if (savedPort && !assignedPorts.has(savedPort) && await isPortAvailable(savedPort)) {
|
|
494
|
+
ports[mapping.envVar] = savedPort;
|
|
495
|
+
dockerEnv[mapping.envVar] = String(savedPort);
|
|
496
|
+
assignedPorts.add(savedPort);
|
|
497
|
+
logger$13.log(` 💾 ${mapping.service}:${mapping.containerPort}: using saved port ${savedPort}`);
|
|
498
|
+
continue;
|
|
470
499
|
}
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
500
|
+
let resolvedPort = await findAvailablePort(mapping.defaultPort);
|
|
501
|
+
while (assignedPorts.has(resolvedPort)) resolvedPort = await findAvailablePort(resolvedPort + 1);
|
|
502
|
+
ports[mapping.envVar] = resolvedPort;
|
|
503
|
+
dockerEnv[mapping.envVar] = String(resolvedPort);
|
|
504
|
+
assignedPorts.add(resolvedPort);
|
|
505
|
+
if (resolvedPort !== mapping.defaultPort) logger$13.log(` ⚡ ${mapping.service}:${mapping.containerPort}: port ${mapping.defaultPort} occupied, using port ${resolvedPort}`);
|
|
506
|
+
else logger$13.log(` ✅ ${mapping.service}:${mapping.containerPort}: using default port ${resolvedPort}`);
|
|
507
|
+
}
|
|
508
|
+
await savePortState(workspaceRoot, ports);
|
|
509
|
+
return {
|
|
510
|
+
dockerEnv,
|
|
511
|
+
ports,
|
|
512
|
+
mappings
|
|
513
|
+
};
|
|
514
|
+
}
|
|
515
|
+
/**
|
|
516
|
+
* Replace a port in a URL string.
|
|
517
|
+
* Handles both `hostname:port` and `localhost:port` patterns.
|
|
518
|
+
* @internal Exported for testing
|
|
519
|
+
*/
|
|
520
|
+
function replacePortInUrl(url, oldPort, newPort) {
|
|
521
|
+
if (oldPort === newPort) return url;
|
|
522
|
+
let result = url.replace(new RegExp(`:${oldPort}(?=[/?#]|$)`, "g"), `:${newPort}`);
|
|
523
|
+
result = result.replace(new RegExp(`%3A${oldPort}(?=[%/?#&]|$)`, "gi"), `%3A${newPort}`);
|
|
524
|
+
return result;
|
|
525
|
+
}
|
|
526
|
+
/**
|
|
527
|
+
* Rewrite connection URLs and port vars in secrets with resolved ports.
|
|
528
|
+
* Uses the parsed compose mappings to determine which default ports to replace.
|
|
529
|
+
* Pure transform — does not modify secrets on disk.
|
|
530
|
+
* @internal Exported for testing
|
|
531
|
+
*/
|
|
532
|
+
function rewriteUrlsWithPorts(secrets, resolvedPorts) {
|
|
533
|
+
const { ports, mappings } = resolvedPorts;
|
|
534
|
+
const result = { ...secrets };
|
|
535
|
+
const portReplacements = [];
|
|
536
|
+
const serviceNames = /* @__PURE__ */ new Set();
|
|
537
|
+
for (const mapping of mappings) {
|
|
538
|
+
serviceNames.add(mapping.service);
|
|
539
|
+
const resolved = ports[mapping.envVar];
|
|
540
|
+
if (resolved !== void 0) portReplacements.push({
|
|
541
|
+
defaultPort: mapping.defaultPort,
|
|
542
|
+
resolvedPort: resolved
|
|
543
|
+
});
|
|
544
|
+
}
|
|
545
|
+
for (const [key, value] of Object.entries(result)) {
|
|
546
|
+
if (!key.endsWith("_HOST")) continue;
|
|
547
|
+
if (serviceNames.has(value)) result[key] = "localhost";
|
|
548
|
+
}
|
|
549
|
+
for (const [key, value] of Object.entries(result)) {
|
|
550
|
+
if (!key.endsWith("_PORT")) continue;
|
|
551
|
+
for (const { defaultPort, resolvedPort } of portReplacements) if (value === String(defaultPort)) result[key] = String(resolvedPort);
|
|
552
|
+
}
|
|
553
|
+
for (const [key, value] of Object.entries(result)) {
|
|
554
|
+
if (!key.endsWith("_URL") && !key.endsWith("_ENDPOINT") && !key.endsWith("_CONNECTION_STRING") && key !== "DATABASE_URL") continue;
|
|
555
|
+
let rewritten = value;
|
|
556
|
+
for (const name$1 of serviceNames) rewritten = rewritten.replace(new RegExp(`@${name$1}:`, "g"), "@localhost:");
|
|
557
|
+
for (const { defaultPort, resolvedPort } of portReplacements) rewritten = replacePortInUrl(rewritten, defaultPort, resolvedPort);
|
|
558
|
+
result[key] = rewritten;
|
|
559
|
+
}
|
|
560
|
+
return result;
|
|
561
|
+
}
|
|
562
|
+
/**
|
|
563
|
+
* Build the environment variables to pass to `docker compose up`.
|
|
564
|
+
* Merges process.env, secrets, and port mappings so that Docker Compose
|
|
565
|
+
* can interpolate variables like ${POSTGRES_USER} correctly.
|
|
566
|
+
* @internal Exported for testing
|
|
567
|
+
*/
|
|
568
|
+
function buildDockerComposeEnv(secretsEnv, portEnv) {
|
|
569
|
+
return {
|
|
570
|
+
...process.env,
|
|
571
|
+
...secretsEnv,
|
|
572
|
+
...portEnv
|
|
573
|
+
};
|
|
574
|
+
}
|
|
575
|
+
/**
|
|
576
|
+
* Parse all service names from a docker-compose.yml file.
|
|
577
|
+
* @internal Exported for testing
|
|
578
|
+
*/
|
|
579
|
+
function parseComposeServiceNames(composePath) {
|
|
580
|
+
if (!(0, node_fs.existsSync)(composePath)) return [];
|
|
581
|
+
const content = (0, node_fs.readFileSync)(composePath, "utf-8");
|
|
582
|
+
const compose = (0, yaml.parse)(content);
|
|
583
|
+
return Object.keys(compose?.services ?? {});
|
|
584
|
+
}
|
|
585
|
+
/**
|
|
586
|
+
* Start docker-compose services for a single-app project (no workspace config).
|
|
587
|
+
* Starts all services defined in docker-compose.yml.
|
|
588
|
+
*/
|
|
589
|
+
async function startComposeServices(cwd, portEnv, secretsEnv) {
|
|
590
|
+
const composeFile = (0, node_path.join)(cwd, "docker-compose.yml");
|
|
591
|
+
if (!(0, node_fs.existsSync)(composeFile)) return;
|
|
592
|
+
const servicesToStart = parseComposeServiceNames(composeFile);
|
|
593
|
+
if (servicesToStart.length === 0) return;
|
|
594
|
+
logger$13.log(`🐳 Starting services: ${servicesToStart.join(", ")}`);
|
|
595
|
+
try {
|
|
596
|
+
(0, node_child_process.execSync)(`docker compose up -d ${servicesToStart.join(" ")}`, {
|
|
597
|
+
cwd,
|
|
598
|
+
stdio: "inherit",
|
|
599
|
+
env: buildDockerComposeEnv(secretsEnv, portEnv)
|
|
600
|
+
});
|
|
601
|
+
logger$13.log("✅ Services started");
|
|
602
|
+
} catch (error) {
|
|
603
|
+
logger$13.error("❌ Failed to start services:", error.message);
|
|
604
|
+
throw error;
|
|
605
|
+
}
|
|
606
|
+
}
|
|
607
|
+
/**
|
|
608
|
+
* Start docker-compose services for a workspace.
|
|
609
|
+
* Discovers all services from docker-compose.yml and starts everything
|
|
610
|
+
* except app services (which are managed by turbo).
|
|
611
|
+
* @internal Exported for testing
|
|
612
|
+
*/
|
|
613
|
+
async function startWorkspaceServices(workspace, portEnv, secretsEnv) {
|
|
614
|
+
const composeFile = (0, node_path.join)(workspace.root, "docker-compose.yml");
|
|
615
|
+
if (!(0, node_fs.existsSync)(composeFile)) return;
|
|
616
|
+
const allServices = parseComposeServiceNames(composeFile);
|
|
617
|
+
const appNames = new Set(Object.keys(workspace.apps));
|
|
618
|
+
const servicesToStart = allServices.filter((name$1) => !appNames.has(name$1));
|
|
619
|
+
if (servicesToStart.length === 0) return;
|
|
620
|
+
logger$13.log(`🐳 Starting services: ${servicesToStart.join(", ")}`);
|
|
621
|
+
try {
|
|
622
|
+
(0, node_child_process.execSync)(`docker compose up -d ${servicesToStart.join(" ")}`, {
|
|
623
|
+
cwd: workspace.root,
|
|
624
|
+
stdio: "inherit",
|
|
625
|
+
env: buildDockerComposeEnv(secretsEnv, portEnv)
|
|
626
|
+
});
|
|
627
|
+
logger$13.log("✅ Services started");
|
|
628
|
+
} catch (error) {
|
|
629
|
+
logger$13.error("❌ Failed to start services:", error.message);
|
|
630
|
+
throw error;
|
|
631
|
+
}
|
|
632
|
+
}
|
|
633
|
+
/**
|
|
634
|
+
* Load and flatten secrets for an app from encrypted storage.
|
|
635
|
+
* For workspace app: maps {APP}_DATABASE_URL → DATABASE_URL.
|
|
636
|
+
* @internal Exported for testing
|
|
637
|
+
*/
|
|
638
|
+
async function loadSecretsForApp(secretsRoot, appName, stages = ["dev", "development"]) {
|
|
639
|
+
let secrets = {};
|
|
640
|
+
for (const stage of stages) if (require_storage.secretsExist(stage, secretsRoot)) {
|
|
641
|
+
const stageSecrets = await require_storage.readStageSecrets(stage, secretsRoot);
|
|
642
|
+
if (stageSecrets) {
|
|
643
|
+
logger$13.log(`🔐 Loading secrets from stage: ${stage}`);
|
|
644
|
+
secrets = require_storage.toEmbeddableSecrets(stageSecrets);
|
|
645
|
+
break;
|
|
646
|
+
}
|
|
647
|
+
}
|
|
648
|
+
if (Object.keys(secrets).length === 0) return {};
|
|
649
|
+
if (!appName) return secrets;
|
|
650
|
+
const prefix = appName.toUpperCase();
|
|
651
|
+
const mapped = { ...secrets };
|
|
652
|
+
const appDbUrl = secrets[`${prefix}_DATABASE_URL`];
|
|
653
|
+
if (appDbUrl) mapped.DATABASE_URL = appDbUrl;
|
|
654
|
+
return mapped;
|
|
655
|
+
}
|
|
656
|
+
/**
|
|
657
|
+
* Walk up the directory tree to find the root containing .gkm/secrets/.
|
|
658
|
+
* @internal Exported for testing
|
|
659
|
+
*/
|
|
660
|
+
function findSecretsRoot(startDir) {
|
|
661
|
+
let dir = startDir;
|
|
662
|
+
while (dir !== "/") {
|
|
663
|
+
if ((0, node_fs.existsSync)((0, node_path.join)(dir, ".gkm", "secrets"))) return dir;
|
|
664
|
+
const parent = (0, node_path.dirname)(dir);
|
|
665
|
+
if (parent === dir) break;
|
|
666
|
+
dir = parent;
|
|
667
|
+
}
|
|
668
|
+
return startDir;
|
|
669
|
+
}
|
|
670
|
+
/**
|
|
671
|
+
* Generate the credentials injection code snippet.
|
|
672
|
+
* This is the common logic used by both entry wrapper and exec preload.
|
|
673
|
+
* @internal
|
|
674
|
+
*/
|
|
675
|
+
function generateCredentialsInjection(secretsJsonPath) {
|
|
676
|
+
return `import { existsSync, readFileSync } from 'node:fs';
|
|
677
|
+
|
|
678
|
+
// Inject dev secrets via globalThis and process.env
|
|
679
|
+
// Using globalThis.__gkm_credentials__ avoids CJS/ESM interop issues where
|
|
680
|
+
// Object.assign on the Credentials export only mutates one module copy.
|
|
681
|
+
const secretsPath = '${secretsJsonPath}';
|
|
682
|
+
if (existsSync(secretsPath)) {
|
|
683
|
+
const secrets = JSON.parse(readFileSync(secretsPath, 'utf-8'));
|
|
684
|
+
globalThis.__gkm_credentials__ = secrets;
|
|
685
|
+
Object.assign(process.env, secrets);
|
|
686
|
+
}
|
|
687
|
+
`;
|
|
688
|
+
}
|
|
689
|
+
/**
|
|
690
|
+
* Create a preload script that injects secrets into Credentials.
|
|
691
|
+
* Used by `gkm exec` to inject secrets before running any command.
|
|
692
|
+
* @internal Exported for testing
|
|
693
|
+
*/
|
|
694
|
+
async function createCredentialsPreload(preloadPath, secretsJsonPath) {
|
|
695
|
+
const content = `/**
|
|
696
|
+
* Credentials preload generated by 'gkm exec'
|
|
697
|
+
* This file is loaded via NODE_OPTIONS="--import <path>"
|
|
698
|
+
*/
|
|
699
|
+
${generateCredentialsInjection(secretsJsonPath)}`;
|
|
700
|
+
await (0, node_fs_promises.writeFile)(preloadPath, content);
|
|
701
|
+
}
|
|
702
|
+
/**
|
|
703
|
+
* Create a wrapper script that injects secrets before importing the entry file.
|
|
704
|
+
* @internal Exported for testing
|
|
705
|
+
*/
|
|
706
|
+
async function createEntryWrapper(wrapperPath, entryPath, secretsJsonPath) {
|
|
707
|
+
const credentialsInjection = secretsJsonPath ? `${generateCredentialsInjection(secretsJsonPath)}
|
|
708
|
+
` : "";
|
|
709
|
+
const content = `#!/usr/bin/env node
|
|
710
|
+
/**
|
|
711
|
+
* Entry wrapper generated by 'gkm dev --entry'
|
|
712
|
+
*/
|
|
713
|
+
${credentialsInjection}// Import and run the user's entry file (dynamic import ensures secrets load first)
|
|
714
|
+
await import('${entryPath}');
|
|
715
|
+
`;
|
|
716
|
+
await (0, node_fs_promises.writeFile)(wrapperPath, content);
|
|
717
|
+
}
|
|
718
|
+
/**
|
|
719
|
+
* Prepare credentials for dev/exec/test modes.
|
|
720
|
+
* Loads workspace config, secrets, resolves Docker ports, rewrites URLs,
|
|
721
|
+
* injects PORT, dependency URLs, and writes credentials JSON.
|
|
722
|
+
*
|
|
723
|
+
* @param options.resolveDockerPorts - How to resolve Docker ports:
|
|
724
|
+
* - `'full'` (default): probe running containers, saved state, then find available ports. Used by dev/test.
|
|
725
|
+
* - `'readonly'`: check running containers and saved state only, never probe for new ports. Used by exec.
|
|
726
|
+
* @param options.stages - Secret stages to try, in order. Default: ['dev', 'development'].
|
|
727
|
+
* @param options.startDocker - Start Docker Compose services after port resolution. Default: false.
|
|
728
|
+
* @param options.secretsFileName - Custom secrets JSON filename. Default: 'dev-secrets-{appName}.json' or 'dev-secrets.json'.
|
|
729
|
+
* @internal Exported for testing
|
|
730
|
+
*/
|
|
731
|
+
async function prepareEntryCredentials(options) {
|
|
732
|
+
const cwd = options.cwd ?? process.cwd();
|
|
733
|
+
const portMode = options.resolveDockerPorts ?? "full";
|
|
734
|
+
let workspaceAppPort;
|
|
735
|
+
let secretsRoot = cwd;
|
|
736
|
+
let appName;
|
|
737
|
+
let appInfo;
|
|
738
|
+
try {
|
|
739
|
+
appInfo = await require_config.loadWorkspaceAppInfo(cwd);
|
|
740
|
+
workspaceAppPort = appInfo.app.port;
|
|
741
|
+
secretsRoot = appInfo.workspaceRoot;
|
|
742
|
+
appName = appInfo.appName;
|
|
743
|
+
} catch (error) {
|
|
744
|
+
logger$13.log(`⚠️ Could not load workspace config: ${error.message}`);
|
|
745
|
+
secretsRoot = findSecretsRoot(cwd);
|
|
746
|
+
appName = require_config.getAppNameFromCwd(cwd) ?? void 0;
|
|
747
|
+
}
|
|
748
|
+
const resolvedPort = options.explicitPort ?? workspaceAppPort ?? 3e3;
|
|
749
|
+
const credentials = await loadSecretsForApp(secretsRoot, appName, options.stages);
|
|
750
|
+
credentials.PORT = String(resolvedPort);
|
|
751
|
+
const composePath = (0, node_path.join)(secretsRoot, "docker-compose.yml");
|
|
752
|
+
const mappings = parseComposePortMappings(composePath);
|
|
753
|
+
if (mappings.length > 0) {
|
|
754
|
+
let resolvedPorts;
|
|
755
|
+
if (portMode === "full") resolvedPorts = await resolveServicePorts(secretsRoot);
|
|
756
|
+
else {
|
|
757
|
+
const savedPorts = await loadPortState(secretsRoot);
|
|
758
|
+
const ports = {};
|
|
759
|
+
for (const mapping of mappings) {
|
|
760
|
+
const containerPort = getContainerHostPort(secretsRoot, mapping.service, mapping.containerPort);
|
|
761
|
+
if (containerPort !== null) ports[mapping.envVar] = containerPort;
|
|
762
|
+
else {
|
|
763
|
+
const saved = savedPorts[mapping.envVar];
|
|
764
|
+
if (saved !== void 0) ports[mapping.envVar] = saved;
|
|
765
|
+
}
|
|
766
|
+
}
|
|
767
|
+
resolvedPorts = {
|
|
768
|
+
dockerEnv: {},
|
|
769
|
+
ports,
|
|
770
|
+
mappings
|
|
771
|
+
};
|
|
772
|
+
}
|
|
773
|
+
if (options.startDocker) if (appInfo) await startWorkspaceServices(appInfo.workspace, resolvedPorts.dockerEnv, credentials);
|
|
774
|
+
else await startComposeServices(secretsRoot, resolvedPorts.dockerEnv, credentials);
|
|
775
|
+
if (Object.keys(resolvedPorts.ports).length > 0) {
|
|
776
|
+
const rewritten = rewriteUrlsWithPorts(credentials, resolvedPorts);
|
|
777
|
+
Object.assign(credentials, rewritten);
|
|
778
|
+
logger$13.log(`🔌 Applied ${Object.keys(resolvedPorts.ports).length} port mapping(s)`);
|
|
779
|
+
}
|
|
780
|
+
}
|
|
781
|
+
if (appInfo?.appName) {
|
|
782
|
+
const depEnv = require_workspace.getDependencyEnvVars(appInfo.workspace, appInfo.appName);
|
|
783
|
+
Object.assign(credentials, depEnv);
|
|
784
|
+
}
|
|
785
|
+
const secretsDir = (0, node_path.join)(secretsRoot, ".gkm");
|
|
786
|
+
await (0, node_fs_promises.mkdir)(secretsDir, { recursive: true });
|
|
787
|
+
const secretsFileName = options.secretsFileName ?? (appName ? `dev-secrets-${appName}.json` : "dev-secrets.json");
|
|
788
|
+
const secretsJsonPath = (0, node_path.join)(secretsDir, secretsFileName);
|
|
789
|
+
await (0, node_fs_promises.writeFile)(secretsJsonPath, JSON.stringify(credentials, null, 2));
|
|
790
|
+
return {
|
|
791
|
+
credentials,
|
|
792
|
+
resolvedPort,
|
|
793
|
+
secretsJsonPath,
|
|
794
|
+
appName,
|
|
795
|
+
secretsRoot,
|
|
796
|
+
appInfo
|
|
797
|
+
};
|
|
798
|
+
}
|
|
799
|
+
|
|
800
|
+
//#endregion
|
|
801
|
+
//#region src/generators/CronGenerator.ts
|
|
802
|
+
var CronGenerator = class extends require_openapi.ConstructGenerator {
|
|
803
|
+
async build(context, constructs, outputDir, options) {
|
|
804
|
+
const provider = options?.provider || "aws-lambda";
|
|
805
|
+
const logger$15 = console;
|
|
806
|
+
const cronInfos = [];
|
|
807
|
+
if (constructs.length === 0 || provider !== "aws-lambda") return cronInfos;
|
|
808
|
+
const cronsDir = (0, node_path.join)(outputDir, "crons");
|
|
809
|
+
await (0, node_fs_promises.mkdir)(cronsDir, { recursive: true });
|
|
810
|
+
for (const { key, construct, path } of constructs) {
|
|
811
|
+
const handlerFile = await this.generateCronHandler(cronsDir, path.relative, key, context);
|
|
812
|
+
cronInfos.push({
|
|
813
|
+
name: key,
|
|
814
|
+
handler: (0, node_path.relative)(process.cwd(), handlerFile).replace(/\.ts$/, ".handler"),
|
|
815
|
+
schedule: construct.schedule || "rate(1 hour)",
|
|
816
|
+
timeout: construct.timeout,
|
|
817
|
+
memorySize: construct.memorySize,
|
|
818
|
+
environment: await construct.getEnvironment()
|
|
819
|
+
});
|
|
820
|
+
logger$15.log(`Generated cron handler: ${key}`);
|
|
821
|
+
}
|
|
822
|
+
return cronInfos;
|
|
823
|
+
}
|
|
824
|
+
isConstruct(value) {
|
|
825
|
+
return __geekmidas_constructs_crons.Cron.isCron(value);
|
|
826
|
+
}
|
|
827
|
+
async generateCronHandler(outputDir, sourceFile, exportName, context) {
|
|
828
|
+
const handlerFileName = `${exportName}.ts`;
|
|
829
|
+
const handlerPath = (0, node_path.join)(outputDir, handlerFileName);
|
|
830
|
+
const relativePath = (0, node_path.relative)((0, node_path.dirname)(handlerPath), sourceFile);
|
|
831
|
+
const importPath = relativePath.replace(/\.ts$/, ".js");
|
|
832
|
+
const relativeEnvParserPath = (0, node_path.relative)((0, node_path.dirname)(handlerPath), context.envParserPath);
|
|
833
|
+
const relativeLoggerPath = (0, node_path.relative)((0, node_path.dirname)(handlerPath), context.loggerPath);
|
|
834
|
+
const content = `import { AWSScheduledFunction } from '@geekmidas/constructs/crons';
|
|
835
|
+
import { ${exportName} } from '${importPath}';
|
|
836
|
+
import ${context.envParserImportPattern} from '${relativeEnvParserPath}';
|
|
837
|
+
import ${context.loggerImportPattern} from '${relativeLoggerPath}';
|
|
838
|
+
|
|
839
|
+
const adapter = new AWSScheduledFunction(envParser, ${exportName});
|
|
840
|
+
|
|
841
|
+
export const handler = adapter.handler;
|
|
842
|
+
`;
|
|
843
|
+
await (0, node_fs_promises.writeFile)(handlerPath, content);
|
|
844
|
+
return handlerPath;
|
|
845
|
+
}
|
|
846
|
+
};
|
|
847
|
+
|
|
848
|
+
//#endregion
|
|
849
|
+
//#region src/generators/FunctionGenerator.ts
|
|
850
|
+
var FunctionGenerator = class extends require_openapi.ConstructGenerator {
|
|
851
|
+
isConstruct(value) {
|
|
852
|
+
return __geekmidas_constructs_functions.Function.isFunction(value);
|
|
853
|
+
}
|
|
854
|
+
async build(context, constructs, outputDir, options) {
|
|
855
|
+
const provider = options?.provider || "aws-lambda";
|
|
856
|
+
const logger$15 = console;
|
|
857
|
+
const functionInfos = [];
|
|
858
|
+
if (constructs.length === 0 || provider !== "aws-lambda") return functionInfos;
|
|
859
|
+
const functionsDir = (0, node_path.join)(outputDir, "functions");
|
|
860
|
+
await (0, node_fs_promises.mkdir)(functionsDir, { recursive: true });
|
|
861
|
+
for (const { key, construct, path } of constructs) {
|
|
862
|
+
const handlerFile = await this.generateFunctionHandler(functionsDir, path.relative, key, context);
|
|
863
|
+
functionInfos.push({
|
|
864
|
+
name: key,
|
|
865
|
+
handler: (0, node_path.relative)(process.cwd(), handlerFile).replace(/\.ts$/, ".handler"),
|
|
866
|
+
timeout: construct.timeout,
|
|
867
|
+
memorySize: construct.memorySize,
|
|
868
|
+
environment: await construct.getEnvironment()
|
|
869
|
+
});
|
|
870
|
+
logger$15.log(`Generated function handler: ${key}`);
|
|
871
|
+
}
|
|
872
|
+
return functionInfos;
|
|
873
|
+
}
|
|
874
|
+
async generateFunctionHandler(outputDir, sourceFile, exportName, context) {
|
|
875
|
+
const handlerFileName = `${exportName}.ts`;
|
|
876
|
+
const handlerPath = (0, node_path.join)(outputDir, handlerFileName);
|
|
877
|
+
const relativePath = (0, node_path.relative)((0, node_path.dirname)(handlerPath), sourceFile);
|
|
878
|
+
const importPath = relativePath.replace(/\.ts$/, ".js");
|
|
879
|
+
const relativeEnvParserPath = (0, node_path.relative)((0, node_path.dirname)(handlerPath), context.envParserPath);
|
|
880
|
+
const relativeLoggerPath = (0, node_path.relative)((0, node_path.dirname)(handlerPath), context.loggerPath);
|
|
881
|
+
const content = `import { AWSLambdaFunction } from '@geekmidas/constructs/aws';
|
|
882
|
+
import { ${exportName} } from '${importPath}';
|
|
883
|
+
import ${context.envParserImportPattern} from '${relativeEnvParserPath}';
|
|
884
|
+
import ${context.loggerImportPattern} from '${relativeLoggerPath}';
|
|
885
|
+
|
|
886
|
+
const adapter = new AWSLambdaFunction(envParser, ${exportName});
|
|
887
|
+
|
|
888
|
+
export const handler = adapter.handler;
|
|
889
|
+
`;
|
|
890
|
+
await (0, node_fs_promises.writeFile)(handlerPath, content);
|
|
891
|
+
return handlerPath;
|
|
892
|
+
}
|
|
893
|
+
};
|
|
894
|
+
|
|
895
|
+
//#endregion
|
|
896
|
+
//#region src/generators/SubscriberGenerator.ts
|
|
897
|
+
var SubscriberGenerator = class extends require_openapi.ConstructGenerator {
|
|
898
|
+
isConstruct(value) {
|
|
899
|
+
return __geekmidas_constructs_subscribers.Subscriber.isSubscriber(value);
|
|
900
|
+
}
|
|
901
|
+
async build(context, constructs, outputDir, options) {
|
|
902
|
+
const provider = options?.provider || "aws-lambda";
|
|
903
|
+
const logger$15 = console;
|
|
904
|
+
const subscriberInfos = [];
|
|
905
|
+
if (provider === "server") {
|
|
906
|
+
await this.generateServerSubscribersFile(outputDir, constructs);
|
|
907
|
+
logger$15.log(`Generated server subscribers file with ${constructs.length} subscribers (polling mode)`);
|
|
908
|
+
return subscriberInfos;
|
|
909
|
+
}
|
|
910
|
+
if (constructs.length === 0) return subscriberInfos;
|
|
911
|
+
if (provider !== "aws-lambda") return subscriberInfos;
|
|
912
|
+
const subscribersDir = (0, node_path.join)(outputDir, "subscribers");
|
|
913
|
+
await (0, node_fs_promises.mkdir)(subscribersDir, { recursive: true });
|
|
914
|
+
for (const { key, construct, path } of constructs) {
|
|
915
|
+
const handlerFile = await this.generateSubscriberHandler(subscribersDir, path.relative, key, construct, context);
|
|
916
|
+
subscriberInfos.push({
|
|
917
|
+
name: key,
|
|
918
|
+
handler: (0, node_path.relative)(process.cwd(), handlerFile).replace(/\.ts$/, ".handler"),
|
|
919
|
+
subscribedEvents: construct.subscribedEvents || [],
|
|
920
|
+
timeout: construct.timeout,
|
|
921
|
+
memorySize: construct.memorySize,
|
|
922
|
+
environment: await construct.getEnvironment()
|
|
923
|
+
});
|
|
924
|
+
logger$15.log(`Generated subscriber handler: ${key}`);
|
|
925
|
+
}
|
|
926
|
+
return subscriberInfos;
|
|
927
|
+
}
|
|
928
|
+
async generateSubscriberHandler(outputDir, sourceFile, exportName, _subscriber, context) {
|
|
474
929
|
const handlerFileName = `${exportName}.ts`;
|
|
475
930
|
const handlerPath = (0, node_path.join)(outputDir, handlerFileName);
|
|
476
931
|
const relativePath = (0, node_path.relative)((0, node_path.dirname)(handlerPath), sourceFile);
|
|
@@ -632,220 +1087,69 @@ export async function setupSubscribers(
|
|
|
632
1087
|
};
|
|
633
1088
|
|
|
634
1089
|
//#endregion
|
|
635
|
-
//#region src/
|
|
636
|
-
const logger$
|
|
637
|
-
/**
|
|
638
|
-
* Load environment files
|
|
639
|
-
* @internal Exported for testing
|
|
640
|
-
*/
|
|
641
|
-
function loadEnvFiles(envConfig, cwd = process.cwd()) {
|
|
642
|
-
const loaded = [];
|
|
643
|
-
const missing = [];
|
|
644
|
-
const envFiles = envConfig ? Array.isArray(envConfig) ? envConfig : [envConfig] : [".env"];
|
|
645
|
-
for (const envFile of envFiles) {
|
|
646
|
-
const envPath = (0, node_path.resolve)(cwd, envFile);
|
|
647
|
-
if ((0, node_fs.existsSync)(envPath)) {
|
|
648
|
-
(0, dotenv.config)({
|
|
649
|
-
path: envPath,
|
|
650
|
-
override: true,
|
|
651
|
-
quiet: true
|
|
652
|
-
});
|
|
653
|
-
loaded.push(envFile);
|
|
654
|
-
} else if (envConfig) missing.push(envFile);
|
|
655
|
-
}
|
|
656
|
-
return {
|
|
657
|
-
loaded,
|
|
658
|
-
missing
|
|
659
|
-
};
|
|
660
|
-
}
|
|
1090
|
+
//#region src/exec/index.ts
|
|
1091
|
+
const logger$12 = console;
|
|
661
1092
|
/**
|
|
662
|
-
*
|
|
663
|
-
*
|
|
1093
|
+
* Run a command with secrets injected into Credentials.
|
|
1094
|
+
* Uses Node's --import flag to preload a script that populates Credentials
|
|
1095
|
+
* before the command loads any modules that depend on them.
|
|
1096
|
+
*
|
|
1097
|
+
* @example
|
|
1098
|
+
* ```bash
|
|
1099
|
+
* gkm exec -- npx @better-auth/cli migrate
|
|
1100
|
+
* gkm exec -- npx prisma migrate dev
|
|
1101
|
+
* ```
|
|
664
1102
|
*/
|
|
665
|
-
async function
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
server.close();
|
|
674
|
-
resolve$3(true);
|
|
675
|
-
});
|
|
676
|
-
server.listen(port);
|
|
1103
|
+
async function execCommand(commandArgs, options = {}) {
|
|
1104
|
+
const cwd = options.cwd ?? process.cwd();
|
|
1105
|
+
if (commandArgs.length === 0) throw new Error("No command specified. Usage: gkm exec -- <command>");
|
|
1106
|
+
const defaultEnv = loadEnvFiles(".env");
|
|
1107
|
+
if (defaultEnv.loaded.length > 0) logger$12.log(`📦 Loaded env: ${defaultEnv.loaded.join(", ")}`);
|
|
1108
|
+
const { credentials, secretsJsonPath, appName } = await prepareEntryCredentials({
|
|
1109
|
+
cwd,
|
|
1110
|
+
resolveDockerPorts: "readonly"
|
|
677
1111
|
});
|
|
678
|
-
}
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
}
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
const
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
for (const [serviceName, serviceConfig] of Object.entries(compose.services)) for (const portMapping of serviceConfig?.ports ?? []) {
|
|
705
|
-
const match = String(portMapping).match(/\$\{(\w+):-(\d+)\}:(\d+)/);
|
|
706
|
-
if (match?.[1] && match[2] && match[3]) results.push({
|
|
707
|
-
service: serviceName,
|
|
708
|
-
envVar: match[1],
|
|
709
|
-
defaultPort: Number(match[2]),
|
|
710
|
-
containerPort: Number(match[3])
|
|
711
|
-
});
|
|
712
|
-
}
|
|
713
|
-
return results;
|
|
714
|
-
}
|
|
715
|
-
/**
|
|
716
|
-
* Load saved port state from .gkm/ports.json.
|
|
717
|
-
* @internal Exported for testing
|
|
718
|
-
*/
|
|
719
|
-
async function loadPortState(workspaceRoot) {
|
|
720
|
-
try {
|
|
721
|
-
const raw = await (0, node_fs_promises.readFile)((0, node_path.join)(workspaceRoot, PORT_STATE_PATH), "utf-8");
|
|
722
|
-
return JSON.parse(raw);
|
|
723
|
-
} catch {
|
|
724
|
-
return {};
|
|
725
|
-
}
|
|
726
|
-
}
|
|
727
|
-
/**
|
|
728
|
-
* Save port state to .gkm/ports.json.
|
|
729
|
-
* @internal Exported for testing
|
|
730
|
-
*/
|
|
731
|
-
async function savePortState(workspaceRoot, ports) {
|
|
732
|
-
const dir = (0, node_path.join)(workspaceRoot, ".gkm");
|
|
733
|
-
await (0, node_fs_promises.mkdir)(dir, { recursive: true });
|
|
734
|
-
await (0, node_fs_promises.writeFile)((0, node_path.join)(workspaceRoot, PORT_STATE_PATH), `${JSON.stringify(ports, null, 2)}\n`);
|
|
735
|
-
}
|
|
736
|
-
/**
|
|
737
|
-
* Check if a project's own Docker container is running and return its host port.
|
|
738
|
-
* Uses `docker compose port` scoped to the project's compose file.
|
|
739
|
-
* @internal Exported for testing
|
|
740
|
-
*/
|
|
741
|
-
function getContainerHostPort(workspaceRoot, service, containerPort) {
|
|
742
|
-
try {
|
|
743
|
-
const result = (0, node_child_process.execSync)(`docker compose port ${service} ${containerPort}`, {
|
|
744
|
-
cwd: workspaceRoot,
|
|
745
|
-
stdio: "pipe"
|
|
746
|
-
}).toString().trim();
|
|
747
|
-
const match = result.match(/:(\d+)$/);
|
|
748
|
-
return match ? Number(match[1]) : null;
|
|
749
|
-
} catch {
|
|
750
|
-
return null;
|
|
751
|
-
}
|
|
752
|
-
}
|
|
753
|
-
/**
|
|
754
|
-
* Resolve host ports for Docker services by parsing docker-compose.yml.
|
|
755
|
-
* Priority: running container → saved state → find available port.
|
|
756
|
-
* Persists resolved ports to .gkm/ports.json.
|
|
757
|
-
* @internal Exported for testing
|
|
758
|
-
*/
|
|
759
|
-
async function resolveServicePorts(workspaceRoot) {
|
|
760
|
-
const composePath = (0, node_path.join)(workspaceRoot, "docker-compose.yml");
|
|
761
|
-
const mappings = parseComposePortMappings(composePath);
|
|
762
|
-
if (mappings.length === 0) return {
|
|
763
|
-
dockerEnv: {},
|
|
764
|
-
ports: {},
|
|
765
|
-
mappings: []
|
|
766
|
-
};
|
|
767
|
-
const savedState = await loadPortState(workspaceRoot);
|
|
768
|
-
const dockerEnv = {};
|
|
769
|
-
const ports = {};
|
|
770
|
-
const assignedPorts = /* @__PURE__ */ new Set();
|
|
771
|
-
logger$11.log("\n🔌 Resolving service ports...");
|
|
772
|
-
for (const mapping of mappings) {
|
|
773
|
-
const containerPort = getContainerHostPort(workspaceRoot, mapping.service, mapping.containerPort);
|
|
774
|
-
if (containerPort !== null) {
|
|
775
|
-
ports[mapping.envVar] = containerPort;
|
|
776
|
-
dockerEnv[mapping.envVar] = String(containerPort);
|
|
777
|
-
assignedPorts.add(containerPort);
|
|
778
|
-
logger$11.log(` 🔄 ${mapping.service}:${mapping.containerPort}: reusing existing container on port ${containerPort}`);
|
|
779
|
-
continue;
|
|
780
|
-
}
|
|
781
|
-
const savedPort = savedState[mapping.envVar];
|
|
782
|
-
if (savedPort && !assignedPorts.has(savedPort) && await isPortAvailable(savedPort)) {
|
|
783
|
-
ports[mapping.envVar] = savedPort;
|
|
784
|
-
dockerEnv[mapping.envVar] = String(savedPort);
|
|
785
|
-
assignedPorts.add(savedPort);
|
|
786
|
-
logger$11.log(` 💾 ${mapping.service}:${mapping.containerPort}: using saved port ${savedPort}`);
|
|
787
|
-
continue;
|
|
1112
|
+
if (appName) logger$12.log(`📦 App: ${appName}`);
|
|
1113
|
+
const secretCount = Object.keys(credentials).filter((k) => k !== "PORT").length;
|
|
1114
|
+
if (secretCount > 0) logger$12.log(`🔐 Loaded ${secretCount} secret(s)`);
|
|
1115
|
+
const preloadDir = (0, node_path.join)(cwd, ".gkm");
|
|
1116
|
+
await (0, node_fs_promises.mkdir)(preloadDir, { recursive: true });
|
|
1117
|
+
const preloadPath = (0, node_path.join)(preloadDir, "credentials-preload.ts");
|
|
1118
|
+
await createCredentialsPreload(preloadPath, secretsJsonPath);
|
|
1119
|
+
const [cmd, ...rawArgs] = commandArgs;
|
|
1120
|
+
if (!cmd) throw new Error("No command specified");
|
|
1121
|
+
const args = rawArgs.map((arg) => arg.replace(/\$PORT\b/g, credentials.PORT ?? "3000"));
|
|
1122
|
+
logger$12.log(`🚀 Running: ${[cmd, ...args].join(" ")}`);
|
|
1123
|
+
const existingNodeOptions = process.env.NODE_OPTIONS ?? "";
|
|
1124
|
+
const tsxImport = "--import=tsx";
|
|
1125
|
+
const preloadImport = `--import=${preloadPath}`;
|
|
1126
|
+
const nodeOptions = [
|
|
1127
|
+
existingNodeOptions,
|
|
1128
|
+
tsxImport,
|
|
1129
|
+
preloadImport
|
|
1130
|
+
].filter(Boolean).join(" ");
|
|
1131
|
+
const child = (0, node_child_process.spawn)(cmd, args, {
|
|
1132
|
+
cwd,
|
|
1133
|
+
stdio: "inherit",
|
|
1134
|
+
env: {
|
|
1135
|
+
...process.env,
|
|
1136
|
+
...credentials,
|
|
1137
|
+
NODE_OPTIONS: nodeOptions
|
|
788
1138
|
}
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
}
|
|
797
|
-
|
|
798
|
-
return {
|
|
799
|
-
dockerEnv,
|
|
800
|
-
ports,
|
|
801
|
-
mappings
|
|
802
|
-
};
|
|
803
|
-
}
|
|
804
|
-
/**
|
|
805
|
-
* Replace a port in a URL string.
|
|
806
|
-
* Handles both `hostname:port` and `localhost:port` patterns.
|
|
807
|
-
* @internal Exported for testing
|
|
808
|
-
*/
|
|
809
|
-
function replacePortInUrl(url, oldPort, newPort) {
|
|
810
|
-
if (oldPort === newPort) return url;
|
|
811
|
-
return url.replace(new RegExp(`:${oldPort}(?=/|$)`, "g"), `:${newPort}`);
|
|
812
|
-
}
|
|
813
|
-
/**
|
|
814
|
-
* Rewrite connection URLs and port vars in secrets with resolved ports.
|
|
815
|
-
* Uses the parsed compose mappings to determine which default ports to replace.
|
|
816
|
-
* Pure transform — does not modify secrets on disk.
|
|
817
|
-
* @internal Exported for testing
|
|
818
|
-
*/
|
|
819
|
-
function rewriteUrlsWithPorts(secrets, resolvedPorts) {
|
|
820
|
-
const { ports, mappings } = resolvedPorts;
|
|
821
|
-
const result = { ...secrets };
|
|
822
|
-
const portReplacements = [];
|
|
823
|
-
const serviceNames = /* @__PURE__ */ new Set();
|
|
824
|
-
for (const mapping of mappings) {
|
|
825
|
-
serviceNames.add(mapping.service);
|
|
826
|
-
const resolved = ports[mapping.envVar];
|
|
827
|
-
if (resolved !== void 0) portReplacements.push({
|
|
828
|
-
defaultPort: mapping.defaultPort,
|
|
829
|
-
resolvedPort: resolved
|
|
830
|
-
});
|
|
831
|
-
}
|
|
832
|
-
for (const [key, value] of Object.entries(result)) {
|
|
833
|
-
if (!key.endsWith("_HOST")) continue;
|
|
834
|
-
if (serviceNames.has(value)) result[key] = "localhost";
|
|
835
|
-
}
|
|
836
|
-
for (const [key, value] of Object.entries(result)) {
|
|
837
|
-
if (!key.endsWith("_PORT")) continue;
|
|
838
|
-
for (const { defaultPort, resolvedPort } of portReplacements) if (value === String(defaultPort)) result[key] = String(resolvedPort);
|
|
839
|
-
}
|
|
840
|
-
for (const [key, value] of Object.entries(result)) {
|
|
841
|
-
if (!key.endsWith("_URL") && !key.endsWith("_ENDPOINT") && key !== "DATABASE_URL") continue;
|
|
842
|
-
let rewritten = value;
|
|
843
|
-
for (const name$1 of serviceNames) rewritten = rewritten.replace(new RegExp(`@${name$1}:`, "g"), "@localhost:");
|
|
844
|
-
for (const { defaultPort, resolvedPort } of portReplacements) rewritten = replacePortInUrl(rewritten, defaultPort, resolvedPort);
|
|
845
|
-
result[key] = rewritten;
|
|
846
|
-
}
|
|
847
|
-
return result;
|
|
1139
|
+
});
|
|
1140
|
+
const exitCode = await new Promise((resolve$4) => {
|
|
1141
|
+
child.on("close", (code) => resolve$4(code ?? 0));
|
|
1142
|
+
child.on("error", (error) => {
|
|
1143
|
+
logger$12.error(`Failed to run command: ${error.message}`);
|
|
1144
|
+
resolve$4(1);
|
|
1145
|
+
});
|
|
1146
|
+
});
|
|
1147
|
+
if (exitCode !== 0) process.exit(exitCode);
|
|
848
1148
|
}
|
|
1149
|
+
|
|
1150
|
+
//#endregion
|
|
1151
|
+
//#region src/dev/index.ts
|
|
1152
|
+
const logger$11 = console;
|
|
849
1153
|
/**
|
|
850
1154
|
* Normalize telescope configuration
|
|
851
1155
|
* @internal Exported for testing
|
|
@@ -1202,103 +1506,6 @@ async function loadDevSecrets(workspace) {
|
|
|
1202
1506
|
return {};
|
|
1203
1507
|
}
|
|
1204
1508
|
/**
|
|
1205
|
-
* Load secrets from a path for dev mode.
|
|
1206
|
-
* For single app: returns secrets as-is.
|
|
1207
|
-
* For workspace app: maps {APP}_DATABASE_URL → DATABASE_URL.
|
|
1208
|
-
* @internal Exported for testing
|
|
1209
|
-
*/
|
|
1210
|
-
async function loadSecretsForApp(secretsRoot, appName) {
|
|
1211
|
-
const stages = ["dev", "development"];
|
|
1212
|
-
let secrets = {};
|
|
1213
|
-
for (const stage of stages) if (require_storage.secretsExist(stage, secretsRoot)) {
|
|
1214
|
-
const stageSecrets = await require_storage.readStageSecrets(stage, secretsRoot);
|
|
1215
|
-
if (stageSecrets) {
|
|
1216
|
-
logger$11.log(`🔐 Loading secrets from stage: ${stage}`);
|
|
1217
|
-
secrets = require_storage.toEmbeddableSecrets(stageSecrets);
|
|
1218
|
-
break;
|
|
1219
|
-
}
|
|
1220
|
-
}
|
|
1221
|
-
if (Object.keys(secrets).length === 0) return {};
|
|
1222
|
-
if (!appName) return secrets;
|
|
1223
|
-
const prefix = appName.toUpperCase();
|
|
1224
|
-
const mapped = { ...secrets };
|
|
1225
|
-
const appDbUrl = secrets[`${prefix}_DATABASE_URL`];
|
|
1226
|
-
if (appDbUrl) mapped.DATABASE_URL = appDbUrl;
|
|
1227
|
-
return mapped;
|
|
1228
|
-
}
|
|
1229
|
-
/**
|
|
1230
|
-
* Build the environment variables to pass to `docker compose up`.
|
|
1231
|
-
* Merges process.env, secrets, and port mappings so that Docker Compose
|
|
1232
|
-
* can interpolate variables like ${POSTGRES_USER} correctly.
|
|
1233
|
-
* @internal Exported for testing
|
|
1234
|
-
*/
|
|
1235
|
-
function buildDockerComposeEnv(secretsEnv, portEnv) {
|
|
1236
|
-
return {
|
|
1237
|
-
...process.env,
|
|
1238
|
-
...secretsEnv,
|
|
1239
|
-
...portEnv
|
|
1240
|
-
};
|
|
1241
|
-
}
|
|
1242
|
-
/**
|
|
1243
|
-
* Parse all service names from a docker-compose.yml file.
|
|
1244
|
-
* @internal Exported for testing
|
|
1245
|
-
*/
|
|
1246
|
-
function parseComposeServiceNames(composePath) {
|
|
1247
|
-
if (!(0, node_fs.existsSync)(composePath)) return [];
|
|
1248
|
-
const content = (0, node_fs.readFileSync)(composePath, "utf-8");
|
|
1249
|
-
const compose = (0, yaml.parse)(content);
|
|
1250
|
-
return Object.keys(compose?.services ?? {});
|
|
1251
|
-
}
|
|
1252
|
-
/**
|
|
1253
|
-
* Start docker-compose services for the workspace.
|
|
1254
|
-
* Parses the docker-compose.yml to discover all services and starts
|
|
1255
|
-
* everything except app services (which are managed by turbo).
|
|
1256
|
-
* This ensures manually added services are always started.
|
|
1257
|
-
* @internal Exported for testing
|
|
1258
|
-
*/
|
|
1259
|
-
/**
|
|
1260
|
-
* Start docker-compose services for a single-app project (no workspace config).
|
|
1261
|
-
* Starts all services defined in docker-compose.yml.
|
|
1262
|
-
*/
|
|
1263
|
-
async function startComposeServices(cwd, portEnv, secretsEnv) {
|
|
1264
|
-
const composeFile = (0, node_path.join)(cwd, "docker-compose.yml");
|
|
1265
|
-
if (!(0, node_fs.existsSync)(composeFile)) return;
|
|
1266
|
-
const servicesToStart = parseComposeServiceNames(composeFile);
|
|
1267
|
-
if (servicesToStart.length === 0) return;
|
|
1268
|
-
logger$11.log(`🐳 Starting services: ${servicesToStart.join(", ")}`);
|
|
1269
|
-
try {
|
|
1270
|
-
(0, node_child_process.execSync)(`docker compose up -d ${servicesToStart.join(" ")}`, {
|
|
1271
|
-
cwd,
|
|
1272
|
-
stdio: "inherit",
|
|
1273
|
-
env: buildDockerComposeEnv(secretsEnv, portEnv)
|
|
1274
|
-
});
|
|
1275
|
-
logger$11.log("✅ Services started");
|
|
1276
|
-
} catch (error) {
|
|
1277
|
-
logger$11.error("❌ Failed to start services:", error.message);
|
|
1278
|
-
throw error;
|
|
1279
|
-
}
|
|
1280
|
-
}
|
|
1281
|
-
async function startWorkspaceServices(workspace, portEnv, secretsEnv) {
|
|
1282
|
-
const composeFile = (0, node_path.join)(workspace.root, "docker-compose.yml");
|
|
1283
|
-
if (!(0, node_fs.existsSync)(composeFile)) return;
|
|
1284
|
-
const allServices = parseComposeServiceNames(composeFile);
|
|
1285
|
-
const appNames = new Set(Object.keys(workspace.apps));
|
|
1286
|
-
const servicesToStart = allServices.filter((name$1) => !appNames.has(name$1));
|
|
1287
|
-
if (servicesToStart.length === 0) return;
|
|
1288
|
-
logger$11.log(`🐳 Starting services: ${servicesToStart.join(", ")}`);
|
|
1289
|
-
try {
|
|
1290
|
-
(0, node_child_process.execSync)(`docker compose up -d ${servicesToStart.join(" ")}`, {
|
|
1291
|
-
cwd: workspace.root,
|
|
1292
|
-
stdio: "inherit",
|
|
1293
|
-
env: buildDockerComposeEnv(secretsEnv, portEnv)
|
|
1294
|
-
});
|
|
1295
|
-
logger$11.log("✅ Services started");
|
|
1296
|
-
} catch (error) {
|
|
1297
|
-
logger$11.error("❌ Failed to start services:", error.message);
|
|
1298
|
-
throw error;
|
|
1299
|
-
}
|
|
1300
|
-
}
|
|
1301
|
-
/**
|
|
1302
1509
|
* Workspace dev command - orchestrates multi-app development using Turbo.
|
|
1303
1510
|
*
|
|
1304
1511
|
* Flow:
|
|
@@ -1465,7 +1672,7 @@ async function workspaceDevCommand(workspace, options) {
|
|
|
1465
1672
|
};
|
|
1466
1673
|
process.on("SIGINT", shutdown);
|
|
1467
1674
|
process.on("SIGTERM", shutdown);
|
|
1468
|
-
return new Promise((resolve$
|
|
1675
|
+
return new Promise((resolve$4, reject) => {
|
|
1469
1676
|
turboProcess.on("error", (error) => {
|
|
1470
1677
|
logger$11.error("❌ Turbo error:", error);
|
|
1471
1678
|
reject(error);
|
|
@@ -1473,7 +1680,7 @@ async function workspaceDevCommand(workspace, options) {
|
|
|
1473
1680
|
turboProcess.on("exit", (code) => {
|
|
1474
1681
|
if (openApiWatcher) openApiWatcher.close().catch(() => {});
|
|
1475
1682
|
if (code !== null && code !== 0) reject(new Error(`Turbo exited with code ${code}`));
|
|
1476
|
-
else resolve$
|
|
1683
|
+
else resolve$4();
|
|
1477
1684
|
});
|
|
1478
1685
|
});
|
|
1479
1686
|
}
|
|
@@ -1501,105 +1708,6 @@ async function buildServer(config, context, provider, enableOpenApi, appRoot = p
|
|
|
1501
1708
|
]);
|
|
1502
1709
|
}
|
|
1503
1710
|
/**
|
|
1504
|
-
* Find the directory containing .gkm/secrets/.
|
|
1505
|
-
* Walks up from cwd until it finds one, or returns cwd.
|
|
1506
|
-
* @internal Exported for testing
|
|
1507
|
-
*/
|
|
1508
|
-
function findSecretsRoot(startDir) {
|
|
1509
|
-
let dir = startDir;
|
|
1510
|
-
while (dir !== "/") {
|
|
1511
|
-
if ((0, node_fs.existsSync)((0, node_path.join)(dir, ".gkm", "secrets"))) return dir;
|
|
1512
|
-
const parent = (0, node_path.dirname)(dir);
|
|
1513
|
-
if (parent === dir) break;
|
|
1514
|
-
dir = parent;
|
|
1515
|
-
}
|
|
1516
|
-
return startDir;
|
|
1517
|
-
}
|
|
1518
|
-
/**
|
|
1519
|
-
* Generate the credentials injection code snippet.
|
|
1520
|
-
* This is the common logic used by both entry wrapper and exec preload.
|
|
1521
|
-
* @internal
|
|
1522
|
-
*/
|
|
1523
|
-
function generateCredentialsInjection(secretsJsonPath) {
|
|
1524
|
-
return `import { existsSync, readFileSync } from 'node:fs';
|
|
1525
|
-
|
|
1526
|
-
// Inject dev secrets via globalThis and process.env
|
|
1527
|
-
// Using globalThis.__gkm_credentials__ avoids CJS/ESM interop issues where
|
|
1528
|
-
// Object.assign on the Credentials export only mutates one module copy.
|
|
1529
|
-
const secretsPath = '${secretsJsonPath}';
|
|
1530
|
-
if (existsSync(secretsPath)) {
|
|
1531
|
-
const secrets = JSON.parse(readFileSync(secretsPath, 'utf-8'));
|
|
1532
|
-
globalThis.__gkm_credentials__ = secrets;
|
|
1533
|
-
Object.assign(process.env, secrets);
|
|
1534
|
-
}
|
|
1535
|
-
`;
|
|
1536
|
-
}
|
|
1537
|
-
/**
|
|
1538
|
-
* Create a preload script that injects secrets into Credentials.
|
|
1539
|
-
* Used by `gkm exec` to inject secrets before running any command.
|
|
1540
|
-
* @internal Exported for testing
|
|
1541
|
-
*/
|
|
1542
|
-
async function createCredentialsPreload(preloadPath, secretsJsonPath) {
|
|
1543
|
-
const content = `/**
|
|
1544
|
-
* Credentials preload generated by 'gkm exec'
|
|
1545
|
-
* This file is loaded via NODE_OPTIONS="--import <path>"
|
|
1546
|
-
*/
|
|
1547
|
-
${generateCredentialsInjection(secretsJsonPath)}`;
|
|
1548
|
-
await (0, node_fs_promises.writeFile)(preloadPath, content);
|
|
1549
|
-
}
|
|
1550
|
-
/**
|
|
1551
|
-
* Create a wrapper script that injects secrets before importing the entry file.
|
|
1552
|
-
* @internal Exported for testing
|
|
1553
|
-
*/
|
|
1554
|
-
async function createEntryWrapper(wrapperPath, entryPath, secretsJsonPath) {
|
|
1555
|
-
const credentialsInjection = secretsJsonPath ? `${generateCredentialsInjection(secretsJsonPath)}
|
|
1556
|
-
` : "";
|
|
1557
|
-
const content = `#!/usr/bin/env node
|
|
1558
|
-
/**
|
|
1559
|
-
* Entry wrapper generated by 'gkm dev --entry'
|
|
1560
|
-
*/
|
|
1561
|
-
${credentialsInjection}// Import and run the user's entry file (dynamic import ensures secrets load first)
|
|
1562
|
-
await import('${entryPath}');
|
|
1563
|
-
`;
|
|
1564
|
-
await (0, node_fs_promises.writeFile)(wrapperPath, content);
|
|
1565
|
-
}
|
|
1566
|
-
/**
|
|
1567
|
-
* Prepare credentials for entry dev mode.
|
|
1568
|
-
* Loads workspace config, secrets, and injects PORT.
|
|
1569
|
-
* @internal Exported for testing
|
|
1570
|
-
*/
|
|
1571
|
-
async function prepareEntryCredentials(options) {
|
|
1572
|
-
const cwd = options.cwd ?? process.cwd();
|
|
1573
|
-
let workspaceAppPort;
|
|
1574
|
-
let secretsRoot = cwd;
|
|
1575
|
-
let appName;
|
|
1576
|
-
try {
|
|
1577
|
-
const appInfo = await require_config.loadWorkspaceAppInfo(cwd);
|
|
1578
|
-
workspaceAppPort = appInfo.app.port;
|
|
1579
|
-
secretsRoot = appInfo.workspaceRoot;
|
|
1580
|
-
appName = appInfo.appName;
|
|
1581
|
-
} catch (error) {
|
|
1582
|
-
logger$11.log(`⚠️ Could not load workspace config: ${error.message}`);
|
|
1583
|
-
secretsRoot = findSecretsRoot(cwd);
|
|
1584
|
-
appName = require_config.getAppNameFromCwd(cwd) ?? void 0;
|
|
1585
|
-
}
|
|
1586
|
-
const resolvedPort = options.explicitPort ?? workspaceAppPort ?? 3e3;
|
|
1587
|
-
const credentials = await loadSecretsForApp(secretsRoot, appName);
|
|
1588
|
-
credentials.PORT = String(resolvedPort);
|
|
1589
|
-
const secretsDir = (0, node_path.join)(secretsRoot, ".gkm");
|
|
1590
|
-
await (0, node_fs_promises.mkdir)(secretsDir, { recursive: true });
|
|
1591
|
-
const secretsFileName = appName ? `dev-secrets-${appName}.json` : "dev-secrets.json";
|
|
1592
|
-
const secretsJsonPath = (0, node_path.join)(secretsDir, secretsFileName);
|
|
1593
|
-
await (0, node_fs_promises.writeFile)(secretsJsonPath, JSON.stringify(credentials, null, 2));
|
|
1594
|
-
return {
|
|
1595
|
-
credentials,
|
|
1596
|
-
resolvedPort,
|
|
1597
|
-
secretsJsonPath,
|
|
1598
|
-
appName,
|
|
1599
|
-
secretsRoot
|
|
1600
|
-
};
|
|
1601
|
-
}
|
|
1602
|
-
/**
|
|
1603
1711
|
* Run any TypeScript file with secret injection.
|
|
1604
1712
|
* Does not require gkm.config.ts.
|
|
1605
1713
|
*/
|
|
@@ -1684,12 +1792,12 @@ var EntryRunner = class {
|
|
|
1684
1792
|
if (code !== null && code !== 0 && code !== 143) logger$11.error(`❌ Process exited with code ${code}`);
|
|
1685
1793
|
this.isRunning = false;
|
|
1686
1794
|
});
|
|
1687
|
-
await new Promise((resolve$
|
|
1795
|
+
await new Promise((resolve$4) => setTimeout(resolve$4, 500));
|
|
1688
1796
|
if (this.isRunning) logger$11.log(`\n🎉 Running at http://localhost:${this.port}`);
|
|
1689
1797
|
}
|
|
1690
1798
|
async restart() {
|
|
1691
1799
|
this.stopProcess();
|
|
1692
|
-
await new Promise((resolve$
|
|
1800
|
+
await new Promise((resolve$4) => setTimeout(resolve$4, 500));
|
|
1693
1801
|
await this.runProcess();
|
|
1694
1802
|
}
|
|
1695
1803
|
stop() {
|
|
@@ -1822,7 +1930,7 @@ var DevServer = class {
|
|
|
1822
1930
|
if (code !== null && code !== 0 && signal !== "SIGTERM") logger$11.error(`❌ Server exited with code ${code}`);
|
|
1823
1931
|
this.isRunning = false;
|
|
1824
1932
|
});
|
|
1825
|
-
await new Promise((resolve$
|
|
1933
|
+
await new Promise((resolve$4) => setTimeout(resolve$4, 1e3));
|
|
1826
1934
|
if (this.isRunning) {
|
|
1827
1935
|
logger$11.log(`\n🎉 Server running at http://localhost:${this.actualPort}`);
|
|
1828
1936
|
if (this.enableOpenApi) logger$11.log(`📚 API Docs available at http://localhost:${this.actualPort}/__docs`);
|
|
@@ -1857,7 +1965,7 @@ var DevServer = class {
|
|
|
1857
1965
|
let attempts = 0;
|
|
1858
1966
|
while (attempts < 30) {
|
|
1859
1967
|
if (await isPortAvailable(portToReuse)) break;
|
|
1860
|
-
await new Promise((resolve$
|
|
1968
|
+
await new Promise((resolve$4) => setTimeout(resolve$4, 100));
|
|
1861
1969
|
attempts++;
|
|
1862
1970
|
}
|
|
1863
1971
|
this.requestedPort = portToReuse;
|
|
@@ -1874,81 +1982,6 @@ var DevServer = class {
|
|
|
1874
1982
|
await fsWriteFile(serverPath, content);
|
|
1875
1983
|
}
|
|
1876
1984
|
};
|
|
1877
|
-
/**
|
|
1878
|
-
* Run a command with secrets injected into Credentials.
|
|
1879
|
-
* Uses Node's --import flag to preload a script that populates Credentials
|
|
1880
|
-
* before the command loads any modules that depend on them.
|
|
1881
|
-
*
|
|
1882
|
-
* @example
|
|
1883
|
-
* ```bash
|
|
1884
|
-
* gkm exec -- npx @better-auth/cli migrate
|
|
1885
|
-
* gkm exec -- npx prisma migrate dev
|
|
1886
|
-
* ```
|
|
1887
|
-
*/
|
|
1888
|
-
async function execCommand(commandArgs, options = {}) {
|
|
1889
|
-
const cwd = options.cwd ?? process.cwd();
|
|
1890
|
-
if (commandArgs.length === 0) throw new Error("No command specified. Usage: gkm exec -- <command>");
|
|
1891
|
-
const defaultEnv = loadEnvFiles(".env");
|
|
1892
|
-
if (defaultEnv.loaded.length > 0) logger$11.log(`📦 Loaded env: ${defaultEnv.loaded.join(", ")}`);
|
|
1893
|
-
const { credentials, secretsJsonPath, appName, secretsRoot } = await prepareEntryCredentials({ cwd });
|
|
1894
|
-
if (appName) logger$11.log(`📦 App: ${appName}`);
|
|
1895
|
-
const secretCount = Object.keys(credentials).filter((k) => k !== "PORT").length;
|
|
1896
|
-
if (secretCount > 0) logger$11.log(`🔐 Loaded ${secretCount} secret(s)`);
|
|
1897
|
-
const composePath = (0, node_path.join)(secretsRoot, "docker-compose.yml");
|
|
1898
|
-
const mappings = parseComposePortMappings(composePath);
|
|
1899
|
-
if (mappings.length > 0) {
|
|
1900
|
-
const ports = await loadPortState(secretsRoot);
|
|
1901
|
-
if (Object.keys(ports).length > 0) {
|
|
1902
|
-
const rewritten = rewriteUrlsWithPorts(credentials, {
|
|
1903
|
-
dockerEnv: {},
|
|
1904
|
-
ports,
|
|
1905
|
-
mappings
|
|
1906
|
-
});
|
|
1907
|
-
Object.assign(credentials, rewritten);
|
|
1908
|
-
logger$11.log(`🔌 Applied ${Object.keys(ports).length} port mapping(s)`);
|
|
1909
|
-
}
|
|
1910
|
-
}
|
|
1911
|
-
try {
|
|
1912
|
-
const appInfo = await require_config.loadWorkspaceAppInfo(cwd);
|
|
1913
|
-
if (appInfo.appName) {
|
|
1914
|
-
const depEnv = require_workspace.getDependencyEnvVars(appInfo.workspace, appInfo.appName);
|
|
1915
|
-
Object.assign(credentials, depEnv);
|
|
1916
|
-
}
|
|
1917
|
-
} catch {}
|
|
1918
|
-
const preloadDir = (0, node_path.join)(cwd, ".gkm");
|
|
1919
|
-
await (0, node_fs_promises.mkdir)(preloadDir, { recursive: true });
|
|
1920
|
-
const preloadPath = (0, node_path.join)(preloadDir, "credentials-preload.ts");
|
|
1921
|
-
await createCredentialsPreload(preloadPath, secretsJsonPath);
|
|
1922
|
-
const [cmd, ...rawArgs] = commandArgs;
|
|
1923
|
-
if (!cmd) throw new Error("No command specified");
|
|
1924
|
-
const args = rawArgs.map((arg) => arg.replace(/\$PORT\b/g, credentials.PORT ?? "3000"));
|
|
1925
|
-
logger$11.log(`🚀 Running: ${[cmd, ...args].join(" ")}`);
|
|
1926
|
-
const existingNodeOptions = process.env.NODE_OPTIONS ?? "";
|
|
1927
|
-
const tsxImport = "--import=tsx";
|
|
1928
|
-
const preloadImport = `--import=${preloadPath}`;
|
|
1929
|
-
const nodeOptions = [
|
|
1930
|
-
existingNodeOptions,
|
|
1931
|
-
tsxImport,
|
|
1932
|
-
preloadImport
|
|
1933
|
-
].filter(Boolean).join(" ");
|
|
1934
|
-
const child = (0, node_child_process.spawn)(cmd, args, {
|
|
1935
|
-
cwd,
|
|
1936
|
-
stdio: "inherit",
|
|
1937
|
-
env: {
|
|
1938
|
-
...process.env,
|
|
1939
|
-
...credentials,
|
|
1940
|
-
NODE_OPTIONS: nodeOptions
|
|
1941
|
-
}
|
|
1942
|
-
});
|
|
1943
|
-
const exitCode = await new Promise((resolve$3) => {
|
|
1944
|
-
child.on("close", (code) => resolve$3(code ?? 0));
|
|
1945
|
-
child.on("error", (error) => {
|
|
1946
|
-
logger$11.error(`Failed to run command: ${error.message}`);
|
|
1947
|
-
resolve$3(1);
|
|
1948
|
-
});
|
|
1949
|
-
});
|
|
1950
|
-
if (exitCode !== 0) process.exit(exitCode);
|
|
1951
|
-
}
|
|
1952
1985
|
|
|
1953
1986
|
//#endregion
|
|
1954
1987
|
//#region src/build/manifests.ts
|
|
@@ -2220,7 +2253,7 @@ async function buildForProvider(provider, context, rootOutputDir, endpointGenera
|
|
|
2220
2253
|
let masterKey;
|
|
2221
2254
|
if (context.production?.bundle && !skipBundle) {
|
|
2222
2255
|
logger$9.log(`\n📦 Bundling production server...`);
|
|
2223
|
-
const { bundleServer } = await Promise.resolve().then(() => require("./bundler-
|
|
2256
|
+
const { bundleServer } = await Promise.resolve().then(() => require("./bundler-i-az1DZ2.cjs"));
|
|
2224
2257
|
const allConstructs = [
|
|
2225
2258
|
...endpoints.map((e) => e.construct),
|
|
2226
2259
|
...functions.map((f) => f.construct),
|
|
@@ -2290,7 +2323,7 @@ async function workspaceBuildCommand(workspace, options) {
|
|
|
2290
2323
|
try {
|
|
2291
2324
|
const turboCommand = getTurboCommand(pm);
|
|
2292
2325
|
logger$9.log(`Running: ${turboCommand}`);
|
|
2293
|
-
await new Promise((resolve$
|
|
2326
|
+
await new Promise((resolve$4, reject) => {
|
|
2294
2327
|
const child = (0, node_child_process.spawn)(turboCommand, {
|
|
2295
2328
|
shell: true,
|
|
2296
2329
|
cwd: workspace.root,
|
|
@@ -2301,7 +2334,7 @@ async function workspaceBuildCommand(workspace, options) {
|
|
|
2301
2334
|
}
|
|
2302
2335
|
});
|
|
2303
2336
|
child.on("close", (code) => {
|
|
2304
|
-
if (code === 0) resolve$
|
|
2337
|
+
if (code === 0) resolve$4();
|
|
2305
2338
|
else reject(new Error(`Turbo build failed with exit code ${code}`));
|
|
2306
2339
|
});
|
|
2307
2340
|
child.on("error", (err) => {
|
|
@@ -5444,7 +5477,7 @@ async function prompt(message, hidden = false) {
|
|
|
5444
5477
|
if (!process.stdin.isTTY) throw new Error("Interactive input required. Please configure manually.");
|
|
5445
5478
|
if (hidden) {
|
|
5446
5479
|
process.stdout.write(message);
|
|
5447
|
-
return new Promise((resolve$
|
|
5480
|
+
return new Promise((resolve$4) => {
|
|
5448
5481
|
let value = "";
|
|
5449
5482
|
const onData = (char) => {
|
|
5450
5483
|
const c = char.toString();
|
|
@@ -5453,7 +5486,7 @@ async function prompt(message, hidden = false) {
|
|
|
5453
5486
|
process.stdin.pause();
|
|
5454
5487
|
process.stdin.removeListener("data", onData);
|
|
5455
5488
|
process.stdout.write("\n");
|
|
5456
|
-
resolve$
|
|
5489
|
+
resolve$4(value);
|
|
5457
5490
|
} else if (c === "") {
|
|
5458
5491
|
process.stdin.setRawMode(false);
|
|
5459
5492
|
process.stdin.pause();
|
|
@@ -6471,7 +6504,7 @@ async function deployCommand(options) {
|
|
|
6471
6504
|
dokployConfig = setupResult.config;
|
|
6472
6505
|
finalRegistry = dokployConfig.registry ?? dockerConfig.registry;
|
|
6473
6506
|
if (setupResult.serviceUrls) {
|
|
6474
|
-
const { readStageSecrets: readStageSecrets$1, writeStageSecrets: writeStageSecrets$1, initStageSecrets } = await Promise.resolve().then(() => require("./storage-
|
|
6507
|
+
const { readStageSecrets: readStageSecrets$1, writeStageSecrets: writeStageSecrets$1, initStageSecrets } = await Promise.resolve().then(() => require("./storage-ChVQI_G7.cjs"));
|
|
6475
6508
|
let secrets = await readStageSecrets$1(stage);
|
|
6476
6509
|
if (!secrets) {
|
|
6477
6510
|
logger$3.log(` Creating secrets file for stage "${stage}"...`);
|
|
@@ -6758,7 +6791,7 @@ const GEEKMIDAS_VERSIONS = {
|
|
|
6758
6791
|
"@geekmidas/constructs": "~3.0.2",
|
|
6759
6792
|
"@geekmidas/db": "~1.0.0",
|
|
6760
6793
|
"@geekmidas/emailkit": "~1.0.0",
|
|
6761
|
-
"@geekmidas/envkit": "~1.0.
|
|
6794
|
+
"@geekmidas/envkit": "~1.0.4",
|
|
6762
6795
|
"@geekmidas/errors": "~1.0.0",
|
|
6763
6796
|
"@geekmidas/events": "~1.1.0",
|
|
6764
6797
|
"@geekmidas/logger": "~1.0.0",
|
|
@@ -11881,75 +11914,29 @@ async function testCommand(options = {}) {
|
|
|
11881
11914
|
console.log(`\n🧪 Running tests with ${stage} environment...\n`);
|
|
11882
11915
|
const defaultEnv = loadEnvFiles(".env");
|
|
11883
11916
|
if (defaultEnv.loaded.length > 0) console.log(` 📦 Loaded env: ${defaultEnv.loaded.join(", ")}`);
|
|
11884
|
-
|
|
11885
|
-
|
|
11886
|
-
|
|
11887
|
-
|
|
11888
|
-
|
|
11889
|
-
|
|
11890
|
-
|
|
11891
|
-
|
|
11892
|
-
|
|
11893
|
-
else throw error;
|
|
11894
|
-
}
|
|
11895
|
-
let dependencyEnv = {};
|
|
11896
|
-
try {
|
|
11897
|
-
const appInfo = await require_config.loadWorkspaceAppInfo(cwd);
|
|
11898
|
-
const resolvedPorts = await resolveServicePorts(appInfo.workspaceRoot);
|
|
11899
|
-
await startWorkspaceServices(appInfo.workspace, resolvedPorts.dockerEnv, secretsEnv);
|
|
11900
|
-
if (resolvedPorts.mappings.length > 0) {
|
|
11901
|
-
secretsEnv = rewriteUrlsWithPorts(secretsEnv, resolvedPorts);
|
|
11902
|
-
console.log(` 🔌 Applied ${Object.keys(resolvedPorts.ports).length} port mapping(s)`);
|
|
11903
|
-
}
|
|
11904
|
-
dependencyEnv = require_workspace.getDependencyEnvVars(appInfo.workspace, appInfo.appName);
|
|
11905
|
-
if (Object.keys(dependencyEnv).length > 0) console.log(` 🔗 Loaded ${Object.keys(dependencyEnv).length} dependency URL(s)`);
|
|
11906
|
-
const sniffed = await sniffAppEnvironment(appInfo.app, appInfo.appName, appInfo.workspaceRoot, { logWarnings: false });
|
|
11917
|
+
const result = await prepareEntryCredentials({
|
|
11918
|
+
stages: [stage],
|
|
11919
|
+
startDocker: true,
|
|
11920
|
+
secretsFileName: "test-secrets.json",
|
|
11921
|
+
resolveDockerPorts: "full"
|
|
11922
|
+
});
|
|
11923
|
+
let finalCredentials = { ...result.credentials };
|
|
11924
|
+
if (result.appInfo) {
|
|
11925
|
+
const sniffed = await sniffAppEnvironment(result.appInfo.app, result.appInfo.appName, result.appInfo.workspaceRoot, { logWarnings: false });
|
|
11907
11926
|
if (sniffed.requiredEnvVars.length > 0) {
|
|
11908
11927
|
const needed = new Set(sniffed.requiredEnvVars);
|
|
11909
|
-
const
|
|
11910
|
-
|
|
11911
|
-
|
|
11912
|
-
};
|
|
11913
|
-
const filteredEnv = {};
|
|
11914
|
-
for (const [key, value] of Object.entries(allEnv)) if (needed.has(key)) filteredEnv[key] = value;
|
|
11915
|
-
secretsEnv = {};
|
|
11916
|
-
dependencyEnv = filteredEnv;
|
|
11928
|
+
const filtered = {};
|
|
11929
|
+
for (const [key, value] of Object.entries(finalCredentials)) if (needed.has(key)) filtered[key] = value;
|
|
11930
|
+
finalCredentials = filtered;
|
|
11917
11931
|
console.log(` 🔍 Sniffed ${sniffed.requiredEnvVars.length} required env var(s)`);
|
|
11918
11932
|
}
|
|
11919
|
-
} catch {
|
|
11920
|
-
const composePath = (0, node_path.join)(cwd, "docker-compose.yml");
|
|
11921
|
-
const mappings = parseComposePortMappings(composePath);
|
|
11922
|
-
if (mappings.length > 0) {
|
|
11923
|
-
const resolvedPorts = await resolveServicePorts(cwd);
|
|
11924
|
-
await startComposeServices(cwd, resolvedPorts.dockerEnv, secretsEnv);
|
|
11925
|
-
if (resolvedPorts.mappings.length > 0) {
|
|
11926
|
-
secretsEnv = rewriteUrlsWithPorts(secretsEnv, resolvedPorts);
|
|
11927
|
-
console.log(` 🔌 Applied ${Object.keys(resolvedPorts.ports).length} port mapping(s)`);
|
|
11928
|
-
} else {
|
|
11929
|
-
const ports = await loadPortState(cwd);
|
|
11930
|
-
if (Object.keys(ports).length > 0) {
|
|
11931
|
-
secretsEnv = rewriteUrlsWithPorts(secretsEnv, {
|
|
11932
|
-
dockerEnv: {},
|
|
11933
|
-
ports,
|
|
11934
|
-
mappings
|
|
11935
|
-
});
|
|
11936
|
-
console.log(` 🔌 Applied ${Object.keys(ports).length} port mapping(s)`);
|
|
11937
|
-
}
|
|
11938
|
-
}
|
|
11939
|
-
}
|
|
11940
11933
|
}
|
|
11941
|
-
|
|
11934
|
+
finalCredentials = rewriteDatabaseUrlForTests(finalCredentials);
|
|
11942
11935
|
console.log("");
|
|
11943
|
-
|
|
11944
|
-
...secretsEnv,
|
|
11945
|
-
...dependencyEnv
|
|
11946
|
-
};
|
|
11936
|
+
await (0, node_fs_promises.writeFile)(result.secretsJsonPath, JSON.stringify(finalCredentials, null, 2));
|
|
11947
11937
|
const gkmDir = (0, node_path.join)(cwd, ".gkm");
|
|
11948
|
-
await (0, node_fs_promises.mkdir)(gkmDir, { recursive: true });
|
|
11949
|
-
const secretsJsonPath = (0, node_path.join)(gkmDir, "test-secrets.json");
|
|
11950
|
-
await (0, node_fs_promises.writeFile)(secretsJsonPath, JSON.stringify(allSecrets, null, 2));
|
|
11951
11938
|
const preloadPath = (0, node_path.join)(gkmDir, "test-credentials-preload.ts");
|
|
11952
|
-
await createCredentialsPreload(preloadPath, secretsJsonPath);
|
|
11939
|
+
await createCredentialsPreload(preloadPath, result.secretsJsonPath);
|
|
11953
11940
|
const existingNodeOptions = process.env.NODE_OPTIONS ?? "";
|
|
11954
11941
|
const tsxImport = "--import=tsx";
|
|
11955
11942
|
const preloadImport = `--import=${preloadPath}`;
|
|
@@ -11969,14 +11956,14 @@ async function testCommand(options = {}) {
|
|
|
11969
11956
|
stdio: "inherit",
|
|
11970
11957
|
env: {
|
|
11971
11958
|
...process.env,
|
|
11972
|
-
...
|
|
11959
|
+
...finalCredentials,
|
|
11973
11960
|
NODE_ENV: "test",
|
|
11974
11961
|
NODE_OPTIONS: nodeOptions
|
|
11975
11962
|
}
|
|
11976
11963
|
});
|
|
11977
|
-
return new Promise((resolve$
|
|
11964
|
+
return new Promise((resolve$4, reject) => {
|
|
11978
11965
|
vitestProcess.on("close", (code) => {
|
|
11979
|
-
if (code === 0) resolve$
|
|
11966
|
+
if (code === 0) resolve$4();
|
|
11980
11967
|
else reject(new Error(`Tests failed with exit code ${code}`));
|
|
11981
11968
|
});
|
|
11982
11969
|
vitestProcess.on("error", (error) => {
|
|
@@ -12373,9 +12360,9 @@ program.command("secrets:push").description("Push secrets to remote provider (SS
|
|
|
12373
12360
|
const globalOptions = program.opts();
|
|
12374
12361
|
if (globalOptions.cwd) process.chdir(globalOptions.cwd);
|
|
12375
12362
|
const { loadWorkspaceConfig: loadWorkspaceConfig$1 } = await Promise.resolve().then(() => require("./config.cjs"));
|
|
12376
|
-
const { pushSecrets: pushSecrets$1 } = await Promise.resolve().then(() => require("./sync-
|
|
12363
|
+
const { pushSecrets: pushSecrets$1 } = await Promise.resolve().then(() => require("./sync-ByaRPBxh.cjs"));
|
|
12377
12364
|
const { reconcileMissingSecrets } = await Promise.resolve().then(() => require("./reconcile-Ch7sIcf8.cjs"));
|
|
12378
|
-
const { readStageSecrets: readStageSecrets$1, writeStageSecrets: writeStageSecrets$1 } = await Promise.resolve().then(() => require("./storage-
|
|
12365
|
+
const { readStageSecrets: readStageSecrets$1, writeStageSecrets: writeStageSecrets$1 } = await Promise.resolve().then(() => require("./storage-ChVQI_G7.cjs"));
|
|
12379
12366
|
const { workspace } = await loadWorkspaceConfig$1();
|
|
12380
12367
|
const secrets = await readStageSecrets$1(options.stage, workspace.root);
|
|
12381
12368
|
if (secrets) {
|
|
@@ -12398,8 +12385,8 @@ program.command("secrets:pull").description("Pull secrets from remote provider (
|
|
|
12398
12385
|
const globalOptions = program.opts();
|
|
12399
12386
|
if (globalOptions.cwd) process.chdir(globalOptions.cwd);
|
|
12400
12387
|
const { loadWorkspaceConfig: loadWorkspaceConfig$1 } = await Promise.resolve().then(() => require("./config.cjs"));
|
|
12401
|
-
const { pullSecrets: pullSecrets$1 } = await Promise.resolve().then(() => require("./sync-
|
|
12402
|
-
const { writeStageSecrets: writeStageSecrets$1 } = await Promise.resolve().then(() => require("./storage-
|
|
12388
|
+
const { pullSecrets: pullSecrets$1 } = await Promise.resolve().then(() => require("./sync-ByaRPBxh.cjs"));
|
|
12389
|
+
const { writeStageSecrets: writeStageSecrets$1 } = await Promise.resolve().then(() => require("./storage-ChVQI_G7.cjs"));
|
|
12403
12390
|
const { reconcileMissingSecrets } = await Promise.resolve().then(() => require("./reconcile-Ch7sIcf8.cjs"));
|
|
12404
12391
|
const { workspace } = await loadWorkspaceConfig$1();
|
|
12405
12392
|
let secrets = await pullSecrets$1(options.stage, workspace);
|
|
@@ -12426,7 +12413,7 @@ program.command("secrets:reconcile").description("Backfill missing custom secret
|
|
|
12426
12413
|
if (globalOptions.cwd) process.chdir(globalOptions.cwd);
|
|
12427
12414
|
const { loadWorkspaceConfig: loadWorkspaceConfig$1 } = await Promise.resolve().then(() => require("./config.cjs"));
|
|
12428
12415
|
const { reconcileMissingSecrets } = await Promise.resolve().then(() => require("./reconcile-Ch7sIcf8.cjs"));
|
|
12429
|
-
const { readStageSecrets: readStageSecrets$1, writeStageSecrets: writeStageSecrets$1 } = await Promise.resolve().then(() => require("./storage-
|
|
12416
|
+
const { readStageSecrets: readStageSecrets$1, writeStageSecrets: writeStageSecrets$1 } = await Promise.resolve().then(() => require("./storage-ChVQI_G7.cjs"));
|
|
12430
12417
|
const { workspace } = await loadWorkspaceConfig$1();
|
|
12431
12418
|
const secrets = await readStageSecrets$1(options.stage, workspace.root);
|
|
12432
12419
|
if (!secrets) {
|