@geekmidas/cli 0.17.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 +2644 -564
- package/dist/index.cjs.map +1 -1
- package/dist/index.mjs +2639 -564
- 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 +9 -4
- 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 +223 -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 -0
- 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,12 +21,13 @@ 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";
|
|
28
|
-
var version = "0.
|
|
30
|
+
var version = "0.18.0";
|
|
29
31
|
var description = "CLI tools for building Lambda handlers, server applications, and generating OpenAPI specs";
|
|
30
32
|
var private$1 = false;
|
|
31
33
|
var type = "module";
|
|
@@ -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
|
|
@@ -1599,6 +2316,7 @@ function getPmConfig(pm) {
|
|
|
1599
2316
|
cacheTarget: "/root/.local/share/pnpm/store",
|
|
1600
2317
|
cacheId: "pnpm",
|
|
1601
2318
|
run: "pnpm",
|
|
2319
|
+
exec: "pnpm exec",
|
|
1602
2320
|
dlx: "pnpm dlx",
|
|
1603
2321
|
addGlobal: "pnpm add -g"
|
|
1604
2322
|
},
|
|
@@ -1610,6 +2328,7 @@ function getPmConfig(pm) {
|
|
|
1610
2328
|
cacheTarget: "/root/.npm",
|
|
1611
2329
|
cacheId: "npm",
|
|
1612
2330
|
run: "npm run",
|
|
2331
|
+
exec: "npx",
|
|
1613
2332
|
dlx: "npx",
|
|
1614
2333
|
addGlobal: "npm install -g"
|
|
1615
2334
|
},
|
|
@@ -1621,6 +2340,7 @@ function getPmConfig(pm) {
|
|
|
1621
2340
|
cacheTarget: "/root/.yarn/cache",
|
|
1622
2341
|
cacheId: "yarn",
|
|
1623
2342
|
run: "yarn",
|
|
2343
|
+
exec: "yarn exec",
|
|
1624
2344
|
dlx: "yarn dlx",
|
|
1625
2345
|
addGlobal: "yarn global add"
|
|
1626
2346
|
},
|
|
@@ -1632,6 +2352,7 @@ function getPmConfig(pm) {
|
|
|
1632
2352
|
cacheTarget: "/root/.bun/install/cache",
|
|
1633
2353
|
cacheId: "bun",
|
|
1634
2354
|
run: "bun run",
|
|
2355
|
+
exec: "bunx",
|
|
1635
2356
|
dlx: "bunx",
|
|
1636
2357
|
addGlobal: "bun add -g"
|
|
1637
2358
|
}
|
|
@@ -1688,8 +2409,14 @@ WORKDIR /app
|
|
|
1688
2409
|
# Copy source (deps already installed)
|
|
1689
2410
|
COPY . .
|
|
1690
2411
|
|
|
1691
|
-
#
|
|
1692
|
-
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
|
|
1693
2420
|
|
|
1694
2421
|
# Stage 3: Production
|
|
1695
2422
|
FROM ${baseImage} AS runner
|
|
@@ -1770,8 +2497,14 @@ WORKDIR /app
|
|
|
1770
2497
|
# Copy pruned source
|
|
1771
2498
|
COPY --from=pruner /app/out/full/ ./
|
|
1772
2499
|
|
|
1773
|
-
#
|
|
1774
|
-
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
|
|
1775
2508
|
|
|
1776
2509
|
# Stage 4: Production
|
|
1777
2510
|
FROM ${baseImage} AS runner
|
|
@@ -1913,8 +2646,8 @@ function resolveDockerConfig$1(config) {
|
|
|
1913
2646
|
const docker = config.docker ?? {};
|
|
1914
2647
|
let defaultImageName = "api";
|
|
1915
2648
|
try {
|
|
1916
|
-
const pkg = require(`${process.cwd()}/package.json`);
|
|
1917
|
-
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(/^@[^/]+\//, "");
|
|
1918
2651
|
} catch {}
|
|
1919
2652
|
return {
|
|
1920
2653
|
registry: docker.registry ?? "",
|
|
@@ -1924,20 +2657,194 @@ function resolveDockerConfig$1(config) {
|
|
|
1924
2657
|
compose: docker.compose
|
|
1925
2658
|
};
|
|
1926
2659
|
}
|
|
1927
|
-
|
|
1928
|
-
//#endregion
|
|
1929
|
-
//#region src/docker/index.ts
|
|
1930
|
-
const logger$5 = console;
|
|
1931
2660
|
/**
|
|
1932
|
-
*
|
|
1933
|
-
*
|
|
1934
|
-
*
|
|
1935
|
-
* Default: Multi-stage Dockerfile that builds from source inside Docker
|
|
1936
|
-
* --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
|
|
1937
2664
|
*/
|
|
1938
|
-
|
|
1939
|
-
const
|
|
1940
|
-
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);
|
|
1941
2848
|
const serverConfig = typeof config.providers?.server === "object" ? config.providers.server : void 0;
|
|
1942
2849
|
const healthCheckPath = serverConfig?.production?.healthCheck ?? "/health";
|
|
1943
2850
|
const useSlim = options.slim === true;
|
|
@@ -1958,9 +2865,9 @@ async function dockerCommand(options) {
|
|
|
1958
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");
|
|
1959
2866
|
let turboPackage = options.turboPackage ?? dockerConfig.imageName;
|
|
1960
2867
|
if (useTurbo && !options.turboPackage) try {
|
|
1961
|
-
const pkg = require(`${process.cwd()}/package.json`);
|
|
1962
|
-
if (pkg.name) {
|
|
1963
|
-
turboPackage = pkg.name;
|
|
2868
|
+
const pkg$1 = require(`${process.cwd()}/package.json`);
|
|
2869
|
+
if (pkg$1.name) {
|
|
2870
|
+
turboPackage = pkg$1.name;
|
|
1964
2871
|
logger$5.log(` Turbo package: ${turboPackage}`);
|
|
1965
2872
|
}
|
|
1966
2873
|
} catch {}
|
|
@@ -2077,6 +2984,85 @@ async function pushDockerImage(imageName, options) {
|
|
|
2077
2984
|
throw new Error(`Failed to push Docker image: ${error instanceof Error ? error.message : "Unknown error"}`);
|
|
2078
2985
|
}
|
|
2079
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
|
+
}
|
|
2080
3066
|
|
|
2081
3067
|
//#endregion
|
|
2082
3068
|
//#region src/deploy/docker.ts
|
|
@@ -2088,8 +3074,8 @@ function getAppNameFromCwd() {
|
|
|
2088
3074
|
const packageJsonPath = (0, node_path.join)(process.cwd(), "package.json");
|
|
2089
3075
|
if (!(0, node_fs.existsSync)(packageJsonPath)) return void 0;
|
|
2090
3076
|
try {
|
|
2091
|
-
const pkg = JSON.parse((0, node_fs.readFileSync)(packageJsonPath, "utf-8"));
|
|
2092
|
-
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(/^@[^/]+\//, "");
|
|
2093
3079
|
} catch {}
|
|
2094
3080
|
return void 0;
|
|
2095
3081
|
}
|
|
@@ -2105,8 +3091,8 @@ function getAppNameFromPackageJson() {
|
|
|
2105
3091
|
const packageJsonPath = (0, node_path.join)(projectRoot, "package.json");
|
|
2106
3092
|
if (!(0, node_fs.existsSync)(packageJsonPath)) return void 0;
|
|
2107
3093
|
try {
|
|
2108
|
-
const pkg = JSON.parse((0, node_fs.readFileSync)(packageJsonPath, "utf-8"));
|
|
2109
|
-
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(/^@[^/]+\//, "");
|
|
2110
3096
|
} catch {}
|
|
2111
3097
|
return void 0;
|
|
2112
3098
|
}
|
|
@@ -2579,7 +3565,7 @@ async function provisionServices(api, projectId, environmentId, appName, service
|
|
|
2579
3565
|
*/
|
|
2580
3566
|
async function ensureDokploySetup(config, dockerConfig, stage, services) {
|
|
2581
3567
|
logger$1.log("\n🔧 Checking Dokploy setup...");
|
|
2582
|
-
const { readStageSecrets: readStageSecrets$1 } = await Promise.resolve().then(() => require("./storage-
|
|
3568
|
+
const { readStageSecrets: readStageSecrets$1 } = await Promise.resolve().then(() => require("./storage-fOR8dMu5.cjs"));
|
|
2583
3569
|
const existingSecrets = await readStageSecrets$1(stage);
|
|
2584
3570
|
const existingUrls = {
|
|
2585
3571
|
DATABASE_URL: existingSecrets?.urls?.DATABASE_URL,
|
|
@@ -2757,94 +3743,335 @@ function generateTag(stage) {
|
|
|
2757
3743
|
return `${stage}-${timestamp}`;
|
|
2758
3744
|
}
|
|
2759
3745
|
/**
|
|
2760
|
-
*
|
|
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
|
|
2761
3752
|
*/
|
|
2762
|
-
async function
|
|
2763
|
-
const { provider, stage, tag,
|
|
2764
|
-
|
|
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...`);
|
|
2765
3757
|
logger$1.log(` Stage: ${stage}`);
|
|
2766
|
-
const config = await require_config.loadConfig();
|
|
2767
3758
|
const imageTag = tag ?? generateTag(stage);
|
|
2768
3759
|
logger$1.log(` Tag: ${imageTag}`);
|
|
2769
|
-
const
|
|
2770
|
-
|
|
2771
|
-
|
|
2772
|
-
|
|
2773
|
-
|
|
2774
|
-
|
|
2775
|
-
|
|
2776
|
-
|
|
2777
|
-
|
|
2778
|
-
const
|
|
2779
|
-
|
|
2780
|
-
|
|
2781
|
-
|
|
2782
|
-
|
|
2783
|
-
postgres: Boolean(composeServices.postgres),
|
|
2784
|
-
redis: Boolean(composeServices.redis),
|
|
2785
|
-
rabbitmq: Boolean(composeServices.rabbitmq)
|
|
2786
|
-
} : void 0;
|
|
2787
|
-
const setupResult = await ensureDokploySetup(config, dockerConfig, stage, dockerServices);
|
|
2788
|
-
dokployConfig = setupResult.config;
|
|
2789
|
-
finalRegistry = dokployConfig.registry ?? dockerConfig.registry;
|
|
2790
|
-
if (setupResult.serviceUrls) {
|
|
2791
|
-
const { readStageSecrets: readStageSecrets$1, writeStageSecrets: writeStageSecrets$1, initStageSecrets } = await Promise.resolve().then(() => require("./storage-UfyTn7Zm.cjs"));
|
|
2792
|
-
let secrets = await readStageSecrets$1(stage);
|
|
2793
|
-
if (!secrets) {
|
|
2794
|
-
logger$1.log(` Creating secrets file for stage "${stage}"...`);
|
|
2795
|
-
secrets = initStageSecrets(stage);
|
|
2796
|
-
}
|
|
2797
|
-
let updated = false;
|
|
2798
|
-
const urlFields = [
|
|
2799
|
-
"DATABASE_URL",
|
|
2800
|
-
"REDIS_URL",
|
|
2801
|
-
"RABBITMQ_URL"
|
|
2802
|
-
];
|
|
2803
|
-
for (const [key, value] of Object.entries(setupResult.serviceUrls)) {
|
|
2804
|
-
if (!value) continue;
|
|
2805
|
-
if (urlFields.includes(key)) {
|
|
2806
|
-
const urlKey = key;
|
|
2807
|
-
if (!secrets.urls[urlKey]) {
|
|
2808
|
-
secrets.urls[urlKey] = value;
|
|
2809
|
-
logger$1.log(` Saved ${key} to secrets.urls`);
|
|
2810
|
-
updated = true;
|
|
2811
|
-
}
|
|
2812
|
-
} else if (!secrets.custom[key]) {
|
|
2813
|
-
secrets.custom[key] = value;
|
|
2814
|
-
logger$1.log(` Saved ${key} to secrets.custom`);
|
|
2815
|
-
updated = true;
|
|
2816
|
-
}
|
|
2817
|
-
}
|
|
2818
|
-
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;
|
|
2819
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`);
|
|
2820
3781
|
}
|
|
2821
|
-
|
|
2822
|
-
|
|
2823
|
-
|
|
2824
|
-
|
|
2825
|
-
|
|
2826
|
-
|
|
2827
|
-
|
|
2828
|
-
|
|
2829
|
-
|
|
2830
|
-
|
|
2831
|
-
|
|
2832
|
-
|
|
2833
|
-
|
|
2834
|
-
|
|
2835
|
-
|
|
2836
|
-
|
|
2837
|
-
|
|
2838
|
-
|
|
2839
|
-
|
|
2840
|
-
|
|
2841
|
-
|
|
2842
|
-
|
|
2843
|
-
|
|
2844
|
-
|
|
2845
|
-
|
|
2846
|
-
|
|
2847
|
-
|
|
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,
|
|
2848
4075
|
tag: imageTag,
|
|
2849
4076
|
skipPush: false,
|
|
2850
4077
|
masterKey,
|
|
@@ -2871,10 +4098,361 @@ async function deployCommand(options) {
|
|
|
2871
4098
|
};
|
|
2872
4099
|
break;
|
|
2873
4100
|
}
|
|
2874
|
-
default: throw new Error(`Unknown deploy provider: ${provider}\nSupported providers: docker, dokploy, aws-lambda`);
|
|
2875
|
-
}
|
|
2876
|
-
logger$1.log("\n✅ Deployment complete!");
|
|
2877
|
-
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
|
+
];
|
|
2878
4456
|
}
|
|
2879
4457
|
|
|
2880
4458
|
//#endregion
|
|
@@ -2886,6 +4464,7 @@ function generateConfigFiles(options, template) {
|
|
|
2886
4464
|
const { telescope, studio, routesStructure } = options;
|
|
2887
4465
|
const isServerless = template.name === "serverless";
|
|
2888
4466
|
const hasWorker = template.name === "worker";
|
|
4467
|
+
const isFullstack = options.template === "fullstack";
|
|
2889
4468
|
const getRoutesGlob = () => {
|
|
2890
4469
|
switch (routesStructure) {
|
|
2891
4470
|
case "centralized-endpoints": return "./src/endpoints/**/*.ts";
|
|
@@ -2893,6 +4472,14 @@ function generateConfigFiles(options, template) {
|
|
|
2893
4472
|
case "domain-based": return "./src/**/routes/*.ts";
|
|
2894
4473
|
}
|
|
2895
4474
|
};
|
|
4475
|
+
if (isFullstack) return generateSingleAppConfigFiles(options, template, {
|
|
4476
|
+
telescope,
|
|
4477
|
+
studio,
|
|
4478
|
+
routesStructure,
|
|
4479
|
+
isServerless,
|
|
4480
|
+
hasWorker,
|
|
4481
|
+
getRoutesGlob
|
|
4482
|
+
});
|
|
2896
4483
|
let gkmConfig = `import { defineConfig } from '@geekmidas/cli/config';
|
|
2897
4484
|
|
|
2898
4485
|
export default defineConfig({
|
|
@@ -2921,8 +4508,7 @@ export default defineConfig({
|
|
|
2921
4508
|
const tsConfig = options.monorepo ? {
|
|
2922
4509
|
extends: "../../tsconfig.json",
|
|
2923
4510
|
compilerOptions: {
|
|
2924
|
-
|
|
2925
|
-
rootDir: "./src",
|
|
4511
|
+
noEmit: true,
|
|
2926
4512
|
baseUrl: ".",
|
|
2927
4513
|
paths: { [`@${options.name}/*`]: ["../../packages/*/src"] }
|
|
2928
4514
|
},
|
|
@@ -2955,7 +4541,7 @@ export default defineConfig({
|
|
|
2955
4541
|
content: `${JSON.stringify(tsConfig, null, 2)}\n`
|
|
2956
4542
|
}];
|
|
2957
4543
|
const biomeConfig = {
|
|
2958
|
-
$schema: "https://biomejs.dev/schemas/
|
|
4544
|
+
$schema: "https://biomejs.dev/schemas/2.3.0/schema.json",
|
|
2959
4545
|
vcs: {
|
|
2960
4546
|
enabled: true,
|
|
2961
4547
|
clientKind: "git",
|
|
@@ -3038,23 +4624,46 @@ export default defineConfig({
|
|
|
3038
4624
|
}
|
|
3039
4625
|
];
|
|
3040
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
|
+
}
|
|
3041
4643
|
|
|
3042
4644
|
//#endregion
|
|
3043
4645
|
//#region src/init/generators/docker.ts
|
|
3044
4646
|
/**
|
|
3045
4647
|
* Generate docker-compose.yml based on template and options
|
|
3046
4648
|
*/
|
|
3047
|
-
function generateDockerFiles(options, template) {
|
|
4649
|
+
function generateDockerFiles(options, template, dbApps) {
|
|
3048
4650
|
const { database } = options;
|
|
3049
4651
|
const isServerless = template.name === "serverless";
|
|
3050
4652
|
const hasWorker = template.name === "worker";
|
|
4653
|
+
const isFullstack = options.template === "fullstack";
|
|
3051
4654
|
const services = [];
|
|
3052
4655
|
const volumes = [];
|
|
4656
|
+
const files = [];
|
|
3053
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` : "";
|
|
3054
4663
|
services.push(` postgres:
|
|
3055
4664
|
image: postgres:16-alpine
|
|
3056
4665
|
container_name: ${options.name}-postgres
|
|
3057
|
-
restart: unless-stopped
|
|
4666
|
+
restart: unless-stopped${envFile}
|
|
3058
4667
|
environment:
|
|
3059
4668
|
POSTGRES_USER: postgres
|
|
3060
4669
|
POSTGRES_PASSWORD: postgres
|
|
@@ -3062,13 +4671,23 @@ function generateDockerFiles(options, template) {
|
|
|
3062
4671
|
ports:
|
|
3063
4672
|
- '5432:5432'
|
|
3064
4673
|
volumes:
|
|
3065
|
-
- postgres_data:/var/lib/postgresql/data
|
|
4674
|
+
- postgres_data:/var/lib/postgresql/data${initVolume}
|
|
3066
4675
|
healthcheck:
|
|
3067
4676
|
test: ['CMD-SHELL', 'pg_isready -U postgres']
|
|
3068
4677
|
interval: 5s
|
|
3069
4678
|
timeout: 5s
|
|
3070
4679
|
retries: 5`);
|
|
3071
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
|
+
}
|
|
3072
4691
|
}
|
|
3073
4692
|
if (isServerless) {
|
|
3074
4693
|
services.push(` redis:
|
|
@@ -3144,105 +4763,85 @@ ${services.join("\n\n")}
|
|
|
3144
4763
|
volumes:
|
|
3145
4764
|
${volumes.join("\n")}
|
|
3146
4765
|
`;
|
|
3147
|
-
|
|
4766
|
+
files.push({
|
|
3148
4767
|
path: "docker-compose.yml",
|
|
3149
4768
|
content: dockerCompose
|
|
3150
|
-
}
|
|
4769
|
+
});
|
|
4770
|
+
return files;
|
|
3151
4771
|
}
|
|
3152
|
-
|
|
3153
|
-
//#endregion
|
|
3154
|
-
//#region src/init/generators/env.ts
|
|
3155
4772
|
/**
|
|
3156
|
-
* Generate
|
|
4773
|
+
* Generate .env file for docker-compose with database passwords
|
|
3157
4774
|
*/
|
|
3158
|
-
function
|
|
3159
|
-
const
|
|
3160
|
-
|
|
3161
|
-
|
|
3162
|
-
|
|
3163
|
-
|
|
3164
|
-
|
|
3165
|
-
|
|
3166
|
-
|
|
3167
|
-
if (isServerless) baseEnv = `# AWS
|
|
3168
|
-
STAGE=dev
|
|
3169
|
-
AWS_REGION=us-east-1
|
|
3170
|
-
LOG_LEVEL=info
|
|
3171
|
-
`;
|
|
3172
|
-
if (database) baseEnv += `
|
|
3173
|
-
# Database
|
|
3174
|
-
DATABASE_URL=postgresql://user:password@localhost:5432/mydb
|
|
3175
|
-
`;
|
|
3176
|
-
if (hasWorker) baseEnv += `
|
|
3177
|
-
# Message Queue
|
|
3178
|
-
RABBITMQ_URL=amqp://localhost:5672
|
|
3179
|
-
`;
|
|
3180
|
-
baseEnv += `
|
|
3181
|
-
# Authentication
|
|
3182
|
-
JWT_SECRET=your-secret-key-change-in-production
|
|
3183
|
-
`;
|
|
3184
|
-
let devEnv = `# Development Environment
|
|
3185
|
-
NODE_ENV=development
|
|
3186
|
-
PORT=3000
|
|
3187
|
-
LOG_LEVEL=debug
|
|
3188
|
-
`;
|
|
3189
|
-
if (isServerless) devEnv = `# Development Environment
|
|
3190
|
-
STAGE=dev
|
|
3191
|
-
AWS_REGION=us-east-1
|
|
3192
|
-
LOG_LEVEL=debug
|
|
3193
|
-
`;
|
|
3194
|
-
if (database) devEnv += `
|
|
3195
|
-
# Database
|
|
3196
|
-
DATABASE_URL=postgresql://postgres:postgres@localhost:5432/mydb_dev
|
|
3197
|
-
`;
|
|
3198
|
-
if (hasWorker) devEnv += `
|
|
3199
|
-
# Message Queue
|
|
3200
|
-
RABBITMQ_URL=amqp://localhost:5672
|
|
3201
|
-
`;
|
|
3202
|
-
devEnv += `
|
|
3203
|
-
# Authentication
|
|
3204
|
-
JWT_SECRET=dev-secret-not-for-production
|
|
3205
|
-
`;
|
|
3206
|
-
let testEnv = `# Test Environment
|
|
3207
|
-
NODE_ENV=test
|
|
3208
|
-
PORT=3001
|
|
3209
|
-
LOG_LEVEL=error
|
|
3210
|
-
`;
|
|
3211
|
-
if (isServerless) testEnv = `# Test Environment
|
|
3212
|
-
STAGE=test
|
|
3213
|
-
AWS_REGION=us-east-1
|
|
3214
|
-
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")}
|
|
3215
4784
|
`;
|
|
3216
|
-
|
|
3217
|
-
|
|
3218
|
-
|
|
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
|
|
3219
4807
|
`;
|
|
3220
|
-
|
|
3221
|
-
#
|
|
3222
|
-
|
|
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
|
|
3223
4821
|
`;
|
|
3224
|
-
|
|
3225
|
-
|
|
3226
|
-
|
|
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!"
|
|
3227
4832
|
`;
|
|
3228
|
-
|
|
3229
|
-
|
|
3230
|
-
|
|
3231
|
-
|
|
3232
|
-
|
|
3233
|
-
|
|
3234
|
-
|
|
3235
|
-
|
|
3236
|
-
|
|
3237
|
-
|
|
3238
|
-
|
|
3239
|
-
|
|
3240
|
-
},
|
|
3241
|
-
{
|
|
3242
|
-
path: ".env.test",
|
|
3243
|
-
content: testEnv
|
|
3244
|
-
}
|
|
3245
|
-
];
|
|
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 = [];
|
|
3246
4845
|
if (!options.monorepo) {
|
|
3247
4846
|
const gitignore = `# Dependencies
|
|
3248
4847
|
node_modules/
|
|
@@ -3251,7 +4850,7 @@ node_modules/
|
|
|
3251
4850
|
dist/
|
|
3252
4851
|
.gkm/
|
|
3253
4852
|
|
|
3254
|
-
# Environment
|
|
4853
|
+
# Environment (legacy - use gkm secrets instead)
|
|
3255
4854
|
.env
|
|
3256
4855
|
.env.local
|
|
3257
4856
|
.env.*.local
|
|
@@ -3320,6 +4919,8 @@ function generateModelsPackage(options) {
|
|
|
3320
4919
|
const tsConfig = {
|
|
3321
4920
|
extends: "../../tsconfig.json",
|
|
3322
4921
|
compilerOptions: {
|
|
4922
|
+
declaration: true,
|
|
4923
|
+
declarationMap: true,
|
|
3323
4924
|
outDir: "./dist",
|
|
3324
4925
|
rootDir: "./src"
|
|
3325
4926
|
},
|
|
@@ -3406,23 +5007,27 @@ export type UpdateUser = z.infer<typeof updateUserSchema>;
|
|
|
3406
5007
|
*/
|
|
3407
5008
|
function generateMonorepoFiles(options, _template) {
|
|
3408
5009
|
if (!options.monorepo) return [];
|
|
5010
|
+
const isFullstack = options.template === "fullstack";
|
|
3409
5011
|
const rootPackageJson = {
|
|
3410
5012
|
name: options.name,
|
|
3411
5013
|
version: "0.0.1",
|
|
3412
5014
|
private: true,
|
|
3413
5015
|
type: "module",
|
|
5016
|
+
packageManager: "pnpm@10.13.1",
|
|
3414
5017
|
scripts: {
|
|
3415
|
-
dev: "turbo dev",
|
|
3416
|
-
build: "turbo build",
|
|
3417
|
-
test: "turbo test",
|
|
3418
|
-
"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",
|
|
3419
5022
|
typecheck: "turbo typecheck",
|
|
3420
5023
|
lint: "biome lint .",
|
|
3421
5024
|
fmt: "biome format . --write",
|
|
3422
|
-
"fmt:check": "biome format ."
|
|
5025
|
+
"fmt:check": "biome format .",
|
|
5026
|
+
...options.deployTarget === "dokploy" ? { deploy: "gkm deploy --provider dokploy --stage production" } : {}
|
|
3423
5027
|
},
|
|
3424
5028
|
devDependencies: {
|
|
3425
|
-
"@biomejs/biome": "~
|
|
5029
|
+
"@biomejs/biome": "~2.3.0",
|
|
5030
|
+
"@geekmidas/cli": GEEKMIDAS_VERSIONS["@geekmidas/cli"],
|
|
3426
5031
|
turbo: "~2.3.0",
|
|
3427
5032
|
typescript: "~5.8.2",
|
|
3428
5033
|
vitest: "~4.0.0"
|
|
@@ -3435,7 +5040,7 @@ function generateMonorepoFiles(options, _template) {
|
|
|
3435
5040
|
- 'packages/*'
|
|
3436
5041
|
`;
|
|
3437
5042
|
const biomeConfig = {
|
|
3438
|
-
$schema: "https://biomejs.dev/schemas/
|
|
5043
|
+
$schema: "https://biomejs.dev/schemas/2.3.0/schema.json",
|
|
3439
5044
|
vcs: {
|
|
3440
5045
|
enabled: true,
|
|
3441
5046
|
clientKind: "git",
|
|
@@ -3510,6 +5115,7 @@ dist/
|
|
|
3510
5115
|
.env
|
|
3511
5116
|
.env.local
|
|
3512
5117
|
.env.*.local
|
|
5118
|
+
docker/.env
|
|
3513
5119
|
|
|
3514
5120
|
# IDE
|
|
3515
5121
|
.idea/
|
|
@@ -3546,14 +5152,27 @@ coverage/
|
|
|
3546
5152
|
esModuleInterop: true,
|
|
3547
5153
|
skipLibCheck: true,
|
|
3548
5154
|
forceConsistentCasingInFileNames: true,
|
|
3549
|
-
resolveJsonModule: true
|
|
3550
|
-
declaration: true,
|
|
3551
|
-
declarationMap: true,
|
|
3552
|
-
composite: true
|
|
5155
|
+
resolveJsonModule: true
|
|
3553
5156
|
},
|
|
3554
5157
|
exclude: ["node_modules", "dist"]
|
|
3555
5158
|
};
|
|
3556
|
-
|
|
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 = [
|
|
3557
5176
|
{
|
|
3558
5177
|
path: "package.json",
|
|
3559
5178
|
content: `${JSON.stringify(rootPackageJson, null, 2)}\n`
|
|
@@ -3574,11 +5193,100 @@ coverage/
|
|
|
3574
5193
|
path: "turbo.json",
|
|
3575
5194
|
content: `${JSON.stringify(turboConfig, null, 2)}\n`
|
|
3576
5195
|
},
|
|
5196
|
+
{
|
|
5197
|
+
path: "vitest.config.ts",
|
|
5198
|
+
content: vitestConfig
|
|
5199
|
+
},
|
|
3577
5200
|
{
|
|
3578
5201
|
path: ".gitignore",
|
|
3579
5202
|
content: gitignore
|
|
3580
5203
|
}
|
|
3581
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;
|
|
3582
5290
|
}
|
|
3583
5291
|
|
|
3584
5292
|
//#endregion
|
|
@@ -3587,18 +5295,18 @@ const apiTemplate = {
|
|
|
3587
5295
|
name: "api",
|
|
3588
5296
|
description: "Full API with auth, database, services",
|
|
3589
5297
|
dependencies: {
|
|
3590
|
-
"@geekmidas/constructs": "
|
|
3591
|
-
"@geekmidas/envkit": "
|
|
3592
|
-
"@geekmidas/logger": "
|
|
3593
|
-
"@geekmidas/services": "
|
|
3594
|
-
"@geekmidas/errors": "
|
|
3595
|
-
"@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"],
|
|
3596
5304
|
hono: "~4.8.2",
|
|
3597
5305
|
pino: "~9.6.0"
|
|
3598
5306
|
},
|
|
3599
5307
|
devDependencies: {
|
|
3600
|
-
"@biomejs/biome": "~
|
|
3601
|
-
"@geekmidas/cli": "
|
|
5308
|
+
"@biomejs/biome": "~2.3.0",
|
|
5309
|
+
"@geekmidas/cli": GEEKMIDAS_VERSIONS["@geekmidas/cli"],
|
|
3602
5310
|
"@types/node": "~22.0.0",
|
|
3603
5311
|
tsx: "~4.20.0",
|
|
3604
5312
|
turbo: "~2.3.0",
|
|
@@ -3635,18 +5343,17 @@ export const logger = createLogger();
|
|
|
3635
5343
|
const files = [
|
|
3636
5344
|
{
|
|
3637
5345
|
path: "src/config/env.ts",
|
|
3638
|
-
content: `import {
|
|
5346
|
+
content: `import { Credentials } from '@geekmidas/envkit/credentials';
|
|
5347
|
+
import { EnvironmentParser } from '@geekmidas/envkit';
|
|
3639
5348
|
|
|
3640
|
-
export const envParser = new EnvironmentParser(process.env);
|
|
5349
|
+
export const envParser = new EnvironmentParser({ ...process.env, ...Credentials });
|
|
3641
5350
|
|
|
5351
|
+
// Global config - only minimal shared values
|
|
5352
|
+
// Service-specific config should be parsed in each service
|
|
3642
5353
|
export const config = envParser
|
|
3643
5354
|
.create((get) => ({
|
|
3644
|
-
|
|
3645
|
-
|
|
3646
|
-
jwtSecret: get('JWT_SECRET').string().default('change-me-in-production'),${options.database ? `
|
|
3647
|
-
database: {
|
|
3648
|
-
url: get('DATABASE_URL').string().default('postgresql://localhost:5432/mydb'),
|
|
3649
|
-
},` : ""}
|
|
5355
|
+
nodeEnv: get('NODE_ENV').enum(['development', 'test', 'production']).default('development'),
|
|
5356
|
+
stage: get('STAGE').enum(['development', 'staging', 'production']).default('development'),
|
|
3650
5357
|
}))
|
|
3651
5358
|
.parse();
|
|
3652
5359
|
`
|
|
@@ -3659,7 +5366,7 @@ export const config = envParser
|
|
|
3659
5366
|
path: getRoutePath("health.ts"),
|
|
3660
5367
|
content: `import { e } from '@geekmidas/constructs/endpoints';
|
|
3661
5368
|
|
|
3662
|
-
export
|
|
5369
|
+
export const healthEndpoint = e
|
|
3663
5370
|
.get('/health')
|
|
3664
5371
|
.handle(async () => ({
|
|
3665
5372
|
status: 'ok',
|
|
@@ -3671,7 +5378,7 @@ export default e
|
|
|
3671
5378
|
path: getRoutePath("users/list.ts"),
|
|
3672
5379
|
content: `import { e } from '@geekmidas/constructs/endpoints';
|
|
3673
5380
|
|
|
3674
|
-
export
|
|
5381
|
+
export const listUsersEndpoint = e
|
|
3675
5382
|
.get('/users')
|
|
3676
5383
|
.handle(async () => ({
|
|
3677
5384
|
users: [
|
|
@@ -3686,7 +5393,7 @@ export default e
|
|
|
3686
5393
|
content: `import { e } from '@geekmidas/constructs/endpoints';
|
|
3687
5394
|
import { z } from 'zod';
|
|
3688
5395
|
|
|
3689
|
-
export
|
|
5396
|
+
export const getUserEndpoint = e
|
|
3690
5397
|
.get('/users/:id')
|
|
3691
5398
|
.params(z.object({ id: z.string() }))
|
|
3692
5399
|
.handle(async ({ params }) => ({
|
|
@@ -3699,7 +5406,7 @@ export default e
|
|
|
3699
5406
|
];
|
|
3700
5407
|
if (options.database) files.push({
|
|
3701
5408
|
path: "src/services/database.ts",
|
|
3702
|
-
content: `import type { Service } from '@geekmidas/services';
|
|
5409
|
+
content: `import type { Service, ServiceRegisterOptions } from '@geekmidas/services';
|
|
3703
5410
|
import { Kysely, PostgresDialect } from 'kysely';
|
|
3704
5411
|
import pg from 'pg';
|
|
3705
5412
|
|
|
@@ -3715,18 +5422,24 @@ export interface Database {
|
|
|
3715
5422
|
|
|
3716
5423
|
export const databaseService = {
|
|
3717
5424
|
serviceName: 'database' as const,
|
|
3718
|
-
async register(envParser) {
|
|
5425
|
+
async register({ envParser, context }: ServiceRegisterOptions) {
|
|
5426
|
+
const logger = context.getLogger();
|
|
5427
|
+
logger.info('Connecting to database');
|
|
5428
|
+
|
|
3719
5429
|
const config = envParser
|
|
3720
5430
|
.create((get) => ({
|
|
3721
5431
|
url: get('DATABASE_URL').string(),
|
|
3722
5432
|
}))
|
|
3723
5433
|
.parse();
|
|
3724
5434
|
|
|
3725
|
-
|
|
5435
|
+
const db = new Kysely<Database>({
|
|
3726
5436
|
dialect: new PostgresDialect({
|
|
3727
5437
|
pool: new pg.Pool({ connectionString: config.url }),
|
|
3728
5438
|
}),
|
|
3729
5439
|
});
|
|
5440
|
+
|
|
5441
|
+
logger.info('Database connection established');
|
|
5442
|
+
return db;
|
|
3730
5443
|
},
|
|
3731
5444
|
} satisfies Service<'database', Kysely<Database>>;
|
|
3732
5445
|
`
|
|
@@ -3747,13 +5460,20 @@ export const telescope = new Telescope({
|
|
|
3747
5460
|
content: `import { Direction, InMemoryMonitoringStorage, Studio } from '@geekmidas/studio';
|
|
3748
5461
|
import { Kysely, PostgresDialect } from 'kysely';
|
|
3749
5462
|
import pg from 'pg';
|
|
3750
|
-
import type { Database } from '../services/database';
|
|
3751
|
-
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();
|
|
3752
5472
|
|
|
3753
5473
|
// Create a Kysely instance for Studio
|
|
3754
5474
|
const db = new Kysely<Database>({
|
|
3755
5475
|
dialect: new PostgresDialect({
|
|
3756
|
-
pool: new pg.Pool({ connectionString:
|
|
5476
|
+
pool: new pg.Pool({ connectionString: studioConfig.databaseUrl }),
|
|
3757
5477
|
}),
|
|
3758
5478
|
});
|
|
3759
5479
|
|
|
@@ -3779,15 +5499,15 @@ const minimalTemplate = {
|
|
|
3779
5499
|
name: "minimal",
|
|
3780
5500
|
description: "Basic health endpoint",
|
|
3781
5501
|
dependencies: {
|
|
3782
|
-
"@geekmidas/constructs": "
|
|
3783
|
-
"@geekmidas/envkit": "
|
|
3784
|
-
"@geekmidas/logger": "
|
|
5502
|
+
"@geekmidas/constructs": GEEKMIDAS_VERSIONS["@geekmidas/constructs"],
|
|
5503
|
+
"@geekmidas/envkit": GEEKMIDAS_VERSIONS["@geekmidas/envkit"],
|
|
5504
|
+
"@geekmidas/logger": GEEKMIDAS_VERSIONS["@geekmidas/logger"],
|
|
3785
5505
|
hono: "~4.8.2",
|
|
3786
5506
|
pino: "~9.6.0"
|
|
3787
5507
|
},
|
|
3788
5508
|
devDependencies: {
|
|
3789
|
-
"@biomejs/biome": "~
|
|
3790
|
-
"@geekmidas/cli": "
|
|
5509
|
+
"@biomejs/biome": "~2.3.0",
|
|
5510
|
+
"@geekmidas/cli": GEEKMIDAS_VERSIONS["@geekmidas/cli"],
|
|
3791
5511
|
"@types/node": "~22.0.0",
|
|
3792
5512
|
tsx: "~4.20.0",
|
|
3793
5513
|
turbo: "~2.3.0",
|
|
@@ -3820,14 +5540,17 @@ export const logger = createLogger();
|
|
|
3820
5540
|
const files = [
|
|
3821
5541
|
{
|
|
3822
5542
|
path: "src/config/env.ts",
|
|
3823
|
-
content: `import {
|
|
5543
|
+
content: `import { Credentials } from '@geekmidas/envkit/credentials';
|
|
5544
|
+
import { EnvironmentParser } from '@geekmidas/envkit';
|
|
3824
5545
|
|
|
3825
|
-
export const envParser = new EnvironmentParser(process.env);
|
|
5546
|
+
export const envParser = new EnvironmentParser({ ...process.env, ...Credentials });
|
|
3826
5547
|
|
|
5548
|
+
// Global config - only minimal shared values
|
|
5549
|
+
// Service-specific config should be parsed in each service
|
|
3827
5550
|
export const config = envParser
|
|
3828
5551
|
.create((get) => ({
|
|
3829
|
-
|
|
3830
|
-
|
|
5552
|
+
nodeEnv: get('NODE_ENV').enum(['development', 'test', 'production']).default('development'),
|
|
5553
|
+
stage: get('STAGE').enum(['development', 'staging', 'production']).default('development'),
|
|
3831
5554
|
}))
|
|
3832
5555
|
.parse();
|
|
3833
5556
|
`
|
|
@@ -3840,7 +5563,7 @@ export const config = envParser
|
|
|
3840
5563
|
path: getRoutePath("health.ts"),
|
|
3841
5564
|
content: `import { e } from '@geekmidas/constructs/endpoints';
|
|
3842
5565
|
|
|
3843
|
-
export
|
|
5566
|
+
export const healthEndpoint = e
|
|
3844
5567
|
.get('/health')
|
|
3845
5568
|
.handle(async () => ({
|
|
3846
5569
|
status: 'ok',
|
|
@@ -3849,27 +5572,9 @@ export default e
|
|
|
3849
5572
|
`
|
|
3850
5573
|
}
|
|
3851
5574
|
];
|
|
3852
|
-
if (options.database) {
|
|
3853
|
-
|
|
3854
|
-
|
|
3855
|
-
content: `import { EnvironmentParser } from '@geekmidas/envkit';
|
|
3856
|
-
|
|
3857
|
-
export const envParser = new EnvironmentParser(process.env);
|
|
3858
|
-
|
|
3859
|
-
export const config = envParser
|
|
3860
|
-
.create((get) => ({
|
|
3861
|
-
port: get('PORT').string().transform(Number).default(3000),
|
|
3862
|
-
nodeEnv: get('NODE_ENV').string().default('development'),
|
|
3863
|
-
database: {
|
|
3864
|
-
url: get('DATABASE_URL').string().default('postgresql://localhost:5432/mydb'),
|
|
3865
|
-
},
|
|
3866
|
-
}))
|
|
3867
|
-
.parse();
|
|
3868
|
-
`
|
|
3869
|
-
};
|
|
3870
|
-
files.push({
|
|
3871
|
-
path: "src/services/database.ts",
|
|
3872
|
-
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';
|
|
3873
5578
|
import { Kysely, PostgresDialect } from 'kysely';
|
|
3874
5579
|
import pg from 'pg';
|
|
3875
5580
|
|
|
@@ -3880,23 +5585,28 @@ export interface Database {
|
|
|
3880
5585
|
|
|
3881
5586
|
export const databaseService = {
|
|
3882
5587
|
serviceName: 'database' as const,
|
|
3883
|
-
async register(envParser) {
|
|
5588
|
+
async register({ envParser, context }: ServiceRegisterOptions) {
|
|
5589
|
+
const logger = context.getLogger();
|
|
5590
|
+
logger.info('Connecting to database');
|
|
5591
|
+
|
|
3884
5592
|
const config = envParser
|
|
3885
5593
|
.create((get) => ({
|
|
3886
5594
|
url: get('DATABASE_URL').string(),
|
|
3887
5595
|
}))
|
|
3888
5596
|
.parse();
|
|
3889
5597
|
|
|
3890
|
-
|
|
5598
|
+
const db = new Kysely<Database>({
|
|
3891
5599
|
dialect: new PostgresDialect({
|
|
3892
5600
|
pool: new pg.Pool({ connectionString: config.url }),
|
|
3893
5601
|
}),
|
|
3894
5602
|
});
|
|
5603
|
+
|
|
5604
|
+
logger.info('Database connection established');
|
|
5605
|
+
return db;
|
|
3895
5606
|
},
|
|
3896
5607
|
} satisfies Service<'database', Kysely<Database>>;
|
|
3897
5608
|
`
|
|
3898
|
-
|
|
3899
|
-
}
|
|
5609
|
+
});
|
|
3900
5610
|
if (options.telescope) files.push({
|
|
3901
5611
|
path: "src/config/telescope.ts",
|
|
3902
5612
|
content: `import { Telescope } from '@geekmidas/telescope';
|
|
@@ -3913,13 +5623,20 @@ export const telescope = new Telescope({
|
|
|
3913
5623
|
content: `import { Direction, InMemoryMonitoringStorage, Studio } from '@geekmidas/studio';
|
|
3914
5624
|
import { Kysely, PostgresDialect } from 'kysely';
|
|
3915
5625
|
import pg from 'pg';
|
|
3916
|
-
import type { Database } from '../services/database';
|
|
3917
|
-
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();
|
|
3918
5635
|
|
|
3919
5636
|
// Create a Kysely instance for Studio
|
|
3920
5637
|
const db = new Kysely<Database>({
|
|
3921
5638
|
dialect: new PostgresDialect({
|
|
3922
|
-
pool: new pg.Pool({ connectionString:
|
|
5639
|
+
pool: new pg.Pool({ connectionString: studioConfig.databaseUrl }),
|
|
3923
5640
|
}),
|
|
3924
5641
|
});
|
|
3925
5642
|
|
|
@@ -3945,16 +5662,16 @@ const serverlessTemplate = {
|
|
|
3945
5662
|
name: "serverless",
|
|
3946
5663
|
description: "AWS Lambda handlers",
|
|
3947
5664
|
dependencies: {
|
|
3948
|
-
"@geekmidas/constructs": "
|
|
3949
|
-
"@geekmidas/envkit": "
|
|
3950
|
-
"@geekmidas/logger": "
|
|
3951
|
-
"@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"],
|
|
3952
5669
|
hono: "~4.8.2",
|
|
3953
5670
|
pino: "~9.6.0"
|
|
3954
5671
|
},
|
|
3955
5672
|
devDependencies: {
|
|
3956
|
-
"@biomejs/biome": "~
|
|
3957
|
-
"@geekmidas/cli": "
|
|
5673
|
+
"@biomejs/biome": "~2.3.0",
|
|
5674
|
+
"@geekmidas/cli": GEEKMIDAS_VERSIONS["@geekmidas/cli"],
|
|
3958
5675
|
"@types/aws-lambda": "~8.10.92",
|
|
3959
5676
|
"@types/node": "~22.0.0",
|
|
3960
5677
|
tsx: "~4.20.0",
|
|
@@ -3988,17 +5705,17 @@ export const logger = createLogger();
|
|
|
3988
5705
|
const files = [
|
|
3989
5706
|
{
|
|
3990
5707
|
path: "src/config/env.ts",
|
|
3991
|
-
content: `import {
|
|
5708
|
+
content: `import { Credentials } from '@geekmidas/envkit/credentials';
|
|
5709
|
+
import { EnvironmentParser } from '@geekmidas/envkit';
|
|
3992
5710
|
|
|
3993
|
-
export const envParser = new EnvironmentParser(process.env);
|
|
5711
|
+
export const envParser = new EnvironmentParser({ ...process.env, ...Credentials });
|
|
3994
5712
|
|
|
5713
|
+
// Global config - only minimal shared values
|
|
5714
|
+
// Service-specific config should be parsed in each service
|
|
3995
5715
|
export const config = envParser
|
|
3996
5716
|
.create((get) => ({
|
|
3997
|
-
|
|
3998
|
-
|
|
3999
|
-
database: {
|
|
4000
|
-
url: get('DATABASE_URL').string(),
|
|
4001
|
-
},` : ""}
|
|
5717
|
+
nodeEnv: get('NODE_ENV').enum(['development', 'test', 'production']).default('development'),
|
|
5718
|
+
stage: get('STAGE').enum(['dev', 'staging', 'prod']).default('dev'),
|
|
4002
5719
|
}))
|
|
4003
5720
|
.parse();
|
|
4004
5721
|
`
|
|
@@ -4011,7 +5728,7 @@ export const config = envParser
|
|
|
4011
5728
|
path: getRoutePath("health.ts"),
|
|
4012
5729
|
content: `import { e } from '@geekmidas/constructs/endpoints';
|
|
4013
5730
|
|
|
4014
|
-
export
|
|
5731
|
+
export const healthEndpoint = e
|
|
4015
5732
|
.get('/health')
|
|
4016
5733
|
.handle(async () => ({
|
|
4017
5734
|
status: 'ok',
|
|
@@ -4025,7 +5742,7 @@ export default e
|
|
|
4025
5742
|
content: `import { f } from '@geekmidas/constructs/functions';
|
|
4026
5743
|
import { z } from 'zod';
|
|
4027
5744
|
|
|
4028
|
-
export
|
|
5745
|
+
export const helloFunction = f
|
|
4029
5746
|
.input(z.object({ name: z.string() }))
|
|
4030
5747
|
.output(z.object({ message: z.string() }))
|
|
4031
5748
|
.handle(async ({ input }) => ({
|
|
@@ -4056,16 +5773,16 @@ const workerTemplate = {
|
|
|
4056
5773
|
name: "worker",
|
|
4057
5774
|
description: "Background job processing",
|
|
4058
5775
|
dependencies: {
|
|
4059
|
-
"@geekmidas/constructs": "
|
|
4060
|
-
"@geekmidas/envkit": "
|
|
4061
|
-
"@geekmidas/logger": "
|
|
4062
|
-
"@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"],
|
|
4063
5780
|
hono: "~4.8.2",
|
|
4064
5781
|
pino: "~9.6.0"
|
|
4065
5782
|
},
|
|
4066
5783
|
devDependencies: {
|
|
4067
|
-
"@biomejs/biome": "~
|
|
4068
|
-
"@geekmidas/cli": "
|
|
5784
|
+
"@biomejs/biome": "~2.3.0",
|
|
5785
|
+
"@geekmidas/cli": GEEKMIDAS_VERSIONS["@geekmidas/cli"],
|
|
4069
5786
|
"@types/node": "~22.0.0",
|
|
4070
5787
|
tsx: "~4.20.0",
|
|
4071
5788
|
turbo: "~2.3.0",
|
|
@@ -4098,20 +5815,17 @@ export const logger = createLogger();
|
|
|
4098
5815
|
const files = [
|
|
4099
5816
|
{
|
|
4100
5817
|
path: "src/config/env.ts",
|
|
4101
|
-
content: `import {
|
|
5818
|
+
content: `import { Credentials } from '@geekmidas/envkit/credentials';
|
|
5819
|
+
import { EnvironmentParser } from '@geekmidas/envkit';
|
|
4102
5820
|
|
|
4103
|
-
export const envParser = new EnvironmentParser(process.env);
|
|
5821
|
+
export const envParser = new EnvironmentParser({ ...process.env, ...Credentials });
|
|
4104
5822
|
|
|
5823
|
+
// Global config - only minimal shared values
|
|
5824
|
+
// Service-specific config should be parsed in each service
|
|
4105
5825
|
export const config = envParser
|
|
4106
5826
|
.create((get) => ({
|
|
4107
|
-
|
|
4108
|
-
|
|
4109
|
-
rabbitmq: {
|
|
4110
|
-
url: get('RABBITMQ_URL').string().default('amqp://localhost:5672'),
|
|
4111
|
-
},${options.database ? `
|
|
4112
|
-
database: {
|
|
4113
|
-
url: get('DATABASE_URL').string().default('postgresql://localhost:5432/mydb'),
|
|
4114
|
-
},` : ""}
|
|
5827
|
+
nodeEnv: get('NODE_ENV').enum(['development', 'test', 'production']).default('development'),
|
|
5828
|
+
stage: get('STAGE').enum(['development', 'staging', 'production']).default('development'),
|
|
4115
5829
|
}))
|
|
4116
5830
|
.parse();
|
|
4117
5831
|
`
|
|
@@ -4124,7 +5838,7 @@ export const config = envParser
|
|
|
4124
5838
|
path: getRoutePath("health.ts"),
|
|
4125
5839
|
content: `import { e } from '@geekmidas/constructs/endpoints';
|
|
4126
5840
|
|
|
4127
|
-
export
|
|
5841
|
+
export const healthEndpoint = e
|
|
4128
5842
|
.get('/health')
|
|
4129
5843
|
.handle(async () => ({
|
|
4130
5844
|
status: 'ok',
|
|
@@ -4141,15 +5855,44 @@ export type AppEvents =
|
|
|
4141
5855
|
| PublishableMessage<'user.created', { userId: string; email: string }>
|
|
4142
5856
|
| PublishableMessage<'user.updated', { userId: string; changes: Record<string, unknown> }>
|
|
4143
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>>;
|
|
4144
5886
|
`
|
|
4145
5887
|
},
|
|
4146
5888
|
{
|
|
4147
5889
|
path: "src/subscribers/user-events.ts",
|
|
4148
5890
|
content: `import { s } from '@geekmidas/constructs/subscribers';
|
|
4149
|
-
import
|
|
5891
|
+
import { eventsPublisherService } from '../events/publisher.js';
|
|
4150
5892
|
|
|
4151
|
-
export
|
|
4152
|
-
.
|
|
5893
|
+
export const userEventsSubscriber = s
|
|
5894
|
+
.publisher(eventsPublisherService)
|
|
5895
|
+
.subscribe(['user.created', 'user.updated'])
|
|
4153
5896
|
.handle(async ({ event, logger }) => {
|
|
4154
5897
|
logger.info({ type: event.type, payload: event.payload }, 'Processing user event');
|
|
4155
5898
|
|
|
@@ -4171,7 +5914,7 @@ export default s<AppEvents>()
|
|
|
4171
5914
|
content: `import { cron } from '@geekmidas/constructs/crons';
|
|
4172
5915
|
|
|
4173
5916
|
// Run every day at midnight
|
|
4174
|
-
export
|
|
5917
|
+
export const cleanupCron = cron('0 0 * * *')
|
|
4175
5918
|
.handle(async ({ logger }) => {
|
|
4176
5919
|
logger.info('Running cleanup job');
|
|
4177
5920
|
|
|
@@ -4214,30 +5957,17 @@ const templates = {
|
|
|
4214
5957
|
worker: workerTemplate
|
|
4215
5958
|
};
|
|
4216
5959
|
/**
|
|
4217
|
-
* Template choices for prompts
|
|
5960
|
+
* Template choices for prompts (Story 1.11 simplified to api + fullstack)
|
|
4218
5961
|
*/
|
|
4219
|
-
const templateChoices = [
|
|
4220
|
-
|
|
4221
|
-
|
|
4222
|
-
|
|
4223
|
-
|
|
4224
|
-
|
|
4225
|
-
|
|
4226
|
-
|
|
4227
|
-
|
|
4228
|
-
description: "Full API with auth, database, services"
|
|
4229
|
-
},
|
|
4230
|
-
{
|
|
4231
|
-
title: "Serverless",
|
|
4232
|
-
value: "serverless",
|
|
4233
|
-
description: "AWS Lambda handlers"
|
|
4234
|
-
},
|
|
4235
|
-
{
|
|
4236
|
-
title: "Worker",
|
|
4237
|
-
value: "worker",
|
|
4238
|
-
description: "Background job processing"
|
|
4239
|
-
}
|
|
4240
|
-
];
|
|
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
|
+
}];
|
|
4241
5971
|
/**
|
|
4242
5972
|
* Logger type choices for prompts
|
|
4243
5973
|
*/
|
|
@@ -4271,13 +6001,77 @@ const routesStructureChoices = [
|
|
|
4271
6001
|
}
|
|
4272
6002
|
];
|
|
4273
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
|
+
/**
|
|
4274
6061
|
* Get a template by name
|
|
4275
6062
|
*/
|
|
4276
6063
|
function getTemplate(name$1) {
|
|
6064
|
+
if (name$1 === "fullstack") return templates.api;
|
|
4277
6065
|
const template = templates[name$1];
|
|
4278
6066
|
if (!template) throw new Error(`Unknown template: ${name$1}`);
|
|
4279
6067
|
return template;
|
|
4280
6068
|
}
|
|
6069
|
+
/**
|
|
6070
|
+
* Check if a template is the fullstack monorepo template
|
|
6071
|
+
*/
|
|
6072
|
+
function isFullstackTemplate(name$1) {
|
|
6073
|
+
return name$1 === "fullstack";
|
|
6074
|
+
}
|
|
4281
6075
|
|
|
4282
6076
|
//#endregion
|
|
4283
6077
|
//#region src/init/generators/package.ts
|
|
@@ -4289,10 +6083,10 @@ function generatePackageJson(options, template) {
|
|
|
4289
6083
|
const dependencies$1 = { ...template.dependencies };
|
|
4290
6084
|
const devDependencies$1 = { ...template.devDependencies };
|
|
4291
6085
|
const scripts$1 = { ...template.scripts };
|
|
4292
|
-
if (telescope) dependencies$1["@geekmidas/telescope"] = "
|
|
4293
|
-
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"];
|
|
4294
6088
|
if (database) {
|
|
4295
|
-
dependencies$1["@geekmidas/db"] = "
|
|
6089
|
+
dependencies$1["@geekmidas/db"] = GEEKMIDAS_VERSIONS["@geekmidas/db"];
|
|
4296
6090
|
dependencies$1.kysely = "~0.28.2";
|
|
4297
6091
|
dependencies$1.pg = "~8.16.0";
|
|
4298
6092
|
devDependencies$1["@types/pg"] = "~8.15.0";
|
|
@@ -4327,19 +6121,219 @@ function generatePackageJson(options, template) {
|
|
|
4327
6121
|
dependencies: sortObject(dependencies$1),
|
|
4328
6122
|
devDependencies: sortObject(devDependencies$1)
|
|
4329
6123
|
};
|
|
4330
|
-
return [{
|
|
4331
|
-
path: "package.json",
|
|
4332
|
-
content: `${JSON.stringify(packageJson, null, 2)}\n`
|
|
4333
|
-
}];
|
|
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
|
+
);
|
|
4334
6234
|
}
|
|
6235
|
+
`;
|
|
6236
|
+
const pageTsx = `import type { User } from '${modelsPackage}';
|
|
4335
6237
|
|
|
4336
|
-
|
|
4337
|
-
|
|
4338
|
-
|
|
4339
|
-
|
|
4340
|
-
|
|
4341
|
-
|
|
4342
|
-
|
|
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
|
+
];
|
|
4343
6337
|
}
|
|
4344
6338
|
|
|
4345
6339
|
//#endregion
|
|
@@ -4407,21 +6401,36 @@ function getRunCommand(pkgManager, script) {
|
|
|
4407
6401
|
//#endregion
|
|
4408
6402
|
//#region src/init/index.ts
|
|
4409
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
|
+
/**
|
|
4410
6419
|
* Main init command - scaffolds a new project
|
|
4411
6420
|
*/
|
|
4412
6421
|
async function initCommand(projectName, options = {}) {
|
|
4413
6422
|
const cwd = process.cwd();
|
|
4414
|
-
const
|
|
6423
|
+
const detectedPkgManager = detectPackageManager(cwd);
|
|
4415
6424
|
prompts.default.override({});
|
|
4416
6425
|
const onCancel = () => {
|
|
4417
6426
|
process.exit(0);
|
|
4418
6427
|
};
|
|
4419
6428
|
const answers = await (0, prompts.default)([
|
|
4420
6429
|
{
|
|
4421
|
-
type: projectName ? null : "text",
|
|
6430
|
+
type: projectName || options.name ? null : "text",
|
|
4422
6431
|
name: "name",
|
|
4423
6432
|
message: "Project name:",
|
|
4424
|
-
initial: "my-
|
|
6433
|
+
initial: "my-app",
|
|
4425
6434
|
validate: (value) => {
|
|
4426
6435
|
const nameValid = validateProjectName(value);
|
|
4427
6436
|
if (nameValid !== true) return nameValid;
|
|
@@ -4438,21 +6447,33 @@ async function initCommand(projectName, options = {}) {
|
|
|
4438
6447
|
initial: 0
|
|
4439
6448
|
},
|
|
4440
6449
|
{
|
|
4441
|
-
type: options.yes ? null : "
|
|
4442
|
-
name: "
|
|
4443
|
-
message: "
|
|
4444
|
-
|
|
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"
|
|
4445
6458
|
},
|
|
4446
6459
|
{
|
|
4447
|
-
type: options.yes ? null : "
|
|
4448
|
-
name: "
|
|
4449
|
-
message: "
|
|
4450
|
-
|
|
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
|
|
4451
6472
|
},
|
|
4452
6473
|
{
|
|
4453
|
-
type:
|
|
4454
|
-
name: "
|
|
4455
|
-
message: "Include
|
|
6474
|
+
type: options.yes ? null : "confirm",
|
|
6475
|
+
name: "telescope",
|
|
6476
|
+
message: "Include Telescope (debugging dashboard)?",
|
|
4456
6477
|
initial: true
|
|
4457
6478
|
},
|
|
4458
6479
|
{
|
|
@@ -4468,74 +6489,146 @@ async function initCommand(projectName, options = {}) {
|
|
|
4468
6489
|
message: "Routes structure:",
|
|
4469
6490
|
choices: routesStructureChoices,
|
|
4470
6491
|
initial: 0
|
|
4471
|
-
},
|
|
4472
|
-
{
|
|
4473
|
-
type: options.yes || options.monorepo !== void 0 ? null : "confirm",
|
|
4474
|
-
name: "monorepo",
|
|
4475
|
-
message: "Setup as monorepo?",
|
|
4476
|
-
initial: false
|
|
4477
|
-
},
|
|
4478
|
-
{
|
|
4479
|
-
type: (prev) => (prev === true || options.monorepo) && !options.apiPath ? "text" : null,
|
|
4480
|
-
name: "apiPath",
|
|
4481
|
-
message: "API app path:",
|
|
4482
|
-
initial: "apps/api"
|
|
4483
6492
|
}
|
|
4484
6493
|
], { onCancel });
|
|
4485
|
-
const name$1 = projectName || answers.name;
|
|
4486
|
-
if (!name$1)
|
|
4487
|
-
|
|
4488
|
-
|
|
4489
|
-
|
|
4490
|
-
|
|
4491
|
-
|
|
4492
|
-
|
|
4493
|
-
|
|
4494
|
-
|
|
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;
|
|
4495
6528
|
const templateOptions = {
|
|
4496
6529
|
name: name$1,
|
|
4497
|
-
template
|
|
6530
|
+
template,
|
|
4498
6531
|
telescope: options.yes ? true : answers.telescope ?? true,
|
|
4499
6532
|
database,
|
|
4500
|
-
studio: database
|
|
6533
|
+
studio: database,
|
|
4501
6534
|
loggerType: options.yes ? "pino" : answers.loggerType ?? "pino",
|
|
4502
6535
|
routesStructure: options.yes ? "centralized-endpoints" : answers.routesStructure ?? "centralized-endpoints",
|
|
4503
6536
|
monorepo,
|
|
4504
|
-
apiPath: monorepo ? options.apiPath ??
|
|
6537
|
+
apiPath: monorepo ? options.apiPath ?? "apps/api" : "",
|
|
6538
|
+
packageManager: pkgManager,
|
|
6539
|
+
deployTarget,
|
|
6540
|
+
services
|
|
4505
6541
|
};
|
|
4506
6542
|
const targetDir = (0, node_path.join)(cwd, name$1);
|
|
4507
|
-
const
|
|
6543
|
+
const baseTemplate = getTemplate(templateOptions.template);
|
|
4508
6544
|
const isMonorepo$1 = templateOptions.monorepo;
|
|
4509
6545
|
const apiPath = templateOptions.apiPath;
|
|
6546
|
+
console.log("\n🚀 Creating your project...\n");
|
|
4510
6547
|
await (0, node_fs_promises.mkdir)(targetDir, { recursive: true });
|
|
4511
6548
|
const appDir = isMonorepo$1 ? (0, node_path.join)(targetDir, apiPath) : targetDir;
|
|
4512
6549
|
if (isMonorepo$1) await (0, node_fs_promises.mkdir)(appDir, { recursive: true });
|
|
4513
|
-
const
|
|
4514
|
-
|
|
4515
|
-
|
|
4516
|
-
|
|
4517
|
-
|
|
4518
|
-
|
|
4519
|
-
|
|
4520
|
-
|
|
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) : [];
|
|
4521
6569
|
for (const { path, content } of rootFiles) {
|
|
4522
6570
|
const fullPath = (0, node_path.join)(targetDir, path);
|
|
4523
6571
|
await (0, node_fs_promises.mkdir)((0, node_path.dirname)(fullPath), { recursive: true });
|
|
4524
6572
|
await (0, node_fs_promises.writeFile)(fullPath, content);
|
|
4525
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
|
+
}
|
|
4526
6579
|
for (const { path, content } of appFiles) {
|
|
4527
6580
|
const fullPath = (0, node_path.join)(appDir, path);
|
|
4528
|
-
const _displayPath = isMonorepo$1 ? `${apiPath}/${path}` : path;
|
|
4529
6581
|
await (0, node_fs_promises.mkdir)((0, node_path.dirname)(fullPath), { recursive: true });
|
|
4530
6582
|
await (0, node_fs_promises.writeFile)(fullPath, content);
|
|
4531
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`);
|
|
4532
6622
|
if (!options.skipInstall) {
|
|
6623
|
+
console.log("\n📦 Installing dependencies...\n");
|
|
4533
6624
|
try {
|
|
4534
6625
|
(0, node_child_process.execSync)(getInstallCommand(pkgManager), {
|
|
4535
6626
|
cwd: targetDir,
|
|
4536
6627
|
stdio: "inherit"
|
|
4537
6628
|
});
|
|
4538
|
-
} catch {
|
|
6629
|
+
} catch {
|
|
6630
|
+
console.error("Failed to install dependencies");
|
|
6631
|
+
}
|
|
4539
6632
|
try {
|
|
4540
6633
|
(0, node_child_process.execSync)("npx @biomejs/biome format --write --unsafe .", {
|
|
4541
6634
|
cwd: targetDir,
|
|
@@ -4543,124 +6636,52 @@ async function initCommand(projectName, options = {}) {
|
|
|
4543
6636
|
});
|
|
4544
6637
|
} catch {}
|
|
4545
6638
|
}
|
|
4546
|
-
|
|
6639
|
+
printNextSteps(name$1, templateOptions, pkgManager);
|
|
4547
6640
|
}
|
|
4548
|
-
|
|
4549
|
-
//#endregion
|
|
4550
|
-
//#region src/secrets/generator.ts
|
|
4551
6641
|
/**
|
|
4552
|
-
*
|
|
4553
|
-
* @param length Password length (default: 32)
|
|
6642
|
+
* Print success message with next steps
|
|
4554
6643
|
*/
|
|
4555
|
-
function
|
|
4556
|
-
|
|
4557
|
-
}
|
|
4558
|
-
|
|
4559
|
-
|
|
4560
|
-
|
|
4561
|
-
|
|
4562
|
-
|
|
4563
|
-
|
|
4564
|
-
|
|
4565
|
-
},
|
|
4566
|
-
redis: {
|
|
4567
|
-
host: "redis",
|
|
4568
|
-
port: 6379,
|
|
4569
|
-
username: "default"
|
|
4570
|
-
},
|
|
4571
|
-
rabbitmq: {
|
|
4572
|
-
host: "rabbitmq",
|
|
4573
|
-
port: 5672,
|
|
4574
|
-
username: "app",
|
|
4575
|
-
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`);
|
|
4576
6654
|
}
|
|
4577
|
-
};
|
|
4578
|
-
|
|
4579
|
-
|
|
4580
|
-
|
|
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
|
-
const { password, host, port } = creds;
|
|
4608
|
-
return `redis://:${encodeURIComponent(password)}@${host}:${port}`;
|
|
4609
|
-
}
|
|
4610
|
-
/**
|
|
4611
|
-
* Generate connection URL for RabbitMQ.
|
|
4612
|
-
*/
|
|
4613
|
-
function generateRabbitmqUrl(creds) {
|
|
4614
|
-
const { username, password, host, port, vhost } = creds;
|
|
4615
|
-
const encodedVhost = encodeURIComponent(vhost ?? "/");
|
|
4616
|
-
return `amqp://${username}:${encodeURIComponent(password)}@${host}:${port}/${encodedVhost}`;
|
|
4617
|
-
}
|
|
4618
|
-
/**
|
|
4619
|
-
* Generate connection URLs from service credentials.
|
|
4620
|
-
*/
|
|
4621
|
-
function generateConnectionUrls(services) {
|
|
4622
|
-
const urls = {};
|
|
4623
|
-
if (services.postgres) urls.DATABASE_URL = generatePostgresUrl(services.postgres);
|
|
4624
|
-
if (services.redis) urls.REDIS_URL = generateRedisUrl(services.redis);
|
|
4625
|
-
if (services.rabbitmq) urls.RABBITMQ_URL = generateRabbitmqUrl(services.rabbitmq);
|
|
4626
|
-
return urls;
|
|
4627
|
-
}
|
|
4628
|
-
/**
|
|
4629
|
-
* Create a new StageSecrets object with generated credentials.
|
|
4630
|
-
*/
|
|
4631
|
-
function createStageSecrets(stage, services) {
|
|
4632
|
-
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
4633
|
-
const serviceCredentials = generateServicesCredentials(services);
|
|
4634
|
-
const urls = generateConnectionUrls(serviceCredentials);
|
|
4635
|
-
return {
|
|
4636
|
-
stage,
|
|
4637
|
-
createdAt: now,
|
|
4638
|
-
updatedAt: now,
|
|
4639
|
-
services: serviceCredentials,
|
|
4640
|
-
urls,
|
|
4641
|
-
custom: {}
|
|
4642
|
-
};
|
|
4643
|
-
}
|
|
4644
|
-
/**
|
|
4645
|
-
* Rotate password for a specific service.
|
|
4646
|
-
*/
|
|
4647
|
-
function rotateServicePassword(secrets, service) {
|
|
4648
|
-
const currentCreds = secrets.services[service];
|
|
4649
|
-
if (!currentCreds) throw new Error(`Service "${service}" not configured in secrets`);
|
|
4650
|
-
const newCreds = {
|
|
4651
|
-
...currentCreds,
|
|
4652
|
-
password: generateSecurePassword()
|
|
4653
|
-
};
|
|
4654
|
-
const newServices = {
|
|
4655
|
-
...secrets.services,
|
|
4656
|
-
[service]: newCreds
|
|
4657
|
-
};
|
|
4658
|
-
return {
|
|
4659
|
-
...secrets,
|
|
4660
|
-
updatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
4661
|
-
services: newServices,
|
|
4662
|
-
urls: generateConnectionUrls(newServices)
|
|
4663
|
-
};
|
|
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("");
|
|
4664
6685
|
}
|
|
4665
6686
|
|
|
4666
6687
|
//#endregion
|
|
@@ -4849,11 +6870,57 @@ function maskUrl(url) {
|
|
|
4849
6870
|
}
|
|
4850
6871
|
}
|
|
4851
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
|
+
|
|
4852
6919
|
//#endregion
|
|
4853
6920
|
//#region src/index.ts
|
|
4854
6921
|
const program = new commander.Command();
|
|
4855
6922
|
program.name("gkm").description("GeekMidas backend framework CLI").version(package_default.version).option("--cwd <path>", "Change working directory");
|
|
4856
|
-
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) => {
|
|
4857
6924
|
try {
|
|
4858
6925
|
const globalOptions = program.opts();
|
|
4859
6926
|
if (globalOptions.cwd) process.chdir(globalOptions.cwd);
|
|
@@ -4910,6 +6977,19 @@ program.command("dev").description("Start development server with automatic relo
|
|
|
4910
6977
|
process.exit(1);
|
|
4911
6978
|
}
|
|
4912
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
|
+
});
|
|
4913
6993
|
program.command("cron").description("Manage cron jobs").action(() => {
|
|
4914
6994
|
const globalOptions = program.opts();
|
|
4915
6995
|
if (globalOptions.cwd) process.chdir(globalOptions.cwd);
|