@geekmidas/cli 1.10.17 → 1.10.19
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 +701 -707
- package/dist/index.cjs.map +1 -1
- package/dist/index.mjs +689 -695
- 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 +2 -2
- 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/index.ts +48 -830
- package/src/exec/index.ts +120 -0
- 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.18";
|
|
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,222 +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
|
});
|
|
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
|
|
1138
|
+
}
|
|
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);
|
|
678
1148
|
}
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
async function findAvailablePort(preferredPort, maxAttempts = 10) {
|
|
684
|
-
for (let i = 0; i < maxAttempts; i++) {
|
|
685
|
-
const port = preferredPort + i;
|
|
686
|
-
if (await isPortAvailable(port)) return port;
|
|
687
|
-
logger$11.log(`⚠️ Port ${port} is in use, trying ${port + 1}...`);
|
|
688
|
-
}
|
|
689
|
-
throw new Error(`Could not find an available port after trying ${maxAttempts} ports starting from ${preferredPort}`);
|
|
690
|
-
}
|
|
691
|
-
const PORT_STATE_PATH = ".gkm/ports.json";
|
|
692
|
-
/**
|
|
693
|
-
* Parse docker-compose.yml and extract all port mappings that use env var interpolation.
|
|
694
|
-
* Entries like `'${POSTGRES_HOST_PORT:-5432}:5432'` are captured.
|
|
695
|
-
* Fixed port mappings like `'5050:80'` are skipped.
|
|
696
|
-
* @internal Exported for testing
|
|
697
|
-
*/
|
|
698
|
-
function parseComposePortMappings(composePath) {
|
|
699
|
-
if (!(0, node_fs.existsSync)(composePath)) return [];
|
|
700
|
-
const content = (0, node_fs.readFileSync)(composePath, "utf-8");
|
|
701
|
-
const compose = (0, yaml.parse)(content);
|
|
702
|
-
if (!compose?.services) return [];
|
|
703
|
-
const results = [];
|
|
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;
|
|
788
|
-
}
|
|
789
|
-
let resolvedPort = await findAvailablePort(mapping.defaultPort);
|
|
790
|
-
while (assignedPorts.has(resolvedPort)) resolvedPort = await findAvailablePort(resolvedPort + 1);
|
|
791
|
-
ports[mapping.envVar] = resolvedPort;
|
|
792
|
-
dockerEnv[mapping.envVar] = String(resolvedPort);
|
|
793
|
-
assignedPorts.add(resolvedPort);
|
|
794
|
-
if (resolvedPort !== mapping.defaultPort) logger$11.log(` ⚡ ${mapping.service}:${mapping.containerPort}: port ${mapping.defaultPort} occupied, using port ${resolvedPort}`);
|
|
795
|
-
else logger$11.log(` ✅ ${mapping.service}:${mapping.containerPort}: using default port ${resolvedPort}`);
|
|
796
|
-
}
|
|
797
|
-
await savePortState(workspaceRoot, ports);
|
|
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
|
-
let result = url.replace(new RegExp(`:${oldPort}(?=[/?#]|$)`, "g"), `:${newPort}`);
|
|
812
|
-
result = result.replace(new RegExp(`%3A${oldPort}(?=[%/?#&]|$)`, "gi"), `%3A${newPort}`);
|
|
813
|
-
return result;
|
|
814
|
-
}
|
|
815
|
-
/**
|
|
816
|
-
* Rewrite connection URLs and port vars in secrets with resolved ports.
|
|
817
|
-
* Uses the parsed compose mappings to determine which default ports to replace.
|
|
818
|
-
* Pure transform — does not modify secrets on disk.
|
|
819
|
-
* @internal Exported for testing
|
|
820
|
-
*/
|
|
821
|
-
function rewriteUrlsWithPorts(secrets, resolvedPorts) {
|
|
822
|
-
const { ports, mappings } = resolvedPorts;
|
|
823
|
-
const result = { ...secrets };
|
|
824
|
-
const portReplacements = [];
|
|
825
|
-
const serviceNames = /* @__PURE__ */ new Set();
|
|
826
|
-
for (const mapping of mappings) {
|
|
827
|
-
serviceNames.add(mapping.service);
|
|
828
|
-
const resolved = ports[mapping.envVar];
|
|
829
|
-
if (resolved !== void 0) portReplacements.push({
|
|
830
|
-
defaultPort: mapping.defaultPort,
|
|
831
|
-
resolvedPort: resolved
|
|
832
|
-
});
|
|
833
|
-
}
|
|
834
|
-
for (const [key, value] of Object.entries(result)) {
|
|
835
|
-
if (!key.endsWith("_HOST")) continue;
|
|
836
|
-
if (serviceNames.has(value)) result[key] = "localhost";
|
|
837
|
-
}
|
|
838
|
-
for (const [key, value] of Object.entries(result)) {
|
|
839
|
-
if (!key.endsWith("_PORT")) continue;
|
|
840
|
-
for (const { defaultPort, resolvedPort } of portReplacements) if (value === String(defaultPort)) result[key] = String(resolvedPort);
|
|
841
|
-
}
|
|
842
|
-
for (const [key, value] of Object.entries(result)) {
|
|
843
|
-
if (!key.endsWith("_URL") && !key.endsWith("_ENDPOINT") && !key.endsWith("_CONNECTION_STRING") && key !== "DATABASE_URL") continue;
|
|
844
|
-
let rewritten = value;
|
|
845
|
-
for (const name$1 of serviceNames) rewritten = rewritten.replace(new RegExp(`@${name$1}:`, "g"), "@localhost:");
|
|
846
|
-
for (const { defaultPort, resolvedPort } of portReplacements) rewritten = replacePortInUrl(rewritten, defaultPort, resolvedPort);
|
|
847
|
-
result[key] = rewritten;
|
|
848
|
-
}
|
|
849
|
-
return result;
|
|
850
|
-
}
|
|
1149
|
+
|
|
1150
|
+
//#endregion
|
|
1151
|
+
//#region src/dev/index.ts
|
|
1152
|
+
const logger$11 = console;
|
|
851
1153
|
/**
|
|
852
1154
|
* Normalize telescope configuration
|
|
853
1155
|
* @internal Exported for testing
|
|
@@ -1025,7 +1327,8 @@ async function devCommand(options) {
|
|
|
1025
1327
|
if (Object.keys(appSecrets).length > 0) {
|
|
1026
1328
|
const secretsDir = (0, node_path.join)(secretsRoot, ".gkm");
|
|
1027
1329
|
await (0, node_fs_promises.mkdir)(secretsDir, { recursive: true });
|
|
1028
|
-
|
|
1330
|
+
const secretsFileName = workspaceAppName ? `dev-secrets-${workspaceAppName}.json` : "dev-secrets.json";
|
|
1331
|
+
secretsJsonPath = (0, node_path.join)(secretsDir, secretsFileName);
|
|
1029
1332
|
await (0, node_fs_promises.writeFile)(secretsJsonPath, JSON.stringify(appSecrets, null, 2));
|
|
1030
1333
|
logger$11.log(`🔐 Loaded ${Object.keys(appSecrets).length} secret(s)`);
|
|
1031
1334
|
}
|
|
@@ -1204,103 +1507,6 @@ async function loadDevSecrets(workspace) {
|
|
|
1204
1507
|
return {};
|
|
1205
1508
|
}
|
|
1206
1509
|
/**
|
|
1207
|
-
* Load secrets from a path for dev mode.
|
|
1208
|
-
* For single app: returns secrets as-is.
|
|
1209
|
-
* For workspace app: maps {APP}_DATABASE_URL → DATABASE_URL.
|
|
1210
|
-
* @internal Exported for testing
|
|
1211
|
-
*/
|
|
1212
|
-
async function loadSecretsForApp(secretsRoot, appName) {
|
|
1213
|
-
const stages = ["dev", "development"];
|
|
1214
|
-
let secrets = {};
|
|
1215
|
-
for (const stage of stages) if (require_storage.secretsExist(stage, secretsRoot)) {
|
|
1216
|
-
const stageSecrets = await require_storage.readStageSecrets(stage, secretsRoot);
|
|
1217
|
-
if (stageSecrets) {
|
|
1218
|
-
logger$11.log(`🔐 Loading secrets from stage: ${stage}`);
|
|
1219
|
-
secrets = require_storage.toEmbeddableSecrets(stageSecrets);
|
|
1220
|
-
break;
|
|
1221
|
-
}
|
|
1222
|
-
}
|
|
1223
|
-
if (Object.keys(secrets).length === 0) return {};
|
|
1224
|
-
if (!appName) return secrets;
|
|
1225
|
-
const prefix = appName.toUpperCase();
|
|
1226
|
-
const mapped = { ...secrets };
|
|
1227
|
-
const appDbUrl = secrets[`${prefix}_DATABASE_URL`];
|
|
1228
|
-
if (appDbUrl) mapped.DATABASE_URL = appDbUrl;
|
|
1229
|
-
return mapped;
|
|
1230
|
-
}
|
|
1231
|
-
/**
|
|
1232
|
-
* Build the environment variables to pass to `docker compose up`.
|
|
1233
|
-
* Merges process.env, secrets, and port mappings so that Docker Compose
|
|
1234
|
-
* can interpolate variables like ${POSTGRES_USER} correctly.
|
|
1235
|
-
* @internal Exported for testing
|
|
1236
|
-
*/
|
|
1237
|
-
function buildDockerComposeEnv(secretsEnv, portEnv) {
|
|
1238
|
-
return {
|
|
1239
|
-
...process.env,
|
|
1240
|
-
...secretsEnv,
|
|
1241
|
-
...portEnv
|
|
1242
|
-
};
|
|
1243
|
-
}
|
|
1244
|
-
/**
|
|
1245
|
-
* Parse all service names from a docker-compose.yml file.
|
|
1246
|
-
* @internal Exported for testing
|
|
1247
|
-
*/
|
|
1248
|
-
function parseComposeServiceNames(composePath) {
|
|
1249
|
-
if (!(0, node_fs.existsSync)(composePath)) return [];
|
|
1250
|
-
const content = (0, node_fs.readFileSync)(composePath, "utf-8");
|
|
1251
|
-
const compose = (0, yaml.parse)(content);
|
|
1252
|
-
return Object.keys(compose?.services ?? {});
|
|
1253
|
-
}
|
|
1254
|
-
/**
|
|
1255
|
-
* Start docker-compose services for the workspace.
|
|
1256
|
-
* Parses the docker-compose.yml to discover all services and starts
|
|
1257
|
-
* everything except app services (which are managed by turbo).
|
|
1258
|
-
* This ensures manually added services are always started.
|
|
1259
|
-
* @internal Exported for testing
|
|
1260
|
-
*/
|
|
1261
|
-
/**
|
|
1262
|
-
* Start docker-compose services for a single-app project (no workspace config).
|
|
1263
|
-
* Starts all services defined in docker-compose.yml.
|
|
1264
|
-
*/
|
|
1265
|
-
async function startComposeServices(cwd, portEnv, secretsEnv) {
|
|
1266
|
-
const composeFile = (0, node_path.join)(cwd, "docker-compose.yml");
|
|
1267
|
-
if (!(0, node_fs.existsSync)(composeFile)) return;
|
|
1268
|
-
const servicesToStart = parseComposeServiceNames(composeFile);
|
|
1269
|
-
if (servicesToStart.length === 0) return;
|
|
1270
|
-
logger$11.log(`🐳 Starting services: ${servicesToStart.join(", ")}`);
|
|
1271
|
-
try {
|
|
1272
|
-
(0, node_child_process.execSync)(`docker compose up -d ${servicesToStart.join(" ")}`, {
|
|
1273
|
-
cwd,
|
|
1274
|
-
stdio: "inherit",
|
|
1275
|
-
env: buildDockerComposeEnv(secretsEnv, portEnv)
|
|
1276
|
-
});
|
|
1277
|
-
logger$11.log("✅ Services started");
|
|
1278
|
-
} catch (error) {
|
|
1279
|
-
logger$11.error("❌ Failed to start services:", error.message);
|
|
1280
|
-
throw error;
|
|
1281
|
-
}
|
|
1282
|
-
}
|
|
1283
|
-
async function startWorkspaceServices(workspace, portEnv, secretsEnv) {
|
|
1284
|
-
const composeFile = (0, node_path.join)(workspace.root, "docker-compose.yml");
|
|
1285
|
-
if (!(0, node_fs.existsSync)(composeFile)) return;
|
|
1286
|
-
const allServices = parseComposeServiceNames(composeFile);
|
|
1287
|
-
const appNames = new Set(Object.keys(workspace.apps));
|
|
1288
|
-
const servicesToStart = allServices.filter((name$1) => !appNames.has(name$1));
|
|
1289
|
-
if (servicesToStart.length === 0) return;
|
|
1290
|
-
logger$11.log(`🐳 Starting services: ${servicesToStart.join(", ")}`);
|
|
1291
|
-
try {
|
|
1292
|
-
(0, node_child_process.execSync)(`docker compose up -d ${servicesToStart.join(" ")}`, {
|
|
1293
|
-
cwd: workspace.root,
|
|
1294
|
-
stdio: "inherit",
|
|
1295
|
-
env: buildDockerComposeEnv(secretsEnv, portEnv)
|
|
1296
|
-
});
|
|
1297
|
-
logger$11.log("✅ Services started");
|
|
1298
|
-
} catch (error) {
|
|
1299
|
-
logger$11.error("❌ Failed to start services:", error.message);
|
|
1300
|
-
throw error;
|
|
1301
|
-
}
|
|
1302
|
-
}
|
|
1303
|
-
/**
|
|
1304
1510
|
* Workspace dev command - orchestrates multi-app development using Turbo.
|
|
1305
1511
|
*
|
|
1306
1512
|
* Flow:
|
|
@@ -1467,7 +1673,7 @@ async function workspaceDevCommand(workspace, options) {
|
|
|
1467
1673
|
};
|
|
1468
1674
|
process.on("SIGINT", shutdown);
|
|
1469
1675
|
process.on("SIGTERM", shutdown);
|
|
1470
|
-
return new Promise((resolve$
|
|
1676
|
+
return new Promise((resolve$4, reject) => {
|
|
1471
1677
|
turboProcess.on("error", (error) => {
|
|
1472
1678
|
logger$11.error("❌ Turbo error:", error);
|
|
1473
1679
|
reject(error);
|
|
@@ -1475,7 +1681,7 @@ async function workspaceDevCommand(workspace, options) {
|
|
|
1475
1681
|
turboProcess.on("exit", (code) => {
|
|
1476
1682
|
if (openApiWatcher) openApiWatcher.close().catch(() => {});
|
|
1477
1683
|
if (code !== null && code !== 0) reject(new Error(`Turbo exited with code ${code}`));
|
|
1478
|
-
else resolve$
|
|
1684
|
+
else resolve$4();
|
|
1479
1685
|
});
|
|
1480
1686
|
});
|
|
1481
1687
|
}
|
|
@@ -1503,105 +1709,6 @@ async function buildServer(config, context, provider, enableOpenApi, appRoot = p
|
|
|
1503
1709
|
]);
|
|
1504
1710
|
}
|
|
1505
1711
|
/**
|
|
1506
|
-
* Find the directory containing .gkm/secrets/.
|
|
1507
|
-
* Walks up from cwd until it finds one, or returns cwd.
|
|
1508
|
-
* @internal Exported for testing
|
|
1509
|
-
*/
|
|
1510
|
-
function findSecretsRoot(startDir) {
|
|
1511
|
-
let dir = startDir;
|
|
1512
|
-
while (dir !== "/") {
|
|
1513
|
-
if ((0, node_fs.existsSync)((0, node_path.join)(dir, ".gkm", "secrets"))) return dir;
|
|
1514
|
-
const parent = (0, node_path.dirname)(dir);
|
|
1515
|
-
if (parent === dir) break;
|
|
1516
|
-
dir = parent;
|
|
1517
|
-
}
|
|
1518
|
-
return startDir;
|
|
1519
|
-
}
|
|
1520
|
-
/**
|
|
1521
|
-
* Generate the credentials injection code snippet.
|
|
1522
|
-
* This is the common logic used by both entry wrapper and exec preload.
|
|
1523
|
-
* @internal
|
|
1524
|
-
*/
|
|
1525
|
-
function generateCredentialsInjection(secretsJsonPath) {
|
|
1526
|
-
return `import { existsSync, readFileSync } from 'node:fs';
|
|
1527
|
-
|
|
1528
|
-
// Inject dev secrets via globalThis and process.env
|
|
1529
|
-
// Using globalThis.__gkm_credentials__ avoids CJS/ESM interop issues where
|
|
1530
|
-
// Object.assign on the Credentials export only mutates one module copy.
|
|
1531
|
-
const secretsPath = '${secretsJsonPath}';
|
|
1532
|
-
if (existsSync(secretsPath)) {
|
|
1533
|
-
const secrets = JSON.parse(readFileSync(secretsPath, 'utf-8'));
|
|
1534
|
-
globalThis.__gkm_credentials__ = secrets;
|
|
1535
|
-
Object.assign(process.env, secrets);
|
|
1536
|
-
}
|
|
1537
|
-
`;
|
|
1538
|
-
}
|
|
1539
|
-
/**
|
|
1540
|
-
* Create a preload script that injects secrets into Credentials.
|
|
1541
|
-
* Used by `gkm exec` to inject secrets before running any command.
|
|
1542
|
-
* @internal Exported for testing
|
|
1543
|
-
*/
|
|
1544
|
-
async function createCredentialsPreload(preloadPath, secretsJsonPath) {
|
|
1545
|
-
const content = `/**
|
|
1546
|
-
* Credentials preload generated by 'gkm exec'
|
|
1547
|
-
* This file is loaded via NODE_OPTIONS="--import <path>"
|
|
1548
|
-
*/
|
|
1549
|
-
${generateCredentialsInjection(secretsJsonPath)}`;
|
|
1550
|
-
await (0, node_fs_promises.writeFile)(preloadPath, content);
|
|
1551
|
-
}
|
|
1552
|
-
/**
|
|
1553
|
-
* Create a wrapper script that injects secrets before importing the entry file.
|
|
1554
|
-
* @internal Exported for testing
|
|
1555
|
-
*/
|
|
1556
|
-
async function createEntryWrapper(wrapperPath, entryPath, secretsJsonPath) {
|
|
1557
|
-
const credentialsInjection = secretsJsonPath ? `${generateCredentialsInjection(secretsJsonPath)}
|
|
1558
|
-
` : "";
|
|
1559
|
-
const content = `#!/usr/bin/env node
|
|
1560
|
-
/**
|
|
1561
|
-
* Entry wrapper generated by 'gkm dev --entry'
|
|
1562
|
-
*/
|
|
1563
|
-
${credentialsInjection}// Import and run the user's entry file (dynamic import ensures secrets load first)
|
|
1564
|
-
await import('${entryPath}');
|
|
1565
|
-
`;
|
|
1566
|
-
await (0, node_fs_promises.writeFile)(wrapperPath, content);
|
|
1567
|
-
}
|
|
1568
|
-
/**
|
|
1569
|
-
* Prepare credentials for entry dev mode.
|
|
1570
|
-
* Loads workspace config, secrets, and injects PORT.
|
|
1571
|
-
* @internal Exported for testing
|
|
1572
|
-
*/
|
|
1573
|
-
async function prepareEntryCredentials(options) {
|
|
1574
|
-
const cwd = options.cwd ?? process.cwd();
|
|
1575
|
-
let workspaceAppPort;
|
|
1576
|
-
let secretsRoot = cwd;
|
|
1577
|
-
let appName;
|
|
1578
|
-
try {
|
|
1579
|
-
const appInfo = await require_config.loadWorkspaceAppInfo(cwd);
|
|
1580
|
-
workspaceAppPort = appInfo.app.port;
|
|
1581
|
-
secretsRoot = appInfo.workspaceRoot;
|
|
1582
|
-
appName = appInfo.appName;
|
|
1583
|
-
} catch (error) {
|
|
1584
|
-
logger$11.log(`⚠️ Could not load workspace config: ${error.message}`);
|
|
1585
|
-
secretsRoot = findSecretsRoot(cwd);
|
|
1586
|
-
appName = require_config.getAppNameFromCwd(cwd) ?? void 0;
|
|
1587
|
-
}
|
|
1588
|
-
const resolvedPort = options.explicitPort ?? workspaceAppPort ?? 3e3;
|
|
1589
|
-
const credentials = await loadSecretsForApp(secretsRoot, appName);
|
|
1590
|
-
credentials.PORT = String(resolvedPort);
|
|
1591
|
-
const secretsDir = (0, node_path.join)(secretsRoot, ".gkm");
|
|
1592
|
-
await (0, node_fs_promises.mkdir)(secretsDir, { recursive: true });
|
|
1593
|
-
const secretsFileName = appName ? `dev-secrets-${appName}.json` : "dev-secrets.json";
|
|
1594
|
-
const secretsJsonPath = (0, node_path.join)(secretsDir, secretsFileName);
|
|
1595
|
-
await (0, node_fs_promises.writeFile)(secretsJsonPath, JSON.stringify(credentials, null, 2));
|
|
1596
|
-
return {
|
|
1597
|
-
credentials,
|
|
1598
|
-
resolvedPort,
|
|
1599
|
-
secretsJsonPath,
|
|
1600
|
-
appName,
|
|
1601
|
-
secretsRoot
|
|
1602
|
-
};
|
|
1603
|
-
}
|
|
1604
|
-
/**
|
|
1605
1712
|
* Run any TypeScript file with secret injection.
|
|
1606
1713
|
* Does not require gkm.config.ts.
|
|
1607
1714
|
*/
|
|
@@ -1686,12 +1793,12 @@ var EntryRunner = class {
|
|
|
1686
1793
|
if (code !== null && code !== 0 && code !== 143) logger$11.error(`❌ Process exited with code ${code}`);
|
|
1687
1794
|
this.isRunning = false;
|
|
1688
1795
|
});
|
|
1689
|
-
await new Promise((resolve$
|
|
1796
|
+
await new Promise((resolve$4) => setTimeout(resolve$4, 500));
|
|
1690
1797
|
if (this.isRunning) logger$11.log(`\n🎉 Running at http://localhost:${this.port}`);
|
|
1691
1798
|
}
|
|
1692
1799
|
async restart() {
|
|
1693
1800
|
this.stopProcess();
|
|
1694
|
-
await new Promise((resolve$
|
|
1801
|
+
await new Promise((resolve$4) => setTimeout(resolve$4, 500));
|
|
1695
1802
|
await this.runProcess();
|
|
1696
1803
|
}
|
|
1697
1804
|
stop() {
|
|
@@ -1824,7 +1931,7 @@ var DevServer = class {
|
|
|
1824
1931
|
if (code !== null && code !== 0 && signal !== "SIGTERM") logger$11.error(`❌ Server exited with code ${code}`);
|
|
1825
1932
|
this.isRunning = false;
|
|
1826
1933
|
});
|
|
1827
|
-
await new Promise((resolve$
|
|
1934
|
+
await new Promise((resolve$4) => setTimeout(resolve$4, 1e3));
|
|
1828
1935
|
if (this.isRunning) {
|
|
1829
1936
|
logger$11.log(`\n🎉 Server running at http://localhost:${this.actualPort}`);
|
|
1830
1937
|
if (this.enableOpenApi) logger$11.log(`📚 API Docs available at http://localhost:${this.actualPort}/__docs`);
|
|
@@ -1859,7 +1966,7 @@ var DevServer = class {
|
|
|
1859
1966
|
let attempts = 0;
|
|
1860
1967
|
while (attempts < 30) {
|
|
1861
1968
|
if (await isPortAvailable(portToReuse)) break;
|
|
1862
|
-
await new Promise((resolve$
|
|
1969
|
+
await new Promise((resolve$4) => setTimeout(resolve$4, 100));
|
|
1863
1970
|
attempts++;
|
|
1864
1971
|
}
|
|
1865
1972
|
this.requestedPort = portToReuse;
|
|
@@ -1876,73 +1983,6 @@ var DevServer = class {
|
|
|
1876
1983
|
await fsWriteFile(serverPath, content);
|
|
1877
1984
|
}
|
|
1878
1985
|
};
|
|
1879
|
-
/**
|
|
1880
|
-
* Run a command with secrets injected into Credentials.
|
|
1881
|
-
* Uses Node's --import flag to preload a script that populates Credentials
|
|
1882
|
-
* before the command loads any modules that depend on them.
|
|
1883
|
-
*
|
|
1884
|
-
* @example
|
|
1885
|
-
* ```bash
|
|
1886
|
-
* gkm exec -- npx @better-auth/cli migrate
|
|
1887
|
-
* gkm exec -- npx prisma migrate dev
|
|
1888
|
-
* ```
|
|
1889
|
-
*/
|
|
1890
|
-
async function execCommand(commandArgs, options = {}) {
|
|
1891
|
-
const cwd = options.cwd ?? process.cwd();
|
|
1892
|
-
if (commandArgs.length === 0) throw new Error("No command specified. Usage: gkm exec -- <command>");
|
|
1893
|
-
const defaultEnv = loadEnvFiles(".env");
|
|
1894
|
-
if (defaultEnv.loaded.length > 0) logger$11.log(`📦 Loaded env: ${defaultEnv.loaded.join(", ")}`);
|
|
1895
|
-
const { credentials, secretsJsonPath, appName, secretsRoot } = await prepareEntryCredentials({ cwd });
|
|
1896
|
-
if (appName) logger$11.log(`📦 App: ${appName}`);
|
|
1897
|
-
const secretCount = Object.keys(credentials).filter((k) => k !== "PORT").length;
|
|
1898
|
-
if (secretCount > 0) logger$11.log(`🔐 Loaded ${secretCount} secret(s)`);
|
|
1899
|
-
const resolvedPorts = await resolveServicePorts(secretsRoot);
|
|
1900
|
-
if (resolvedPorts.mappings.length > 0 && Object.keys(resolvedPorts.ports).length > 0) {
|
|
1901
|
-
const rewritten = rewriteUrlsWithPorts(credentials, resolvedPorts);
|
|
1902
|
-
Object.assign(credentials, rewritten);
|
|
1903
|
-
logger$11.log(`🔌 Applied ${Object.keys(resolvedPorts.ports).length} port mapping(s)`);
|
|
1904
|
-
}
|
|
1905
|
-
try {
|
|
1906
|
-
const appInfo = await require_config.loadWorkspaceAppInfo(cwd);
|
|
1907
|
-
if (appInfo.appName) {
|
|
1908
|
-
const depEnv = require_workspace.getDependencyEnvVars(appInfo.workspace, appInfo.appName);
|
|
1909
|
-
Object.assign(credentials, depEnv);
|
|
1910
|
-
}
|
|
1911
|
-
} catch {}
|
|
1912
|
-
const preloadDir = (0, node_path.join)(cwd, ".gkm");
|
|
1913
|
-
await (0, node_fs_promises.mkdir)(preloadDir, { recursive: true });
|
|
1914
|
-
const preloadPath = (0, node_path.join)(preloadDir, "credentials-preload.ts");
|
|
1915
|
-
await createCredentialsPreload(preloadPath, secretsJsonPath);
|
|
1916
|
-
const [cmd, ...rawArgs] = commandArgs;
|
|
1917
|
-
if (!cmd) throw new Error("No command specified");
|
|
1918
|
-
const args = rawArgs.map((arg) => arg.replace(/\$PORT\b/g, credentials.PORT ?? "3000"));
|
|
1919
|
-
logger$11.log(`🚀 Running: ${[cmd, ...args].join(" ")}`);
|
|
1920
|
-
const existingNodeOptions = process.env.NODE_OPTIONS ?? "";
|
|
1921
|
-
const tsxImport = "--import=tsx";
|
|
1922
|
-
const preloadImport = `--import=${preloadPath}`;
|
|
1923
|
-
const nodeOptions = [
|
|
1924
|
-
existingNodeOptions,
|
|
1925
|
-
tsxImport,
|
|
1926
|
-
preloadImport
|
|
1927
|
-
].filter(Boolean).join(" ");
|
|
1928
|
-
const child = (0, node_child_process.spawn)(cmd, args, {
|
|
1929
|
-
cwd,
|
|
1930
|
-
stdio: "inherit",
|
|
1931
|
-
env: {
|
|
1932
|
-
...process.env,
|
|
1933
|
-
...credentials,
|
|
1934
|
-
NODE_OPTIONS: nodeOptions
|
|
1935
|
-
}
|
|
1936
|
-
});
|
|
1937
|
-
const exitCode = await new Promise((resolve$3) => {
|
|
1938
|
-
child.on("close", (code) => resolve$3(code ?? 0));
|
|
1939
|
-
child.on("error", (error) => {
|
|
1940
|
-
logger$11.error(`Failed to run command: ${error.message}`);
|
|
1941
|
-
resolve$3(1);
|
|
1942
|
-
});
|
|
1943
|
-
});
|
|
1944
|
-
if (exitCode !== 0) process.exit(exitCode);
|
|
1945
|
-
}
|
|
1946
1986
|
|
|
1947
1987
|
//#endregion
|
|
1948
1988
|
//#region src/build/manifests.ts
|
|
@@ -2214,7 +2254,7 @@ async function buildForProvider(provider, context, rootOutputDir, endpointGenera
|
|
|
2214
2254
|
let masterKey;
|
|
2215
2255
|
if (context.production?.bundle && !skipBundle) {
|
|
2216
2256
|
logger$9.log(`\n📦 Bundling production server...`);
|
|
2217
|
-
const { bundleServer } = await Promise.resolve().then(() => require("./bundler-
|
|
2257
|
+
const { bundleServer } = await Promise.resolve().then(() => require("./bundler-i-az1DZ2.cjs"));
|
|
2218
2258
|
const allConstructs = [
|
|
2219
2259
|
...endpoints.map((e) => e.construct),
|
|
2220
2260
|
...functions.map((f) => f.construct),
|
|
@@ -2284,7 +2324,7 @@ async function workspaceBuildCommand(workspace, options) {
|
|
|
2284
2324
|
try {
|
|
2285
2325
|
const turboCommand = getTurboCommand(pm);
|
|
2286
2326
|
logger$9.log(`Running: ${turboCommand}`);
|
|
2287
|
-
await new Promise((resolve$
|
|
2327
|
+
await new Promise((resolve$4, reject) => {
|
|
2288
2328
|
const child = (0, node_child_process.spawn)(turboCommand, {
|
|
2289
2329
|
shell: true,
|
|
2290
2330
|
cwd: workspace.root,
|
|
@@ -2295,7 +2335,7 @@ async function workspaceBuildCommand(workspace, options) {
|
|
|
2295
2335
|
}
|
|
2296
2336
|
});
|
|
2297
2337
|
child.on("close", (code) => {
|
|
2298
|
-
if (code === 0) resolve$
|
|
2338
|
+
if (code === 0) resolve$4();
|
|
2299
2339
|
else reject(new Error(`Turbo build failed with exit code ${code}`));
|
|
2300
2340
|
});
|
|
2301
2341
|
child.on("error", (err) => {
|
|
@@ -5438,7 +5478,7 @@ async function prompt(message, hidden = false) {
|
|
|
5438
5478
|
if (!process.stdin.isTTY) throw new Error("Interactive input required. Please configure manually.");
|
|
5439
5479
|
if (hidden) {
|
|
5440
5480
|
process.stdout.write(message);
|
|
5441
|
-
return new Promise((resolve$
|
|
5481
|
+
return new Promise((resolve$4) => {
|
|
5442
5482
|
let value = "";
|
|
5443
5483
|
const onData = (char) => {
|
|
5444
5484
|
const c = char.toString();
|
|
@@ -5447,7 +5487,7 @@ async function prompt(message, hidden = false) {
|
|
|
5447
5487
|
process.stdin.pause();
|
|
5448
5488
|
process.stdin.removeListener("data", onData);
|
|
5449
5489
|
process.stdout.write("\n");
|
|
5450
|
-
resolve$
|
|
5490
|
+
resolve$4(value);
|
|
5451
5491
|
} else if (c === "") {
|
|
5452
5492
|
process.stdin.setRawMode(false);
|
|
5453
5493
|
process.stdin.pause();
|
|
@@ -6465,7 +6505,7 @@ async function deployCommand(options) {
|
|
|
6465
6505
|
dokployConfig = setupResult.config;
|
|
6466
6506
|
finalRegistry = dokployConfig.registry ?? dockerConfig.registry;
|
|
6467
6507
|
if (setupResult.serviceUrls) {
|
|
6468
|
-
const { readStageSecrets: readStageSecrets$1, writeStageSecrets: writeStageSecrets$1, initStageSecrets } = await Promise.resolve().then(() => require("./storage-
|
|
6508
|
+
const { readStageSecrets: readStageSecrets$1, writeStageSecrets: writeStageSecrets$1, initStageSecrets } = await Promise.resolve().then(() => require("./storage-ChVQI_G7.cjs"));
|
|
6469
6509
|
let secrets = await readStageSecrets$1(stage);
|
|
6470
6510
|
if (!secrets) {
|
|
6471
6511
|
logger$3.log(` Creating secrets file for stage "${stage}"...`);
|
|
@@ -11875,75 +11915,29 @@ async function testCommand(options = {}) {
|
|
|
11875
11915
|
console.log(`\n🧪 Running tests with ${stage} environment...\n`);
|
|
11876
11916
|
const defaultEnv = loadEnvFiles(".env");
|
|
11877
11917
|
if (defaultEnv.loaded.length > 0) console.log(` 📦 Loaded env: ${defaultEnv.loaded.join(", ")}`);
|
|
11878
|
-
|
|
11879
|
-
|
|
11880
|
-
|
|
11881
|
-
|
|
11882
|
-
|
|
11883
|
-
|
|
11884
|
-
|
|
11885
|
-
|
|
11886
|
-
|
|
11887
|
-
else throw error;
|
|
11888
|
-
}
|
|
11889
|
-
let dependencyEnv = {};
|
|
11890
|
-
try {
|
|
11891
|
-
const appInfo = await require_config.loadWorkspaceAppInfo(cwd);
|
|
11892
|
-
const resolvedPorts = await resolveServicePorts(appInfo.workspaceRoot);
|
|
11893
|
-
await startWorkspaceServices(appInfo.workspace, resolvedPorts.dockerEnv, secretsEnv);
|
|
11894
|
-
if (resolvedPorts.mappings.length > 0) {
|
|
11895
|
-
secretsEnv = rewriteUrlsWithPorts(secretsEnv, resolvedPorts);
|
|
11896
|
-
console.log(` 🔌 Applied ${Object.keys(resolvedPorts.ports).length} port mapping(s)`);
|
|
11897
|
-
}
|
|
11898
|
-
dependencyEnv = require_workspace.getDependencyEnvVars(appInfo.workspace, appInfo.appName);
|
|
11899
|
-
if (Object.keys(dependencyEnv).length > 0) console.log(` 🔗 Loaded ${Object.keys(dependencyEnv).length} dependency URL(s)`);
|
|
11900
|
-
const sniffed = await sniffAppEnvironment(appInfo.app, appInfo.appName, appInfo.workspaceRoot, { logWarnings: false });
|
|
11918
|
+
const result = await prepareEntryCredentials({
|
|
11919
|
+
stages: [stage],
|
|
11920
|
+
startDocker: true,
|
|
11921
|
+
secretsFileName: "test-secrets.json",
|
|
11922
|
+
resolveDockerPorts: "full"
|
|
11923
|
+
});
|
|
11924
|
+
let finalCredentials = { ...result.credentials };
|
|
11925
|
+
if (result.appInfo) {
|
|
11926
|
+
const sniffed = await sniffAppEnvironment(result.appInfo.app, result.appInfo.appName, result.appInfo.workspaceRoot, { logWarnings: false });
|
|
11901
11927
|
if (sniffed.requiredEnvVars.length > 0) {
|
|
11902
11928
|
const needed = new Set(sniffed.requiredEnvVars);
|
|
11903
|
-
const
|
|
11904
|
-
|
|
11905
|
-
|
|
11906
|
-
};
|
|
11907
|
-
const filteredEnv = {};
|
|
11908
|
-
for (const [key, value] of Object.entries(allEnv)) if (needed.has(key)) filteredEnv[key] = value;
|
|
11909
|
-
secretsEnv = {};
|
|
11910
|
-
dependencyEnv = filteredEnv;
|
|
11929
|
+
const filtered = {};
|
|
11930
|
+
for (const [key, value] of Object.entries(finalCredentials)) if (needed.has(key)) filtered[key] = value;
|
|
11931
|
+
finalCredentials = filtered;
|
|
11911
11932
|
console.log(` 🔍 Sniffed ${sniffed.requiredEnvVars.length} required env var(s)`);
|
|
11912
11933
|
}
|
|
11913
|
-
} catch {
|
|
11914
|
-
const composePath = (0, node_path.join)(cwd, "docker-compose.yml");
|
|
11915
|
-
const mappings = parseComposePortMappings(composePath);
|
|
11916
|
-
if (mappings.length > 0) {
|
|
11917
|
-
const resolvedPorts = await resolveServicePorts(cwd);
|
|
11918
|
-
await startComposeServices(cwd, resolvedPorts.dockerEnv, secretsEnv);
|
|
11919
|
-
if (resolvedPorts.mappings.length > 0) {
|
|
11920
|
-
secretsEnv = rewriteUrlsWithPorts(secretsEnv, resolvedPorts);
|
|
11921
|
-
console.log(` 🔌 Applied ${Object.keys(resolvedPorts.ports).length} port mapping(s)`);
|
|
11922
|
-
} else {
|
|
11923
|
-
const ports = await loadPortState(cwd);
|
|
11924
|
-
if (Object.keys(ports).length > 0) {
|
|
11925
|
-
secretsEnv = rewriteUrlsWithPorts(secretsEnv, {
|
|
11926
|
-
dockerEnv: {},
|
|
11927
|
-
ports,
|
|
11928
|
-
mappings
|
|
11929
|
-
});
|
|
11930
|
-
console.log(` 🔌 Applied ${Object.keys(ports).length} port mapping(s)`);
|
|
11931
|
-
}
|
|
11932
|
-
}
|
|
11933
|
-
}
|
|
11934
11934
|
}
|
|
11935
|
-
|
|
11935
|
+
finalCredentials = rewriteDatabaseUrlForTests(finalCredentials);
|
|
11936
11936
|
console.log("");
|
|
11937
|
-
|
|
11938
|
-
...secretsEnv,
|
|
11939
|
-
...dependencyEnv
|
|
11940
|
-
};
|
|
11937
|
+
await (0, node_fs_promises.writeFile)(result.secretsJsonPath, JSON.stringify(finalCredentials, null, 2));
|
|
11941
11938
|
const gkmDir = (0, node_path.join)(cwd, ".gkm");
|
|
11942
|
-
await (0, node_fs_promises.mkdir)(gkmDir, { recursive: true });
|
|
11943
|
-
const secretsJsonPath = (0, node_path.join)(gkmDir, "test-secrets.json");
|
|
11944
|
-
await (0, node_fs_promises.writeFile)(secretsJsonPath, JSON.stringify(allSecrets, null, 2));
|
|
11945
11939
|
const preloadPath = (0, node_path.join)(gkmDir, "test-credentials-preload.ts");
|
|
11946
|
-
await createCredentialsPreload(preloadPath, secretsJsonPath);
|
|
11940
|
+
await createCredentialsPreload(preloadPath, result.secretsJsonPath);
|
|
11947
11941
|
const existingNodeOptions = process.env.NODE_OPTIONS ?? "";
|
|
11948
11942
|
const tsxImport = "--import=tsx";
|
|
11949
11943
|
const preloadImport = `--import=${preloadPath}`;
|
|
@@ -11963,14 +11957,14 @@ async function testCommand(options = {}) {
|
|
|
11963
11957
|
stdio: "inherit",
|
|
11964
11958
|
env: {
|
|
11965
11959
|
...process.env,
|
|
11966
|
-
...
|
|
11960
|
+
...finalCredentials,
|
|
11967
11961
|
NODE_ENV: "test",
|
|
11968
11962
|
NODE_OPTIONS: nodeOptions
|
|
11969
11963
|
}
|
|
11970
11964
|
});
|
|
11971
|
-
return new Promise((resolve$
|
|
11965
|
+
return new Promise((resolve$4, reject) => {
|
|
11972
11966
|
vitestProcess.on("close", (code) => {
|
|
11973
|
-
if (code === 0) resolve$
|
|
11967
|
+
if (code === 0) resolve$4();
|
|
11974
11968
|
else reject(new Error(`Tests failed with exit code ${code}`));
|
|
11975
11969
|
});
|
|
11976
11970
|
vitestProcess.on("error", (error) => {
|
|
@@ -12367,9 +12361,9 @@ program.command("secrets:push").description("Push secrets to remote provider (SS
|
|
|
12367
12361
|
const globalOptions = program.opts();
|
|
12368
12362
|
if (globalOptions.cwd) process.chdir(globalOptions.cwd);
|
|
12369
12363
|
const { loadWorkspaceConfig: loadWorkspaceConfig$1 } = await Promise.resolve().then(() => require("./config.cjs"));
|
|
12370
|
-
const { pushSecrets: pushSecrets$1 } = await Promise.resolve().then(() => require("./sync-
|
|
12364
|
+
const { pushSecrets: pushSecrets$1 } = await Promise.resolve().then(() => require("./sync-ByaRPBxh.cjs"));
|
|
12371
12365
|
const { reconcileMissingSecrets } = await Promise.resolve().then(() => require("./reconcile-Ch7sIcf8.cjs"));
|
|
12372
|
-
const { readStageSecrets: readStageSecrets$1, writeStageSecrets: writeStageSecrets$1 } = await Promise.resolve().then(() => require("./storage-
|
|
12366
|
+
const { readStageSecrets: readStageSecrets$1, writeStageSecrets: writeStageSecrets$1 } = await Promise.resolve().then(() => require("./storage-ChVQI_G7.cjs"));
|
|
12373
12367
|
const { workspace } = await loadWorkspaceConfig$1();
|
|
12374
12368
|
const secrets = await readStageSecrets$1(options.stage, workspace.root);
|
|
12375
12369
|
if (secrets) {
|
|
@@ -12392,8 +12386,8 @@ program.command("secrets:pull").description("Pull secrets from remote provider (
|
|
|
12392
12386
|
const globalOptions = program.opts();
|
|
12393
12387
|
if (globalOptions.cwd) process.chdir(globalOptions.cwd);
|
|
12394
12388
|
const { loadWorkspaceConfig: loadWorkspaceConfig$1 } = await Promise.resolve().then(() => require("./config.cjs"));
|
|
12395
|
-
const { pullSecrets: pullSecrets$1 } = await Promise.resolve().then(() => require("./sync-
|
|
12396
|
-
const { writeStageSecrets: writeStageSecrets$1 } = await Promise.resolve().then(() => require("./storage-
|
|
12389
|
+
const { pullSecrets: pullSecrets$1 } = await Promise.resolve().then(() => require("./sync-ByaRPBxh.cjs"));
|
|
12390
|
+
const { writeStageSecrets: writeStageSecrets$1 } = await Promise.resolve().then(() => require("./storage-ChVQI_G7.cjs"));
|
|
12397
12391
|
const { reconcileMissingSecrets } = await Promise.resolve().then(() => require("./reconcile-Ch7sIcf8.cjs"));
|
|
12398
12392
|
const { workspace } = await loadWorkspaceConfig$1();
|
|
12399
12393
|
let secrets = await pullSecrets$1(options.stage, workspace);
|
|
@@ -12420,7 +12414,7 @@ program.command("secrets:reconcile").description("Backfill missing custom secret
|
|
|
12420
12414
|
if (globalOptions.cwd) process.chdir(globalOptions.cwd);
|
|
12421
12415
|
const { loadWorkspaceConfig: loadWorkspaceConfig$1 } = await Promise.resolve().then(() => require("./config.cjs"));
|
|
12422
12416
|
const { reconcileMissingSecrets } = await Promise.resolve().then(() => require("./reconcile-Ch7sIcf8.cjs"));
|
|
12423
|
-
const { readStageSecrets: readStageSecrets$1, writeStageSecrets: writeStageSecrets$1 } = await Promise.resolve().then(() => require("./storage-
|
|
12417
|
+
const { readStageSecrets: readStageSecrets$1, writeStageSecrets: writeStageSecrets$1 } = await Promise.resolve().then(() => require("./storage-ChVQI_G7.cjs"));
|
|
12424
12418
|
const { workspace } = await loadWorkspaceConfig$1();
|
|
12425
12419
|
const secrets = await readStageSecrets$1(options.stage, workspace.root);
|
|
12426
12420
|
if (!secrets) {
|