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