@geekmidas/cli 0.18.0 → 0.20.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 +2640 -564
- package/dist/index.cjs.map +1 -1
- package/dist/index.mjs +2635 -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 +219 -4
- package/src/index.ts +24 -0
- package/src/init/__tests__/generators.spec.ts +17 -24
- package/src/init/__tests__/init.spec.ts +157 -5
- package/src/init/generators/auth.ts +220 -0
- package/src/init/generators/config.ts +61 -4
- package/src/init/generators/docker.ts +115 -8
- package/src/init/generators/env.ts +7 -127
- package/src/init/generators/index.ts +1 -0
- package/src/init/generators/models.ts +3 -1
- package/src/init/generators/monorepo.ts +154 -10
- package/src/init/generators/package.ts +5 -3
- package/src/init/generators/web.ts +213 -0
- package/src/init/index.ts +290 -58
- package/src/init/templates/api.ts +38 -29
- package/src/init/templates/index.ts +132 -4
- package/src/init/templates/minimal.ts +33 -35
- package/src/init/templates/serverless.ts +16 -19
- package/src/init/templates/worker.ts +50 -25
- package/src/init/versions.ts +47 -0
- package/src/secrets/keystore.ts +144 -0
- package/src/secrets/storage.ts +109 -6
- package/src/test/index.ts +97 -0
- package/src/workspace/__tests__/client-generator.spec.ts +357 -0
- package/src/workspace/__tests__/index.spec.ts +543 -0
- package/src/workspace/__tests__/schema.spec.ts +519 -0
- package/src/workspace/__tests__/type-inference.spec.ts +251 -0
- package/src/workspace/client-generator.ts +307 -0
- package/src/workspace/index.ts +372 -0
- package/src/workspace/schema.ts +368 -0
- package/src/workspace/types.ts +336 -0
- package/tsconfig.tsbuildinfo +1 -1
- package/tsdown.config.ts +1 -0
- package/dist/config-AmInkU7k.cjs.map +0 -1
- package/dist/config-DYULeEv8.mjs.map +0 -1
- package/dist/dokploy-api-B7KxOQr3.cjs +0 -3
- package/dist/dokploy-api-DHvfmWbi.mjs +0 -3
- package/dist/storage-BaOP55oq.mjs +0 -147
- package/dist/storage-BaOP55oq.mjs.map +0 -1
- package/dist/storage-Bn3K9Ccu.cjs.map +0 -1
package/dist/index.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.20.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
|
|
@@ -1696,8 +2408,14 @@ WORKDIR /app
|
|
|
1696
2408
|
# Copy source (deps already installed)
|
|
1697
2409
|
COPY . .
|
|
1698
2410
|
|
|
1699
|
-
#
|
|
1700
|
-
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
|
|
1701
2419
|
|
|
1702
2420
|
# Stage 3: Production
|
|
1703
2421
|
FROM ${baseImage} AS runner
|
|
@@ -1778,8 +2496,14 @@ WORKDIR /app
|
|
|
1778
2496
|
# Copy pruned source
|
|
1779
2497
|
COPY --from=pruner /app/out/full/ ./
|
|
1780
2498
|
|
|
1781
|
-
#
|
|
1782
|
-
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
|
|
1783
2507
|
|
|
1784
2508
|
# Stage 4: Production
|
|
1785
2509
|
FROM ${baseImage} AS runner
|
|
@@ -1921,8 +2645,8 @@ function resolveDockerConfig$1(config$1) {
|
|
|
1921
2645
|
const docker = config$1.docker ?? {};
|
|
1922
2646
|
let defaultImageName = "api";
|
|
1923
2647
|
try {
|
|
1924
|
-
const pkg = __require(`${process.cwd()}/package.json`);
|
|
1925
|
-
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(/^@[^/]+\//, "");
|
|
1926
2650
|
} catch {}
|
|
1927
2651
|
return {
|
|
1928
2652
|
registry: docker.registry ?? "",
|
|
@@ -1932,20 +2656,194 @@ function resolveDockerConfig$1(config$1) {
|
|
|
1932
2656
|
compose: docker.compose
|
|
1933
2657
|
};
|
|
1934
2658
|
}
|
|
1935
|
-
|
|
1936
|
-
//#endregion
|
|
1937
|
-
//#region src/docker/index.ts
|
|
1938
|
-
const logger$5 = console;
|
|
1939
2659
|
/**
|
|
1940
|
-
*
|
|
1941
|
-
*
|
|
1942
|
-
*
|
|
1943
|
-
* Default: Multi-stage Dockerfile that builds from source inside Docker
|
|
1944
|
-
* --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
|
|
1945
2663
|
*/
|
|
1946
|
-
|
|
1947
|
-
const
|
|
1948
|
-
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);
|
|
1949
2847
|
const serverConfig = typeof config$1.providers?.server === "object" ? config$1.providers.server : void 0;
|
|
1950
2848
|
const healthCheckPath = serverConfig?.production?.healthCheck ?? "/health";
|
|
1951
2849
|
const useSlim = options.slim === true;
|
|
@@ -1966,9 +2864,9 @@ async function dockerCommand(options) {
|
|
|
1966
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");
|
|
1967
2865
|
let turboPackage = options.turboPackage ?? dockerConfig.imageName;
|
|
1968
2866
|
if (useTurbo && !options.turboPackage) try {
|
|
1969
|
-
const pkg = __require(`${process.cwd()}/package.json`);
|
|
1970
|
-
if (pkg.name) {
|
|
1971
|
-
turboPackage = pkg.name;
|
|
2867
|
+
const pkg$1 = __require(`${process.cwd()}/package.json`);
|
|
2868
|
+
if (pkg$1.name) {
|
|
2869
|
+
turboPackage = pkg$1.name;
|
|
1972
2870
|
logger$5.log(` Turbo package: ${turboPackage}`);
|
|
1973
2871
|
}
|
|
1974
2872
|
} catch {}
|
|
@@ -2085,6 +2983,85 @@ async function pushDockerImage(imageName, options) {
|
|
|
2085
2983
|
throw new Error(`Failed to push Docker image: ${error instanceof Error ? error.message : "Unknown error"}`);
|
|
2086
2984
|
}
|
|
2087
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
|
+
}
|
|
2088
3065
|
|
|
2089
3066
|
//#endregion
|
|
2090
3067
|
//#region src/deploy/docker.ts
|
|
@@ -2096,8 +3073,8 @@ function getAppNameFromCwd() {
|
|
|
2096
3073
|
const packageJsonPath = join(process.cwd(), "package.json");
|
|
2097
3074
|
if (!existsSync(packageJsonPath)) return void 0;
|
|
2098
3075
|
try {
|
|
2099
|
-
const pkg = JSON.parse(readFileSync(packageJsonPath, "utf-8"));
|
|
2100
|
-
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(/^@[^/]+\//, "");
|
|
2101
3078
|
} catch {}
|
|
2102
3079
|
return void 0;
|
|
2103
3080
|
}
|
|
@@ -2113,8 +3090,8 @@ function getAppNameFromPackageJson() {
|
|
|
2113
3090
|
const packageJsonPath = join(projectRoot, "package.json");
|
|
2114
3091
|
if (!existsSync(packageJsonPath)) return void 0;
|
|
2115
3092
|
try {
|
|
2116
|
-
const pkg = JSON.parse(readFileSync(packageJsonPath, "utf-8"));
|
|
2117
|
-
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(/^@[^/]+\//, "");
|
|
2118
3095
|
} catch {}
|
|
2119
3096
|
return void 0;
|
|
2120
3097
|
}
|
|
@@ -2587,7 +3564,7 @@ async function provisionServices(api, projectId, environmentId, appName, service
|
|
|
2587
3564
|
*/
|
|
2588
3565
|
async function ensureDokploySetup(config$1, dockerConfig, stage, services) {
|
|
2589
3566
|
logger$1.log("\n🔧 Checking Dokploy setup...");
|
|
2590
|
-
const { readStageSecrets: readStageSecrets$1 } = await import("./storage-
|
|
3567
|
+
const { readStageSecrets: readStageSecrets$1 } = await import("./storage-DNj_I11J.mjs");
|
|
2591
3568
|
const existingSecrets = await readStageSecrets$1(stage);
|
|
2592
3569
|
const existingUrls = {
|
|
2593
3570
|
DATABASE_URL: existingSecrets?.urls?.DATABASE_URL,
|
|
@@ -2765,94 +3742,335 @@ function generateTag(stage) {
|
|
|
2765
3742
|
return `${stage}-${timestamp}`;
|
|
2766
3743
|
}
|
|
2767
3744
|
/**
|
|
2768
|
-
*
|
|
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
|
|
2769
3751
|
*/
|
|
2770
|
-
async function
|
|
2771
|
-
const { provider, stage, tag,
|
|
2772
|
-
|
|
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...`);
|
|
2773
3756
|
logger$1.log(` Stage: ${stage}`);
|
|
2774
|
-
const config$1 = await loadConfig();
|
|
2775
3757
|
const imageTag = tag ?? generateTag(stage);
|
|
2776
3758
|
logger$1.log(` Tag: ${imageTag}`);
|
|
2777
|
-
const
|
|
2778
|
-
|
|
2779
|
-
|
|
2780
|
-
|
|
2781
|
-
|
|
2782
|
-
|
|
2783
|
-
|
|
2784
|
-
|
|
2785
|
-
|
|
2786
|
-
const
|
|
2787
|
-
|
|
2788
|
-
|
|
2789
|
-
|
|
2790
|
-
|
|
2791
|
-
postgres: Boolean(composeServices.postgres),
|
|
2792
|
-
redis: Boolean(composeServices.redis),
|
|
2793
|
-
rabbitmq: Boolean(composeServices.rabbitmq)
|
|
2794
|
-
} : void 0;
|
|
2795
|
-
const setupResult = await ensureDokploySetup(config$1, dockerConfig, stage, dockerServices);
|
|
2796
|
-
dokployConfig = setupResult.config;
|
|
2797
|
-
finalRegistry = dokployConfig.registry ?? dockerConfig.registry;
|
|
2798
|
-
if (setupResult.serviceUrls) {
|
|
2799
|
-
const { readStageSecrets: readStageSecrets$1, writeStageSecrets: writeStageSecrets$1, initStageSecrets } = await import("./storage-nkGIjeXt.mjs");
|
|
2800
|
-
let secrets = await readStageSecrets$1(stage);
|
|
2801
|
-
if (!secrets) {
|
|
2802
|
-
logger$1.log(` Creating secrets file for stage "${stage}"...`);
|
|
2803
|
-
secrets = initStageSecrets(stage);
|
|
2804
|
-
}
|
|
2805
|
-
let updated = false;
|
|
2806
|
-
const urlFields = [
|
|
2807
|
-
"DATABASE_URL",
|
|
2808
|
-
"REDIS_URL",
|
|
2809
|
-
"RABBITMQ_URL"
|
|
2810
|
-
];
|
|
2811
|
-
for (const [key, value] of Object.entries(setupResult.serviceUrls)) {
|
|
2812
|
-
if (!value) continue;
|
|
2813
|
-
if (urlFields.includes(key)) {
|
|
2814
|
-
const urlKey = key;
|
|
2815
|
-
if (!secrets.urls[urlKey]) {
|
|
2816
|
-
secrets.urls[urlKey] = value;
|
|
2817
|
-
logger$1.log(` Saved ${key} to secrets.urls`);
|
|
2818
|
-
updated = true;
|
|
2819
|
-
}
|
|
2820
|
-
} else if (!secrets.custom[key]) {
|
|
2821
|
-
secrets.custom[key] = value;
|
|
2822
|
-
logger$1.log(` Saved ${key} to secrets.custom`);
|
|
2823
|
-
updated = true;
|
|
2824
|
-
}
|
|
2825
|
-
}
|
|
2826
|
-
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;
|
|
2827
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`);
|
|
2828
3780
|
}
|
|
2829
|
-
|
|
2830
|
-
|
|
2831
|
-
|
|
2832
|
-
|
|
2833
|
-
|
|
2834
|
-
|
|
2835
|
-
|
|
2836
|
-
|
|
2837
|
-
|
|
2838
|
-
|
|
2839
|
-
|
|
2840
|
-
|
|
2841
|
-
|
|
2842
|
-
|
|
2843
|
-
|
|
2844
|
-
|
|
2845
|
-
|
|
2846
|
-
|
|
2847
|
-
|
|
2848
|
-
|
|
2849
|
-
|
|
2850
|
-
|
|
2851
|
-
|
|
2852
|
-
|
|
2853
|
-
|
|
2854
|
-
|
|
2855
|
-
|
|
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,
|
|
2856
4074
|
tag: imageTag,
|
|
2857
4075
|
skipPush: false,
|
|
2858
4076
|
masterKey,
|
|
@@ -2879,10 +4097,361 @@ async function deployCommand(options) {
|
|
|
2879
4097
|
};
|
|
2880
4098
|
break;
|
|
2881
4099
|
}
|
|
2882
|
-
default: throw new Error(`Unknown deploy provider: ${provider}\nSupported providers: docker, dokploy, aws-lambda`);
|
|
2883
|
-
}
|
|
2884
|
-
logger$1.log("\n✅ Deployment complete!");
|
|
2885
|
-
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
|
+
];
|
|
2886
4455
|
}
|
|
2887
4456
|
|
|
2888
4457
|
//#endregion
|
|
@@ -2894,6 +4463,7 @@ function generateConfigFiles(options, template) {
|
|
|
2894
4463
|
const { telescope, studio, routesStructure } = options;
|
|
2895
4464
|
const isServerless = template.name === "serverless";
|
|
2896
4465
|
const hasWorker = template.name === "worker";
|
|
4466
|
+
const isFullstack = options.template === "fullstack";
|
|
2897
4467
|
const getRoutesGlob = () => {
|
|
2898
4468
|
switch (routesStructure) {
|
|
2899
4469
|
case "centralized-endpoints": return "./src/endpoints/**/*.ts";
|
|
@@ -2901,6 +4471,14 @@ function generateConfigFiles(options, template) {
|
|
|
2901
4471
|
case "domain-based": return "./src/**/routes/*.ts";
|
|
2902
4472
|
}
|
|
2903
4473
|
};
|
|
4474
|
+
if (isFullstack) return generateSingleAppConfigFiles(options, template, {
|
|
4475
|
+
telescope,
|
|
4476
|
+
studio,
|
|
4477
|
+
routesStructure,
|
|
4478
|
+
isServerless,
|
|
4479
|
+
hasWorker,
|
|
4480
|
+
getRoutesGlob
|
|
4481
|
+
});
|
|
2904
4482
|
let gkmConfig = `import { defineConfig } from '@geekmidas/cli/config';
|
|
2905
4483
|
|
|
2906
4484
|
export default defineConfig({
|
|
@@ -2929,8 +4507,7 @@ export default defineConfig({
|
|
|
2929
4507
|
const tsConfig = options.monorepo ? {
|
|
2930
4508
|
extends: "../../tsconfig.json",
|
|
2931
4509
|
compilerOptions: {
|
|
2932
|
-
|
|
2933
|
-
rootDir: "./src",
|
|
4510
|
+
noEmit: true,
|
|
2934
4511
|
baseUrl: ".",
|
|
2935
4512
|
paths: { [`@${options.name}/*`]: ["../../packages/*/src"] }
|
|
2936
4513
|
},
|
|
@@ -2963,7 +4540,7 @@ export default defineConfig({
|
|
|
2963
4540
|
content: `${JSON.stringify(tsConfig, null, 2)}\n`
|
|
2964
4541
|
}];
|
|
2965
4542
|
const biomeConfig = {
|
|
2966
|
-
$schema: "https://biomejs.dev/schemas/
|
|
4543
|
+
$schema: "https://biomejs.dev/schemas/2.3.0/schema.json",
|
|
2967
4544
|
vcs: {
|
|
2968
4545
|
enabled: true,
|
|
2969
4546
|
clientKind: "git",
|
|
@@ -3046,23 +4623,46 @@ export default defineConfig({
|
|
|
3046
4623
|
}
|
|
3047
4624
|
];
|
|
3048
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
|
+
}
|
|
3049
4642
|
|
|
3050
4643
|
//#endregion
|
|
3051
4644
|
//#region src/init/generators/docker.ts
|
|
3052
4645
|
/**
|
|
3053
4646
|
* Generate docker-compose.yml based on template and options
|
|
3054
4647
|
*/
|
|
3055
|
-
function generateDockerFiles(options, template) {
|
|
4648
|
+
function generateDockerFiles(options, template, dbApps) {
|
|
3056
4649
|
const { database } = options;
|
|
3057
4650
|
const isServerless = template.name === "serverless";
|
|
3058
4651
|
const hasWorker = template.name === "worker";
|
|
4652
|
+
const isFullstack = options.template === "fullstack";
|
|
3059
4653
|
const services = [];
|
|
3060
4654
|
const volumes = [];
|
|
4655
|
+
const files = [];
|
|
3061
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` : "";
|
|
3062
4662
|
services.push(` postgres:
|
|
3063
4663
|
image: postgres:16-alpine
|
|
3064
4664
|
container_name: ${options.name}-postgres
|
|
3065
|
-
restart: unless-stopped
|
|
4665
|
+
restart: unless-stopped${envFile}
|
|
3066
4666
|
environment:
|
|
3067
4667
|
POSTGRES_USER: postgres
|
|
3068
4668
|
POSTGRES_PASSWORD: postgres
|
|
@@ -3070,13 +4670,23 @@ function generateDockerFiles(options, template) {
|
|
|
3070
4670
|
ports:
|
|
3071
4671
|
- '5432:5432'
|
|
3072
4672
|
volumes:
|
|
3073
|
-
- postgres_data:/var/lib/postgresql/data
|
|
4673
|
+
- postgres_data:/var/lib/postgresql/data${initVolume}
|
|
3074
4674
|
healthcheck:
|
|
3075
4675
|
test: ['CMD-SHELL', 'pg_isready -U postgres']
|
|
3076
4676
|
interval: 5s
|
|
3077
4677
|
timeout: 5s
|
|
3078
4678
|
retries: 5`);
|
|
3079
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
|
+
}
|
|
3080
4690
|
}
|
|
3081
4691
|
if (isServerless) {
|
|
3082
4692
|
services.push(` redis:
|
|
@@ -3152,105 +4762,85 @@ ${services.join("\n\n")}
|
|
|
3152
4762
|
volumes:
|
|
3153
4763
|
${volumes.join("\n")}
|
|
3154
4764
|
`;
|
|
3155
|
-
|
|
4765
|
+
files.push({
|
|
3156
4766
|
path: "docker-compose.yml",
|
|
3157
4767
|
content: dockerCompose
|
|
3158
|
-
}
|
|
4768
|
+
});
|
|
4769
|
+
return files;
|
|
3159
4770
|
}
|
|
3160
|
-
|
|
3161
|
-
//#endregion
|
|
3162
|
-
//#region src/init/generators/env.ts
|
|
3163
4771
|
/**
|
|
3164
|
-
* Generate
|
|
4772
|
+
* Generate .env file for docker-compose with database passwords
|
|
3165
4773
|
*/
|
|
3166
|
-
function
|
|
3167
|
-
const
|
|
3168
|
-
|
|
3169
|
-
|
|
3170
|
-
|
|
3171
|
-
|
|
3172
|
-
|
|
3173
|
-
|
|
3174
|
-
|
|
3175
|
-
if (isServerless) baseEnv = `# AWS
|
|
3176
|
-
STAGE=dev
|
|
3177
|
-
AWS_REGION=us-east-1
|
|
3178
|
-
LOG_LEVEL=info
|
|
3179
|
-
`;
|
|
3180
|
-
if (database) baseEnv += `
|
|
3181
|
-
# Database
|
|
3182
|
-
DATABASE_URL=postgresql://user:password@localhost:5432/mydb
|
|
3183
|
-
`;
|
|
3184
|
-
if (hasWorker) baseEnv += `
|
|
3185
|
-
# Message Queue
|
|
3186
|
-
RABBITMQ_URL=amqp://localhost:5672
|
|
3187
|
-
`;
|
|
3188
|
-
baseEnv += `
|
|
3189
|
-
# Authentication
|
|
3190
|
-
JWT_SECRET=your-secret-key-change-in-production
|
|
3191
|
-
`;
|
|
3192
|
-
let devEnv = `# Development Environment
|
|
3193
|
-
NODE_ENV=development
|
|
3194
|
-
PORT=3000
|
|
3195
|
-
LOG_LEVEL=debug
|
|
3196
|
-
`;
|
|
3197
|
-
if (isServerless) devEnv = `# Development Environment
|
|
3198
|
-
STAGE=dev
|
|
3199
|
-
AWS_REGION=us-east-1
|
|
3200
|
-
LOG_LEVEL=debug
|
|
3201
|
-
`;
|
|
3202
|
-
if (database) devEnv += `
|
|
3203
|
-
# Database
|
|
3204
|
-
DATABASE_URL=postgresql://postgres:postgres@localhost:5432/mydb_dev
|
|
3205
|
-
`;
|
|
3206
|
-
if (hasWorker) devEnv += `
|
|
3207
|
-
# Message Queue
|
|
3208
|
-
RABBITMQ_URL=amqp://localhost:5672
|
|
3209
|
-
`;
|
|
3210
|
-
devEnv += `
|
|
3211
|
-
# Authentication
|
|
3212
|
-
JWT_SECRET=dev-secret-not-for-production
|
|
3213
|
-
`;
|
|
3214
|
-
let testEnv = `# Test Environment
|
|
3215
|
-
NODE_ENV=test
|
|
3216
|
-
PORT=3001
|
|
3217
|
-
LOG_LEVEL=error
|
|
3218
|
-
`;
|
|
3219
|
-
if (isServerless) testEnv = `# Test Environment
|
|
3220
|
-
STAGE=test
|
|
3221
|
-
AWS_REGION=us-east-1
|
|
3222
|
-
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")}
|
|
3223
4783
|
`;
|
|
3224
|
-
|
|
3225
|
-
|
|
3226
|
-
|
|
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
|
|
3227
4806
|
`;
|
|
3228
|
-
|
|
3229
|
-
#
|
|
3230
|
-
|
|
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
|
|
3231
4820
|
`;
|
|
3232
|
-
|
|
3233
|
-
|
|
3234
|
-
|
|
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!"
|
|
3235
4831
|
`;
|
|
3236
|
-
|
|
3237
|
-
|
|
3238
|
-
|
|
3239
|
-
|
|
3240
|
-
|
|
3241
|
-
|
|
3242
|
-
|
|
3243
|
-
|
|
3244
|
-
|
|
3245
|
-
|
|
3246
|
-
|
|
3247
|
-
|
|
3248
|
-
},
|
|
3249
|
-
{
|
|
3250
|
-
path: ".env.test",
|
|
3251
|
-
content: testEnv
|
|
3252
|
-
}
|
|
3253
|
-
];
|
|
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 = [];
|
|
3254
4844
|
if (!options.monorepo) {
|
|
3255
4845
|
const gitignore = `# Dependencies
|
|
3256
4846
|
node_modules/
|
|
@@ -3259,7 +4849,7 @@ node_modules/
|
|
|
3259
4849
|
dist/
|
|
3260
4850
|
.gkm/
|
|
3261
4851
|
|
|
3262
|
-
# Environment
|
|
4852
|
+
# Environment (legacy - use gkm secrets instead)
|
|
3263
4853
|
.env
|
|
3264
4854
|
.env.local
|
|
3265
4855
|
.env.*.local
|
|
@@ -3328,6 +4918,8 @@ function generateModelsPackage(options) {
|
|
|
3328
4918
|
const tsConfig = {
|
|
3329
4919
|
extends: "../../tsconfig.json",
|
|
3330
4920
|
compilerOptions: {
|
|
4921
|
+
declaration: true,
|
|
4922
|
+
declarationMap: true,
|
|
3331
4923
|
outDir: "./dist",
|
|
3332
4924
|
rootDir: "./src"
|
|
3333
4925
|
},
|
|
@@ -3414,23 +5006,27 @@ export type UpdateUser = z.infer<typeof updateUserSchema>;
|
|
|
3414
5006
|
*/
|
|
3415
5007
|
function generateMonorepoFiles(options, _template) {
|
|
3416
5008
|
if (!options.monorepo) return [];
|
|
5009
|
+
const isFullstack = options.template === "fullstack";
|
|
3417
5010
|
const rootPackageJson = {
|
|
3418
5011
|
name: options.name,
|
|
3419
5012
|
version: "0.0.1",
|
|
3420
5013
|
private: true,
|
|
3421
5014
|
type: "module",
|
|
5015
|
+
packageManager: "pnpm@10.13.1",
|
|
3422
5016
|
scripts: {
|
|
3423
|
-
dev: "turbo dev",
|
|
3424
|
-
build: "turbo build",
|
|
3425
|
-
test: "turbo test",
|
|
3426
|
-
"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",
|
|
3427
5021
|
typecheck: "turbo typecheck",
|
|
3428
5022
|
lint: "biome lint .",
|
|
3429
5023
|
fmt: "biome format . --write",
|
|
3430
|
-
"fmt:check": "biome format ."
|
|
5024
|
+
"fmt:check": "biome format .",
|
|
5025
|
+
...options.deployTarget === "dokploy" ? { deploy: "gkm deploy --provider dokploy --stage production" } : {}
|
|
3431
5026
|
},
|
|
3432
5027
|
devDependencies: {
|
|
3433
|
-
"@biomejs/biome": "~
|
|
5028
|
+
"@biomejs/biome": "~2.3.0",
|
|
5029
|
+
"@geekmidas/cli": GEEKMIDAS_VERSIONS["@geekmidas/cli"],
|
|
3434
5030
|
turbo: "~2.3.0",
|
|
3435
5031
|
typescript: "~5.8.2",
|
|
3436
5032
|
vitest: "~4.0.0"
|
|
@@ -3443,7 +5039,7 @@ function generateMonorepoFiles(options, _template) {
|
|
|
3443
5039
|
- 'packages/*'
|
|
3444
5040
|
`;
|
|
3445
5041
|
const biomeConfig = {
|
|
3446
|
-
$schema: "https://biomejs.dev/schemas/
|
|
5042
|
+
$schema: "https://biomejs.dev/schemas/2.3.0/schema.json",
|
|
3447
5043
|
vcs: {
|
|
3448
5044
|
enabled: true,
|
|
3449
5045
|
clientKind: "git",
|
|
@@ -3518,6 +5114,7 @@ dist/
|
|
|
3518
5114
|
.env
|
|
3519
5115
|
.env.local
|
|
3520
5116
|
.env.*.local
|
|
5117
|
+
docker/.env
|
|
3521
5118
|
|
|
3522
5119
|
# IDE
|
|
3523
5120
|
.idea/
|
|
@@ -3554,14 +5151,27 @@ coverage/
|
|
|
3554
5151
|
esModuleInterop: true,
|
|
3555
5152
|
skipLibCheck: true,
|
|
3556
5153
|
forceConsistentCasingInFileNames: true,
|
|
3557
|
-
resolveJsonModule: true
|
|
3558
|
-
declaration: true,
|
|
3559
|
-
declarationMap: true,
|
|
3560
|
-
composite: true
|
|
5154
|
+
resolveJsonModule: true
|
|
3561
5155
|
},
|
|
3562
5156
|
exclude: ["node_modules", "dist"]
|
|
3563
5157
|
};
|
|
3564
|
-
|
|
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 = [
|
|
3565
5175
|
{
|
|
3566
5176
|
path: "package.json",
|
|
3567
5177
|
content: `${JSON.stringify(rootPackageJson, null, 2)}\n`
|
|
@@ -3582,11 +5192,100 @@ coverage/
|
|
|
3582
5192
|
path: "turbo.json",
|
|
3583
5193
|
content: `${JSON.stringify(turboConfig, null, 2)}\n`
|
|
3584
5194
|
},
|
|
5195
|
+
{
|
|
5196
|
+
path: "vitest.config.ts",
|
|
5197
|
+
content: vitestConfig
|
|
5198
|
+
},
|
|
3585
5199
|
{
|
|
3586
5200
|
path: ".gitignore",
|
|
3587
5201
|
content: gitignore
|
|
3588
5202
|
}
|
|
3589
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;
|
|
3590
5289
|
}
|
|
3591
5290
|
|
|
3592
5291
|
//#endregion
|
|
@@ -3595,18 +5294,18 @@ const apiTemplate = {
|
|
|
3595
5294
|
name: "api",
|
|
3596
5295
|
description: "Full API with auth, database, services",
|
|
3597
5296
|
dependencies: {
|
|
3598
|
-
"@geekmidas/constructs": "
|
|
3599
|
-
"@geekmidas/envkit": "
|
|
3600
|
-
"@geekmidas/logger": "
|
|
3601
|
-
"@geekmidas/services": "
|
|
3602
|
-
"@geekmidas/errors": "
|
|
3603
|
-
"@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"],
|
|
3604
5303
|
hono: "~4.8.2",
|
|
3605
5304
|
pino: "~9.6.0"
|
|
3606
5305
|
},
|
|
3607
5306
|
devDependencies: {
|
|
3608
|
-
"@biomejs/biome": "~
|
|
3609
|
-
"@geekmidas/cli": "
|
|
5307
|
+
"@biomejs/biome": "~2.3.0",
|
|
5308
|
+
"@geekmidas/cli": GEEKMIDAS_VERSIONS["@geekmidas/cli"],
|
|
3610
5309
|
"@types/node": "~22.0.0",
|
|
3611
5310
|
tsx: "~4.20.0",
|
|
3612
5311
|
turbo: "~2.3.0",
|
|
@@ -3643,18 +5342,17 @@ export const logger = createLogger();
|
|
|
3643
5342
|
const files = [
|
|
3644
5343
|
{
|
|
3645
5344
|
path: "src/config/env.ts",
|
|
3646
|
-
content: `import {
|
|
5345
|
+
content: `import { Credentials } from '@geekmidas/envkit/credentials';
|
|
5346
|
+
import { EnvironmentParser } from '@geekmidas/envkit';
|
|
3647
5347
|
|
|
3648
|
-
export const envParser = new EnvironmentParser(process.env);
|
|
5348
|
+
export const envParser = new EnvironmentParser({ ...process.env, ...Credentials });
|
|
3649
5349
|
|
|
5350
|
+
// Global config - only minimal shared values
|
|
5351
|
+
// Service-specific config should be parsed in each service
|
|
3650
5352
|
export const config = envParser
|
|
3651
5353
|
.create((get) => ({
|
|
3652
|
-
|
|
3653
|
-
|
|
3654
|
-
jwtSecret: get('JWT_SECRET').string().default('change-me-in-production'),${options.database ? `
|
|
3655
|
-
database: {
|
|
3656
|
-
url: get('DATABASE_URL').string().default('postgresql://localhost:5432/mydb'),
|
|
3657
|
-
},` : ""}
|
|
5354
|
+
nodeEnv: get('NODE_ENV').enum(['development', 'test', 'production']).default('development'),
|
|
5355
|
+
stage: get('STAGE').enum(['development', 'staging', 'production']).default('development'),
|
|
3658
5356
|
}))
|
|
3659
5357
|
.parse();
|
|
3660
5358
|
`
|
|
@@ -3667,7 +5365,7 @@ export const config = envParser
|
|
|
3667
5365
|
path: getRoutePath("health.ts"),
|
|
3668
5366
|
content: `import { e } from '@geekmidas/constructs/endpoints';
|
|
3669
5367
|
|
|
3670
|
-
export
|
|
5368
|
+
export const healthEndpoint = e
|
|
3671
5369
|
.get('/health')
|
|
3672
5370
|
.handle(async () => ({
|
|
3673
5371
|
status: 'ok',
|
|
@@ -3679,7 +5377,7 @@ export default e
|
|
|
3679
5377
|
path: getRoutePath("users/list.ts"),
|
|
3680
5378
|
content: `import { e } from '@geekmidas/constructs/endpoints';
|
|
3681
5379
|
|
|
3682
|
-
export
|
|
5380
|
+
export const listUsersEndpoint = e
|
|
3683
5381
|
.get('/users')
|
|
3684
5382
|
.handle(async () => ({
|
|
3685
5383
|
users: [
|
|
@@ -3694,7 +5392,7 @@ export default e
|
|
|
3694
5392
|
content: `import { e } from '@geekmidas/constructs/endpoints';
|
|
3695
5393
|
import { z } from 'zod';
|
|
3696
5394
|
|
|
3697
|
-
export
|
|
5395
|
+
export const getUserEndpoint = e
|
|
3698
5396
|
.get('/users/:id')
|
|
3699
5397
|
.params(z.object({ id: z.string() }))
|
|
3700
5398
|
.handle(async ({ params }) => ({
|
|
@@ -3707,7 +5405,7 @@ export default e
|
|
|
3707
5405
|
];
|
|
3708
5406
|
if (options.database) files.push({
|
|
3709
5407
|
path: "src/services/database.ts",
|
|
3710
|
-
content: `import type { Service } from '@geekmidas/services';
|
|
5408
|
+
content: `import type { Service, ServiceRegisterOptions } from '@geekmidas/services';
|
|
3711
5409
|
import { Kysely, PostgresDialect } from 'kysely';
|
|
3712
5410
|
import pg from 'pg';
|
|
3713
5411
|
|
|
@@ -3723,18 +5421,24 @@ export interface Database {
|
|
|
3723
5421
|
|
|
3724
5422
|
export const databaseService = {
|
|
3725
5423
|
serviceName: 'database' as const,
|
|
3726
|
-
async register(envParser) {
|
|
5424
|
+
async register({ envParser, context }: ServiceRegisterOptions) {
|
|
5425
|
+
const logger = context.getLogger();
|
|
5426
|
+
logger.info('Connecting to database');
|
|
5427
|
+
|
|
3727
5428
|
const config = envParser
|
|
3728
5429
|
.create((get) => ({
|
|
3729
5430
|
url: get('DATABASE_URL').string(),
|
|
3730
5431
|
}))
|
|
3731
5432
|
.parse();
|
|
3732
5433
|
|
|
3733
|
-
|
|
5434
|
+
const db = new Kysely<Database>({
|
|
3734
5435
|
dialect: new PostgresDialect({
|
|
3735
5436
|
pool: new pg.Pool({ connectionString: config.url }),
|
|
3736
5437
|
}),
|
|
3737
5438
|
});
|
|
5439
|
+
|
|
5440
|
+
logger.info('Database connection established');
|
|
5441
|
+
return db;
|
|
3738
5442
|
},
|
|
3739
5443
|
} satisfies Service<'database', Kysely<Database>>;
|
|
3740
5444
|
`
|
|
@@ -3755,13 +5459,20 @@ export const telescope = new Telescope({
|
|
|
3755
5459
|
content: `import { Direction, InMemoryMonitoringStorage, Studio } from '@geekmidas/studio';
|
|
3756
5460
|
import { Kysely, PostgresDialect } from 'kysely';
|
|
3757
5461
|
import pg from 'pg';
|
|
3758
|
-
import type { Database } from '../services/database';
|
|
3759
|
-
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();
|
|
3760
5471
|
|
|
3761
5472
|
// Create a Kysely instance for Studio
|
|
3762
5473
|
const db = new Kysely<Database>({
|
|
3763
5474
|
dialect: new PostgresDialect({
|
|
3764
|
-
pool: new pg.Pool({ connectionString:
|
|
5475
|
+
pool: new pg.Pool({ connectionString: studioConfig.databaseUrl }),
|
|
3765
5476
|
}),
|
|
3766
5477
|
});
|
|
3767
5478
|
|
|
@@ -3787,15 +5498,15 @@ const minimalTemplate = {
|
|
|
3787
5498
|
name: "minimal",
|
|
3788
5499
|
description: "Basic health endpoint",
|
|
3789
5500
|
dependencies: {
|
|
3790
|
-
"@geekmidas/constructs": "
|
|
3791
|
-
"@geekmidas/envkit": "
|
|
3792
|
-
"@geekmidas/logger": "
|
|
5501
|
+
"@geekmidas/constructs": GEEKMIDAS_VERSIONS["@geekmidas/constructs"],
|
|
5502
|
+
"@geekmidas/envkit": GEEKMIDAS_VERSIONS["@geekmidas/envkit"],
|
|
5503
|
+
"@geekmidas/logger": GEEKMIDAS_VERSIONS["@geekmidas/logger"],
|
|
3793
5504
|
hono: "~4.8.2",
|
|
3794
5505
|
pino: "~9.6.0"
|
|
3795
5506
|
},
|
|
3796
5507
|
devDependencies: {
|
|
3797
|
-
"@biomejs/biome": "~
|
|
3798
|
-
"@geekmidas/cli": "
|
|
5508
|
+
"@biomejs/biome": "~2.3.0",
|
|
5509
|
+
"@geekmidas/cli": GEEKMIDAS_VERSIONS["@geekmidas/cli"],
|
|
3799
5510
|
"@types/node": "~22.0.0",
|
|
3800
5511
|
tsx: "~4.20.0",
|
|
3801
5512
|
turbo: "~2.3.0",
|
|
@@ -3828,14 +5539,17 @@ export const logger = createLogger();
|
|
|
3828
5539
|
const files = [
|
|
3829
5540
|
{
|
|
3830
5541
|
path: "src/config/env.ts",
|
|
3831
|
-
content: `import {
|
|
5542
|
+
content: `import { Credentials } from '@geekmidas/envkit/credentials';
|
|
5543
|
+
import { EnvironmentParser } from '@geekmidas/envkit';
|
|
3832
5544
|
|
|
3833
|
-
export const envParser = new EnvironmentParser(process.env);
|
|
5545
|
+
export const envParser = new EnvironmentParser({ ...process.env, ...Credentials });
|
|
3834
5546
|
|
|
5547
|
+
// Global config - only minimal shared values
|
|
5548
|
+
// Service-specific config should be parsed in each service
|
|
3835
5549
|
export const config = envParser
|
|
3836
5550
|
.create((get) => ({
|
|
3837
|
-
|
|
3838
|
-
|
|
5551
|
+
nodeEnv: get('NODE_ENV').enum(['development', 'test', 'production']).default('development'),
|
|
5552
|
+
stage: get('STAGE').enum(['development', 'staging', 'production']).default('development'),
|
|
3839
5553
|
}))
|
|
3840
5554
|
.parse();
|
|
3841
5555
|
`
|
|
@@ -3848,7 +5562,7 @@ export const config = envParser
|
|
|
3848
5562
|
path: getRoutePath("health.ts"),
|
|
3849
5563
|
content: `import { e } from '@geekmidas/constructs/endpoints';
|
|
3850
5564
|
|
|
3851
|
-
export
|
|
5565
|
+
export const healthEndpoint = e
|
|
3852
5566
|
.get('/health')
|
|
3853
5567
|
.handle(async () => ({
|
|
3854
5568
|
status: 'ok',
|
|
@@ -3857,27 +5571,9 @@ export default e
|
|
|
3857
5571
|
`
|
|
3858
5572
|
}
|
|
3859
5573
|
];
|
|
3860
|
-
if (options.database) {
|
|
3861
|
-
|
|
3862
|
-
|
|
3863
|
-
content: `import { EnvironmentParser } from '@geekmidas/envkit';
|
|
3864
|
-
|
|
3865
|
-
export const envParser = new EnvironmentParser(process.env);
|
|
3866
|
-
|
|
3867
|
-
export const config = envParser
|
|
3868
|
-
.create((get) => ({
|
|
3869
|
-
port: get('PORT').string().transform(Number).default(3000),
|
|
3870
|
-
nodeEnv: get('NODE_ENV').string().default('development'),
|
|
3871
|
-
database: {
|
|
3872
|
-
url: get('DATABASE_URL').string().default('postgresql://localhost:5432/mydb'),
|
|
3873
|
-
},
|
|
3874
|
-
}))
|
|
3875
|
-
.parse();
|
|
3876
|
-
`
|
|
3877
|
-
};
|
|
3878
|
-
files.push({
|
|
3879
|
-
path: "src/services/database.ts",
|
|
3880
|
-
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';
|
|
3881
5577
|
import { Kysely, PostgresDialect } from 'kysely';
|
|
3882
5578
|
import pg from 'pg';
|
|
3883
5579
|
|
|
@@ -3888,23 +5584,28 @@ export interface Database {
|
|
|
3888
5584
|
|
|
3889
5585
|
export const databaseService = {
|
|
3890
5586
|
serviceName: 'database' as const,
|
|
3891
|
-
async register(envParser) {
|
|
5587
|
+
async register({ envParser, context }: ServiceRegisterOptions) {
|
|
5588
|
+
const logger = context.getLogger();
|
|
5589
|
+
logger.info('Connecting to database');
|
|
5590
|
+
|
|
3892
5591
|
const config = envParser
|
|
3893
5592
|
.create((get) => ({
|
|
3894
5593
|
url: get('DATABASE_URL').string(),
|
|
3895
5594
|
}))
|
|
3896
5595
|
.parse();
|
|
3897
5596
|
|
|
3898
|
-
|
|
5597
|
+
const db = new Kysely<Database>({
|
|
3899
5598
|
dialect: new PostgresDialect({
|
|
3900
5599
|
pool: new pg.Pool({ connectionString: config.url }),
|
|
3901
5600
|
}),
|
|
3902
5601
|
});
|
|
5602
|
+
|
|
5603
|
+
logger.info('Database connection established');
|
|
5604
|
+
return db;
|
|
3903
5605
|
},
|
|
3904
5606
|
} satisfies Service<'database', Kysely<Database>>;
|
|
3905
5607
|
`
|
|
3906
|
-
|
|
3907
|
-
}
|
|
5608
|
+
});
|
|
3908
5609
|
if (options.telescope) files.push({
|
|
3909
5610
|
path: "src/config/telescope.ts",
|
|
3910
5611
|
content: `import { Telescope } from '@geekmidas/telescope';
|
|
@@ -3921,13 +5622,20 @@ export const telescope = new Telescope({
|
|
|
3921
5622
|
content: `import { Direction, InMemoryMonitoringStorage, Studio } from '@geekmidas/studio';
|
|
3922
5623
|
import { Kysely, PostgresDialect } from 'kysely';
|
|
3923
5624
|
import pg from 'pg';
|
|
3924
|
-
import type { Database } from '../services/database';
|
|
3925
|
-
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();
|
|
3926
5634
|
|
|
3927
5635
|
// Create a Kysely instance for Studio
|
|
3928
5636
|
const db = new Kysely<Database>({
|
|
3929
5637
|
dialect: new PostgresDialect({
|
|
3930
|
-
pool: new pg.Pool({ connectionString:
|
|
5638
|
+
pool: new pg.Pool({ connectionString: studioConfig.databaseUrl }),
|
|
3931
5639
|
}),
|
|
3932
5640
|
});
|
|
3933
5641
|
|
|
@@ -3953,16 +5661,16 @@ const serverlessTemplate = {
|
|
|
3953
5661
|
name: "serverless",
|
|
3954
5662
|
description: "AWS Lambda handlers",
|
|
3955
5663
|
dependencies: {
|
|
3956
|
-
"@geekmidas/constructs": "
|
|
3957
|
-
"@geekmidas/envkit": "
|
|
3958
|
-
"@geekmidas/logger": "
|
|
3959
|
-
"@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"],
|
|
3960
5668
|
hono: "~4.8.2",
|
|
3961
5669
|
pino: "~9.6.0"
|
|
3962
5670
|
},
|
|
3963
5671
|
devDependencies: {
|
|
3964
|
-
"@biomejs/biome": "~
|
|
3965
|
-
"@geekmidas/cli": "
|
|
5672
|
+
"@biomejs/biome": "~2.3.0",
|
|
5673
|
+
"@geekmidas/cli": GEEKMIDAS_VERSIONS["@geekmidas/cli"],
|
|
3966
5674
|
"@types/aws-lambda": "~8.10.92",
|
|
3967
5675
|
"@types/node": "~22.0.0",
|
|
3968
5676
|
tsx: "~4.20.0",
|
|
@@ -3996,17 +5704,17 @@ export const logger = createLogger();
|
|
|
3996
5704
|
const files = [
|
|
3997
5705
|
{
|
|
3998
5706
|
path: "src/config/env.ts",
|
|
3999
|
-
content: `import {
|
|
5707
|
+
content: `import { Credentials } from '@geekmidas/envkit/credentials';
|
|
5708
|
+
import { EnvironmentParser } from '@geekmidas/envkit';
|
|
4000
5709
|
|
|
4001
|
-
export const envParser = new EnvironmentParser(process.env);
|
|
5710
|
+
export const envParser = new EnvironmentParser({ ...process.env, ...Credentials });
|
|
4002
5711
|
|
|
5712
|
+
// Global config - only minimal shared values
|
|
5713
|
+
// Service-specific config should be parsed in each service
|
|
4003
5714
|
export const config = envParser
|
|
4004
5715
|
.create((get) => ({
|
|
4005
|
-
|
|
4006
|
-
|
|
4007
|
-
database: {
|
|
4008
|
-
url: get('DATABASE_URL').string(),
|
|
4009
|
-
},` : ""}
|
|
5716
|
+
nodeEnv: get('NODE_ENV').enum(['development', 'test', 'production']).default('development'),
|
|
5717
|
+
stage: get('STAGE').enum(['dev', 'staging', 'prod']).default('dev'),
|
|
4010
5718
|
}))
|
|
4011
5719
|
.parse();
|
|
4012
5720
|
`
|
|
@@ -4019,7 +5727,7 @@ export const config = envParser
|
|
|
4019
5727
|
path: getRoutePath("health.ts"),
|
|
4020
5728
|
content: `import { e } from '@geekmidas/constructs/endpoints';
|
|
4021
5729
|
|
|
4022
|
-
export
|
|
5730
|
+
export const healthEndpoint = e
|
|
4023
5731
|
.get('/health')
|
|
4024
5732
|
.handle(async () => ({
|
|
4025
5733
|
status: 'ok',
|
|
@@ -4033,7 +5741,7 @@ export default e
|
|
|
4033
5741
|
content: `import { f } from '@geekmidas/constructs/functions';
|
|
4034
5742
|
import { z } from 'zod';
|
|
4035
5743
|
|
|
4036
|
-
export
|
|
5744
|
+
export const helloFunction = f
|
|
4037
5745
|
.input(z.object({ name: z.string() }))
|
|
4038
5746
|
.output(z.object({ message: z.string() }))
|
|
4039
5747
|
.handle(async ({ input }) => ({
|
|
@@ -4064,16 +5772,16 @@ const workerTemplate = {
|
|
|
4064
5772
|
name: "worker",
|
|
4065
5773
|
description: "Background job processing",
|
|
4066
5774
|
dependencies: {
|
|
4067
|
-
"@geekmidas/constructs": "
|
|
4068
|
-
"@geekmidas/envkit": "
|
|
4069
|
-
"@geekmidas/logger": "
|
|
4070
|
-
"@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"],
|
|
4071
5779
|
hono: "~4.8.2",
|
|
4072
5780
|
pino: "~9.6.0"
|
|
4073
5781
|
},
|
|
4074
5782
|
devDependencies: {
|
|
4075
|
-
"@biomejs/biome": "~
|
|
4076
|
-
"@geekmidas/cli": "
|
|
5783
|
+
"@biomejs/biome": "~2.3.0",
|
|
5784
|
+
"@geekmidas/cli": GEEKMIDAS_VERSIONS["@geekmidas/cli"],
|
|
4077
5785
|
"@types/node": "~22.0.0",
|
|
4078
5786
|
tsx: "~4.20.0",
|
|
4079
5787
|
turbo: "~2.3.0",
|
|
@@ -4106,20 +5814,17 @@ export const logger = createLogger();
|
|
|
4106
5814
|
const files = [
|
|
4107
5815
|
{
|
|
4108
5816
|
path: "src/config/env.ts",
|
|
4109
|
-
content: `import {
|
|
5817
|
+
content: `import { Credentials } from '@geekmidas/envkit/credentials';
|
|
5818
|
+
import { EnvironmentParser } from '@geekmidas/envkit';
|
|
4110
5819
|
|
|
4111
|
-
export const envParser = new EnvironmentParser(process.env);
|
|
5820
|
+
export const envParser = new EnvironmentParser({ ...process.env, ...Credentials });
|
|
4112
5821
|
|
|
5822
|
+
// Global config - only minimal shared values
|
|
5823
|
+
// Service-specific config should be parsed in each service
|
|
4113
5824
|
export const config = envParser
|
|
4114
5825
|
.create((get) => ({
|
|
4115
|
-
|
|
4116
|
-
|
|
4117
|
-
rabbitmq: {
|
|
4118
|
-
url: get('RABBITMQ_URL').string().default('amqp://localhost:5672'),
|
|
4119
|
-
},${options.database ? `
|
|
4120
|
-
database: {
|
|
4121
|
-
url: get('DATABASE_URL').string().default('postgresql://localhost:5432/mydb'),
|
|
4122
|
-
},` : ""}
|
|
5826
|
+
nodeEnv: get('NODE_ENV').enum(['development', 'test', 'production']).default('development'),
|
|
5827
|
+
stage: get('STAGE').enum(['development', 'staging', 'production']).default('development'),
|
|
4123
5828
|
}))
|
|
4124
5829
|
.parse();
|
|
4125
5830
|
`
|
|
@@ -4132,7 +5837,7 @@ export const config = envParser
|
|
|
4132
5837
|
path: getRoutePath("health.ts"),
|
|
4133
5838
|
content: `import { e } from '@geekmidas/constructs/endpoints';
|
|
4134
5839
|
|
|
4135
|
-
export
|
|
5840
|
+
export const healthEndpoint = e
|
|
4136
5841
|
.get('/health')
|
|
4137
5842
|
.handle(async () => ({
|
|
4138
5843
|
status: 'ok',
|
|
@@ -4149,15 +5854,44 @@ export type AppEvents =
|
|
|
4149
5854
|
| PublishableMessage<'user.created', { userId: string; email: string }>
|
|
4150
5855
|
| PublishableMessage<'user.updated', { userId: string; changes: Record<string, unknown> }>
|
|
4151
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>>;
|
|
4152
5885
|
`
|
|
4153
5886
|
},
|
|
4154
5887
|
{
|
|
4155
5888
|
path: "src/subscribers/user-events.ts",
|
|
4156
5889
|
content: `import { s } from '@geekmidas/constructs/subscribers';
|
|
4157
|
-
import
|
|
5890
|
+
import { eventsPublisherService } from '../events/publisher.js';
|
|
4158
5891
|
|
|
4159
|
-
export
|
|
4160
|
-
.
|
|
5892
|
+
export const userEventsSubscriber = s
|
|
5893
|
+
.publisher(eventsPublisherService)
|
|
5894
|
+
.subscribe(['user.created', 'user.updated'])
|
|
4161
5895
|
.handle(async ({ event, logger }) => {
|
|
4162
5896
|
logger.info({ type: event.type, payload: event.payload }, 'Processing user event');
|
|
4163
5897
|
|
|
@@ -4179,7 +5913,7 @@ export default s<AppEvents>()
|
|
|
4179
5913
|
content: `import { cron } from '@geekmidas/constructs/crons';
|
|
4180
5914
|
|
|
4181
5915
|
// Run every day at midnight
|
|
4182
|
-
export
|
|
5916
|
+
export const cleanupCron = cron('0 0 * * *')
|
|
4183
5917
|
.handle(async ({ logger }) => {
|
|
4184
5918
|
logger.info('Running cleanup job');
|
|
4185
5919
|
|
|
@@ -4222,30 +5956,17 @@ const templates = {
|
|
|
4222
5956
|
worker: workerTemplate
|
|
4223
5957
|
};
|
|
4224
5958
|
/**
|
|
4225
|
-
* Template choices for prompts
|
|
5959
|
+
* Template choices for prompts (Story 1.11 simplified to api + fullstack)
|
|
4226
5960
|
*/
|
|
4227
|
-
const templateChoices = [
|
|
4228
|
-
|
|
4229
|
-
|
|
4230
|
-
|
|
4231
|
-
|
|
4232
|
-
|
|
4233
|
-
|
|
4234
|
-
|
|
4235
|
-
|
|
4236
|
-
description: "Full API with auth, database, services"
|
|
4237
|
-
},
|
|
4238
|
-
{
|
|
4239
|
-
title: "Serverless",
|
|
4240
|
-
value: "serverless",
|
|
4241
|
-
description: "AWS Lambda handlers"
|
|
4242
|
-
},
|
|
4243
|
-
{
|
|
4244
|
-
title: "Worker",
|
|
4245
|
-
value: "worker",
|
|
4246
|
-
description: "Background job processing"
|
|
4247
|
-
}
|
|
4248
|
-
];
|
|
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
|
+
}];
|
|
4249
5970
|
/**
|
|
4250
5971
|
* Logger type choices for prompts
|
|
4251
5972
|
*/
|
|
@@ -4279,13 +6000,77 @@ const routesStructureChoices = [
|
|
|
4279
6000
|
}
|
|
4280
6001
|
];
|
|
4281
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
|
+
/**
|
|
4282
6060
|
* Get a template by name
|
|
4283
6061
|
*/
|
|
4284
6062
|
function getTemplate(name$1) {
|
|
6063
|
+
if (name$1 === "fullstack") return templates.api;
|
|
4285
6064
|
const template = templates[name$1];
|
|
4286
6065
|
if (!template) throw new Error(`Unknown template: ${name$1}`);
|
|
4287
6066
|
return template;
|
|
4288
6067
|
}
|
|
6068
|
+
/**
|
|
6069
|
+
* Check if a template is the fullstack monorepo template
|
|
6070
|
+
*/
|
|
6071
|
+
function isFullstackTemplate(name$1) {
|
|
6072
|
+
return name$1 === "fullstack";
|
|
6073
|
+
}
|
|
4289
6074
|
|
|
4290
6075
|
//#endregion
|
|
4291
6076
|
//#region src/init/generators/package.ts
|
|
@@ -4297,10 +6082,10 @@ function generatePackageJson(options, template) {
|
|
|
4297
6082
|
const dependencies$1 = { ...template.dependencies };
|
|
4298
6083
|
const devDependencies$1 = { ...template.devDependencies };
|
|
4299
6084
|
const scripts$1 = { ...template.scripts };
|
|
4300
|
-
if (telescope) dependencies$1["@geekmidas/telescope"] = "
|
|
4301
|
-
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"];
|
|
4302
6087
|
if (database) {
|
|
4303
|
-
dependencies$1["@geekmidas/db"] = "
|
|
6088
|
+
dependencies$1["@geekmidas/db"] = GEEKMIDAS_VERSIONS["@geekmidas/db"];
|
|
4304
6089
|
dependencies$1.kysely = "~0.28.2";
|
|
4305
6090
|
dependencies$1.pg = "~8.16.0";
|
|
4306
6091
|
devDependencies$1["@types/pg"] = "~8.15.0";
|
|
@@ -4335,19 +6120,219 @@ function generatePackageJson(options, template) {
|
|
|
4335
6120
|
dependencies: sortObject(dependencies$1),
|
|
4336
6121
|
devDependencies: sortObject(devDependencies$1)
|
|
4337
6122
|
};
|
|
4338
|
-
return [{
|
|
4339
|
-
path: "package.json",
|
|
4340
|
-
content: `${JSON.stringify(packageJson, null, 2)}\n`
|
|
4341
|
-
}];
|
|
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
|
+
);
|
|
4342
6233
|
}
|
|
6234
|
+
`;
|
|
6235
|
+
const pageTsx = `import type { User } from '${modelsPackage}';
|
|
4343
6236
|
|
|
4344
|
-
|
|
4345
|
-
|
|
4346
|
-
|
|
4347
|
-
|
|
4348
|
-
|
|
4349
|
-
|
|
4350
|
-
|
|
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
|
+
];
|
|
4351
6336
|
}
|
|
4352
6337
|
|
|
4353
6338
|
//#endregion
|
|
@@ -4415,21 +6400,36 @@ function getRunCommand(pkgManager, script) {
|
|
|
4415
6400
|
//#endregion
|
|
4416
6401
|
//#region src/init/index.ts
|
|
4417
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
|
+
/**
|
|
4418
6418
|
* Main init command - scaffolds a new project
|
|
4419
6419
|
*/
|
|
4420
6420
|
async function initCommand(projectName, options = {}) {
|
|
4421
6421
|
const cwd = process.cwd();
|
|
4422
|
-
const
|
|
6422
|
+
const detectedPkgManager = detectPackageManager(cwd);
|
|
4423
6423
|
prompts.override({});
|
|
4424
6424
|
const onCancel = () => {
|
|
4425
6425
|
process.exit(0);
|
|
4426
6426
|
};
|
|
4427
6427
|
const answers = await prompts([
|
|
4428
6428
|
{
|
|
4429
|
-
type: projectName ? null : "text",
|
|
6429
|
+
type: projectName || options.name ? null : "text",
|
|
4430
6430
|
name: "name",
|
|
4431
6431
|
message: "Project name:",
|
|
4432
|
-
initial: "my-
|
|
6432
|
+
initial: "my-app",
|
|
4433
6433
|
validate: (value) => {
|
|
4434
6434
|
const nameValid = validateProjectName(value);
|
|
4435
6435
|
if (nameValid !== true) return nameValid;
|
|
@@ -4446,21 +6446,33 @@ async function initCommand(projectName, options = {}) {
|
|
|
4446
6446
|
initial: 0
|
|
4447
6447
|
},
|
|
4448
6448
|
{
|
|
4449
|
-
type: options.yes ? null : "
|
|
4450
|
-
name: "
|
|
4451
|
-
message: "
|
|
4452
|
-
|
|
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"
|
|
4453
6457
|
},
|
|
4454
6458
|
{
|
|
4455
|
-
type: options.yes ? null : "
|
|
4456
|
-
name: "
|
|
4457
|
-
message: "
|
|
4458
|
-
|
|
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
|
|
4459
6471
|
},
|
|
4460
6472
|
{
|
|
4461
|
-
type:
|
|
4462
|
-
name: "
|
|
4463
|
-
message: "Include
|
|
6473
|
+
type: options.yes ? null : "confirm",
|
|
6474
|
+
name: "telescope",
|
|
6475
|
+
message: "Include Telescope (debugging dashboard)?",
|
|
4464
6476
|
initial: true
|
|
4465
6477
|
},
|
|
4466
6478
|
{
|
|
@@ -4476,74 +6488,146 @@ async function initCommand(projectName, options = {}) {
|
|
|
4476
6488
|
message: "Routes structure:",
|
|
4477
6489
|
choices: routesStructureChoices,
|
|
4478
6490
|
initial: 0
|
|
4479
|
-
},
|
|
4480
|
-
{
|
|
4481
|
-
type: options.yes || options.monorepo !== void 0 ? null : "confirm",
|
|
4482
|
-
name: "monorepo",
|
|
4483
|
-
message: "Setup as monorepo?",
|
|
4484
|
-
initial: false
|
|
4485
|
-
},
|
|
4486
|
-
{
|
|
4487
|
-
type: (prev) => (prev === true || options.monorepo) && !options.apiPath ? "text" : null,
|
|
4488
|
-
name: "apiPath",
|
|
4489
|
-
message: "API app path:",
|
|
4490
|
-
initial: "apps/api"
|
|
4491
6491
|
}
|
|
4492
6492
|
], { onCancel });
|
|
4493
|
-
const name$1 = projectName || answers.name;
|
|
4494
|
-
if (!name$1)
|
|
4495
|
-
|
|
4496
|
-
|
|
4497
|
-
|
|
4498
|
-
|
|
4499
|
-
|
|
4500
|
-
|
|
4501
|
-
|
|
4502
|
-
|
|
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;
|
|
4503
6527
|
const templateOptions = {
|
|
4504
6528
|
name: name$1,
|
|
4505
|
-
template
|
|
6529
|
+
template,
|
|
4506
6530
|
telescope: options.yes ? true : answers.telescope ?? true,
|
|
4507
6531
|
database,
|
|
4508
|
-
studio: database
|
|
6532
|
+
studio: database,
|
|
4509
6533
|
loggerType: options.yes ? "pino" : answers.loggerType ?? "pino",
|
|
4510
6534
|
routesStructure: options.yes ? "centralized-endpoints" : answers.routesStructure ?? "centralized-endpoints",
|
|
4511
6535
|
monorepo,
|
|
4512
|
-
apiPath: monorepo ? options.apiPath ??
|
|
6536
|
+
apiPath: monorepo ? options.apiPath ?? "apps/api" : "",
|
|
6537
|
+
packageManager: pkgManager,
|
|
6538
|
+
deployTarget,
|
|
6539
|
+
services
|
|
4513
6540
|
};
|
|
4514
6541
|
const targetDir = join(cwd, name$1);
|
|
4515
|
-
const
|
|
6542
|
+
const baseTemplate = getTemplate(templateOptions.template);
|
|
4516
6543
|
const isMonorepo$1 = templateOptions.monorepo;
|
|
4517
6544
|
const apiPath = templateOptions.apiPath;
|
|
6545
|
+
console.log("\n🚀 Creating your project...\n");
|
|
4518
6546
|
await mkdir(targetDir, { recursive: true });
|
|
4519
6547
|
const appDir = isMonorepo$1 ? join(targetDir, apiPath) : targetDir;
|
|
4520
6548
|
if (isMonorepo$1) await mkdir(appDir, { recursive: true });
|
|
4521
|
-
const
|
|
4522
|
-
|
|
4523
|
-
|
|
4524
|
-
|
|
4525
|
-
|
|
4526
|
-
|
|
4527
|
-
|
|
4528
|
-
|
|
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) : [];
|
|
4529
6568
|
for (const { path, content } of rootFiles) {
|
|
4530
6569
|
const fullPath = join(targetDir, path);
|
|
4531
6570
|
await mkdir(dirname(fullPath), { recursive: true });
|
|
4532
6571
|
await writeFile(fullPath, content);
|
|
4533
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
|
+
}
|
|
4534
6578
|
for (const { path, content } of appFiles) {
|
|
4535
6579
|
const fullPath = join(appDir, path);
|
|
4536
|
-
const _displayPath = isMonorepo$1 ? `${apiPath}/${path}` : path;
|
|
4537
6580
|
await mkdir(dirname(fullPath), { recursive: true });
|
|
4538
6581
|
await writeFile(fullPath, content);
|
|
4539
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`);
|
|
4540
6621
|
if (!options.skipInstall) {
|
|
6622
|
+
console.log("\n📦 Installing dependencies...\n");
|
|
4541
6623
|
try {
|
|
4542
6624
|
execSync(getInstallCommand(pkgManager), {
|
|
4543
6625
|
cwd: targetDir,
|
|
4544
6626
|
stdio: "inherit"
|
|
4545
6627
|
});
|
|
4546
|
-
} catch {
|
|
6628
|
+
} catch {
|
|
6629
|
+
console.error("Failed to install dependencies");
|
|
6630
|
+
}
|
|
4547
6631
|
try {
|
|
4548
6632
|
execSync("npx @biomejs/biome format --write --unsafe .", {
|
|
4549
6633
|
cwd: targetDir,
|
|
@@ -4551,124 +6635,52 @@ async function initCommand(projectName, options = {}) {
|
|
|
4551
6635
|
});
|
|
4552
6636
|
} catch {}
|
|
4553
6637
|
}
|
|
4554
|
-
|
|
6638
|
+
printNextSteps(name$1, templateOptions, pkgManager);
|
|
4555
6639
|
}
|
|
4556
|
-
|
|
4557
|
-
//#endregion
|
|
4558
|
-
//#region src/secrets/generator.ts
|
|
4559
6640
|
/**
|
|
4560
|
-
*
|
|
4561
|
-
* @param length Password length (default: 32)
|
|
6641
|
+
* Print success message with next steps
|
|
4562
6642
|
*/
|
|
4563
|
-
function
|
|
4564
|
-
|
|
4565
|
-
}
|
|
4566
|
-
|
|
4567
|
-
|
|
4568
|
-
|
|
4569
|
-
|
|
4570
|
-
|
|
4571
|
-
|
|
4572
|
-
|
|
4573
|
-
},
|
|
4574
|
-
redis: {
|
|
4575
|
-
host: "redis",
|
|
4576
|
-
port: 6379,
|
|
4577
|
-
username: "default"
|
|
4578
|
-
},
|
|
4579
|
-
rabbitmq: {
|
|
4580
|
-
host: "rabbitmq",
|
|
4581
|
-
port: 5672,
|
|
4582
|
-
username: "app",
|
|
4583
|
-
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`);
|
|
4584
6653
|
}
|
|
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
|
-
|
|
4612
|
-
|
|
4613
|
-
|
|
4614
|
-
|
|
4615
|
-
const { password, host, port } = creds;
|
|
4616
|
-
return `redis://:${encodeURIComponent(password)}@${host}:${port}`;
|
|
4617
|
-
}
|
|
4618
|
-
/**
|
|
4619
|
-
* Generate connection URL for RabbitMQ.
|
|
4620
|
-
*/
|
|
4621
|
-
function generateRabbitmqUrl(creds) {
|
|
4622
|
-
const { username, password, host, port, vhost } = creds;
|
|
4623
|
-
const encodedVhost = encodeURIComponent(vhost ?? "/");
|
|
4624
|
-
return `amqp://${username}:${encodeURIComponent(password)}@${host}:${port}/${encodedVhost}`;
|
|
4625
|
-
}
|
|
4626
|
-
/**
|
|
4627
|
-
* Generate connection URLs from service credentials.
|
|
4628
|
-
*/
|
|
4629
|
-
function generateConnectionUrls(services) {
|
|
4630
|
-
const urls = {};
|
|
4631
|
-
if (services.postgres) urls.DATABASE_URL = generatePostgresUrl(services.postgres);
|
|
4632
|
-
if (services.redis) urls.REDIS_URL = generateRedisUrl(services.redis);
|
|
4633
|
-
if (services.rabbitmq) urls.RABBITMQ_URL = generateRabbitmqUrl(services.rabbitmq);
|
|
4634
|
-
return urls;
|
|
4635
|
-
}
|
|
4636
|
-
/**
|
|
4637
|
-
* Create a new StageSecrets object with generated credentials.
|
|
4638
|
-
*/
|
|
4639
|
-
function createStageSecrets(stage, services) {
|
|
4640
|
-
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
4641
|
-
const serviceCredentials = generateServicesCredentials(services);
|
|
4642
|
-
const urls = generateConnectionUrls(serviceCredentials);
|
|
4643
|
-
return {
|
|
4644
|
-
stage,
|
|
4645
|
-
createdAt: now,
|
|
4646
|
-
updatedAt: now,
|
|
4647
|
-
services: serviceCredentials,
|
|
4648
|
-
urls,
|
|
4649
|
-
custom: {}
|
|
4650
|
-
};
|
|
4651
|
-
}
|
|
4652
|
-
/**
|
|
4653
|
-
* Rotate password for a specific service.
|
|
4654
|
-
*/
|
|
4655
|
-
function rotateServicePassword(secrets, service) {
|
|
4656
|
-
const currentCreds = secrets.services[service];
|
|
4657
|
-
if (!currentCreds) throw new Error(`Service "${service}" not configured in secrets`);
|
|
4658
|
-
const newCreds = {
|
|
4659
|
-
...currentCreds,
|
|
4660
|
-
password: generateSecurePassword()
|
|
4661
|
-
};
|
|
4662
|
-
const newServices = {
|
|
4663
|
-
...secrets.services,
|
|
4664
|
-
[service]: newCreds
|
|
4665
|
-
};
|
|
4666
|
-
return {
|
|
4667
|
-
...secrets,
|
|
4668
|
-
updatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
4669
|
-
services: newServices,
|
|
4670
|
-
urls: generateConnectionUrls(newServices)
|
|
4671
|
-
};
|
|
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("");
|
|
4672
6684
|
}
|
|
4673
6685
|
|
|
4674
6686
|
//#endregion
|
|
@@ -4857,11 +6869,57 @@ function maskUrl(url) {
|
|
|
4857
6869
|
}
|
|
4858
6870
|
}
|
|
4859
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
|
+
|
|
4860
6918
|
//#endregion
|
|
4861
6919
|
//#region src/index.ts
|
|
4862
6920
|
const program = new Command();
|
|
4863
6921
|
program.name("gkm").description("GeekMidas backend framework CLI").version(package_default.version).option("--cwd <path>", "Change working directory");
|
|
4864
|
-
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) => {
|
|
4865
6923
|
try {
|
|
4866
6924
|
const globalOptions = program.opts();
|
|
4867
6925
|
if (globalOptions.cwd) process.chdir(globalOptions.cwd);
|
|
@@ -4918,6 +6976,19 @@ program.command("dev").description("Start development server with automatic relo
|
|
|
4918
6976
|
process.exit(1);
|
|
4919
6977
|
}
|
|
4920
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
|
+
});
|
|
4921
6992
|
program.command("cron").description("Manage cron jobs").action(() => {
|
|
4922
6993
|
const globalOptions = program.opts();
|
|
4923
6994
|
if (globalOptions.cwd) process.chdir(globalOptions.cwd);
|