@geekmidas/cli 0.18.0 → 0.19.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{bundler-C74EKlNa.cjs → bundler-CyHg1v_T.cjs} +3 -3
- package/dist/{bundler-C74EKlNa.cjs.map → bundler-CyHg1v_T.cjs.map} +1 -1
- package/dist/{bundler-B6z6HEeh.mjs → bundler-DQIuE3Kn.mjs} +3 -3
- package/dist/{bundler-B6z6HEeh.mjs.map → bundler-DQIuE3Kn.mjs.map} +1 -1
- package/dist/{config-DYULeEv8.mjs → config-BaYqrF3n.mjs} +48 -10
- package/dist/config-BaYqrF3n.mjs.map +1 -0
- package/dist/{config-AmInkU7k.cjs → config-CxrLu8ia.cjs} +53 -9
- package/dist/config-CxrLu8ia.cjs.map +1 -0
- package/dist/config.cjs +4 -1
- package/dist/config.d.cts +27 -2
- package/dist/config.d.cts.map +1 -1
- package/dist/config.d.mts +27 -2
- package/dist/config.d.mts.map +1 -1
- package/dist/config.mjs +3 -2
- package/dist/dokploy-api-B0w17y4_.mjs +3 -0
- package/dist/{dokploy-api-CaETb2L6.mjs → dokploy-api-B9qR2Yn1.mjs} +1 -1
- package/dist/{dokploy-api-CaETb2L6.mjs.map → dokploy-api-B9qR2Yn1.mjs.map} +1 -1
- package/dist/dokploy-api-BnGeUqN4.cjs +3 -0
- package/dist/{dokploy-api-C7F9VykY.cjs → dokploy-api-C5czOZoc.cjs} +1 -1
- package/dist/{dokploy-api-C7F9VykY.cjs.map → dokploy-api-C5czOZoc.cjs.map} +1 -1
- package/dist/{encryption-D7Efcdi9.cjs → encryption-BAz0xQ1Q.cjs} +1 -1
- package/dist/{encryption-D7Efcdi9.cjs.map → encryption-BAz0xQ1Q.cjs.map} +1 -1
- package/dist/{encryption-h4Nb6W-M.mjs → encryption-JtMsiGNp.mjs} +2 -2
- package/dist/{encryption-h4Nb6W-M.mjs.map → encryption-JtMsiGNp.mjs.map} +1 -1
- package/dist/index-CWN-bgrO.d.mts +495 -0
- package/dist/index-CWN-bgrO.d.mts.map +1 -0
- package/dist/index-DEWYvYvg.d.cts +495 -0
- package/dist/index-DEWYvYvg.d.cts.map +1 -0
- package/dist/index.cjs +2639 -563
- package/dist/index.cjs.map +1 -1
- package/dist/index.mjs +2634 -563
- package/dist/index.mjs.map +1 -1
- package/dist/{openapi-CZVcfxk-.mjs → openapi-CgqR6Jkw.mjs} +3 -3
- package/dist/{openapi-CZVcfxk-.mjs.map → openapi-CgqR6Jkw.mjs.map} +1 -1
- package/dist/{openapi-C89hhkZC.cjs → openapi-DfpxS0xv.cjs} +8 -2
- package/dist/{openapi-C89hhkZC.cjs.map → openapi-DfpxS0xv.cjs.map} +1 -1
- package/dist/{openapi-react-query-CM2_qlW9.mjs → openapi-react-query-5rSortLH.mjs} +1 -1
- package/dist/{openapi-react-query-CM2_qlW9.mjs.map → openapi-react-query-5rSortLH.mjs.map} +1 -1
- package/dist/{openapi-react-query-iKjfLzff.cjs → openapi-react-query-DvNpdDpM.cjs} +1 -1
- package/dist/{openapi-react-query-iKjfLzff.cjs.map → openapi-react-query-DvNpdDpM.cjs.map} +1 -1
- package/dist/openapi-react-query.cjs +1 -1
- package/dist/openapi-react-query.mjs +1 -1
- package/dist/openapi.cjs +3 -2
- package/dist/openapi.d.cts +1 -1
- package/dist/openapi.d.mts +1 -1
- package/dist/openapi.mjs +3 -2
- package/dist/{storage-Bn3K9Ccu.cjs → storage-BPRgh3DU.cjs} +136 -5
- package/dist/storage-BPRgh3DU.cjs.map +1 -0
- package/dist/{storage-nkGIjeXt.mjs → storage-DNj_I11J.mjs} +1 -1
- package/dist/storage-Dhst7BhI.mjs +272 -0
- package/dist/storage-Dhst7BhI.mjs.map +1 -0
- package/dist/{storage-UfyTn7Zm.cjs → storage-fOR8dMu5.cjs} +1 -1
- package/dist/{types-iFk5ms7y.d.mts → types-K2uQJ-FO.d.mts} +2 -2
- package/dist/{types-BgaMXsUa.d.cts.map → types-K2uQJ-FO.d.mts.map} +1 -1
- package/dist/{types-BgaMXsUa.d.cts → types-l53qUmGt.d.cts} +2 -2
- package/dist/{types-iFk5ms7y.d.mts.map → types-l53qUmGt.d.cts.map} +1 -1
- package/dist/workspace/index.cjs +19 -0
- package/dist/workspace/index.d.cts +3 -0
- package/dist/workspace/index.d.mts +3 -0
- package/dist/workspace/index.mjs +3 -0
- package/dist/workspace-CPLEZDZf.mjs +3788 -0
- package/dist/workspace-CPLEZDZf.mjs.map +1 -0
- package/dist/workspace-iWgBlX6h.cjs +3885 -0
- package/dist/workspace-iWgBlX6h.cjs.map +1 -0
- package/package.json +8 -3
- package/src/build/__tests__/workspace-build.spec.ts +215 -0
- package/src/build/index.ts +189 -1
- package/src/config.ts +71 -14
- package/src/deploy/__tests__/docker.spec.ts +1 -1
- package/src/deploy/__tests__/index.spec.ts +305 -1
- package/src/deploy/index.ts +426 -4
- package/src/deploy/types.ts +32 -0
- package/src/dev/__tests__/index.spec.ts +572 -1
- package/src/dev/index.ts +582 -2
- package/src/docker/__tests__/compose.spec.ts +425 -0
- package/src/docker/__tests__/templates.spec.ts +145 -0
- package/src/docker/compose.ts +248 -0
- package/src/docker/index.ts +159 -3
- package/src/docker/templates.ts +219 -4
- package/src/index.ts +24 -0
- package/src/init/__tests__/generators.spec.ts +17 -24
- package/src/init/__tests__/init.spec.ts +157 -5
- package/src/init/generators/auth.ts +220 -0
- package/src/init/generators/config.ts +61 -4
- package/src/init/generators/docker.ts +115 -8
- package/src/init/generators/env.ts +7 -127
- package/src/init/generators/index.ts +1 -0
- package/src/init/generators/models.ts +3 -1
- package/src/init/generators/monorepo.ts +154 -10
- package/src/init/generators/package.ts +5 -3
- package/src/init/generators/web.ts +213 -0
- package/src/init/index.ts +290 -58
- package/src/init/templates/api.ts +38 -29
- package/src/init/templates/index.ts +132 -4
- package/src/init/templates/minimal.ts +33 -35
- package/src/init/templates/serverless.ts +16 -19
- package/src/init/templates/worker.ts +50 -25
- package/src/init/versions.ts +47 -0
- package/src/secrets/keystore.ts +144 -0
- package/src/secrets/storage.ts +109 -6
- package/src/test/index.ts +97 -0
- package/src/workspace/__tests__/client-generator.spec.ts +357 -0
- package/src/workspace/__tests__/index.spec.ts +543 -0
- package/src/workspace/__tests__/schema.spec.ts +519 -0
- package/src/workspace/__tests__/type-inference.spec.ts +251 -0
- package/src/workspace/client-generator.ts +307 -0
- package/src/workspace/index.ts +372 -0
- package/src/workspace/schema.ts +368 -0
- package/src/workspace/types.ts +336 -0
- package/tsconfig.tsbuildinfo +1 -1
- package/tsdown.config.ts +1 -0
- package/dist/config-AmInkU7k.cjs.map +0 -1
- package/dist/config-DYULeEv8.mjs.map +0 -1
- package/dist/dokploy-api-B7KxOQr3.cjs +0 -3
- package/dist/dokploy-api-DHvfmWbi.mjs +0 -3
- package/dist/storage-BaOP55oq.mjs +0 -147
- package/dist/storage-BaOP55oq.mjs.map +0 -1
- package/dist/storage-Bn3K9Ccu.cjs.map +0 -1
package/dist/index.cjs
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
#!/usr/bin/env -S npx tsx
|
|
2
2
|
const require_chunk = require('./chunk-CUT6urMc.cjs');
|
|
3
|
-
const
|
|
4
|
-
const
|
|
5
|
-
const
|
|
6
|
-
const
|
|
7
|
-
const
|
|
3
|
+
const require_workspace = require('./workspace-iWgBlX6h.cjs');
|
|
4
|
+
const require_config = require('./config-CxrLu8ia.cjs');
|
|
5
|
+
const require_openapi = require('./openapi-DfpxS0xv.cjs');
|
|
6
|
+
const require_storage = require('./storage-BPRgh3DU.cjs');
|
|
7
|
+
const require_dokploy_api = require('./dokploy-api-C5czOZoc.cjs');
|
|
8
|
+
const require_openapi_react_query = require('./openapi-react-query-DvNpdDpM.cjs');
|
|
8
9
|
const node_fs = require_chunk.__toESM(require("node:fs"));
|
|
9
10
|
const node_path = require_chunk.__toESM(require("node:path"));
|
|
10
11
|
const commander = require_chunk.__toESM(require("commander"));
|
|
@@ -20,8 +21,9 @@ const fast_glob = require_chunk.__toESM(require("fast-glob"));
|
|
|
20
21
|
const __geekmidas_constructs_crons = require_chunk.__toESM(require("@geekmidas/constructs/crons"));
|
|
21
22
|
const __geekmidas_constructs_functions = require_chunk.__toESM(require("@geekmidas/constructs/functions"));
|
|
22
23
|
const __geekmidas_constructs_subscribers = require_chunk.__toESM(require("@geekmidas/constructs/subscribers"));
|
|
23
|
-
const prompts = require_chunk.__toESM(require("prompts"));
|
|
24
24
|
const node_crypto = require_chunk.__toESM(require("node:crypto"));
|
|
25
|
+
const prompts = require_chunk.__toESM(require("prompts"));
|
|
26
|
+
const node_module = require_chunk.__toESM(require("node:module"));
|
|
25
27
|
|
|
26
28
|
//#region package.json
|
|
27
29
|
var name = "@geekmidas/cli";
|
|
@@ -40,6 +42,11 @@ var exports$1 = {
|
|
|
40
42
|
"import": "./dist/config.mjs",
|
|
41
43
|
"require": "./dist/config.cjs"
|
|
42
44
|
},
|
|
45
|
+
"./workspace": {
|
|
46
|
+
"types": "./dist/workspace/index.d.ts",
|
|
47
|
+
"import": "./dist/workspace/index.mjs",
|
|
48
|
+
"require": "./dist/workspace/index.cjs"
|
|
49
|
+
},
|
|
43
50
|
"./openapi": {
|
|
44
51
|
"types": "./dist/openapi.d.ts",
|
|
45
52
|
"import": "./dist/openapi.mjs",
|
|
@@ -214,12 +221,12 @@ async function getDokployRegistryId(options) {
|
|
|
214
221
|
|
|
215
222
|
//#endregion
|
|
216
223
|
//#region src/auth/index.ts
|
|
217
|
-
const logger$
|
|
224
|
+
const logger$10 = console;
|
|
218
225
|
/**
|
|
219
226
|
* Validate Dokploy token by making a test API call
|
|
220
227
|
*/
|
|
221
228
|
async function validateDokployToken(endpoint, token) {
|
|
222
|
-
const { DokployApi: DokployApi$1 } = await Promise.resolve().then(() => require("./dokploy-api-
|
|
229
|
+
const { DokployApi: DokployApi$1 } = await Promise.resolve().then(() => require("./dokploy-api-BnGeUqN4.cjs"));
|
|
223
230
|
const api = new DokployApi$1({
|
|
224
231
|
baseUrl: endpoint,
|
|
225
232
|
token
|
|
@@ -282,36 +289,36 @@ async function prompt$1(message, hidden = false) {
|
|
|
282
289
|
async function loginCommand(options) {
|
|
283
290
|
const { service, token: providedToken, endpoint: providedEndpoint } = options;
|
|
284
291
|
if (service === "dokploy") {
|
|
285
|
-
logger$
|
|
292
|
+
logger$10.log("\n🔐 Logging in to Dokploy...\n");
|
|
286
293
|
let endpoint = providedEndpoint;
|
|
287
294
|
if (!endpoint) endpoint = await prompt$1("Dokploy URL (e.g., https://dokploy.example.com): ");
|
|
288
295
|
endpoint = endpoint.replace(/\/$/, "");
|
|
289
296
|
try {
|
|
290
297
|
new URL(endpoint);
|
|
291
298
|
} catch {
|
|
292
|
-
logger$
|
|
299
|
+
logger$10.error("Invalid URL format");
|
|
293
300
|
process.exit(1);
|
|
294
301
|
}
|
|
295
302
|
let token = providedToken;
|
|
296
303
|
if (!token) {
|
|
297
|
-
logger$
|
|
304
|
+
logger$10.log(`\nGenerate a token at: ${endpoint}/settings/profile\n`);
|
|
298
305
|
token = await prompt$1("API Token: ", true);
|
|
299
306
|
}
|
|
300
307
|
if (!token) {
|
|
301
|
-
logger$
|
|
308
|
+
logger$10.error("Token is required");
|
|
302
309
|
process.exit(1);
|
|
303
310
|
}
|
|
304
|
-
logger$
|
|
311
|
+
logger$10.log("\nValidating credentials...");
|
|
305
312
|
const isValid = await validateDokployToken(endpoint, token);
|
|
306
313
|
if (!isValid) {
|
|
307
|
-
logger$
|
|
314
|
+
logger$10.error("\n✗ Invalid credentials. Please check your token and try again.");
|
|
308
315
|
process.exit(1);
|
|
309
316
|
}
|
|
310
317
|
await storeDokployCredentials(token, endpoint);
|
|
311
|
-
logger$
|
|
312
|
-
logger$
|
|
313
|
-
logger$
|
|
314
|
-
logger$
|
|
318
|
+
logger$10.log("\n✓ Successfully logged in to Dokploy!");
|
|
319
|
+
logger$10.log(` Endpoint: ${endpoint}`);
|
|
320
|
+
logger$10.log(` Credentials stored in: ${getCredentialsPath()}`);
|
|
321
|
+
logger$10.log("\nYou can now use deploy commands without setting DOKPLOY_API_TOKEN.");
|
|
315
322
|
}
|
|
316
323
|
}
|
|
317
324
|
/**
|
|
@@ -321,28 +328,28 @@ async function logoutCommand(options) {
|
|
|
321
328
|
const { service = "dokploy" } = options;
|
|
322
329
|
if (service === "all") {
|
|
323
330
|
const dokployRemoved = await removeDokployCredentials();
|
|
324
|
-
if (dokployRemoved) logger$
|
|
325
|
-
else logger$
|
|
331
|
+
if (dokployRemoved) logger$10.log("\n✓ Logged out from all services");
|
|
332
|
+
else logger$10.log("\nNo stored credentials found");
|
|
326
333
|
return;
|
|
327
334
|
}
|
|
328
335
|
if (service === "dokploy") {
|
|
329
336
|
const removed = await removeDokployCredentials();
|
|
330
|
-
if (removed) logger$
|
|
331
|
-
else logger$
|
|
337
|
+
if (removed) logger$10.log("\n✓ Logged out from Dokploy");
|
|
338
|
+
else logger$10.log("\nNo Dokploy credentials found");
|
|
332
339
|
}
|
|
333
340
|
}
|
|
334
341
|
/**
|
|
335
342
|
* Show current login status
|
|
336
343
|
*/
|
|
337
344
|
async function whoamiCommand() {
|
|
338
|
-
logger$
|
|
345
|
+
logger$10.log("\n📋 Current credentials:\n");
|
|
339
346
|
const dokploy = await getDokployCredentials();
|
|
340
347
|
if (dokploy) {
|
|
341
|
-
logger$
|
|
342
|
-
logger$
|
|
343
|
-
logger$
|
|
344
|
-
} else logger$
|
|
345
|
-
logger$
|
|
348
|
+
logger$10.log(" Dokploy:");
|
|
349
|
+
logger$10.log(` Endpoint: ${dokploy.endpoint}`);
|
|
350
|
+
logger$10.log(` Token: ${maskToken(dokploy.token)}`);
|
|
351
|
+
} else logger$10.log(" Dokploy: Not logged in");
|
|
352
|
+
logger$10.log(`\n Credentials file: ${getCredentialsPath()}`);
|
|
346
353
|
}
|
|
347
354
|
/**
|
|
348
355
|
* Mask a token for display
|
|
@@ -428,7 +435,7 @@ function isEnabled(config) {
|
|
|
428
435
|
var CronGenerator = class extends require_openapi.ConstructGenerator {
|
|
429
436
|
async build(context, constructs, outputDir, options) {
|
|
430
437
|
const provider = options?.provider || "aws-lambda";
|
|
431
|
-
const logger$
|
|
438
|
+
const logger$11 = console;
|
|
432
439
|
const cronInfos = [];
|
|
433
440
|
if (constructs.length === 0 || provider !== "aws-lambda") return cronInfos;
|
|
434
441
|
const cronsDir = (0, node_path.join)(outputDir, "crons");
|
|
@@ -443,7 +450,7 @@ var CronGenerator = class extends require_openapi.ConstructGenerator {
|
|
|
443
450
|
memorySize: construct.memorySize,
|
|
444
451
|
environment: await construct.getEnvironment()
|
|
445
452
|
});
|
|
446
|
-
logger$
|
|
453
|
+
logger$11.log(`Generated cron handler: ${key}`);
|
|
447
454
|
}
|
|
448
455
|
return cronInfos;
|
|
449
456
|
}
|
|
@@ -479,7 +486,7 @@ var FunctionGenerator = class extends require_openapi.ConstructGenerator {
|
|
|
479
486
|
}
|
|
480
487
|
async build(context, constructs, outputDir, options) {
|
|
481
488
|
const provider = options?.provider || "aws-lambda";
|
|
482
|
-
const logger$
|
|
489
|
+
const logger$11 = console;
|
|
483
490
|
const functionInfos = [];
|
|
484
491
|
if (constructs.length === 0 || provider !== "aws-lambda") return functionInfos;
|
|
485
492
|
const functionsDir = (0, node_path.join)(outputDir, "functions");
|
|
@@ -493,7 +500,7 @@ var FunctionGenerator = class extends require_openapi.ConstructGenerator {
|
|
|
493
500
|
memorySize: construct.memorySize,
|
|
494
501
|
environment: await construct.getEnvironment()
|
|
495
502
|
});
|
|
496
|
-
logger$
|
|
503
|
+
logger$11.log(`Generated function handler: ${key}`);
|
|
497
504
|
}
|
|
498
505
|
return functionInfos;
|
|
499
506
|
}
|
|
@@ -526,11 +533,11 @@ var SubscriberGenerator = class extends require_openapi.ConstructGenerator {
|
|
|
526
533
|
}
|
|
527
534
|
async build(context, constructs, outputDir, options) {
|
|
528
535
|
const provider = options?.provider || "aws-lambda";
|
|
529
|
-
const logger$
|
|
536
|
+
const logger$11 = console;
|
|
530
537
|
const subscriberInfos = [];
|
|
531
538
|
if (provider === "server") {
|
|
532
539
|
await this.generateServerSubscribersFile(outputDir, constructs);
|
|
533
|
-
logger$
|
|
540
|
+
logger$11.log(`Generated server subscribers file with ${constructs.length} subscribers (polling mode)`);
|
|
534
541
|
return subscriberInfos;
|
|
535
542
|
}
|
|
536
543
|
if (constructs.length === 0) return subscriberInfos;
|
|
@@ -547,7 +554,7 @@ var SubscriberGenerator = class extends require_openapi.ConstructGenerator {
|
|
|
547
554
|
memorySize: construct.memorySize,
|
|
548
555
|
environment: await construct.getEnvironment()
|
|
549
556
|
});
|
|
550
|
-
logger$
|
|
557
|
+
logger$11.log(`Generated subscriber handler: ${key}`);
|
|
551
558
|
}
|
|
552
559
|
return subscriberInfos;
|
|
553
560
|
}
|
|
@@ -711,6 +718,148 @@ export async function setupSubscribers(
|
|
|
711
718
|
}
|
|
712
719
|
};
|
|
713
720
|
|
|
721
|
+
//#endregion
|
|
722
|
+
//#region src/workspace/client-generator.ts
|
|
723
|
+
const logger$9 = console;
|
|
724
|
+
/**
|
|
725
|
+
* Cache of OpenAPI spec hashes to detect changes.
|
|
726
|
+
*/
|
|
727
|
+
const specHashCache = /* @__PURE__ */ new Map();
|
|
728
|
+
/**
|
|
729
|
+
* Calculate hash of content for change detection.
|
|
730
|
+
*/
|
|
731
|
+
function hashContent(content) {
|
|
732
|
+
return (0, node_crypto.createHash)("sha256").update(content).digest("hex").slice(0, 16);
|
|
733
|
+
}
|
|
734
|
+
/**
|
|
735
|
+
* Normalize routes to an array of patterns.
|
|
736
|
+
* @internal Exported for use in dev command
|
|
737
|
+
*/
|
|
738
|
+
function normalizeRoutes(routes) {
|
|
739
|
+
if (!routes) return [];
|
|
740
|
+
return Array.isArray(routes) ? routes : [routes];
|
|
741
|
+
}
|
|
742
|
+
/**
|
|
743
|
+
* Generate OpenAPI spec for a backend app.
|
|
744
|
+
* Returns the spec content and endpoint count.
|
|
745
|
+
*/
|
|
746
|
+
async function generateBackendOpenApi(workspace, appName) {
|
|
747
|
+
const app = workspace.apps[appName];
|
|
748
|
+
if (!app || app.type !== "backend" || !app.routes) return null;
|
|
749
|
+
const appPath = (0, node_path.join)(workspace.root, app.path);
|
|
750
|
+
const routesPatterns = normalizeRoutes(app.routes);
|
|
751
|
+
if (routesPatterns.length === 0) return null;
|
|
752
|
+
const endpointGenerator = new require_openapi.EndpointGenerator();
|
|
753
|
+
const allLoadedEndpoints = [];
|
|
754
|
+
for (const pattern of routesPatterns) {
|
|
755
|
+
const fullPattern = (0, node_path.join)(appPath, pattern);
|
|
756
|
+
const loaded = await endpointGenerator.load(fullPattern);
|
|
757
|
+
allLoadedEndpoints.push(...loaded);
|
|
758
|
+
}
|
|
759
|
+
const loadedEndpoints = allLoadedEndpoints;
|
|
760
|
+
if (loadedEndpoints.length === 0) return null;
|
|
761
|
+
const endpoints = loadedEndpoints.map(({ construct }) => construct);
|
|
762
|
+
const tsGenerator = new require_openapi.OpenApiTsGenerator();
|
|
763
|
+
const content = await tsGenerator.generate(endpoints, {
|
|
764
|
+
title: `${appName} API`,
|
|
765
|
+
version: "1.0.0",
|
|
766
|
+
description: `Auto-generated API client for ${appName}`
|
|
767
|
+
});
|
|
768
|
+
return {
|
|
769
|
+
content,
|
|
770
|
+
endpointCount: loadedEndpoints.length
|
|
771
|
+
};
|
|
772
|
+
}
|
|
773
|
+
/**
|
|
774
|
+
* Generate client for a frontend app from its backend dependencies.
|
|
775
|
+
* Only regenerates if the OpenAPI spec has changed.
|
|
776
|
+
*/
|
|
777
|
+
async function generateClientForFrontend(workspace, frontendAppName, options = {}) {
|
|
778
|
+
const results = [];
|
|
779
|
+
const frontendApp = workspace.apps[frontendAppName];
|
|
780
|
+
if (!frontendApp || frontendApp.type !== "frontend") return results;
|
|
781
|
+
const dependencies$1 = frontendApp.dependencies || [];
|
|
782
|
+
const backendDeps = dependencies$1.filter((dep) => {
|
|
783
|
+
const depApp = workspace.apps[dep];
|
|
784
|
+
return depApp?.type === "backend" && depApp.routes;
|
|
785
|
+
});
|
|
786
|
+
if (backendDeps.length === 0) return results;
|
|
787
|
+
const clientOutput = frontendApp.client?.output || "src/api";
|
|
788
|
+
const frontendPath = (0, node_path.join)(workspace.root, frontendApp.path);
|
|
789
|
+
const outputDir = (0, node_path.join)(frontendPath, clientOutput);
|
|
790
|
+
for (const backendAppName of backendDeps) {
|
|
791
|
+
const result = {
|
|
792
|
+
frontendApp: frontendAppName,
|
|
793
|
+
backendApp: backendAppName,
|
|
794
|
+
outputPath: "",
|
|
795
|
+
endpointCount: 0,
|
|
796
|
+
generated: false
|
|
797
|
+
};
|
|
798
|
+
try {
|
|
799
|
+
const spec = await generateBackendOpenApi(workspace, backendAppName);
|
|
800
|
+
if (!spec) {
|
|
801
|
+
result.reason = "No endpoints found in backend";
|
|
802
|
+
results.push(result);
|
|
803
|
+
continue;
|
|
804
|
+
}
|
|
805
|
+
result.endpointCount = spec.endpointCount;
|
|
806
|
+
const cacheKey = `${backendAppName}:${frontendAppName}`;
|
|
807
|
+
const newHash = hashContent(spec.content);
|
|
808
|
+
const oldHash = specHashCache.get(cacheKey);
|
|
809
|
+
if (!options.force && oldHash === newHash) {
|
|
810
|
+
result.reason = "No schema changes detected";
|
|
811
|
+
results.push(result);
|
|
812
|
+
continue;
|
|
813
|
+
}
|
|
814
|
+
await (0, node_fs_promises.mkdir)(outputDir, { recursive: true });
|
|
815
|
+
const fileName = backendDeps.length === 1 ? "openapi.ts" : `${backendAppName}-api.ts`;
|
|
816
|
+
const outputPath = (0, node_path.join)(outputDir, fileName);
|
|
817
|
+
const backendRelPath = (0, node_path.relative)((0, node_path.dirname)(outputPath), (0, node_path.join)(workspace.root, workspace.apps[backendAppName].path));
|
|
818
|
+
const clientContent = `/**
|
|
819
|
+
* Auto-generated API client for ${backendAppName}
|
|
820
|
+
* Generated from: ${backendRelPath}
|
|
821
|
+
*
|
|
822
|
+
* DO NOT EDIT - This file is automatically regenerated when backend schemas change.
|
|
823
|
+
*/
|
|
824
|
+
|
|
825
|
+
${spec.content}
|
|
826
|
+
`;
|
|
827
|
+
await (0, node_fs_promises.writeFile)(outputPath, clientContent);
|
|
828
|
+
specHashCache.set(cacheKey, newHash);
|
|
829
|
+
result.outputPath = outputPath;
|
|
830
|
+
result.generated = true;
|
|
831
|
+
results.push(result);
|
|
832
|
+
} catch (error) {
|
|
833
|
+
result.reason = `Error: ${error.message}`;
|
|
834
|
+
results.push(result);
|
|
835
|
+
}
|
|
836
|
+
}
|
|
837
|
+
return results;
|
|
838
|
+
}
|
|
839
|
+
/**
|
|
840
|
+
* Generate clients for all frontend apps in the workspace.
|
|
841
|
+
*/
|
|
842
|
+
async function generateAllClients(workspace, options = {}) {
|
|
843
|
+
const log = options.silent ? () => {} : logger$9.log.bind(logger$9);
|
|
844
|
+
const allResults = [];
|
|
845
|
+
for (const [appName, app] of Object.entries(workspace.apps)) if (app.type === "frontend" && app.dependencies.length > 0) {
|
|
846
|
+
const results = await generateClientForFrontend(workspace, appName, { force: options.force });
|
|
847
|
+
for (const result of results) {
|
|
848
|
+
if (result.generated) log(`📦 Generated client for ${result.frontendApp} from ${result.backendApp} (${result.endpointCount} endpoints)`);
|
|
849
|
+
allResults.push(result);
|
|
850
|
+
}
|
|
851
|
+
}
|
|
852
|
+
return allResults;
|
|
853
|
+
}
|
|
854
|
+
/**
|
|
855
|
+
* Get frontend apps that depend on a backend app.
|
|
856
|
+
*/
|
|
857
|
+
function getDependentFrontends(workspace, backendAppName) {
|
|
858
|
+
const dependentApps = [];
|
|
859
|
+
for (const [appName, app] of Object.entries(workspace.apps)) if (app.type === "frontend" && app.dependencies.includes(backendAppName)) dependentApps.push(appName);
|
|
860
|
+
return dependentApps;
|
|
861
|
+
}
|
|
862
|
+
|
|
714
863
|
//#endregion
|
|
715
864
|
//#region src/dev/index.ts
|
|
716
865
|
const logger$8 = console;
|
|
@@ -865,7 +1014,12 @@ function getProductionConfigFromGkm(config) {
|
|
|
865
1014
|
async function devCommand(options) {
|
|
866
1015
|
const defaultEnv = loadEnvFiles(".env");
|
|
867
1016
|
if (defaultEnv.loaded.length > 0) logger$8.log(`📦 Loaded env: ${defaultEnv.loaded.join(", ")}`);
|
|
868
|
-
const
|
|
1017
|
+
const loadedConfig = await require_config.loadWorkspaceConfig();
|
|
1018
|
+
if (loadedConfig.type === "workspace") {
|
|
1019
|
+
logger$8.log("📦 Detected workspace configuration");
|
|
1020
|
+
return workspaceDevCommand(loadedConfig.workspace, options);
|
|
1021
|
+
}
|
|
1022
|
+
const config = loadedConfig.raw;
|
|
869
1023
|
if (config.env) {
|
|
870
1024
|
const { loaded, missing } = loadEnvFiles(config.env);
|
|
871
1025
|
if (loaded.length > 0) logger$8.log(`📦 Loaded env: ${loaded.join(", ")}`);
|
|
@@ -970,6 +1124,312 @@ async function devCommand(options) {
|
|
|
970
1124
|
process.on("SIGINT", shutdown);
|
|
971
1125
|
process.on("SIGTERM", shutdown);
|
|
972
1126
|
}
|
|
1127
|
+
/**
|
|
1128
|
+
* Generate all dependency environment variables for all apps.
|
|
1129
|
+
* Returns a flat object with all {APP_NAME}_URL variables.
|
|
1130
|
+
* @internal Exported for testing
|
|
1131
|
+
*/
|
|
1132
|
+
function generateAllDependencyEnvVars(workspace, urlPrefix = "http://localhost") {
|
|
1133
|
+
const env = {};
|
|
1134
|
+
for (const appName of Object.keys(workspace.apps)) {
|
|
1135
|
+
const appEnv = require_workspace.getDependencyEnvVars(workspace, appName, urlPrefix);
|
|
1136
|
+
Object.assign(env, appEnv);
|
|
1137
|
+
}
|
|
1138
|
+
return env;
|
|
1139
|
+
}
|
|
1140
|
+
/**
|
|
1141
|
+
* Check for port conflicts across all apps.
|
|
1142
|
+
* Returns list of conflicts if any ports are duplicated.
|
|
1143
|
+
* @internal Exported for testing
|
|
1144
|
+
*/
|
|
1145
|
+
function checkPortConflicts(workspace) {
|
|
1146
|
+
const conflicts = [];
|
|
1147
|
+
const portToApp = /* @__PURE__ */ new Map();
|
|
1148
|
+
for (const [appName, app] of Object.entries(workspace.apps)) {
|
|
1149
|
+
const existingApp = portToApp.get(app.port);
|
|
1150
|
+
if (existingApp) conflicts.push({
|
|
1151
|
+
app1: existingApp,
|
|
1152
|
+
app2: appName,
|
|
1153
|
+
port: app.port
|
|
1154
|
+
});
|
|
1155
|
+
else portToApp.set(app.port, appName);
|
|
1156
|
+
}
|
|
1157
|
+
return conflicts;
|
|
1158
|
+
}
|
|
1159
|
+
/**
|
|
1160
|
+
* Next.js config file patterns to check.
|
|
1161
|
+
*/
|
|
1162
|
+
const NEXTJS_CONFIG_FILES = [
|
|
1163
|
+
"next.config.js",
|
|
1164
|
+
"next.config.ts",
|
|
1165
|
+
"next.config.mjs"
|
|
1166
|
+
];
|
|
1167
|
+
/**
|
|
1168
|
+
* Validate a frontend (Next.js) app configuration.
|
|
1169
|
+
* Checks for Next.js config file and dependency.
|
|
1170
|
+
* @internal Exported for testing
|
|
1171
|
+
*/
|
|
1172
|
+
async function validateFrontendApp(appName, appPath, workspaceRoot) {
|
|
1173
|
+
const errors = [];
|
|
1174
|
+
const warnings = [];
|
|
1175
|
+
const fullPath = (0, node_path.join)(workspaceRoot, appPath);
|
|
1176
|
+
const hasConfigFile = NEXTJS_CONFIG_FILES.some((file) => (0, node_fs.existsSync)((0, node_path.join)(fullPath, file)));
|
|
1177
|
+
if (!hasConfigFile) errors.push(`Next.js config file not found. Expected one of: ${NEXTJS_CONFIG_FILES.join(", ")}`);
|
|
1178
|
+
const packageJsonPath = (0, node_path.join)(fullPath, "package.json");
|
|
1179
|
+
if ((0, node_fs.existsSync)(packageJsonPath)) try {
|
|
1180
|
+
const pkg$1 = require(packageJsonPath);
|
|
1181
|
+
const deps = {
|
|
1182
|
+
...pkg$1.dependencies,
|
|
1183
|
+
...pkg$1.devDependencies
|
|
1184
|
+
};
|
|
1185
|
+
if (!deps.next) errors.push("Next.js not found in dependencies. Run: pnpm add next react react-dom");
|
|
1186
|
+
if (!pkg$1.scripts?.dev) warnings.push("No \"dev\" script found in package.json. Turbo expects a \"dev\" script to run.");
|
|
1187
|
+
} catch {
|
|
1188
|
+
errors.push(`Failed to read package.json at ${packageJsonPath}`);
|
|
1189
|
+
}
|
|
1190
|
+
else errors.push(`package.json not found at ${appPath}. Run: pnpm init in the app directory.`);
|
|
1191
|
+
return {
|
|
1192
|
+
appName,
|
|
1193
|
+
valid: errors.length === 0,
|
|
1194
|
+
errors,
|
|
1195
|
+
warnings
|
|
1196
|
+
};
|
|
1197
|
+
}
|
|
1198
|
+
/**
|
|
1199
|
+
* Validate all frontend apps in the workspace.
|
|
1200
|
+
* Returns validation results for each frontend app.
|
|
1201
|
+
* @internal Exported for testing
|
|
1202
|
+
*/
|
|
1203
|
+
async function validateFrontendApps(workspace) {
|
|
1204
|
+
const results = [];
|
|
1205
|
+
for (const [appName, app] of Object.entries(workspace.apps)) if (app.type === "frontend") {
|
|
1206
|
+
const result = await validateFrontendApp(appName, app.path, workspace.root);
|
|
1207
|
+
results.push(result);
|
|
1208
|
+
}
|
|
1209
|
+
return results;
|
|
1210
|
+
}
|
|
1211
|
+
/**
|
|
1212
|
+
* Load secrets for development stage.
|
|
1213
|
+
* Returns env vars to inject, or empty object if secrets not configured/found.
|
|
1214
|
+
* @internal Exported for testing
|
|
1215
|
+
*/
|
|
1216
|
+
async function loadDevSecrets(workspace) {
|
|
1217
|
+
if (!workspace.secrets.enabled) return {};
|
|
1218
|
+
const stages = ["dev", "development"];
|
|
1219
|
+
for (const stage of stages) if (require_storage.secretsExist(stage, workspace.root)) {
|
|
1220
|
+
const secrets = await require_storage.readStageSecrets(stage, workspace.root);
|
|
1221
|
+
if (secrets) {
|
|
1222
|
+
logger$8.log(`🔐 Loading secrets from stage: ${stage}`);
|
|
1223
|
+
return require_storage.toEmbeddableSecrets(secrets);
|
|
1224
|
+
}
|
|
1225
|
+
}
|
|
1226
|
+
logger$8.warn("⚠️ Secrets enabled but no dev/development secrets found. Run \"gkm secrets:init --stage dev\"");
|
|
1227
|
+
return {};
|
|
1228
|
+
}
|
|
1229
|
+
/**
|
|
1230
|
+
* Start docker-compose services for the workspace.
|
|
1231
|
+
* @internal Exported for testing
|
|
1232
|
+
*/
|
|
1233
|
+
async function startWorkspaceServices(workspace) {
|
|
1234
|
+
const services = workspace.services;
|
|
1235
|
+
if (!services.db && !services.cache && !services.mail) return;
|
|
1236
|
+
const servicesToStart = [];
|
|
1237
|
+
if (services.db) servicesToStart.push("postgres");
|
|
1238
|
+
if (services.cache) servicesToStart.push("redis");
|
|
1239
|
+
if (services.mail) servicesToStart.push("mailpit");
|
|
1240
|
+
if (servicesToStart.length === 0) return;
|
|
1241
|
+
logger$8.log(`🐳 Starting services: ${servicesToStart.join(", ")}`);
|
|
1242
|
+
try {
|
|
1243
|
+
const composeFile = (0, node_path.join)(workspace.root, "docker-compose.yml");
|
|
1244
|
+
if (!(0, node_fs.existsSync)(composeFile)) {
|
|
1245
|
+
logger$8.warn("⚠️ No docker-compose.yml found. Services will not be started.");
|
|
1246
|
+
return;
|
|
1247
|
+
}
|
|
1248
|
+
(0, node_child_process.execSync)(`docker-compose up -d ${servicesToStart.join(" ")}`, {
|
|
1249
|
+
cwd: workspace.root,
|
|
1250
|
+
stdio: "inherit"
|
|
1251
|
+
});
|
|
1252
|
+
logger$8.log("✅ Services started");
|
|
1253
|
+
} catch (error) {
|
|
1254
|
+
logger$8.error("❌ Failed to start services:", error.message);
|
|
1255
|
+
throw error;
|
|
1256
|
+
}
|
|
1257
|
+
}
|
|
1258
|
+
/**
|
|
1259
|
+
* Workspace dev command - orchestrates multi-app development using Turbo.
|
|
1260
|
+
*
|
|
1261
|
+
* Flow:
|
|
1262
|
+
* 1. Check for port conflicts
|
|
1263
|
+
* 2. Start docker-compose services (db, cache, mail)
|
|
1264
|
+
* 3. Generate dependency URLs ({APP_NAME}_URL)
|
|
1265
|
+
* 4. Spawn turbo run dev with injected env vars
|
|
1266
|
+
*/
|
|
1267
|
+
async function workspaceDevCommand(workspace, options) {
|
|
1268
|
+
const appCount = Object.keys(workspace.apps).length;
|
|
1269
|
+
const backendApps = Object.entries(workspace.apps).filter(([_, app]) => app.type === "backend");
|
|
1270
|
+
const frontendApps = Object.entries(workspace.apps).filter(([_, app]) => app.type === "frontend");
|
|
1271
|
+
logger$8.log(`\n🚀 Starting workspace: ${workspace.name}`);
|
|
1272
|
+
logger$8.log(` ${backendApps.length} backend app(s), ${frontendApps.length} frontend app(s)`);
|
|
1273
|
+
const conflicts = checkPortConflicts(workspace);
|
|
1274
|
+
if (conflicts.length > 0) {
|
|
1275
|
+
for (const conflict of conflicts) logger$8.error(`❌ Port conflict: Apps "${conflict.app1}" and "${conflict.app2}" both use port ${conflict.port}`);
|
|
1276
|
+
throw new Error("Port conflicts detected. Please assign unique ports to each app.");
|
|
1277
|
+
}
|
|
1278
|
+
if (frontendApps.length > 0) {
|
|
1279
|
+
logger$8.log("\n🔍 Validating frontend apps...");
|
|
1280
|
+
const validationResults = await validateFrontendApps(workspace);
|
|
1281
|
+
let hasErrors = false;
|
|
1282
|
+
for (const result of validationResults) {
|
|
1283
|
+
if (!result.valid) {
|
|
1284
|
+
hasErrors = true;
|
|
1285
|
+
logger$8.error(`\n❌ Frontend app "${result.appName}" validation failed:`);
|
|
1286
|
+
for (const error of result.errors) logger$8.error(` • ${error}`);
|
|
1287
|
+
}
|
|
1288
|
+
for (const warning of result.warnings) logger$8.warn(` ⚠️ ${result.appName}: ${warning}`);
|
|
1289
|
+
}
|
|
1290
|
+
if (hasErrors) throw new Error("Frontend app validation failed. Fix the issues above and try again.");
|
|
1291
|
+
logger$8.log("✅ Frontend apps validated");
|
|
1292
|
+
}
|
|
1293
|
+
if (frontendApps.length > 0) {
|
|
1294
|
+
const clientResults = await generateAllClients(workspace, { force: true });
|
|
1295
|
+
const generatedCount = clientResults.filter((r) => r.generated).length;
|
|
1296
|
+
if (generatedCount > 0) logger$8.log(`\n📦 Generated ${generatedCount} API client(s)`);
|
|
1297
|
+
}
|
|
1298
|
+
await startWorkspaceServices(workspace);
|
|
1299
|
+
const secretsEnv = await loadDevSecrets(workspace);
|
|
1300
|
+
if (Object.keys(secretsEnv).length > 0) logger$8.log(` Loaded ${Object.keys(secretsEnv).length} secret(s)`);
|
|
1301
|
+
const dependencyEnv = generateAllDependencyEnvVars(workspace);
|
|
1302
|
+
if (Object.keys(dependencyEnv).length > 0) {
|
|
1303
|
+
logger$8.log("📡 Dependency URLs:");
|
|
1304
|
+
for (const [key, value] of Object.entries(dependencyEnv)) logger$8.log(` ${key}=${value}`);
|
|
1305
|
+
}
|
|
1306
|
+
let turboFilter = [];
|
|
1307
|
+
if (options.app) {
|
|
1308
|
+
if (!workspace.apps[options.app]) {
|
|
1309
|
+
const appNames = Object.keys(workspace.apps).join(", ");
|
|
1310
|
+
throw new Error(`App "${options.app}" not found. Available apps: ${appNames}`);
|
|
1311
|
+
}
|
|
1312
|
+
turboFilter = ["--filter", options.app];
|
|
1313
|
+
logger$8.log(`\n🎯 Running single app: ${options.app}`);
|
|
1314
|
+
} else if (options.filter) {
|
|
1315
|
+
turboFilter = ["--filter", options.filter];
|
|
1316
|
+
logger$8.log(`\n🔍 Using filter: ${options.filter}`);
|
|
1317
|
+
} else logger$8.log(`\n🎯 Running all ${appCount} apps`);
|
|
1318
|
+
const buildOrder = require_workspace.getAppBuildOrder(workspace);
|
|
1319
|
+
logger$8.log("\n📋 Apps (in dependency order):");
|
|
1320
|
+
for (const appName of buildOrder) {
|
|
1321
|
+
const app = workspace.apps[appName];
|
|
1322
|
+
if (!app) continue;
|
|
1323
|
+
const deps = app.dependencies.length > 0 ? ` (depends on: ${app.dependencies.join(", ")})` : "";
|
|
1324
|
+
logger$8.log(` ${app.type === "backend" ? "🔧" : "🌐"} ${appName} → http://localhost:${app.port}${deps}`);
|
|
1325
|
+
}
|
|
1326
|
+
const turboEnv = {
|
|
1327
|
+
...process.env,
|
|
1328
|
+
...secretsEnv,
|
|
1329
|
+
...dependencyEnv,
|
|
1330
|
+
NODE_ENV: "development"
|
|
1331
|
+
};
|
|
1332
|
+
logger$8.log("\n🏃 Starting turbo run dev...\n");
|
|
1333
|
+
const turboProcess = (0, node_child_process.spawn)("pnpm", [
|
|
1334
|
+
"turbo",
|
|
1335
|
+
"run",
|
|
1336
|
+
"dev",
|
|
1337
|
+
...turboFilter
|
|
1338
|
+
], {
|
|
1339
|
+
cwd: workspace.root,
|
|
1340
|
+
stdio: "inherit",
|
|
1341
|
+
env: turboEnv
|
|
1342
|
+
});
|
|
1343
|
+
let endpointWatcher = null;
|
|
1344
|
+
if (frontendApps.length > 0 && backendApps.length > 0) {
|
|
1345
|
+
const watchPatterns = [];
|
|
1346
|
+
const backendRouteMap = /* @__PURE__ */ new Map();
|
|
1347
|
+
for (const [appName, app] of backendApps) {
|
|
1348
|
+
const routePatterns = normalizeRoutes(app.routes);
|
|
1349
|
+
for (const routePattern of routePatterns) {
|
|
1350
|
+
const fullPattern = (0, node_path.join)(workspace.root, app.path, routePattern);
|
|
1351
|
+
watchPatterns.push(fullPattern);
|
|
1352
|
+
const patternKey = (0, node_path.join)(app.path, routePattern);
|
|
1353
|
+
const existing = backendRouteMap.get(patternKey) || [];
|
|
1354
|
+
backendRouteMap.set(patternKey, [...existing, appName]);
|
|
1355
|
+
}
|
|
1356
|
+
}
|
|
1357
|
+
if (watchPatterns.length > 0) {
|
|
1358
|
+
const resolvedFiles = await (0, fast_glob.default)(watchPatterns, {
|
|
1359
|
+
cwd: workspace.root,
|
|
1360
|
+
absolute: true,
|
|
1361
|
+
onlyFiles: true
|
|
1362
|
+
});
|
|
1363
|
+
if (resolvedFiles.length > 0) {
|
|
1364
|
+
logger$8.log(`\n👀 Watching ${resolvedFiles.length} endpoint file(s) for schema changes`);
|
|
1365
|
+
endpointWatcher = chokidar.default.watch(resolvedFiles, {
|
|
1366
|
+
ignored: /(^|[/\\])\../,
|
|
1367
|
+
persistent: true,
|
|
1368
|
+
ignoreInitial: true
|
|
1369
|
+
});
|
|
1370
|
+
let regenerateTimeout = null;
|
|
1371
|
+
endpointWatcher.on("change", async (changedPath) => {
|
|
1372
|
+
if (regenerateTimeout) clearTimeout(regenerateTimeout);
|
|
1373
|
+
regenerateTimeout = setTimeout(async () => {
|
|
1374
|
+
const changedBackends = [];
|
|
1375
|
+
for (const [appName, app] of backendApps) {
|
|
1376
|
+
const routePatterns = normalizeRoutes(app.routes);
|
|
1377
|
+
for (const routePattern of routePatterns) {
|
|
1378
|
+
const routesDir = (0, node_path.join)(workspace.root, app.path, routePattern.split("*")[0] || "");
|
|
1379
|
+
if (changedPath.startsWith(routesDir.replace(/\/$/, ""))) {
|
|
1380
|
+
changedBackends.push(appName);
|
|
1381
|
+
break;
|
|
1382
|
+
}
|
|
1383
|
+
}
|
|
1384
|
+
}
|
|
1385
|
+
if (changedBackends.length === 0) return;
|
|
1386
|
+
const affectedFrontends = /* @__PURE__ */ new Set();
|
|
1387
|
+
for (const backend of changedBackends) {
|
|
1388
|
+
const dependents = getDependentFrontends(workspace, backend);
|
|
1389
|
+
for (const frontend of dependents) affectedFrontends.add(frontend);
|
|
1390
|
+
}
|
|
1391
|
+
if (affectedFrontends.size === 0) return;
|
|
1392
|
+
logger$8.log(`\n🔄 Detected schema change in ${changedBackends.join(", ")}`);
|
|
1393
|
+
for (const frontend of affectedFrontends) try {
|
|
1394
|
+
const results = await generateClientForFrontend(workspace, frontend);
|
|
1395
|
+
for (const result of results) if (result.generated) logger$8.log(` 📦 Regenerated client for ${result.frontendApp} (${result.endpointCount} endpoints)`);
|
|
1396
|
+
} catch (error) {
|
|
1397
|
+
logger$8.error(` ❌ Failed to regenerate client for ${frontend}: ${error.message}`);
|
|
1398
|
+
}
|
|
1399
|
+
}, 500);
|
|
1400
|
+
});
|
|
1401
|
+
}
|
|
1402
|
+
}
|
|
1403
|
+
}
|
|
1404
|
+
let isShuttingDown = false;
|
|
1405
|
+
const shutdown = () => {
|
|
1406
|
+
if (isShuttingDown) return;
|
|
1407
|
+
isShuttingDown = true;
|
|
1408
|
+
logger$8.log("\n🛑 Shutting down workspace...");
|
|
1409
|
+
if (endpointWatcher) endpointWatcher.close().catch(() => {});
|
|
1410
|
+
if (turboProcess.pid) try {
|
|
1411
|
+
process.kill(-turboProcess.pid, "SIGTERM");
|
|
1412
|
+
} catch {
|
|
1413
|
+
turboProcess.kill("SIGTERM");
|
|
1414
|
+
}
|
|
1415
|
+
setTimeout(() => {
|
|
1416
|
+
process.exit(0);
|
|
1417
|
+
}, 2e3);
|
|
1418
|
+
};
|
|
1419
|
+
process.on("SIGINT", shutdown);
|
|
1420
|
+
process.on("SIGTERM", shutdown);
|
|
1421
|
+
return new Promise((resolve$1, reject) => {
|
|
1422
|
+
turboProcess.on("error", (error) => {
|
|
1423
|
+
logger$8.error("❌ Turbo error:", error);
|
|
1424
|
+
reject(error);
|
|
1425
|
+
});
|
|
1426
|
+
turboProcess.on("exit", (code) => {
|
|
1427
|
+
if (endpointWatcher) endpointWatcher.close().catch(() => {});
|
|
1428
|
+
if (code !== null && code !== 0) reject(new Error(`Turbo exited with code ${code}`));
|
|
1429
|
+
else resolve$1();
|
|
1430
|
+
});
|
|
1431
|
+
});
|
|
1432
|
+
}
|
|
973
1433
|
async function buildServer(config, context, provider, enableOpenApi) {
|
|
974
1434
|
const endpointGenerator = new require_openapi.EndpointGenerator();
|
|
975
1435
|
const functionGenerator = new FunctionGenerator();
|
|
@@ -1083,10 +1543,10 @@ var DevServer = class {
|
|
|
1083
1543
|
await this.start();
|
|
1084
1544
|
}
|
|
1085
1545
|
async createServerEntry() {
|
|
1086
|
-
const { writeFile: writeFile$
|
|
1087
|
-
const { relative: relative$
|
|
1546
|
+
const { writeFile: writeFile$9 } = await import("node:fs/promises");
|
|
1547
|
+
const { relative: relative$7, dirname: dirname$7 } = await import("node:path");
|
|
1088
1548
|
const serverPath = (0, node_path.join)(process.cwd(), ".gkm", this.provider, "server.ts");
|
|
1089
|
-
const relativeAppPath = relative$
|
|
1549
|
+
const relativeAppPath = relative$7(dirname$7(serverPath), (0, node_path.join)(dirname$7(serverPath), "app.js"));
|
|
1090
1550
|
const serveCode = this.runtime === "bun" ? `Bun.serve({
|
|
1091
1551
|
port,
|
|
1092
1552
|
fetch: app.fetch,
|
|
@@ -1126,7 +1586,7 @@ start({
|
|
|
1126
1586
|
process.exit(1);
|
|
1127
1587
|
});
|
|
1128
1588
|
`;
|
|
1129
|
-
await writeFile$
|
|
1589
|
+
await writeFile$9(serverPath, content);
|
|
1130
1590
|
}
|
|
1131
1591
|
};
|
|
1132
1592
|
|
|
@@ -1197,6 +1657,11 @@ export type RoutePath = Route['path'];
|
|
|
1197
1657
|
//#region src/build/index.ts
|
|
1198
1658
|
const logger$6 = console;
|
|
1199
1659
|
async function buildCommand(options) {
|
|
1660
|
+
const loadedConfig = await require_config.loadWorkspaceConfig();
|
|
1661
|
+
if (loadedConfig.type === "workspace") {
|
|
1662
|
+
logger$6.log("📦 Detected workspace configuration");
|
|
1663
|
+
return workspaceBuildCommand(loadedConfig.workspace, options);
|
|
1664
|
+
}
|
|
1200
1665
|
const config = await require_config.loadConfig();
|
|
1201
1666
|
const resolved = resolveProviders(config, options);
|
|
1202
1667
|
const productionConfigFromGkm = getProductionConfigFromGkm(config);
|
|
@@ -1293,7 +1758,7 @@ async function buildForProvider(provider, context, rootOutputDir, endpointGenera
|
|
|
1293
1758
|
let masterKey;
|
|
1294
1759
|
if (context.production?.bundle && !skipBundle) {
|
|
1295
1760
|
logger$6.log(`\n📦 Bundling production server...`);
|
|
1296
|
-
const { bundleServer } = await Promise.resolve().then(() => require("./bundler-
|
|
1761
|
+
const { bundleServer } = await Promise.resolve().then(() => require("./bundler-CyHg1v_T.cjs"));
|
|
1297
1762
|
const allConstructs = [
|
|
1298
1763
|
...endpoints.map((e) => e.construct),
|
|
1299
1764
|
...functions.map((f) => f.construct),
|
|
@@ -1322,6 +1787,101 @@ async function buildForProvider(provider, context, rootOutputDir, endpointGenera
|
|
|
1322
1787
|
} else await generateAwsManifest(rootOutputDir, routes, functionInfos, cronInfos, subscriberInfos);
|
|
1323
1788
|
return {};
|
|
1324
1789
|
}
|
|
1790
|
+
/**
|
|
1791
|
+
* Detect available package manager.
|
|
1792
|
+
* @internal Exported for testing
|
|
1793
|
+
*/
|
|
1794
|
+
function detectPackageManager$2() {
|
|
1795
|
+
if ((0, node_fs.existsSync)("pnpm-lock.yaml")) return "pnpm";
|
|
1796
|
+
if ((0, node_fs.existsSync)("yarn.lock")) return "yarn";
|
|
1797
|
+
return "npm";
|
|
1798
|
+
}
|
|
1799
|
+
/**
|
|
1800
|
+
* Get the turbo command for running builds.
|
|
1801
|
+
* @internal Exported for testing
|
|
1802
|
+
*/
|
|
1803
|
+
function getTurboCommand(pm, filter) {
|
|
1804
|
+
const filterArg = filter ? ` --filter=${filter}` : "";
|
|
1805
|
+
switch (pm) {
|
|
1806
|
+
case "pnpm": return `pnpm exec turbo run build${filterArg}`;
|
|
1807
|
+
case "yarn": return `yarn turbo run build${filterArg}`;
|
|
1808
|
+
case "npm": return `npx turbo run build${filterArg}`;
|
|
1809
|
+
}
|
|
1810
|
+
}
|
|
1811
|
+
/**
|
|
1812
|
+
* Build all apps in a workspace using Turbo for dependency-ordered parallel builds.
|
|
1813
|
+
* @internal Exported for testing
|
|
1814
|
+
*/
|
|
1815
|
+
async function workspaceBuildCommand(workspace, options) {
|
|
1816
|
+
const results = [];
|
|
1817
|
+
const apps = Object.entries(workspace.apps);
|
|
1818
|
+
const backendApps = apps.filter(([, app]) => app.type === "backend");
|
|
1819
|
+
const frontendApps = apps.filter(([, app]) => app.type === "frontend");
|
|
1820
|
+
logger$6.log(`\n🏗️ Building workspace: ${workspace.name}`);
|
|
1821
|
+
logger$6.log(` Backend apps: ${backendApps.map(([name$1]) => name$1).join(", ") || "none"}`);
|
|
1822
|
+
logger$6.log(` Frontend apps: ${frontendApps.map(([name$1]) => name$1).join(", ") || "none"}`);
|
|
1823
|
+
if (options.production) logger$6.log(` 🏭 Production mode enabled`);
|
|
1824
|
+
const buildOrder = require_workspace.getAppBuildOrder(workspace);
|
|
1825
|
+
logger$6.log(` Build order: ${buildOrder.join(" → ")}`);
|
|
1826
|
+
const pm = detectPackageManager$2();
|
|
1827
|
+
logger$6.log(`\n📦 Using ${pm} with Turbo for parallel builds...\n`);
|
|
1828
|
+
try {
|
|
1829
|
+
const turboCommand = getTurboCommand(pm);
|
|
1830
|
+
logger$6.log(`Running: ${turboCommand}`);
|
|
1831
|
+
await new Promise((resolve$1, reject) => {
|
|
1832
|
+
const child = (0, node_child_process.spawn)(turboCommand, {
|
|
1833
|
+
shell: true,
|
|
1834
|
+
cwd: workspace.root,
|
|
1835
|
+
stdio: "inherit",
|
|
1836
|
+
env: {
|
|
1837
|
+
...process.env,
|
|
1838
|
+
NODE_ENV: options.production ? "production" : "development"
|
|
1839
|
+
}
|
|
1840
|
+
});
|
|
1841
|
+
child.on("close", (code) => {
|
|
1842
|
+
if (code === 0) resolve$1();
|
|
1843
|
+
else reject(new Error(`Turbo build failed with exit code ${code}`));
|
|
1844
|
+
});
|
|
1845
|
+
child.on("error", (err) => {
|
|
1846
|
+
reject(err);
|
|
1847
|
+
});
|
|
1848
|
+
});
|
|
1849
|
+
for (const [appName, app] of apps) {
|
|
1850
|
+
const outputPath = getAppOutputPath(workspace, appName, app);
|
|
1851
|
+
results.push({
|
|
1852
|
+
appName,
|
|
1853
|
+
type: app.type,
|
|
1854
|
+
success: true,
|
|
1855
|
+
outputPath
|
|
1856
|
+
});
|
|
1857
|
+
}
|
|
1858
|
+
logger$6.log(`\n✅ Workspace build complete!`);
|
|
1859
|
+
logger$6.log(`\n📋 Build Summary:`);
|
|
1860
|
+
for (const result of results) {
|
|
1861
|
+
const icon = result.type === "backend" ? "⚙️" : "🌐";
|
|
1862
|
+
logger$6.log(` ${icon} ${result.appName}: ${result.outputPath || "built"}`);
|
|
1863
|
+
}
|
|
1864
|
+
} catch (error) {
|
|
1865
|
+
const errorMessage = error instanceof Error ? error.message : "Build failed";
|
|
1866
|
+
logger$6.log(`\n❌ Build failed: ${errorMessage}`);
|
|
1867
|
+
for (const [appName, app] of apps) results.push({
|
|
1868
|
+
appName,
|
|
1869
|
+
type: app.type,
|
|
1870
|
+
success: false,
|
|
1871
|
+
error: errorMessage
|
|
1872
|
+
});
|
|
1873
|
+
throw error;
|
|
1874
|
+
}
|
|
1875
|
+
return { apps: results };
|
|
1876
|
+
}
|
|
1877
|
+
/**
|
|
1878
|
+
* Get the output path for a built app.
|
|
1879
|
+
*/
|
|
1880
|
+
function getAppOutputPath(workspace, _appName, app) {
|
|
1881
|
+
const appPath = (0, node_path.join)(workspace.root, app.path);
|
|
1882
|
+
if (app.type === "frontend") return (0, node_path.join)(appPath, ".next");
|
|
1883
|
+
else return (0, node_path.join)(appPath, ".gkm");
|
|
1884
|
+
}
|
|
1325
1885
|
|
|
1326
1886
|
//#endregion
|
|
1327
1887
|
//#region src/docker/compose.ts
|
|
@@ -1509,6 +2069,163 @@ networks:
|
|
|
1509
2069
|
driver: bridge
|
|
1510
2070
|
`;
|
|
1511
2071
|
}
|
|
2072
|
+
/**
|
|
2073
|
+
* Generate docker-compose.yml for a workspace with all apps as services.
|
|
2074
|
+
* Apps can communicate with each other via service names.
|
|
2075
|
+
* @internal Exported for testing
|
|
2076
|
+
*/
|
|
2077
|
+
function generateWorkspaceCompose(workspace, options = {}) {
|
|
2078
|
+
const { registry } = options;
|
|
2079
|
+
const apps = Object.entries(workspace.apps);
|
|
2080
|
+
const services = workspace.services;
|
|
2081
|
+
const hasPostgres = services.db !== void 0 && services.db !== false;
|
|
2082
|
+
const hasRedis = services.cache !== void 0 && services.cache !== false;
|
|
2083
|
+
const hasMail = services.mail !== void 0 && services.mail !== false;
|
|
2084
|
+
const postgresImage = getInfraServiceImage("postgres", services.db);
|
|
2085
|
+
const redisImage = getInfraServiceImage("redis", services.cache);
|
|
2086
|
+
let yaml = `# Docker Compose for ${workspace.name} workspace
|
|
2087
|
+
# Generated by gkm - do not edit manually
|
|
2088
|
+
|
|
2089
|
+
services:
|
|
2090
|
+
`;
|
|
2091
|
+
for (const [appName, app] of apps) yaml += generateAppService(appName, app, apps, {
|
|
2092
|
+
registry,
|
|
2093
|
+
hasPostgres,
|
|
2094
|
+
hasRedis
|
|
2095
|
+
});
|
|
2096
|
+
if (hasPostgres) yaml += `
|
|
2097
|
+
postgres:
|
|
2098
|
+
image: ${postgresImage}
|
|
2099
|
+
container_name: ${workspace.name}-postgres
|
|
2100
|
+
restart: unless-stopped
|
|
2101
|
+
environment:
|
|
2102
|
+
POSTGRES_USER: \${POSTGRES_USER:-postgres}
|
|
2103
|
+
POSTGRES_PASSWORD: \${POSTGRES_PASSWORD:-postgres}
|
|
2104
|
+
POSTGRES_DB: \${POSTGRES_DB:-app}
|
|
2105
|
+
volumes:
|
|
2106
|
+
- postgres_data:/var/lib/postgresql/data
|
|
2107
|
+
healthcheck:
|
|
2108
|
+
test: ["CMD-SHELL", "pg_isready -U postgres"]
|
|
2109
|
+
interval: 5s
|
|
2110
|
+
timeout: 5s
|
|
2111
|
+
retries: 5
|
|
2112
|
+
networks:
|
|
2113
|
+
- workspace-network
|
|
2114
|
+
`;
|
|
2115
|
+
if (hasRedis) yaml += `
|
|
2116
|
+
redis:
|
|
2117
|
+
image: ${redisImage}
|
|
2118
|
+
container_name: ${workspace.name}-redis
|
|
2119
|
+
restart: unless-stopped
|
|
2120
|
+
volumes:
|
|
2121
|
+
- redis_data:/data
|
|
2122
|
+
healthcheck:
|
|
2123
|
+
test: ["CMD", "redis-cli", "ping"]
|
|
2124
|
+
interval: 5s
|
|
2125
|
+
timeout: 5s
|
|
2126
|
+
retries: 5
|
|
2127
|
+
networks:
|
|
2128
|
+
- workspace-network
|
|
2129
|
+
`;
|
|
2130
|
+
if (hasMail) yaml += `
|
|
2131
|
+
mailpit:
|
|
2132
|
+
image: axllent/mailpit:latest
|
|
2133
|
+
container_name: ${workspace.name}-mailpit
|
|
2134
|
+
restart: unless-stopped
|
|
2135
|
+
ports:
|
|
2136
|
+
- "8025:8025" # Web UI
|
|
2137
|
+
- "1025:1025" # SMTP
|
|
2138
|
+
networks:
|
|
2139
|
+
- workspace-network
|
|
2140
|
+
`;
|
|
2141
|
+
yaml += `
|
|
2142
|
+
volumes:
|
|
2143
|
+
`;
|
|
2144
|
+
if (hasPostgres) yaml += ` postgres_data:
|
|
2145
|
+
`;
|
|
2146
|
+
if (hasRedis) yaml += ` redis_data:
|
|
2147
|
+
`;
|
|
2148
|
+
yaml += `
|
|
2149
|
+
networks:
|
|
2150
|
+
workspace-network:
|
|
2151
|
+
driver: bridge
|
|
2152
|
+
`;
|
|
2153
|
+
return yaml;
|
|
2154
|
+
}
|
|
2155
|
+
/**
|
|
2156
|
+
* Get infrastructure service image with version.
|
|
2157
|
+
*/
|
|
2158
|
+
function getInfraServiceImage(serviceName, config) {
|
|
2159
|
+
const defaults = {
|
|
2160
|
+
postgres: "postgres:16-alpine",
|
|
2161
|
+
redis: "redis:7-alpine"
|
|
2162
|
+
};
|
|
2163
|
+
if (!config || config === true) return defaults[serviceName];
|
|
2164
|
+
if (typeof config === "object") {
|
|
2165
|
+
if (config.image) return config.image;
|
|
2166
|
+
if (config.version) {
|
|
2167
|
+
const baseImage = serviceName === "postgres" ? "postgres" : "redis";
|
|
2168
|
+
return `${baseImage}:${config.version}`;
|
|
2169
|
+
}
|
|
2170
|
+
}
|
|
2171
|
+
return defaults[serviceName];
|
|
2172
|
+
}
|
|
2173
|
+
/**
|
|
2174
|
+
* Generate a service definition for an app.
|
|
2175
|
+
*/
|
|
2176
|
+
function generateAppService(appName, app, allApps, options) {
|
|
2177
|
+
const { registry, hasPostgres, hasRedis } = options;
|
|
2178
|
+
const imageRef = registry ? `\${REGISTRY:-${registry}}/` : "";
|
|
2179
|
+
const healthCheckPath = app.type === "frontend" ? "/" : "/health";
|
|
2180
|
+
const healthCheckCmd = app.type === "frontend" ? `["CMD", "wget", "-q", "--spider", "http://localhost:${app.port}/"]` : `["CMD", "wget", "-q", "--spider", "http://localhost:${app.port}${healthCheckPath}"]`;
|
|
2181
|
+
let yaml = `
|
|
2182
|
+
${appName}:
|
|
2183
|
+
build:
|
|
2184
|
+
context: .
|
|
2185
|
+
dockerfile: .gkm/docker/Dockerfile.${appName}
|
|
2186
|
+
image: ${imageRef}\${${appName.toUpperCase()}_IMAGE:-${appName}}:\${TAG:-latest}
|
|
2187
|
+
container_name: ${appName}
|
|
2188
|
+
restart: unless-stopped
|
|
2189
|
+
ports:
|
|
2190
|
+
- "\${${appName.toUpperCase()}_PORT:-${app.port}}:${app.port}"
|
|
2191
|
+
environment:
|
|
2192
|
+
- NODE_ENV=production
|
|
2193
|
+
- PORT=${app.port}
|
|
2194
|
+
`;
|
|
2195
|
+
for (const dep of app.dependencies) {
|
|
2196
|
+
const depApp = allApps.find(([name$1]) => name$1 === dep)?.[1];
|
|
2197
|
+
if (depApp) yaml += ` - ${dep.toUpperCase()}_URL=http://${dep}:${depApp.port}
|
|
2198
|
+
`;
|
|
2199
|
+
}
|
|
2200
|
+
if (app.type === "backend") {
|
|
2201
|
+
if (hasPostgres) yaml += ` - DATABASE_URL=\${DATABASE_URL:-postgresql://postgres:postgres@postgres:5432/app}
|
|
2202
|
+
`;
|
|
2203
|
+
if (hasRedis) yaml += ` - REDIS_URL=\${REDIS_URL:-redis://redis:6379}
|
|
2204
|
+
`;
|
|
2205
|
+
}
|
|
2206
|
+
yaml += ` healthcheck:
|
|
2207
|
+
test: ${healthCheckCmd}
|
|
2208
|
+
interval: 30s
|
|
2209
|
+
timeout: 3s
|
|
2210
|
+
retries: 3
|
|
2211
|
+
`;
|
|
2212
|
+
const dependencies$1 = [...app.dependencies];
|
|
2213
|
+
if (app.type === "backend") {
|
|
2214
|
+
if (hasPostgres) dependencies$1.push("postgres");
|
|
2215
|
+
if (hasRedis) dependencies$1.push("redis");
|
|
2216
|
+
}
|
|
2217
|
+
if (dependencies$1.length > 0) {
|
|
2218
|
+
yaml += ` depends_on:
|
|
2219
|
+
`;
|
|
2220
|
+
for (const dep of dependencies$1) yaml += ` ${dep}:
|
|
2221
|
+
condition: service_healthy
|
|
2222
|
+
`;
|
|
2223
|
+
}
|
|
2224
|
+
yaml += ` networks:
|
|
2225
|
+
- workspace-network
|
|
2226
|
+
`;
|
|
2227
|
+
return yaml;
|
|
2228
|
+
}
|
|
1512
2229
|
|
|
1513
2230
|
//#endregion
|
|
1514
2231
|
//#region src/docker/templates.ts
|
|
@@ -1692,8 +2409,14 @@ WORKDIR /app
|
|
|
1692
2409
|
# Copy source (deps already installed)
|
|
1693
2410
|
COPY . .
|
|
1694
2411
|
|
|
1695
|
-
#
|
|
1696
|
-
RUN
|
|
2412
|
+
# Debug: Show node_modules/.bin contents and build production server
|
|
2413
|
+
RUN echo "=== node_modules/.bin contents ===" && \
|
|
2414
|
+
ls -la node_modules/.bin/ 2>/dev/null || echo "node_modules/.bin not found" && \
|
|
2415
|
+
echo "=== Checking for gkm ===" && \
|
|
2416
|
+
which gkm 2>/dev/null || echo "gkm not in PATH" && \
|
|
2417
|
+
ls -la node_modules/.bin/gkm 2>/dev/null || echo "gkm binary not found in node_modules/.bin" && \
|
|
2418
|
+
echo "=== Running build ===" && \
|
|
2419
|
+
./node_modules/.bin/gkm build --provider server --production
|
|
1697
2420
|
|
|
1698
2421
|
# Stage 3: Production
|
|
1699
2422
|
FROM ${baseImage} AS runner
|
|
@@ -1774,8 +2497,14 @@ WORKDIR /app
|
|
|
1774
2497
|
# Copy pruned source
|
|
1775
2498
|
COPY --from=pruner /app/out/full/ ./
|
|
1776
2499
|
|
|
1777
|
-
#
|
|
1778
|
-
RUN
|
|
2500
|
+
# Debug: Show node_modules/.bin contents and build production server
|
|
2501
|
+
RUN echo "=== node_modules/.bin contents ===" && \
|
|
2502
|
+
ls -la node_modules/.bin/ 2>/dev/null || echo "node_modules/.bin not found" && \
|
|
2503
|
+
echo "=== Checking for gkm ===" && \
|
|
2504
|
+
which gkm 2>/dev/null || echo "gkm not in PATH" && \
|
|
2505
|
+
ls -la node_modules/.bin/gkm 2>/dev/null || echo "gkm binary not found in node_modules/.bin" && \
|
|
2506
|
+
echo "=== Running build ===" && \
|
|
2507
|
+
./node_modules/.bin/gkm build --provider server --production
|
|
1779
2508
|
|
|
1780
2509
|
# Stage 4: Production
|
|
1781
2510
|
FROM ${baseImage} AS runner
|
|
@@ -1917,8 +2646,8 @@ function resolveDockerConfig$1(config) {
|
|
|
1917
2646
|
const docker = config.docker ?? {};
|
|
1918
2647
|
let defaultImageName = "api";
|
|
1919
2648
|
try {
|
|
1920
|
-
const pkg = require(`${process.cwd()}/package.json`);
|
|
1921
|
-
if (pkg.name) defaultImageName = pkg.name.replace(/^@[^/]+\//, "");
|
|
2649
|
+
const pkg$1 = require(`${process.cwd()}/package.json`);
|
|
2650
|
+
if (pkg$1.name) defaultImageName = pkg$1.name.replace(/^@[^/]+\//, "");
|
|
1922
2651
|
} catch {}
|
|
1923
2652
|
return {
|
|
1924
2653
|
registry: docker.registry ?? "",
|
|
@@ -1928,20 +2657,194 @@ function resolveDockerConfig$1(config) {
|
|
|
1928
2657
|
compose: docker.compose
|
|
1929
2658
|
};
|
|
1930
2659
|
}
|
|
1931
|
-
|
|
1932
|
-
//#endregion
|
|
1933
|
-
//#region src/docker/index.ts
|
|
1934
|
-
const logger$5 = console;
|
|
1935
2660
|
/**
|
|
1936
|
-
*
|
|
1937
|
-
*
|
|
1938
|
-
*
|
|
1939
|
-
* Default: Multi-stage Dockerfile that builds from source inside Docker
|
|
1940
|
-
* --slim: Slim Dockerfile that copies pre-built bundle (requires prior build)
|
|
2661
|
+
* Generate a Dockerfile for Next.js frontend apps using standalone output.
|
|
2662
|
+
* Uses turbo prune for monorepo optimization.
|
|
2663
|
+
* @internal Exported for testing
|
|
1941
2664
|
*/
|
|
1942
|
-
|
|
1943
|
-
const
|
|
1944
|
-
const
|
|
2665
|
+
function generateNextjsDockerfile(options) {
|
|
2666
|
+
const { baseImage, port, appPath, turboPackage, packageManager } = options;
|
|
2667
|
+
const pm = getPmConfig(packageManager);
|
|
2668
|
+
const installPm = pm.install ? `RUN ${pm.install}` : "";
|
|
2669
|
+
const turboInstallCmd = getTurboInstallCmd(packageManager);
|
|
2670
|
+
const turboCmd = packageManager === "pnpm" ? "pnpm dlx turbo" : "npx turbo";
|
|
2671
|
+
return `# syntax=docker/dockerfile:1
|
|
2672
|
+
# Next.js standalone Dockerfile with turbo prune optimization
|
|
2673
|
+
|
|
2674
|
+
# Stage 1: Prune monorepo
|
|
2675
|
+
FROM ${baseImage} AS pruner
|
|
2676
|
+
|
|
2677
|
+
WORKDIR /app
|
|
2678
|
+
|
|
2679
|
+
${installPm}
|
|
2680
|
+
|
|
2681
|
+
COPY . .
|
|
2682
|
+
|
|
2683
|
+
# Prune to only include necessary packages
|
|
2684
|
+
RUN ${turboCmd} prune ${turboPackage} --docker
|
|
2685
|
+
|
|
2686
|
+
# Stage 2: Install dependencies
|
|
2687
|
+
FROM ${baseImage} AS deps
|
|
2688
|
+
|
|
2689
|
+
WORKDIR /app
|
|
2690
|
+
|
|
2691
|
+
${installPm}
|
|
2692
|
+
|
|
2693
|
+
# Copy pruned lockfile and package.jsons
|
|
2694
|
+
COPY --from=pruner /app/out/${pm.lockfile} ./
|
|
2695
|
+
COPY --from=pruner /app/out/json/ ./
|
|
2696
|
+
|
|
2697
|
+
# Install dependencies
|
|
2698
|
+
RUN --mount=type=cache,id=${pm.cacheId},target=${pm.cacheTarget} \\
|
|
2699
|
+
${turboInstallCmd}
|
|
2700
|
+
|
|
2701
|
+
# Stage 3: Build
|
|
2702
|
+
FROM deps AS builder
|
|
2703
|
+
|
|
2704
|
+
WORKDIR /app
|
|
2705
|
+
|
|
2706
|
+
# Copy pruned source
|
|
2707
|
+
COPY --from=pruner /app/out/full/ ./
|
|
2708
|
+
|
|
2709
|
+
# Set Next.js to produce standalone output
|
|
2710
|
+
ENV NEXT_TELEMETRY_DISABLED=1
|
|
2711
|
+
|
|
2712
|
+
# Build the application
|
|
2713
|
+
RUN ${turboCmd} run build --filter=${turboPackage}
|
|
2714
|
+
|
|
2715
|
+
# Stage 4: Production
|
|
2716
|
+
FROM ${baseImage} AS runner
|
|
2717
|
+
|
|
2718
|
+
WORKDIR /app
|
|
2719
|
+
|
|
2720
|
+
# Install tini for proper signal handling
|
|
2721
|
+
RUN apk add --no-cache tini
|
|
2722
|
+
|
|
2723
|
+
# Create non-root user
|
|
2724
|
+
RUN addgroup --system --gid 1001 nodejs && \\
|
|
2725
|
+
adduser --system --uid 1001 nextjs
|
|
2726
|
+
|
|
2727
|
+
# Set environment
|
|
2728
|
+
ENV NODE_ENV=production
|
|
2729
|
+
ENV NEXT_TELEMETRY_DISABLED=1
|
|
2730
|
+
ENV PORT=${port}
|
|
2731
|
+
ENV HOSTNAME="0.0.0.0"
|
|
2732
|
+
|
|
2733
|
+
# Copy static files and standalone output
|
|
2734
|
+
COPY --from=builder --chown=nextjs:nodejs /app/${appPath}/.next/standalone ./
|
|
2735
|
+
COPY --from=builder --chown=nextjs:nodejs /app/${appPath}/.next/static ./${appPath}/.next/static
|
|
2736
|
+
COPY --from=builder --chown=nextjs:nodejs /app/${appPath}/public ./${appPath}/public
|
|
2737
|
+
|
|
2738
|
+
# Health check
|
|
2739
|
+
HEALTHCHECK --interval=30s --timeout=3s --start-period=10s --retries=3 \\
|
|
2740
|
+
CMD wget -q --spider http://localhost:${port}/ || exit 1
|
|
2741
|
+
|
|
2742
|
+
USER nextjs
|
|
2743
|
+
|
|
2744
|
+
EXPOSE ${port}
|
|
2745
|
+
|
|
2746
|
+
ENTRYPOINT ["/sbin/tini", "--"]
|
|
2747
|
+
CMD ["node", "${appPath}/server.js"]
|
|
2748
|
+
`;
|
|
2749
|
+
}
|
|
2750
|
+
/**
|
|
2751
|
+
* Generate a Dockerfile for backend apps in a workspace.
|
|
2752
|
+
* Uses turbo prune for monorepo optimization.
|
|
2753
|
+
* @internal Exported for testing
|
|
2754
|
+
*/
|
|
2755
|
+
function generateBackendDockerfile(options) {
|
|
2756
|
+
const { baseImage, port, appPath, turboPackage, packageManager, healthCheckPath = "/health" } = options;
|
|
2757
|
+
const pm = getPmConfig(packageManager);
|
|
2758
|
+
const installPm = pm.install ? `RUN ${pm.install}` : "";
|
|
2759
|
+
const turboInstallCmd = getTurboInstallCmd(packageManager);
|
|
2760
|
+
const turboCmd = packageManager === "pnpm" ? "pnpm dlx turbo" : "npx turbo";
|
|
2761
|
+
return `# syntax=docker/dockerfile:1
|
|
2762
|
+
# Backend Dockerfile with turbo prune optimization
|
|
2763
|
+
|
|
2764
|
+
# Stage 1: Prune monorepo
|
|
2765
|
+
FROM ${baseImage} AS pruner
|
|
2766
|
+
|
|
2767
|
+
WORKDIR /app
|
|
2768
|
+
|
|
2769
|
+
${installPm}
|
|
2770
|
+
|
|
2771
|
+
COPY . .
|
|
2772
|
+
|
|
2773
|
+
# Prune to only include necessary packages
|
|
2774
|
+
RUN ${turboCmd} prune ${turboPackage} --docker
|
|
2775
|
+
|
|
2776
|
+
# Stage 2: Install dependencies
|
|
2777
|
+
FROM ${baseImage} AS deps
|
|
2778
|
+
|
|
2779
|
+
WORKDIR /app
|
|
2780
|
+
|
|
2781
|
+
${installPm}
|
|
2782
|
+
|
|
2783
|
+
# Copy pruned lockfile and package.jsons
|
|
2784
|
+
COPY --from=pruner /app/out/${pm.lockfile} ./
|
|
2785
|
+
COPY --from=pruner /app/out/json/ ./
|
|
2786
|
+
|
|
2787
|
+
# Install dependencies
|
|
2788
|
+
RUN --mount=type=cache,id=${pm.cacheId},target=${pm.cacheTarget} \\
|
|
2789
|
+
${turboInstallCmd}
|
|
2790
|
+
|
|
2791
|
+
# Stage 3: Build
|
|
2792
|
+
FROM deps AS builder
|
|
2793
|
+
|
|
2794
|
+
WORKDIR /app
|
|
2795
|
+
|
|
2796
|
+
# Copy pruned source
|
|
2797
|
+
COPY --from=pruner /app/out/full/ ./
|
|
2798
|
+
|
|
2799
|
+
# Build production server using gkm
|
|
2800
|
+
RUN cd ${appPath} && ./node_modules/.bin/gkm build --provider server --production
|
|
2801
|
+
|
|
2802
|
+
# Stage 4: Production
|
|
2803
|
+
FROM ${baseImage} AS runner
|
|
2804
|
+
|
|
2805
|
+
WORKDIR /app
|
|
2806
|
+
|
|
2807
|
+
RUN apk add --no-cache tini
|
|
2808
|
+
|
|
2809
|
+
RUN addgroup --system --gid 1001 nodejs && \\
|
|
2810
|
+
adduser --system --uid 1001 hono
|
|
2811
|
+
|
|
2812
|
+
# Copy bundled server
|
|
2813
|
+
COPY --from=builder --chown=hono:nodejs /app/${appPath}/.gkm/server/dist/server.mjs ./
|
|
2814
|
+
|
|
2815
|
+
ENV NODE_ENV=production
|
|
2816
|
+
ENV PORT=${port}
|
|
2817
|
+
|
|
2818
|
+
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \\
|
|
2819
|
+
CMD wget -q --spider http://localhost:${port}${healthCheckPath} || exit 1
|
|
2820
|
+
|
|
2821
|
+
USER hono
|
|
2822
|
+
|
|
2823
|
+
EXPOSE ${port}
|
|
2824
|
+
|
|
2825
|
+
ENTRYPOINT ["/sbin/tini", "--"]
|
|
2826
|
+
CMD ["node", "server.mjs"]
|
|
2827
|
+
`;
|
|
2828
|
+
}
|
|
2829
|
+
|
|
2830
|
+
//#endregion
|
|
2831
|
+
//#region src/docker/index.ts
|
|
2832
|
+
const logger$5 = console;
|
|
2833
|
+
/**
|
|
2834
|
+
* Docker command implementation
|
|
2835
|
+
* Generates Dockerfile, docker-compose.yml, and related files
|
|
2836
|
+
*
|
|
2837
|
+
* Default: Multi-stage Dockerfile that builds from source inside Docker
|
|
2838
|
+
* --slim: Slim Dockerfile that copies pre-built bundle (requires prior build)
|
|
2839
|
+
*/
|
|
2840
|
+
async function dockerCommand(options) {
|
|
2841
|
+
const loadedConfig = await require_config.loadWorkspaceConfig();
|
|
2842
|
+
if (loadedConfig.type === "workspace") {
|
|
2843
|
+
logger$5.log("📦 Detected workspace configuration");
|
|
2844
|
+
return workspaceDockerCommand(loadedConfig.workspace, options);
|
|
2845
|
+
}
|
|
2846
|
+
const config = await require_config.loadConfig();
|
|
2847
|
+
const dockerConfig = resolveDockerConfig$1(config);
|
|
1945
2848
|
const serverConfig = typeof config.providers?.server === "object" ? config.providers.server : void 0;
|
|
1946
2849
|
const healthCheckPath = serverConfig?.production?.healthCheck ?? "/health";
|
|
1947
2850
|
const useSlim = options.slim === true;
|
|
@@ -1962,9 +2865,9 @@ async function dockerCommand(options) {
|
|
|
1962
2865
|
} else throw new Error("Monorepo detected but turbo.json not found.\n\nDocker builds in monorepos require Turborepo for proper dependency isolation.\n\nTo fix this:\n 1. Install turbo: pnpm add -Dw turbo\n 2. Create turbo.json in your monorepo root\n 3. Run this command again\n\nSee: https://turbo.build/repo/docs/guides/tools/docker");
|
|
1963
2866
|
let turboPackage = options.turboPackage ?? dockerConfig.imageName;
|
|
1964
2867
|
if (useTurbo && !options.turboPackage) try {
|
|
1965
|
-
const pkg = require(`${process.cwd()}/package.json`);
|
|
1966
|
-
if (pkg.name) {
|
|
1967
|
-
turboPackage = pkg.name;
|
|
2868
|
+
const pkg$1 = require(`${process.cwd()}/package.json`);
|
|
2869
|
+
if (pkg$1.name) {
|
|
2870
|
+
turboPackage = pkg$1.name;
|
|
1968
2871
|
logger$5.log(` Turbo package: ${turboPackage}`);
|
|
1969
2872
|
}
|
|
1970
2873
|
} catch {}
|
|
@@ -2081,6 +2984,85 @@ async function pushDockerImage(imageName, options) {
|
|
|
2081
2984
|
throw new Error(`Failed to push Docker image: ${error instanceof Error ? error.message : "Unknown error"}`);
|
|
2082
2985
|
}
|
|
2083
2986
|
}
|
|
2987
|
+
/**
|
|
2988
|
+
* Get the package name from package.json in an app directory.
|
|
2989
|
+
*/
|
|
2990
|
+
function getAppPackageName(appPath) {
|
|
2991
|
+
try {
|
|
2992
|
+
const pkg$1 = require(`${appPath}/package.json`);
|
|
2993
|
+
return pkg$1.name;
|
|
2994
|
+
} catch {
|
|
2995
|
+
return void 0;
|
|
2996
|
+
}
|
|
2997
|
+
}
|
|
2998
|
+
/**
|
|
2999
|
+
* Generate Dockerfiles for all apps in a workspace.
|
|
3000
|
+
* @internal Exported for testing
|
|
3001
|
+
*/
|
|
3002
|
+
async function workspaceDockerCommand(workspace, options) {
|
|
3003
|
+
const results = [];
|
|
3004
|
+
const apps = Object.entries(workspace.apps);
|
|
3005
|
+
logger$5.log(`\n🐳 Generating Dockerfiles for workspace: ${workspace.name}`);
|
|
3006
|
+
const dockerDir = (0, node_path.join)(workspace.root, ".gkm", "docker");
|
|
3007
|
+
await (0, node_fs_promises.mkdir)(dockerDir, { recursive: true });
|
|
3008
|
+
const packageManager = detectPackageManager$1(workspace.root);
|
|
3009
|
+
logger$5.log(` Package manager: ${packageManager}`);
|
|
3010
|
+
for (const [appName, app] of apps) {
|
|
3011
|
+
const appPath = app.path;
|
|
3012
|
+
const fullAppPath = (0, node_path.join)(workspace.root, appPath);
|
|
3013
|
+
const turboPackage = getAppPackageName(fullAppPath) ?? appName;
|
|
3014
|
+
const imageName = appName;
|
|
3015
|
+
logger$5.log(`\n 📄 Generating Dockerfile for ${appName} (${app.type})`);
|
|
3016
|
+
let dockerfile;
|
|
3017
|
+
if (app.type === "frontend") dockerfile = generateNextjsDockerfile({
|
|
3018
|
+
imageName,
|
|
3019
|
+
baseImage: "node:22-alpine",
|
|
3020
|
+
port: app.port,
|
|
3021
|
+
appPath,
|
|
3022
|
+
turboPackage,
|
|
3023
|
+
packageManager
|
|
3024
|
+
});
|
|
3025
|
+
else dockerfile = generateBackendDockerfile({
|
|
3026
|
+
imageName,
|
|
3027
|
+
baseImage: "node:22-alpine",
|
|
3028
|
+
port: app.port,
|
|
3029
|
+
appPath,
|
|
3030
|
+
turboPackage,
|
|
3031
|
+
packageManager,
|
|
3032
|
+
healthCheckPath: "/health"
|
|
3033
|
+
});
|
|
3034
|
+
const dockerfilePath = (0, node_path.join)(dockerDir, `Dockerfile.${appName}`);
|
|
3035
|
+
await (0, node_fs_promises.writeFile)(dockerfilePath, dockerfile);
|
|
3036
|
+
logger$5.log(` Generated: .gkm/docker/Dockerfile.${appName}`);
|
|
3037
|
+
results.push({
|
|
3038
|
+
appName,
|
|
3039
|
+
type: app.type,
|
|
3040
|
+
dockerfile: dockerfilePath,
|
|
3041
|
+
imageName
|
|
3042
|
+
});
|
|
3043
|
+
}
|
|
3044
|
+
const dockerignore = generateDockerignore();
|
|
3045
|
+
const dockerignorePath = (0, node_path.join)(workspace.root, ".dockerignore");
|
|
3046
|
+
await (0, node_fs_promises.writeFile)(dockerignorePath, dockerignore);
|
|
3047
|
+
logger$5.log(`\n Generated: .dockerignore (workspace root)`);
|
|
3048
|
+
const dockerCompose = generateWorkspaceCompose(workspace, { registry: options.registry });
|
|
3049
|
+
const composePath = (0, node_path.join)(dockerDir, "docker-compose.yml");
|
|
3050
|
+
await (0, node_fs_promises.writeFile)(composePath, dockerCompose);
|
|
3051
|
+
logger$5.log(` Generated: .gkm/docker/docker-compose.yml`);
|
|
3052
|
+
logger$5.log(`\n✅ Generated ${results.length} Dockerfile(s) + docker-compose.yml`);
|
|
3053
|
+
logger$5.log("\n📋 Build commands:");
|
|
3054
|
+
for (const result of results) {
|
|
3055
|
+
const icon = result.type === "backend" ? "⚙️" : "🌐";
|
|
3056
|
+
logger$5.log(` ${icon} docker build -f .gkm/docker/Dockerfile.${result.appName} -t ${result.imageName} .`);
|
|
3057
|
+
}
|
|
3058
|
+
logger$5.log("\n📋 Run all services:");
|
|
3059
|
+
logger$5.log(" docker compose -f .gkm/docker/docker-compose.yml up --build");
|
|
3060
|
+
return {
|
|
3061
|
+
apps: results,
|
|
3062
|
+
dockerCompose: composePath,
|
|
3063
|
+
dockerignore: dockerignorePath
|
|
3064
|
+
};
|
|
3065
|
+
}
|
|
2084
3066
|
|
|
2085
3067
|
//#endregion
|
|
2086
3068
|
//#region src/deploy/docker.ts
|
|
@@ -2092,8 +3074,8 @@ function getAppNameFromCwd() {
|
|
|
2092
3074
|
const packageJsonPath = (0, node_path.join)(process.cwd(), "package.json");
|
|
2093
3075
|
if (!(0, node_fs.existsSync)(packageJsonPath)) return void 0;
|
|
2094
3076
|
try {
|
|
2095
|
-
const pkg = JSON.parse((0, node_fs.readFileSync)(packageJsonPath, "utf-8"));
|
|
2096
|
-
if (pkg.name) return pkg.name.replace(/^@[^/]+\//, "");
|
|
3077
|
+
const pkg$1 = JSON.parse((0, node_fs.readFileSync)(packageJsonPath, "utf-8"));
|
|
3078
|
+
if (pkg$1.name) return pkg$1.name.replace(/^@[^/]+\//, "");
|
|
2097
3079
|
} catch {}
|
|
2098
3080
|
return void 0;
|
|
2099
3081
|
}
|
|
@@ -2109,8 +3091,8 @@ function getAppNameFromPackageJson() {
|
|
|
2109
3091
|
const packageJsonPath = (0, node_path.join)(projectRoot, "package.json");
|
|
2110
3092
|
if (!(0, node_fs.existsSync)(packageJsonPath)) return void 0;
|
|
2111
3093
|
try {
|
|
2112
|
-
const pkg = JSON.parse((0, node_fs.readFileSync)(packageJsonPath, "utf-8"));
|
|
2113
|
-
if (pkg.name) return pkg.name.replace(/^@[^/]+\//, "");
|
|
3094
|
+
const pkg$1 = JSON.parse((0, node_fs.readFileSync)(packageJsonPath, "utf-8"));
|
|
3095
|
+
if (pkg$1.name) return pkg$1.name.replace(/^@[^/]+\//, "");
|
|
2114
3096
|
} catch {}
|
|
2115
3097
|
return void 0;
|
|
2116
3098
|
}
|
|
@@ -2583,7 +3565,7 @@ async function provisionServices(api, projectId, environmentId, appName, service
|
|
|
2583
3565
|
*/
|
|
2584
3566
|
async function ensureDokploySetup(config, dockerConfig, stage, services) {
|
|
2585
3567
|
logger$1.log("\n🔧 Checking Dokploy setup...");
|
|
2586
|
-
const { readStageSecrets: readStageSecrets$1 } = await Promise.resolve().then(() => require("./storage-
|
|
3568
|
+
const { readStageSecrets: readStageSecrets$1 } = await Promise.resolve().then(() => require("./storage-fOR8dMu5.cjs"));
|
|
2587
3569
|
const existingSecrets = await readStageSecrets$1(stage);
|
|
2588
3570
|
const existingUrls = {
|
|
2589
3571
|
DATABASE_URL: existingSecrets?.urls?.DATABASE_URL,
|
|
@@ -2761,94 +3743,335 @@ function generateTag(stage) {
|
|
|
2761
3743
|
return `${stage}-${timestamp}`;
|
|
2762
3744
|
}
|
|
2763
3745
|
/**
|
|
2764
|
-
*
|
|
3746
|
+
* Deploy all apps in a workspace to Dokploy.
|
|
3747
|
+
* - Workspace maps to one Dokploy project
|
|
3748
|
+
* - Each app maps to one Dokploy application
|
|
3749
|
+
* - Deploys in dependency order (backends before dependent frontends)
|
|
3750
|
+
* - Syncs environment variables including {APP_NAME}_URL
|
|
3751
|
+
* @internal Exported for testing
|
|
2765
3752
|
*/
|
|
2766
|
-
async function
|
|
2767
|
-
const { provider, stage, tag,
|
|
2768
|
-
|
|
3753
|
+
async function workspaceDeployCommand(workspace, options) {
|
|
3754
|
+
const { provider, stage, tag, skipBuild, apps: selectedApps } = options;
|
|
3755
|
+
if (provider !== "dokploy") throw new Error(`Workspace deployment only supports Dokploy. Got: ${provider}`);
|
|
3756
|
+
logger$1.log(`\n🚀 Deploying workspace "${workspace.name}" to Dokploy...`);
|
|
2769
3757
|
logger$1.log(` Stage: ${stage}`);
|
|
2770
|
-
const config = await require_config.loadConfig();
|
|
2771
3758
|
const imageTag = tag ?? generateTag(stage);
|
|
2772
3759
|
logger$1.log(` Tag: ${imageTag}`);
|
|
2773
|
-
const
|
|
2774
|
-
|
|
2775
|
-
|
|
2776
|
-
|
|
2777
|
-
|
|
2778
|
-
|
|
2779
|
-
|
|
2780
|
-
|
|
2781
|
-
|
|
2782
|
-
const
|
|
2783
|
-
|
|
2784
|
-
|
|
2785
|
-
|
|
2786
|
-
|
|
2787
|
-
postgres: Boolean(composeServices.postgres),
|
|
2788
|
-
redis: Boolean(composeServices.redis),
|
|
2789
|
-
rabbitmq: Boolean(composeServices.rabbitmq)
|
|
2790
|
-
} : void 0;
|
|
2791
|
-
const setupResult = await ensureDokploySetup(config, dockerConfig, stage, dockerServices);
|
|
2792
|
-
dokployConfig = setupResult.config;
|
|
2793
|
-
finalRegistry = dokployConfig.registry ?? dockerConfig.registry;
|
|
2794
|
-
if (setupResult.serviceUrls) {
|
|
2795
|
-
const { readStageSecrets: readStageSecrets$1, writeStageSecrets: writeStageSecrets$1, initStageSecrets } = await Promise.resolve().then(() => require("./storage-UfyTn7Zm.cjs"));
|
|
2796
|
-
let secrets = await readStageSecrets$1(stage);
|
|
2797
|
-
if (!secrets) {
|
|
2798
|
-
logger$1.log(` Creating secrets file for stage "${stage}"...`);
|
|
2799
|
-
secrets = initStageSecrets(stage);
|
|
2800
|
-
}
|
|
2801
|
-
let updated = false;
|
|
2802
|
-
const urlFields = [
|
|
2803
|
-
"DATABASE_URL",
|
|
2804
|
-
"REDIS_URL",
|
|
2805
|
-
"RABBITMQ_URL"
|
|
2806
|
-
];
|
|
2807
|
-
for (const [key, value] of Object.entries(setupResult.serviceUrls)) {
|
|
2808
|
-
if (!value) continue;
|
|
2809
|
-
if (urlFields.includes(key)) {
|
|
2810
|
-
const urlKey = key;
|
|
2811
|
-
if (!secrets.urls[urlKey]) {
|
|
2812
|
-
secrets.urls[urlKey] = value;
|
|
2813
|
-
logger$1.log(` Saved ${key} to secrets.urls`);
|
|
2814
|
-
updated = true;
|
|
2815
|
-
}
|
|
2816
|
-
} else if (!secrets.custom[key]) {
|
|
2817
|
-
secrets.custom[key] = value;
|
|
2818
|
-
logger$1.log(` Saved ${key} to secrets.custom`);
|
|
2819
|
-
updated = true;
|
|
2820
|
-
}
|
|
2821
|
-
}
|
|
2822
|
-
if (updated) await writeStageSecrets$1(secrets);
|
|
3760
|
+
const buildOrder = require_workspace.getAppBuildOrder(workspace);
|
|
3761
|
+
let appsToDeployNames = buildOrder;
|
|
3762
|
+
if (selectedApps && selectedApps.length > 0) {
|
|
3763
|
+
const invalidApps = selectedApps.filter((name$1) => !workspace.apps[name$1]);
|
|
3764
|
+
if (invalidApps.length > 0) throw new Error(`Unknown apps: ${invalidApps.join(", ")}\nAvailable apps: ${Object.keys(workspace.apps).join(", ")}`);
|
|
3765
|
+
appsToDeployNames = buildOrder.filter((name$1) => selectedApps.includes(name$1));
|
|
3766
|
+
logger$1.log(` Deploying apps: ${appsToDeployNames.join(", ")}`);
|
|
3767
|
+
} else logger$1.log(` Deploying all apps: ${appsToDeployNames.join(", ")}`);
|
|
3768
|
+
const dokployApps = appsToDeployNames.filter((name$1) => {
|
|
3769
|
+
const app = workspace.apps[name$1];
|
|
3770
|
+
const target = app.resolvedDeployTarget;
|
|
3771
|
+
if (!require_workspace.isDeployTargetSupported(target)) {
|
|
3772
|
+
logger$1.log(` ⚠️ Skipping ${name$1}: ${require_workspace.getDeployTargetError(target, name$1)}`);
|
|
3773
|
+
return false;
|
|
2823
3774
|
}
|
|
3775
|
+
return true;
|
|
3776
|
+
});
|
|
3777
|
+
if (dokployApps.length === 0) throw new Error("No apps to deploy. All selected apps have unsupported deploy targets.");
|
|
3778
|
+
if (dokployApps.length !== appsToDeployNames.length) {
|
|
3779
|
+
const skipped = appsToDeployNames.filter((name$1) => !dokployApps.includes(name$1));
|
|
3780
|
+
logger$1.log(` 📌 ${skipped.length} app(s) skipped due to unsupported targets`);
|
|
2824
3781
|
}
|
|
2825
|
-
|
|
2826
|
-
|
|
2827
|
-
|
|
2828
|
-
|
|
2829
|
-
|
|
2830
|
-
|
|
2831
|
-
|
|
2832
|
-
|
|
2833
|
-
|
|
2834
|
-
|
|
2835
|
-
|
|
2836
|
-
|
|
2837
|
-
|
|
2838
|
-
|
|
2839
|
-
|
|
2840
|
-
|
|
2841
|
-
|
|
2842
|
-
|
|
2843
|
-
|
|
2844
|
-
|
|
2845
|
-
|
|
2846
|
-
|
|
2847
|
-
|
|
2848
|
-
|
|
2849
|
-
|
|
2850
|
-
|
|
2851
|
-
|
|
3782
|
+
appsToDeployNames = dokployApps;
|
|
3783
|
+
let creds = await getDokployCredentials();
|
|
3784
|
+
if (!creds) {
|
|
3785
|
+
logger$1.log("\n📋 Dokploy credentials not found. Let's set them up.");
|
|
3786
|
+
const endpoint = await prompt("Dokploy URL (e.g., https://dokploy.example.com): ");
|
|
3787
|
+
const normalizedEndpoint = endpoint.replace(/\/$/, "");
|
|
3788
|
+
try {
|
|
3789
|
+
new URL(normalizedEndpoint);
|
|
3790
|
+
} catch {
|
|
3791
|
+
throw new Error("Invalid URL format");
|
|
3792
|
+
}
|
|
3793
|
+
logger$1.log(`\nGenerate a token at: ${normalizedEndpoint}/settings/profile\n`);
|
|
3794
|
+
const token = await prompt("API Token: ", true);
|
|
3795
|
+
logger$1.log("\nValidating credentials...");
|
|
3796
|
+
const isValid = await validateDokployToken(normalizedEndpoint, token);
|
|
3797
|
+
if (!isValid) throw new Error("Invalid credentials. Please check your token.");
|
|
3798
|
+
await storeDokployCredentials(token, normalizedEndpoint);
|
|
3799
|
+
creds = {
|
|
3800
|
+
token,
|
|
3801
|
+
endpoint: normalizedEndpoint
|
|
3802
|
+
};
|
|
3803
|
+
logger$1.log("✓ Credentials saved");
|
|
3804
|
+
}
|
|
3805
|
+
const api = new require_dokploy_api.DokployApi({
|
|
3806
|
+
baseUrl: creds.endpoint,
|
|
3807
|
+
token: creds.token
|
|
3808
|
+
});
|
|
3809
|
+
logger$1.log("\n📁 Setting up Dokploy project...");
|
|
3810
|
+
const projectName = workspace.name;
|
|
3811
|
+
const projects = await api.listProjects();
|
|
3812
|
+
let project = projects.find((p) => p.name.toLowerCase() === projectName.toLowerCase());
|
|
3813
|
+
let environmentId;
|
|
3814
|
+
if (project) {
|
|
3815
|
+
logger$1.log(` Found existing project: ${project.name}`);
|
|
3816
|
+
const projectDetails = await api.getProject(project.projectId);
|
|
3817
|
+
const environments = projectDetails.environments ?? [];
|
|
3818
|
+
const matchingEnv = environments.find((e) => e.name.toLowerCase() === stage.toLowerCase());
|
|
3819
|
+
if (matchingEnv) {
|
|
3820
|
+
environmentId = matchingEnv.environmentId;
|
|
3821
|
+
logger$1.log(` Using environment: ${matchingEnv.name}`);
|
|
3822
|
+
} else {
|
|
3823
|
+
logger$1.log(` Creating "${stage}" environment...`);
|
|
3824
|
+
const env = await api.createEnvironment(project.projectId, stage);
|
|
3825
|
+
environmentId = env.environmentId;
|
|
3826
|
+
logger$1.log(` ✓ Created environment: ${stage}`);
|
|
3827
|
+
}
|
|
3828
|
+
} else {
|
|
3829
|
+
logger$1.log(` Creating project: ${projectName}`);
|
|
3830
|
+
const result = await api.createProject(projectName);
|
|
3831
|
+
project = result.project;
|
|
3832
|
+
if (result.environment.name.toLowerCase() !== stage.toLowerCase()) {
|
|
3833
|
+
logger$1.log(` Creating "${stage}" environment...`);
|
|
3834
|
+
const env = await api.createEnvironment(project.projectId, stage);
|
|
3835
|
+
environmentId = env.environmentId;
|
|
3836
|
+
} else environmentId = result.environment.environmentId;
|
|
3837
|
+
logger$1.log(` ✓ Created project: ${project.projectId}`);
|
|
3838
|
+
}
|
|
3839
|
+
logger$1.log("\n🐳 Checking registry...");
|
|
3840
|
+
let registryId = await getDokployRegistryId();
|
|
3841
|
+
const registry = workspace.deploy.dokploy?.registry;
|
|
3842
|
+
if (registryId) try {
|
|
3843
|
+
const reg = await api.getRegistry(registryId);
|
|
3844
|
+
logger$1.log(` Using registry: ${reg.registryName}`);
|
|
3845
|
+
} catch {
|
|
3846
|
+
logger$1.log(" ⚠ Stored registry not found, clearing...");
|
|
3847
|
+
registryId = void 0;
|
|
3848
|
+
await storeDokployRegistryId("");
|
|
3849
|
+
}
|
|
3850
|
+
if (!registryId) {
|
|
3851
|
+
const registries = await api.listRegistries();
|
|
3852
|
+
if (registries.length > 0) {
|
|
3853
|
+
registryId = registries[0].registryId;
|
|
3854
|
+
await storeDokployRegistryId(registryId);
|
|
3855
|
+
logger$1.log(` Using registry: ${registries[0].registryName}`);
|
|
3856
|
+
} else if (registry) {
|
|
3857
|
+
logger$1.log(" No registries found in Dokploy. Let's create one.");
|
|
3858
|
+
logger$1.log(` Registry URL: ${registry}`);
|
|
3859
|
+
const username = await prompt("Registry username: ");
|
|
3860
|
+
const password = await prompt("Registry password/token: ", true);
|
|
3861
|
+
const reg = await api.createRegistry("Default Registry", registry, username, password);
|
|
3862
|
+
registryId = reg.registryId;
|
|
3863
|
+
await storeDokployRegistryId(registryId);
|
|
3864
|
+
logger$1.log(` ✓ Registry created: ${registryId}`);
|
|
3865
|
+
} else logger$1.log(" ⚠ No registry configured. Set deploy.dokploy.registry in workspace config");
|
|
3866
|
+
}
|
|
3867
|
+
const services = workspace.services;
|
|
3868
|
+
const dockerServices = {
|
|
3869
|
+
postgres: services.db !== void 0 && services.db !== false,
|
|
3870
|
+
redis: services.cache !== void 0 && services.cache !== false
|
|
3871
|
+
};
|
|
3872
|
+
if (dockerServices.postgres || dockerServices.redis) {
|
|
3873
|
+
logger$1.log("\n🔧 Provisioning infrastructure services...");
|
|
3874
|
+
await provisionServices(api, project.projectId, environmentId, workspace.name, dockerServices);
|
|
3875
|
+
}
|
|
3876
|
+
const deployedAppUrls = {};
|
|
3877
|
+
logger$1.log("\n📦 Deploying applications...");
|
|
3878
|
+
const results = [];
|
|
3879
|
+
for (const appName of appsToDeployNames) {
|
|
3880
|
+
const app = workspace.apps[appName];
|
|
3881
|
+
const appPath = app.path;
|
|
3882
|
+
logger$1.log(`\n ${app.type === "backend" ? "⚙️" : "🌐"} Deploying ${appName}...`);
|
|
3883
|
+
try {
|
|
3884
|
+
const dokployAppName = `${workspace.name}-${appName}`;
|
|
3885
|
+
let application;
|
|
3886
|
+
try {
|
|
3887
|
+
application = await api.createApplication(dokployAppName, project.projectId, environmentId);
|
|
3888
|
+
logger$1.log(` Created application: ${application.applicationId}`);
|
|
3889
|
+
} catch (error) {
|
|
3890
|
+
const message = error instanceof Error ? error.message : "Unknown error";
|
|
3891
|
+
if (message.includes("already exists") || message.includes("duplicate")) logger$1.log(` Application already exists`);
|
|
3892
|
+
else throw error;
|
|
3893
|
+
}
|
|
3894
|
+
if (!skipBuild) {
|
|
3895
|
+
logger$1.log(` Building ${appName}...`);
|
|
3896
|
+
const originalCwd = process.cwd();
|
|
3897
|
+
const fullAppPath = `${workspace.root}/${appPath}`;
|
|
3898
|
+
try {
|
|
3899
|
+
process.chdir(fullAppPath);
|
|
3900
|
+
await buildCommand({
|
|
3901
|
+
provider: "server",
|
|
3902
|
+
production: true,
|
|
3903
|
+
stage
|
|
3904
|
+
});
|
|
3905
|
+
} finally {
|
|
3906
|
+
process.chdir(originalCwd);
|
|
3907
|
+
}
|
|
3908
|
+
}
|
|
3909
|
+
const imageName = `${workspace.name}-${appName}`;
|
|
3910
|
+
const imageRef = registry ? `${registry}/${imageName}:${imageTag}` : `${imageName}:${imageTag}`;
|
|
3911
|
+
logger$1.log(` Building Docker image: ${imageRef}`);
|
|
3912
|
+
await deployDocker({
|
|
3913
|
+
stage,
|
|
3914
|
+
tag: imageTag,
|
|
3915
|
+
skipPush: false,
|
|
3916
|
+
config: {
|
|
3917
|
+
registry,
|
|
3918
|
+
imageName
|
|
3919
|
+
}
|
|
3920
|
+
});
|
|
3921
|
+
const envVars = [`NODE_ENV=production`, `PORT=${app.port}`];
|
|
3922
|
+
for (const dep of app.dependencies) {
|
|
3923
|
+
const depUrl = deployedAppUrls[dep];
|
|
3924
|
+
if (depUrl) envVars.push(`${dep.toUpperCase()}_URL=${depUrl}`);
|
|
3925
|
+
}
|
|
3926
|
+
if (app.type === "backend") {
|
|
3927
|
+
if (dockerServices.postgres) envVars.push(`DATABASE_URL=\${DATABASE_URL:-postgresql://postgres:postgres@${workspace.name}-db:5432/app}`);
|
|
3928
|
+
if (dockerServices.redis) envVars.push(`REDIS_URL=\${REDIS_URL:-redis://${workspace.name}-cache:6379}`);
|
|
3929
|
+
}
|
|
3930
|
+
if (application) {
|
|
3931
|
+
await api.saveDockerProvider(application.applicationId, imageRef, { registryId });
|
|
3932
|
+
await api.saveApplicationEnv(application.applicationId, envVars.join("\n"));
|
|
3933
|
+
logger$1.log(` Deploying to Dokploy...`);
|
|
3934
|
+
await api.deployApplication(application.applicationId);
|
|
3935
|
+
const appUrl = `http://${dokployAppName}:${app.port}`;
|
|
3936
|
+
deployedAppUrls[appName] = appUrl;
|
|
3937
|
+
results.push({
|
|
3938
|
+
appName,
|
|
3939
|
+
type: app.type,
|
|
3940
|
+
success: true,
|
|
3941
|
+
applicationId: application.applicationId,
|
|
3942
|
+
imageRef
|
|
3943
|
+
});
|
|
3944
|
+
logger$1.log(` ✓ ${appName} deployed successfully`);
|
|
3945
|
+
} else {
|
|
3946
|
+
const appUrl = `http://${dokployAppName}:${app.port}`;
|
|
3947
|
+
deployedAppUrls[appName] = appUrl;
|
|
3948
|
+
results.push({
|
|
3949
|
+
appName,
|
|
3950
|
+
type: app.type,
|
|
3951
|
+
success: true,
|
|
3952
|
+
imageRef
|
|
3953
|
+
});
|
|
3954
|
+
logger$1.log(` ✓ ${appName} image pushed (app already exists)`);
|
|
3955
|
+
}
|
|
3956
|
+
} catch (error) {
|
|
3957
|
+
const message = error instanceof Error ? error.message : "Unknown error";
|
|
3958
|
+
logger$1.log(` ✗ Failed to deploy ${appName}: ${message}`);
|
|
3959
|
+
results.push({
|
|
3960
|
+
appName,
|
|
3961
|
+
type: app.type,
|
|
3962
|
+
success: false,
|
|
3963
|
+
error: message
|
|
3964
|
+
});
|
|
3965
|
+
}
|
|
3966
|
+
}
|
|
3967
|
+
const successCount = results.filter((r) => r.success).length;
|
|
3968
|
+
const failedCount = results.filter((r) => !r.success).length;
|
|
3969
|
+
logger$1.log(`\n${"─".repeat(50)}`);
|
|
3970
|
+
logger$1.log(`\n✅ Workspace deployment complete!`);
|
|
3971
|
+
logger$1.log(` Project: ${project.projectId}`);
|
|
3972
|
+
logger$1.log(` Successful: ${successCount}`);
|
|
3973
|
+
if (failedCount > 0) logger$1.log(` Failed: ${failedCount}`);
|
|
3974
|
+
return {
|
|
3975
|
+
apps: results,
|
|
3976
|
+
projectId: project.projectId,
|
|
3977
|
+
successCount,
|
|
3978
|
+
failedCount
|
|
3979
|
+
};
|
|
3980
|
+
}
|
|
3981
|
+
/**
|
|
3982
|
+
* Main deploy command
|
|
3983
|
+
*/
|
|
3984
|
+
async function deployCommand(options) {
|
|
3985
|
+
const { provider, stage, tag, skipPush, skipBuild } = options;
|
|
3986
|
+
const loadedConfig = await require_config.loadWorkspaceConfig();
|
|
3987
|
+
if (loadedConfig.type === "workspace") {
|
|
3988
|
+
logger$1.log("📦 Detected workspace configuration");
|
|
3989
|
+
return workspaceDeployCommand(loadedConfig.workspace, options);
|
|
3990
|
+
}
|
|
3991
|
+
logger$1.log(`\n🚀 Deploying to ${provider}...`);
|
|
3992
|
+
logger$1.log(` Stage: ${stage}`);
|
|
3993
|
+
const config = await require_config.loadConfig();
|
|
3994
|
+
const imageTag = tag ?? generateTag(stage);
|
|
3995
|
+
logger$1.log(` Tag: ${imageTag}`);
|
|
3996
|
+
const dockerConfig = resolveDockerConfig(config);
|
|
3997
|
+
const imageName = dockerConfig.imageName;
|
|
3998
|
+
const registry = dockerConfig.registry;
|
|
3999
|
+
const imageRef = registry ? `${registry}/${imageName}:${imageTag}` : `${imageName}:${imageTag}`;
|
|
4000
|
+
let dokployConfig;
|
|
4001
|
+
let finalRegistry = registry;
|
|
4002
|
+
if (provider === "dokploy") {
|
|
4003
|
+
const composeServices = config.docker?.compose?.services;
|
|
4004
|
+
logger$1.log(`\n🔍 Docker compose config: ${JSON.stringify(config.docker?.compose)}`);
|
|
4005
|
+
const dockerServices = composeServices ? Array.isArray(composeServices) ? {
|
|
4006
|
+
postgres: composeServices.includes("postgres"),
|
|
4007
|
+
redis: composeServices.includes("redis"),
|
|
4008
|
+
rabbitmq: composeServices.includes("rabbitmq")
|
|
4009
|
+
} : {
|
|
4010
|
+
postgres: Boolean(composeServices.postgres),
|
|
4011
|
+
redis: Boolean(composeServices.redis),
|
|
4012
|
+
rabbitmq: Boolean(composeServices.rabbitmq)
|
|
4013
|
+
} : void 0;
|
|
4014
|
+
const setupResult = await ensureDokploySetup(config, dockerConfig, stage, dockerServices);
|
|
4015
|
+
dokployConfig = setupResult.config;
|
|
4016
|
+
finalRegistry = dokployConfig.registry ?? dockerConfig.registry;
|
|
4017
|
+
if (setupResult.serviceUrls) {
|
|
4018
|
+
const { readStageSecrets: readStageSecrets$1, writeStageSecrets: writeStageSecrets$1, initStageSecrets } = await Promise.resolve().then(() => require("./storage-fOR8dMu5.cjs"));
|
|
4019
|
+
let secrets = await readStageSecrets$1(stage);
|
|
4020
|
+
if (!secrets) {
|
|
4021
|
+
logger$1.log(` Creating secrets file for stage "${stage}"...`);
|
|
4022
|
+
secrets = initStageSecrets(stage);
|
|
4023
|
+
}
|
|
4024
|
+
let updated = false;
|
|
4025
|
+
const urlFields = [
|
|
4026
|
+
"DATABASE_URL",
|
|
4027
|
+
"REDIS_URL",
|
|
4028
|
+
"RABBITMQ_URL"
|
|
4029
|
+
];
|
|
4030
|
+
for (const [key, value] of Object.entries(setupResult.serviceUrls)) {
|
|
4031
|
+
if (!value) continue;
|
|
4032
|
+
if (urlFields.includes(key)) {
|
|
4033
|
+
const urlKey = key;
|
|
4034
|
+
if (!secrets.urls[urlKey]) {
|
|
4035
|
+
secrets.urls[urlKey] = value;
|
|
4036
|
+
logger$1.log(` Saved ${key} to secrets.urls`);
|
|
4037
|
+
updated = true;
|
|
4038
|
+
}
|
|
4039
|
+
} else if (!secrets.custom[key]) {
|
|
4040
|
+
secrets.custom[key] = value;
|
|
4041
|
+
logger$1.log(` Saved ${key} to secrets.custom`);
|
|
4042
|
+
updated = true;
|
|
4043
|
+
}
|
|
4044
|
+
}
|
|
4045
|
+
if (updated) await writeStageSecrets$1(secrets);
|
|
4046
|
+
}
|
|
4047
|
+
}
|
|
4048
|
+
let masterKey;
|
|
4049
|
+
if (!skipBuild) {
|
|
4050
|
+
logger$1.log(`\n📦 Building for production...`);
|
|
4051
|
+
const buildResult = await buildCommand({
|
|
4052
|
+
provider: "server",
|
|
4053
|
+
production: true,
|
|
4054
|
+
stage
|
|
4055
|
+
});
|
|
4056
|
+
masterKey = buildResult.masterKey;
|
|
4057
|
+
} else logger$1.log(`\n⏭️ Skipping build (--skip-build)`);
|
|
4058
|
+
let result;
|
|
4059
|
+
switch (provider) {
|
|
4060
|
+
case "docker": {
|
|
4061
|
+
result = await deployDocker({
|
|
4062
|
+
stage,
|
|
4063
|
+
tag: imageTag,
|
|
4064
|
+
skipPush,
|
|
4065
|
+
masterKey,
|
|
4066
|
+
config: dockerConfig
|
|
4067
|
+
});
|
|
4068
|
+
break;
|
|
4069
|
+
}
|
|
4070
|
+
case "dokploy": {
|
|
4071
|
+
if (!dokployConfig) throw new Error("Dokploy config not initialized");
|
|
4072
|
+
const finalImageRef = finalRegistry ? `${finalRegistry}/${imageName}:${imageTag}` : `${imageName}:${imageTag}`;
|
|
4073
|
+
await deployDocker({
|
|
4074
|
+
stage,
|
|
2852
4075
|
tag: imageTag,
|
|
2853
4076
|
skipPush: false,
|
|
2854
4077
|
masterKey,
|
|
@@ -2875,10 +4098,361 @@ async function deployCommand(options) {
|
|
|
2875
4098
|
};
|
|
2876
4099
|
break;
|
|
2877
4100
|
}
|
|
2878
|
-
default: throw new Error(`Unknown deploy provider: ${provider}\nSupported providers: docker, dokploy, aws-lambda`);
|
|
2879
|
-
}
|
|
2880
|
-
logger$1.log("\n✅ Deployment complete!");
|
|
2881
|
-
return result;
|
|
4101
|
+
default: throw new Error(`Unknown deploy provider: ${provider}\nSupported providers: docker, dokploy, aws-lambda`);
|
|
4102
|
+
}
|
|
4103
|
+
logger$1.log("\n✅ Deployment complete!");
|
|
4104
|
+
return result;
|
|
4105
|
+
}
|
|
4106
|
+
|
|
4107
|
+
//#endregion
|
|
4108
|
+
//#region src/secrets/generator.ts
|
|
4109
|
+
/**
|
|
4110
|
+
* Generate a secure random password using URL-safe base64 characters.
|
|
4111
|
+
* @param length Password length (default: 32)
|
|
4112
|
+
*/
|
|
4113
|
+
function generateSecurePassword(length = 32) {
|
|
4114
|
+
return (0, node_crypto.randomBytes)(Math.ceil(length * 3 / 4)).toString("base64url").slice(0, length);
|
|
4115
|
+
}
|
|
4116
|
+
/** Default service configurations */
|
|
4117
|
+
const SERVICE_DEFAULTS = {
|
|
4118
|
+
postgres: {
|
|
4119
|
+
host: "postgres",
|
|
4120
|
+
port: 5432,
|
|
4121
|
+
username: "app",
|
|
4122
|
+
database: "app"
|
|
4123
|
+
},
|
|
4124
|
+
redis: {
|
|
4125
|
+
host: "redis",
|
|
4126
|
+
port: 6379,
|
|
4127
|
+
username: "default"
|
|
4128
|
+
},
|
|
4129
|
+
rabbitmq: {
|
|
4130
|
+
host: "rabbitmq",
|
|
4131
|
+
port: 5672,
|
|
4132
|
+
username: "app",
|
|
4133
|
+
vhost: "/"
|
|
4134
|
+
}
|
|
4135
|
+
};
|
|
4136
|
+
/**
|
|
4137
|
+
* Generate credentials for a specific service.
|
|
4138
|
+
*/
|
|
4139
|
+
function generateServiceCredentials(service) {
|
|
4140
|
+
const defaults = SERVICE_DEFAULTS[service];
|
|
4141
|
+
return {
|
|
4142
|
+
...defaults,
|
|
4143
|
+
password: generateSecurePassword()
|
|
4144
|
+
};
|
|
4145
|
+
}
|
|
4146
|
+
/**
|
|
4147
|
+
* Generate credentials for multiple services.
|
|
4148
|
+
*/
|
|
4149
|
+
function generateServicesCredentials(services) {
|
|
4150
|
+
const result = {};
|
|
4151
|
+
for (const service of services) result[service] = generateServiceCredentials(service);
|
|
4152
|
+
return result;
|
|
4153
|
+
}
|
|
4154
|
+
/**
|
|
4155
|
+
* Generate connection URL for PostgreSQL.
|
|
4156
|
+
*/
|
|
4157
|
+
function generatePostgresUrl(creds) {
|
|
4158
|
+
const { username, password, host, port, database } = creds;
|
|
4159
|
+
return `postgresql://${username}:${encodeURIComponent(password)}@${host}:${port}/${database}`;
|
|
4160
|
+
}
|
|
4161
|
+
/**
|
|
4162
|
+
* Generate connection URL for Redis.
|
|
4163
|
+
*/
|
|
4164
|
+
function generateRedisUrl(creds) {
|
|
4165
|
+
const { password, host, port } = creds;
|
|
4166
|
+
return `redis://:${encodeURIComponent(password)}@${host}:${port}`;
|
|
4167
|
+
}
|
|
4168
|
+
/**
|
|
4169
|
+
* Generate connection URL for RabbitMQ.
|
|
4170
|
+
*/
|
|
4171
|
+
function generateRabbitmqUrl(creds) {
|
|
4172
|
+
const { username, password, host, port, vhost } = creds;
|
|
4173
|
+
const encodedVhost = encodeURIComponent(vhost ?? "/");
|
|
4174
|
+
return `amqp://${username}:${encodeURIComponent(password)}@${host}:${port}/${encodedVhost}`;
|
|
4175
|
+
}
|
|
4176
|
+
/**
|
|
4177
|
+
* Generate connection URLs from service credentials.
|
|
4178
|
+
*/
|
|
4179
|
+
function generateConnectionUrls(services) {
|
|
4180
|
+
const urls = {};
|
|
4181
|
+
if (services.postgres) urls.DATABASE_URL = generatePostgresUrl(services.postgres);
|
|
4182
|
+
if (services.redis) urls.REDIS_URL = generateRedisUrl(services.redis);
|
|
4183
|
+
if (services.rabbitmq) urls.RABBITMQ_URL = generateRabbitmqUrl(services.rabbitmq);
|
|
4184
|
+
return urls;
|
|
4185
|
+
}
|
|
4186
|
+
/**
|
|
4187
|
+
* Create a new StageSecrets object with generated credentials.
|
|
4188
|
+
*/
|
|
4189
|
+
function createStageSecrets(stage, services) {
|
|
4190
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
4191
|
+
const serviceCredentials = generateServicesCredentials(services);
|
|
4192
|
+
const urls = generateConnectionUrls(serviceCredentials);
|
|
4193
|
+
return {
|
|
4194
|
+
stage,
|
|
4195
|
+
createdAt: now,
|
|
4196
|
+
updatedAt: now,
|
|
4197
|
+
services: serviceCredentials,
|
|
4198
|
+
urls,
|
|
4199
|
+
custom: {}
|
|
4200
|
+
};
|
|
4201
|
+
}
|
|
4202
|
+
/**
|
|
4203
|
+
* Rotate password for a specific service.
|
|
4204
|
+
*/
|
|
4205
|
+
function rotateServicePassword(secrets, service) {
|
|
4206
|
+
const currentCreds = secrets.services[service];
|
|
4207
|
+
if (!currentCreds) throw new Error(`Service "${service}" not configured in secrets`);
|
|
4208
|
+
const newCreds = {
|
|
4209
|
+
...currentCreds,
|
|
4210
|
+
password: generateSecurePassword()
|
|
4211
|
+
};
|
|
4212
|
+
const newServices = {
|
|
4213
|
+
...secrets.services,
|
|
4214
|
+
[service]: newCreds
|
|
4215
|
+
};
|
|
4216
|
+
return {
|
|
4217
|
+
...secrets,
|
|
4218
|
+
updatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
4219
|
+
services: newServices,
|
|
4220
|
+
urls: generateConnectionUrls(newServices)
|
|
4221
|
+
};
|
|
4222
|
+
}
|
|
4223
|
+
|
|
4224
|
+
//#endregion
|
|
4225
|
+
//#region src/init/versions.ts
|
|
4226
|
+
const require$1 = (0, node_module.createRequire)(require("url").pathToFileURL(__filename).href);
|
|
4227
|
+
const pkg = require$1("../package.json");
|
|
4228
|
+
/**
|
|
4229
|
+
* CLI version from package.json (used for scaffolded projects)
|
|
4230
|
+
*/
|
|
4231
|
+
const CLI_VERSION = `~${pkg.version}`;
|
|
4232
|
+
/**
|
|
4233
|
+
* Current released versions of @geekmidas packages
|
|
4234
|
+
* Update these when publishing new versions
|
|
4235
|
+
* Note: CLI version is read from package.json via CLI_VERSION
|
|
4236
|
+
*/
|
|
4237
|
+
const GEEKMIDAS_VERSIONS = {
|
|
4238
|
+
"@geekmidas/audit": "~0.2.0",
|
|
4239
|
+
"@geekmidas/auth": "~0.2.0",
|
|
4240
|
+
"@geekmidas/cache": "~0.2.0",
|
|
4241
|
+
"@geekmidas/cli": CLI_VERSION,
|
|
4242
|
+
"@geekmidas/client": "~0.5.0",
|
|
4243
|
+
"@geekmidas/cloud": "~0.2.0",
|
|
4244
|
+
"@geekmidas/constructs": "~0.6.0",
|
|
4245
|
+
"@geekmidas/db": "~0.3.0",
|
|
4246
|
+
"@geekmidas/emailkit": "~0.2.0",
|
|
4247
|
+
"@geekmidas/envkit": "~0.4.0",
|
|
4248
|
+
"@geekmidas/errors": "~0.1.0",
|
|
4249
|
+
"@geekmidas/events": "~0.2.0",
|
|
4250
|
+
"@geekmidas/logger": "~0.4.0",
|
|
4251
|
+
"@geekmidas/rate-limit": "~0.3.0",
|
|
4252
|
+
"@geekmidas/schema": "~0.1.0",
|
|
4253
|
+
"@geekmidas/services": "~0.2.0",
|
|
4254
|
+
"@geekmidas/storage": "~0.1.0",
|
|
4255
|
+
"@geekmidas/studio": "~0.4.0",
|
|
4256
|
+
"@geekmidas/telescope": "~0.4.0",
|
|
4257
|
+
"@geekmidas/testkit": "~0.6.0"
|
|
4258
|
+
};
|
|
4259
|
+
|
|
4260
|
+
//#endregion
|
|
4261
|
+
//#region src/init/generators/auth.ts
|
|
4262
|
+
/**
|
|
4263
|
+
* Generate auth app files for fullstack template
|
|
4264
|
+
* Uses better-auth with magic link authentication
|
|
4265
|
+
*/
|
|
4266
|
+
function generateAuthAppFiles(options) {
|
|
4267
|
+
if (!options.monorepo || options.template !== "fullstack") return [];
|
|
4268
|
+
const packageName = `@${options.name}/auth`;
|
|
4269
|
+
const modelsPackage = `@${options.name}/models`;
|
|
4270
|
+
const packageJson = {
|
|
4271
|
+
name: packageName,
|
|
4272
|
+
version: "0.0.1",
|
|
4273
|
+
private: true,
|
|
4274
|
+
type: "module",
|
|
4275
|
+
scripts: {
|
|
4276
|
+
dev: "tsx watch src/index.ts",
|
|
4277
|
+
build: "tsc",
|
|
4278
|
+
start: "node dist/index.js",
|
|
4279
|
+
typecheck: "tsc --noEmit"
|
|
4280
|
+
},
|
|
4281
|
+
dependencies: {
|
|
4282
|
+
[modelsPackage]: "workspace:*",
|
|
4283
|
+
"@geekmidas/envkit": GEEKMIDAS_VERSIONS["@geekmidas/envkit"],
|
|
4284
|
+
"@geekmidas/logger": GEEKMIDAS_VERSIONS["@geekmidas/logger"],
|
|
4285
|
+
"@hono/node-server": "~1.13.0",
|
|
4286
|
+
"better-auth": "~1.2.0",
|
|
4287
|
+
hono: "~4.8.0",
|
|
4288
|
+
kysely: "~0.27.0",
|
|
4289
|
+
pg: "~8.13.0"
|
|
4290
|
+
},
|
|
4291
|
+
devDependencies: {
|
|
4292
|
+
"@types/node": "~22.0.0",
|
|
4293
|
+
"@types/pg": "~8.11.0",
|
|
4294
|
+
tsx: "~4.20.0",
|
|
4295
|
+
typescript: "~5.8.2"
|
|
4296
|
+
}
|
|
4297
|
+
};
|
|
4298
|
+
const tsConfig = {
|
|
4299
|
+
extends: "../../tsconfig.json",
|
|
4300
|
+
compilerOptions: {
|
|
4301
|
+
noEmit: true,
|
|
4302
|
+
baseUrl: ".",
|
|
4303
|
+
paths: { [`@${options.name}/*`]: ["../../packages/*/src"] }
|
|
4304
|
+
},
|
|
4305
|
+
include: ["src/**/*.ts"],
|
|
4306
|
+
exclude: ["node_modules", "dist"]
|
|
4307
|
+
};
|
|
4308
|
+
const envTs = `import { Credentials } from '@geekmidas/envkit/credentials';
|
|
4309
|
+
import { EnvironmentParser } from '@geekmidas/envkit';
|
|
4310
|
+
|
|
4311
|
+
export const envParser = new EnvironmentParser({ ...process.env, ...Credentials });
|
|
4312
|
+
|
|
4313
|
+
// Global config - only minimal shared values
|
|
4314
|
+
// Service-specific config should be parsed where needed
|
|
4315
|
+
export const config = envParser
|
|
4316
|
+
.create((get) => ({
|
|
4317
|
+
nodeEnv: get('NODE_ENV').enum(['development', 'test', 'production']).default('development'),
|
|
4318
|
+
stage: get('STAGE').enum(['development', 'staging', 'production']).default('development'),
|
|
4319
|
+
}))
|
|
4320
|
+
.parse();
|
|
4321
|
+
`;
|
|
4322
|
+
const loggerTs = `import { createLogger } from '@geekmidas/logger/${options.loggerType}';
|
|
4323
|
+
|
|
4324
|
+
export const logger = createLogger();
|
|
4325
|
+
`;
|
|
4326
|
+
const authTs = `import { betterAuth } from 'better-auth';
|
|
4327
|
+
import { magicLink } from 'better-auth/plugins';
|
|
4328
|
+
import { Pool } from 'pg';
|
|
4329
|
+
import { envParser } from './config/env.js';
|
|
4330
|
+
import { logger } from './config/logger.js';
|
|
4331
|
+
|
|
4332
|
+
// Parse auth-specific config (no defaults - values from secrets)
|
|
4333
|
+
const authConfig = envParser
|
|
4334
|
+
.create((get) => ({
|
|
4335
|
+
databaseUrl: get('DATABASE_URL').string(),
|
|
4336
|
+
baseUrl: get('BETTER_AUTH_URL').string(),
|
|
4337
|
+
trustedOrigins: get('BETTER_AUTH_TRUSTED_ORIGINS').string(),
|
|
4338
|
+
secret: get('BETTER_AUTH_SECRET').string(),
|
|
4339
|
+
}))
|
|
4340
|
+
.parse();
|
|
4341
|
+
|
|
4342
|
+
export const auth = betterAuth({
|
|
4343
|
+
database: new Pool({
|
|
4344
|
+
connectionString: authConfig.databaseUrl,
|
|
4345
|
+
}),
|
|
4346
|
+
baseURL: authConfig.baseUrl,
|
|
4347
|
+
trustedOrigins: authConfig.trustedOrigins.split(','),
|
|
4348
|
+
secret: authConfig.secret,
|
|
4349
|
+
plugins: [
|
|
4350
|
+
magicLink({
|
|
4351
|
+
sendMagicLink: async ({ email, url }) => {
|
|
4352
|
+
// TODO: Implement email sending using @geekmidas/emailkit
|
|
4353
|
+
// For development, log the magic link
|
|
4354
|
+
logger.info({ email, url }, 'Magic link generated');
|
|
4355
|
+
console.log('\\n================================');
|
|
4356
|
+
console.log('MAGIC LINK FOR:', email);
|
|
4357
|
+
console.log(url);
|
|
4358
|
+
console.log('================================\\n');
|
|
4359
|
+
},
|
|
4360
|
+
expiresIn: 300, // 5 minutes
|
|
4361
|
+
}),
|
|
4362
|
+
],
|
|
4363
|
+
emailAndPassword: {
|
|
4364
|
+
enabled: false, // Only magic link for now
|
|
4365
|
+
},
|
|
4366
|
+
});
|
|
4367
|
+
|
|
4368
|
+
export type Auth = typeof auth;
|
|
4369
|
+
`;
|
|
4370
|
+
const indexTs = `import { Hono } from 'hono';
|
|
4371
|
+
import { cors } from 'hono/cors';
|
|
4372
|
+
import { serve } from '@hono/node-server';
|
|
4373
|
+
import { auth } from './auth.js';
|
|
4374
|
+
import { envParser } from './config/env.js';
|
|
4375
|
+
import { logger } from './config/logger.js';
|
|
4376
|
+
|
|
4377
|
+
// Parse server config (no defaults - values from secrets)
|
|
4378
|
+
const serverConfig = envParser
|
|
4379
|
+
.create((get) => ({
|
|
4380
|
+
port: get('PORT').string().transform(Number),
|
|
4381
|
+
trustedOrigins: get('BETTER_AUTH_TRUSTED_ORIGINS').string(),
|
|
4382
|
+
}))
|
|
4383
|
+
.parse();
|
|
4384
|
+
|
|
4385
|
+
const app = new Hono();
|
|
4386
|
+
|
|
4387
|
+
// CORS must be registered before routes
|
|
4388
|
+
app.use(
|
|
4389
|
+
'/api/auth/*',
|
|
4390
|
+
cors({
|
|
4391
|
+
origin: serverConfig.trustedOrigins.split(','),
|
|
4392
|
+
allowHeaders: ['Content-Type', 'Authorization'],
|
|
4393
|
+
allowMethods: ['POST', 'GET', 'OPTIONS'],
|
|
4394
|
+
credentials: true,
|
|
4395
|
+
}),
|
|
4396
|
+
);
|
|
4397
|
+
|
|
4398
|
+
// Health check endpoint
|
|
4399
|
+
app.get('/health', (c) => {
|
|
4400
|
+
return c.json({
|
|
4401
|
+
status: 'ok',
|
|
4402
|
+
service: 'auth',
|
|
4403
|
+
timestamp: new Date().toISOString(),
|
|
4404
|
+
});
|
|
4405
|
+
});
|
|
4406
|
+
|
|
4407
|
+
// Mount better-auth handler
|
|
4408
|
+
app.on(['POST', 'GET'], '/api/auth/*', (c) => {
|
|
4409
|
+
return auth.handler(c.req.raw);
|
|
4410
|
+
});
|
|
4411
|
+
|
|
4412
|
+
logger.info({ port: serverConfig.port }, 'Starting auth server');
|
|
4413
|
+
|
|
4414
|
+
serve({
|
|
4415
|
+
fetch: app.fetch,
|
|
4416
|
+
port: serverConfig.port,
|
|
4417
|
+
}, (info) => {
|
|
4418
|
+
logger.info({ port: info.port }, 'Auth server running');
|
|
4419
|
+
});
|
|
4420
|
+
`;
|
|
4421
|
+
const gitignore = `node_modules/
|
|
4422
|
+
dist/
|
|
4423
|
+
.env.local
|
|
4424
|
+
*.log
|
|
4425
|
+
`;
|
|
4426
|
+
return [
|
|
4427
|
+
{
|
|
4428
|
+
path: "apps/auth/package.json",
|
|
4429
|
+
content: `${JSON.stringify(packageJson, null, 2)}\n`
|
|
4430
|
+
},
|
|
4431
|
+
{
|
|
4432
|
+
path: "apps/auth/tsconfig.json",
|
|
4433
|
+
content: `${JSON.stringify(tsConfig, null, 2)}\n`
|
|
4434
|
+
},
|
|
4435
|
+
{
|
|
4436
|
+
path: "apps/auth/src/config/env.ts",
|
|
4437
|
+
content: envTs
|
|
4438
|
+
},
|
|
4439
|
+
{
|
|
4440
|
+
path: "apps/auth/src/config/logger.ts",
|
|
4441
|
+
content: loggerTs
|
|
4442
|
+
},
|
|
4443
|
+
{
|
|
4444
|
+
path: "apps/auth/src/auth.ts",
|
|
4445
|
+
content: authTs
|
|
4446
|
+
},
|
|
4447
|
+
{
|
|
4448
|
+
path: "apps/auth/src/index.ts",
|
|
4449
|
+
content: indexTs
|
|
4450
|
+
},
|
|
4451
|
+
{
|
|
4452
|
+
path: "apps/auth/.gitignore",
|
|
4453
|
+
content: gitignore
|
|
4454
|
+
}
|
|
4455
|
+
];
|
|
2882
4456
|
}
|
|
2883
4457
|
|
|
2884
4458
|
//#endregion
|
|
@@ -2890,6 +4464,7 @@ function generateConfigFiles(options, template) {
|
|
|
2890
4464
|
const { telescope, studio, routesStructure } = options;
|
|
2891
4465
|
const isServerless = template.name === "serverless";
|
|
2892
4466
|
const hasWorker = template.name === "worker";
|
|
4467
|
+
const isFullstack = options.template === "fullstack";
|
|
2893
4468
|
const getRoutesGlob = () => {
|
|
2894
4469
|
switch (routesStructure) {
|
|
2895
4470
|
case "centralized-endpoints": return "./src/endpoints/**/*.ts";
|
|
@@ -2897,6 +4472,14 @@ function generateConfigFiles(options, template) {
|
|
|
2897
4472
|
case "domain-based": return "./src/**/routes/*.ts";
|
|
2898
4473
|
}
|
|
2899
4474
|
};
|
|
4475
|
+
if (isFullstack) return generateSingleAppConfigFiles(options, template, {
|
|
4476
|
+
telescope,
|
|
4477
|
+
studio,
|
|
4478
|
+
routesStructure,
|
|
4479
|
+
isServerless,
|
|
4480
|
+
hasWorker,
|
|
4481
|
+
getRoutesGlob
|
|
4482
|
+
});
|
|
2900
4483
|
let gkmConfig = `import { defineConfig } from '@geekmidas/cli/config';
|
|
2901
4484
|
|
|
2902
4485
|
export default defineConfig({
|
|
@@ -2925,8 +4508,7 @@ export default defineConfig({
|
|
|
2925
4508
|
const tsConfig = options.monorepo ? {
|
|
2926
4509
|
extends: "../../tsconfig.json",
|
|
2927
4510
|
compilerOptions: {
|
|
2928
|
-
|
|
2929
|
-
rootDir: "./src",
|
|
4511
|
+
noEmit: true,
|
|
2930
4512
|
baseUrl: ".",
|
|
2931
4513
|
paths: { [`@${options.name}/*`]: ["../../packages/*/src"] }
|
|
2932
4514
|
},
|
|
@@ -2959,7 +4541,7 @@ export default defineConfig({
|
|
|
2959
4541
|
content: `${JSON.stringify(tsConfig, null, 2)}\n`
|
|
2960
4542
|
}];
|
|
2961
4543
|
const biomeConfig = {
|
|
2962
|
-
$schema: "https://biomejs.dev/schemas/
|
|
4544
|
+
$schema: "https://biomejs.dev/schemas/2.3.0/schema.json",
|
|
2963
4545
|
vcs: {
|
|
2964
4546
|
enabled: true,
|
|
2965
4547
|
clientKind: "git",
|
|
@@ -3042,23 +4624,46 @@ export default defineConfig({
|
|
|
3042
4624
|
}
|
|
3043
4625
|
];
|
|
3044
4626
|
}
|
|
4627
|
+
function generateSingleAppConfigFiles(options, _template, _helpers) {
|
|
4628
|
+
const tsConfig = {
|
|
4629
|
+
extends: "../../tsconfig.json",
|
|
4630
|
+
compilerOptions: {
|
|
4631
|
+
noEmit: true,
|
|
4632
|
+
baseUrl: ".",
|
|
4633
|
+
paths: { [`@${options.name}/*`]: ["../../packages/*/src"] }
|
|
4634
|
+
},
|
|
4635
|
+
include: ["src/**/*.ts"],
|
|
4636
|
+
exclude: ["node_modules", "dist"]
|
|
4637
|
+
};
|
|
4638
|
+
return [{
|
|
4639
|
+
path: "tsconfig.json",
|
|
4640
|
+
content: `${JSON.stringify(tsConfig, null, 2)}\n`
|
|
4641
|
+
}];
|
|
4642
|
+
}
|
|
3045
4643
|
|
|
3046
4644
|
//#endregion
|
|
3047
4645
|
//#region src/init/generators/docker.ts
|
|
3048
4646
|
/**
|
|
3049
4647
|
* Generate docker-compose.yml based on template and options
|
|
3050
4648
|
*/
|
|
3051
|
-
function generateDockerFiles(options, template) {
|
|
4649
|
+
function generateDockerFiles(options, template, dbApps) {
|
|
3052
4650
|
const { database } = options;
|
|
3053
4651
|
const isServerless = template.name === "serverless";
|
|
3054
4652
|
const hasWorker = template.name === "worker";
|
|
4653
|
+
const isFullstack = options.template === "fullstack";
|
|
3055
4654
|
const services = [];
|
|
3056
4655
|
const volumes = [];
|
|
4656
|
+
const files = [];
|
|
3057
4657
|
if (database) {
|
|
4658
|
+
const initVolume = isFullstack && dbApps?.length ? `
|
|
4659
|
+
- ./docker/postgres/init.sh:/docker-entrypoint-initdb.d/init.sh:ro` : "";
|
|
4660
|
+
const envFile = isFullstack && dbApps?.length ? `
|
|
4661
|
+
env_file:
|
|
4662
|
+
- ./docker/.env` : "";
|
|
3058
4663
|
services.push(` postgres:
|
|
3059
4664
|
image: postgres:16-alpine
|
|
3060
4665
|
container_name: ${options.name}-postgres
|
|
3061
|
-
restart: unless-stopped
|
|
4666
|
+
restart: unless-stopped${envFile}
|
|
3062
4667
|
environment:
|
|
3063
4668
|
POSTGRES_USER: postgres
|
|
3064
4669
|
POSTGRES_PASSWORD: postgres
|
|
@@ -3066,13 +4671,23 @@ function generateDockerFiles(options, template) {
|
|
|
3066
4671
|
ports:
|
|
3067
4672
|
- '5432:5432'
|
|
3068
4673
|
volumes:
|
|
3069
|
-
- postgres_data:/var/lib/postgresql/data
|
|
4674
|
+
- postgres_data:/var/lib/postgresql/data${initVolume}
|
|
3070
4675
|
healthcheck:
|
|
3071
4676
|
test: ['CMD-SHELL', 'pg_isready -U postgres']
|
|
3072
4677
|
interval: 5s
|
|
3073
4678
|
timeout: 5s
|
|
3074
4679
|
retries: 5`);
|
|
3075
4680
|
volumes.push(" postgres_data:");
|
|
4681
|
+
if (isFullstack && dbApps?.length) {
|
|
4682
|
+
files.push({
|
|
4683
|
+
path: "docker/postgres/init.sh",
|
|
4684
|
+
content: generatePostgresInitScript(dbApps)
|
|
4685
|
+
});
|
|
4686
|
+
files.push({
|
|
4687
|
+
path: "docker/.env",
|
|
4688
|
+
content: generateDockerEnv(dbApps)
|
|
4689
|
+
});
|
|
4690
|
+
}
|
|
3076
4691
|
}
|
|
3077
4692
|
if (isServerless) {
|
|
3078
4693
|
services.push(` redis:
|
|
@@ -3148,105 +4763,85 @@ ${services.join("\n\n")}
|
|
|
3148
4763
|
volumes:
|
|
3149
4764
|
${volumes.join("\n")}
|
|
3150
4765
|
`;
|
|
3151
|
-
|
|
4766
|
+
files.push({
|
|
3152
4767
|
path: "docker-compose.yml",
|
|
3153
4768
|
content: dockerCompose
|
|
3154
|
-
}
|
|
4769
|
+
});
|
|
4770
|
+
return files;
|
|
3155
4771
|
}
|
|
3156
|
-
|
|
3157
|
-
//#endregion
|
|
3158
|
-
//#region src/init/generators/env.ts
|
|
3159
4772
|
/**
|
|
3160
|
-
* Generate
|
|
4773
|
+
* Generate .env file for docker-compose with database passwords
|
|
3161
4774
|
*/
|
|
3162
|
-
function
|
|
3163
|
-
const
|
|
3164
|
-
|
|
3165
|
-
|
|
3166
|
-
|
|
3167
|
-
|
|
3168
|
-
|
|
3169
|
-
|
|
3170
|
-
|
|
3171
|
-
if (isServerless) baseEnv = `# AWS
|
|
3172
|
-
STAGE=dev
|
|
3173
|
-
AWS_REGION=us-east-1
|
|
3174
|
-
LOG_LEVEL=info
|
|
3175
|
-
`;
|
|
3176
|
-
if (database) baseEnv += `
|
|
3177
|
-
# Database
|
|
3178
|
-
DATABASE_URL=postgresql://user:password@localhost:5432/mydb
|
|
3179
|
-
`;
|
|
3180
|
-
if (hasWorker) baseEnv += `
|
|
3181
|
-
# Message Queue
|
|
3182
|
-
RABBITMQ_URL=amqp://localhost:5672
|
|
3183
|
-
`;
|
|
3184
|
-
baseEnv += `
|
|
3185
|
-
# Authentication
|
|
3186
|
-
JWT_SECRET=your-secret-key-change-in-production
|
|
3187
|
-
`;
|
|
3188
|
-
let devEnv = `# Development Environment
|
|
3189
|
-
NODE_ENV=development
|
|
3190
|
-
PORT=3000
|
|
3191
|
-
LOG_LEVEL=debug
|
|
3192
|
-
`;
|
|
3193
|
-
if (isServerless) devEnv = `# Development Environment
|
|
3194
|
-
STAGE=dev
|
|
3195
|
-
AWS_REGION=us-east-1
|
|
3196
|
-
LOG_LEVEL=debug
|
|
3197
|
-
`;
|
|
3198
|
-
if (database) devEnv += `
|
|
3199
|
-
# Database
|
|
3200
|
-
DATABASE_URL=postgresql://postgres:postgres@localhost:5432/mydb_dev
|
|
3201
|
-
`;
|
|
3202
|
-
if (hasWorker) devEnv += `
|
|
3203
|
-
# Message Queue
|
|
3204
|
-
RABBITMQ_URL=amqp://localhost:5672
|
|
3205
|
-
`;
|
|
3206
|
-
devEnv += `
|
|
3207
|
-
# Authentication
|
|
3208
|
-
JWT_SECRET=dev-secret-not-for-production
|
|
3209
|
-
`;
|
|
3210
|
-
let testEnv = `# Test Environment
|
|
3211
|
-
NODE_ENV=test
|
|
3212
|
-
PORT=3001
|
|
3213
|
-
LOG_LEVEL=error
|
|
3214
|
-
`;
|
|
3215
|
-
if (isServerless) testEnv = `# Test Environment
|
|
3216
|
-
STAGE=test
|
|
3217
|
-
AWS_REGION=us-east-1
|
|
3218
|
-
LOG_LEVEL=error
|
|
4775
|
+
function generateDockerEnv(apps) {
|
|
4776
|
+
const envVars = apps.map((app) => {
|
|
4777
|
+
const envVar = `${app.name.toUpperCase()}_DB_PASSWORD`;
|
|
4778
|
+
return `${envVar}=${app.password}`;
|
|
4779
|
+
});
|
|
4780
|
+
return `# Auto-generated docker environment file
|
|
4781
|
+
# Contains database passwords for docker-compose postgres init
|
|
4782
|
+
# This file is gitignored - do not commit to version control
|
|
4783
|
+
${envVars.join("\n")}
|
|
3219
4784
|
`;
|
|
3220
|
-
|
|
3221
|
-
|
|
3222
|
-
|
|
4785
|
+
}
|
|
4786
|
+
/**
|
|
4787
|
+
* Generate PostgreSQL init shell script that creates per-app users with separate schemas
|
|
4788
|
+
* Uses environment variables for passwords (more secure than hardcoded values)
|
|
4789
|
+
* - api user: uses public schema
|
|
4790
|
+
* - auth user: uses auth schema with search_path=auth
|
|
4791
|
+
*/
|
|
4792
|
+
function generatePostgresInitScript(apps) {
|
|
4793
|
+
const userCreations = apps.map((app) => {
|
|
4794
|
+
const userName = app.name.replace(/-/g, "_");
|
|
4795
|
+
const envVar = `${app.name.toUpperCase()}_DB_PASSWORD`;
|
|
4796
|
+
const isApi = app.name === "api";
|
|
4797
|
+
const schemaName = isApi ? "public" : userName;
|
|
4798
|
+
if (isApi) return `
|
|
4799
|
+
# Create ${app.name} user (uses public schema)
|
|
4800
|
+
echo "Creating user ${userName}..."
|
|
4801
|
+
psql -v ON_ERROR_STOP=1 --username "$POSTGRES_USER" --dbname "$POSTGRES_DB" <<-EOSQL
|
|
4802
|
+
CREATE USER ${userName} WITH PASSWORD '$${envVar}';
|
|
4803
|
+
GRANT ALL ON SCHEMA public TO ${userName};
|
|
4804
|
+
ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT ALL ON TABLES TO ${userName};
|
|
4805
|
+
ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT ALL ON SEQUENCES TO ${userName};
|
|
4806
|
+
EOSQL
|
|
3223
4807
|
`;
|
|
3224
|
-
|
|
3225
|
-
#
|
|
3226
|
-
|
|
4808
|
+
return `
|
|
4809
|
+
# Create ${app.name} user with dedicated schema
|
|
4810
|
+
echo "Creating user ${userName} with schema ${schemaName}..."
|
|
4811
|
+
psql -v ON_ERROR_STOP=1 --username "$POSTGRES_USER" --dbname "$POSTGRES_DB" <<-EOSQL
|
|
4812
|
+
CREATE USER ${userName} WITH PASSWORD '$${envVar}';
|
|
4813
|
+
CREATE SCHEMA ${schemaName} AUTHORIZATION ${userName};
|
|
4814
|
+
ALTER USER ${userName} SET search_path TO ${schemaName};
|
|
4815
|
+
GRANT USAGE ON SCHEMA ${schemaName} TO ${userName};
|
|
4816
|
+
GRANT ALL ON ALL TABLES IN SCHEMA ${schemaName} TO ${userName};
|
|
4817
|
+
GRANT ALL ON ALL SEQUENCES IN SCHEMA ${schemaName} TO ${userName};
|
|
4818
|
+
ALTER DEFAULT PRIVILEGES IN SCHEMA ${schemaName} GRANT ALL ON TABLES TO ${userName};
|
|
4819
|
+
ALTER DEFAULT PRIVILEGES IN SCHEMA ${schemaName} GRANT ALL ON SEQUENCES TO ${userName};
|
|
4820
|
+
EOSQL
|
|
3227
4821
|
`;
|
|
3228
|
-
|
|
3229
|
-
|
|
3230
|
-
|
|
4822
|
+
});
|
|
4823
|
+
return `#!/bin/bash
|
|
4824
|
+
set -e
|
|
4825
|
+
|
|
4826
|
+
# Auto-generated PostgreSQL init script
|
|
4827
|
+
# Creates per-app users with separate schemas in a single database
|
|
4828
|
+
# - api: uses public schema
|
|
4829
|
+
# - auth: uses auth schema (search_path=auth)
|
|
4830
|
+
${userCreations.join("\n")}
|
|
4831
|
+
echo "Database initialization complete!"
|
|
3231
4832
|
`;
|
|
3232
|
-
|
|
3233
|
-
|
|
3234
|
-
|
|
3235
|
-
|
|
3236
|
-
|
|
3237
|
-
|
|
3238
|
-
|
|
3239
|
-
|
|
3240
|
-
|
|
3241
|
-
|
|
3242
|
-
|
|
3243
|
-
|
|
3244
|
-
},
|
|
3245
|
-
{
|
|
3246
|
-
path: ".env.test",
|
|
3247
|
-
content: testEnv
|
|
3248
|
-
}
|
|
3249
|
-
];
|
|
4833
|
+
}
|
|
4834
|
+
|
|
4835
|
+
//#endregion
|
|
4836
|
+
//#region src/init/generators/env.ts
|
|
4837
|
+
/**
|
|
4838
|
+
* Generate environment-related files (.gitignore only).
|
|
4839
|
+
* Note: .env files are no longer generated. Use `gkm secrets:init` to initialize
|
|
4840
|
+
* encrypted secrets stored in `.gkm/secrets/{stage}.json` with keys stored at
|
|
4841
|
+
* `~/.gkm/{project-name}/{stage}.key`.
|
|
4842
|
+
*/
|
|
4843
|
+
function generateEnvFiles(options, _template) {
|
|
4844
|
+
const files = [];
|
|
3250
4845
|
if (!options.monorepo) {
|
|
3251
4846
|
const gitignore = `# Dependencies
|
|
3252
4847
|
node_modules/
|
|
@@ -3255,7 +4850,7 @@ node_modules/
|
|
|
3255
4850
|
dist/
|
|
3256
4851
|
.gkm/
|
|
3257
4852
|
|
|
3258
|
-
# Environment
|
|
4853
|
+
# Environment (legacy - use gkm secrets instead)
|
|
3259
4854
|
.env
|
|
3260
4855
|
.env.local
|
|
3261
4856
|
.env.*.local
|
|
@@ -3324,6 +4919,8 @@ function generateModelsPackage(options) {
|
|
|
3324
4919
|
const tsConfig = {
|
|
3325
4920
|
extends: "../../tsconfig.json",
|
|
3326
4921
|
compilerOptions: {
|
|
4922
|
+
declaration: true,
|
|
4923
|
+
declarationMap: true,
|
|
3327
4924
|
outDir: "./dist",
|
|
3328
4925
|
rootDir: "./src"
|
|
3329
4926
|
},
|
|
@@ -3410,23 +5007,27 @@ export type UpdateUser = z.infer<typeof updateUserSchema>;
|
|
|
3410
5007
|
*/
|
|
3411
5008
|
function generateMonorepoFiles(options, _template) {
|
|
3412
5009
|
if (!options.monorepo) return [];
|
|
5010
|
+
const isFullstack = options.template === "fullstack";
|
|
3413
5011
|
const rootPackageJson = {
|
|
3414
5012
|
name: options.name,
|
|
3415
5013
|
version: "0.0.1",
|
|
3416
5014
|
private: true,
|
|
3417
5015
|
type: "module",
|
|
5016
|
+
packageManager: "pnpm@10.13.1",
|
|
3418
5017
|
scripts: {
|
|
3419
|
-
dev: "turbo dev",
|
|
3420
|
-
build: "turbo build",
|
|
3421
|
-
test: "turbo test",
|
|
3422
|
-
"test:once": "turbo test:once",
|
|
5018
|
+
dev: isFullstack ? "gkm dev" : "turbo dev",
|
|
5019
|
+
build: isFullstack ? "gkm build" : "turbo build",
|
|
5020
|
+
test: isFullstack ? "gkm test" : "turbo test",
|
|
5021
|
+
"test:once": isFullstack ? "gkm test --run" : "turbo test:once",
|
|
3423
5022
|
typecheck: "turbo typecheck",
|
|
3424
5023
|
lint: "biome lint .",
|
|
3425
5024
|
fmt: "biome format . --write",
|
|
3426
|
-
"fmt:check": "biome format ."
|
|
5025
|
+
"fmt:check": "biome format .",
|
|
5026
|
+
...options.deployTarget === "dokploy" ? { deploy: "gkm deploy --provider dokploy --stage production" } : {}
|
|
3427
5027
|
},
|
|
3428
5028
|
devDependencies: {
|
|
3429
|
-
"@biomejs/biome": "~
|
|
5029
|
+
"@biomejs/biome": "~2.3.0",
|
|
5030
|
+
"@geekmidas/cli": GEEKMIDAS_VERSIONS["@geekmidas/cli"],
|
|
3430
5031
|
turbo: "~2.3.0",
|
|
3431
5032
|
typescript: "~5.8.2",
|
|
3432
5033
|
vitest: "~4.0.0"
|
|
@@ -3439,7 +5040,7 @@ function generateMonorepoFiles(options, _template) {
|
|
|
3439
5040
|
- 'packages/*'
|
|
3440
5041
|
`;
|
|
3441
5042
|
const biomeConfig = {
|
|
3442
|
-
$schema: "https://biomejs.dev/schemas/
|
|
5043
|
+
$schema: "https://biomejs.dev/schemas/2.3.0/schema.json",
|
|
3443
5044
|
vcs: {
|
|
3444
5045
|
enabled: true,
|
|
3445
5046
|
clientKind: "git",
|
|
@@ -3514,6 +5115,7 @@ dist/
|
|
|
3514
5115
|
.env
|
|
3515
5116
|
.env.local
|
|
3516
5117
|
.env.*.local
|
|
5118
|
+
docker/.env
|
|
3517
5119
|
|
|
3518
5120
|
# IDE
|
|
3519
5121
|
.idea/
|
|
@@ -3550,14 +5152,27 @@ coverage/
|
|
|
3550
5152
|
esModuleInterop: true,
|
|
3551
5153
|
skipLibCheck: true,
|
|
3552
5154
|
forceConsistentCasingInFileNames: true,
|
|
3553
|
-
resolveJsonModule: true
|
|
3554
|
-
declaration: true,
|
|
3555
|
-
declarationMap: true,
|
|
3556
|
-
composite: true
|
|
5155
|
+
resolveJsonModule: true
|
|
3557
5156
|
},
|
|
3558
5157
|
exclude: ["node_modules", "dist"]
|
|
3559
5158
|
};
|
|
3560
|
-
|
|
5159
|
+
const vitestConfig = `import { defineConfig } from 'vitest/config';
|
|
5160
|
+
|
|
5161
|
+
export default defineConfig({
|
|
5162
|
+
test: {
|
|
5163
|
+
globals: true,
|
|
5164
|
+
environment: 'node',
|
|
5165
|
+
include: ['apps/**/*.{test,spec}.ts', 'packages/**/*.{test,spec}.ts'],
|
|
5166
|
+
exclude: ['**/node_modules/**', '**/dist/**'],
|
|
5167
|
+
coverage: {
|
|
5168
|
+
provider: 'v8',
|
|
5169
|
+
reporter: ['text', 'json', 'html'],
|
|
5170
|
+
exclude: ['**/node_modules/**', '**/dist/**', '**/*.d.ts'],
|
|
5171
|
+
},
|
|
5172
|
+
},
|
|
5173
|
+
});
|
|
5174
|
+
`;
|
|
5175
|
+
const files = [
|
|
3561
5176
|
{
|
|
3562
5177
|
path: "package.json",
|
|
3563
5178
|
content: `${JSON.stringify(rootPackageJson, null, 2)}\n`
|
|
@@ -3578,11 +5193,100 @@ coverage/
|
|
|
3578
5193
|
path: "turbo.json",
|
|
3579
5194
|
content: `${JSON.stringify(turboConfig, null, 2)}\n`
|
|
3580
5195
|
},
|
|
5196
|
+
{
|
|
5197
|
+
path: "vitest.config.ts",
|
|
5198
|
+
content: vitestConfig
|
|
5199
|
+
},
|
|
3581
5200
|
{
|
|
3582
5201
|
path: ".gitignore",
|
|
3583
5202
|
content: gitignore
|
|
3584
5203
|
}
|
|
3585
5204
|
];
|
|
5205
|
+
if (isFullstack) files.push({
|
|
5206
|
+
path: "gkm.config.ts",
|
|
5207
|
+
content: generateWorkspaceConfig(options)
|
|
5208
|
+
});
|
|
5209
|
+
return files;
|
|
5210
|
+
}
|
|
5211
|
+
/**
|
|
5212
|
+
* Generate gkm.config.ts with defineWorkspace for fullstack template
|
|
5213
|
+
*/
|
|
5214
|
+
function generateWorkspaceConfig(options) {
|
|
5215
|
+
const { telescope, services, deployTarget, routesStructure } = options;
|
|
5216
|
+
const getRoutesGlob = () => {
|
|
5217
|
+
switch (routesStructure) {
|
|
5218
|
+
case "centralized-endpoints": return "./src/endpoints/**/*.ts";
|
|
5219
|
+
case "centralized-routes": return "./src/routes/**/*.ts";
|
|
5220
|
+
case "domain-based": return "./src/**/routes/*.ts";
|
|
5221
|
+
}
|
|
5222
|
+
};
|
|
5223
|
+
let config = `import { defineWorkspace } from '@geekmidas/cli/config';
|
|
5224
|
+
|
|
5225
|
+
export default defineWorkspace({
|
|
5226
|
+
name: '${options.name}',
|
|
5227
|
+
apps: {
|
|
5228
|
+
api: {
|
|
5229
|
+
type: 'backend',
|
|
5230
|
+
path: 'apps/api',
|
|
5231
|
+
port: 3000,
|
|
5232
|
+
routes: '${getRoutesGlob()}',
|
|
5233
|
+
envParser: './src/config/env#envParser',
|
|
5234
|
+
logger: './src/config/logger#logger',`;
|
|
5235
|
+
if (telescope) config += `
|
|
5236
|
+
telescope: {
|
|
5237
|
+
enabled: true,
|
|
5238
|
+
path: '/__telescope',
|
|
5239
|
+
},`;
|
|
5240
|
+
config += `
|
|
5241
|
+
openapi: {
|
|
5242
|
+
enabled: true,
|
|
5243
|
+
},
|
|
5244
|
+
},
|
|
5245
|
+
auth: {
|
|
5246
|
+
type: 'backend',
|
|
5247
|
+
path: 'apps/auth',
|
|
5248
|
+
port: 3002,
|
|
5249
|
+
envParser: './src/config/env#envParser',
|
|
5250
|
+
logger: './src/config/logger#logger',
|
|
5251
|
+
},
|
|
5252
|
+
web: {
|
|
5253
|
+
type: 'frontend',
|
|
5254
|
+
framework: 'nextjs',
|
|
5255
|
+
path: 'apps/web',
|
|
5256
|
+
port: 3001,
|
|
5257
|
+
dependencies: ['api', 'auth'],
|
|
5258
|
+
client: {
|
|
5259
|
+
output: './src/api',
|
|
5260
|
+
},
|
|
5261
|
+
},
|
|
5262
|
+
},
|
|
5263
|
+
shared: {
|
|
5264
|
+
packages: ['packages/*'],
|
|
5265
|
+
models: {
|
|
5266
|
+
path: 'packages/models',
|
|
5267
|
+
schema: 'zod',
|
|
5268
|
+
},
|
|
5269
|
+
},`;
|
|
5270
|
+
if (services.db || services.cache || services.mail) {
|
|
5271
|
+
config += `
|
|
5272
|
+
services: {`;
|
|
5273
|
+
if (services.db) config += `
|
|
5274
|
+
db: true,`;
|
|
5275
|
+
if (services.cache) config += `
|
|
5276
|
+
cache: true,`;
|
|
5277
|
+
if (services.mail) config += `
|
|
5278
|
+
mail: true,`;
|
|
5279
|
+
config += `
|
|
5280
|
+
},`;
|
|
5281
|
+
}
|
|
5282
|
+
if (deployTarget === "dokploy") config += `
|
|
5283
|
+
deploy: {
|
|
5284
|
+
default: 'dokploy',
|
|
5285
|
+
},`;
|
|
5286
|
+
config += `
|
|
5287
|
+
});
|
|
5288
|
+
`;
|
|
5289
|
+
return config;
|
|
3586
5290
|
}
|
|
3587
5291
|
|
|
3588
5292
|
//#endregion
|
|
@@ -3591,18 +5295,18 @@ const apiTemplate = {
|
|
|
3591
5295
|
name: "api",
|
|
3592
5296
|
description: "Full API with auth, database, services",
|
|
3593
5297
|
dependencies: {
|
|
3594
|
-
"@geekmidas/constructs": "
|
|
3595
|
-
"@geekmidas/envkit": "
|
|
3596
|
-
"@geekmidas/logger": "
|
|
3597
|
-
"@geekmidas/services": "
|
|
3598
|
-
"@geekmidas/errors": "
|
|
3599
|
-
"@geekmidas/auth": "
|
|
5298
|
+
"@geekmidas/constructs": GEEKMIDAS_VERSIONS["@geekmidas/constructs"],
|
|
5299
|
+
"@geekmidas/envkit": GEEKMIDAS_VERSIONS["@geekmidas/envkit"],
|
|
5300
|
+
"@geekmidas/logger": GEEKMIDAS_VERSIONS["@geekmidas/logger"],
|
|
5301
|
+
"@geekmidas/services": GEEKMIDAS_VERSIONS["@geekmidas/services"],
|
|
5302
|
+
"@geekmidas/errors": GEEKMIDAS_VERSIONS["@geekmidas/errors"],
|
|
5303
|
+
"@geekmidas/auth": GEEKMIDAS_VERSIONS["@geekmidas/auth"],
|
|
3600
5304
|
hono: "~4.8.2",
|
|
3601
5305
|
pino: "~9.6.0"
|
|
3602
5306
|
},
|
|
3603
5307
|
devDependencies: {
|
|
3604
|
-
"@biomejs/biome": "~
|
|
3605
|
-
"@geekmidas/cli": "
|
|
5308
|
+
"@biomejs/biome": "~2.3.0",
|
|
5309
|
+
"@geekmidas/cli": GEEKMIDAS_VERSIONS["@geekmidas/cli"],
|
|
3606
5310
|
"@types/node": "~22.0.0",
|
|
3607
5311
|
tsx: "~4.20.0",
|
|
3608
5312
|
turbo: "~2.3.0",
|
|
@@ -3639,18 +5343,17 @@ export const logger = createLogger();
|
|
|
3639
5343
|
const files = [
|
|
3640
5344
|
{
|
|
3641
5345
|
path: "src/config/env.ts",
|
|
3642
|
-
content: `import {
|
|
5346
|
+
content: `import { Credentials } from '@geekmidas/envkit/credentials';
|
|
5347
|
+
import { EnvironmentParser } from '@geekmidas/envkit';
|
|
3643
5348
|
|
|
3644
|
-
export const envParser = new EnvironmentParser(process.env);
|
|
5349
|
+
export const envParser = new EnvironmentParser({ ...process.env, ...Credentials });
|
|
3645
5350
|
|
|
5351
|
+
// Global config - only minimal shared values
|
|
5352
|
+
// Service-specific config should be parsed in each service
|
|
3646
5353
|
export const config = envParser
|
|
3647
5354
|
.create((get) => ({
|
|
3648
|
-
|
|
3649
|
-
|
|
3650
|
-
jwtSecret: get('JWT_SECRET').string().default('change-me-in-production'),${options.database ? `
|
|
3651
|
-
database: {
|
|
3652
|
-
url: get('DATABASE_URL').string().default('postgresql://localhost:5432/mydb'),
|
|
3653
|
-
},` : ""}
|
|
5355
|
+
nodeEnv: get('NODE_ENV').enum(['development', 'test', 'production']).default('development'),
|
|
5356
|
+
stage: get('STAGE').enum(['development', 'staging', 'production']).default('development'),
|
|
3654
5357
|
}))
|
|
3655
5358
|
.parse();
|
|
3656
5359
|
`
|
|
@@ -3663,7 +5366,7 @@ export const config = envParser
|
|
|
3663
5366
|
path: getRoutePath("health.ts"),
|
|
3664
5367
|
content: `import { e } from '@geekmidas/constructs/endpoints';
|
|
3665
5368
|
|
|
3666
|
-
export
|
|
5369
|
+
export const healthEndpoint = e
|
|
3667
5370
|
.get('/health')
|
|
3668
5371
|
.handle(async () => ({
|
|
3669
5372
|
status: 'ok',
|
|
@@ -3675,7 +5378,7 @@ export default e
|
|
|
3675
5378
|
path: getRoutePath("users/list.ts"),
|
|
3676
5379
|
content: `import { e } from '@geekmidas/constructs/endpoints';
|
|
3677
5380
|
|
|
3678
|
-
export
|
|
5381
|
+
export const listUsersEndpoint = e
|
|
3679
5382
|
.get('/users')
|
|
3680
5383
|
.handle(async () => ({
|
|
3681
5384
|
users: [
|
|
@@ -3690,7 +5393,7 @@ export default e
|
|
|
3690
5393
|
content: `import { e } from '@geekmidas/constructs/endpoints';
|
|
3691
5394
|
import { z } from 'zod';
|
|
3692
5395
|
|
|
3693
|
-
export
|
|
5396
|
+
export const getUserEndpoint = e
|
|
3694
5397
|
.get('/users/:id')
|
|
3695
5398
|
.params(z.object({ id: z.string() }))
|
|
3696
5399
|
.handle(async ({ params }) => ({
|
|
@@ -3703,7 +5406,7 @@ export default e
|
|
|
3703
5406
|
];
|
|
3704
5407
|
if (options.database) files.push({
|
|
3705
5408
|
path: "src/services/database.ts",
|
|
3706
|
-
content: `import type { Service } from '@geekmidas/services';
|
|
5409
|
+
content: `import type { Service, ServiceRegisterOptions } from '@geekmidas/services';
|
|
3707
5410
|
import { Kysely, PostgresDialect } from 'kysely';
|
|
3708
5411
|
import pg from 'pg';
|
|
3709
5412
|
|
|
@@ -3719,18 +5422,24 @@ export interface Database {
|
|
|
3719
5422
|
|
|
3720
5423
|
export const databaseService = {
|
|
3721
5424
|
serviceName: 'database' as const,
|
|
3722
|
-
async register(envParser) {
|
|
5425
|
+
async register({ envParser, context }: ServiceRegisterOptions) {
|
|
5426
|
+
const logger = context.getLogger();
|
|
5427
|
+
logger.info('Connecting to database');
|
|
5428
|
+
|
|
3723
5429
|
const config = envParser
|
|
3724
5430
|
.create((get) => ({
|
|
3725
5431
|
url: get('DATABASE_URL').string(),
|
|
3726
5432
|
}))
|
|
3727
5433
|
.parse();
|
|
3728
5434
|
|
|
3729
|
-
|
|
5435
|
+
const db = new Kysely<Database>({
|
|
3730
5436
|
dialect: new PostgresDialect({
|
|
3731
5437
|
pool: new pg.Pool({ connectionString: config.url }),
|
|
3732
5438
|
}),
|
|
3733
5439
|
});
|
|
5440
|
+
|
|
5441
|
+
logger.info('Database connection established');
|
|
5442
|
+
return db;
|
|
3734
5443
|
},
|
|
3735
5444
|
} satisfies Service<'database', Kysely<Database>>;
|
|
3736
5445
|
`
|
|
@@ -3751,13 +5460,20 @@ export const telescope = new Telescope({
|
|
|
3751
5460
|
content: `import { Direction, InMemoryMonitoringStorage, Studio } from '@geekmidas/studio';
|
|
3752
5461
|
import { Kysely, PostgresDialect } from 'kysely';
|
|
3753
5462
|
import pg from 'pg';
|
|
3754
|
-
import type { Database } from '../services/database';
|
|
3755
|
-
import {
|
|
5463
|
+
import type { Database } from '../services/database.js';
|
|
5464
|
+
import { envParser } from './env.js';
|
|
5465
|
+
|
|
5466
|
+
// Parse database config for Studio
|
|
5467
|
+
const studioConfig = envParser
|
|
5468
|
+
.create((get) => ({
|
|
5469
|
+
databaseUrl: get('DATABASE_URL').string(),
|
|
5470
|
+
}))
|
|
5471
|
+
.parse();
|
|
3756
5472
|
|
|
3757
5473
|
// Create a Kysely instance for Studio
|
|
3758
5474
|
const db = new Kysely<Database>({
|
|
3759
5475
|
dialect: new PostgresDialect({
|
|
3760
|
-
pool: new pg.Pool({ connectionString:
|
|
5476
|
+
pool: new pg.Pool({ connectionString: studioConfig.databaseUrl }),
|
|
3761
5477
|
}),
|
|
3762
5478
|
});
|
|
3763
5479
|
|
|
@@ -3783,15 +5499,15 @@ const minimalTemplate = {
|
|
|
3783
5499
|
name: "minimal",
|
|
3784
5500
|
description: "Basic health endpoint",
|
|
3785
5501
|
dependencies: {
|
|
3786
|
-
"@geekmidas/constructs": "
|
|
3787
|
-
"@geekmidas/envkit": "
|
|
3788
|
-
"@geekmidas/logger": "
|
|
5502
|
+
"@geekmidas/constructs": GEEKMIDAS_VERSIONS["@geekmidas/constructs"],
|
|
5503
|
+
"@geekmidas/envkit": GEEKMIDAS_VERSIONS["@geekmidas/envkit"],
|
|
5504
|
+
"@geekmidas/logger": GEEKMIDAS_VERSIONS["@geekmidas/logger"],
|
|
3789
5505
|
hono: "~4.8.2",
|
|
3790
5506
|
pino: "~9.6.0"
|
|
3791
5507
|
},
|
|
3792
5508
|
devDependencies: {
|
|
3793
|
-
"@biomejs/biome": "~
|
|
3794
|
-
"@geekmidas/cli": "
|
|
5509
|
+
"@biomejs/biome": "~2.3.0",
|
|
5510
|
+
"@geekmidas/cli": GEEKMIDAS_VERSIONS["@geekmidas/cli"],
|
|
3795
5511
|
"@types/node": "~22.0.0",
|
|
3796
5512
|
tsx: "~4.20.0",
|
|
3797
5513
|
turbo: "~2.3.0",
|
|
@@ -3824,14 +5540,17 @@ export const logger = createLogger();
|
|
|
3824
5540
|
const files = [
|
|
3825
5541
|
{
|
|
3826
5542
|
path: "src/config/env.ts",
|
|
3827
|
-
content: `import {
|
|
5543
|
+
content: `import { Credentials } from '@geekmidas/envkit/credentials';
|
|
5544
|
+
import { EnvironmentParser } from '@geekmidas/envkit';
|
|
3828
5545
|
|
|
3829
|
-
export const envParser = new EnvironmentParser(process.env);
|
|
5546
|
+
export const envParser = new EnvironmentParser({ ...process.env, ...Credentials });
|
|
3830
5547
|
|
|
5548
|
+
// Global config - only minimal shared values
|
|
5549
|
+
// Service-specific config should be parsed in each service
|
|
3831
5550
|
export const config = envParser
|
|
3832
5551
|
.create((get) => ({
|
|
3833
|
-
|
|
3834
|
-
|
|
5552
|
+
nodeEnv: get('NODE_ENV').enum(['development', 'test', 'production']).default('development'),
|
|
5553
|
+
stage: get('STAGE').enum(['development', 'staging', 'production']).default('development'),
|
|
3835
5554
|
}))
|
|
3836
5555
|
.parse();
|
|
3837
5556
|
`
|
|
@@ -3844,7 +5563,7 @@ export const config = envParser
|
|
|
3844
5563
|
path: getRoutePath("health.ts"),
|
|
3845
5564
|
content: `import { e } from '@geekmidas/constructs/endpoints';
|
|
3846
5565
|
|
|
3847
|
-
export
|
|
5566
|
+
export const healthEndpoint = e
|
|
3848
5567
|
.get('/health')
|
|
3849
5568
|
.handle(async () => ({
|
|
3850
5569
|
status: 'ok',
|
|
@@ -3853,27 +5572,9 @@ export default e
|
|
|
3853
5572
|
`
|
|
3854
5573
|
}
|
|
3855
5574
|
];
|
|
3856
|
-
if (options.database) {
|
|
3857
|
-
|
|
3858
|
-
|
|
3859
|
-
content: `import { EnvironmentParser } from '@geekmidas/envkit';
|
|
3860
|
-
|
|
3861
|
-
export const envParser = new EnvironmentParser(process.env);
|
|
3862
|
-
|
|
3863
|
-
export const config = envParser
|
|
3864
|
-
.create((get) => ({
|
|
3865
|
-
port: get('PORT').string().transform(Number).default(3000),
|
|
3866
|
-
nodeEnv: get('NODE_ENV').string().default('development'),
|
|
3867
|
-
database: {
|
|
3868
|
-
url: get('DATABASE_URL').string().default('postgresql://localhost:5432/mydb'),
|
|
3869
|
-
},
|
|
3870
|
-
}))
|
|
3871
|
-
.parse();
|
|
3872
|
-
`
|
|
3873
|
-
};
|
|
3874
|
-
files.push({
|
|
3875
|
-
path: "src/services/database.ts",
|
|
3876
|
-
content: `import type { Service } from '@geekmidas/services';
|
|
5575
|
+
if (options.database) files.push({
|
|
5576
|
+
path: "src/services/database.ts",
|
|
5577
|
+
content: `import type { Service, ServiceRegisterOptions } from '@geekmidas/services';
|
|
3877
5578
|
import { Kysely, PostgresDialect } from 'kysely';
|
|
3878
5579
|
import pg from 'pg';
|
|
3879
5580
|
|
|
@@ -3884,23 +5585,28 @@ export interface Database {
|
|
|
3884
5585
|
|
|
3885
5586
|
export const databaseService = {
|
|
3886
5587
|
serviceName: 'database' as const,
|
|
3887
|
-
async register(envParser) {
|
|
5588
|
+
async register({ envParser, context }: ServiceRegisterOptions) {
|
|
5589
|
+
const logger = context.getLogger();
|
|
5590
|
+
logger.info('Connecting to database');
|
|
5591
|
+
|
|
3888
5592
|
const config = envParser
|
|
3889
5593
|
.create((get) => ({
|
|
3890
5594
|
url: get('DATABASE_URL').string(),
|
|
3891
5595
|
}))
|
|
3892
5596
|
.parse();
|
|
3893
5597
|
|
|
3894
|
-
|
|
5598
|
+
const db = new Kysely<Database>({
|
|
3895
5599
|
dialect: new PostgresDialect({
|
|
3896
5600
|
pool: new pg.Pool({ connectionString: config.url }),
|
|
3897
5601
|
}),
|
|
3898
5602
|
});
|
|
5603
|
+
|
|
5604
|
+
logger.info('Database connection established');
|
|
5605
|
+
return db;
|
|
3899
5606
|
},
|
|
3900
5607
|
} satisfies Service<'database', Kysely<Database>>;
|
|
3901
5608
|
`
|
|
3902
|
-
|
|
3903
|
-
}
|
|
5609
|
+
});
|
|
3904
5610
|
if (options.telescope) files.push({
|
|
3905
5611
|
path: "src/config/telescope.ts",
|
|
3906
5612
|
content: `import { Telescope } from '@geekmidas/telescope';
|
|
@@ -3917,13 +5623,20 @@ export const telescope = new Telescope({
|
|
|
3917
5623
|
content: `import { Direction, InMemoryMonitoringStorage, Studio } from '@geekmidas/studio';
|
|
3918
5624
|
import { Kysely, PostgresDialect } from 'kysely';
|
|
3919
5625
|
import pg from 'pg';
|
|
3920
|
-
import type { Database } from '../services/database';
|
|
3921
|
-
import {
|
|
5626
|
+
import type { Database } from '../services/database.js';
|
|
5627
|
+
import { envParser } from './env.js';
|
|
5628
|
+
|
|
5629
|
+
// Parse database config for Studio
|
|
5630
|
+
const studioConfig = envParser
|
|
5631
|
+
.create((get) => ({
|
|
5632
|
+
databaseUrl: get('DATABASE_URL').string(),
|
|
5633
|
+
}))
|
|
5634
|
+
.parse();
|
|
3922
5635
|
|
|
3923
5636
|
// Create a Kysely instance for Studio
|
|
3924
5637
|
const db = new Kysely<Database>({
|
|
3925
5638
|
dialect: new PostgresDialect({
|
|
3926
|
-
pool: new pg.Pool({ connectionString:
|
|
5639
|
+
pool: new pg.Pool({ connectionString: studioConfig.databaseUrl }),
|
|
3927
5640
|
}),
|
|
3928
5641
|
});
|
|
3929
5642
|
|
|
@@ -3949,16 +5662,16 @@ const serverlessTemplate = {
|
|
|
3949
5662
|
name: "serverless",
|
|
3950
5663
|
description: "AWS Lambda handlers",
|
|
3951
5664
|
dependencies: {
|
|
3952
|
-
"@geekmidas/constructs": "
|
|
3953
|
-
"@geekmidas/envkit": "
|
|
3954
|
-
"@geekmidas/logger": "
|
|
3955
|
-
"@geekmidas/cloud": "
|
|
5665
|
+
"@geekmidas/constructs": GEEKMIDAS_VERSIONS["@geekmidas/constructs"],
|
|
5666
|
+
"@geekmidas/envkit": GEEKMIDAS_VERSIONS["@geekmidas/envkit"],
|
|
5667
|
+
"@geekmidas/logger": GEEKMIDAS_VERSIONS["@geekmidas/logger"],
|
|
5668
|
+
"@geekmidas/cloud": GEEKMIDAS_VERSIONS["@geekmidas/cloud"],
|
|
3956
5669
|
hono: "~4.8.2",
|
|
3957
5670
|
pino: "~9.6.0"
|
|
3958
5671
|
},
|
|
3959
5672
|
devDependencies: {
|
|
3960
|
-
"@biomejs/biome": "~
|
|
3961
|
-
"@geekmidas/cli": "
|
|
5673
|
+
"@biomejs/biome": "~2.3.0",
|
|
5674
|
+
"@geekmidas/cli": GEEKMIDAS_VERSIONS["@geekmidas/cli"],
|
|
3962
5675
|
"@types/aws-lambda": "~8.10.92",
|
|
3963
5676
|
"@types/node": "~22.0.0",
|
|
3964
5677
|
tsx: "~4.20.0",
|
|
@@ -3992,17 +5705,17 @@ export const logger = createLogger();
|
|
|
3992
5705
|
const files = [
|
|
3993
5706
|
{
|
|
3994
5707
|
path: "src/config/env.ts",
|
|
3995
|
-
content: `import {
|
|
5708
|
+
content: `import { Credentials } from '@geekmidas/envkit/credentials';
|
|
5709
|
+
import { EnvironmentParser } from '@geekmidas/envkit';
|
|
3996
5710
|
|
|
3997
|
-
export const envParser = new EnvironmentParser(process.env);
|
|
5711
|
+
export const envParser = new EnvironmentParser({ ...process.env, ...Credentials });
|
|
3998
5712
|
|
|
5713
|
+
// Global config - only minimal shared values
|
|
5714
|
+
// Service-specific config should be parsed in each service
|
|
3999
5715
|
export const config = envParser
|
|
4000
5716
|
.create((get) => ({
|
|
4001
|
-
|
|
4002
|
-
|
|
4003
|
-
database: {
|
|
4004
|
-
url: get('DATABASE_URL').string(),
|
|
4005
|
-
},` : ""}
|
|
5717
|
+
nodeEnv: get('NODE_ENV').enum(['development', 'test', 'production']).default('development'),
|
|
5718
|
+
stage: get('STAGE').enum(['dev', 'staging', 'prod']).default('dev'),
|
|
4006
5719
|
}))
|
|
4007
5720
|
.parse();
|
|
4008
5721
|
`
|
|
@@ -4015,7 +5728,7 @@ export const config = envParser
|
|
|
4015
5728
|
path: getRoutePath("health.ts"),
|
|
4016
5729
|
content: `import { e } from '@geekmidas/constructs/endpoints';
|
|
4017
5730
|
|
|
4018
|
-
export
|
|
5731
|
+
export const healthEndpoint = e
|
|
4019
5732
|
.get('/health')
|
|
4020
5733
|
.handle(async () => ({
|
|
4021
5734
|
status: 'ok',
|
|
@@ -4029,7 +5742,7 @@ export default e
|
|
|
4029
5742
|
content: `import { f } from '@geekmidas/constructs/functions';
|
|
4030
5743
|
import { z } from 'zod';
|
|
4031
5744
|
|
|
4032
|
-
export
|
|
5745
|
+
export const helloFunction = f
|
|
4033
5746
|
.input(z.object({ name: z.string() }))
|
|
4034
5747
|
.output(z.object({ message: z.string() }))
|
|
4035
5748
|
.handle(async ({ input }) => ({
|
|
@@ -4060,16 +5773,16 @@ const workerTemplate = {
|
|
|
4060
5773
|
name: "worker",
|
|
4061
5774
|
description: "Background job processing",
|
|
4062
5775
|
dependencies: {
|
|
4063
|
-
"@geekmidas/constructs": "
|
|
4064
|
-
"@geekmidas/envkit": "
|
|
4065
|
-
"@geekmidas/logger": "
|
|
4066
|
-
"@geekmidas/events": "
|
|
5776
|
+
"@geekmidas/constructs": GEEKMIDAS_VERSIONS["@geekmidas/constructs"],
|
|
5777
|
+
"@geekmidas/envkit": GEEKMIDAS_VERSIONS["@geekmidas/envkit"],
|
|
5778
|
+
"@geekmidas/logger": GEEKMIDAS_VERSIONS["@geekmidas/logger"],
|
|
5779
|
+
"@geekmidas/events": GEEKMIDAS_VERSIONS["@geekmidas/events"],
|
|
4067
5780
|
hono: "~4.8.2",
|
|
4068
5781
|
pino: "~9.6.0"
|
|
4069
5782
|
},
|
|
4070
5783
|
devDependencies: {
|
|
4071
|
-
"@biomejs/biome": "~
|
|
4072
|
-
"@geekmidas/cli": "
|
|
5784
|
+
"@biomejs/biome": "~2.3.0",
|
|
5785
|
+
"@geekmidas/cli": GEEKMIDAS_VERSIONS["@geekmidas/cli"],
|
|
4073
5786
|
"@types/node": "~22.0.0",
|
|
4074
5787
|
tsx: "~4.20.0",
|
|
4075
5788
|
turbo: "~2.3.0",
|
|
@@ -4102,20 +5815,17 @@ export const logger = createLogger();
|
|
|
4102
5815
|
const files = [
|
|
4103
5816
|
{
|
|
4104
5817
|
path: "src/config/env.ts",
|
|
4105
|
-
content: `import {
|
|
5818
|
+
content: `import { Credentials } from '@geekmidas/envkit/credentials';
|
|
5819
|
+
import { EnvironmentParser } from '@geekmidas/envkit';
|
|
4106
5820
|
|
|
4107
|
-
export const envParser = new EnvironmentParser(process.env);
|
|
5821
|
+
export const envParser = new EnvironmentParser({ ...process.env, ...Credentials });
|
|
4108
5822
|
|
|
5823
|
+
// Global config - only minimal shared values
|
|
5824
|
+
// Service-specific config should be parsed in each service
|
|
4109
5825
|
export const config = envParser
|
|
4110
5826
|
.create((get) => ({
|
|
4111
|
-
|
|
4112
|
-
|
|
4113
|
-
rabbitmq: {
|
|
4114
|
-
url: get('RABBITMQ_URL').string().default('amqp://localhost:5672'),
|
|
4115
|
-
},${options.database ? `
|
|
4116
|
-
database: {
|
|
4117
|
-
url: get('DATABASE_URL').string().default('postgresql://localhost:5432/mydb'),
|
|
4118
|
-
},` : ""}
|
|
5827
|
+
nodeEnv: get('NODE_ENV').enum(['development', 'test', 'production']).default('development'),
|
|
5828
|
+
stage: get('STAGE').enum(['development', 'staging', 'production']).default('development'),
|
|
4119
5829
|
}))
|
|
4120
5830
|
.parse();
|
|
4121
5831
|
`
|
|
@@ -4128,7 +5838,7 @@ export const config = envParser
|
|
|
4128
5838
|
path: getRoutePath("health.ts"),
|
|
4129
5839
|
content: `import { e } from '@geekmidas/constructs/endpoints';
|
|
4130
5840
|
|
|
4131
|
-
export
|
|
5841
|
+
export const healthEndpoint = e
|
|
4132
5842
|
.get('/health')
|
|
4133
5843
|
.handle(async () => ({
|
|
4134
5844
|
status: 'ok',
|
|
@@ -4145,15 +5855,44 @@ export type AppEvents =
|
|
|
4145
5855
|
| PublishableMessage<'user.created', { userId: string; email: string }>
|
|
4146
5856
|
| PublishableMessage<'user.updated', { userId: string; changes: Record<string, unknown> }>
|
|
4147
5857
|
| PublishableMessage<'order.placed', { orderId: string; userId: string; total: number }>;
|
|
5858
|
+
`
|
|
5859
|
+
},
|
|
5860
|
+
{
|
|
5861
|
+
path: "src/events/publisher.ts",
|
|
5862
|
+
content: `import type { Service, ServiceRegisterOptions } from '@geekmidas/services';
|
|
5863
|
+
import { Publisher, type EventPublisher } from '@geekmidas/events';
|
|
5864
|
+
import type { AppEvents } from './types.js';
|
|
5865
|
+
|
|
5866
|
+
export const eventsPublisherService = {
|
|
5867
|
+
serviceName: 'events' as const,
|
|
5868
|
+
async register({ envParser, context }: ServiceRegisterOptions) {
|
|
5869
|
+
const logger = context.getLogger();
|
|
5870
|
+
logger.info('Connecting to message broker');
|
|
5871
|
+
|
|
5872
|
+
const config = envParser
|
|
5873
|
+
.create((get) => ({
|
|
5874
|
+
url: get('RABBITMQ_URL').string().default('amqp://localhost:5672'),
|
|
5875
|
+
}))
|
|
5876
|
+
.parse();
|
|
5877
|
+
|
|
5878
|
+
const publisher = await Publisher.fromConnectionString<AppEvents>(
|
|
5879
|
+
\`rabbitmq://\${config.url.replace('amqp://', '')}?exchange=events\`
|
|
5880
|
+
);
|
|
5881
|
+
|
|
5882
|
+
logger.info('Message broker connection established');
|
|
5883
|
+
return publisher;
|
|
5884
|
+
},
|
|
5885
|
+
} satisfies Service<'events', EventPublisher<AppEvents>>;
|
|
4148
5886
|
`
|
|
4149
5887
|
},
|
|
4150
5888
|
{
|
|
4151
5889
|
path: "src/subscribers/user-events.ts",
|
|
4152
5890
|
content: `import { s } from '@geekmidas/constructs/subscribers';
|
|
4153
|
-
import
|
|
5891
|
+
import { eventsPublisherService } from '../events/publisher.js';
|
|
4154
5892
|
|
|
4155
|
-
export
|
|
4156
|
-
.
|
|
5893
|
+
export const userEventsSubscriber = s
|
|
5894
|
+
.publisher(eventsPublisherService)
|
|
5895
|
+
.subscribe(['user.created', 'user.updated'])
|
|
4157
5896
|
.handle(async ({ event, logger }) => {
|
|
4158
5897
|
logger.info({ type: event.type, payload: event.payload }, 'Processing user event');
|
|
4159
5898
|
|
|
@@ -4175,7 +5914,7 @@ export default s<AppEvents>()
|
|
|
4175
5914
|
content: `import { cron } from '@geekmidas/constructs/crons';
|
|
4176
5915
|
|
|
4177
5916
|
// Run every day at midnight
|
|
4178
|
-
export
|
|
5917
|
+
export const cleanupCron = cron('0 0 * * *')
|
|
4179
5918
|
.handle(async ({ logger }) => {
|
|
4180
5919
|
logger.info('Running cleanup job');
|
|
4181
5920
|
|
|
@@ -4218,30 +5957,17 @@ const templates = {
|
|
|
4218
5957
|
worker: workerTemplate
|
|
4219
5958
|
};
|
|
4220
5959
|
/**
|
|
4221
|
-
* Template choices for prompts
|
|
5960
|
+
* Template choices for prompts (Story 1.11 simplified to api + fullstack)
|
|
4222
5961
|
*/
|
|
4223
|
-
const templateChoices = [
|
|
4224
|
-
|
|
4225
|
-
|
|
4226
|
-
|
|
4227
|
-
|
|
4228
|
-
|
|
4229
|
-
|
|
4230
|
-
|
|
4231
|
-
|
|
4232
|
-
description: "Full API with auth, database, services"
|
|
4233
|
-
},
|
|
4234
|
-
{
|
|
4235
|
-
title: "Serverless",
|
|
4236
|
-
value: "serverless",
|
|
4237
|
-
description: "AWS Lambda handlers"
|
|
4238
|
-
},
|
|
4239
|
-
{
|
|
4240
|
-
title: "Worker",
|
|
4241
|
-
value: "worker",
|
|
4242
|
-
description: "Background job processing"
|
|
4243
|
-
}
|
|
4244
|
-
];
|
|
5962
|
+
const templateChoices = [{
|
|
5963
|
+
title: "API",
|
|
5964
|
+
value: "api",
|
|
5965
|
+
description: "Single backend API with endpoints"
|
|
5966
|
+
}, {
|
|
5967
|
+
title: "Fullstack",
|
|
5968
|
+
value: "fullstack",
|
|
5969
|
+
description: "Monorepo with API + Next.js + shared models"
|
|
5970
|
+
}];
|
|
4245
5971
|
/**
|
|
4246
5972
|
* Logger type choices for prompts
|
|
4247
5973
|
*/
|
|
@@ -4275,13 +6001,77 @@ const routesStructureChoices = [
|
|
|
4275
6001
|
}
|
|
4276
6002
|
];
|
|
4277
6003
|
/**
|
|
6004
|
+
* Package manager choices for prompts
|
|
6005
|
+
*/
|
|
6006
|
+
const packageManagerChoices = [
|
|
6007
|
+
{
|
|
6008
|
+
title: "pnpm",
|
|
6009
|
+
value: "pnpm",
|
|
6010
|
+
description: "Fast, disk space efficient (recommended)"
|
|
6011
|
+
},
|
|
6012
|
+
{
|
|
6013
|
+
title: "npm",
|
|
6014
|
+
value: "npm",
|
|
6015
|
+
description: "Node.js default package manager"
|
|
6016
|
+
},
|
|
6017
|
+
{
|
|
6018
|
+
title: "yarn",
|
|
6019
|
+
value: "yarn",
|
|
6020
|
+
description: "Yarn package manager"
|
|
6021
|
+
},
|
|
6022
|
+
{
|
|
6023
|
+
title: "bun",
|
|
6024
|
+
value: "bun",
|
|
6025
|
+
description: "Fast JavaScript runtime and package manager"
|
|
6026
|
+
}
|
|
6027
|
+
];
|
|
6028
|
+
/**
|
|
6029
|
+
* Deploy target choices for prompts
|
|
6030
|
+
*/
|
|
6031
|
+
const deployTargetChoices = [{
|
|
6032
|
+
title: "Dokploy",
|
|
6033
|
+
value: "dokploy",
|
|
6034
|
+
description: "Deploy to Dokploy (Docker-based hosting)"
|
|
6035
|
+
}, {
|
|
6036
|
+
title: "Configure later",
|
|
6037
|
+
value: "none",
|
|
6038
|
+
description: "Skip deployment setup for now"
|
|
6039
|
+
}];
|
|
6040
|
+
/**
|
|
6041
|
+
* Services choices for multi-select prompt
|
|
6042
|
+
*/
|
|
6043
|
+
const servicesChoices = [
|
|
6044
|
+
{
|
|
6045
|
+
title: "PostgreSQL",
|
|
6046
|
+
value: "db",
|
|
6047
|
+
description: "PostgreSQL database"
|
|
6048
|
+
},
|
|
6049
|
+
{
|
|
6050
|
+
title: "Redis",
|
|
6051
|
+
value: "cache",
|
|
6052
|
+
description: "Redis cache"
|
|
6053
|
+
},
|
|
6054
|
+
{
|
|
6055
|
+
title: "Mailpit",
|
|
6056
|
+
value: "mail",
|
|
6057
|
+
description: "Email testing service (dev only)"
|
|
6058
|
+
}
|
|
6059
|
+
];
|
|
6060
|
+
/**
|
|
4278
6061
|
* Get a template by name
|
|
4279
6062
|
*/
|
|
4280
6063
|
function getTemplate(name$1) {
|
|
6064
|
+
if (name$1 === "fullstack") return templates.api;
|
|
4281
6065
|
const template = templates[name$1];
|
|
4282
6066
|
if (!template) throw new Error(`Unknown template: ${name$1}`);
|
|
4283
6067
|
return template;
|
|
4284
6068
|
}
|
|
6069
|
+
/**
|
|
6070
|
+
* Check if a template is the fullstack monorepo template
|
|
6071
|
+
*/
|
|
6072
|
+
function isFullstackTemplate(name$1) {
|
|
6073
|
+
return name$1 === "fullstack";
|
|
6074
|
+
}
|
|
4285
6075
|
|
|
4286
6076
|
//#endregion
|
|
4287
6077
|
//#region src/init/generators/package.ts
|
|
@@ -4293,10 +6083,10 @@ function generatePackageJson(options, template) {
|
|
|
4293
6083
|
const dependencies$1 = { ...template.dependencies };
|
|
4294
6084
|
const devDependencies$1 = { ...template.devDependencies };
|
|
4295
6085
|
const scripts$1 = { ...template.scripts };
|
|
4296
|
-
if (telescope) dependencies$1["@geekmidas/telescope"] = "
|
|
4297
|
-
if (studio) dependencies$1["@geekmidas/studio"] = "
|
|
6086
|
+
if (telescope) dependencies$1["@geekmidas/telescope"] = GEEKMIDAS_VERSIONS["@geekmidas/telescope"];
|
|
6087
|
+
if (studio) dependencies$1["@geekmidas/studio"] = GEEKMIDAS_VERSIONS["@geekmidas/studio"];
|
|
4298
6088
|
if (database) {
|
|
4299
|
-
dependencies$1["@geekmidas/db"] = "
|
|
6089
|
+
dependencies$1["@geekmidas/db"] = GEEKMIDAS_VERSIONS["@geekmidas/db"];
|
|
4300
6090
|
dependencies$1.kysely = "~0.28.2";
|
|
4301
6091
|
dependencies$1.pg = "~8.16.0";
|
|
4302
6092
|
devDependencies$1["@types/pg"] = "~8.15.0";
|
|
@@ -4331,19 +6121,219 @@ function generatePackageJson(options, template) {
|
|
|
4331
6121
|
dependencies: sortObject(dependencies$1),
|
|
4332
6122
|
devDependencies: sortObject(devDependencies$1)
|
|
4333
6123
|
};
|
|
4334
|
-
return [{
|
|
4335
|
-
path: "package.json",
|
|
4336
|
-
content: `${JSON.stringify(packageJson, null, 2)}\n`
|
|
4337
|
-
}];
|
|
6124
|
+
return [{
|
|
6125
|
+
path: "package.json",
|
|
6126
|
+
content: `${JSON.stringify(packageJson, null, 2)}\n`
|
|
6127
|
+
}];
|
|
6128
|
+
}
|
|
6129
|
+
|
|
6130
|
+
//#endregion
|
|
6131
|
+
//#region src/init/generators/source.ts
|
|
6132
|
+
/**
|
|
6133
|
+
* Generate source files from template
|
|
6134
|
+
*/
|
|
6135
|
+
function generateSourceFiles(options, template) {
|
|
6136
|
+
return template.files(options);
|
|
6137
|
+
}
|
|
6138
|
+
|
|
6139
|
+
//#endregion
|
|
6140
|
+
//#region src/init/generators/web.ts
|
|
6141
|
+
/**
|
|
6142
|
+
* Generate Next.js web app files for fullstack template
|
|
6143
|
+
*/
|
|
6144
|
+
function generateWebAppFiles(options) {
|
|
6145
|
+
if (!options.monorepo || options.template !== "fullstack") return [];
|
|
6146
|
+
const packageName = `@${options.name}/web`;
|
|
6147
|
+
const modelsPackage = `@${options.name}/models`;
|
|
6148
|
+
const packageJson = {
|
|
6149
|
+
name: packageName,
|
|
6150
|
+
version: "0.0.1",
|
|
6151
|
+
private: true,
|
|
6152
|
+
type: "module",
|
|
6153
|
+
scripts: {
|
|
6154
|
+
dev: "next dev -p 3001",
|
|
6155
|
+
build: "next build",
|
|
6156
|
+
start: "next start",
|
|
6157
|
+
typecheck: "tsc --noEmit"
|
|
6158
|
+
},
|
|
6159
|
+
dependencies: {
|
|
6160
|
+
[modelsPackage]: "workspace:*",
|
|
6161
|
+
next: "~16.1.0",
|
|
6162
|
+
react: "~19.2.0",
|
|
6163
|
+
"react-dom": "~19.2.0"
|
|
6164
|
+
},
|
|
6165
|
+
devDependencies: {
|
|
6166
|
+
"@types/node": "~22.0.0",
|
|
6167
|
+
"@types/react": "~19.0.0",
|
|
6168
|
+
"@types/react-dom": "~19.0.0",
|
|
6169
|
+
typescript: "~5.8.2"
|
|
6170
|
+
}
|
|
6171
|
+
};
|
|
6172
|
+
const nextConfig = `import type { NextConfig } from 'next';
|
|
6173
|
+
|
|
6174
|
+
const nextConfig: NextConfig = {
|
|
6175
|
+
output: 'standalone',
|
|
6176
|
+
reactStrictMode: true,
|
|
6177
|
+
transpilePackages: ['${modelsPackage}'],
|
|
6178
|
+
};
|
|
6179
|
+
|
|
6180
|
+
export default nextConfig;
|
|
6181
|
+
`;
|
|
6182
|
+
const tsConfig = {
|
|
6183
|
+
extends: "../../tsconfig.json",
|
|
6184
|
+
compilerOptions: {
|
|
6185
|
+
lib: [
|
|
6186
|
+
"dom",
|
|
6187
|
+
"dom.iterable",
|
|
6188
|
+
"ES2022"
|
|
6189
|
+
],
|
|
6190
|
+
allowJs: true,
|
|
6191
|
+
skipLibCheck: true,
|
|
6192
|
+
strict: true,
|
|
6193
|
+
noEmit: true,
|
|
6194
|
+
esModuleInterop: true,
|
|
6195
|
+
module: "ESNext",
|
|
6196
|
+
moduleResolution: "bundler",
|
|
6197
|
+
resolveJsonModule: true,
|
|
6198
|
+
isolatedModules: true,
|
|
6199
|
+
jsx: "preserve",
|
|
6200
|
+
incremental: true,
|
|
6201
|
+
plugins: [{ name: "next" }],
|
|
6202
|
+
paths: {
|
|
6203
|
+
"@/*": ["./src/*"],
|
|
6204
|
+
[`${modelsPackage}`]: ["../../packages/models/src"],
|
|
6205
|
+
[`${modelsPackage}/*`]: ["../../packages/models/src/*"]
|
|
6206
|
+
},
|
|
6207
|
+
baseUrl: "."
|
|
6208
|
+
},
|
|
6209
|
+
include: [
|
|
6210
|
+
"next-env.d.ts",
|
|
6211
|
+
"**/*.ts",
|
|
6212
|
+
"**/*.tsx",
|
|
6213
|
+
".next/types/**/*.ts"
|
|
6214
|
+
],
|
|
6215
|
+
exclude: ["node_modules"]
|
|
6216
|
+
};
|
|
6217
|
+
const layoutTsx = `import type { Metadata } from 'next';
|
|
6218
|
+
|
|
6219
|
+
export const metadata: Metadata = {
|
|
6220
|
+
title: '${options.name}',
|
|
6221
|
+
description: 'Created with gkm init',
|
|
6222
|
+
};
|
|
6223
|
+
|
|
6224
|
+
export default function RootLayout({
|
|
6225
|
+
children,
|
|
6226
|
+
}: {
|
|
6227
|
+
children: React.ReactNode;
|
|
6228
|
+
}) {
|
|
6229
|
+
return (
|
|
6230
|
+
<html lang="en">
|
|
6231
|
+
<body>{children}</body>
|
|
6232
|
+
</html>
|
|
6233
|
+
);
|
|
4338
6234
|
}
|
|
6235
|
+
`;
|
|
6236
|
+
const pageTsx = `import type { User } from '${modelsPackage}';
|
|
4339
6237
|
|
|
4340
|
-
|
|
4341
|
-
|
|
4342
|
-
|
|
4343
|
-
|
|
4344
|
-
|
|
4345
|
-
|
|
4346
|
-
|
|
6238
|
+
export default async function Home() {
|
|
6239
|
+
// Example: Fetch from API
|
|
6240
|
+
const apiUrl = process.env.API_URL || 'http://localhost:3000';
|
|
6241
|
+
let health = null;
|
|
6242
|
+
|
|
6243
|
+
try {
|
|
6244
|
+
const response = await fetch(\`\${apiUrl}/health\`, {
|
|
6245
|
+
cache: 'no-store',
|
|
6246
|
+
});
|
|
6247
|
+
health = await response.json();
|
|
6248
|
+
} catch (error) {
|
|
6249
|
+
console.error('Failed to fetch health:', error);
|
|
6250
|
+
}
|
|
6251
|
+
|
|
6252
|
+
// Example: Type-safe model usage
|
|
6253
|
+
const exampleUser: User = {
|
|
6254
|
+
id: '123e4567-e89b-12d3-a456-426614174000',
|
|
6255
|
+
email: 'user@example.com',
|
|
6256
|
+
name: 'Example User',
|
|
6257
|
+
createdAt: new Date(),
|
|
6258
|
+
updatedAt: new Date(),
|
|
6259
|
+
};
|
|
6260
|
+
|
|
6261
|
+
return (
|
|
6262
|
+
<main style={{ padding: '2rem', fontFamily: 'system-ui' }}>
|
|
6263
|
+
<h1>Welcome to ${options.name}</h1>
|
|
6264
|
+
|
|
6265
|
+
<section style={{ marginTop: '2rem' }}>
|
|
6266
|
+
<h2>API Status</h2>
|
|
6267
|
+
{health ? (
|
|
6268
|
+
<pre style={{ background: '#f0f0f0', padding: '1rem', borderRadius: '8px' }}>
|
|
6269
|
+
{JSON.stringify(health, null, 2)}
|
|
6270
|
+
</pre>
|
|
6271
|
+
) : (
|
|
6272
|
+
<p>Unable to connect to API at {apiUrl}</p>
|
|
6273
|
+
)}
|
|
6274
|
+
</section>
|
|
6275
|
+
|
|
6276
|
+
<section style={{ marginTop: '2rem' }}>
|
|
6277
|
+
<h2>Shared Models</h2>
|
|
6278
|
+
<p>This user object is typed from @${options.name}/models:</p>
|
|
6279
|
+
<pre style={{ background: '#f0f0f0', padding: '1rem', borderRadius: '8px' }}>
|
|
6280
|
+
{JSON.stringify(exampleUser, null, 2)}
|
|
6281
|
+
</pre>
|
|
6282
|
+
</section>
|
|
6283
|
+
|
|
6284
|
+
<section style={{ marginTop: '2rem' }}>
|
|
6285
|
+
<h2>Next Steps</h2>
|
|
6286
|
+
<ul>
|
|
6287
|
+
<li>Edit <code>apps/web/src/app/page.tsx</code> to customize this page</li>
|
|
6288
|
+
<li>Add API routes in <code>apps/api/src/endpoints/</code></li>
|
|
6289
|
+
<li>Define shared schemas in <code>packages/models/src/</code></li>
|
|
6290
|
+
</ul>
|
|
6291
|
+
</section>
|
|
6292
|
+
</main>
|
|
6293
|
+
);
|
|
6294
|
+
}
|
|
6295
|
+
`;
|
|
6296
|
+
const envLocal = `# API URL (injected automatically in workspace mode)
|
|
6297
|
+
API_URL=http://localhost:3000
|
|
6298
|
+
|
|
6299
|
+
# Other environment variables
|
|
6300
|
+
# NEXT_PUBLIC_API_URL=http://localhost:3000
|
|
6301
|
+
`;
|
|
6302
|
+
const gitignore = `.next/
|
|
6303
|
+
node_modules/
|
|
6304
|
+
.env.local
|
|
6305
|
+
*.log
|
|
6306
|
+
`;
|
|
6307
|
+
return [
|
|
6308
|
+
{
|
|
6309
|
+
path: "apps/web/package.json",
|
|
6310
|
+
content: `${JSON.stringify(packageJson, null, 2)}\n`
|
|
6311
|
+
},
|
|
6312
|
+
{
|
|
6313
|
+
path: "apps/web/next.config.ts",
|
|
6314
|
+
content: nextConfig
|
|
6315
|
+
},
|
|
6316
|
+
{
|
|
6317
|
+
path: "apps/web/tsconfig.json",
|
|
6318
|
+
content: `${JSON.stringify(tsConfig, null, 2)}\n`
|
|
6319
|
+
},
|
|
6320
|
+
{
|
|
6321
|
+
path: "apps/web/src/app/layout.tsx",
|
|
6322
|
+
content: layoutTsx
|
|
6323
|
+
},
|
|
6324
|
+
{
|
|
6325
|
+
path: "apps/web/src/app/page.tsx",
|
|
6326
|
+
content: pageTsx
|
|
6327
|
+
},
|
|
6328
|
+
{
|
|
6329
|
+
path: "apps/web/.env.local",
|
|
6330
|
+
content: envLocal
|
|
6331
|
+
},
|
|
6332
|
+
{
|
|
6333
|
+
path: "apps/web/.gitignore",
|
|
6334
|
+
content: gitignore
|
|
6335
|
+
}
|
|
6336
|
+
];
|
|
4347
6337
|
}
|
|
4348
6338
|
|
|
4349
6339
|
//#endregion
|
|
@@ -4411,21 +6401,36 @@ function getRunCommand(pkgManager, script) {
|
|
|
4411
6401
|
//#endregion
|
|
4412
6402
|
//#region src/init/index.ts
|
|
4413
6403
|
/**
|
|
6404
|
+
* Generate a secure random password for database users
|
|
6405
|
+
*/
|
|
6406
|
+
function generateDbPassword() {
|
|
6407
|
+
return `${Date.now().toString(36)}${Math.random().toString(36).slice(2)}${Math.random().toString(36).slice(2)}`;
|
|
6408
|
+
}
|
|
6409
|
+
/**
|
|
6410
|
+
* Generate database URL for an app
|
|
6411
|
+
* All apps connect to the same database, but use different users/schemas
|
|
6412
|
+
*/
|
|
6413
|
+
function generateDbUrl(appName, password, projectName, host = "localhost", port = 5432) {
|
|
6414
|
+
const userName = appName.replace(/-/g, "_");
|
|
6415
|
+
const dbName = `${projectName.replace(/-/g, "_")}_dev`;
|
|
6416
|
+
return `postgresql://${userName}:${password}@${host}:${port}/${dbName}`;
|
|
6417
|
+
}
|
|
6418
|
+
/**
|
|
4414
6419
|
* Main init command - scaffolds a new project
|
|
4415
6420
|
*/
|
|
4416
6421
|
async function initCommand(projectName, options = {}) {
|
|
4417
6422
|
const cwd = process.cwd();
|
|
4418
|
-
const
|
|
6423
|
+
const detectedPkgManager = detectPackageManager(cwd);
|
|
4419
6424
|
prompts.default.override({});
|
|
4420
6425
|
const onCancel = () => {
|
|
4421
6426
|
process.exit(0);
|
|
4422
6427
|
};
|
|
4423
6428
|
const answers = await (0, prompts.default)([
|
|
4424
6429
|
{
|
|
4425
|
-
type: projectName ? null : "text",
|
|
6430
|
+
type: projectName || options.name ? null : "text",
|
|
4426
6431
|
name: "name",
|
|
4427
6432
|
message: "Project name:",
|
|
4428
|
-
initial: "my-
|
|
6433
|
+
initial: "my-app",
|
|
4429
6434
|
validate: (value) => {
|
|
4430
6435
|
const nameValid = validateProjectName(value);
|
|
4431
6436
|
if (nameValid !== true) return nameValid;
|
|
@@ -4442,21 +6447,33 @@ async function initCommand(projectName, options = {}) {
|
|
|
4442
6447
|
initial: 0
|
|
4443
6448
|
},
|
|
4444
6449
|
{
|
|
4445
|
-
type: options.yes ? null : "
|
|
4446
|
-
name: "
|
|
4447
|
-
message: "
|
|
4448
|
-
|
|
6450
|
+
type: options.yes ? null : "multiselect",
|
|
6451
|
+
name: "services",
|
|
6452
|
+
message: "Services (space to select, enter to confirm):",
|
|
6453
|
+
choices: servicesChoices.map((c) => ({
|
|
6454
|
+
...c,
|
|
6455
|
+
selected: true
|
|
6456
|
+
})),
|
|
6457
|
+
hint: "- Space to select. Return to submit"
|
|
4449
6458
|
},
|
|
4450
6459
|
{
|
|
4451
|
-
type: options.yes ? null : "
|
|
4452
|
-
name: "
|
|
4453
|
-
message: "
|
|
4454
|
-
|
|
6460
|
+
type: options.yes ? null : "select",
|
|
6461
|
+
name: "packageManager",
|
|
6462
|
+
message: "Package manager:",
|
|
6463
|
+
choices: packageManagerChoices,
|
|
6464
|
+
initial: packageManagerChoices.findIndex((c) => c.value === detectedPkgManager)
|
|
6465
|
+
},
|
|
6466
|
+
{
|
|
6467
|
+
type: options.yes ? null : "select",
|
|
6468
|
+
name: "deployTarget",
|
|
6469
|
+
message: "Deployment target:",
|
|
6470
|
+
choices: deployTargetChoices,
|
|
6471
|
+
initial: 0
|
|
4455
6472
|
},
|
|
4456
6473
|
{
|
|
4457
|
-
type:
|
|
4458
|
-
name: "
|
|
4459
|
-
message: "Include
|
|
6474
|
+
type: options.yes ? null : "confirm",
|
|
6475
|
+
name: "telescope",
|
|
6476
|
+
message: "Include Telescope (debugging dashboard)?",
|
|
4460
6477
|
initial: true
|
|
4461
6478
|
},
|
|
4462
6479
|
{
|
|
@@ -4472,74 +6489,146 @@ async function initCommand(projectName, options = {}) {
|
|
|
4472
6489
|
message: "Routes structure:",
|
|
4473
6490
|
choices: routesStructureChoices,
|
|
4474
6491
|
initial: 0
|
|
4475
|
-
},
|
|
4476
|
-
{
|
|
4477
|
-
type: options.yes || options.monorepo !== void 0 ? null : "confirm",
|
|
4478
|
-
name: "monorepo",
|
|
4479
|
-
message: "Setup as monorepo?",
|
|
4480
|
-
initial: false
|
|
4481
|
-
},
|
|
4482
|
-
{
|
|
4483
|
-
type: (prev) => (prev === true || options.monorepo) && !options.apiPath ? "text" : null,
|
|
4484
|
-
name: "apiPath",
|
|
4485
|
-
message: "API app path:",
|
|
4486
|
-
initial: "apps/api"
|
|
4487
6492
|
}
|
|
4488
6493
|
], { onCancel });
|
|
4489
|
-
const name$1 = projectName || answers.name;
|
|
4490
|
-
if (!name$1)
|
|
4491
|
-
|
|
4492
|
-
|
|
4493
|
-
|
|
4494
|
-
|
|
4495
|
-
|
|
4496
|
-
|
|
4497
|
-
|
|
4498
|
-
|
|
6494
|
+
const name$1 = projectName || options.name || answers.name;
|
|
6495
|
+
if (!name$1) {
|
|
6496
|
+
console.error("Project name is required");
|
|
6497
|
+
process.exit(1);
|
|
6498
|
+
}
|
|
6499
|
+
if (projectName || options.name) {
|
|
6500
|
+
const nameToValidate = projectName || options.name;
|
|
6501
|
+
const nameValid = validateProjectName(nameToValidate);
|
|
6502
|
+
if (nameValid !== true) {
|
|
6503
|
+
console.error(nameValid);
|
|
6504
|
+
process.exit(1);
|
|
6505
|
+
}
|
|
6506
|
+
const dirValid = checkDirectoryExists(nameToValidate, cwd);
|
|
6507
|
+
if (dirValid !== true) {
|
|
6508
|
+
console.error(dirValid);
|
|
6509
|
+
process.exit(1);
|
|
6510
|
+
}
|
|
6511
|
+
}
|
|
6512
|
+
const template = options.template || answers.template || "api";
|
|
6513
|
+
const isFullstack = isFullstackTemplate(template);
|
|
6514
|
+
const monorepo = isFullstack || options.monorepo || false;
|
|
6515
|
+
const servicesArray = options.yes ? [
|
|
6516
|
+
"db",
|
|
6517
|
+
"cache",
|
|
6518
|
+
"mail"
|
|
6519
|
+
] : answers.services || [];
|
|
6520
|
+
const services = {
|
|
6521
|
+
db: servicesArray.includes("db"),
|
|
6522
|
+
cache: servicesArray.includes("cache"),
|
|
6523
|
+
mail: servicesArray.includes("mail")
|
|
6524
|
+
};
|
|
6525
|
+
const pkgManager = options.pm ? options.pm : options.yes ? "pnpm" : answers.packageManager ?? detectedPkgManager;
|
|
6526
|
+
const deployTarget = options.yes ? "dokploy" : answers.deployTarget ?? "dokploy";
|
|
6527
|
+
const database = services.db;
|
|
4499
6528
|
const templateOptions = {
|
|
4500
6529
|
name: name$1,
|
|
4501
|
-
template
|
|
6530
|
+
template,
|
|
4502
6531
|
telescope: options.yes ? true : answers.telescope ?? true,
|
|
4503
6532
|
database,
|
|
4504
|
-
studio: database
|
|
6533
|
+
studio: database,
|
|
4505
6534
|
loggerType: options.yes ? "pino" : answers.loggerType ?? "pino",
|
|
4506
6535
|
routesStructure: options.yes ? "centralized-endpoints" : answers.routesStructure ?? "centralized-endpoints",
|
|
4507
6536
|
monorepo,
|
|
4508
|
-
apiPath: monorepo ? options.apiPath ??
|
|
6537
|
+
apiPath: monorepo ? options.apiPath ?? "apps/api" : "",
|
|
6538
|
+
packageManager: pkgManager,
|
|
6539
|
+
deployTarget,
|
|
6540
|
+
services
|
|
4509
6541
|
};
|
|
4510
6542
|
const targetDir = (0, node_path.join)(cwd, name$1);
|
|
4511
|
-
const
|
|
6543
|
+
const baseTemplate = getTemplate(templateOptions.template);
|
|
4512
6544
|
const isMonorepo$1 = templateOptions.monorepo;
|
|
4513
6545
|
const apiPath = templateOptions.apiPath;
|
|
6546
|
+
console.log("\n🚀 Creating your project...\n");
|
|
4514
6547
|
await (0, node_fs_promises.mkdir)(targetDir, { recursive: true });
|
|
4515
6548
|
const appDir = isMonorepo$1 ? (0, node_path.join)(targetDir, apiPath) : targetDir;
|
|
4516
6549
|
if (isMonorepo$1) await (0, node_fs_promises.mkdir)(appDir, { recursive: true });
|
|
4517
|
-
const
|
|
4518
|
-
|
|
4519
|
-
|
|
4520
|
-
|
|
4521
|
-
|
|
4522
|
-
|
|
4523
|
-
|
|
4524
|
-
|
|
6550
|
+
const dbApps = [];
|
|
6551
|
+
if (isFullstack && services.db) dbApps.push({
|
|
6552
|
+
name: "api",
|
|
6553
|
+
password: generateDbPassword()
|
|
6554
|
+
}, {
|
|
6555
|
+
name: "auth",
|
|
6556
|
+
password: generateDbPassword()
|
|
6557
|
+
});
|
|
6558
|
+
const appFiles = baseTemplate ? [
|
|
6559
|
+
...generatePackageJson(templateOptions, baseTemplate),
|
|
6560
|
+
...generateConfigFiles(templateOptions, baseTemplate),
|
|
6561
|
+
...generateEnvFiles(templateOptions, baseTemplate),
|
|
6562
|
+
...generateSourceFiles(templateOptions, baseTemplate),
|
|
6563
|
+
...isMonorepo$1 ? [] : generateDockerFiles(templateOptions, baseTemplate, dbApps)
|
|
6564
|
+
] : [];
|
|
6565
|
+
const dockerFiles = isMonorepo$1 && baseTemplate ? generateDockerFiles(templateOptions, baseTemplate, dbApps) : [];
|
|
6566
|
+
const rootFiles = baseTemplate ? [...generateMonorepoFiles(templateOptions, baseTemplate), ...generateModelsPackage(templateOptions)] : [];
|
|
6567
|
+
const webAppFiles = isFullstack ? generateWebAppFiles(templateOptions) : [];
|
|
6568
|
+
const authAppFiles = isFullstack ? generateAuthAppFiles(templateOptions) : [];
|
|
4525
6569
|
for (const { path, content } of rootFiles) {
|
|
4526
6570
|
const fullPath = (0, node_path.join)(targetDir, path);
|
|
4527
6571
|
await (0, node_fs_promises.mkdir)((0, node_path.dirname)(fullPath), { recursive: true });
|
|
4528
6572
|
await (0, node_fs_promises.writeFile)(fullPath, content);
|
|
4529
6573
|
}
|
|
6574
|
+
for (const { path, content } of dockerFiles) {
|
|
6575
|
+
const fullPath = (0, node_path.join)(targetDir, path);
|
|
6576
|
+
await (0, node_fs_promises.mkdir)((0, node_path.dirname)(fullPath), { recursive: true });
|
|
6577
|
+
await (0, node_fs_promises.writeFile)(fullPath, content);
|
|
6578
|
+
}
|
|
4530
6579
|
for (const { path, content } of appFiles) {
|
|
4531
6580
|
const fullPath = (0, node_path.join)(appDir, path);
|
|
4532
|
-
const _displayPath = isMonorepo$1 ? `${apiPath}/${path}` : path;
|
|
4533
6581
|
await (0, node_fs_promises.mkdir)((0, node_path.dirname)(fullPath), { recursive: true });
|
|
4534
6582
|
await (0, node_fs_promises.writeFile)(fullPath, content);
|
|
4535
6583
|
}
|
|
6584
|
+
for (const { path, content } of webAppFiles) {
|
|
6585
|
+
const fullPath = (0, node_path.join)(targetDir, path);
|
|
6586
|
+
await (0, node_fs_promises.mkdir)((0, node_path.dirname)(fullPath), { recursive: true });
|
|
6587
|
+
await (0, node_fs_promises.writeFile)(fullPath, content);
|
|
6588
|
+
}
|
|
6589
|
+
for (const { path, content } of authAppFiles) {
|
|
6590
|
+
const fullPath = (0, node_path.join)(targetDir, path);
|
|
6591
|
+
await (0, node_fs_promises.mkdir)((0, node_path.dirname)(fullPath), { recursive: true });
|
|
6592
|
+
await (0, node_fs_promises.writeFile)(fullPath, content);
|
|
6593
|
+
}
|
|
6594
|
+
console.log("🔐 Initializing encrypted secrets...\n");
|
|
6595
|
+
const secretServices = [];
|
|
6596
|
+
if (services.db) secretServices.push("postgres");
|
|
6597
|
+
if (services.cache) secretServices.push("redis");
|
|
6598
|
+
const devSecrets = createStageSecrets("development", secretServices);
|
|
6599
|
+
const customSecrets = {
|
|
6600
|
+
NODE_ENV: "development",
|
|
6601
|
+
PORT: "3000",
|
|
6602
|
+
LOG_LEVEL: "debug",
|
|
6603
|
+
JWT_SECRET: `dev-${Date.now()}-${Math.random().toString(36).slice(2)}`
|
|
6604
|
+
};
|
|
6605
|
+
if (isFullstack && dbApps.length > 0) {
|
|
6606
|
+
for (const app of dbApps) {
|
|
6607
|
+
const urlKey = `${app.name.toUpperCase()}_DATABASE_URL`;
|
|
6608
|
+
customSecrets[urlKey] = generateDbUrl(app.name, app.password, name$1);
|
|
6609
|
+
const passwordKey = `${app.name.toUpperCase()}_DB_PASSWORD`;
|
|
6610
|
+
customSecrets[passwordKey] = app.password;
|
|
6611
|
+
}
|
|
6612
|
+
customSecrets.AUTH_PORT = "3002";
|
|
6613
|
+
customSecrets.BETTER_AUTH_SECRET = `better-auth-${Date.now()}-${Math.random().toString(36).slice(2)}`;
|
|
6614
|
+
customSecrets.BETTER_AUTH_URL = "http://localhost:3002";
|
|
6615
|
+
customSecrets.BETTER_AUTH_TRUSTED_ORIGINS = "http://localhost:3000,http://localhost:3001";
|
|
6616
|
+
}
|
|
6617
|
+
devSecrets.custom = customSecrets;
|
|
6618
|
+
await require_storage.writeStageSecrets(devSecrets, targetDir);
|
|
6619
|
+
const keyPath = require_storage.getKeyPath("development", name$1);
|
|
6620
|
+
console.log(` Secrets: .gkm/secrets/development.json (encrypted)`);
|
|
6621
|
+
console.log(` Key: ${keyPath}\n`);
|
|
4536
6622
|
if (!options.skipInstall) {
|
|
6623
|
+
console.log("\n📦 Installing dependencies...\n");
|
|
4537
6624
|
try {
|
|
4538
6625
|
(0, node_child_process.execSync)(getInstallCommand(pkgManager), {
|
|
4539
6626
|
cwd: targetDir,
|
|
4540
6627
|
stdio: "inherit"
|
|
4541
6628
|
});
|
|
4542
|
-
} catch {
|
|
6629
|
+
} catch {
|
|
6630
|
+
console.error("Failed to install dependencies");
|
|
6631
|
+
}
|
|
4543
6632
|
try {
|
|
4544
6633
|
(0, node_child_process.execSync)("npx @biomejs/biome format --write --unsafe .", {
|
|
4545
6634
|
cwd: targetDir,
|
|
@@ -4547,124 +6636,52 @@ async function initCommand(projectName, options = {}) {
|
|
|
4547
6636
|
});
|
|
4548
6637
|
} catch {}
|
|
4549
6638
|
}
|
|
4550
|
-
|
|
6639
|
+
printNextSteps(name$1, templateOptions, pkgManager);
|
|
4551
6640
|
}
|
|
4552
|
-
|
|
4553
|
-
//#endregion
|
|
4554
|
-
//#region src/secrets/generator.ts
|
|
4555
6641
|
/**
|
|
4556
|
-
*
|
|
4557
|
-
* @param length Password length (default: 32)
|
|
6642
|
+
* Print success message with next steps
|
|
4558
6643
|
*/
|
|
4559
|
-
function
|
|
4560
|
-
|
|
4561
|
-
}
|
|
4562
|
-
|
|
4563
|
-
|
|
4564
|
-
|
|
4565
|
-
|
|
4566
|
-
|
|
4567
|
-
|
|
4568
|
-
|
|
4569
|
-
},
|
|
4570
|
-
redis: {
|
|
4571
|
-
host: "redis",
|
|
4572
|
-
port: 6379,
|
|
4573
|
-
username: "default"
|
|
4574
|
-
},
|
|
4575
|
-
rabbitmq: {
|
|
4576
|
-
host: "rabbitmq",
|
|
4577
|
-
port: 5672,
|
|
4578
|
-
username: "app",
|
|
4579
|
-
vhost: "/"
|
|
6644
|
+
function printNextSteps(projectName, options, pkgManager) {
|
|
6645
|
+
const devCommand$1 = getRunCommand(pkgManager, "dev");
|
|
6646
|
+
const cdCommand = `cd ${projectName}`;
|
|
6647
|
+
console.log(`\n${"─".repeat(50)}`);
|
|
6648
|
+
console.log("\n✅ Project created successfully!\n");
|
|
6649
|
+
console.log("Next steps:\n");
|
|
6650
|
+
console.log(` ${cdCommand}`);
|
|
6651
|
+
if (options.services.db) {
|
|
6652
|
+
console.log(` # Start PostgreSQL (if not running)`);
|
|
6653
|
+
console.log(` docker compose up -d postgres`);
|
|
4580
6654
|
}
|
|
4581
|
-
};
|
|
4582
|
-
|
|
4583
|
-
|
|
4584
|
-
|
|
4585
|
-
|
|
4586
|
-
|
|
4587
|
-
|
|
4588
|
-
|
|
4589
|
-
|
|
4590
|
-
|
|
4591
|
-
}
|
|
4592
|
-
|
|
4593
|
-
|
|
4594
|
-
|
|
4595
|
-
|
|
4596
|
-
|
|
4597
|
-
|
|
4598
|
-
|
|
4599
|
-
|
|
4600
|
-
|
|
4601
|
-
|
|
4602
|
-
|
|
4603
|
-
|
|
4604
|
-
|
|
4605
|
-
|
|
4606
|
-
}
|
|
4607
|
-
|
|
4608
|
-
|
|
4609
|
-
|
|
4610
|
-
|
|
4611
|
-
const { password, host, port } = creds;
|
|
4612
|
-
return `redis://:${encodeURIComponent(password)}@${host}:${port}`;
|
|
4613
|
-
}
|
|
4614
|
-
/**
|
|
4615
|
-
* Generate connection URL for RabbitMQ.
|
|
4616
|
-
*/
|
|
4617
|
-
function generateRabbitmqUrl(creds) {
|
|
4618
|
-
const { username, password, host, port, vhost } = creds;
|
|
4619
|
-
const encodedVhost = encodeURIComponent(vhost ?? "/");
|
|
4620
|
-
return `amqp://${username}:${encodeURIComponent(password)}@${host}:${port}/${encodedVhost}`;
|
|
4621
|
-
}
|
|
4622
|
-
/**
|
|
4623
|
-
* Generate connection URLs from service credentials.
|
|
4624
|
-
*/
|
|
4625
|
-
function generateConnectionUrls(services) {
|
|
4626
|
-
const urls = {};
|
|
4627
|
-
if (services.postgres) urls.DATABASE_URL = generatePostgresUrl(services.postgres);
|
|
4628
|
-
if (services.redis) urls.REDIS_URL = generateRedisUrl(services.redis);
|
|
4629
|
-
if (services.rabbitmq) urls.RABBITMQ_URL = generateRabbitmqUrl(services.rabbitmq);
|
|
4630
|
-
return urls;
|
|
4631
|
-
}
|
|
4632
|
-
/**
|
|
4633
|
-
* Create a new StageSecrets object with generated credentials.
|
|
4634
|
-
*/
|
|
4635
|
-
function createStageSecrets(stage, services) {
|
|
4636
|
-
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
4637
|
-
const serviceCredentials = generateServicesCredentials(services);
|
|
4638
|
-
const urls = generateConnectionUrls(serviceCredentials);
|
|
4639
|
-
return {
|
|
4640
|
-
stage,
|
|
4641
|
-
createdAt: now,
|
|
4642
|
-
updatedAt: now,
|
|
4643
|
-
services: serviceCredentials,
|
|
4644
|
-
urls,
|
|
4645
|
-
custom: {}
|
|
4646
|
-
};
|
|
4647
|
-
}
|
|
4648
|
-
/**
|
|
4649
|
-
* Rotate password for a specific service.
|
|
4650
|
-
*/
|
|
4651
|
-
function rotateServicePassword(secrets, service) {
|
|
4652
|
-
const currentCreds = secrets.services[service];
|
|
4653
|
-
if (!currentCreds) throw new Error(`Service "${service}" not configured in secrets`);
|
|
4654
|
-
const newCreds = {
|
|
4655
|
-
...currentCreds,
|
|
4656
|
-
password: generateSecurePassword()
|
|
4657
|
-
};
|
|
4658
|
-
const newServices = {
|
|
4659
|
-
...secrets.services,
|
|
4660
|
-
[service]: newCreds
|
|
4661
|
-
};
|
|
4662
|
-
return {
|
|
4663
|
-
...secrets,
|
|
4664
|
-
updatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
4665
|
-
services: newServices,
|
|
4666
|
-
urls: generateConnectionUrls(newServices)
|
|
4667
|
-
};
|
|
6655
|
+
console.log(` ${devCommand$1}`);
|
|
6656
|
+
console.log("");
|
|
6657
|
+
if (options.monorepo) {
|
|
6658
|
+
console.log("📁 Project structure:");
|
|
6659
|
+
console.log(` ${projectName}/`);
|
|
6660
|
+
console.log(` ├── apps/`);
|
|
6661
|
+
console.log(` │ ├── api/ # Backend API`);
|
|
6662
|
+
if (isFullstackTemplate(options.template)) {
|
|
6663
|
+
console.log(` │ ├── auth/ # Auth service (better-auth)`);
|
|
6664
|
+
console.log(` │ └── web/ # Next.js frontend`);
|
|
6665
|
+
}
|
|
6666
|
+
console.log(` ├── packages/`);
|
|
6667
|
+
console.log(` │ └── models/ # Shared Zod schemas`);
|
|
6668
|
+
console.log(` ├── .gkm/secrets/ # Encrypted secrets`);
|
|
6669
|
+
console.log(` ├── gkm.config.ts # Workspace config`);
|
|
6670
|
+
console.log(` └── turbo.json # Turbo config`);
|
|
6671
|
+
console.log("");
|
|
6672
|
+
}
|
|
6673
|
+
console.log("🔐 Secrets management:");
|
|
6674
|
+
console.log(` gkm secrets:show --stage development # View secrets`);
|
|
6675
|
+
console.log(` gkm secrets:set KEY VALUE --stage development # Add secret`);
|
|
6676
|
+
console.log(` gkm secrets:init --stage production # Create production secrets`);
|
|
6677
|
+
console.log("");
|
|
6678
|
+
if (options.deployTarget === "dokploy") {
|
|
6679
|
+
console.log("🚀 Deployment:");
|
|
6680
|
+
console.log(` ${getRunCommand(pkgManager, "deploy")}`);
|
|
6681
|
+
console.log("");
|
|
6682
|
+
}
|
|
6683
|
+
console.log("📚 Documentation: https://docs.geekmidas.dev");
|
|
6684
|
+
console.log("");
|
|
4668
6685
|
}
|
|
4669
6686
|
|
|
4670
6687
|
//#endregion
|
|
@@ -4853,11 +6870,57 @@ function maskUrl(url) {
|
|
|
4853
6870
|
}
|
|
4854
6871
|
}
|
|
4855
6872
|
|
|
6873
|
+
//#endregion
|
|
6874
|
+
//#region src/test/index.ts
|
|
6875
|
+
/**
|
|
6876
|
+
* Run tests with secrets loaded from the specified stage.
|
|
6877
|
+
* Secrets are decrypted and injected into the environment.
|
|
6878
|
+
*/
|
|
6879
|
+
async function testCommand(options = {}) {
|
|
6880
|
+
const stage = options.stage ?? "development";
|
|
6881
|
+
console.log(`\n🧪 Running tests with ${stage} secrets...\n`);
|
|
6882
|
+
let envVars = {};
|
|
6883
|
+
try {
|
|
6884
|
+
const secrets = await require_storage.readStageSecrets(stage);
|
|
6885
|
+
if (secrets) {
|
|
6886
|
+
envVars = require_storage.toEmbeddableSecrets(secrets);
|
|
6887
|
+
console.log(` Loaded ${Object.keys(envVars).length} secrets from ${stage}\n`);
|
|
6888
|
+
} else console.log(` No secrets found for ${stage}, running without secrets\n`);
|
|
6889
|
+
} catch (error) {
|
|
6890
|
+
if (error instanceof Error && error.message.includes("key not found")) console.log(` Decryption key not found for ${stage}, running without secrets\n`);
|
|
6891
|
+
else throw error;
|
|
6892
|
+
}
|
|
6893
|
+
const args = [];
|
|
6894
|
+
if (options.run) args.push("run");
|
|
6895
|
+
else if (options.watch) args.push("--watch");
|
|
6896
|
+
if (options.coverage) args.push("--coverage");
|
|
6897
|
+
if (options.ui) args.push("--ui");
|
|
6898
|
+
if (options.pattern) args.push(options.pattern);
|
|
6899
|
+
const vitestProcess = (0, node_child_process.spawn)("npx", ["vitest", ...args], {
|
|
6900
|
+
cwd: process.cwd(),
|
|
6901
|
+
stdio: "inherit",
|
|
6902
|
+
env: {
|
|
6903
|
+
...process.env,
|
|
6904
|
+
...envVars,
|
|
6905
|
+
NODE_ENV: "test"
|
|
6906
|
+
}
|
|
6907
|
+
});
|
|
6908
|
+
return new Promise((resolve$1, reject) => {
|
|
6909
|
+
vitestProcess.on("close", (code) => {
|
|
6910
|
+
if (code === 0) resolve$1();
|
|
6911
|
+
else reject(new Error(`Tests failed with exit code ${code}`));
|
|
6912
|
+
});
|
|
6913
|
+
vitestProcess.on("error", (error) => {
|
|
6914
|
+
reject(error);
|
|
6915
|
+
});
|
|
6916
|
+
});
|
|
6917
|
+
}
|
|
6918
|
+
|
|
4856
6919
|
//#endregion
|
|
4857
6920
|
//#region src/index.ts
|
|
4858
6921
|
const program = new commander.Command();
|
|
4859
6922
|
program.name("gkm").description("GeekMidas backend framework CLI").version(package_default.version).option("--cwd <path>", "Change working directory");
|
|
4860
|
-
program.command("init").description("Scaffold a new project").argument("[name]", "Project name").option("--template <template>", "Project template (minimal, api, serverless, worker)").option("--skip-install", "Skip dependency installation", false).option("-y, --yes", "Skip prompts, use defaults", false).option("--monorepo", "Setup as monorepo with packages/models", false).option("--api-path <path>", "API app path in monorepo (default: apps/api)").action(async (name$1, options) => {
|
|
6923
|
+
program.command("init").description("Scaffold a new project").argument("[name]", "Project name").option("--template <template>", "Project template (minimal, api, serverless, worker)").option("--skip-install", "Skip dependency installation", false).option("-y, --yes", "Skip prompts, use defaults", false).option("--monorepo", "Setup as monorepo with packages/models", false).option("--api-path <path>", "API app path in monorepo (default: apps/api)").option("--pm <manager>", "Package manager (pnpm, npm, yarn, bun)").action(async (name$1, options) => {
|
|
4861
6924
|
try {
|
|
4862
6925
|
const globalOptions = program.opts();
|
|
4863
6926
|
if (globalOptions.cwd) process.chdir(globalOptions.cwd);
|
|
@@ -4914,6 +6977,19 @@ program.command("dev").description("Start development server with automatic relo
|
|
|
4914
6977
|
process.exit(1);
|
|
4915
6978
|
}
|
|
4916
6979
|
});
|
|
6980
|
+
program.command("test").description("Run tests with secrets loaded from environment").option("--stage <stage>", "Stage to load secrets from", "development").option("--run", "Run tests once without watch mode").option("--watch", "Enable watch mode").option("--coverage", "Generate coverage report").option("--ui", "Open Vitest UI").argument("[pattern]", "Pattern to filter tests").action(async (pattern, options) => {
|
|
6981
|
+
try {
|
|
6982
|
+
const globalOptions = program.opts();
|
|
6983
|
+
if (globalOptions.cwd) process.chdir(globalOptions.cwd);
|
|
6984
|
+
await testCommand({
|
|
6985
|
+
...options,
|
|
6986
|
+
pattern
|
|
6987
|
+
});
|
|
6988
|
+
} catch (error) {
|
|
6989
|
+
console.error(error instanceof Error ? error.message : "Command failed");
|
|
6990
|
+
process.exit(1);
|
|
6991
|
+
}
|
|
6992
|
+
});
|
|
4917
6993
|
program.command("cron").description("Manage cron jobs").action(() => {
|
|
4918
6994
|
const globalOptions = program.opts();
|
|
4919
6995
|
if (globalOptions.cwd) process.chdir(globalOptions.cwd);
|