@geekmidas/cli 10.0.0-alpha.5 → 10.0.0-alpha.9
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/README.md +12 -8
- package/bin/adjacent-tsconfig.mjs +59 -0
- package/bin/gkm.mjs +7 -1
- package/dist/{config-C21ocYNd.cjs → config-BZ1F0WuA.cjs} +8 -81
- package/dist/config-BZ1F0WuA.cjs.map +1 -0
- package/dist/{config-WRdsQYZK.mjs → config-DVVph9yz.mjs} +10 -77
- package/dist/config-DVVph9yz.mjs.map +1 -0
- package/dist/config.cjs +2 -2
- package/dist/config.d.cts +2 -2
- package/dist/config.d.mts +2 -2
- package/dist/config.mjs +2 -2
- package/dist/{credentials-G1JwBoXH.cjs → credentials-Xamqe_NJ.cjs} +20 -19
- package/dist/credentials-Xamqe_NJ.cjs.map +1 -0
- package/dist/{credentials-BBIRJVp4.mjs → credentials-dP8vRVRv.mjs} +20 -19
- package/dist/credentials-dP8vRVRv.mjs.map +1 -0
- package/dist/discover-CAlFgYec.mjs.map +1 -1
- package/dist/discover-DTmFeloL.cjs.map +1 -1
- package/dist/{index-BThUzEIp.d.mts → index-C6e-ULLx.d.mts} +2 -2
- package/dist/index-C6e-ULLx.d.mts.map +1 -0
- package/dist/{index-CHB4tSzX.d.cts → index-CR5urhWX.d.cts} +2 -2
- package/dist/index-CR5urhWX.d.cts.map +1 -0
- package/dist/index.cjs +871 -337
- package/dist/index.cjs.map +1 -1
- package/dist/index.mjs +871 -337
- package/dist/index.mjs.map +1 -1
- package/dist/{openapi-CA0yJUTN.cjs → openapi-SjsgARie.cjs} +122 -50
- package/dist/openapi-SjsgARie.cjs.map +1 -0
- package/dist/{openapi-kkQbsRVj.mjs → openapi-Xx0A7Wg0.mjs} +104 -50
- package/dist/openapi-Xx0A7Wg0.mjs.map +1 -0
- package/dist/openapi.cjs +5 -3
- package/dist/openapi.d.cts +46 -10
- package/dist/openapi.d.cts.map +1 -1
- package/dist/openapi.d.mts +46 -10
- package/dist/openapi.d.mts.map +1 -1
- package/dist/openapi.mjs +4 -4
- package/dist/reconcile/public.d.cts +1 -1
- package/dist/reconcile/public.d.mts +1 -1
- package/dist/{setup-ZijS4qeC.mjs → setup-BA8ciTPU.mjs} +4 -4
- package/dist/setup-BsZvZP6f.cjs +11 -0
- package/dist/{setup-DXvRLTZN.mjs → setup-C8KOx8_8.mjs} +5 -5
- package/dist/setup-C8KOx8_8.mjs.map +1 -0
- package/dist/{setup-Cm2UnAdh.cjs → setup-hC6hI86f.cjs} +5 -5
- package/dist/setup-hC6hI86f.cjs.map +1 -0
- package/dist/{types-C-arnQ1r.d.mts → types-6pkp2RRj.d.mts} +46 -10
- package/dist/types-6pkp2RRj.d.mts.map +1 -0
- package/dist/{types-abJQuk7m.d.cts → types-Y3fKQSAK.d.cts} +46 -10
- package/dist/types-Y3fKQSAK.d.cts.map +1 -0
- package/dist/workspace/index.cjs +1 -1
- package/dist/workspace/index.d.cts +2 -2
- package/dist/workspace/index.d.mts +2 -2
- package/dist/workspace/index.mjs +1 -1
- package/dist/workspace-BzrG_2SI.mjs +8 -0
- package/dist/{workspace-BcucjwvA.cjs → workspace-C6VJQVtG.cjs} +79 -17
- package/dist/workspace-C6VJQVtG.cjs.map +1 -0
- package/dist/{workspace-DdJss8I6.cjs → workspace-XXtNhqJl.cjs} +3 -3
- package/dist/{workspace-BFMrAKqI.mjs → workspace-zoqRbihZ.mjs} +62 -18
- package/dist/workspace-zoqRbihZ.mjs.map +1 -0
- package/package.json +14 -13
- package/dist/config-C21ocYNd.cjs.map +0 -1
- package/dist/config-WRdsQYZK.mjs.map +0 -1
- package/dist/credentials-BBIRJVp4.mjs.map +0 -1
- package/dist/credentials-G1JwBoXH.cjs.map +0 -1
- package/dist/index-BThUzEIp.d.mts.map +0 -1
- package/dist/index-CHB4tSzX.d.cts.map +0 -1
- package/dist/openapi-CA0yJUTN.cjs.map +0 -1
- package/dist/openapi-kkQbsRVj.mjs.map +0 -1
- package/dist/setup-B5_oHHUj.cjs +0 -11
- package/dist/setup-Cm2UnAdh.cjs.map +0 -1
- package/dist/setup-DXvRLTZN.mjs.map +0 -1
- package/dist/types-C-arnQ1r.d.mts.map +0 -1
- package/dist/types-abJQuk7m.d.cts.map +0 -1
- package/dist/workspace-BFMrAKqI.mjs.map +0 -1
- package/dist/workspace-BcucjwvA.cjs.map +0 -1
- package/dist/workspace-k4l3rsFh.mjs +0 -8
package/dist/index.cjs
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
#!/usr/bin/env -S npx tsx
|
|
2
2
|
const require_chunk = require('./chunk-CUT6urMc.cjs');
|
|
3
3
|
const require_discover = require('./discover-DTmFeloL.cjs');
|
|
4
|
-
const require_config = require('./config-
|
|
5
|
-
const require_workspace = require('./workspace-
|
|
4
|
+
const require_config = require('./config-BZ1F0WuA.cjs');
|
|
5
|
+
const require_workspace = require('./workspace-C6VJQVtG.cjs');
|
|
6
6
|
const require_credentials = require('./credentials-BapzjyV5.cjs');
|
|
7
|
-
const require_credentials$1 = require('./credentials-
|
|
7
|
+
const require_credentials$1 = require('./credentials-Xamqe_NJ.cjs');
|
|
8
8
|
const require_generator = require('./generator-CNzBw7g5.cjs');
|
|
9
9
|
const require_storage = require('./storage-BJmzYxt8.cjs');
|
|
10
|
-
const require_openapi = require('./openapi-
|
|
10
|
+
const require_openapi = require('./openapi-SjsgARie.cjs');
|
|
11
11
|
const require_emit = require('./emit-CbcRolsa.cjs');
|
|
12
12
|
const require_dokploy_api = require('./dokploy-api-BsVH0y_s.cjs');
|
|
13
13
|
const require_encryption = require('./encryption-C6jaU0hE.cjs');
|
|
@@ -15,7 +15,7 @@ const require_CachedStateProvider = require('./CachedStateProvider-CqPdW7CB.cjs'
|
|
|
15
15
|
const require_fullstack_secrets = require('./fullstack-secrets-C-GGohke.cjs');
|
|
16
16
|
const require_openapi_react_query = require('./openapi-react-query-CmJ1xDbn.cjs');
|
|
17
17
|
require('./sync-SizaChmC.cjs');
|
|
18
|
-
const require_setup = require('./setup-
|
|
18
|
+
const require_setup = require('./setup-hC6hI86f.cjs');
|
|
19
19
|
const node_fs = require_chunk.__toESM(require("node:fs"));
|
|
20
20
|
const node_path = require_chunk.__toESM(require("node:path"));
|
|
21
21
|
const __geekmidas_manifest = require_chunk.__toESM(require("@geekmidas/manifest"));
|
|
@@ -43,7 +43,7 @@ const prompts = require_chunk.__toESM(require("prompts"));
|
|
|
43
43
|
|
|
44
44
|
//#region package.json
|
|
45
45
|
var name = "@geekmidas/cli";
|
|
46
|
-
var version = "10.0.0-alpha.
|
|
46
|
+
var version = "10.0.0-alpha.9";
|
|
47
47
|
var description = "CLI tools for building Lambda handlers, server applications, and generating OpenAPI specs";
|
|
48
48
|
var private$1 = false;
|
|
49
49
|
var type = "module";
|
|
@@ -143,6 +143,7 @@ var dependencies = {
|
|
|
143
143
|
"dotenv": "~18.0.1",
|
|
144
144
|
"esbuild": "~0.28.2",
|
|
145
145
|
"fast-glob": "~3.3.3",
|
|
146
|
+
"get-tsconfig": "~4.10.1",
|
|
146
147
|
"hono": "~4.13.8",
|
|
147
148
|
"lodash.kebabcase": "~4.1.1",
|
|
148
149
|
"openapi-typescript": "~7.13.0",
|
|
@@ -157,8 +158,8 @@ var devDependencies = {
|
|
|
157
158
|
"@types/node": "~24.9.1",
|
|
158
159
|
"@types/pg": "~8.23.1",
|
|
159
160
|
"@types/prompts": "~2.4.9",
|
|
160
|
-
"typescript": "
|
|
161
|
-
"vitest": "
|
|
161
|
+
"typescript": "~5.8.2",
|
|
162
|
+
"vitest": "~3.2.4",
|
|
162
163
|
"zod": "~4.6.5"
|
|
163
164
|
};
|
|
164
165
|
var peerDependencies = { "@geekmidas/telescope": "workspace:~" };
|
|
@@ -415,11 +416,16 @@ var CronGenerator = class extends require_discover.ConstructGenerator {
|
|
|
415
416
|
const provider = options?.provider || "aws-lambda";
|
|
416
417
|
const logger$12 = console;
|
|
417
418
|
const cronInfos = [];
|
|
419
|
+
if (provider === "server") {
|
|
420
|
+
await this.generateServerCronsFile(outputDir, constructs);
|
|
421
|
+
logger$12.log(`Generated server crons file with ${constructs.length} crons`);
|
|
422
|
+
return cronInfos;
|
|
423
|
+
}
|
|
418
424
|
if (constructs.length === 0 || provider !== "aws-lambda") return cronInfos;
|
|
419
425
|
const cronsDir = (0, node_path.join)(outputDir, "crons");
|
|
420
426
|
await (0, node_fs_promises.mkdir)(cronsDir, { recursive: true });
|
|
421
427
|
for (const { key, construct, path } of constructs) {
|
|
422
|
-
const handlerFile = await this.generateCronHandler(cronsDir, path.relative, key, context);
|
|
428
|
+
const handlerFile = await this.generateCronHandler(cronsDir, path.relative, key, context, construct.owner);
|
|
423
429
|
cronInfos.push({
|
|
424
430
|
name: key,
|
|
425
431
|
handler: (0, node_path.relative)(process.cwd(), handlerFile).replace(/\.ts$/, ".handler"),
|
|
@@ -436,17 +442,173 @@ var CronGenerator = class extends require_discover.ConstructGenerator {
|
|
|
436
442
|
isConstruct(value) {
|
|
437
443
|
return __geekmidas_constructs_crons.Cron.isCron(value);
|
|
438
444
|
}
|
|
439
|
-
|
|
445
|
+
/**
|
|
446
|
+
* The crons file a server entry imports.
|
|
447
|
+
*
|
|
448
|
+
* Shaped after `generateServerSubscribersFile`: one module, every cron, a
|
|
449
|
+
* `setupCrons` the generated entry calls beside `setupSubscribers`.
|
|
450
|
+
*
|
|
451
|
+
* It picks its scheduler from what the process can reach. With a Postgres it
|
|
452
|
+
* uses pg-boss, whose schedules live in the database — so a deployment
|
|
453
|
+
* running four replicas fires each job once, which is the thing a timer in
|
|
454
|
+
* every process gets wrong and does not report. With no database it falls
|
|
455
|
+
* back to timers and says plainly that it is doing so.
|
|
456
|
+
*/
|
|
457
|
+
async generateServerCronsFile(outputDir, crons) {
|
|
458
|
+
await (0, node_fs_promises.mkdir)(outputDir, { recursive: true });
|
|
459
|
+
const cronsPath = (0, node_path.join)(outputDir, "crons.ts");
|
|
460
|
+
const importsByFile = /* @__PURE__ */ new Map();
|
|
461
|
+
for (const { path, key } of crons) {
|
|
462
|
+
const relativePath = (0, node_path.relative)((0, node_path.dirname)(cronsPath), path.relative);
|
|
463
|
+
const importPath = relativePath.replace(/\.ts$/, ".js");
|
|
464
|
+
if (!importsByFile.has(importPath)) importsByFile.set(importPath, []);
|
|
465
|
+
importsByFile.get(importPath)?.push(key);
|
|
466
|
+
}
|
|
467
|
+
const imports = Array.from(importsByFile.entries()).map(([importPath, exports$2]) => `import { ${exports$2.join(", ")} } from '${importPath}';`).join("\n");
|
|
468
|
+
const entries = crons.map(({ key }) => ` { name: '${key}', cron: ${key} }`).join(",\n");
|
|
469
|
+
const content = `/**
|
|
470
|
+
* Generated cron setup.
|
|
471
|
+
*
|
|
472
|
+
* Schedules every cron this app declares, in the process that serves it.
|
|
473
|
+
*
|
|
474
|
+
* The schedule lives in Postgres, through pg-boss, in the database the worker
|
|
475
|
+
* named with \`.database(db)\`. That is what makes running several replicas
|
|
476
|
+
* safe: each job fires once, where a timer in every process fires it once per
|
|
477
|
+
* replica and reports nothing.
|
|
478
|
+
*
|
|
479
|
+
* No connection string appears here. The construct that owns the database is
|
|
480
|
+
* the only thing that knows its key, and it is resolved through service
|
|
481
|
+
* discovery like any other dependency.
|
|
482
|
+
*/
|
|
483
|
+
import type { EnvironmentParser } from '@geekmidas/envkit';
|
|
484
|
+
import type { Logger } from '@geekmidas/logger';
|
|
485
|
+
import { toCronExpression } from '@geekmidas/constructs/crons';
|
|
486
|
+
import { ServiceDiscovery } from '@geekmidas/services';
|
|
487
|
+
${imports}
|
|
488
|
+
|
|
489
|
+
const crons = [
|
|
490
|
+
${entries}
|
|
491
|
+
];
|
|
492
|
+
|
|
493
|
+
type Stoppable = { stop: () => Promise<void> | void };
|
|
494
|
+
const running: Stoppable[] = [];
|
|
495
|
+
|
|
496
|
+
export async function setupCrons(
|
|
497
|
+
envParser: EnvironmentParser<any>,
|
|
498
|
+
logger: Logger,
|
|
499
|
+
): Promise<void> {
|
|
500
|
+
if (crons.length === 0) {
|
|
501
|
+
return;
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
const serviceDiscovery = ServiceDiscovery.getInstance(envParser);
|
|
505
|
+
|
|
506
|
+
// Resolved once per cron rather than per firing: registering services on
|
|
507
|
+
// every tick would reconnect a database every minute.
|
|
508
|
+
const prepared = [];
|
|
509
|
+
for (const { name, cron } of crons) {
|
|
510
|
+
try {
|
|
511
|
+
const schedule = toCronExpression(cron.schedule);
|
|
512
|
+
const services = cron.services.length > 0
|
|
513
|
+
? await serviceDiscovery.register(cron.services)
|
|
514
|
+
: {};
|
|
515
|
+
|
|
516
|
+
prepared.push({ name, cron, schedule, services });
|
|
517
|
+
} catch (error) {
|
|
518
|
+
// An unrepresentable schedule is a build-time mistake, and failing the
|
|
519
|
+
// process for it would take the HTTP server down with it.
|
|
520
|
+
logger.error({ error, cron: name }, 'Cron has no runnable schedule, skipping');
|
|
521
|
+
}
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
const run = async (entry: (typeof prepared)[number]) => {
|
|
525
|
+
try {
|
|
526
|
+
await entry.cron.handler({
|
|
527
|
+
input: undefined,
|
|
528
|
+
services: entry.services,
|
|
529
|
+
logger: entry.cron.logger,
|
|
530
|
+
});
|
|
531
|
+
} catch (error) {
|
|
532
|
+
logger.error({ error, cron: entry.name }, 'Cron failed');
|
|
533
|
+
}
|
|
534
|
+
};
|
|
535
|
+
|
|
536
|
+
// Declared once on the worker, carried onto every cron it built — so this
|
|
537
|
+
// reads it from any of them rather than from config or the environment.
|
|
538
|
+
const scheduleStore = prepared.find(({ cron }) => cron.scheduleStore)?.cron
|
|
539
|
+
.scheduleStore;
|
|
540
|
+
|
|
541
|
+
if (!scheduleStore) {
|
|
542
|
+
// Declared, not discovered. A worker says where its schedules live with
|
|
543
|
+
// \`.database(db)\`; inferring one from whatever database happened to be
|
|
544
|
+
// around would move the schedules the day an app declared a second.
|
|
545
|
+
logger.error(
|
|
546
|
+
{ crons: prepared.map(({ name }) => name) },
|
|
547
|
+
'These crons have nowhere to keep their schedule. A server fires its own ' +
|
|
548
|
+
'crons, and a process scheduling in memory fires each job once per ' +
|
|
549
|
+
'replica — so the schedule lives in Postgres. Say where with ' +
|
|
550
|
+
'\`new Worker(…).database(db)\`.',
|
|
551
|
+
);
|
|
552
|
+
return;
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
const [store] = await serviceDiscovery
|
|
556
|
+
.register([scheduleStore])
|
|
557
|
+
.then((s) => Object.values(s));
|
|
558
|
+
|
|
559
|
+
const { PgBoss } = await import('pg-boss');
|
|
560
|
+
// The pool the construct already opened — no connection string is read here,
|
|
561
|
+
// and none is named anywhere outside the construct that owns it.
|
|
562
|
+
const boss = new PgBoss({ db: store as never });
|
|
563
|
+
await boss.start();
|
|
564
|
+
|
|
565
|
+
// What this app declares now. Anything else under the prefix belonged to a
|
|
566
|
+
// cron since renamed or deleted, and would keep firing at a handler that has
|
|
567
|
+
// gone.
|
|
568
|
+
//
|
|
569
|
+
// The prefix is \`cron.\` rather than \`cron:\`: pg-boss accepts
|
|
570
|
+
// alphanumerics, underscores, hyphens, periods and slashes in a queue name,
|
|
571
|
+
// and rejects a colon outright.
|
|
572
|
+
const declared = new Set(prepared.map(({ name }) => \`cron.\${name}\`));
|
|
573
|
+
for (const existing of await boss.getSchedules()) {
|
|
574
|
+
if (existing.name.startsWith('cron.') && !declared.has(existing.name)) {
|
|
575
|
+
await boss.unschedule(existing.name);
|
|
576
|
+
logger.info({ cron: existing.name }, 'Removed a schedule nothing declares');
|
|
577
|
+
}
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
for (const entry of prepared) {
|
|
581
|
+
const queue = \`cron.\${entry.name}\`;
|
|
582
|
+
await boss.createQueue(queue);
|
|
583
|
+
// UTC, so this agrees with the schedule a deploy target would have made.
|
|
584
|
+
await boss.schedule(queue, entry.schedule, undefined, { tz: 'UTC' });
|
|
585
|
+
await boss.work(queue, () => run(entry));
|
|
586
|
+
|
|
587
|
+
logger.info({ cron: entry.name, schedule: entry.schedule }, 'Cron scheduled');
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
running.push({ stop: () => boss.stop() });
|
|
591
|
+
logger.info({ count: prepared.length }, 'Crons scheduled');
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
export async function stopCrons(): Promise<void> {
|
|
595
|
+
await Promise.all(running.map((r) => r.stop()));
|
|
596
|
+
running.length = 0;
|
|
597
|
+
}
|
|
598
|
+
`;
|
|
599
|
+
await (0, node_fs_promises.writeFile)(cronsPath, content);
|
|
600
|
+
return cronsPath;
|
|
601
|
+
}
|
|
602
|
+
async generateCronHandler(outputDir, sourceFile, exportName, context, owner) {
|
|
440
603
|
const handlerFileName = `${exportName}.ts`;
|
|
441
604
|
const handlerPath = (0, node_path.join)(outputDir, handlerFileName);
|
|
442
605
|
const relativePath = (0, node_path.relative)((0, node_path.dirname)(handlerPath), sourceFile);
|
|
443
606
|
const importPath = relativePath.replace(/\.ts$/, ".js");
|
|
444
|
-
const
|
|
445
|
-
const relativeLoggerPath = (0, node_path.relative)((0, node_path.dirname)(handlerPath), context.loggerPath);
|
|
607
|
+
const runtime = require_openapi.runtimeFor(context, (0, node_path.dirname)(handlerPath), owner);
|
|
446
608
|
const content = `import { AWSScheduledFunction } from '@geekmidas/constructs/aws';
|
|
447
609
|
import { ${exportName} } from '${importPath}';
|
|
448
|
-
|
|
449
|
-
|
|
610
|
+
${runtime.imports}
|
|
611
|
+
${runtime.bindings}
|
|
450
612
|
|
|
451
613
|
const adapter = new AWSScheduledFunction(envParser, ${exportName});
|
|
452
614
|
|
|
@@ -562,7 +724,7 @@ var FunctionGenerator = class extends require_discover.ConstructGenerator {
|
|
|
562
724
|
const functionsDir = (0, node_path.join)(outputDir, "functions");
|
|
563
725
|
await (0, node_fs_promises.mkdir)(functionsDir, { recursive: true });
|
|
564
726
|
for (const { key, construct, path } of constructs) {
|
|
565
|
-
const handlerFile = await this.generateFunctionHandler(functionsDir, path.relative, key, context);
|
|
727
|
+
const handlerFile = await this.generateFunctionHandler(functionsDir, path.relative, key, context, construct.owner);
|
|
566
728
|
functionInfos.push({
|
|
567
729
|
name: key,
|
|
568
730
|
handler: (0, node_path.relative)(process.cwd(), handlerFile).replace(/\.ts$/, ".handler"),
|
|
@@ -575,17 +737,16 @@ var FunctionGenerator = class extends require_discover.ConstructGenerator {
|
|
|
575
737
|
}
|
|
576
738
|
return functionInfos;
|
|
577
739
|
}
|
|
578
|
-
async generateFunctionHandler(outputDir, sourceFile, exportName, context) {
|
|
740
|
+
async generateFunctionHandler(outputDir, sourceFile, exportName, context, owner) {
|
|
579
741
|
const handlerFileName = `${exportName}.ts`;
|
|
580
742
|
const handlerPath = (0, node_path.join)(outputDir, handlerFileName);
|
|
581
743
|
const relativePath = (0, node_path.relative)((0, node_path.dirname)(handlerPath), sourceFile);
|
|
582
744
|
const importPath = relativePath.replace(/\.ts$/, ".js");
|
|
583
|
-
const
|
|
584
|
-
const relativeLoggerPath = (0, node_path.relative)((0, node_path.dirname)(handlerPath), context.loggerPath);
|
|
745
|
+
const runtime = require_openapi.runtimeFor(context, (0, node_path.dirname)(handlerPath), owner);
|
|
585
746
|
const content = `import { AWSLambdaFunction } from '@geekmidas/constructs/aws';
|
|
586
747
|
import { ${exportName} } from '${importPath}';
|
|
587
|
-
|
|
588
|
-
|
|
748
|
+
${runtime.imports}
|
|
749
|
+
${runtime.bindings}
|
|
589
750
|
|
|
590
751
|
const adapter = new AWSLambdaFunction(envParser, ${exportName});
|
|
591
752
|
|
|
@@ -624,7 +785,7 @@ var QueueGenerator = class extends require_discover.ConstructGenerator {
|
|
|
624
785
|
const queuesDir = (0, node_path.join)(outputDir, "queues");
|
|
625
786
|
await (0, node_fs_promises.mkdir)(queuesDir, { recursive: true });
|
|
626
787
|
for (const { key, construct, path } of constructs) {
|
|
627
|
-
const handlerFile = await this.generateQueueHandler(queuesDir, path.relative, key, context);
|
|
788
|
+
const handlerFile = await this.generateQueueHandler(queuesDir, path.relative, key, context, construct.owner);
|
|
628
789
|
queueInfos.push({
|
|
629
790
|
name: construct.name,
|
|
630
791
|
handler: (0, node_path.relative)(process.cwd(), handlerFile).replace(/\.ts$/, ".handler"),
|
|
@@ -638,13 +799,14 @@ var QueueGenerator = class extends require_discover.ConstructGenerator {
|
|
|
638
799
|
}
|
|
639
800
|
return queueInfos;
|
|
640
801
|
}
|
|
641
|
-
async generateQueueHandler(outputDir, sourceFile, exportName, context) {
|
|
802
|
+
async generateQueueHandler(outputDir, sourceFile, exportName, context, owner) {
|
|
642
803
|
const handlerPath = (0, node_path.join)(outputDir, `${exportName}.ts`);
|
|
643
804
|
const importPath = (0, node_path.relative)((0, node_path.dirname)(handlerPath), sourceFile).replace(/\.ts$/, ".js");
|
|
644
|
-
const
|
|
805
|
+
const runtime = require_openapi.runtimeFor(context, (0, node_path.dirname)(handlerPath), owner);
|
|
645
806
|
const content = `import { AWSLambdaQueue } from '@geekmidas/constructs/aws';
|
|
646
807
|
import { ${exportName} } from '${importPath}';
|
|
647
|
-
|
|
808
|
+
${runtime.imports}
|
|
809
|
+
${runtime.bindings}
|
|
648
810
|
|
|
649
811
|
const adapter = new AWSLambdaQueue(envParser, ${exportName});
|
|
650
812
|
|
|
@@ -787,7 +949,7 @@ var SubscriberGenerator = class extends require_discover.ConstructGenerator {
|
|
|
787
949
|
const subscribersDir = (0, node_path.join)(outputDir, "subscribers");
|
|
788
950
|
await (0, node_fs_promises.mkdir)(subscribersDir, { recursive: true });
|
|
789
951
|
for (const { key, construct, path } of constructs) {
|
|
790
|
-
const handlerFile = await this.generateSubscriberHandler(subscribersDir, path.relative, key, construct, context);
|
|
952
|
+
const handlerFile = await this.generateSubscriberHandler(subscribersDir, path.relative, key, construct, context, construct.owner);
|
|
791
953
|
subscriberInfos.push({
|
|
792
954
|
name: key,
|
|
793
955
|
handler: (0, node_path.relative)(process.cwd(), handlerFile).replace(/\.ts$/, ".handler"),
|
|
@@ -805,15 +967,16 @@ var SubscriberGenerator = class extends require_discover.ConstructGenerator {
|
|
|
805
967
|
}
|
|
806
968
|
return subscriberInfos;
|
|
807
969
|
}
|
|
808
|
-
async generateSubscriberHandler(outputDir, sourceFile, exportName, _subscriber, context) {
|
|
970
|
+
async generateSubscriberHandler(outputDir, sourceFile, exportName, _subscriber, context, owner) {
|
|
809
971
|
const handlerFileName = `${exportName}.ts`;
|
|
810
972
|
const handlerPath = (0, node_path.join)(outputDir, handlerFileName);
|
|
811
973
|
const relativePath = (0, node_path.relative)((0, node_path.dirname)(handlerPath), sourceFile);
|
|
812
974
|
const importPath = relativePath.replace(/\.ts$/, ".js");
|
|
813
|
-
const
|
|
975
|
+
const runtime = require_openapi.runtimeFor(context, (0, node_path.dirname)(handlerPath), owner);
|
|
814
976
|
const content = `import { AWSLambdaSubscriber } from '@geekmidas/constructs/aws';
|
|
815
977
|
import { ${exportName} } from '${importPath}';
|
|
816
|
-
|
|
978
|
+
${runtime.imports}
|
|
979
|
+
${runtime.bindings}
|
|
817
980
|
|
|
818
981
|
const adapter = new AWSLambdaSubscriber(envParser, ${exportName});
|
|
819
982
|
|
|
@@ -1196,13 +1359,7 @@ async function devCommand(options) {
|
|
|
1196
1359
|
}
|
|
1197
1360
|
const resolved = resolveProviders(config, { provider: "server" });
|
|
1198
1361
|
logger$9.log("🚀 Starting development server...");
|
|
1199
|
-
logger$9.log(`Loading
|
|
1200
|
-
if (config.functions) logger$9.log(`Loading functions from: ${config.functions}`);
|
|
1201
|
-
if (config.crons) logger$9.log(`Loading crons from: ${config.crons}`);
|
|
1202
|
-
if (config.subscribers) logger$9.log(`Loading subscribers from: ${config.subscribers}`);
|
|
1203
|
-
logger$9.log(`Using envParser: ${config.envParser}`);
|
|
1204
|
-
const { path: envParserPath, importPattern: envParserImportPattern } = require_config.parseModuleConfig(config.envParser, "envParser");
|
|
1205
|
-
const { path: loggerPath, importPattern: loggerImportPattern } = require_config.parseModuleConfig(config.logger, "logger");
|
|
1362
|
+
logger$9.log(`Loading constructs from: ${config.constructs}`);
|
|
1206
1363
|
const telescope = normalizeTelescopeConfig(config.telescope);
|
|
1207
1364
|
if (telescope) logger$9.log(`🔭 Telescope enabled at ${telescope.path}`);
|
|
1208
1365
|
const studio = normalizeStudioConfig(config.studio);
|
|
@@ -1213,21 +1370,17 @@ async function devCommand(options) {
|
|
|
1213
1370
|
const enableOpenApi = openApiConfig.enabled || resolved.enableOpenApi;
|
|
1214
1371
|
if (enableOpenApi) logger$9.log(`📄 OpenAPI output: ${require_openapi.OPENAPI_OUTPUT_PATH}`);
|
|
1215
1372
|
const buildContext = {
|
|
1216
|
-
envParserPath,
|
|
1217
|
-
envParserImportPattern,
|
|
1218
|
-
loggerPath,
|
|
1219
|
-
loggerImportPattern,
|
|
1220
1373
|
telescope,
|
|
1221
1374
|
studio,
|
|
1222
1375
|
hooks
|
|
1223
1376
|
};
|
|
1224
1377
|
await buildServer(config, buildContext, resolved.providers[0], enableOpenApi, appRoot);
|
|
1225
|
-
if (enableOpenApi) await require_openapi.
|
|
1378
|
+
if (enableOpenApi) await require_openapi.generateOpenApiFrom(config.constructs, { openapi: config.openapi });
|
|
1226
1379
|
const runtime = config.runtime ?? "node";
|
|
1227
1380
|
let secretsJsonPath;
|
|
1228
|
-
const appSecrets = await require_credentials$1.loadSecretsForApp(secretsRoot, workspaceAppName);
|
|
1381
|
+
const appSecrets = await require_credentials$1.loadSecretsForApp(secretsRoot, config.stages.local, workspaceAppName);
|
|
1229
1382
|
if (workspace && require_credentials$1.usesConstructs(workspace)) {
|
|
1230
|
-
const reconciled = await require_credentials$1.reconcileWorkspace(workspace, { stage:
|
|
1383
|
+
const reconciled = await require_credentials$1.reconcileWorkspace(workspace, { stage: workspace.stages.local });
|
|
1231
1384
|
if (reconciled.changed && reconciled.plan.containers.length > 0) {
|
|
1232
1385
|
logger$9.log(`🐳 Services: ${reconciled.plan.containers.join(", ")}`);
|
|
1233
1386
|
for (const [container, address] of Object.entries(reconciled.addresses)) logger$9.log(` ${container}: ${address}`);
|
|
@@ -1255,19 +1408,9 @@ async function devCommand(options) {
|
|
|
1255
1408
|
}
|
|
1256
1409
|
const devServer = new DevServer(resolved.providers[0], options.port ?? workspaceAppPort ?? 3e3, options.portExplicit ?? false, enableOpenApi, telescope, studio, runtime, appRoot, secretsJsonPath);
|
|
1257
1410
|
await devServer.start();
|
|
1258
|
-
const envParserFile = config.envParser.split("#")[0] ?? config.envParser;
|
|
1259
|
-
const loggerFile = config.logger.split("#")[0] ?? config.logger;
|
|
1260
1411
|
const hooksFileParts = config.hooks?.server?.split("#");
|
|
1261
1412
|
const hooksFile = hooksFileParts?.[0];
|
|
1262
|
-
const watchPatterns = [
|
|
1263
|
-
config.routes,
|
|
1264
|
-
...config.functions ? [config.functions] : [],
|
|
1265
|
-
...config.crons ? [config.crons] : [],
|
|
1266
|
-
...config.subscribers ? [config.subscribers] : [],
|
|
1267
|
-
envParserFile.endsWith(".ts") ? envParserFile : `${envParserFile}.ts`,
|
|
1268
|
-
loggerFile.endsWith(".ts") ? loggerFile : `${loggerFile}.ts`,
|
|
1269
|
-
...hooksFile ? [hooksFile.endsWith(".ts") ? hooksFile : `${hooksFile}.ts`] : []
|
|
1270
|
-
].flat().filter((p) => typeof p === "string");
|
|
1413
|
+
const watchPatterns = [config.constructs, ...hooksFile ? [hooksFile.endsWith(".ts") ? hooksFile : `${hooksFile}.ts`] : []].flat().filter((p) => typeof p === "string");
|
|
1271
1414
|
const normalizedPatterns = watchPatterns.map((p) => p.startsWith("./") ? p.slice(2) : p);
|
|
1272
1415
|
logger$9.log(`👀 Watching for changes in: ${normalizedPatterns.join(", ")}`);
|
|
1273
1416
|
const resolvedFiles = await (0, fast_glob.default)(normalizedPatterns, {
|
|
@@ -1300,7 +1443,8 @@ async function devCommand(options) {
|
|
|
1300
1443
|
try {
|
|
1301
1444
|
logger$9.log("🔄 Rebuilding...");
|
|
1302
1445
|
await buildServer(config, buildContext, resolved.providers[0], enableOpenApi, appRoot, true);
|
|
1303
|
-
if (enableOpenApi) await require_openapi.
|
|
1446
|
+
if (enableOpenApi) await require_openapi.generateOpenApiFrom(config.constructs, {
|
|
1447
|
+
openapi: config.openapi,
|
|
1304
1448
|
silent: true,
|
|
1305
1449
|
bustCache: true
|
|
1306
1450
|
});
|
|
@@ -1518,15 +1662,15 @@ async function validateFrontendApps(workspace) {
|
|
|
1518
1662
|
*/
|
|
1519
1663
|
async function loadDevSecrets(workspace) {
|
|
1520
1664
|
if (!workspace.secrets.enabled) return {};
|
|
1521
|
-
const
|
|
1522
|
-
|
|
1665
|
+
const stage = workspace.stages.local;
|
|
1666
|
+
if (require_storage.secretsExist(stage, workspace.root)) {
|
|
1523
1667
|
const secrets = await require_storage.readStageSecrets(stage, workspace.root);
|
|
1524
1668
|
if (secrets) {
|
|
1525
1669
|
logger$9.log(`🔐 Loading secrets from stage: ${stage}`);
|
|
1526
1670
|
return require_storage.toEmbeddableSecrets(secrets);
|
|
1527
1671
|
}
|
|
1528
1672
|
}
|
|
1529
|
-
logger$9.warn(
|
|
1673
|
+
logger$9.warn(`⚠️ Secrets enabled but no "${stage}" secrets found. Run "gkm setup" to initialize the local stage`);
|
|
1530
1674
|
return {};
|
|
1531
1675
|
}
|
|
1532
1676
|
/**
|
|
@@ -1570,7 +1714,7 @@ async function workspaceDevCommand(workspace, options) {
|
|
|
1570
1714
|
const rawSecrets = await loadDevSecrets(workspace);
|
|
1571
1715
|
let secretsEnv;
|
|
1572
1716
|
if (require_credentials$1.usesConstructs(workspace)) {
|
|
1573
|
-
const reconciled = await require_credentials$1.reconcileWorkspace(workspace, { stage:
|
|
1717
|
+
const reconciled = await require_credentials$1.reconcileWorkspace(workspace, { stage: workspace.stages.local });
|
|
1574
1718
|
if (reconciled.changed && reconciled.plan.containers.length > 0) {
|
|
1575
1719
|
logger$9.log(`🐳 Services: ${reconciled.plan.containers.join(", ")}`);
|
|
1576
1720
|
for (const [container, address] of Object.entries(reconciled.addresses)) logger$9.log(` ${container}: ${address}`);
|
|
@@ -1714,7 +1858,7 @@ async function buildServer(config, context, provider, enableOpenApi, appRoot = p
|
|
|
1714
1858
|
const queueGenerator = new QueueGenerator();
|
|
1715
1859
|
const topicGenerator = new TopicGenerator();
|
|
1716
1860
|
const [allEndpoints, allFunctions, allCrons, allSubscribers, allQueues, allTopics] = await Promise.all([
|
|
1717
|
-
endpointGenerator.load(config.
|
|
1861
|
+
endpointGenerator.load(config.constructs, appRoot, bustCache),
|
|
1718
1862
|
config.functions ? functionGenerator.load(config.functions, appRoot, bustCache) : [],
|
|
1719
1863
|
config.crons ? cronGenerator.load(config.crons, appRoot, bustCache) : [],
|
|
1720
1864
|
config.subscribers ? subscriberGenerator.load(config.subscribers, appRoot, bustCache) : [],
|
|
@@ -2302,18 +2446,36 @@ export default app;
|
|
|
2302
2446
|
`);
|
|
2303
2447
|
}
|
|
2304
2448
|
const logger$7 = console;
|
|
2449
|
+
/**
|
|
2450
|
+
* What a build run at the workspace root reads.
|
|
2451
|
+
*
|
|
2452
|
+
* One backend app there, and it is that app's config, so its own `openapi`
|
|
2453
|
+
* and `telescope` settings still apply. Otherwise it is the workspace's: every
|
|
2454
|
+
* construct glob, and the backends they resolve to.
|
|
2455
|
+
*/
|
|
2456
|
+
function rootGkmConfig(workspace) {
|
|
2457
|
+
const backends = Object.entries(workspace.apps).filter(([, app]) => app.type === "backend");
|
|
2458
|
+
const only = backends.length === 1 ? backends[0] : void 0;
|
|
2459
|
+
const own = only ? require_workspace.getAppGkmConfig(workspace, only[0]) : void 0;
|
|
2460
|
+
return own ?? {
|
|
2461
|
+
services: workspace.services,
|
|
2462
|
+
stages: workspace.stages,
|
|
2463
|
+
constructs: require_workspace.allConstructGlobs(workspace)
|
|
2464
|
+
};
|
|
2465
|
+
}
|
|
2305
2466
|
async function buildCommand(options) {
|
|
2306
2467
|
const loadedConfig = await require_config.loadWorkspaceConfig();
|
|
2307
2468
|
if (loadedConfig.type === "workspace") {
|
|
2308
2469
|
const cwd = (0, node_path.resolve)(process.cwd());
|
|
2309
|
-
const workspaceRoot = (0, node_path.resolve)(loadedConfig.workspace.root);
|
|
2310
|
-
const isAtWorkspaceRoot = cwd === workspaceRoot;
|
|
2311
|
-
|
|
2470
|
+
const workspaceRoot$1 = (0, node_path.resolve)(loadedConfig.workspace.root);
|
|
2471
|
+
const isAtWorkspaceRoot = cwd === workspaceRoot$1;
|
|
2472
|
+
const someAppIsElsewhere = Object.values(loadedConfig.workspace.apps).some((app) => (0, node_path.resolve)(workspaceRoot$1, app.path) !== workspaceRoot$1);
|
|
2473
|
+
if (isAtWorkspaceRoot && someAppIsElsewhere) {
|
|
2312
2474
|
logger$7.log("📦 Detected workspace configuration");
|
|
2313
2475
|
return workspaceBuildCommand(loadedConfig.workspace, options);
|
|
2314
2476
|
}
|
|
2315
2477
|
}
|
|
2316
|
-
const config = loadedConfig.type
|
|
2478
|
+
const config = loadedConfig.type !== "workspace" ? await require_config.loadConfig() : (0, node_path.resolve)(process.cwd()) === (0, node_path.resolve)(loadedConfig.workspace.root) ? rootGkmConfig(loadedConfig.workspace) : (await require_config.loadAppConfig()).gkmConfig;
|
|
2317
2479
|
const resolved = resolveProviders(config, options);
|
|
2318
2480
|
const backendConfig = config.services ?? loadedConfig.workspace?.services;
|
|
2319
2481
|
const cacheBackend = require_credentials$1.cacheBackendOf(backendConfig?.cache);
|
|
@@ -2321,13 +2483,7 @@ async function buildCommand(options) {
|
|
|
2321
2483
|
const production = normalizeProductionConfig(options.production ?? false, productionConfigFromGkm);
|
|
2322
2484
|
if (production) logger$7.log(`🏭 Building for PRODUCTION`);
|
|
2323
2485
|
logger$7.log(`Building with providers: ${resolved.providers.join(", ")}`);
|
|
2324
|
-
logger$7.log(`Loading
|
|
2325
|
-
if (config.functions) logger$7.log(`Loading functions from: ${formatRoutes(config.functions)}`);
|
|
2326
|
-
if (config.crons) logger$7.log(`Loading crons from: ${formatRoutes(config.crons)}`);
|
|
2327
|
-
if (config.subscribers) logger$7.log(`Loading subscribers from: ${formatRoutes(config.subscribers)}`);
|
|
2328
|
-
logger$7.log(`Using envParser: ${config.envParser}`);
|
|
2329
|
-
const { path: envParserPath, importPattern: envParserImportPattern } = require_config.parseModuleConfig(config.envParser, "envParser");
|
|
2330
|
-
const { path: loggerPath, importPattern: loggerImportPattern } = require_config.parseModuleConfig(config.logger, "logger");
|
|
2486
|
+
logger$7.log(`Loading constructs from: ${formatRoutes(config.constructs)}`);
|
|
2331
2487
|
const telescope = production ? void 0 : normalizeTelescopeConfig(config.telescope);
|
|
2332
2488
|
if (telescope) logger$7.log(`🔭 Telescope enabled at ${telescope.path}`);
|
|
2333
2489
|
const studio = production ? void 0 : normalizeStudioConfig(config.studio);
|
|
@@ -2352,10 +2508,23 @@ async function buildCommand(options) {
|
|
|
2352
2508
|
sources: constructSources
|
|
2353
2509
|
}) : {};
|
|
2354
2510
|
const surfaces = Object.values(declared).filter((d) => d.kind === "rest-api");
|
|
2355
|
-
const
|
|
2511
|
+
const workspaceRoot = loadedConfig.workspace.root;
|
|
2512
|
+
const served = surfaces.find((d) => (0, node_path.resolve)(workspaceRoot, d.path) === (0, node_path.resolve)(process.cwd()));
|
|
2513
|
+
const primary = served ?? surfaces.find((d) => d.endpoints.length === 0) ?? surfaces[0];
|
|
2356
2514
|
const browsable = Object.entries(declared).find(([, d]) => d.kind === "database");
|
|
2357
2515
|
const browsableSource = browsable ? constructSources[browsable[0]] : void 0;
|
|
2516
|
+
const owners = {};
|
|
2517
|
+
for (const [id, declaration] of Object.entries(declared)) {
|
|
2518
|
+
if (declaration.kind !== "rest-api" && declaration.kind !== "worker") continue;
|
|
2519
|
+
const source = constructSources[id];
|
|
2520
|
+
if (!source) continue;
|
|
2521
|
+
owners[id] = {
|
|
2522
|
+
specifier: source.file,
|
|
2523
|
+
exportName: source.exportName
|
|
2524
|
+
};
|
|
2525
|
+
}
|
|
2358
2526
|
const buildContext = {
|
|
2527
|
+
owners,
|
|
2359
2528
|
...primary ? { surface: {
|
|
2360
2529
|
id: primary.id,
|
|
2361
2530
|
trustedOriginsKey: (0, __geekmidas_manifest.provideKey)(primary.id, "trustedOrigins"),
|
|
@@ -2365,10 +2534,6 @@ async function buildCommand(options) {
|
|
|
2365
2534
|
exportName: constructSources[primary.id].exportName
|
|
2366
2535
|
} } : {}
|
|
2367
2536
|
} } : {},
|
|
2368
|
-
envParserPath,
|
|
2369
|
-
envParserImportPattern,
|
|
2370
|
-
loggerPath,
|
|
2371
|
-
loggerImportPattern,
|
|
2372
2537
|
telescope,
|
|
2373
2538
|
studio: studio && browsableSource ? {
|
|
2374
2539
|
...studio,
|
|
@@ -2395,14 +2560,16 @@ async function buildCommand(options) {
|
|
|
2395
2560
|
const subscriberGenerator = new SubscriberGenerator();
|
|
2396
2561
|
const queueGenerator = new QueueGenerator();
|
|
2397
2562
|
const topicGenerator = new TopicGenerator();
|
|
2398
|
-
const
|
|
2399
|
-
|
|
2400
|
-
|
|
2401
|
-
|
|
2402
|
-
|
|
2403
|
-
|
|
2404
|
-
|
|
2563
|
+
const code = constructGlobs$1;
|
|
2564
|
+
const [loadedEndpoints, allFunctions, allCrons, allSubscribers, allQueues, allTopics] = await Promise.all([
|
|
2565
|
+
endpointGenerator.load(code),
|
|
2566
|
+
functionGenerator.load(code),
|
|
2567
|
+
cronGenerator.load(code),
|
|
2568
|
+
subscriberGenerator.load(code),
|
|
2569
|
+
queueGenerator.load(code),
|
|
2570
|
+
topicGenerator.load(code)
|
|
2405
2571
|
]);
|
|
2572
|
+
const allEndpoints = primary ? loadedEndpoints.filter(({ construct }) => !construct.surface || construct.surface.id === primary.id) : loadedEndpoints;
|
|
2406
2573
|
logger$7.log(`Found ${allEndpoints.length} endpoints`);
|
|
2407
2574
|
logger$7.log(`Found ${allFunctions.length} functions`);
|
|
2408
2575
|
logger$7.log(`Found ${allCrons.length} crons`);
|
|
@@ -2410,7 +2577,7 @@ async function buildCommand(options) {
|
|
|
2410
2577
|
logger$7.log(`Found ${allQueues.length} queues`);
|
|
2411
2578
|
logger$7.log(`Found ${allTopics.length} topics`);
|
|
2412
2579
|
if (allEndpoints.length === 0 && allFunctions.length === 0 && allCrons.length === 0 && allSubscribers.length === 0 && allQueues.length === 0 && allTopics.length === 0) {
|
|
2413
|
-
const selfServing =
|
|
2580
|
+
const selfServing = served && served.endpoints.length > 0 && constructSources[served.id] !== void 0 ? [served.id, served] : void 0;
|
|
2414
2581
|
if (selfServing) {
|
|
2415
2582
|
const [id] = selfServing;
|
|
2416
2583
|
await writeSurfaceEntry((0, node_path.join)(process.cwd(), ".gkm", "server"), constructSources[id]);
|
|
@@ -2427,7 +2594,7 @@ async function buildCommand(options) {
|
|
|
2427
2594
|
const providerResult = await buildForProvider(provider, buildContext, rootOutputDir, endpointGenerator, functionGenerator, cronGenerator, subscriberGenerator, queueGenerator, topicGenerator, allEndpoints, allFunctions, allCrons, allSubscribers, allQueues, allTopics, resolved.enableOpenApi, options.skipBundle ?? false, options.stage);
|
|
2428
2595
|
if (providerResult.masterKey) result = providerResult;
|
|
2429
2596
|
}
|
|
2430
|
-
await require_openapi.generateOpenApi(
|
|
2597
|
+
await require_openapi.generateOpenApi(allEndpoints.map(({ construct }) => construct), { openapi: config.openapi });
|
|
2431
2598
|
return result;
|
|
2432
2599
|
}
|
|
2433
2600
|
async function buildForProvider(provider, context, rootOutputDir, endpointGenerator, functionGenerator, cronGenerator, subscriberGenerator, queueGenerator, topicGenerator, endpoints, functions, crons, subscribers, queues, topics, enableOpenApi, skipBundle, stage) {
|
|
@@ -2681,7 +2848,6 @@ async function workspaceBuildCommand(workspace, options) {
|
|
|
2681
2848
|
});
|
|
2682
2849
|
}
|
|
2683
2850
|
logger$7.log(`\n✅ Workspace build complete!`);
|
|
2684
|
-
await require_openapi.openapiCommand({ cwd: workspace.root });
|
|
2685
2851
|
logger$7.log(`\n📋 Build Summary:`);
|
|
2686
2852
|
for (const result of results) {
|
|
2687
2853
|
const icon = result.type === "backend" ? "⚙️" : result.type === "mobile" ? "📱" : "🌐";
|
|
@@ -3051,7 +3217,7 @@ function minioCompose(options) {
|
|
|
3051
3217
|
return [
|
|
3052
3218
|
"services:",
|
|
3053
3219
|
` ${service}:`,
|
|
3054
|
-
" image:
|
|
3220
|
+
" image: pgsty/minio:RELEASE.2026-08-04T00-00-00Z",
|
|
3055
3221
|
` command: server /data --console-address ":9001"`,
|
|
3056
3222
|
" environment:",
|
|
3057
3223
|
` MINIO_ROOT_USER: ${user}`,
|
|
@@ -3870,7 +4036,7 @@ const DEFAULT_SERVICE_IMAGES = {
|
|
|
3870
4036
|
postgres: "postgres",
|
|
3871
4037
|
redis: "redis",
|
|
3872
4038
|
rabbitmq: "rabbitmq",
|
|
3873
|
-
minio: "
|
|
4039
|
+
minio: "pgsty/minio",
|
|
3874
4040
|
mailpit: "axllent/mailpit",
|
|
3875
4041
|
localstack: "floci/floci"
|
|
3876
4042
|
};
|
|
@@ -3879,7 +4045,7 @@ const DEFAULT_SERVICE_VERSIONS = {
|
|
|
3879
4045
|
postgres: `${__geekmidas_manifest.DEFAULT_POSTGRES_VERSION}-alpine`,
|
|
3880
4046
|
redis: "7-alpine",
|
|
3881
4047
|
rabbitmq: "3-management-alpine",
|
|
3882
|
-
minio: "RELEASE.
|
|
4048
|
+
minio: "RELEASE.2026-08-04T00-00-00Z",
|
|
3883
4049
|
mailpit: "latest",
|
|
3884
4050
|
localstack: "latest"
|
|
3885
4051
|
};
|
|
@@ -4331,7 +4497,7 @@ function getInfraServiceImage(serviceName$1, pin) {
|
|
|
4331
4497
|
const defaults = {
|
|
4332
4498
|
postgres: "postgres:18-alpine",
|
|
4333
4499
|
redis: "redis:7-alpine",
|
|
4334
|
-
minio: "
|
|
4500
|
+
minio: "pgsty/minio:RELEASE.2026-08-04T00-00-00Z"
|
|
4335
4501
|
};
|
|
4336
4502
|
return pin || defaults[serviceName$1];
|
|
4337
4503
|
}
|
|
@@ -5531,7 +5697,7 @@ async function workspaceDockerCommand(workspace, options) {
|
|
|
5531
5697
|
logger$5.log(`\n Generated: .dockerignore (workspace root)`);
|
|
5532
5698
|
const dockerCompose = generateWorkspaceCompose(workspace, {
|
|
5533
5699
|
registry: options.registry,
|
|
5534
|
-
containers: await require_credentials$1.derivedContainers(workspace,
|
|
5700
|
+
containers: await require_credentials$1.derivedContainers(workspace, workspace.stages.local)
|
|
5535
5701
|
});
|
|
5536
5702
|
const composePath = (0, node_path.join)(dockerDir, "docker-compose.yml");
|
|
5537
5703
|
await (0, node_fs_promises.writeFile)(composePath, dockerCompose);
|
|
@@ -6977,6 +7143,7 @@ async function workspaceDeployCommand(configured, options) {
|
|
|
6977
7143
|
*/
|
|
6978
7144
|
async function deployCommand(options) {
|
|
6979
7145
|
const loadedConfig = await require_config.loadWorkspaceConfig();
|
|
7146
|
+
require_workspace.assertDeployedStage(loadedConfig.workspace.stages, options.stage);
|
|
6980
7147
|
return workspaceDeployCommand(loadedConfig.workspace, options);
|
|
6981
7148
|
}
|
|
6982
7149
|
|
|
@@ -7352,47 +7519,47 @@ const CONSTRUCTS_GLOB = "./src/constructs/**/*.ts";
|
|
|
7352
7519
|
*/
|
|
7353
7520
|
const WORKSPACE_CONSTRUCTS_GLOB = "./constructs/**/*.ts";
|
|
7354
7521
|
/**
|
|
7355
|
-
*
|
|
7522
|
+
* Where each routes structure puts an app's handlers, relative to the app.
|
|
7356
7523
|
*
|
|
7357
|
-
*
|
|
7358
|
-
*
|
|
7524
|
+
* `.ts` only, never `.tsx`: a TanStack site keeps its own `src/routes/`, and
|
|
7525
|
+
* this glob must not import a frontend's route components.
|
|
7359
7526
|
*/
|
|
7360
|
-
function
|
|
7361
|
-
|
|
7362
|
-
|
|
7363
|
-
|
|
7364
|
-
|
|
7365
|
-
|
|
7366
|
-
ownerUrlKey: (0, __geekmidas_manifest.provideKey)(id, "ownerUrl")
|
|
7367
|
-
};
|
|
7368
|
-
}
|
|
7369
|
-
/** The bucket a project named `name` declares. */
|
|
7370
|
-
function storageFor(name$1) {
|
|
7371
|
-
const id = (0, __geekmidas_manifest.canonicalId)(`${name$1}-uploads`);
|
|
7372
|
-
return {
|
|
7373
|
-
id,
|
|
7374
|
-
service: (0, __geekmidas_manifest.serviceKey)(id),
|
|
7375
|
-
urlKey: (0, __geekmidas_manifest.provideKey)(id, "url")
|
|
7376
|
-
};
|
|
7527
|
+
function routesGlob(structure) {
|
|
7528
|
+
switch (structure) {
|
|
7529
|
+
case "centralized-endpoints": return "./src/endpoints/**/*.ts";
|
|
7530
|
+
case "centralized-routes": return "./src/routes/**/*.ts";
|
|
7531
|
+
case "domain-based": return "./src/**/routes/*.ts";
|
|
7532
|
+
}
|
|
7377
7533
|
}
|
|
7378
|
-
/**
|
|
7379
|
-
|
|
7380
|
-
|
|
7534
|
+
/**
|
|
7535
|
+
* Plain ids — `Database`, not `Beetlefit`.
|
|
7536
|
+
*
|
|
7537
|
+
* The workspace `name` already scopes every physical name
|
|
7538
|
+
* (`production-beetlefit-database`), so an id that carried the project too
|
|
7539
|
+
* would say it twice: `production-beetlefit-beetlefit-cache`. Within a
|
|
7540
|
+
* workspace the constructs are declared once, at its root, so there is no
|
|
7541
|
+
* second app's `Database` to collide with.
|
|
7542
|
+
*/
|
|
7543
|
+
function scaffolded(id) {
|
|
7381
7544
|
return {
|
|
7382
7545
|
id,
|
|
7383
7546
|
service: (0, __geekmidas_manifest.serviceKey)(id),
|
|
7384
7547
|
urlKey: (0, __geekmidas_manifest.provideKey)(id, "url")
|
|
7385
7548
|
};
|
|
7386
7549
|
}
|
|
7387
|
-
/** The
|
|
7388
|
-
function
|
|
7389
|
-
const id = (0, __geekmidas_manifest.canonicalId)(`${name$1}-cache`);
|
|
7550
|
+
/** The database a project declares. */
|
|
7551
|
+
function databaseFor() {
|
|
7390
7552
|
return {
|
|
7391
|
-
|
|
7392
|
-
|
|
7393
|
-
urlKey: (0, __geekmidas_manifest.provideKey)(id, "url")
|
|
7553
|
+
...scaffolded("Database"),
|
|
7554
|
+
ownerUrlKey: (0, __geekmidas_manifest.provideKey)("Database", "ownerUrl")
|
|
7394
7555
|
};
|
|
7395
7556
|
}
|
|
7557
|
+
/** The bucket a project declares. */
|
|
7558
|
+
const storageFor = () => scaffolded("Uploads");
|
|
7559
|
+
/** The mail sender a project declares. */
|
|
7560
|
+
const emailFor = () => scaffolded("Mail");
|
|
7561
|
+
/** The cache a project declares. */
|
|
7562
|
+
const cacheFor = () => scaffolded("Cache");
|
|
7396
7563
|
/**
|
|
7397
7564
|
* The files a project's declared database needs.
|
|
7398
7565
|
*
|
|
@@ -7400,8 +7567,8 @@ function cacheFor(name$1) {
|
|
|
7400
7567
|
* the same way — and so the schema type, the migration, and the id the test
|
|
7401
7568
|
* setup reads all come from one place.
|
|
7402
7569
|
*/
|
|
7403
|
-
function databaseFiles(
|
|
7404
|
-
const db = databaseFor(
|
|
7570
|
+
function databaseFiles() {
|
|
7571
|
+
const db = databaseFor();
|
|
7405
7572
|
return [{
|
|
7406
7573
|
path: "src/constructs/database.ts",
|
|
7407
7574
|
content: `import { KyselyDatabase } from '@geekmidas/constructs/database/kysely';
|
|
@@ -7430,7 +7597,16 @@ export interface Database {
|
|
|
7430
7597
|
*/
|
|
7431
7598
|
export const database = new KyselyDatabase<Database, '${db.id}'>('${db.id}');
|
|
7432
7599
|
`
|
|
7433
|
-
},
|
|
7600
|
+
}, usersMigration()];
|
|
7601
|
+
}
|
|
7602
|
+
/**
|
|
7603
|
+
* The table the scaffolded endpoints and factories expect.
|
|
7604
|
+
*
|
|
7605
|
+
* Its own function because a workspace's API needs it without the database
|
|
7606
|
+
* construct beside it — that one lives at the workspace root.
|
|
7607
|
+
*/
|
|
7608
|
+
function usersMigration() {
|
|
7609
|
+
return {
|
|
7434
7610
|
path: "src/db/migrations/001_create_users.ts",
|
|
7435
7611
|
content: `import type { Kysely } from 'kysely';
|
|
7436
7612
|
|
|
@@ -7458,7 +7634,18 @@ export async function down(db: Kysely<unknown>): Promise<void> {
|
|
|
7458
7634
|
await db.schema.dropTable('users').execute();
|
|
7459
7635
|
}
|
|
7460
7636
|
`
|
|
7461
|
-
}
|
|
7637
|
+
};
|
|
7638
|
+
}
|
|
7639
|
+
/**
|
|
7640
|
+
* What a workspace's `constructs` glob reaches: the root constructs, and the
|
|
7641
|
+
* handlers of every app laid out the way `init` was told.
|
|
7642
|
+
*
|
|
7643
|
+
* Every app in `appsDir` (`apps/*`) rather than the API's own directory.
|
|
7644
|
+
* Endpoints are split by the surface they were built from, so naming one app
|
|
7645
|
+
* here would only decide which apps' endpoints are silently never loaded.
|
|
7646
|
+
*/
|
|
7647
|
+
function workspaceConstructsGlobs(structure, appsDir) {
|
|
7648
|
+
return [WORKSPACE_CONSTRUCTS_GLOB, `./${appsDir}/*/${routesGlob(structure).replace(/^\.\//, "")}`];
|
|
7462
7649
|
}
|
|
7463
7650
|
|
|
7464
7651
|
//#endregion
|
|
@@ -7538,6 +7725,7 @@ what the OpenAPI spec says, which env vars a process needs.
|
|
|
7538
7725
|
\`\`\`typescript
|
|
7539
7726
|
export default defineWorkspace({
|
|
7540
7727
|
name: '${name$1}',
|
|
7728
|
+
stages: { local: 'dev', deployed: ['staging', 'prod'], protected: ['prod'] },
|
|
7541
7729
|
constructs: '${constructsGlob}',
|
|
7542
7730
|
secrets: { enabled: true },
|
|
7543
7731
|
});
|
|
@@ -7694,11 +7882,11 @@ Crons, subscribers and functions are declared the same way endpoints are: as
|
|
|
7694
7882
|
exported values the build discovers.
|
|
7695
7883
|
|
|
7696
7884
|
\`\`\`typescript
|
|
7697
|
-
// a scheduled job
|
|
7698
|
-
import {
|
|
7885
|
+
// a scheduled job, built from the worker that runs it
|
|
7886
|
+
import { worker } from '../constructs/worker.ts';
|
|
7699
7887
|
|
|
7700
|
-
export const cleanup =
|
|
7701
|
-
.
|
|
7888
|
+
export const cleanup = worker
|
|
7889
|
+
.cron('rate(1 day)')
|
|
7702
7890
|
.handle(async ({ logger }) => {
|
|
7703
7891
|
logger.info('cleaning up');
|
|
7704
7892
|
});
|
|
@@ -7706,10 +7894,10 @@ export const cleanup = c
|
|
|
7706
7894
|
|
|
7707
7895
|
\`\`\`typescript
|
|
7708
7896
|
// a topic subscriber
|
|
7709
|
-
import { s } from '@geekmidas/constructs/subscribers';
|
|
7710
7897
|
import { userEvents } from '../constructs/topics.ts';
|
|
7898
|
+
import { worker } from '../constructs/worker.ts';
|
|
7711
7899
|
|
|
7712
|
-
export const onUserCreated =
|
|
7900
|
+
export const onUserCreated = worker
|
|
7713
7901
|
.topic(userEvents)
|
|
7714
7902
|
.subscribe(['user.created'])
|
|
7715
7903
|
.handle(async ({ events, logger }) => {
|
|
@@ -7792,6 +7980,30 @@ ${database ? "gkm test # starts services, injects secrets, then runs vites
|
|
|
7792
7980
|
Run tests through \`gkm test\`, not \`vitest\` directly${database ? " — it is what starts the database and injects the secrets the suite needs" : ""}.`;
|
|
7793
7981
|
}
|
|
7794
7982
|
|
|
7983
|
+
//#endregion
|
|
7984
|
+
//#region src/init/dependencies.ts
|
|
7985
|
+
/**
|
|
7986
|
+
* Third-party ranges a scaffold installs beside the `@geekmidas` packages.
|
|
7987
|
+
*
|
|
7988
|
+
* Held to the peer ranges those packages declare, in one place, because a
|
|
7989
|
+
* template that pinned its own copy drifted below them: `kysely ~0.28` against
|
|
7990
|
+
* a `~0.29.6` peer is two copies of Kysely and a `Kysely<DB>` from one that is
|
|
7991
|
+
* not assignable to the other's.
|
|
7992
|
+
*
|
|
7993
|
+
* Hand-maintained, unlike `versions.ts`, which `sync-versions` rewrites whole.
|
|
7994
|
+
*/
|
|
7995
|
+
const DEPENDENCY_VERSIONS = {
|
|
7996
|
+
"@tanstack/react-query": "~5.80.0",
|
|
7997
|
+
"better-auth": "~1.7.5",
|
|
7998
|
+
hono: "~4.13.8",
|
|
7999
|
+
kysely: "~0.29.6",
|
|
8000
|
+
"kysely-ctl": "~0.21.0",
|
|
8001
|
+
pg: "~8.23.0",
|
|
8002
|
+
pino: "~10.3.1",
|
|
8003
|
+
sst: "~4.17.1",
|
|
8004
|
+
zod: "~4.6.5"
|
|
8005
|
+
};
|
|
8006
|
+
|
|
7795
8007
|
//#endregion
|
|
7796
8008
|
//#region src/init/versions.ts
|
|
7797
8009
|
const require$1 = (0, node_module.createRequire)(require("url").pathToFileURL(__filename).href);
|
|
@@ -7814,26 +8026,26 @@ const CLI_VERSION = `~${pkg.version}`;
|
|
|
7814
8026
|
* Run: pnpm --filter @geekmidas/cli sync-versions
|
|
7815
8027
|
*/
|
|
7816
8028
|
const GEEKMIDAS_VERSIONS = {
|
|
7817
|
-
"@geekmidas/audit": "~10.0.0-alpha.
|
|
7818
|
-
"@geekmidas/auth": "~10.0.0-alpha.
|
|
7819
|
-
"@geekmidas/cache": "~10.0.0-alpha.
|
|
7820
|
-
"@geekmidas/client": "~10.0.0-alpha.
|
|
7821
|
-
"@geekmidas/cloud": "~10.0.0-alpha.
|
|
7822
|
-
"@geekmidas/constructs": "~10.0.0-alpha.
|
|
7823
|
-
"@geekmidas/db": "~10.0.0-alpha.
|
|
7824
|
-
"@geekmidas/emailkit": "~10.0.0-alpha.
|
|
7825
|
-
"@geekmidas/envkit": "~10.0.0-alpha.
|
|
7826
|
-
"@geekmidas/errors": "~10.0.0-alpha.
|
|
7827
|
-
"@geekmidas/events": "~10.0.0-alpha.
|
|
7828
|
-
"@geekmidas/logger": "~10.0.0-alpha.
|
|
7829
|
-
"@geekmidas/manifest": "~10.0.0-alpha.
|
|
7830
|
-
"@geekmidas/rate-limit": "~10.0.0-alpha.
|
|
7831
|
-
"@geekmidas/schema": "~10.0.0-alpha.
|
|
7832
|
-
"@geekmidas/services": "~10.0.0-alpha.
|
|
7833
|
-
"@geekmidas/storage": "~10.0.0-alpha.
|
|
7834
|
-
"@geekmidas/studio": "~10.0.0-alpha.
|
|
7835
|
-
"@geekmidas/telescope": "~10.0.0-alpha.
|
|
7836
|
-
"@geekmidas/testkit": "~10.0.0-alpha.
|
|
8029
|
+
"@geekmidas/audit": "~10.0.0-alpha.9",
|
|
8030
|
+
"@geekmidas/auth": "~10.0.0-alpha.9",
|
|
8031
|
+
"@geekmidas/cache": "~10.0.0-alpha.9",
|
|
8032
|
+
"@geekmidas/client": "~10.0.0-alpha.9",
|
|
8033
|
+
"@geekmidas/cloud": "~10.0.0-alpha.9",
|
|
8034
|
+
"@geekmidas/constructs": "~10.0.0-alpha.9",
|
|
8035
|
+
"@geekmidas/db": "~10.0.0-alpha.9",
|
|
8036
|
+
"@geekmidas/emailkit": "~10.0.0-alpha.9",
|
|
8037
|
+
"@geekmidas/envkit": "~10.0.0-alpha.9",
|
|
8038
|
+
"@geekmidas/errors": "~10.0.0-alpha.9",
|
|
8039
|
+
"@geekmidas/events": "~10.0.0-alpha.9",
|
|
8040
|
+
"@geekmidas/logger": "~10.0.0-alpha.9",
|
|
8041
|
+
"@geekmidas/manifest": "~10.0.0-alpha.9",
|
|
8042
|
+
"@geekmidas/rate-limit": "~10.0.0-alpha.9",
|
|
8043
|
+
"@geekmidas/schema": "~10.0.0-alpha.9",
|
|
8044
|
+
"@geekmidas/services": "~10.0.0-alpha.9",
|
|
8045
|
+
"@geekmidas/storage": "~10.0.0-alpha.9",
|
|
8046
|
+
"@geekmidas/studio": "~10.0.0-alpha.9",
|
|
8047
|
+
"@geekmidas/telescope": "~10.0.0-alpha.9",
|
|
8048
|
+
"@geekmidas/testkit": "~10.0.0-alpha.9",
|
|
7837
8049
|
"@geekmidas/cli": CLI_VERSION
|
|
7838
8050
|
};
|
|
7839
8051
|
|
|
@@ -7853,27 +8065,28 @@ function generateAuthAppFiles(options) {
|
|
|
7853
8065
|
private: true,
|
|
7854
8066
|
type: "module",
|
|
7855
8067
|
scripts: {
|
|
7856
|
-
dev: "gkm dev
|
|
7857
|
-
build: "
|
|
7858
|
-
start: "node dist/index.js",
|
|
7859
|
-
typecheck: "tsc --noEmit",
|
|
8068
|
+
dev: "gkm dev",
|
|
8069
|
+
build: "gkm build",
|
|
7860
8070
|
"db:migrate": "gkm exec -- npx @better-auth/cli migrate",
|
|
7861
8071
|
"db:generate": "gkm exec -- npx @better-auth/cli generate"
|
|
7862
8072
|
},
|
|
7863
8073
|
dependencies: {
|
|
7864
8074
|
[modelsPackage]: "workspace:*",
|
|
8075
|
+
"@geekmidas/auth": GEEKMIDAS_VERSIONS["@geekmidas/auth"],
|
|
8076
|
+
"@geekmidas/constructs": GEEKMIDAS_VERSIONS["@geekmidas/constructs"],
|
|
8077
|
+
"@geekmidas/db": GEEKMIDAS_VERSIONS["@geekmidas/db"],
|
|
7865
8078
|
"@geekmidas/envkit": GEEKMIDAS_VERSIONS["@geekmidas/envkit"],
|
|
7866
8079
|
"@geekmidas/logger": GEEKMIDAS_VERSIONS["@geekmidas/logger"],
|
|
7867
|
-
"@hono/node-server": "~1.
|
|
7868
|
-
"better-auth": "
|
|
7869
|
-
hono:
|
|
7870
|
-
kysely:
|
|
7871
|
-
pg:
|
|
8080
|
+
"@hono/node-server": "~1.14.1",
|
|
8081
|
+
"better-auth": DEPENDENCY_VERSIONS["better-auth"],
|
|
8082
|
+
hono: DEPENDENCY_VERSIONS.hono,
|
|
8083
|
+
kysely: DEPENDENCY_VERSIONS.kysely,
|
|
8084
|
+
pg: DEPENDENCY_VERSIONS.pg
|
|
7872
8085
|
},
|
|
7873
8086
|
devDependencies: {
|
|
7874
8087
|
"@geekmidas/cli": GEEKMIDAS_VERSIONS["@geekmidas/cli"],
|
|
7875
8088
|
"@types/node": "~22.0.0",
|
|
7876
|
-
"@types/pg": "~8.
|
|
8089
|
+
"@types/pg": "~8.23.1",
|
|
7877
8090
|
tsx: "~4.20.0",
|
|
7878
8091
|
typescript: "~5.8.2"
|
|
7879
8092
|
}
|
|
@@ -7913,6 +8126,30 @@ dist/
|
|
|
7913
8126
|
];
|
|
7914
8127
|
}
|
|
7915
8128
|
|
|
8129
|
+
//#endregion
|
|
8130
|
+
//#region src/init/generators/stages.ts
|
|
8131
|
+
/** The `stages` block a generated gkm.config.ts declares, indented two spaces. */
|
|
8132
|
+
function stagesBlock(stages) {
|
|
8133
|
+
const list = (names) => `[${names.map((name$1) => `'${name$1}'`).join(", ")}]`;
|
|
8134
|
+
return `
|
|
8135
|
+
// Named by this project and read by every command: \`gkm dev\`, \`exec\` and
|
|
8136
|
+
// \`test\` run as \`local\`, and \`gkm deploy --stage\` accepts only \`deployed\`.
|
|
8137
|
+
stages: {
|
|
8138
|
+
local: '${stages.local}',
|
|
8139
|
+
deployed: ${list(stages.deployed)},${stages.protected?.length ? `
|
|
8140
|
+
// Retained and protected on removal.
|
|
8141
|
+
protected: ${list(stages.protected)},` : ""}
|
|
8142
|
+
},`;
|
|
8143
|
+
}
|
|
8144
|
+
/**
|
|
8145
|
+
* The `STAGE` the scaffolded env config parses: one of the declared stages,
|
|
8146
|
+
* defaulting to the local one.
|
|
8147
|
+
*/
|
|
8148
|
+
function stageEnv(stages) {
|
|
8149
|
+
const names = [stages.local, ...stages.deployed].map((name$1) => `'${name$1}'`).join(", ");
|
|
8150
|
+
return `get('STAGE').enum([${names}]).default('${stages.local}')`;
|
|
8151
|
+
}
|
|
8152
|
+
|
|
7916
8153
|
//#endregion
|
|
7917
8154
|
//#region src/init/generators/config.ts
|
|
7918
8155
|
/**
|
|
@@ -7937,13 +8174,7 @@ function generateConfigFiles(options, template) {
|
|
|
7937
8174
|
const isServerless = template.name === "serverless";
|
|
7938
8175
|
const hasWorker = template.name === "worker";
|
|
7939
8176
|
const isFullstack = options.template === "fullstack";
|
|
7940
|
-
const getRoutesGlob = () =>
|
|
7941
|
-
switch (routesStructure) {
|
|
7942
|
-
case "centralized-endpoints": return "./src/endpoints/**/*.ts";
|
|
7943
|
-
case "centralized-routes": return "./src/routes/**/*.ts";
|
|
7944
|
-
case "domain-based": return "./src/**/routes/*.ts";
|
|
7945
|
-
}
|
|
7946
|
-
};
|
|
8177
|
+
const getRoutesGlob = () => routesGlob(routesStructure);
|
|
7947
8178
|
if (isFullstack) return generateSingleAppConfigFiles(options, template, {
|
|
7948
8179
|
telescope,
|
|
7949
8180
|
studio,
|
|
@@ -7954,7 +8185,7 @@ function generateConfigFiles(options, template) {
|
|
|
7954
8185
|
});
|
|
7955
8186
|
let gkmConfig = `import { defineConfig } from '@geekmidas/cli/config';
|
|
7956
8187
|
|
|
7957
|
-
export default defineConfig({${options.monorepo ? "" : `
|
|
8188
|
+
export default defineConfig({${stagesBlock(options.stages)}${options.monorepo ? "" : `
|
|
7958
8189
|
// One glob, every kind. What reconcile reads to derive the containers this
|
|
7959
8190
|
// app needs: a declared database is why a Postgres exists at all, so nothing
|
|
7960
8191
|
// lists \`postgres\` anywhere.
|
|
@@ -7996,21 +8227,7 @@ export default defineConfig({${options.monorepo ? "" : `
|
|
|
7996
8227
|
gkmConfig += `
|
|
7997
8228
|
});
|
|
7998
8229
|
`;
|
|
7999
|
-
const tsConfig = options.monorepo ? {
|
|
8000
|
-
extends: "../../tsconfig.json",
|
|
8001
|
-
compilerOptions: {
|
|
8002
|
-
noEmit: true,
|
|
8003
|
-
allowImportingTsExtensions: true,
|
|
8004
|
-
baseUrl: ".",
|
|
8005
|
-
paths: {
|
|
8006
|
-
"~/*": ["./src/*"],
|
|
8007
|
-
[`@${options.name}/constructs/*`]: ["../../constructs/*"],
|
|
8008
|
-
[`@${options.name}/*`]: ["../../packages/*/src"]
|
|
8009
|
-
}
|
|
8010
|
-
},
|
|
8011
|
-
include: ["src/**/*.ts"],
|
|
8012
|
-
exclude: ["node_modules", "dist"]
|
|
8013
|
-
} : {
|
|
8230
|
+
const tsConfig = options.monorepo ? workspaceApiTsConfig(options.name) : {
|
|
8014
8231
|
compilerOptions: {
|
|
8015
8232
|
target: "ES2022",
|
|
8016
8233
|
module: "NodeNext",
|
|
@@ -8022,7 +8239,9 @@ export default defineConfig({${options.monorepo ? "" : `
|
|
|
8022
8239
|
forceConsistentCasingInFileNames: true,
|
|
8023
8240
|
resolveJsonModule: true,
|
|
8024
8241
|
noEmit: true,
|
|
8025
|
-
allowImportingTsExtensions: true
|
|
8242
|
+
allowImportingTsExtensions: true,
|
|
8243
|
+
baseUrl: ".",
|
|
8244
|
+
paths: { "~/*": ["./src/*"] }
|
|
8026
8245
|
},
|
|
8027
8246
|
include: ["src/**/*.ts"],
|
|
8028
8247
|
exclude: ["node_modules", "dist"]
|
|
@@ -8048,7 +8267,7 @@ export default defineConfig({${options.monorepo ? "" : `
|
|
|
8048
8267
|
clientKind: "git",
|
|
8049
8268
|
useIgnoreFile: true
|
|
8050
8269
|
},
|
|
8051
|
-
|
|
8270
|
+
assist: { actions: { source: { organizeImports: "on" } } },
|
|
8052
8271
|
formatter: {
|
|
8053
8272
|
enabled: true,
|
|
8054
8273
|
indentStyle: "space",
|
|
@@ -8072,11 +8291,13 @@ export default defineConfig({${options.monorepo ? "" : `
|
|
|
8072
8291
|
style: { noNonNullAssertion: "off" }
|
|
8073
8292
|
}
|
|
8074
8293
|
},
|
|
8075
|
-
|
|
8076
|
-
|
|
8077
|
-
"
|
|
8078
|
-
"
|
|
8079
|
-
"
|
|
8294
|
+
css: { parser: { tailwindDirectives: true } },
|
|
8295
|
+
files: { includes: [
|
|
8296
|
+
"**",
|
|
8297
|
+
"!**/node_modules",
|
|
8298
|
+
"!**/dist",
|
|
8299
|
+
"!**/.gkm",
|
|
8300
|
+
"!**/coverage"
|
|
8080
8301
|
] }
|
|
8081
8302
|
};
|
|
8082
8303
|
const turboConfig = {
|
|
@@ -8130,8 +8351,16 @@ export default defineConfig({${options.monorepo ? "" : `
|
|
|
8130
8351
|
});
|
|
8131
8352
|
return files$1;
|
|
8132
8353
|
}
|
|
8133
|
-
|
|
8134
|
-
|
|
8354
|
+
/**
|
|
8355
|
+
* The tsconfig of an API inside a workspace.
|
|
8356
|
+
*
|
|
8357
|
+
* One definition for both ways of getting one — the fullstack template and
|
|
8358
|
+
* `--monorepo` — because the two copies had drifted: only one of them mapped
|
|
8359
|
+
* the root constructs, so in the other `@<name>/constructs/api.ts` fell through
|
|
8360
|
+
* to the `packages/` mapping and nothing could load the surface.
|
|
8361
|
+
*/
|
|
8362
|
+
function workspaceApiTsConfig(name$1) {
|
|
8363
|
+
return {
|
|
8135
8364
|
extends: "../../tsconfig.json",
|
|
8136
8365
|
compilerOptions: {
|
|
8137
8366
|
noEmit: true,
|
|
@@ -8139,12 +8368,16 @@ function generateSingleAppConfigFiles(options, _template, _helpers) {
|
|
|
8139
8368
|
baseUrl: ".",
|
|
8140
8369
|
paths: {
|
|
8141
8370
|
"~/*": ["./src/*"],
|
|
8142
|
-
[`@${
|
|
8371
|
+
[`@${name$1}/constructs/*`]: ["../../constructs/*"],
|
|
8372
|
+
[`@${name$1}/*`]: ["../../packages/*/src"]
|
|
8143
8373
|
}
|
|
8144
8374
|
},
|
|
8145
8375
|
include: ["src/**/*.ts"],
|
|
8146
8376
|
exclude: ["node_modules", "dist"]
|
|
8147
8377
|
};
|
|
8378
|
+
}
|
|
8379
|
+
function generateSingleAppConfigFiles(options, _template, _helpers) {
|
|
8380
|
+
const tsConfig = workspaceApiTsConfig(options.name);
|
|
8148
8381
|
const files$1 = [{
|
|
8149
8382
|
path: "tsconfig.json",
|
|
8150
8383
|
content: `${JSON.stringify(tsConfig, null, 2)}\n`
|
|
@@ -8280,7 +8513,7 @@ function generateDockerFiles(options, template, dbApps) {
|
|
|
8280
8513
|
MP_SMTP_AUTH: \${SMTP_USER:-${options.name}}:\${SMTP_PASS:-${options.name}}`);
|
|
8281
8514
|
if (options.services?.storage) {
|
|
8282
8515
|
services.push(` minio:
|
|
8283
|
-
image:
|
|
8516
|
+
image: pgsty/minio:RELEASE.2026-08-04T00-00-00Z
|
|
8284
8517
|
container_name: ${options.name}-minio
|
|
8285
8518
|
restart: unless-stopped
|
|
8286
8519
|
entrypoint: sh
|
|
@@ -8553,12 +8786,12 @@ function generateExpoAppFiles(options) {
|
|
|
8553
8786
|
dependencies: {
|
|
8554
8787
|
[apiPackage]: "workspace:*",
|
|
8555
8788
|
[modelsPackage]: "workspace:*",
|
|
8556
|
-
"@better-auth/expo": "
|
|
8789
|
+
"@better-auth/expo": DEPENDENCY_VERSIONS["better-auth"],
|
|
8557
8790
|
"@geekmidas/client": GEEKMIDAS_VERSIONS["@geekmidas/client"],
|
|
8558
8791
|
"@geekmidas/envkit": GEEKMIDAS_VERSIONS["@geekmidas/envkit"],
|
|
8559
8792
|
"@react-navigation/native": "^7.1.0",
|
|
8560
|
-
"@tanstack/react-query": "
|
|
8561
|
-
"better-auth": "
|
|
8793
|
+
"@tanstack/react-query": DEPENDENCY_VERSIONS["@tanstack/react-query"],
|
|
8794
|
+
"better-auth": DEPENDENCY_VERSIONS["better-auth"],
|
|
8562
8795
|
expo: "~55.0.0",
|
|
8563
8796
|
"expo-constants": "~55.0.0",
|
|
8564
8797
|
"expo-dev-client": "~55.0.0",
|
|
@@ -8694,7 +8927,7 @@ module.exports = withNativeWind(config, { input: './global.css' });
|
|
|
8694
8927
|
"@/*": ["./*"],
|
|
8695
8928
|
[modelsPackage]: ["../../packages/models/src"],
|
|
8696
8929
|
[`${modelsPackage}/*`]: ["../../packages/models/src/*"],
|
|
8697
|
-
[`${apiPackage}/client`]: ["../../apps/api/.gkm/openapi.ts"]
|
|
8930
|
+
[`${apiPackage}/client`]: ["../../apps/api/.gkm/openapi/api.ts"]
|
|
8698
8931
|
}
|
|
8699
8932
|
},
|
|
8700
8933
|
include: [
|
|
@@ -9252,14 +9485,142 @@ export type ListUsersResponse = z.infer<typeof ListUsersResponseSchema>;
|
|
|
9252
9485
|
];
|
|
9253
9486
|
}
|
|
9254
9487
|
|
|
9488
|
+
//#endregion
|
|
9489
|
+
//#region src/init/generators/deploy.ts
|
|
9490
|
+
/** One `deploy:<stage>` script per deployed stage. */
|
|
9491
|
+
const perStage = (options, command) => Object.fromEntries(options.stages.deployed.map((stage) => [`deploy:${stage}`, command(stage)]));
|
|
9492
|
+
function deployPackage(options) {
|
|
9493
|
+
switch (options.deployTarget) {
|
|
9494
|
+
case "dokploy": return {
|
|
9495
|
+
scripts: perStage(options, (stage) => `gkm deploy --provider dokploy --stage ${stage}`),
|
|
9496
|
+
dependencies: {},
|
|
9497
|
+
devDependencies: {}
|
|
9498
|
+
};
|
|
9499
|
+
case "sst": {
|
|
9500
|
+
const v = GEEKMIDAS_VERSIONS;
|
|
9501
|
+
return {
|
|
9502
|
+
scripts: perStage(options, (stage) => `gkm build --provider aws && sst deploy --stage ${stage}`),
|
|
9503
|
+
dependencies: {
|
|
9504
|
+
"@geekmidas/cloud": v["@geekmidas/cloud"],
|
|
9505
|
+
"@geekmidas/db": v["@geekmidas/db"],
|
|
9506
|
+
"@geekmidas/envkit": v["@geekmidas/envkit"],
|
|
9507
|
+
"@geekmidas/events": v["@geekmidas/events"],
|
|
9508
|
+
"@geekmidas/manifest": v["@geekmidas/manifest"],
|
|
9509
|
+
"@geekmidas/storage": v["@geekmidas/storage"],
|
|
9510
|
+
pg: DEPENDENCY_VERSIONS.pg
|
|
9511
|
+
},
|
|
9512
|
+
devDependencies: { sst: DEPENDENCY_VERSIONS.sst }
|
|
9513
|
+
};
|
|
9514
|
+
}
|
|
9515
|
+
default: return {
|
|
9516
|
+
scripts: {},
|
|
9517
|
+
dependencies: {},
|
|
9518
|
+
devDependencies: {}
|
|
9519
|
+
};
|
|
9520
|
+
}
|
|
9521
|
+
}
|
|
9522
|
+
function generateDeployFiles(options) {
|
|
9523
|
+
if (options.deployTarget !== "sst") return [];
|
|
9524
|
+
if (!options.region) throw new Error("An SST deploy needs a region.");
|
|
9525
|
+
const db = databaseFor();
|
|
9526
|
+
const mail = emailFor();
|
|
9527
|
+
const hasDatabase = options.template === "fullstack" || options.services.db;
|
|
9528
|
+
const inputs = [...hasDatabase ? [` // RDS needs a network. Created here rather than assumed, because a
|
|
9529
|
+
// VPC with NAT is a monthly cost in an account that may already
|
|
9530
|
+
// have one — replace it with \`sst.aws.Vpc.get(…)\` if so.
|
|
9531
|
+
${db.id}: { vpc },`] : [], ...options.services.mail ? [` // Every provider rejects an unverified sender, so there is no
|
|
9532
|
+
// default; the synth stops with \`EmailNeedsSender\` until it is set.
|
|
9533
|
+
${mail.id}: { from: process.env.MAIL_FROM as string },`] : []];
|
|
9534
|
+
return [{
|
|
9535
|
+
path: "sst.config.ts",
|
|
9536
|
+
content: `/// <reference path="./.sst/platform/config.d.ts" />
|
|
9537
|
+
|
|
9538
|
+
/**
|
|
9539
|
+
* ${options.name} on AWS.
|
|
9540
|
+
*
|
|
9541
|
+
* No bucket, cluster, queue or IAM is listed here: they come from the
|
|
9542
|
+
* constructs the application declares, through \`fromManifest\`. What is here
|
|
9543
|
+
* is what varies by stage, and the inputs a declaration cannot carry.
|
|
9544
|
+
*
|
|
9545
|
+
* Deploy with \`pnpm run deploy:<stage>\`, which builds the manifest first.
|
|
9546
|
+
*/
|
|
9547
|
+
const region = '${options.region}';
|
|
9548
|
+
|
|
9549
|
+
/** From \`stages.protected\` in gkm.config.ts. */
|
|
9550
|
+
const PROTECTED: string[] = ${JSON.stringify(options.stages.protected ?? []).replace(/"/g, "'")};
|
|
9551
|
+
|
|
9552
|
+
export default $config({
|
|
9553
|
+
app(input) {
|
|
9554
|
+
return {
|
|
9555
|
+
name: '${options.name}',
|
|
9556
|
+
// Any other stage is disposable; a protected one's database should not
|
|
9557
|
+
// vanish because somebody removed a stack.
|
|
9558
|
+
removal: PROTECTED.includes(input?.stage) ? 'retain' : 'remove',
|
|
9559
|
+
protect: PROTECTED.includes(input?.stage),
|
|
9560
|
+
home: 'aws',
|
|
9561
|
+
providers: { aws: { region } },
|
|
9562
|
+
};
|
|
9563
|
+
},
|
|
9564
|
+
|
|
9565
|
+
async run() {
|
|
9566
|
+
const { App, fromManifest, Stack } = await import('@geekmidas/cloud/sst');
|
|
9567
|
+
// Written by \`gkm build --provider aws\`. Imported rather than discovered,
|
|
9568
|
+
// so SST's toolchain never evaluates the application's own modules.
|
|
9569
|
+
const { backends, constructs } = await import('./.gkm/manifest/aws.js');
|
|
9570
|
+
|
|
9571
|
+
${hasDatabase ? " const vpc = new sst.aws.Vpc('Vpc', { nat: 'ec2' });\n\n" : ""} const app = new App({
|
|
9572
|
+
name: $app.name,
|
|
9573
|
+
stage: $app.stage,
|
|
9574
|
+
domain: '',
|
|
9575
|
+
region,
|
|
9576
|
+
hostedZoneId: '',
|
|
9577
|
+
});
|
|
9578
|
+
|
|
9579
|
+
return fromManifest(
|
|
9580
|
+
new Stack(app, '${(0, __geekmidas_manifest.canonicalId)(options.name)}'),
|
|
9581
|
+
constructs,
|
|
9582
|
+
{
|
|
9583
|
+
${inputs.join("\n")}
|
|
9584
|
+
},
|
|
9585
|
+
backends,
|
|
9586
|
+
);
|
|
9587
|
+
},
|
|
9588
|
+
});
|
|
9589
|
+
`
|
|
9590
|
+
}];
|
|
9591
|
+
}
|
|
9592
|
+
|
|
9255
9593
|
//#endregion
|
|
9256
9594
|
//#region src/init/generators/monorepo.ts
|
|
9257
9595
|
/**
|
|
9596
|
+
* What the root `constructs/` folder imports, and the peers each construct
|
|
9597
|
+
* needs, for the services this workspace was scaffolded with.
|
|
9598
|
+
*/
|
|
9599
|
+
function rootConstructDependencies(options) {
|
|
9600
|
+
const v = GEEKMIDAS_VERSIONS;
|
|
9601
|
+
return {
|
|
9602
|
+
"@geekmidas/constructs": v["@geekmidas/constructs"],
|
|
9603
|
+
"@geekmidas/logger": v["@geekmidas/logger"],
|
|
9604
|
+
...options.loggerType === "pino" ? { pino: DEPENDENCY_VERSIONS.pino } : {},
|
|
9605
|
+
"@geekmidas/telescope": v["@geekmidas/telescope"],
|
|
9606
|
+
"@geekmidas/auth": v["@geekmidas/auth"],
|
|
9607
|
+
"better-auth": DEPENDENCY_VERSIONS["better-auth"],
|
|
9608
|
+
"@geekmidas/db": v["@geekmidas/db"],
|
|
9609
|
+
kysely: DEPENDENCY_VERSIONS.kysely,
|
|
9610
|
+
pg: DEPENDENCY_VERSIONS.pg,
|
|
9611
|
+
...options.services.storage ? { "@geekmidas/storage": v["@geekmidas/storage"] } : {},
|
|
9612
|
+
...options.services.mail ? { "@geekmidas/emailkit": v["@geekmidas/emailkit"] } : {},
|
|
9613
|
+
...options.services.cache ? { "@geekmidas/cache": v["@geekmidas/cache"] } : {}
|
|
9614
|
+
};
|
|
9615
|
+
}
|
|
9616
|
+
/**
|
|
9258
9617
|
* Generate monorepo root files (pnpm-workspace.yaml, root package.json, etc.)
|
|
9259
9618
|
*/
|
|
9260
9619
|
function generateMonorepoFiles(options, _template) {
|
|
9261
9620
|
if (!options.monorepo) return [];
|
|
9262
9621
|
const isFullstack = options.template === "fullstack";
|
|
9622
|
+
const deploy = deployPackage(options);
|
|
9623
|
+
const isSst = options.deployTarget === "sst";
|
|
9263
9624
|
const rootPackageJson = {
|
|
9264
9625
|
name: options.name,
|
|
9265
9626
|
version: "0.0.1",
|
|
@@ -9276,9 +9637,13 @@ function generateMonorepoFiles(options, _template) {
|
|
|
9276
9637
|
fmt: "biome format . --write",
|
|
9277
9638
|
"fmt:check": "biome format .",
|
|
9278
9639
|
...isFullstack ? { storybook: "pnpm --filter ./packages/ui storybook" } : {},
|
|
9279
|
-
...
|
|
9640
|
+
...deploy.scripts
|
|
9641
|
+
},
|
|
9642
|
+
dependencies: {
|
|
9643
|
+
zod: DEPENDENCY_VERSIONS.zod,
|
|
9644
|
+
...isFullstack ? rootConstructDependencies(options) : {},
|
|
9645
|
+
...deploy.dependencies
|
|
9280
9646
|
},
|
|
9281
|
-
dependencies: { zod: "~4.1.0" },
|
|
9282
9647
|
devDependencies: {
|
|
9283
9648
|
"@biomejs/biome": "~2.3.0",
|
|
9284
9649
|
"@geekmidas/cli": GEEKMIDAS_VERSIONS["@geekmidas/cli"],
|
|
@@ -9286,7 +9651,8 @@ function generateMonorepoFiles(options, _template) {
|
|
|
9286
9651
|
tsx: "~4.20.0",
|
|
9287
9652
|
turbo: "~2.3.0",
|
|
9288
9653
|
typescript: "~5.8.2",
|
|
9289
|
-
vitest: "~4.0.0"
|
|
9654
|
+
vitest: "~4.0.0",
|
|
9655
|
+
...deploy.devDependencies
|
|
9290
9656
|
}
|
|
9291
9657
|
};
|
|
9292
9658
|
const apiPathParts = options.apiPath.split("/");
|
|
@@ -9302,7 +9668,7 @@ function generateMonorepoFiles(options, _template) {
|
|
|
9302
9668
|
clientKind: "git",
|
|
9303
9669
|
useIgnoreFile: true
|
|
9304
9670
|
},
|
|
9305
|
-
|
|
9671
|
+
assist: { actions: { source: { organizeImports: "on" } } },
|
|
9306
9672
|
formatter: {
|
|
9307
9673
|
enabled: true,
|
|
9308
9674
|
indentStyle: "space",
|
|
@@ -9326,11 +9692,14 @@ function generateMonorepoFiles(options, _template) {
|
|
|
9326
9692
|
style: { noNonNullAssertion: "off" }
|
|
9327
9693
|
}
|
|
9328
9694
|
},
|
|
9329
|
-
|
|
9330
|
-
|
|
9331
|
-
"
|
|
9332
|
-
"
|
|
9333
|
-
"
|
|
9695
|
+
css: { parser: { tailwindDirectives: true } },
|
|
9696
|
+
files: { includes: [
|
|
9697
|
+
"**",
|
|
9698
|
+
"!**/node_modules",
|
|
9699
|
+
"!**/dist",
|
|
9700
|
+
"!**/.gkm",
|
|
9701
|
+
"!**/coverage",
|
|
9702
|
+
...isSst ? ["!**/.sst", "!sst-env.d.ts"] : []
|
|
9334
9703
|
] }
|
|
9335
9704
|
};
|
|
9336
9705
|
const turboConfig = {
|
|
@@ -9396,12 +9765,14 @@ coverage/
|
|
|
9396
9765
|
|
|
9397
9766
|
# Turbo
|
|
9398
9767
|
.turbo/
|
|
9399
|
-
`;
|
|
9768
|
+
${isSst ? "\n# SST\n.sst/\n" : ""}`;
|
|
9400
9769
|
const tsConfig = {
|
|
9401
9770
|
compilerOptions: {
|
|
9402
9771
|
...isFullstack ? {
|
|
9403
9772
|
baseUrl: ".",
|
|
9404
|
-
paths: { [`@${options.name}/constructs/*`]: ["./constructs/*"] }
|
|
9773
|
+
paths: { [`@${options.name}/constructs/*`]: ["./constructs/*"] },
|
|
9774
|
+
allowImportingTsExtensions: true,
|
|
9775
|
+
noEmit: true
|
|
9405
9776
|
} : {},
|
|
9406
9777
|
target: "ES2022",
|
|
9407
9778
|
module: "NodeNext",
|
|
@@ -9413,16 +9784,21 @@ coverage/
|
|
|
9413
9784
|
forceConsistentCasingInFileNames: true,
|
|
9414
9785
|
resolveJsonModule: true
|
|
9415
9786
|
},
|
|
9416
|
-
exclude: [
|
|
9787
|
+
exclude: [
|
|
9788
|
+
"node_modules",
|
|
9789
|
+
"dist",
|
|
9790
|
+
...isSst ? ["sst.config.ts", ".sst"] : []
|
|
9791
|
+
]
|
|
9417
9792
|
};
|
|
9418
9793
|
const vitestConfig = `import { defineConfig } from 'vitest/config';
|
|
9419
9794
|
|
|
9420
9795
|
export default defineConfig({
|
|
9421
9796
|
test: {
|
|
9422
|
-
|
|
9423
|
-
|
|
9424
|
-
|
|
9425
|
-
|
|
9797
|
+
// Each app and package is its own project, so its own config — an app's
|
|
9798
|
+
// \`globalSetup\`, its path aliases — applies to its own tests.
|
|
9799
|
+
projects: ['apps/*', 'packages/*'],
|
|
9800
|
+
// The shared packages ship without tests of their own.
|
|
9801
|
+
passWithNoTests: true,
|
|
9426
9802
|
coverage: {
|
|
9427
9803
|
provider: 'v8',
|
|
9428
9804
|
reporter: ['text', 'json', 'html'],
|
|
@@ -9503,7 +9879,8 @@ export default defineConfig({
|
|
|
9503
9879
|
{
|
|
9504
9880
|
path: ".vscode/extensions.json",
|
|
9505
9881
|
content: `${JSON.stringify(vscodeExtensions, null, " ")}\n`
|
|
9506
|
-
}
|
|
9882
|
+
},
|
|
9883
|
+
...generateDeployFiles(options)
|
|
9507
9884
|
];
|
|
9508
9885
|
if (isFullstack) files$1.push({
|
|
9509
9886
|
path: "gkm.config.ts",
|
|
@@ -9530,12 +9907,17 @@ export default defineWorkspace({
|
|
|
9530
9907
|
// The scope every physical name is built from: \`Database\` becomes
|
|
9531
9908
|
// \`production-${options.name}-database\` on Dokploy and on AWS alike.
|
|
9532
9909
|
name: '${options.name}',
|
|
9910
|
+
${stagesBlock(options.stages)}
|
|
9533
9911
|
|
|
9534
|
-
//
|
|
9912
|
+
// Every kind, in every app. A declared database is why a Postgres exists, a
|
|
9535
9913
|
// declared bucket is why MinIO does, a declared topic is why a broker does —
|
|
9536
9914
|
// none of it listed here. It is also where the apps come from: a
|
|
9537
|
-
// \`StaticSite\` is an app, and so is every \`RestApi\`.
|
|
9538
|
-
|
|
9915
|
+
// \`StaticSite\` is an app, and so is every \`RestApi\`. And it finds each
|
|
9916
|
+
// app's endpoints, crons and subscribers: an endpoint belongs to the surface
|
|
9917
|
+
// it was built from, not to the directory it sits in.
|
|
9918
|
+
constructs: [
|
|
9919
|
+
${workspaceConstructsGlobs(options.routesStructure, (0, node_path.dirname)(options.apiPath)).map((glob) => ` '${glob}',`).join("\n")}
|
|
9920
|
+
],
|
|
9539
9921
|
|
|
9540
9922
|
secrets: {
|
|
9541
9923
|
enabled: true,
|
|
@@ -9564,7 +9946,7 @@ export default defineWorkspace({
|
|
|
9564
9946
|
function generateRootConstructs(options) {
|
|
9565
9947
|
if (!options.monorepo || options.template !== "fullstack") return [];
|
|
9566
9948
|
const { name: name$1, services, frontendFramework } = options;
|
|
9567
|
-
const db = databaseFor(
|
|
9949
|
+
const db = databaseFor();
|
|
9568
9950
|
const files$1 = [];
|
|
9569
9951
|
files$1.push({
|
|
9570
9952
|
path: "constructs/logger.ts",
|
|
@@ -9630,6 +10012,7 @@ import { authDb } from './database.ts';
|
|
|
9630
10012
|
* origin down.
|
|
9631
10013
|
*/
|
|
9632
10014
|
export const auth = new BetterAuth('Auth', {
|
|
10015
|
+
path: 'apps/auth',
|
|
9633
10016
|
database: authDb,
|
|
9634
10017
|
basePath: '/api/auth',
|
|
9635
10018
|
});
|
|
@@ -9644,25 +10027,22 @@ import { logger } from './logger.ts';
|
|
|
9644
10027
|
/**
|
|
9645
10028
|
* The application's HTTP surface — one RestApi, one container.
|
|
9646
10029
|
*
|
|
9647
|
-
*
|
|
9648
|
-
* \`
|
|
9649
|
-
* \`src/\`, where the conventional layout has them at the app root.
|
|
10030
|
+
* \`path\` is where its app lives. Its handlers are found by the workspace's
|
|
10031
|
+
* \`constructs\` glob, and belong to it because they were built from it.
|
|
9650
10032
|
*/
|
|
9651
10033
|
export const api = new RestApi('Api', {
|
|
10034
|
+
path: '${options.apiPath}',
|
|
10035
|
+
|
|
9652
10036
|
// Typed out rather than omitted: an API that ships open because a field was
|
|
9653
10037
|
// left off is the one default worth refusing to have.
|
|
9654
10038
|
defaultAuthorizer: 'none',
|
|
9655
10039
|
|
|
9656
10040
|
// The actual logger, not a path to one.
|
|
9657
10041
|
logger,
|
|
9658
|
-
|
|
9659
|
-
app: {
|
|
9660
|
-
code: './src/{endpoints,functions,crons,queues,topics,subscribers}/**/*.ts',
|
|
9661
|
-
},
|
|
9662
10042
|
}).auth(auth);
|
|
9663
10043
|
`
|
|
9664
10044
|
});
|
|
9665
|
-
const variant = frontendFramework === "tanstack-start" ? "{ variant: 'tanstack' }" : frontendFramework === "expo" ? void 0 : "{ variant: 'next' }";
|
|
10045
|
+
const variant = frontendFramework === "tanstack-start" ? "{ path: 'apps/web', variant: 'tanstack' }" : frontendFramework === "expo" ? void 0 : "{ path: 'apps/web', variant: 'next' }";
|
|
9666
10046
|
if (variant !== void 0) files$1.push({
|
|
9667
10047
|
path: "constructs/site.ts",
|
|
9668
10048
|
content: `import { StaticSite } from '@geekmidas/constructs/site';
|
|
@@ -9672,7 +10052,7 @@ import { auth } from './auth.ts';
|
|
|
9672
10052
|
/**
|
|
9673
10053
|
* The frontend — a construct like any other, which is what makes it an app.
|
|
9674
10054
|
*
|
|
9675
|
-
*
|
|
10055
|
+
* \`path\` is where it lives. \`.dependsOn()\` is the single fact
|
|
9676
10056
|
* behind four things that are hand-maintained otherwise: this site's
|
|
9677
10057
|
* build-time API URL, the API's CORS origins, the auth server's trusted
|
|
9678
10058
|
* origins, and which generated client lands here.
|
|
@@ -9681,7 +10061,7 @@ export const web = new StaticSite('Web', ${variant}).dependsOn([api, auth]);
|
|
|
9681
10061
|
`
|
|
9682
10062
|
});
|
|
9683
10063
|
if (services.storage) {
|
|
9684
|
-
const bucket = storageFor(
|
|
10064
|
+
const bucket = storageFor();
|
|
9685
10065
|
files$1.push({
|
|
9686
10066
|
path: "constructs/storage.ts",
|
|
9687
10067
|
content: `import { ObjectStorage } from '@geekmidas/constructs/object-storage';
|
|
@@ -9692,7 +10072,7 @@ export const uploads = new ObjectStorage('${bucket.id}');
|
|
|
9692
10072
|
});
|
|
9693
10073
|
}
|
|
9694
10074
|
if (services.mail) {
|
|
9695
|
-
const mail = emailFor(
|
|
10075
|
+
const mail = emailFor();
|
|
9696
10076
|
files$1.push({
|
|
9697
10077
|
path: "constructs/email.ts",
|
|
9698
10078
|
content: `import { Email } from '@geekmidas/constructs/email';
|
|
@@ -9703,7 +10083,7 @@ export const email = new Email('${mail.id}', { templates: {} });
|
|
|
9703
10083
|
});
|
|
9704
10084
|
}
|
|
9705
10085
|
if (services.cache) {
|
|
9706
|
-
const kv = cacheFor(
|
|
10086
|
+
const kv = cacheFor();
|
|
9707
10087
|
files$1.push({
|
|
9708
10088
|
path: "constructs/cache.ts",
|
|
9709
10089
|
content: `import { Cache } from '@geekmidas/constructs/cache';
|
|
@@ -9736,9 +10116,9 @@ const apiTemplate = {
|
|
|
9736
10116
|
"@geekmidas/errors": GEEKMIDAS_VERSIONS["@geekmidas/errors"],
|
|
9737
10117
|
"@geekmidas/auth": GEEKMIDAS_VERSIONS["@geekmidas/auth"],
|
|
9738
10118
|
"@hono/node-server": "~1.14.1",
|
|
9739
|
-
hono:
|
|
9740
|
-
pino:
|
|
9741
|
-
zod:
|
|
10119
|
+
hono: DEPENDENCY_VERSIONS.hono,
|
|
10120
|
+
pino: DEPENDENCY_VERSIONS.pino,
|
|
10121
|
+
zod: DEPENDENCY_VERSIONS.zod
|
|
9742
10122
|
},
|
|
9743
10123
|
devDependencies: {
|
|
9744
10124
|
"@biomejs/biome": "~2.3.0",
|
|
@@ -9762,10 +10142,10 @@ const apiTemplate = {
|
|
|
9762
10142
|
},
|
|
9763
10143
|
files: (options) => {
|
|
9764
10144
|
const { loggerType, routesStructure, monorepo, name: name$1, services } = options;
|
|
9765
|
-
const bucket = storageFor(
|
|
9766
|
-
const kv = cacheFor(
|
|
9767
|
-
const db = databaseFor(
|
|
9768
|
-
const mail = emailFor(
|
|
10145
|
+
const bucket = storageFor();
|
|
10146
|
+
const kv = cacheFor();
|
|
10147
|
+
const db = databaseFor();
|
|
10148
|
+
const mail = emailFor();
|
|
9769
10149
|
const src = (path) => monorepo ? `~/${path}` : `./${path}`;
|
|
9770
10150
|
const declares = !monorepo;
|
|
9771
10151
|
const loggerContent = `import { createLogger } from '@geekmidas/logger/${loggerType}';
|
|
@@ -9799,6 +10179,9 @@ export const logger = createLogger();
|
|
|
9799
10179
|
import { logger } from '../config/logger.ts';
|
|
9800
10180
|
|
|
9801
10181
|
export const api = new RestApi('Api', {
|
|
10182
|
+
// The project is the app.
|
|
10183
|
+
path: '.',
|
|
10184
|
+
|
|
9802
10185
|
// Typed out rather than omitted: an API that ships open because a field was
|
|
9803
10186
|
// left off is the one default worth refusing to have.
|
|
9804
10187
|
defaultAuthorizer: 'none',
|
|
@@ -9821,7 +10204,7 @@ export const envParser = new EnvironmentParser({ ...process.env, ...Credentials
|
|
|
9821
10204
|
export const config = envParser
|
|
9822
10205
|
.create((get) => ({
|
|
9823
10206
|
nodeEnv: get('NODE_ENV').enum(['development', 'test', 'production']).default('development'),
|
|
9824
|
-
stage:
|
|
10207
|
+
stage: ${stageEnv(options.stages)},
|
|
9825
10208
|
}))
|
|
9826
10209
|
.parse();
|
|
9827
10210
|
`
|
|
@@ -9973,7 +10356,7 @@ export const authService = {
|
|
|
9973
10356
|
headers: { cookie },
|
|
9974
10357
|
});
|
|
9975
10358
|
if (!res.ok) return null;
|
|
9976
|
-
return res.json();
|
|
10359
|
+
return (await res.json()) as Session | null;
|
|
9977
10360
|
},
|
|
9978
10361
|
};
|
|
9979
10362
|
},
|
|
@@ -9986,7 +10369,6 @@ export const authService = {
|
|
|
9986
10369
|
import { api } from '${constructsImport("api")}';${options.database ? `
|
|
9987
10370
|
import { database } from '${constructsImport("database")}';` : ""}
|
|
9988
10371
|
import { authService, type Session } from './services/auth.ts';
|
|
9989
|
-
import { logger } from './config/logger.ts';
|
|
9990
10372
|
|
|
9991
10373
|
/**
|
|
9992
10374
|
* The shared endpoint factory — no session required.
|
|
@@ -10044,7 +10426,8 @@ import { database } from '${constructsImport("database")}';` : ""}
|
|
|
10044
10426
|
export const router = api.endpoints${options.database ? ".database(database)" : ""};
|
|
10045
10427
|
`
|
|
10046
10428
|
});
|
|
10047
|
-
if (options.database && declares) files$1.push(...databaseFiles(
|
|
10429
|
+
if (options.database && declares) files$1.push(...databaseFiles());
|
|
10430
|
+
else if (options.database) files$1.push(usersMigration());
|
|
10048
10431
|
if (services.storage && declares) files$1.push({
|
|
10049
10432
|
path: "src/constructs/storage.ts",
|
|
10050
10433
|
content: `import { ObjectStorage } from '@geekmidas/constructs/object-storage';
|
|
@@ -10194,9 +10577,9 @@ const minimalTemplate = {
|
|
|
10194
10577
|
"@geekmidas/rate-limit": GEEKMIDAS_VERSIONS["@geekmidas/rate-limit"],
|
|
10195
10578
|
"@geekmidas/schema": GEEKMIDAS_VERSIONS["@geekmidas/schema"],
|
|
10196
10579
|
"@hono/node-server": "~1.14.1",
|
|
10197
|
-
hono:
|
|
10198
|
-
pino:
|
|
10199
|
-
zod:
|
|
10580
|
+
hono: DEPENDENCY_VERSIONS.hono,
|
|
10581
|
+
pino: DEPENDENCY_VERSIONS.pino,
|
|
10582
|
+
zod: DEPENDENCY_VERSIONS.zod
|
|
10200
10583
|
},
|
|
10201
10584
|
devDependencies: {
|
|
10202
10585
|
"@biomejs/biome": "~2.3.0",
|
|
@@ -10220,7 +10603,7 @@ const minimalTemplate = {
|
|
|
10220
10603
|
},
|
|
10221
10604
|
files: (options) => {
|
|
10222
10605
|
const { loggerType, routesStructure, name: name$1 } = options;
|
|
10223
|
-
const db = databaseFor(
|
|
10606
|
+
const db = databaseFor();
|
|
10224
10607
|
const loggerContent = `import { createLogger } from '@geekmidas/logger/${loggerType}';
|
|
10225
10608
|
|
|
10226
10609
|
export const logger = createLogger();
|
|
@@ -10240,6 +10623,9 @@ export const logger = createLogger();
|
|
|
10240
10623
|
import { logger } from '../config/logger.ts';
|
|
10241
10624
|
|
|
10242
10625
|
export const api = new RestApi('Api', {
|
|
10626
|
+
// The project is the app.
|
|
10627
|
+
path: '.',
|
|
10628
|
+
|
|
10243
10629
|
// Typed out rather than omitted: an API that ships open because a field was
|
|
10244
10630
|
// left off is the one default worth refusing to have.
|
|
10245
10631
|
defaultAuthorizer: 'none',
|
|
@@ -10262,7 +10648,7 @@ export const envParser = new EnvironmentParser({ ...process.env, ...Credentials
|
|
|
10262
10648
|
export const config = envParser
|
|
10263
10649
|
.create((get) => ({
|
|
10264
10650
|
nodeEnv: get('NODE_ENV').enum(['development', 'test', 'production']).default('development'),
|
|
10265
|
-
stage:
|
|
10651
|
+
stage: ${stageEnv(options.stages)},
|
|
10266
10652
|
}))
|
|
10267
10653
|
.parse();
|
|
10268
10654
|
`
|
|
@@ -10289,7 +10675,7 @@ export const healthEndpoint = api
|
|
|
10289
10675
|
`
|
|
10290
10676
|
}
|
|
10291
10677
|
];
|
|
10292
|
-
if (options.database) files$1.push(...databaseFiles(
|
|
10678
|
+
if (options.database) files$1.push(...databaseFiles());
|
|
10293
10679
|
if (options.telescope) files$1.push({
|
|
10294
10680
|
path: "src/config/telescope.ts",
|
|
10295
10681
|
content: `import { Telescope } from '@geekmidas/telescope';
|
|
@@ -10353,8 +10739,8 @@ const serverlessTemplate = {
|
|
|
10353
10739
|
"@geekmidas/rate-limit": GEEKMIDAS_VERSIONS["@geekmidas/rate-limit"],
|
|
10354
10740
|
"@geekmidas/schema": GEEKMIDAS_VERSIONS["@geekmidas/schema"],
|
|
10355
10741
|
"@hono/node-server": "~1.14.1",
|
|
10356
|
-
hono:
|
|
10357
|
-
pino:
|
|
10742
|
+
hono: DEPENDENCY_VERSIONS.hono,
|
|
10743
|
+
pino: DEPENDENCY_VERSIONS.pino
|
|
10358
10744
|
},
|
|
10359
10745
|
devDependencies: {
|
|
10360
10746
|
"@biomejs/biome": "~2.3.0",
|
|
@@ -10397,6 +10783,9 @@ export const logger = createLogger();
|
|
|
10397
10783
|
import { logger } from '../config/logger.ts';
|
|
10398
10784
|
|
|
10399
10785
|
export const api = new RestApi('Api', {
|
|
10786
|
+
// The project is the app.
|
|
10787
|
+
path: '.',
|
|
10788
|
+
|
|
10400
10789
|
// Typed out rather than omitted: an API that ships open because a field was
|
|
10401
10790
|
// left off is the one default worth refusing to have.
|
|
10402
10791
|
defaultAuthorizer: 'none',
|
|
@@ -10419,7 +10808,7 @@ export const envParser = new EnvironmentParser({ ...process.env, ...Credentials
|
|
|
10419
10808
|
export const config = envParser
|
|
10420
10809
|
.create((get) => ({
|
|
10421
10810
|
nodeEnv: get('NODE_ENV').enum(['development', 'test', 'production']).default('development'),
|
|
10422
|
-
stage:
|
|
10811
|
+
stage: ${stageEnv(options.stages)},
|
|
10423
10812
|
}))
|
|
10424
10813
|
.parse();
|
|
10425
10814
|
`
|
|
@@ -10449,19 +10838,27 @@ export const healthEndpoint = api
|
|
|
10449
10838
|
},
|
|
10450
10839
|
{
|
|
10451
10840
|
path: "src/functions/hello.ts",
|
|
10452
|
-
content: `import {
|
|
10453
|
-
import {
|
|
10841
|
+
content: `import { z } from 'zod';
|
|
10842
|
+
import { worker } from '~/constructs/worker.ts';
|
|
10454
10843
|
|
|
10455
|
-
export const helloFunction =
|
|
10844
|
+
export const helloFunction = worker
|
|
10456
10845
|
.input(z.object({ name: z.string() }))
|
|
10457
10846
|
.output(z.object({ message: z.string() }))
|
|
10458
10847
|
.handle(async ({ input }) => ({
|
|
10459
10848
|
message: \`Hello, \${input.name}!\`,
|
|
10460
10849
|
}));
|
|
10850
|
+
`
|
|
10851
|
+
},
|
|
10852
|
+
{
|
|
10853
|
+
path: "src/constructs/worker.ts",
|
|
10854
|
+
content: `import { Worker } from '@geekmidas/constructs/worker';
|
|
10855
|
+
import { logger } from '../config/logger.ts';
|
|
10856
|
+
|
|
10857
|
+
export const worker = new Worker('Jobs', { logger });
|
|
10461
10858
|
`
|
|
10462
10859
|
}
|
|
10463
10860
|
];
|
|
10464
|
-
if (options.database) files$1.push(...databaseFiles(
|
|
10861
|
+
if (options.database) files$1.push(...databaseFiles());
|
|
10465
10862
|
if (options.telescope) files$1.push({
|
|
10466
10863
|
path: "src/config/telescope.ts",
|
|
10467
10864
|
content: `import { Telescope } from '@geekmidas/telescope';
|
|
@@ -10470,7 +10867,7 @@ import { InMemoryStorage } from '@geekmidas/telescope/storage/memory';
|
|
|
10470
10867
|
// Note: For production Lambda, consider using a persistent storage
|
|
10471
10868
|
export const telescope = new Telescope({
|
|
10472
10869
|
storage: new InMemoryStorage({ maxEntries: 50 }),
|
|
10473
|
-
enabled: process.env.STAGE === '
|
|
10870
|
+
enabled: process.env.STAGE === '${options.stages.local}',
|
|
10474
10871
|
});
|
|
10475
10872
|
`
|
|
10476
10873
|
});
|
|
@@ -10490,12 +10887,11 @@ export const telescope = new Telescope({
|
|
|
10490
10887
|
* that was missing: it takes no authorizer, publishes no address, and hands out
|
|
10491
10888
|
* factories already carrying its logger.
|
|
10492
10889
|
*
|
|
10493
|
-
* It scaffolds
|
|
10494
|
-
*
|
|
10495
|
-
*
|
|
10496
|
-
*
|
|
10497
|
-
*
|
|
10498
|
-
* when there is a scheduler to run it.
|
|
10890
|
+
* It scaffolds a cron when the project has a database, and not otherwise. A
|
|
10891
|
+
* server fires its own crons, and the schedule lives in Postgres so that
|
|
10892
|
+
* running more than one replica still fires each job once — so a worker names
|
|
10893
|
+
* the database it keeps schedules in. Without one there is nowhere to keep
|
|
10894
|
+
* them, and an example that could not run is worse than none.
|
|
10499
10895
|
*/
|
|
10500
10896
|
const workerTemplate = {
|
|
10501
10897
|
name: "worker",
|
|
@@ -10507,7 +10903,7 @@ const workerTemplate = {
|
|
|
10507
10903
|
"@geekmidas/events": GEEKMIDAS_VERSIONS["@geekmidas/events"],
|
|
10508
10904
|
"@geekmidas/logger": GEEKMIDAS_VERSIONS["@geekmidas/logger"],
|
|
10509
10905
|
"@geekmidas/schema": GEEKMIDAS_VERSIONS["@geekmidas/schema"],
|
|
10510
|
-
pino:
|
|
10906
|
+
pino: DEPENDENCY_VERSIONS.pino
|
|
10511
10907
|
},
|
|
10512
10908
|
devDependencies: {
|
|
10513
10909
|
"@biomejs/biome": "~2.3.0",
|
|
@@ -10529,7 +10925,7 @@ const workerTemplate = {
|
|
|
10529
10925
|
"fmt:check": "biome format ."
|
|
10530
10926
|
},
|
|
10531
10927
|
files: (options) => {
|
|
10532
|
-
const { loggerType, name: name$1 } = options;
|
|
10928
|
+
const { loggerType, name: name$1, database } = options;
|
|
10533
10929
|
const loggerContent = `import { createLogger } from '@geekmidas/logger/${loggerType}';
|
|
10534
10930
|
|
|
10535
10931
|
export const logger = createLogger();
|
|
@@ -10538,9 +10934,9 @@ export const logger = createLogger();
|
|
|
10538
10934
|
{
|
|
10539
10935
|
path: "src/constructs/worker.ts",
|
|
10540
10936
|
content: `import { Worker } from '@geekmidas/constructs/worker';
|
|
10541
|
-
import { logger } from '../config/logger.ts';
|
|
10937
|
+
import { logger } from '../config/logger.ts';${database ? "\nimport { database } from './database.ts';" : ""}
|
|
10542
10938
|
|
|
10543
|
-
export const worker = new Worker('Jobs', { logger });
|
|
10939
|
+
export const worker = new Worker('Jobs', { logger })${database ? ".database(database)" : ""};
|
|
10544
10940
|
`
|
|
10545
10941
|
},
|
|
10546
10942
|
{
|
|
@@ -10555,7 +10951,7 @@ export const envParser = new EnvironmentParser({ ...process.env, ...Credentials
|
|
|
10555
10951
|
export const config = envParser
|
|
10556
10952
|
.create((get) => ({
|
|
10557
10953
|
nodeEnv: get('NODE_ENV').enum(['development', 'test', 'production']).default('development'),
|
|
10558
|
-
stage:
|
|
10954
|
+
stage: ${stageEnv(options.stages)},
|
|
10559
10955
|
}))
|
|
10560
10956
|
.parse();
|
|
10561
10957
|
`
|
|
@@ -10608,7 +11004,7 @@ export const eventsPublisherService = {
|
|
|
10608
11004
|
content: `import { worker } from '~/constructs/worker.ts';
|
|
10609
11005
|
import { eventsPublisherService } from '~/events/publisher.ts';
|
|
10610
11006
|
|
|
10611
|
-
export const userEventsSubscriber = worker
|
|
11007
|
+
export const userEventsSubscriber = worker
|
|
10612
11008
|
.publisher(eventsPublisherService)
|
|
10613
11009
|
.subscribe(['user.created', 'user.updated'])
|
|
10614
11010
|
.handle(async ({ events, logger }) => {
|
|
@@ -10630,7 +11026,22 @@ export const userEventsSubscriber = worker.subscribers
|
|
|
10630
11026
|
`
|
|
10631
11027
|
}
|
|
10632
11028
|
];
|
|
10633
|
-
if (
|
|
11029
|
+
if (database) files$1.push({
|
|
11030
|
+
path: "src/crons/cleanup.ts",
|
|
11031
|
+
content: `import { worker } from '~/constructs/worker.ts';
|
|
11032
|
+
|
|
11033
|
+
export const cleanup = worker
|
|
11034
|
+
.cron('rate(1 day)')
|
|
11035
|
+
.handle(async ({ logger }) => {
|
|
11036
|
+
logger.info('Running cleanup');
|
|
11037
|
+
|
|
11038
|
+
// Delete old sessions, clear temp files, whatever falls out of date.
|
|
11039
|
+
|
|
11040
|
+
logger.info('Cleanup complete');
|
|
11041
|
+
});
|
|
11042
|
+
`
|
|
11043
|
+
});
|
|
11044
|
+
if (options.database) files$1.push(...databaseFiles());
|
|
10634
11045
|
if (options.telescope) files$1.push({
|
|
10635
11046
|
path: "src/config/telescope.ts",
|
|
10636
11047
|
content: `import { Telescope } from '@geekmidas/telescope';
|
|
@@ -10651,7 +11062,7 @@ export const telescope = new Telescope({
|
|
|
10651
11062
|
/**
|
|
10652
11063
|
* OpenAPI output path (fixed, not configurable)
|
|
10653
11064
|
*/
|
|
10654
|
-
const OPENAPI_OUTPUT_PATH$1 = "./.gkm/openapi.ts";
|
|
11065
|
+
const OPENAPI_OUTPUT_PATH$1 = "./.gkm/openapi/api.ts";
|
|
10655
11066
|
/**
|
|
10656
11067
|
* Endpoints output path (generated by gkm dev/build)
|
|
10657
11068
|
*/
|
|
@@ -10757,15 +11168,23 @@ const packageManagerChoices = [
|
|
|
10757
11168
|
/**
|
|
10758
11169
|
* Deploy target choices for prompts
|
|
10759
11170
|
*/
|
|
10760
|
-
const deployTargetChoices = [
|
|
10761
|
-
|
|
10762
|
-
|
|
10763
|
-
|
|
10764
|
-
|
|
10765
|
-
|
|
10766
|
-
|
|
10767
|
-
|
|
10768
|
-
|
|
11171
|
+
const deployTargetChoices = [
|
|
11172
|
+
{
|
|
11173
|
+
title: "Dokploy",
|
|
11174
|
+
value: "dokploy",
|
|
11175
|
+
description: "Deploy to Dokploy (Docker-based hosting)"
|
|
11176
|
+
},
|
|
11177
|
+
{
|
|
11178
|
+
title: "AWS (SST)",
|
|
11179
|
+
value: "sst",
|
|
11180
|
+
description: "Deploy to your AWS account through SST"
|
|
11181
|
+
},
|
|
11182
|
+
{
|
|
11183
|
+
title: "Configure later",
|
|
11184
|
+
value: "none",
|
|
11185
|
+
description: "Skip deployment setup for now"
|
|
11186
|
+
}
|
|
11187
|
+
];
|
|
10769
11188
|
/**
|
|
10770
11189
|
* Services choices for multi-select prompt
|
|
10771
11190
|
*/
|
|
@@ -10858,12 +11277,12 @@ function generatePackageJson(options, template) {
|
|
|
10858
11277
|
if (declares && services.mail) dependencies$1["@geekmidas/emailkit"] = GEEKMIDAS_VERSIONS["@geekmidas/emailkit"];
|
|
10859
11278
|
if (database) {
|
|
10860
11279
|
dependencies$1["@geekmidas/db"] = GEEKMIDAS_VERSIONS["@geekmidas/db"];
|
|
10861
|
-
dependencies$1.kysely =
|
|
10862
|
-
dependencies$1.pg =
|
|
10863
|
-
devDependencies$1["@types/pg"] = "~8.
|
|
11280
|
+
dependencies$1.kysely = DEPENDENCY_VERSIONS.kysely;
|
|
11281
|
+
dependencies$1.pg = DEPENDENCY_VERSIONS.pg;
|
|
11282
|
+
devDependencies$1["@types/pg"] = "~8.23.1";
|
|
10864
11283
|
devDependencies$1["@geekmidas/testkit"] = GEEKMIDAS_VERSIONS["@geekmidas/testkit"];
|
|
10865
11284
|
devDependencies$1["@faker-js/faker"] = "~9.8.0";
|
|
10866
|
-
devDependencies$1["kysely-ctl"] = "
|
|
11285
|
+
devDependencies$1["kysely-ctl"] = DEPENDENCY_VERSIONS["kysely-ctl"];
|
|
10867
11286
|
devDependencies$1["vite-tsconfig-paths"] = "~5.1.0";
|
|
10868
11287
|
}
|
|
10869
11288
|
if (monorepo) {
|
|
@@ -10883,6 +11302,8 @@ function generatePackageJson(options, template) {
|
|
|
10883
11302
|
const appName = pathParts[pathParts.length - 1] || "api";
|
|
10884
11303
|
packageName = `@${name$1}/${appName}`;
|
|
10885
11304
|
}
|
|
11305
|
+
dependencies$1["@geekmidas/client"] = GEEKMIDAS_VERSIONS["@geekmidas/client"];
|
|
11306
|
+
dependencies$1["@tanstack/react-query"] = DEPENDENCY_VERSIONS["@tanstack/react-query"];
|
|
10886
11307
|
const packageJson = {
|
|
10887
11308
|
name: packageName,
|
|
10888
11309
|
version: "0.0.1",
|
|
@@ -10920,7 +11341,7 @@ function generateSourceFiles(options, template) {
|
|
|
10920
11341
|
*/
|
|
10921
11342
|
function generateTestFiles(options, _template) {
|
|
10922
11343
|
if (!options.database) return [];
|
|
10923
|
-
const db = databaseFor(
|
|
11344
|
+
const db = databaseFor();
|
|
10924
11345
|
const declares = !options.monorepo;
|
|
10925
11346
|
const schema = declares ? "../src/constructs/database.ts" : "~/services/database.ts";
|
|
10926
11347
|
const runtimeUrl = declares ? db.urlKey : "DATABASE_URL";
|
|
@@ -10956,14 +11377,16 @@ import pg from 'pg';
|
|
|
10956
11377
|
import { wrapVitestKyselyTransaction } from '@geekmidas/testkit/kysely';
|
|
10957
11378
|
import type { Database } from '${schema}';
|
|
10958
11379
|
|
|
10959
|
-
const
|
|
11380
|
+
const db = new Kysely<Database>({
|
|
10960
11381
|
dialect: new PostgresDialect({
|
|
10961
11382
|
pool: new pg.Pool({ connectionString: process.env.${runtimeUrl} }),
|
|
10962
11383
|
}),
|
|
10963
11384
|
});
|
|
10964
11385
|
|
|
10965
11386
|
export const it = wrapVitestKyselyTransaction<Database>(itVitest, {
|
|
10966
|
-
|
|
11387
|
+
// A function, or a construct: an instance is taken for a construct and has
|
|
11388
|
+
// no service to register.
|
|
11389
|
+
connection: () => db,
|
|
10967
11390
|
});
|
|
10968
11391
|
`
|
|
10969
11392
|
},
|
|
@@ -11048,10 +11471,10 @@ export const usersBuilder = KyselyFactory.createBuilder<Database, 'users'>(
|
|
|
11048
11471
|
import { it } from './config.ts';
|
|
11049
11472
|
|
|
11050
11473
|
describe('example', () => {
|
|
11051
|
-
it('should have a working test setup', async ({
|
|
11052
|
-
//
|
|
11053
|
-
//
|
|
11054
|
-
expect(
|
|
11474
|
+
it('should have a working test setup', async ({ trx }) => {
|
|
11475
|
+
// trx is a Kysely transaction; everything in it is rolled back after the
|
|
11476
|
+
// test
|
|
11477
|
+
expect(trx).toBeDefined();
|
|
11055
11478
|
});
|
|
11056
11479
|
});
|
|
11057
11480
|
`
|
|
@@ -12302,8 +12725,8 @@ export const Alert: Story = {
|
|
|
12302
12725
|
),
|
|
12303
12726
|
};
|
|
12304
12727
|
`;
|
|
12305
|
-
const componentsUiIndex = `export { Button, type ButtonProps, buttonVariants } from './button.tsx';
|
|
12306
|
-
export { Input } from './input.tsx';
|
|
12728
|
+
const componentsUiIndex = `export { Button, type ButtonProps, buttonVariants } from './button/index.tsx';
|
|
12729
|
+
export { Input } from './input/index.tsx';
|
|
12307
12730
|
export {
|
|
12308
12731
|
Card,
|
|
12309
12732
|
CardHeader,
|
|
@@ -12311,17 +12734,17 @@ export {
|
|
|
12311
12734
|
CardTitle,
|
|
12312
12735
|
CardDescription,
|
|
12313
12736
|
CardContent,
|
|
12314
|
-
} from './card.tsx';
|
|
12315
|
-
export { Label } from './label.tsx';
|
|
12316
|
-
export { Badge, type BadgeProps, badgeVariants } from './badge.tsx';
|
|
12317
|
-
export { Separator } from './separator.tsx';
|
|
12318
|
-
export { Tabs, TabsList, TabsTrigger, TabsContent } from './tabs.tsx';
|
|
12737
|
+
} from './card/index.tsx';
|
|
12738
|
+
export { Label } from './label/index.tsx';
|
|
12739
|
+
export { Badge, type BadgeProps, badgeVariants } from './badge/index.tsx';
|
|
12740
|
+
export { Separator } from './separator/index.tsx';
|
|
12741
|
+
export { Tabs, TabsList, TabsTrigger, TabsContent } from './tabs/index.tsx';
|
|
12319
12742
|
export {
|
|
12320
12743
|
Tooltip,
|
|
12321
12744
|
TooltipTrigger,
|
|
12322
12745
|
TooltipContent,
|
|
12323
12746
|
TooltipProvider,
|
|
12324
|
-
} from './tooltip.tsx';
|
|
12747
|
+
} from './tooltip/index.tsx';
|
|
12325
12748
|
export {
|
|
12326
12749
|
Dialog,
|
|
12327
12750
|
DialogPortal,
|
|
@@ -12333,7 +12756,7 @@ export {
|
|
|
12333
12756
|
DialogFooter,
|
|
12334
12757
|
DialogTitle,
|
|
12335
12758
|
DialogDescription,
|
|
12336
|
-
} from './dialog.tsx';
|
|
12759
|
+
} from './dialog/index.tsx';
|
|
12337
12760
|
`;
|
|
12338
12761
|
const buttonIndexTsx = buttonTsx;
|
|
12339
12762
|
const inputIndexTsx = inputTsx;
|
|
@@ -12501,8 +12924,8 @@ function generateWebAppFiles(options) {
|
|
|
12501
12924
|
[uiPackage]: "workspace:*",
|
|
12502
12925
|
"@geekmidas/client": GEEKMIDAS_VERSIONS["@geekmidas/client"],
|
|
12503
12926
|
"@geekmidas/envkit": GEEKMIDAS_VERSIONS["@geekmidas/envkit"],
|
|
12504
|
-
"@tanstack/react-query": "
|
|
12505
|
-
"better-auth": "
|
|
12927
|
+
"@tanstack/react-query": DEPENDENCY_VERSIONS["@tanstack/react-query"],
|
|
12928
|
+
"better-auth": DEPENDENCY_VERSIONS["better-auth"],
|
|
12506
12929
|
next: "~16.1.0",
|
|
12507
12930
|
react: "~19.2.0",
|
|
12508
12931
|
"react-dom": "~19.2.0"
|
|
@@ -12558,7 +12981,7 @@ export default nextConfig;
|
|
|
12558
12981
|
baseUrl: ".",
|
|
12559
12982
|
paths: {
|
|
12560
12983
|
"~/*": ["./src/*", "../../packages/ui/src/*"],
|
|
12561
|
-
[`${apiPackage}/client`]: ["../../apps/api/.gkm/openapi.ts"],
|
|
12984
|
+
[`${apiPackage}/client`]: ["../../apps/api/.gkm/openapi/api.ts"],
|
|
12562
12985
|
[`${modelsPackage}`]: ["../../packages/models/src"],
|
|
12563
12986
|
[`${modelsPackage}/*`]: ["../../packages/models/src/*"],
|
|
12564
12987
|
[`${uiPackage}`]: ["../../packages/ui/src"],
|
|
@@ -12571,12 +12994,7 @@ export default nextConfig;
|
|
|
12571
12994
|
"**/*.tsx",
|
|
12572
12995
|
".next/types/**/*.ts"
|
|
12573
12996
|
],
|
|
12574
|
-
exclude: ["node_modules"]
|
|
12575
|
-
references: [
|
|
12576
|
-
{ path: "../../apps/api" },
|
|
12577
|
-
{ path: "../../packages/ui" },
|
|
12578
|
-
{ path: "../../packages/models" }
|
|
12579
|
-
]
|
|
12997
|
+
exclude: ["node_modules"]
|
|
12580
12998
|
};
|
|
12581
12999
|
const queryClientTs = `import { QueryClient } from '@tanstack/react-query';
|
|
12582
13000
|
|
|
@@ -12839,10 +13257,10 @@ function generateTanStackWebFiles(options) {
|
|
|
12839
13257
|
[uiPackage]: "workspace:*",
|
|
12840
13258
|
"@geekmidas/client": GEEKMIDAS_VERSIONS["@geekmidas/client"],
|
|
12841
13259
|
"@geekmidas/envkit": GEEKMIDAS_VERSIONS["@geekmidas/envkit"],
|
|
12842
|
-
"@tanstack/react-query": "
|
|
13260
|
+
"@tanstack/react-query": DEPENDENCY_VERSIONS["@tanstack/react-query"],
|
|
12843
13261
|
"@tanstack/react-router": "^1.87.0",
|
|
12844
13262
|
"@tanstack/react-start": "^1.87.0",
|
|
12845
|
-
"better-auth": "
|
|
13263
|
+
"better-auth": DEPENDENCY_VERSIONS["better-auth"],
|
|
12846
13264
|
react: "~19.2.0",
|
|
12847
13265
|
"react-dom": "~19.2.0"
|
|
12848
13266
|
},
|
|
@@ -12910,7 +13328,7 @@ export default defineConfig({
|
|
|
12910
13328
|
[`${modelsPackage}/*`]: ["../../packages/models/src/*"],
|
|
12911
13329
|
[uiPackage]: ["../../packages/ui/src"],
|
|
12912
13330
|
[`${uiPackage}/*`]: ["../../packages/ui/src/*"],
|
|
12913
|
-
[`${apiPackage}/client`]: ["../../apps/api/.gkm/openapi.ts"]
|
|
13331
|
+
[`${apiPackage}/client`]: ["../../apps/api/.gkm/openapi/api.ts"]
|
|
12914
13332
|
}
|
|
12915
13333
|
},
|
|
12916
13334
|
include: [
|
|
@@ -13200,12 +13618,27 @@ function detectPackageManager(cwd = process.cwd()) {
|
|
|
13200
13618
|
if (userAgent.includes("bun")) return "bun";
|
|
13201
13619
|
return "npm";
|
|
13202
13620
|
}
|
|
13621
|
+
/** `gkm init`'s flags, which a name can swallow when a space goes missing. */
|
|
13622
|
+
const INIT_FLAGS = [
|
|
13623
|
+
"yes",
|
|
13624
|
+
"name",
|
|
13625
|
+
"template",
|
|
13626
|
+
"monorepo",
|
|
13627
|
+
"api-path",
|
|
13628
|
+
"pm",
|
|
13629
|
+
"skip-install"
|
|
13630
|
+
];
|
|
13203
13631
|
/**
|
|
13204
13632
|
* Validate project name for npm package naming conventions
|
|
13205
13633
|
*/
|
|
13206
13634
|
function validateProjectName(name$1) {
|
|
13207
13635
|
if (!name$1) return "Project name is required";
|
|
13208
13636
|
if (!/^[a-z0-9-_@/.]+$/i.test(name$1)) return "Project name can only contain letters, numbers, hyphens, underscores, @, /, and .";
|
|
13637
|
+
const flag = name$1.match(/--(\w[\w-]*)$/)?.[1];
|
|
13638
|
+
if (flag && INIT_FLAGS.includes(flag)) {
|
|
13639
|
+
const intended = name$1.slice(0, -(flag.length + 2));
|
|
13640
|
+
return `"${name$1}" ends in the --${flag} flag. Did you mean \`${intended} --${flag}\`?`;
|
|
13641
|
+
}
|
|
13209
13642
|
const reserved = [
|
|
13210
13643
|
"node_modules",
|
|
13211
13644
|
".git",
|
|
@@ -13245,6 +13678,18 @@ function getRunCommand(pkgManager, script) {
|
|
|
13245
13678
|
default: return `npm run ${script}`;
|
|
13246
13679
|
}
|
|
13247
13680
|
}
|
|
13681
|
+
/**
|
|
13682
|
+
* Run a binary the project installed, through its package manager — so it is
|
|
13683
|
+
* the project's own version, not whatever \`npx\` would fetch.
|
|
13684
|
+
*/
|
|
13685
|
+
function getExecCommand(pkgManager, command) {
|
|
13686
|
+
switch (pkgManager) {
|
|
13687
|
+
case "pnpm": return `pnpm exec ${command}`;
|
|
13688
|
+
case "yarn": return `yarn ${command}`;
|
|
13689
|
+
case "bun": return `bunx ${command}`;
|
|
13690
|
+
default: return `npx --no-install ${command}`;
|
|
13691
|
+
}
|
|
13692
|
+
}
|
|
13248
13693
|
const lockfileByPm = {
|
|
13249
13694
|
pnpm: "pnpm-lock.yaml",
|
|
13250
13695
|
yarn: "yarn.lock",
|
|
@@ -13315,12 +13760,31 @@ function findWorkspacePackages(cwd, pm) {
|
|
|
13315
13760
|
|
|
13316
13761
|
//#endregion
|
|
13317
13762
|
//#region src/init/index.ts
|
|
13763
|
+
/** `' staging, prod '` → `['staging', 'prod']`. */
|
|
13764
|
+
const stageList = (value) => value.split(",").map((name$1) => name$1.trim()).filter(Boolean);
|
|
13765
|
+
/** `eu-west-1`, `us-gov-west-1`, `ap-southeast-2`. */
|
|
13766
|
+
const AWS_REGION = /^[a-z]{2}(-gov)?-[a-z]+-\d$/;
|
|
13318
13767
|
/**
|
|
13319
13768
|
* Main init command - scaffolds a new project
|
|
13320
13769
|
*/
|
|
13321
13770
|
async function initCommand(projectName, options = {}) {
|
|
13322
13771
|
const cwd = process.cwd();
|
|
13323
13772
|
const detectedPkgManager = detectPackageManager(cwd);
|
|
13773
|
+
if (options.deploy && !deployTargetChoices.some((choice) => choice.value === options.deploy)) throw new Error(`Unknown deploy target "${options.deploy}". Use ${deployTargetChoices.map((c) => c.value).join(", ")}.`);
|
|
13774
|
+
if (options.region && !AWS_REGION.test(options.region)) throw new Error(`"${options.region}" is not an AWS region. Use one like eu-west-1.`);
|
|
13775
|
+
if (options.stages || options.localStage || options.protectedStage) {
|
|
13776
|
+
const deployed = stageList(options.stages ?? "");
|
|
13777
|
+
const problems = [
|
|
13778
|
+
...require_workspace.deployedProblems(deployed),
|
|
13779
|
+
...options.localStage ? require_workspace.stageProblems({
|
|
13780
|
+
local: options.localStage,
|
|
13781
|
+
deployed
|
|
13782
|
+
}) : [],
|
|
13783
|
+
...options.protectedStage && !deployed.includes(options.protectedStage) ? [`--protected-stage "${options.protectedStage}" is not in --stages`] : []
|
|
13784
|
+
];
|
|
13785
|
+
if (problems.length) throw new Error([...new Set(problems)].join("\n"));
|
|
13786
|
+
}
|
|
13787
|
+
let deployedSoFar = [];
|
|
13324
13788
|
prompts.default.override({});
|
|
13325
13789
|
const onCancel = () => {
|
|
13326
13790
|
process.exit(0);
|
|
@@ -13371,12 +13835,49 @@ async function initCommand(projectName, options = {}) {
|
|
|
13371
13835
|
initial: packageManagerChoices.findIndex((c) => c.value === detectedPkgManager)
|
|
13372
13836
|
},
|
|
13373
13837
|
{
|
|
13374
|
-
type: options.yes ? null : "select",
|
|
13838
|
+
type: options.yes || options.deploy ? null : "select",
|
|
13375
13839
|
name: "deployTarget",
|
|
13376
13840
|
message: "Deployment target:",
|
|
13377
13841
|
choices: deployTargetChoices,
|
|
13378
13842
|
initial: 0
|
|
13379
13843
|
},
|
|
13844
|
+
{
|
|
13845
|
+
type: (_prev, values) => !options.yes && !options.region && (options.deploy ?? values.deployTarget) === "sst" ? "text" : null,
|
|
13846
|
+
name: "region",
|
|
13847
|
+
message: "AWS region (e.g. eu-west-1):",
|
|
13848
|
+
validate: (value) => AWS_REGION.test(value.trim()) || "An AWS region, like eu-west-1"
|
|
13849
|
+
},
|
|
13850
|
+
{
|
|
13851
|
+
type: options.yes || options.stages ? null : "text",
|
|
13852
|
+
name: "deployedStages",
|
|
13853
|
+
message: "Deployed stages, comma-separated (e.g. staging, prod):",
|
|
13854
|
+
validate: (value) => {
|
|
13855
|
+
const deployed = stageList(value);
|
|
13856
|
+
if (deployed.length === 0) return "At least one stage";
|
|
13857
|
+
return require_workspace.deployedProblems(deployed)[0] ?? true;
|
|
13858
|
+
}
|
|
13859
|
+
},
|
|
13860
|
+
{
|
|
13861
|
+
type: (_prev, values) => options.yes || options.protectedStage ? null : stageList(options.stages ?? values.deployedStages ?? "").length > 1 ? "select" : null,
|
|
13862
|
+
name: "protectedStage",
|
|
13863
|
+
message: "Which one is production (retained and protected)?",
|
|
13864
|
+
choices: (_prev, values) => stageList(options.stages ?? values.deployedStages ?? "").map((name$2) => ({
|
|
13865
|
+
title: name$2,
|
|
13866
|
+
value: name$2
|
|
13867
|
+
}))
|
|
13868
|
+
},
|
|
13869
|
+
{
|
|
13870
|
+
type: (_prev, values) => {
|
|
13871
|
+
deployedSoFar = stageList(options.stages ?? values.deployedStages ?? "");
|
|
13872
|
+
return options.yes || options.localStage ? null : "text";
|
|
13873
|
+
},
|
|
13874
|
+
name: "localStage",
|
|
13875
|
+
message: "Local stage, what gkm dev runs as (e.g. dev):",
|
|
13876
|
+
validate: (value) => require_workspace.stageProblems({
|
|
13877
|
+
local: value.trim(),
|
|
13878
|
+
deployed: deployedSoFar
|
|
13879
|
+
})[0] ?? true
|
|
13880
|
+
},
|
|
13380
13881
|
{
|
|
13381
13882
|
type: options.yes ? null : "confirm",
|
|
13382
13883
|
name: "telescope",
|
|
@@ -13442,7 +13943,7 @@ async function initCommand(projectName, options = {}) {
|
|
|
13442
13943
|
};
|
|
13443
13944
|
if (services.events === "pgboss") services.db = true;
|
|
13444
13945
|
const pkgManager = options.pm ? options.pm : options.yes ? "pnpm" : answers.packageManager ?? detectedPkgManager;
|
|
13445
|
-
const deployTarget = options.yes ? "
|
|
13946
|
+
const deployTarget = options.deploy ?? (options.yes ? "none" : answers.deployTarget ?? "none");
|
|
13446
13947
|
const database = services.db;
|
|
13447
13948
|
const frontendFramework = isFullstack ? options.yes ? "nextjs" : answers.frontendFramework ?? "nextjs" : void 0;
|
|
13448
13949
|
const templateOptions = {
|
|
@@ -13457,6 +13958,8 @@ async function initCommand(projectName, options = {}) {
|
|
|
13457
13958
|
apiPath: monorepo ? options.apiPath ?? "apps/api" : "",
|
|
13458
13959
|
packageManager: pkgManager,
|
|
13459
13960
|
deployTarget,
|
|
13961
|
+
stages: resolveStages(options, answers),
|
|
13962
|
+
...deployTarget === "sst" ? { region: options.region ?? (options.yes ? "eu-west-1" : answers.region?.trim()) } : {},
|
|
13460
13963
|
services,
|
|
13461
13964
|
frontendFramework
|
|
13462
13965
|
};
|
|
@@ -13532,12 +14035,12 @@ async function initCommand(projectName, options = {}) {
|
|
|
13532
14035
|
if (services.mail) secretServices.push("mailpit");
|
|
13533
14036
|
if (services.events === "sns") secretServices.push("localstack");
|
|
13534
14037
|
if (services.events === "rabbitmq") secretServices.push("rabbitmq");
|
|
13535
|
-
const
|
|
14038
|
+
const local = templateOptions.stages.local;
|
|
14039
|
+
const devSecrets = require_generator.createStageSecrets(local, secretServices, {
|
|
13536
14040
|
projectName: name$1,
|
|
13537
14041
|
eventsBackend: services.events
|
|
13538
14042
|
});
|
|
13539
14043
|
const customSecrets = {
|
|
13540
|
-
NODE_ENV: "development",
|
|
13541
14044
|
PORT: "3000",
|
|
13542
14045
|
LOG_LEVEL: "debug",
|
|
13543
14046
|
JWT_SECRET: `dev-${Date.now()}-${Math.random().toString(36).slice(2)}`
|
|
@@ -13557,8 +14060,8 @@ async function initCommand(projectName, options = {}) {
|
|
|
13557
14060
|
}
|
|
13558
14061
|
devSecrets.custom = customSecrets;
|
|
13559
14062
|
await require_storage.writeStageSecrets(devSecrets, targetDir);
|
|
13560
|
-
const keyPath = require_storage.getKeyPath(
|
|
13561
|
-
console.log(` Secrets: .gkm/secrets
|
|
14063
|
+
const keyPath = require_storage.getKeyPath(local, name$1);
|
|
14064
|
+
console.log(` Secrets: .gkm/secrets/${local}.json (encrypted)`);
|
|
13562
14065
|
console.log(` Key: ${keyPath}\n`);
|
|
13563
14066
|
if (!options.skipInstall) {
|
|
13564
14067
|
console.log("\n📦 Installing dependencies...\n");
|
|
@@ -13571,11 +14074,13 @@ async function initCommand(projectName, options = {}) {
|
|
|
13571
14074
|
console.error("Failed to install dependencies");
|
|
13572
14075
|
}
|
|
13573
14076
|
try {
|
|
13574
|
-
(0, node_child_process.execSync)("
|
|
14077
|
+
(0, node_child_process.execSync)(getExecCommand(pkgManager, "biome check --write ."), {
|
|
13575
14078
|
cwd: targetDir,
|
|
13576
14079
|
stdio: "inherit"
|
|
13577
14080
|
});
|
|
13578
|
-
} catch {
|
|
14081
|
+
} catch {
|
|
14082
|
+
console.warn(`\n⚠️ Formatting did not finish cleanly. Run ${getRunCommand(pkgManager, "fmt")} to see why.`);
|
|
14083
|
+
}
|
|
13579
14084
|
}
|
|
13580
14085
|
console.log("\n📦 Initializing git repository...\n");
|
|
13581
14086
|
try {
|
|
@@ -13647,19 +14152,46 @@ function printNextSteps(projectName, options, pkgManager) {
|
|
|
13647
14152
|
console.log(` └── turbo.json # Turbo config`);
|
|
13648
14153
|
console.log("");
|
|
13649
14154
|
}
|
|
14155
|
+
const { local, deployed } = options.stages;
|
|
13650
14156
|
console.log("🔐 Secrets management:");
|
|
13651
|
-
console.log(` gkm secrets:show --stage
|
|
13652
|
-
console.log(` gkm secrets:set KEY VALUE --stage
|
|
13653
|
-
console.log(` gkm secrets:init --stage
|
|
14157
|
+
console.log(` gkm secrets:show --stage ${local} # View secrets`);
|
|
14158
|
+
console.log(` gkm secrets:set KEY VALUE --stage ${local} # Add secret`);
|
|
14159
|
+
for (const stage of deployed) console.log(` gkm secrets:init --stage ${stage} # Create ${stage} secrets`);
|
|
13654
14160
|
console.log("");
|
|
13655
14161
|
if (options.deployTarget === "dokploy") {
|
|
13656
14162
|
console.log("🚀 Deployment:");
|
|
13657
|
-
console.log(` ${getRunCommand(pkgManager,
|
|
14163
|
+
for (const stage of deployed) console.log(` ${getRunCommand(pkgManager, `deploy:${stage}`)}`);
|
|
14164
|
+
console.log("");
|
|
14165
|
+
}
|
|
14166
|
+
if (options.deployTarget === "sst") {
|
|
14167
|
+
console.log("🚀 Deployment (AWS, through SST):");
|
|
14168
|
+
for (const stage of deployed) console.log(` ${getRunCommand(pkgManager, `deploy:${stage}`)} # gkm build --provider aws, then sst deploy`);
|
|
14169
|
+
console.log(` Uses your AWS credentials, in ${options.region}.`);
|
|
14170
|
+
if (options.services.mail) console.log(" Set MAIL_FROM to a sender verified in SES first.");
|
|
13658
14171
|
console.log("");
|
|
13659
14172
|
}
|
|
13660
14173
|
console.log("📚 Documentation: https://geekmidas.github.io/toolbox/");
|
|
13661
14174
|
console.log("");
|
|
13662
14175
|
}
|
|
14176
|
+
/**
|
|
14177
|
+
* The stages the project declares, from the answers or the flags.
|
|
14178
|
+
*
|
|
14179
|
+
* `--yes` with no stage flags has nobody to ask and needs an answer, so it
|
|
14180
|
+
* takes `local`, and one deployed stage, `production`, that is protected.
|
|
14181
|
+
*/
|
|
14182
|
+
function resolveStages(options, answers) {
|
|
14183
|
+
const deployed = stageList(options.stages ?? answers.deployedStages ?? (options.yes ? "production" : ""));
|
|
14184
|
+
const kept = options.protectedStage ?? answers.protectedStage ?? (deployed.length === 1 || options.yes ? deployed[0] : void 0);
|
|
14185
|
+
const local = (options.localStage ?? answers.localStage ?? (options.yes ? "local" : "")).trim();
|
|
14186
|
+
const stages = {
|
|
14187
|
+
local,
|
|
14188
|
+
deployed,
|
|
14189
|
+
...kept ? { protected: [kept] } : {}
|
|
14190
|
+
};
|
|
14191
|
+
const problems = require_workspace.stageProblems(stages);
|
|
14192
|
+
if (problems.length) throw new Error(problems.join("\n"));
|
|
14193
|
+
return stages;
|
|
14194
|
+
}
|
|
13663
14195
|
|
|
13664
14196
|
//#endregion
|
|
13665
14197
|
//#region src/secrets/index.ts
|
|
@@ -13870,19 +14402,20 @@ function maskUrl(url) {
|
|
|
13870
14402
|
* Environment variables are sniffed to inject only what the app needs.
|
|
13871
14403
|
*/
|
|
13872
14404
|
async function testCommand(options = {}) {
|
|
13873
|
-
const stage = options.stage ?? "development";
|
|
13874
14405
|
const cwd = process.cwd();
|
|
14406
|
+
const stage = options.stage ?? await require_config.loadWorkspaceConfig(cwd).then((loaded) => loaded.workspace.stages.local).catch(() => void 0);
|
|
14407
|
+
if (!stage) throw new Error("No stage to test with: add `stages` to gkm.config.ts, or pass --stage.");
|
|
13875
14408
|
console.log(`\n🧪 Running tests with ${stage} environment...\n`);
|
|
13876
14409
|
const autoSetup = options.autoSetup || Boolean(process.env.GKM_AUTO_SETUP);
|
|
13877
14410
|
if (autoSetup) {
|
|
13878
|
-
const { ensureStageSecrets } = await Promise.resolve().then(() => require("./setup-
|
|
14411
|
+
const { ensureStageSecrets } = await Promise.resolve().then(() => require("./setup-BsZvZP6f.cjs"));
|
|
13879
14412
|
const generated = await ensureStageSecrets(stage, cwd);
|
|
13880
14413
|
if (generated) console.log(` 🔐 Generated fresh ${stage} secrets (auto-setup)`);
|
|
13881
14414
|
}
|
|
13882
14415
|
const defaultEnv = require_credentials$1.loadEnvFiles(".env");
|
|
13883
14416
|
if (defaultEnv.loaded.length > 0) console.log(` 📦 Loaded env: ${defaultEnv.loaded.join(", ")}`);
|
|
13884
14417
|
const result = await require_credentials$1.prepareEntryCredentials({
|
|
13885
|
-
|
|
14418
|
+
stage,
|
|
13886
14419
|
startDocker: true,
|
|
13887
14420
|
secretsFileName: "test-secrets.json",
|
|
13888
14421
|
resolveDockerPorts: "full",
|
|
@@ -14110,7 +14643,7 @@ program.name("gkm").description("GeekMidas backend framework CLI").version(packa
|
|
|
14110
14643
|
const opts = program.opts();
|
|
14111
14644
|
if (opts.debug) enableDebug();
|
|
14112
14645
|
});
|
|
14113
|
-
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) => {
|
|
14646
|
+
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)").option("--deploy <target>", "Where it deploys (dokploy, sst, none)").option("--region <region>", "AWS region for an SST deploy (e.g. eu-west-1)").option("--stages <names>", "Deployed stages, comma-separated (e.g. staging,prod)").option("--protected-stage <name>", "Which deployed stage is production").option("--local-stage <name>", "What gkm dev runs as (e.g. dev)").option("--region <region>", "AWS region for an SST deploy (e.g. eu-west-1)").action(async (name$1, options) => {
|
|
14114
14647
|
try {
|
|
14115
14648
|
const globalOptions = program.opts();
|
|
14116
14649
|
if (globalOptions.cwd) process.chdir(globalOptions.cwd);
|
|
@@ -14130,7 +14663,7 @@ program.command("trust").description("Trust the local edge's certificate authori
|
|
|
14130
14663
|
process.exit(1);
|
|
14131
14664
|
}
|
|
14132
14665
|
});
|
|
14133
|
-
program.command("setup").description("Reconcile declared constructs — containers, databases, buckets, secrets").option("--stage <stage>", "Stage name
|
|
14666
|
+
program.command("setup").description("Reconcile declared constructs — containers, databases, buckets, secrets").option("--stage <stage>", "Stage name (default: stages.local)").option("--force", "Regenerate secrets even if they exist").option("--skip-docker", "Skip starting Docker services").option("-y, --yes", "Skip prompts").action(async (options) => {
|
|
14134
14667
|
try {
|
|
14135
14668
|
const globalOptions = program.opts();
|
|
14136
14669
|
if (globalOptions.cwd) process.chdir(globalOptions.cwd);
|
|
@@ -14202,7 +14735,7 @@ program.command("exec").description("Run a command with secrets injected into Cr
|
|
|
14202
14735
|
process.exit(1);
|
|
14203
14736
|
}
|
|
14204
14737
|
});
|
|
14205
|
-
program.command("test").description("Run tests with secrets loaded from environment").option("--stage <stage>", "Stage to load secrets from
|
|
14738
|
+
program.command("test").description("Run tests with secrets loaded from environment").option("--stage <stage>", "Stage to load secrets from (default: stages.local)").option("--run", "Run tests once without watch mode").option("--watch", "Enable watch mode").option("--coverage", "Generate coverage report").option("--ui", "Open Vitest UI").option("--auto-setup", "Generate a fresh stage (secrets + key) from the workspace config when none exists (for CI; also via GKM_AUTO_SETUP)").argument("[pattern]", "Pattern to filter tests").action(async (pattern, options) => {
|
|
14206
14739
|
try {
|
|
14207
14740
|
const globalOptions = program.opts();
|
|
14208
14741
|
if (globalOptions.cwd) process.chdir(globalOptions.cwd);
|
|
@@ -14350,7 +14883,7 @@ program.command("secrets:push").description("Push secrets to remote provider (SS
|
|
|
14350
14883
|
const { workspace } = await loadWorkspaceConfig$1();
|
|
14351
14884
|
const secrets = await readStageSecrets$1(options.stage, workspace.root);
|
|
14352
14885
|
if (secrets) {
|
|
14353
|
-
const { derivedContainers: derivedContainers$1 } = await Promise.resolve().then(() => require("./workspace-
|
|
14886
|
+
const { derivedContainers: derivedContainers$1 } = await Promise.resolve().then(() => require("./workspace-XXtNhqJl.cjs"));
|
|
14354
14887
|
const result = reconcileMissingSecrets(secrets, workspace, await derivedContainers$1(workspace, options.stage));
|
|
14355
14888
|
if (result) {
|
|
14356
14889
|
await writeStageSecrets$1(result.secrets, workspace.root);
|
|
@@ -14379,7 +14912,7 @@ program.command("secrets:pull").description("Pull secrets from remote provider (
|
|
|
14379
14912
|
console.error(`No remote secrets found for stage "${options.stage}".`);
|
|
14380
14913
|
process.exit(1);
|
|
14381
14914
|
}
|
|
14382
|
-
const { derivedContainers: derivedContainers$1 } = await Promise.resolve().then(() => require("./workspace-
|
|
14915
|
+
const { derivedContainers: derivedContainers$1 } = await Promise.resolve().then(() => require("./workspace-XXtNhqJl.cjs"));
|
|
14383
14916
|
const result = reconcileMissingSecrets(secrets, workspace, await derivedContainers$1(workspace, options.stage));
|
|
14384
14917
|
if (result) {
|
|
14385
14918
|
secrets = result.secrets;
|
|
@@ -14393,7 +14926,7 @@ program.command("secrets:pull").description("Pull secrets from remote provider (
|
|
|
14393
14926
|
process.exit(1);
|
|
14394
14927
|
}
|
|
14395
14928
|
});
|
|
14396
|
-
program.command("secrets:reconcile").description("Backfill missing custom secrets from workspace config").option("--stage <stage>", "Stage name
|
|
14929
|
+
program.command("secrets:reconcile").description("Backfill missing custom secrets from workspace config").option("--stage <stage>", "Stage name (default: stages.local)").action(async (options) => {
|
|
14397
14930
|
try {
|
|
14398
14931
|
const globalOptions = program.opts();
|
|
14399
14932
|
if (globalOptions.cwd) process.chdir(globalOptions.cwd);
|
|
@@ -14401,19 +14934,20 @@ program.command("secrets:reconcile").description("Backfill missing custom secret
|
|
|
14401
14934
|
const { reconcileMissingSecrets } = await Promise.resolve().then(() => require("./reconcile-D0pimNiy.cjs"));
|
|
14402
14935
|
const { readStageSecrets: readStageSecrets$1, writeStageSecrets: writeStageSecrets$1 } = await Promise.resolve().then(() => require("./storage-BM9hGpgv.cjs"));
|
|
14403
14936
|
const { workspace } = await loadWorkspaceConfig$1();
|
|
14404
|
-
const
|
|
14937
|
+
const stage = options.stage ?? workspace.stages.local;
|
|
14938
|
+
const secrets = await readStageSecrets$1(stage, workspace.root);
|
|
14405
14939
|
if (!secrets) {
|
|
14406
|
-
console.error(`No secrets found for stage "${
|
|
14940
|
+
console.error(`No secrets found for stage "${stage}". Run "gkm secrets:init --stage ${stage}" first.`);
|
|
14407
14941
|
process.exit(1);
|
|
14408
14942
|
}
|
|
14409
|
-
const { derivedContainers: derivedContainers$1 } = await Promise.resolve().then(() => require("./workspace-
|
|
14410
|
-
const result = reconcileMissingSecrets(secrets, workspace, await derivedContainers$1(workspace,
|
|
14943
|
+
const { derivedContainers: derivedContainers$1 } = await Promise.resolve().then(() => require("./workspace-XXtNhqJl.cjs"));
|
|
14944
|
+
const result = reconcileMissingSecrets(secrets, workspace, await derivedContainers$1(workspace, stage));
|
|
14411
14945
|
if (!result) {
|
|
14412
|
-
console.log(`\n✓ Secrets for stage "${
|
|
14946
|
+
console.log(`\n✓ Secrets for stage "${stage}" are up-to-date`);
|
|
14413
14947
|
return;
|
|
14414
14948
|
}
|
|
14415
14949
|
await writeStageSecrets$1(result.secrets, workspace.root);
|
|
14416
|
-
console.log(`\n✓ Reconciled ${result.addedKeys.length} missing secret(s) for stage "${
|
|
14950
|
+
console.log(`\n✓ Reconciled ${result.addedKeys.length} missing secret(s) for stage "${stage}":`);
|
|
14417
14951
|
for (const key of result.addedKeys) console.log(` + ${key}`);
|
|
14418
14952
|
} catch (error) {
|
|
14419
14953
|
console.error(formatError(error));
|