@geekmidas/cli 10.0.0-alpha.0 → 10.0.0-alpha.10
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 -15
- package/bin/adjacent-tsconfig.mjs +59 -0
- package/bin/gkm.mjs +7 -1
- package/dist/{config-DbPE1KXF.cjs → config-BZ1F0WuA.cjs} +7 -80
- package/dist/config-BZ1F0WuA.cjs.map +1 -0
- package/dist/{config-D8o6I_7Z.mjs → config-DVVph9yz.mjs} +9 -76
- 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-K961ThG3.cjs → credentials-Xamqe_NJ.cjs} +20 -19
- package/dist/credentials-Xamqe_NJ.cjs.map +1 -0
- package/dist/{credentials-Dh0Q6HzY.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 +2154 -859
- package/dist/index.cjs.map +1 -1
- package/dist/index.mjs +2156 -861
- package/dist/index.mjs.map +1 -1
- package/dist/{openapi-CljONd_M.cjs → openapi-SjsgARie.cjs} +127 -50
- package/dist/openapi-SjsgARie.cjs.map +1 -0
- package/dist/{openapi-CgpMWMmM.mjs → openapi-Xx0A7Wg0.mjs} +109 -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-DSutDAlt.mjs → setup-BA8ciTPU.mjs} +4 -4
- package/dist/setup-BsZvZP6f.cjs +11 -0
- package/dist/{setup-DOsYueeQ.mjs → setup-C8KOx8_8.mjs} +5 -5
- package/dist/setup-C8KOx8_8.mjs.map +1 -0
- package/dist/{setup-CJiQM2pX.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-C6VJQVtG.cjs +6845 -0
- package/dist/workspace-C6VJQVtG.cjs.map +1 -0
- package/dist/{workspace-8acupeaI.cjs → workspace-XXtNhqJl.cjs} +3 -3
- package/dist/workspace-zoqRbihZ.mjs +6678 -0
- package/dist/workspace-zoqRbihZ.mjs.map +1 -0
- package/package.json +33 -32
- package/dist/config-D8o6I_7Z.mjs.map +0 -1
- package/dist/config-DbPE1KXF.cjs.map +0 -1
- package/dist/credentials-Dh0Q6HzY.mjs.map +0 -1
- package/dist/credentials-K961ThG3.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-CgpMWMmM.mjs.map +0 -1
- package/dist/openapi-CljONd_M.cjs.map +0 -1
- package/dist/setup-CJiQM2pX.cjs.map +0 -1
- package/dist/setup-DMtU3p41.cjs +0 -11
- package/dist/setup-DOsYueeQ.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-7FqprxuJ.mjs +0 -4323
- package/dist/workspace-7FqprxuJ.mjs.map +0 -1
- package/dist/workspace-XvqO9jIv.cjs +0 -4472
- package/dist/workspace-XvqO9jIv.cjs.map +0 -1
- package/dist/workspace-yP5AHnrn.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 = "
|
|
46
|
+
var version = "10.0.0-alpha.10";
|
|
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";
|
|
@@ -124,12 +124,12 @@ var repository = {
|
|
|
124
124
|
"url": "https://github.com/geekmidas/toolbox"
|
|
125
125
|
};
|
|
126
126
|
var dependencies = {
|
|
127
|
-
"@apidevtools/swagger-parser": "
|
|
128
|
-
"@aws-sdk/client-iam": "~3.
|
|
129
|
-
"@aws-sdk/client-route-53": "~3.
|
|
130
|
-
"@aws-sdk/client-s3": "~3.
|
|
131
|
-
"@aws-sdk/client-ssm": "~3.
|
|
132
|
-
"@aws-sdk/credential-providers": "~3.
|
|
127
|
+
"@apidevtools/swagger-parser": "~13.1.0",
|
|
128
|
+
"@aws-sdk/client-iam": "~3.1136.0",
|
|
129
|
+
"@aws-sdk/client-route-53": "~3.1136.0",
|
|
130
|
+
"@aws-sdk/client-s3": "~3.1136.0",
|
|
131
|
+
"@aws-sdk/client-ssm": "~3.1136.0",
|
|
132
|
+
"@aws-sdk/credential-providers": "~3.1136.0",
|
|
133
133
|
"@geekmidas/cache": "workspace:*",
|
|
134
134
|
"@geekmidas/constructs": "workspace:~",
|
|
135
135
|
"@geekmidas/db": "workspace:*",
|
|
@@ -138,28 +138,29 @@ var dependencies = {
|
|
|
138
138
|
"@geekmidas/logger": "workspace:~",
|
|
139
139
|
"@geekmidas/manifest": "workspace:*",
|
|
140
140
|
"@geekmidas/schema": "workspace:~",
|
|
141
|
-
"chokidar": "~
|
|
142
|
-
"commander": "
|
|
143
|
-
"dotenv": "~
|
|
144
|
-
"esbuild": "
|
|
145
|
-
"fast-glob": "
|
|
146
|
-
"
|
|
147
|
-
"
|
|
148
|
-
"
|
|
149
|
-
"
|
|
141
|
+
"chokidar": "~5.0.0",
|
|
142
|
+
"commander": "~15.0.0",
|
|
143
|
+
"dotenv": "~18.0.1",
|
|
144
|
+
"esbuild": "~0.28.2",
|
|
145
|
+
"fast-glob": "~3.3.3",
|
|
146
|
+
"get-tsconfig": "~4.10.1",
|
|
147
|
+
"hono": "~4.13.8",
|
|
148
|
+
"lodash.kebabcase": "~4.1.1",
|
|
149
|
+
"openapi-typescript": "~7.13.0",
|
|
150
|
+
"pg": "~8.23.0",
|
|
150
151
|
"prompts": "~2.4.2",
|
|
151
152
|
"tsx": "~4.20.3",
|
|
152
|
-
"yaml": "~2.
|
|
153
|
+
"yaml": "~2.9.1"
|
|
153
154
|
};
|
|
154
155
|
var devDependencies = {
|
|
155
156
|
"@geekmidas/testkit": "workspace:*",
|
|
156
|
-
"@types/lodash.kebabcase": "
|
|
157
|
+
"@types/lodash.kebabcase": "~4.1.9",
|
|
157
158
|
"@types/node": "~24.9.1",
|
|
158
|
-
"@types/pg": "~8.
|
|
159
|
+
"@types/pg": "~8.23.1",
|
|
159
160
|
"@types/prompts": "~2.4.9",
|
|
160
|
-
"typescript": "
|
|
161
|
-
"vitest": "
|
|
162
|
-
"zod": "~4.
|
|
161
|
+
"typescript": "~5.8.2",
|
|
162
|
+
"vitest": "~3.2.4",
|
|
163
|
+
"zod": "~4.6.5"
|
|
163
164
|
};
|
|
164
165
|
var peerDependencies = { "@geekmidas/telescope": "workspace:~" };
|
|
165
166
|
var peerDependenciesMeta = { "@geekmidas/telescope": { "optional": true } };
|
|
@@ -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
|
|
446
|
-
const content = `import { AWSScheduledFunction } from '@geekmidas/constructs/crons';
|
|
607
|
+
const runtime = require_openapi.runtimeFor(context, (0, node_path.dirname)(handlerPath), owner);
|
|
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
|
|
|
@@ -7343,301 +7510,56 @@ function printStateDetails(state) {
|
|
|
7343
7510
|
}
|
|
7344
7511
|
|
|
7345
7512
|
//#endregion
|
|
7346
|
-
//#region src/init/
|
|
7347
|
-
|
|
7348
|
-
|
|
7349
|
-
try {
|
|
7350
|
-
return require$1("../package.json");
|
|
7351
|
-
} catch {
|
|
7352
|
-
return require$1("../../package.json");
|
|
7353
|
-
}
|
|
7354
|
-
}
|
|
7355
|
-
const pkg = loadPackageJson();
|
|
7513
|
+
//#region src/init/constructs.ts
|
|
7514
|
+
/** The glob every generated config points at. One glob, every kind. */
|
|
7515
|
+
const CONSTRUCTS_GLOB = "./src/constructs/**/*.ts";
|
|
7356
7516
|
/**
|
|
7357
|
-
*
|
|
7517
|
+
* Where a workspace keeps its constructs: at the root, beside the apps that
|
|
7518
|
+
* share them, rather than inside any one of them.
|
|
7358
7519
|
*/
|
|
7359
|
-
const
|
|
7520
|
+
const WORKSPACE_CONSTRUCTS_GLOB = "./constructs/**/*.ts";
|
|
7360
7521
|
/**
|
|
7361
|
-
*
|
|
7522
|
+
* Where each routes structure puts an app's handlers, relative to the app.
|
|
7362
7523
|
*
|
|
7363
|
-
*
|
|
7364
|
-
*
|
|
7365
|
-
*/
|
|
7366
|
-
const GEEKMIDAS_VERSIONS = {
|
|
7367
|
-
"@geekmidas/audit": "~9.0.2",
|
|
7368
|
-
"@geekmidas/auth": "~9.0.2",
|
|
7369
|
-
"@geekmidas/cache": "~9.0.2",
|
|
7370
|
-
"@geekmidas/client": "~9.0.2",
|
|
7371
|
-
"@geekmidas/cloud": "~9.0.2",
|
|
7372
|
-
"@geekmidas/constructs": "~9.0.2",
|
|
7373
|
-
"@geekmidas/db": "~9.0.2",
|
|
7374
|
-
"@geekmidas/emailkit": "~9.0.2",
|
|
7375
|
-
"@geekmidas/envkit": "~9.0.2",
|
|
7376
|
-
"@geekmidas/errors": "~9.0.2",
|
|
7377
|
-
"@geekmidas/events": "~9.0.2",
|
|
7378
|
-
"@geekmidas/logger": "~9.0.2",
|
|
7379
|
-
"@geekmidas/manifest": "~9.0.2",
|
|
7380
|
-
"@geekmidas/rate-limit": "~9.0.2",
|
|
7381
|
-
"@geekmidas/schema": "~9.0.2",
|
|
7382
|
-
"@geekmidas/services": "~9.0.2",
|
|
7383
|
-
"@geekmidas/storage": "~9.0.2",
|
|
7384
|
-
"@geekmidas/studio": "~9.0.2",
|
|
7385
|
-
"@geekmidas/telescope": "~9.0.2",
|
|
7386
|
-
"@geekmidas/testkit": "~9.0.2",
|
|
7387
|
-
"@geekmidas/cli": CLI_VERSION
|
|
7388
|
-
};
|
|
7389
|
-
|
|
7390
|
-
//#endregion
|
|
7391
|
-
//#region src/init/generators/auth.ts
|
|
7392
|
-
/**
|
|
7393
|
-
* Generate auth app files for fullstack template
|
|
7394
|
-
* Uses better-auth with magic link authentication
|
|
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.
|
|
7395
7526
|
*/
|
|
7396
|
-
function
|
|
7397
|
-
|
|
7398
|
-
|
|
7399
|
-
|
|
7400
|
-
|
|
7401
|
-
|
|
7402
|
-
version: "0.0.1",
|
|
7403
|
-
private: true,
|
|
7404
|
-
type: "module",
|
|
7405
|
-
scripts: {
|
|
7406
|
-
dev: "gkm dev --entry ./src/index.ts",
|
|
7407
|
-
build: "tsc",
|
|
7408
|
-
start: "node dist/index.js",
|
|
7409
|
-
typecheck: "tsc --noEmit",
|
|
7410
|
-
"db:migrate": "gkm exec -- npx @better-auth/cli migrate",
|
|
7411
|
-
"db:generate": "gkm exec -- npx @better-auth/cli generate"
|
|
7412
|
-
},
|
|
7413
|
-
dependencies: {
|
|
7414
|
-
[modelsPackage]: "workspace:*",
|
|
7415
|
-
"@geekmidas/envkit": GEEKMIDAS_VERSIONS["@geekmidas/envkit"],
|
|
7416
|
-
"@geekmidas/logger": GEEKMIDAS_VERSIONS["@geekmidas/logger"],
|
|
7417
|
-
"@hono/node-server": "~1.13.0",
|
|
7418
|
-
"better-auth": "~1.2.0",
|
|
7419
|
-
hono: "~4.8.0",
|
|
7420
|
-
kysely: "~0.27.0",
|
|
7421
|
-
pg: "~8.13.0"
|
|
7422
|
-
},
|
|
7423
|
-
devDependencies: {
|
|
7424
|
-
"@geekmidas/cli": GEEKMIDAS_VERSIONS["@geekmidas/cli"],
|
|
7425
|
-
"@types/node": "~22.0.0",
|
|
7426
|
-
"@types/pg": "~8.11.0",
|
|
7427
|
-
tsx: "~4.20.0",
|
|
7428
|
-
typescript: "~5.8.2"
|
|
7429
|
-
}
|
|
7430
|
-
};
|
|
7431
|
-
const tsConfig = {
|
|
7432
|
-
extends: "../../tsconfig.json",
|
|
7433
|
-
compilerOptions: {
|
|
7434
|
-
noEmit: true,
|
|
7435
|
-
allowImportingTsExtensions: true,
|
|
7436
|
-
baseUrl: ".",
|
|
7437
|
-
paths: {
|
|
7438
|
-
"~/*": ["./src/*"],
|
|
7439
|
-
[`@${options.name}/*`]: ["../../packages/*/src"]
|
|
7440
|
-
}
|
|
7441
|
-
},
|
|
7442
|
-
include: ["src/**/*.ts"],
|
|
7443
|
-
exclude: ["node_modules", "dist"]
|
|
7444
|
-
};
|
|
7445
|
-
const envTs = `import { Credentials } from '@geekmidas/envkit/credentials';
|
|
7446
|
-
import { EnvironmentParser } from '@geekmidas/envkit';
|
|
7447
|
-
|
|
7448
|
-
export const envParser = new EnvironmentParser({ ...process.env, ...Credentials });
|
|
7449
|
-
|
|
7450
|
-
// Global config - only minimal shared values
|
|
7451
|
-
// Service-specific config should be parsed where needed
|
|
7452
|
-
export const config = envParser
|
|
7453
|
-
.create((get) => ({
|
|
7454
|
-
nodeEnv: get('NODE_ENV').enum(['development', 'test', 'production']).default('development'),
|
|
7455
|
-
stage: get('STAGE').enum(['development', 'staging', 'production']).default('development'),
|
|
7456
|
-
}))
|
|
7457
|
-
.parse();
|
|
7458
|
-
`;
|
|
7459
|
-
const loggerTs = `import { createLogger } from '@geekmidas/logger/${options.loggerType}';
|
|
7460
|
-
|
|
7461
|
-
export const logger = createLogger();
|
|
7462
|
-
`;
|
|
7463
|
-
const authTs = `import { betterAuth } from 'better-auth';
|
|
7464
|
-
import { magicLink } from 'better-auth/plugins';
|
|
7465
|
-
import pg from 'pg';
|
|
7466
|
-
import { envParser } from './config/env.ts';
|
|
7467
|
-
import { logger } from './config/logger.ts';
|
|
7468
|
-
|
|
7469
|
-
// Parse auth-specific config (no defaults - values from secrets)
|
|
7470
|
-
const authConfig = envParser
|
|
7471
|
-
.create((get) => ({
|
|
7472
|
-
databaseUrl: get('DATABASE_URL').string(),
|
|
7473
|
-
baseUrl: get('BETTER_AUTH_URL').string(),
|
|
7474
|
-
trustedOrigins: get('BETTER_AUTH_TRUSTED_ORIGINS').string(),
|
|
7475
|
-
secret: get('BETTER_AUTH_SECRET').string(),
|
|
7476
|
-
}))
|
|
7477
|
-
.parse();
|
|
7478
|
-
|
|
7479
|
-
export const auth = betterAuth({
|
|
7480
|
-
database: new pg.Pool({
|
|
7481
|
-
connectionString: authConfig.databaseUrl,
|
|
7482
|
-
}),
|
|
7483
|
-
baseURL: authConfig.baseUrl,
|
|
7484
|
-
trustedOrigins: authConfig.trustedOrigins.split(','),
|
|
7485
|
-
secret: authConfig.secret,
|
|
7486
|
-
plugins: [
|
|
7487
|
-
magicLink({
|
|
7488
|
-
sendMagicLink: async ({ email, url }) => {
|
|
7489
|
-
// TODO: Implement email sending using @geekmidas/emailkit
|
|
7490
|
-
// For development, log the magic link
|
|
7491
|
-
logger.info({ email, url }, 'Magic link generated');
|
|
7492
|
-
console.log('\\n================================');
|
|
7493
|
-
console.log('MAGIC LINK FOR:', email);
|
|
7494
|
-
console.log(url);
|
|
7495
|
-
console.log('================================\\n');
|
|
7496
|
-
},
|
|
7497
|
-
expiresIn: 300, // 5 minutes
|
|
7498
|
-
}),
|
|
7499
|
-
],
|
|
7500
|
-
emailAndPassword: {
|
|
7501
|
-
enabled: false, // Only magic link for now
|
|
7502
|
-
},
|
|
7503
|
-
});
|
|
7504
|
-
|
|
7505
|
-
export type Auth = typeof auth;
|
|
7506
|
-
`;
|
|
7507
|
-
const indexTs = `import { Hono } from 'hono';
|
|
7508
|
-
import { cors } from 'hono/cors';
|
|
7509
|
-
import { serve } from '@hono/node-server';
|
|
7510
|
-
import { auth } from './auth.ts';
|
|
7511
|
-
import { envParser } from './config/env.ts';
|
|
7512
|
-
import { logger } from './config/logger.ts';
|
|
7513
|
-
|
|
7514
|
-
// Parse server config (no defaults - values from secrets)
|
|
7515
|
-
const serverConfig = envParser
|
|
7516
|
-
.create((get) => ({
|
|
7517
|
-
port: get('PORT').string().transform(Number),
|
|
7518
|
-
trustedOrigins: get('BETTER_AUTH_TRUSTED_ORIGINS').string(),
|
|
7519
|
-
}))
|
|
7520
|
-
.parse();
|
|
7521
|
-
|
|
7522
|
-
const app = new Hono();
|
|
7523
|
-
|
|
7524
|
-
// CORS must be registered before routes
|
|
7525
|
-
app.use(
|
|
7526
|
-
'/api/auth/*',
|
|
7527
|
-
cors({
|
|
7528
|
-
origin: serverConfig.trustedOrigins.split(','),
|
|
7529
|
-
allowHeaders: ['Content-Type', 'Authorization'],
|
|
7530
|
-
allowMethods: ['POST', 'GET', 'OPTIONS'],
|
|
7531
|
-
credentials: true,
|
|
7532
|
-
}),
|
|
7533
|
-
);
|
|
7534
|
-
|
|
7535
|
-
// Health check endpoint
|
|
7536
|
-
app.get('/health', (c) => {
|
|
7537
|
-
return c.json({
|
|
7538
|
-
status: 'ok',
|
|
7539
|
-
service: 'auth',
|
|
7540
|
-
timestamp: new Date().toISOString(),
|
|
7541
|
-
});
|
|
7542
|
-
});
|
|
7543
|
-
|
|
7544
|
-
// Mount better-auth handler
|
|
7545
|
-
app.on(['POST', 'GET'], '/api/auth/*', (c) => {
|
|
7546
|
-
return auth.handler(c.req.raw);
|
|
7547
|
-
});
|
|
7548
|
-
|
|
7549
|
-
logger.info({ port: serverConfig.port }, 'Starting auth server');
|
|
7550
|
-
|
|
7551
|
-
serve({
|
|
7552
|
-
fetch: app.fetch,
|
|
7553
|
-
port: serverConfig.port,
|
|
7554
|
-
}, (info) => {
|
|
7555
|
-
logger.info({ port: info.port }, 'Auth server running');
|
|
7556
|
-
});
|
|
7557
|
-
`;
|
|
7558
|
-
const gitignore = `node_modules/
|
|
7559
|
-
dist/
|
|
7560
|
-
.env.local
|
|
7561
|
-
*.log
|
|
7562
|
-
`;
|
|
7563
|
-
return [
|
|
7564
|
-
{
|
|
7565
|
-
path: "apps/auth/package.json",
|
|
7566
|
-
content: `${JSON.stringify(packageJson, null, 2)}\n`
|
|
7567
|
-
},
|
|
7568
|
-
{
|
|
7569
|
-
path: "apps/auth/tsconfig.json",
|
|
7570
|
-
content: `${JSON.stringify(tsConfig, null, 2)}\n`
|
|
7571
|
-
},
|
|
7572
|
-
{
|
|
7573
|
-
path: "apps/auth/src/config/env.ts",
|
|
7574
|
-
content: envTs
|
|
7575
|
-
},
|
|
7576
|
-
{
|
|
7577
|
-
path: "apps/auth/src/config/logger.ts",
|
|
7578
|
-
content: loggerTs
|
|
7579
|
-
},
|
|
7580
|
-
{
|
|
7581
|
-
path: "apps/auth/src/auth.ts",
|
|
7582
|
-
content: authTs
|
|
7583
|
-
},
|
|
7584
|
-
{
|
|
7585
|
-
path: "apps/auth/src/index.ts",
|
|
7586
|
-
content: indexTs
|
|
7587
|
-
},
|
|
7588
|
-
{
|
|
7589
|
-
path: "apps/auth/.gitignore",
|
|
7590
|
-
content: gitignore
|
|
7591
|
-
}
|
|
7592
|
-
];
|
|
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
|
+
}
|
|
7593
7533
|
}
|
|
7594
|
-
|
|
7595
|
-
//#endregion
|
|
7596
|
-
//#region src/init/constructs.ts
|
|
7597
|
-
/** The glob every generated config points at. One glob, every kind. */
|
|
7598
|
-
const CONSTRUCTS_GLOB = "./src/constructs/**/*.ts";
|
|
7599
7534
|
/**
|
|
7600
|
-
*
|
|
7535
|
+
* Plain ids — `Database`, not `Beetlefit`.
|
|
7601
7536
|
*
|
|
7602
|
-
*
|
|
7603
|
-
*
|
|
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.
|
|
7604
7542
|
*/
|
|
7605
|
-
function
|
|
7606
|
-
const id = (0, __geekmidas_manifest.canonicalId)(name$1);
|
|
7607
|
-
return {
|
|
7608
|
-
id,
|
|
7609
|
-
service: (0, __geekmidas_manifest.serviceKey)(id),
|
|
7610
|
-
urlKey: (0, __geekmidas_manifest.provideKey)(id, "url"),
|
|
7611
|
-
ownerUrlKey: (0, __geekmidas_manifest.provideKey)(id, "ownerUrl")
|
|
7612
|
-
};
|
|
7613
|
-
}
|
|
7614
|
-
/** The bucket a project named `name` declares. */
|
|
7615
|
-
function storageFor(name$1) {
|
|
7616
|
-
const id = (0, __geekmidas_manifest.canonicalId)(`${name$1}-uploads`);
|
|
7617
|
-
return {
|
|
7618
|
-
id,
|
|
7619
|
-
service: (0, __geekmidas_manifest.serviceKey)(id),
|
|
7620
|
-
urlKey: (0, __geekmidas_manifest.provideKey)(id, "url")
|
|
7621
|
-
};
|
|
7622
|
-
}
|
|
7623
|
-
/** The mail sender a project named `name` declares. */
|
|
7624
|
-
function emailFor(name$1) {
|
|
7625
|
-
const id = (0, __geekmidas_manifest.canonicalId)(`${name$1}-mail`);
|
|
7543
|
+
function scaffolded(id) {
|
|
7626
7544
|
return {
|
|
7627
7545
|
id,
|
|
7628
7546
|
service: (0, __geekmidas_manifest.serviceKey)(id),
|
|
7629
7547
|
urlKey: (0, __geekmidas_manifest.provideKey)(id, "url")
|
|
7630
7548
|
};
|
|
7631
7549
|
}
|
|
7632
|
-
/** The
|
|
7633
|
-
function
|
|
7634
|
-
const id = (0, __geekmidas_manifest.canonicalId)(`${name$1}-cache`);
|
|
7550
|
+
/** The database a project declares. */
|
|
7551
|
+
function databaseFor() {
|
|
7635
7552
|
return {
|
|
7636
|
-
|
|
7637
|
-
|
|
7638
|
-
urlKey: (0, __geekmidas_manifest.provideKey)(id, "url")
|
|
7553
|
+
...scaffolded("Database"),
|
|
7554
|
+
ownerUrlKey: (0, __geekmidas_manifest.provideKey)("Database", "ownerUrl")
|
|
7639
7555
|
};
|
|
7640
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");
|
|
7641
7563
|
/**
|
|
7642
7564
|
* The files a project's declared database needs.
|
|
7643
7565
|
*
|
|
@@ -7645,8 +7567,8 @@ function cacheFor(name$1) {
|
|
|
7645
7567
|
* the same way — and so the schema type, the migration, and the id the test
|
|
7646
7568
|
* setup reads all come from one place.
|
|
7647
7569
|
*/
|
|
7648
|
-
function databaseFiles(
|
|
7649
|
-
const db = databaseFor(
|
|
7570
|
+
function databaseFiles() {
|
|
7571
|
+
const db = databaseFor();
|
|
7650
7572
|
return [{
|
|
7651
7573
|
path: "src/constructs/database.ts",
|
|
7652
7574
|
content: `import { KyselyDatabase } from '@geekmidas/constructs/database/kysely';
|
|
@@ -7675,7 +7597,16 @@ export interface Database {
|
|
|
7675
7597
|
*/
|
|
7676
7598
|
export const database = new KyselyDatabase<Database, '${db.id}'>('${db.id}');
|
|
7677
7599
|
`
|
|
7678
|
-
},
|
|
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 {
|
|
7679
7610
|
path: "src/db/migrations/001_create_users.ts",
|
|
7680
7611
|
content: `import type { Kysely } from 'kysely';
|
|
7681
7612
|
|
|
@@ -7703,25 +7634,538 @@ export async function down(db: Kysely<unknown>): Promise<void> {
|
|
|
7703
7634
|
await db.schema.dropTable('users').execute();
|
|
7704
7635
|
}
|
|
7705
7636
|
`
|
|
7706
|
-
}
|
|
7637
|
+
};
|
|
7707
7638
|
}
|
|
7708
|
-
|
|
7709
|
-
//#endregion
|
|
7710
|
-
//#region src/init/generators/config.ts
|
|
7711
7639
|
/**
|
|
7712
|
-
*
|
|
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.
|
|
7713
7646
|
*/
|
|
7714
|
-
|
|
7715
|
-
|
|
7647
|
+
function workspaceConstructsGlobs(structure, appsDir) {
|
|
7648
|
+
return [WORKSPACE_CONSTRUCTS_GLOB, `./${appsDir}/*/${routesGlob(structure).replace(/^\.\//, "")}`];
|
|
7649
|
+
}
|
|
7716
7650
|
|
|
7717
|
-
|
|
7718
|
-
|
|
7719
|
-
|
|
7720
|
-
|
|
7721
|
-
|
|
7722
|
-
|
|
7723
|
-
|
|
7724
|
-
|
|
7651
|
+
//#endregion
|
|
7652
|
+
//#region src/init/generators/agents.ts
|
|
7653
|
+
/**
|
|
7654
|
+
* `AGENTS.md` and `CLAUDE.md` for a scaffolded project.
|
|
7655
|
+
*
|
|
7656
|
+
* A generated project is the first thing an agent sees, and without this it
|
|
7657
|
+
* sees only the output: a `gkm.config.ts` naming three keys, a `constructs/`
|
|
7658
|
+
* directory, and a build step that reads both. None of that says what the
|
|
7659
|
+
* shape *is*, so an agent reconstructs it from the framework it recognises —
|
|
7660
|
+
* and writes an Express app beside a `RestApi`, or restates in config what a
|
|
7661
|
+
* construct already declared, which is the exact mistake v10 exists to make
|
|
7662
|
+
* impossible.
|
|
7663
|
+
*
|
|
7664
|
+
* Instructions live in `AGENTS.md` because that is the file every tool reads.
|
|
7665
|
+
* `CLAUDE.md` points at it rather than copying it: two files saying the same
|
|
7666
|
+
* thing is two files to keep in step, and the copy that drifts is always the
|
|
7667
|
+
* one nobody opened.
|
|
7668
|
+
*
|
|
7669
|
+
* Content is derived from the same `TemplateOptions` the rest of the scaffold
|
|
7670
|
+
* is built from, so a project with no database gets no database section, and a
|
|
7671
|
+
* worker gets no endpoint guide. A guide that documents what was not generated
|
|
7672
|
+
* teaches the reader to distrust it.
|
|
7673
|
+
*/
|
|
7674
|
+
function generateAgentFiles(options, template) {
|
|
7675
|
+
return [{
|
|
7676
|
+
path: "AGENTS.md",
|
|
7677
|
+
content: agentsContent(options, template)
|
|
7678
|
+
}, {
|
|
7679
|
+
path: "CLAUDE.md",
|
|
7680
|
+
content: claudeContent()
|
|
7681
|
+
}];
|
|
7682
|
+
}
|
|
7683
|
+
/**
|
|
7684
|
+
* The pointer.
|
|
7685
|
+
*
|
|
7686
|
+
* Deliberately short and deliberately not a summary — a summary is a second
|
|
7687
|
+
* copy of the conventions that ages independently of the first.
|
|
7688
|
+
*/
|
|
7689
|
+
function claudeContent() {
|
|
7690
|
+
return `# CLAUDE.md
|
|
7691
|
+
|
|
7692
|
+
The conventions for this project live in [AGENTS.md](./AGENTS.md).
|
|
7693
|
+
|
|
7694
|
+
Read that file before writing code here. It is the single copy, kept that way
|
|
7695
|
+
on purpose: anything repeated here would be a second thing to keep in step, and
|
|
7696
|
+
the stale one is always the copy nobody opened.
|
|
7697
|
+
`;
|
|
7698
|
+
}
|
|
7699
|
+
function agentsContent(options, template) {
|
|
7700
|
+
const { name: name$1, monorepo, database, telescope, studio, packageManager, services } = options;
|
|
7701
|
+
const constructsGlob = monorepo ? WORKSPACE_CONSTRUCTS_GLOB : CONSTRUCTS_GLOB;
|
|
7702
|
+
const isWorker = template.name === "worker";
|
|
7703
|
+
const isServerless = template.name === "serverless";
|
|
7704
|
+
const run = packageManager === "npm" ? "npx" : `${packageManager} exec`;
|
|
7705
|
+
const sections = [];
|
|
7706
|
+
sections.push(`# Agent conventions — ${name$1}
|
|
7707
|
+
|
|
7708
|
+
Rules for anyone, human or agent, writing code in this project. Built with
|
|
7709
|
+
[\`@geekmidas/toolbox\`](https://github.com/geekmidas/toolbox).
|
|
7710
|
+
|
|
7711
|
+
Read this before adding a file. Most of what follows exists because the obvious
|
|
7712
|
+
approach from another framework produces something that compiles, runs, and is
|
|
7713
|
+
wrong in a way nothing catches.`);
|
|
7714
|
+
sections.push(`## The one idea
|
|
7715
|
+
|
|
7716
|
+
**Constructs declare; config does not restate.**
|
|
7717
|
+
|
|
7718
|
+
A construct — a \`RestApi\`, a database, a bucket, a topic — is the single
|
|
7719
|
+
declaration of a thing. The build reads those declarations and derives
|
|
7720
|
+
everything downstream from them: which containers exist, which apps get built,
|
|
7721
|
+
what the OpenAPI spec says, which env vars a process needs.
|
|
7722
|
+
|
|
7723
|
+
\`gkm.config.ts\` holds only what no construct can answer:
|
|
7724
|
+
|
|
7725
|
+
\`\`\`typescript
|
|
7726
|
+
export default defineWorkspace({
|
|
7727
|
+
name: '${name$1}',
|
|
7728
|
+
stages: { local: 'dev', deployed: ['staging', 'prod'], protected: ['prod'] },
|
|
7729
|
+
constructs: '${constructsGlob}',
|
|
7730
|
+
secrets: { enabled: true },
|
|
7731
|
+
});
|
|
7732
|
+
\`\`\`
|
|
7733
|
+
|
|
7734
|
+
If you find yourself adding a key to config that names something already
|
|
7735
|
+
declared in a construct — a path, a port, a route, a container — stop. That is
|
|
7736
|
+
the drift this design removes. The second copy is the one that goes wrong.`);
|
|
7737
|
+
sections.push(`## Layout
|
|
7738
|
+
|
|
7739
|
+
\`\`\`
|
|
7740
|
+
${layoutBlock(options, template)}
|
|
7741
|
+
\`\`\`
|
|
7742
|
+
|
|
7743
|
+
Handlers are found by **one glob**, and what kind a file exports is decided by
|
|
7744
|
+
the value it exports, not by the directory it sits in.`);
|
|
7745
|
+
sections.push(`## Commands
|
|
7746
|
+
|
|
7747
|
+
\`\`\`bash
|
|
7748
|
+
${run} gkm dev # dev server, hot reload, services up, secrets injected
|
|
7749
|
+
${run} gkm build # production build
|
|
7750
|
+
${run} gkm test # tests, with secrets injected
|
|
7751
|
+
${run} gkm exec -- <cmd> # run any command with secrets injected
|
|
7752
|
+
${run} gkm openapi # regenerate the OpenAPI spec
|
|
7753
|
+
\`\`\`
|
|
7754
|
+
|
|
7755
|
+
Secrets are encrypted at rest and injected at run time:
|
|
7756
|
+
|
|
7757
|
+
\`\`\`bash
|
|
7758
|
+
${run} gkm secrets:set DATABASE_URL postgres://...
|
|
7759
|
+
${run} gkm secrets:show
|
|
7760
|
+
\`\`\`
|
|
7761
|
+
|
|
7762
|
+
**Never run a tool that needs project env vars directly.** It will see an empty
|
|
7763
|
+
environment and fail in a way that looks like a config bug. Go through
|
|
7764
|
+
\`gkm exec --\`, which is what injects them.`);
|
|
7765
|
+
if (!isWorker) sections.push(endpointSection(options));
|
|
7766
|
+
if (isWorker || isServerless || services.events) sections.push(backgroundSection(template));
|
|
7767
|
+
if (database) sections.push(databaseSection());
|
|
7768
|
+
sections.push(`## Environment
|
|
7769
|
+
|
|
7770
|
+
Environment is parsed once, through \`envkit\`, and never read from
|
|
7771
|
+
\`process.env\` directly:
|
|
7772
|
+
|
|
7773
|
+
\`\`\`typescript
|
|
7774
|
+
import { EnvironmentParser } from '@geekmidas/envkit';
|
|
7775
|
+
import { Credentials } from '@geekmidas/envkit/credentials';
|
|
7776
|
+
|
|
7777
|
+
export const envParser = new EnvironmentParser({
|
|
7778
|
+
...process.env,
|
|
7779
|
+
...Credentials,
|
|
7780
|
+
});
|
|
7781
|
+
|
|
7782
|
+
export const config = envParser
|
|
7783
|
+
.create((get) => ({
|
|
7784
|
+
port: get('PORT').string().transform(Number).default(3000),
|
|
7785
|
+
}))
|
|
7786
|
+
.parse();
|
|
7787
|
+
\`\`\`
|
|
7788
|
+
|
|
7789
|
+
\`Credentials\` is what \`gkm dev\` and \`gkm exec\` inject. A bare
|
|
7790
|
+
\`process.env.FOO\` bypasses both the decryption and the validation, so it reads
|
|
7791
|
+
as \`undefined\` in exactly the environments that matter.`);
|
|
7792
|
+
sections.push(conventionsSection());
|
|
7793
|
+
sections.push(testingSection(database));
|
|
7794
|
+
if (telescope || studio) {
|
|
7795
|
+
const tools = [telescope && "**Telescope** — request and exception monitoring", studio && "**Studio** — dev dashboard and database browser"].filter(Boolean);
|
|
7796
|
+
sections.push(`## Dev tools
|
|
7797
|
+
|
|
7798
|
+
${tools.map((t) => `- ${t}`).join("\n")}
|
|
7799
|
+
|
|
7800
|
+
Both are derived from declared constructs. Neither needs wiring up by hand.`);
|
|
7801
|
+
}
|
|
7802
|
+
sections.push(`## Before you say it works
|
|
7803
|
+
|
|
7804
|
+
\`\`\`bash
|
|
7805
|
+
${run} gkm build
|
|
7806
|
+
\`\`\`
|
|
7807
|
+
|
|
7808
|
+
A build is the only thing that proves the manifest still resolves. Code that
|
|
7809
|
+
typechecks can still declare two constructs with one id, or a handler no glob
|
|
7810
|
+
reaches — and neither shows up until something reads the graph.`);
|
|
7811
|
+
return `${sections.join("\n\n")}\n`;
|
|
7812
|
+
}
|
|
7813
|
+
function layoutBlock(options, template) {
|
|
7814
|
+
const { monorepo, apiPath, database } = options;
|
|
7815
|
+
const isWorker = template.name === "worker";
|
|
7816
|
+
if (monorepo) return `constructs/ # every construct: the surface, the database, topics
|
|
7817
|
+
${apiPath}/ # the API app
|
|
7818
|
+
gkm.config.ts # name, constructs glob, secrets`;
|
|
7819
|
+
const lines = ["src/constructs/ # the surface, and anything it declares", "src/config/ # env parser, logger"];
|
|
7820
|
+
if (!isWorker) lines.push("src/endpoints/ # HTTP handlers");
|
|
7821
|
+
if (isWorker) {
|
|
7822
|
+
lines.push("src/crons/ # scheduled work");
|
|
7823
|
+
lines.push("src/subscribers/ # topic consumers");
|
|
7824
|
+
}
|
|
7825
|
+
if (database) lines.push("src/db/ # migrations, schema");
|
|
7826
|
+
lines.push("gkm.config.ts # name, constructs glob, secrets");
|
|
7827
|
+
return lines.join("\n");
|
|
7828
|
+
}
|
|
7829
|
+
function endpointSection(options) {
|
|
7830
|
+
const { database } = options;
|
|
7831
|
+
return `## Adding an endpoint
|
|
7832
|
+
|
|
7833
|
+
An endpoint is built **from the surface that serves it**. That is where its
|
|
7834
|
+
logger, environment parser and authorizers come from, so none of them is passed
|
|
7835
|
+
per endpoint:
|
|
7836
|
+
|
|
7837
|
+
\`\`\`typescript
|
|
7838
|
+
import { z } from 'zod';
|
|
7839
|
+
import { router } from '../constructs/router.ts';
|
|
7840
|
+
|
|
7841
|
+
export const createUser = router
|
|
7842
|
+
.post('/users')
|
|
7843
|
+
.body(z.object({ name: z.string(), email: z.email() }))
|
|
7844
|
+
.output(z.object({ id: z.uuid() }))
|
|
7845
|
+
.handle(async ({ body, logger${database ? ", services" : ""} }) => {
|
|
7846
|
+
logger.info({ name: body.name }, 'creating user');
|
|
7847
|
+
${database ? ` const user = await services.database
|
|
7848
|
+
.insertInto('users')
|
|
7849
|
+
.values({ name: body.name, email: body.email })
|
|
7850
|
+
.returningAll()
|
|
7851
|
+
.executeTakeFirstOrThrow();
|
|
7852
|
+
|
|
7853
|
+
return { id: user.id };` : ` return { id: crypto.randomUUID() };`}
|
|
7854
|
+
});
|
|
7855
|
+
\`\`\`
|
|
7856
|
+
|
|
7857
|
+
**Export it.** An endpoint that is never exported is never found — the glob
|
|
7858
|
+
loads the module and filters by value, so a handler assigned to a local
|
|
7859
|
+
\`const\` simply does not exist as far as the build is concerned.
|
|
7860
|
+
|
|
7861
|
+
**Do not write an HTTP server.** No \`express()\`, no \`new Hono()\`, no
|
|
7862
|
+
\`app.listen\`. The entry is generated from the surface; a hand-written server
|
|
7863
|
+
beside it is a second app that shadows the real one.
|
|
7864
|
+
|
|
7865
|
+
### Branching the factory
|
|
7866
|
+
|
|
7867
|
+
Share what a *group* of endpoints needs by branching once, in its own file:
|
|
7868
|
+
|
|
7869
|
+
\`\`\`typescript
|
|
7870
|
+
export const router = api.endpoints${database ? ".database(database)" : ""};
|
|
7871
|
+
export const sessionRouter = router.session(/* … */);
|
|
7872
|
+
\`\`\`
|
|
7873
|
+
|
|
7874
|
+
Branch for a group, never for a single endpoint — a one-endpoint branch is
|
|
7875
|
+
indirection with nothing on the other side of it.`;
|
|
7876
|
+
}
|
|
7877
|
+
function backgroundSection(template) {
|
|
7878
|
+
const isWorker = template.name === "worker";
|
|
7879
|
+
return `## Background work
|
|
7880
|
+
|
|
7881
|
+
Crons, subscribers and functions are declared the same way endpoints are: as
|
|
7882
|
+
exported values the build discovers.
|
|
7883
|
+
|
|
7884
|
+
\`\`\`typescript
|
|
7885
|
+
// a scheduled job, built from the worker that runs it
|
|
7886
|
+
import { worker } from '../constructs/worker.ts';
|
|
7887
|
+
|
|
7888
|
+
export const cleanup = worker
|
|
7889
|
+
.cron('rate(1 day)')
|
|
7890
|
+
.handle(async ({ logger }) => {
|
|
7891
|
+
logger.info('cleaning up');
|
|
7892
|
+
});
|
|
7893
|
+
\`\`\`
|
|
7894
|
+
|
|
7895
|
+
\`\`\`typescript
|
|
7896
|
+
// a topic subscriber
|
|
7897
|
+
import { userEvents } from '../constructs/topics.ts';
|
|
7898
|
+
import { worker } from '../constructs/worker.ts';
|
|
7899
|
+
|
|
7900
|
+
export const onUserCreated = worker
|
|
7901
|
+
.topic(userEvents)
|
|
7902
|
+
.subscribe(['user.created'])
|
|
7903
|
+
.handle(async ({ events, logger }) => {
|
|
7904
|
+
logger.info({ count: events.length }, 'handling user events');
|
|
7905
|
+
});
|
|
7906
|
+
\`\`\`
|
|
7907
|
+
|
|
7908
|
+
Handlers receive a **batch**, not one event. Both transports deliver in
|
|
7909
|
+
batches, so a per-event round trip is a round trip per event for no reason.
|
|
7910
|
+
|
|
7911
|
+
${isWorker ? `This project is background work and nothing else — there is no HTTP surface
|
|
7912
|
+
to add routes to.` : `Declaring a topic is what makes the broker exist. Nothing lists it in config.`}`;
|
|
7913
|
+
}
|
|
7914
|
+
function databaseSection() {
|
|
7915
|
+
return `## Database
|
|
7916
|
+
|
|
7917
|
+
The database is a **construct**, not a hand-rolled service. Declaring it is what
|
|
7918
|
+
makes a Postgres exist locally and a database exist on deploy — nothing names
|
|
7919
|
+
\`postgres\` in config.
|
|
7920
|
+
|
|
7921
|
+
An endpoint reaches it by declaring the dependency, and gets it typed:
|
|
7922
|
+
|
|
7923
|
+
\`\`\`typescript
|
|
7924
|
+
export const listUsers = router
|
|
7925
|
+
.get('/users')
|
|
7926
|
+
.output(z.array(z.object({ id: z.uuid() })))
|
|
7927
|
+
.handle(async ({ services }) => {
|
|
7928
|
+
return services.database.selectFrom('users').selectAll().execute();
|
|
7929
|
+
});
|
|
7930
|
+
\`\`\`
|
|
7931
|
+
|
|
7932
|
+
Queries go through Kysely. Never open your own connection pool — the one the
|
|
7933
|
+
construct provides is the one that gets credentials, pooling and shutdown.`;
|
|
7934
|
+
}
|
|
7935
|
+
function conventionsSection() {
|
|
7936
|
+
return `## Conventions
|
|
7937
|
+
|
|
7938
|
+
**Zod formats are top-level schemas.**
|
|
7939
|
+
|
|
7940
|
+
\`\`\`typescript
|
|
7941
|
+
// yes
|
|
7942
|
+
z.email()
|
|
7943
|
+
z.url()
|
|
7944
|
+
z.uuid()
|
|
7945
|
+
|
|
7946
|
+
// no
|
|
7947
|
+
z.string().email()
|
|
7948
|
+
z.string().url()
|
|
7949
|
+
\`\`\`
|
|
7950
|
+
|
|
7951
|
+
Zod 4 made each format a schema in its own right. The chained form still runs,
|
|
7952
|
+
but it types as a \`ZodString\` carrying a check, so it cannot be composed or
|
|
7953
|
+
narrowed — and it emits different JSON Schema, which means a published API
|
|
7954
|
+
contract would depend on which spelling someone reached for.
|
|
7955
|
+
|
|
7956
|
+
\`envkit\` is not affected: \`get('ADMIN_EMAIL').string().email()\` is its own
|
|
7957
|
+
builder, not Zod's. Leave it alone.
|
|
7958
|
+
|
|
7959
|
+
**Style.** Two-space indent, single quotes, semicolons, trailing commas, 80
|
|
7960
|
+
columns. \`import type\` for type-only imports. Run the formatter rather than
|
|
7961
|
+
matching it by hand.
|
|
7962
|
+
|
|
7963
|
+
**Construct ids are unique.** Two constructs sharing an id fails discovery for
|
|
7964
|
+
the whole project, not just the second one.`;
|
|
7965
|
+
}
|
|
7966
|
+
function testingSection(database) {
|
|
7967
|
+
return `## Testing
|
|
7968
|
+
|
|
7969
|
+
**Integration over unit.** Prefer the real dependency to a mock — a real
|
|
7970
|
+
database, a real cache. Mocks are for things you genuinely cannot run:
|
|
7971
|
+
the filesystem, the clock, external HTTP (use MSW for that).
|
|
7972
|
+
|
|
7973
|
+
**Test behaviour, not implementation.** Assert what the code did, not which
|
|
7974
|
+
functions it called on the way.
|
|
7975
|
+
|
|
7976
|
+
\`\`\`bash
|
|
7977
|
+
${database ? "gkm test # starts services, injects secrets, then runs vitest" : "gkm test"}
|
|
7978
|
+
\`\`\`
|
|
7979
|
+
|
|
7980
|
+
Run tests through \`gkm test\`, not \`vitest\` directly${database ? " — it is what starts the database and injects the secrets the suite needs" : ""}.`;
|
|
7981
|
+
}
|
|
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
|
+
|
|
8007
|
+
//#endregion
|
|
8008
|
+
//#region src/init/versions.ts
|
|
8009
|
+
const require$1 = (0, node_module.createRequire)(require("url").pathToFileURL(__filename).href);
|
|
8010
|
+
function loadPackageJson() {
|
|
8011
|
+
try {
|
|
8012
|
+
return require$1("../package.json");
|
|
8013
|
+
} catch {
|
|
8014
|
+
return require$1("../../package.json");
|
|
8015
|
+
}
|
|
8016
|
+
}
|
|
8017
|
+
const pkg = loadPackageJson();
|
|
8018
|
+
/**
|
|
8019
|
+
* CLI version resolved from package.json at runtime
|
|
8020
|
+
*/
|
|
8021
|
+
const CLI_VERSION = `~${pkg.version}`;
|
|
8022
|
+
/**
|
|
8023
|
+
* Package versions for @geekmidas packages
|
|
8024
|
+
*
|
|
8025
|
+
* AUTO-GENERATED (except CLI) - Do not edit manually
|
|
8026
|
+
* Run: pnpm --filter @geekmidas/cli sync-versions
|
|
8027
|
+
*/
|
|
8028
|
+
const GEEKMIDAS_VERSIONS = {
|
|
8029
|
+
"@geekmidas/audit": "~10.0.0-alpha.10",
|
|
8030
|
+
"@geekmidas/auth": "~10.0.0-alpha.10",
|
|
8031
|
+
"@geekmidas/cache": "~10.0.0-alpha.10",
|
|
8032
|
+
"@geekmidas/client": "~10.0.0-alpha.10",
|
|
8033
|
+
"@geekmidas/cloud": "~10.0.0-alpha.10",
|
|
8034
|
+
"@geekmidas/constructs": "~10.0.0-alpha.10",
|
|
8035
|
+
"@geekmidas/db": "~10.0.0-alpha.10",
|
|
8036
|
+
"@geekmidas/emailkit": "~10.0.0-alpha.10",
|
|
8037
|
+
"@geekmidas/envkit": "~10.0.0-alpha.10",
|
|
8038
|
+
"@geekmidas/errors": "~10.0.0-alpha.10",
|
|
8039
|
+
"@geekmidas/events": "~10.0.0-alpha.10",
|
|
8040
|
+
"@geekmidas/logger": "~10.0.0-alpha.10",
|
|
8041
|
+
"@geekmidas/manifest": "~10.0.0-alpha.10",
|
|
8042
|
+
"@geekmidas/rate-limit": "~10.0.0-alpha.10",
|
|
8043
|
+
"@geekmidas/schema": "~10.0.0-alpha.10",
|
|
8044
|
+
"@geekmidas/services": "~10.0.0-alpha.10",
|
|
8045
|
+
"@geekmidas/storage": "~10.0.0-alpha.10",
|
|
8046
|
+
"@geekmidas/studio": "~10.0.0-alpha.10",
|
|
8047
|
+
"@geekmidas/telescope": "~10.0.0-alpha.10",
|
|
8048
|
+
"@geekmidas/testkit": "~10.0.0-alpha.10",
|
|
8049
|
+
"@geekmidas/cli": CLI_VERSION
|
|
8050
|
+
};
|
|
8051
|
+
|
|
8052
|
+
//#endregion
|
|
8053
|
+
//#region src/init/generators/auth.ts
|
|
8054
|
+
/**
|
|
8055
|
+
* Generate auth app files for fullstack template
|
|
8056
|
+
* Uses better-auth with magic link authentication
|
|
8057
|
+
*/
|
|
8058
|
+
function generateAuthAppFiles(options) {
|
|
8059
|
+
if (!options.monorepo || options.template !== "fullstack") return [];
|
|
8060
|
+
const packageName = `@${options.name}/auth`;
|
|
8061
|
+
const modelsPackage = `@${options.name}/models`;
|
|
8062
|
+
const packageJson = {
|
|
8063
|
+
name: packageName,
|
|
8064
|
+
version: "0.0.1",
|
|
8065
|
+
private: true,
|
|
8066
|
+
type: "module",
|
|
8067
|
+
scripts: {
|
|
8068
|
+
dev: "gkm dev",
|
|
8069
|
+
build: "gkm build",
|
|
8070
|
+
"db:migrate": "gkm exec -- npx @better-auth/cli migrate",
|
|
8071
|
+
"db:generate": "gkm exec -- npx @better-auth/cli generate"
|
|
8072
|
+
},
|
|
8073
|
+
dependencies: {
|
|
8074
|
+
[modelsPackage]: "workspace:*",
|
|
8075
|
+
"@geekmidas/auth": GEEKMIDAS_VERSIONS["@geekmidas/auth"],
|
|
8076
|
+
"@geekmidas/constructs": GEEKMIDAS_VERSIONS["@geekmidas/constructs"],
|
|
8077
|
+
"@geekmidas/db": GEEKMIDAS_VERSIONS["@geekmidas/db"],
|
|
8078
|
+
"@geekmidas/envkit": GEEKMIDAS_VERSIONS["@geekmidas/envkit"],
|
|
8079
|
+
"@geekmidas/logger": GEEKMIDAS_VERSIONS["@geekmidas/logger"],
|
|
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
|
|
8085
|
+
},
|
|
8086
|
+
devDependencies: {
|
|
8087
|
+
"@geekmidas/cli": GEEKMIDAS_VERSIONS["@geekmidas/cli"],
|
|
8088
|
+
"@types/node": "~22.0.0",
|
|
8089
|
+
"@types/pg": "~8.23.1",
|
|
8090
|
+
tsx: "~4.20.0",
|
|
8091
|
+
typescript: "~5.8.2"
|
|
8092
|
+
}
|
|
8093
|
+
};
|
|
8094
|
+
const tsConfig = {
|
|
8095
|
+
extends: "../../tsconfig.json",
|
|
8096
|
+
compilerOptions: {
|
|
8097
|
+
noEmit: true,
|
|
8098
|
+
allowImportingTsExtensions: true,
|
|
8099
|
+
baseUrl: ".",
|
|
8100
|
+
paths: {
|
|
8101
|
+
"~/*": ["./src/*"],
|
|
8102
|
+
[`@${options.name}/*`]: ["../../packages/*/src"]
|
|
8103
|
+
}
|
|
8104
|
+
},
|
|
8105
|
+
include: ["src/**/*.ts"],
|
|
8106
|
+
exclude: ["node_modules", "dist"]
|
|
8107
|
+
};
|
|
8108
|
+
const gitignore = `node_modules/
|
|
8109
|
+
dist/
|
|
8110
|
+
.env.local
|
|
8111
|
+
*.log
|
|
8112
|
+
`;
|
|
8113
|
+
return [
|
|
8114
|
+
{
|
|
8115
|
+
path: "apps/auth/package.json",
|
|
8116
|
+
content: `${JSON.stringify(packageJson, null, 2)}\n`
|
|
8117
|
+
},
|
|
8118
|
+
{
|
|
8119
|
+
path: "apps/auth/tsconfig.json",
|
|
8120
|
+
content: `${JSON.stringify(tsConfig, null, 2)}\n`
|
|
8121
|
+
},
|
|
8122
|
+
{
|
|
8123
|
+
path: "apps/auth/.gitignore",
|
|
8124
|
+
content: gitignore
|
|
8125
|
+
}
|
|
8126
|
+
];
|
|
8127
|
+
}
|
|
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
|
+
|
|
8153
|
+
//#endregion
|
|
8154
|
+
//#region src/init/generators/config.ts
|
|
8155
|
+
/**
|
|
8156
|
+
* Vitest config content with globalSetup for database-enabled apps
|
|
8157
|
+
*/
|
|
8158
|
+
const vitestConfigContent = `import { defineConfig } from 'vitest/config';
|
|
8159
|
+
import tsconfigPaths from 'vite-tsconfig-paths';
|
|
8160
|
+
|
|
8161
|
+
export default defineConfig({
|
|
8162
|
+
plugins: [tsconfigPaths()],
|
|
8163
|
+
test: {
|
|
8164
|
+
environment: 'node',
|
|
8165
|
+
globalSetup: './test/globalSetup.ts',
|
|
8166
|
+
},
|
|
8167
|
+
});
|
|
8168
|
+
`;
|
|
7725
8169
|
/**
|
|
7726
8170
|
* Generate configuration files (gkm.config.ts, tsconfig.json, biome.json, turbo.json)
|
|
7727
8171
|
*/
|
|
@@ -7730,13 +8174,7 @@ function generateConfigFiles(options, template) {
|
|
|
7730
8174
|
const isServerless = template.name === "serverless";
|
|
7731
8175
|
const hasWorker = template.name === "worker";
|
|
7732
8176
|
const isFullstack = options.template === "fullstack";
|
|
7733
|
-
const getRoutesGlob = () =>
|
|
7734
|
-
switch (routesStructure) {
|
|
7735
|
-
case "centralized-endpoints": return "./src/endpoints/**/*.ts";
|
|
7736
|
-
case "centralized-routes": return "./src/routes/**/*.ts";
|
|
7737
|
-
case "domain-based": return "./src/**/routes/*.ts";
|
|
7738
|
-
}
|
|
7739
|
-
};
|
|
8177
|
+
const getRoutesGlob = () => routesGlob(routesStructure);
|
|
7740
8178
|
if (isFullstack) return generateSingleAppConfigFiles(options, template, {
|
|
7741
8179
|
telescope,
|
|
7742
8180
|
studio,
|
|
@@ -7747,7 +8185,7 @@ function generateConfigFiles(options, template) {
|
|
|
7747
8185
|
});
|
|
7748
8186
|
let gkmConfig = `import { defineConfig } from '@geekmidas/cli/config';
|
|
7749
8187
|
|
|
7750
|
-
export default defineConfig({${options.monorepo ? "" : `
|
|
8188
|
+
export default defineConfig({${stagesBlock(options.stages)}${options.monorepo ? "" : `
|
|
7751
8189
|
// One glob, every kind. What reconcile reads to derive the containers this
|
|
7752
8190
|
// app needs: a declared database is why a Postgres exists at all, so nothing
|
|
7753
8191
|
// lists \`postgres\` anywhere.
|
|
@@ -7789,20 +8227,7 @@ export default defineConfig({${options.monorepo ? "" : `
|
|
|
7789
8227
|
gkmConfig += `
|
|
7790
8228
|
});
|
|
7791
8229
|
`;
|
|
7792
|
-
const tsConfig = options.monorepo ? {
|
|
7793
|
-
extends: "../../tsconfig.json",
|
|
7794
|
-
compilerOptions: {
|
|
7795
|
-
noEmit: true,
|
|
7796
|
-
allowImportingTsExtensions: true,
|
|
7797
|
-
baseUrl: ".",
|
|
7798
|
-
paths: {
|
|
7799
|
-
"~/*": ["./src/*"],
|
|
7800
|
-
[`@${options.name}/*`]: ["../../packages/*/src"]
|
|
7801
|
-
}
|
|
7802
|
-
},
|
|
7803
|
-
include: ["src/**/*.ts"],
|
|
7804
|
-
exclude: ["node_modules", "dist"]
|
|
7805
|
-
} : {
|
|
8230
|
+
const tsConfig = options.monorepo ? workspaceApiTsConfig(options.name) : {
|
|
7806
8231
|
compilerOptions: {
|
|
7807
8232
|
target: "ES2022",
|
|
7808
8233
|
module: "NodeNext",
|
|
@@ -7814,7 +8239,9 @@ export default defineConfig({${options.monorepo ? "" : `
|
|
|
7814
8239
|
forceConsistentCasingInFileNames: true,
|
|
7815
8240
|
resolveJsonModule: true,
|
|
7816
8241
|
noEmit: true,
|
|
7817
|
-
allowImportingTsExtensions: true
|
|
8242
|
+
allowImportingTsExtensions: true,
|
|
8243
|
+
baseUrl: ".",
|
|
8244
|
+
paths: { "~/*": ["./src/*"] }
|
|
7818
8245
|
},
|
|
7819
8246
|
include: ["src/**/*.ts"],
|
|
7820
8247
|
exclude: ["node_modules", "dist"]
|
|
@@ -7840,7 +8267,7 @@ export default defineConfig({${options.monorepo ? "" : `
|
|
|
7840
8267
|
clientKind: "git",
|
|
7841
8268
|
useIgnoreFile: true
|
|
7842
8269
|
},
|
|
7843
|
-
|
|
8270
|
+
assist: { actions: { source: { organizeImports: "on" } } },
|
|
7844
8271
|
formatter: {
|
|
7845
8272
|
enabled: true,
|
|
7846
8273
|
indentStyle: "space",
|
|
@@ -7864,11 +8291,13 @@ export default defineConfig({${options.monorepo ? "" : `
|
|
|
7864
8291
|
style: { noNonNullAssertion: "off" }
|
|
7865
8292
|
}
|
|
7866
8293
|
},
|
|
7867
|
-
|
|
7868
|
-
|
|
7869
|
-
"
|
|
7870
|
-
"
|
|
7871
|
-
"
|
|
8294
|
+
css: { parser: { tailwindDirectives: true } },
|
|
8295
|
+
files: { includes: [
|
|
8296
|
+
"**",
|
|
8297
|
+
"!**/node_modules",
|
|
8298
|
+
"!**/dist",
|
|
8299
|
+
"!**/.gkm",
|
|
8300
|
+
"!**/coverage"
|
|
7872
8301
|
] }
|
|
7873
8302
|
};
|
|
7874
8303
|
const turboConfig = {
|
|
@@ -7922,8 +8351,16 @@ export default defineConfig({${options.monorepo ? "" : `
|
|
|
7922
8351
|
});
|
|
7923
8352
|
return files$1;
|
|
7924
8353
|
}
|
|
7925
|
-
|
|
7926
|
-
|
|
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 {
|
|
7927
8364
|
extends: "../../tsconfig.json",
|
|
7928
8365
|
compilerOptions: {
|
|
7929
8366
|
noEmit: true,
|
|
@@ -7931,12 +8368,16 @@ function generateSingleAppConfigFiles(options, _template, _helpers) {
|
|
|
7931
8368
|
baseUrl: ".",
|
|
7932
8369
|
paths: {
|
|
7933
8370
|
"~/*": ["./src/*"],
|
|
7934
|
-
[`@${
|
|
8371
|
+
[`@${name$1}/constructs/*`]: ["../../constructs/*"],
|
|
8372
|
+
[`@${name$1}/*`]: ["../../packages/*/src"]
|
|
7935
8373
|
}
|
|
7936
8374
|
},
|
|
7937
8375
|
include: ["src/**/*.ts"],
|
|
7938
8376
|
exclude: ["node_modules", "dist"]
|
|
7939
8377
|
};
|
|
8378
|
+
}
|
|
8379
|
+
function generateSingleAppConfigFiles(options, _template, _helpers) {
|
|
8380
|
+
const tsConfig = workspaceApiTsConfig(options.name);
|
|
7940
8381
|
const files$1 = [{
|
|
7941
8382
|
path: "tsconfig.json",
|
|
7942
8383
|
content: `${JSON.stringify(tsConfig, null, 2)}\n`
|
|
@@ -8072,7 +8513,7 @@ function generateDockerFiles(options, template, dbApps) {
|
|
|
8072
8513
|
MP_SMTP_AUTH: \${SMTP_USER:-${options.name}}:\${SMTP_PASS:-${options.name}}`);
|
|
8073
8514
|
if (options.services?.storage) {
|
|
8074
8515
|
services.push(` minio:
|
|
8075
|
-
image:
|
|
8516
|
+
image: pgsty/minio:RELEASE.2026-08-04T00-00-00Z
|
|
8076
8517
|
container_name: ${options.name}-minio
|
|
8077
8518
|
restart: unless-stopped
|
|
8078
8519
|
entrypoint: sh
|
|
@@ -8313,6 +8754,327 @@ coverage/
|
|
|
8313
8754
|
return files$1;
|
|
8314
8755
|
}
|
|
8315
8756
|
|
|
8757
|
+
//#endregion
|
|
8758
|
+
//#region src/init/generators/github.ts
|
|
8759
|
+
/** How each package manager is set up, installs, and runs a script. */
|
|
8760
|
+
const TOOLING = {
|
|
8761
|
+
pnpm: {
|
|
8762
|
+
setup: ` - uses: pnpm/action-setup@v4
|
|
8763
|
+
PNPM_VERSION
|
|
8764
|
+
- uses: actions/setup-node@v4
|
|
8765
|
+
with:
|
|
8766
|
+
node-version: 22
|
|
8767
|
+
cache: pnpm
|
|
8768
|
+
`,
|
|
8769
|
+
install: "pnpm install --frozen-lockfile",
|
|
8770
|
+
run: "pnpm run",
|
|
8771
|
+
exec: "pnpm exec"
|
|
8772
|
+
},
|
|
8773
|
+
npm: {
|
|
8774
|
+
setup: ` - uses: actions/setup-node@v4
|
|
8775
|
+
with:
|
|
8776
|
+
node-version: 22
|
|
8777
|
+
cache: npm
|
|
8778
|
+
`,
|
|
8779
|
+
install: "npm ci",
|
|
8780
|
+
run: "npm run",
|
|
8781
|
+
exec: "npx --no-install"
|
|
8782
|
+
},
|
|
8783
|
+
yarn: {
|
|
8784
|
+
setup: ` - run: corepack enable
|
|
8785
|
+
|
|
8786
|
+
- uses: actions/setup-node@v4
|
|
8787
|
+
with:
|
|
8788
|
+
node-version: 22
|
|
8789
|
+
cache: yarn
|
|
8790
|
+
`,
|
|
8791
|
+
install: "yarn install --immutable",
|
|
8792
|
+
run: "yarn run",
|
|
8793
|
+
exec: "yarn"
|
|
8794
|
+
},
|
|
8795
|
+
bun: {
|
|
8796
|
+
setup: ` - uses: oven-sh/setup-bun@v2
|
|
8797
|
+
`,
|
|
8798
|
+
install: "bun install --frozen-lockfile",
|
|
8799
|
+
run: "bun run",
|
|
8800
|
+
exec: "bunx"
|
|
8801
|
+
}
|
|
8802
|
+
};
|
|
8803
|
+
/** The package manager's steps, with pnpm pinned where package.json does not. */
|
|
8804
|
+
function tooling(options) {
|
|
8805
|
+
const pm = TOOLING[options.packageManager];
|
|
8806
|
+
return {
|
|
8807
|
+
...pm,
|
|
8808
|
+
setup: pm.setup.replace("PNPM_VERSION\n", options.monorepo ? "" : " with:\n version: 10\n")
|
|
8809
|
+
};
|
|
8810
|
+
}
|
|
8811
|
+
function ci(options) {
|
|
8812
|
+
const pm = tooling(options);
|
|
8813
|
+
return `name: CI
|
|
8814
|
+
|
|
8815
|
+
on:
|
|
8816
|
+
pull_request:
|
|
8817
|
+
branches: [main]
|
|
8818
|
+
|
|
8819
|
+
concurrency:
|
|
8820
|
+
group: \${{ github.workflow }}-\${{ github.ref }}
|
|
8821
|
+
cancel-in-progress: true
|
|
8822
|
+
|
|
8823
|
+
permissions:
|
|
8824
|
+
contents: read
|
|
8825
|
+
|
|
8826
|
+
jobs:
|
|
8827
|
+
validate:
|
|
8828
|
+
name: Validate
|
|
8829
|
+
runs-on: ubuntu-latest
|
|
8830
|
+
steps:
|
|
8831
|
+
- uses: actions/checkout@v4
|
|
8832
|
+
|
|
8833
|
+
${pm.setup}
|
|
8834
|
+
- name: Install
|
|
8835
|
+
run: ${pm.install}
|
|
8836
|
+
|
|
8837
|
+
- name: Build
|
|
8838
|
+
run: ${pm.run} build
|
|
8839
|
+
|
|
8840
|
+
- name: Lint
|
|
8841
|
+
run: ${pm.run} lint
|
|
8842
|
+
|
|
8843
|
+
- name: Typecheck
|
|
8844
|
+
run: ${pm.run} typecheck
|
|
8845
|
+
|
|
8846
|
+
# \`gkm test\` starts the containers the declared constructs need (the
|
|
8847
|
+
# runner has Docker) and, with GKM_AUTO_SETUP, generates throwaway
|
|
8848
|
+
# secrets for the test stage — there is no key to share with CI.
|
|
8849
|
+
- name: Test
|
|
8850
|
+
timeout-minutes: 15
|
|
8851
|
+
run: ${pm.run} test:once
|
|
8852
|
+
env:
|
|
8853
|
+
CI: true
|
|
8854
|
+
GKM_AUTO_SETUP: 1
|
|
8855
|
+
`;
|
|
8856
|
+
}
|
|
8857
|
+
const releaseDrafterWorkflow = `name: Release Drafter
|
|
8858
|
+
|
|
8859
|
+
on:
|
|
8860
|
+
push:
|
|
8861
|
+
branches: [main]
|
|
8862
|
+
pull_request:
|
|
8863
|
+
types: [opened, reopened, synchronize, edited]
|
|
8864
|
+
|
|
8865
|
+
permissions:
|
|
8866
|
+
contents: read
|
|
8867
|
+
|
|
8868
|
+
jobs:
|
|
8869
|
+
draft:
|
|
8870
|
+
name: Update release draft
|
|
8871
|
+
runs-on: ubuntu-latest
|
|
8872
|
+
permissions:
|
|
8873
|
+
contents: write
|
|
8874
|
+
pull-requests: write
|
|
8875
|
+
steps:
|
|
8876
|
+
- uses: release-drafter/release-drafter@v6
|
|
8877
|
+
with:
|
|
8878
|
+
commitish: main
|
|
8879
|
+
env:
|
|
8880
|
+
GITHUB_TOKEN: \${{ secrets.GITHUB_TOKEN }}
|
|
8881
|
+
`;
|
|
8882
|
+
function releaseDrafterConfig(options) {
|
|
8883
|
+
const mobile = options.frontendFramework === "expo" ? ` - title: "📱 Mobile"
|
|
8884
|
+
labels: [mobile, ios, android]
|
|
8885
|
+
collapse-after: 10
|
|
8886
|
+
` : "";
|
|
8887
|
+
return `name-template: "v$RESOLVED_VERSION"
|
|
8888
|
+
tag-template: "v$RESOLVED_VERSION"
|
|
8889
|
+
|
|
8890
|
+
categories:
|
|
8891
|
+
- title: "✨ New Features"
|
|
8892
|
+
labels: [feature, enhancement]
|
|
8893
|
+
collapse-after: 10
|
|
8894
|
+
- title: "🐛 Bug Fixes"
|
|
8895
|
+
labels: [bug, fix]
|
|
8896
|
+
collapse-after: 10
|
|
8897
|
+
${mobile} - title: "🏗 Infrastructure"
|
|
8898
|
+
labels: [infrastructure, ci]
|
|
8899
|
+
collapse-after: 10
|
|
8900
|
+
- title: "📦 Dependencies"
|
|
8901
|
+
labels: [dependencies]
|
|
8902
|
+
collapse-after: 5
|
|
8903
|
+
- title: "🔒 Security"
|
|
8904
|
+
labels: [security]
|
|
8905
|
+
- title: "🧹 Maintenance"
|
|
8906
|
+
labels: [chore, refactor]
|
|
8907
|
+
collapse-after: 5
|
|
8908
|
+
|
|
8909
|
+
change-template: "- $TITLE @$AUTHOR (#$NUMBER)"
|
|
8910
|
+
change-title-escapes: '\\<*_&'
|
|
8911
|
+
|
|
8912
|
+
version-resolver:
|
|
8913
|
+
major:
|
|
8914
|
+
labels: [breaking-change, major]
|
|
8915
|
+
minor:
|
|
8916
|
+
labels: [feature, enhancement, minor]
|
|
8917
|
+
patch:
|
|
8918
|
+
labels: [bug, fix, chore, dependencies, patch]
|
|
8919
|
+
default: patch
|
|
8920
|
+
|
|
8921
|
+
# Labelled from the conventional-commit prefix of the pull request title.
|
|
8922
|
+
autolabeler:
|
|
8923
|
+
- label: feature
|
|
8924
|
+
title: ["/^feat/i"]
|
|
8925
|
+
- label: bug
|
|
8926
|
+
title: ["/^fix/i"]
|
|
8927
|
+
- label: chore
|
|
8928
|
+
title: ["/^chore/i", "/^refactor/i"]
|
|
8929
|
+
- label: dependencies
|
|
8930
|
+
title: ["/^deps/i", "/^chore\\\\(deps\\\\)/i"]
|
|
8931
|
+
- label: ci
|
|
8932
|
+
title: ["/^ci/i"]
|
|
8933
|
+
- label: breaking-change
|
|
8934
|
+
title: ["/^\\\\w+(\\\\(.+\\\\))?!:/"]
|
|
8935
|
+
|
|
8936
|
+
template: |
|
|
8937
|
+
## What's Changed
|
|
8938
|
+
|
|
8939
|
+
$CHANGES
|
|
8940
|
+
|
|
8941
|
+
**Full Changelog**: https://github.com/$OWNER/$REPOSITORY/compare/$PREVIOUS_TAG...v$RESOLVED_VERSION
|
|
8942
|
+
`;
|
|
8943
|
+
}
|
|
8944
|
+
function deploy(options) {
|
|
8945
|
+
const pm = tooling(options);
|
|
8946
|
+
const sst = options.deployTarget === "sst";
|
|
8947
|
+
const credentials = sst ? `
|
|
8948
|
+
# An IAM role GitHub assumes through OIDC — no long-lived AWS keys. Set
|
|
8949
|
+
# AWS_ROLE_ARN as a variable on each environment.
|
|
8950
|
+
- uses: aws-actions/configure-aws-credentials@v4
|
|
8951
|
+
with:
|
|
8952
|
+
role-to-assume: \${{ vars.AWS_ROLE_ARN }}
|
|
8953
|
+
aws-region: ${options.region}
|
|
8954
|
+
` : "";
|
|
8955
|
+
const deployEnv = sst ? "" : `
|
|
8956
|
+
DOKPLOY_API_TOKEN: \${{ secrets.DOKPLOY_API_TOKEN }}
|
|
8957
|
+
DOKPLOY_ENDPOINT: \${{ vars.DOKPLOY_ENDPOINT }}`;
|
|
8958
|
+
return `name: Deploy
|
|
8959
|
+
|
|
8960
|
+
# Which stages exist, and which are protected, is read from \`stages\` in
|
|
8961
|
+
# gkm.config.ts — nothing here names one:
|
|
8962
|
+
#
|
|
8963
|
+
# push to main every deployed stage that is not protected
|
|
8964
|
+
# release published the protected stages (publish the drafted release)
|
|
8965
|
+
# run workflow the one stage you name
|
|
8966
|
+
on:
|
|
8967
|
+
push:
|
|
8968
|
+
branches: [main]
|
|
8969
|
+
release:
|
|
8970
|
+
types: [published]
|
|
8971
|
+
workflow_dispatch:
|
|
8972
|
+
inputs:
|
|
8973
|
+
stage:
|
|
8974
|
+
description: A deployed stage from gkm.config.ts
|
|
8975
|
+
required: true
|
|
8976
|
+
|
|
8977
|
+
permissions:
|
|
8978
|
+
contents: read
|
|
8979
|
+
id-token: write
|
|
8980
|
+
|
|
8981
|
+
jobs:
|
|
8982
|
+
stages:
|
|
8983
|
+
name: Pick stages
|
|
8984
|
+
runs-on: ubuntu-latest
|
|
8985
|
+
outputs:
|
|
8986
|
+
stages: \${{ steps.pick.outputs.stages }}
|
|
8987
|
+
steps:
|
|
8988
|
+
- uses: actions/checkout@v4
|
|
8989
|
+
|
|
8990
|
+
${pm.setup}
|
|
8991
|
+
- name: Install
|
|
8992
|
+
run: ${pm.install}
|
|
8993
|
+
|
|
8994
|
+
- name: Read stages from gkm.config.ts
|
|
8995
|
+
id: pick
|
|
8996
|
+
run: |
|
|
8997
|
+
${pm.exec} tsx -e "
|
|
8998
|
+
import config from './gkm.config.ts';
|
|
8999
|
+
const { deployed, protected: kept = [] } = config.stages;
|
|
9000
|
+
const event = process.env.EVENT;
|
|
9001
|
+
if (event === 'workflow_dispatch' && !deployed.includes(process.env.STAGE)) {
|
|
9002
|
+
console.error('Not a deployed stage: ' + process.env.STAGE + ' (deployed: ' + deployed.join(', ') + ')');
|
|
9003
|
+
process.exit(1);
|
|
9004
|
+
}
|
|
9005
|
+
const stages =
|
|
9006
|
+
event === 'workflow_dispatch' ? [process.env.STAGE]
|
|
9007
|
+
: event === 'release' ? kept
|
|
9008
|
+
: deployed.filter((stage) => !kept.includes(stage));
|
|
9009
|
+
console.log('stages=' + JSON.stringify(stages));
|
|
9010
|
+
" >> "$GITHUB_OUTPUT"
|
|
9011
|
+
env:
|
|
9012
|
+
EVENT: \${{ github.event_name }}
|
|
9013
|
+
STAGE: \${{ inputs.stage }}
|
|
9014
|
+
|
|
9015
|
+
deploy:
|
|
9016
|
+
name: Deploy \${{ matrix.stage }}
|
|
9017
|
+
needs: stages
|
|
9018
|
+
if: needs.stages.outputs.stages != '[]'
|
|
9019
|
+
runs-on: ubuntu-latest
|
|
9020
|
+
strategy:
|
|
9021
|
+
fail-fast: false
|
|
9022
|
+
matrix:
|
|
9023
|
+
stage: \${{ fromJSON(needs.stages.outputs.stages) }}
|
|
9024
|
+
# One GitHub environment per stage: its secrets and variables, and any
|
|
9025
|
+
# approval rule you put on a protected one.
|
|
9026
|
+
environment: \${{ matrix.stage }}
|
|
9027
|
+
concurrency:
|
|
9028
|
+
group: deploy-\${{ matrix.stage }}
|
|
9029
|
+
cancel-in-progress: false
|
|
9030
|
+
steps:
|
|
9031
|
+
- uses: actions/checkout@v4
|
|
9032
|
+
|
|
9033
|
+
${pm.setup}
|
|
9034
|
+
- name: Install
|
|
9035
|
+
run: ${pm.install}
|
|
9036
|
+
|
|
9037
|
+
# The stage's secrets are committed encrypted; the key never is. Each
|
|
9038
|
+
# environment holds its own as GKM_SECRETS_KEY.
|
|
9039
|
+
# Owner-only, as the CLI writes its own keys. The stage reaches the shell
|
|
9040
|
+
# as a variable, never pasted into the script: on a manual run it is
|
|
9041
|
+
# whatever was typed.
|
|
9042
|
+
- name: Stage secrets key
|
|
9043
|
+
run: |
|
|
9044
|
+
mkdir -p -m 700 ~/.gkm/${options.name}
|
|
9045
|
+
(umask 077 && printf '%s' "$KEY" > ~/.gkm/${options.name}/"$STAGE".key)
|
|
9046
|
+
env:
|
|
9047
|
+
KEY: \${{ secrets.GKM_SECRETS_KEY }}
|
|
9048
|
+
STAGE: \${{ matrix.stage }}
|
|
9049
|
+
${credentials}
|
|
9050
|
+
- name: Deploy
|
|
9051
|
+
run: ${pm.run} "deploy:$STAGE"
|
|
9052
|
+
env:
|
|
9053
|
+
STAGE: \${{ matrix.stage }}${deployEnv}
|
|
9054
|
+
`;
|
|
9055
|
+
}
|
|
9056
|
+
function generateGithubFiles(options) {
|
|
9057
|
+
const files$1 = [
|
|
9058
|
+
{
|
|
9059
|
+
path: ".github/workflows/ci.yml",
|
|
9060
|
+
content: ci(options)
|
|
9061
|
+
},
|
|
9062
|
+
{
|
|
9063
|
+
path: ".github/workflows/release-drafter.yml",
|
|
9064
|
+
content: releaseDrafterWorkflow
|
|
9065
|
+
},
|
|
9066
|
+
{
|
|
9067
|
+
path: ".github/release-drafter.yml",
|
|
9068
|
+
content: releaseDrafterConfig(options)
|
|
9069
|
+
}
|
|
9070
|
+
];
|
|
9071
|
+
if (options.monorepo && options.deployTarget !== "none") files$1.push({
|
|
9072
|
+
path: ".github/workflows/deploy.yml",
|
|
9073
|
+
content: deploy(options)
|
|
9074
|
+
});
|
|
9075
|
+
return files$1;
|
|
9076
|
+
}
|
|
9077
|
+
|
|
8316
9078
|
//#endregion
|
|
8317
9079
|
//#region src/init/generators/mobile-expo.ts
|
|
8318
9080
|
/**
|
|
@@ -8345,12 +9107,12 @@ function generateExpoAppFiles(options) {
|
|
|
8345
9107
|
dependencies: {
|
|
8346
9108
|
[apiPackage]: "workspace:*",
|
|
8347
9109
|
[modelsPackage]: "workspace:*",
|
|
8348
|
-
"@better-auth/expo": "
|
|
9110
|
+
"@better-auth/expo": DEPENDENCY_VERSIONS["better-auth"],
|
|
8349
9111
|
"@geekmidas/client": GEEKMIDAS_VERSIONS["@geekmidas/client"],
|
|
8350
9112
|
"@geekmidas/envkit": GEEKMIDAS_VERSIONS["@geekmidas/envkit"],
|
|
8351
9113
|
"@react-navigation/native": "^7.1.0",
|
|
8352
|
-
"@tanstack/react-query": "
|
|
8353
|
-
"better-auth": "
|
|
9114
|
+
"@tanstack/react-query": DEPENDENCY_VERSIONS["@tanstack/react-query"],
|
|
9115
|
+
"better-auth": DEPENDENCY_VERSIONS["better-auth"],
|
|
8354
9116
|
expo: "~55.0.0",
|
|
8355
9117
|
"expo-constants": "~55.0.0",
|
|
8356
9118
|
"expo-dev-client": "~55.0.0",
|
|
@@ -8486,7 +9248,7 @@ module.exports = withNativeWind(config, { input: './global.css' });
|
|
|
8486
9248
|
"@/*": ["./*"],
|
|
8487
9249
|
[modelsPackage]: ["../../packages/models/src"],
|
|
8488
9250
|
[`${modelsPackage}/*`]: ["../../packages/models/src/*"],
|
|
8489
|
-
[`${apiPackage}/client`]: ["../../apps/api/.gkm/openapi.ts"]
|
|
9251
|
+
[`${apiPackage}/client`]: ["../../apps/api/.gkm/openapi/api.ts"]
|
|
8490
9252
|
}
|
|
8491
9253
|
},
|
|
8492
9254
|
include: [
|
|
@@ -9044,14 +9806,142 @@ export type ListUsersResponse = z.infer<typeof ListUsersResponseSchema>;
|
|
|
9044
9806
|
];
|
|
9045
9807
|
}
|
|
9046
9808
|
|
|
9809
|
+
//#endregion
|
|
9810
|
+
//#region src/init/generators/deploy.ts
|
|
9811
|
+
/** One `deploy:<stage>` script per deployed stage. */
|
|
9812
|
+
const perStage = (options, command) => Object.fromEntries(options.stages.deployed.map((stage) => [`deploy:${stage}`, command(stage)]));
|
|
9813
|
+
function deployPackage(options) {
|
|
9814
|
+
switch (options.deployTarget) {
|
|
9815
|
+
case "dokploy": return {
|
|
9816
|
+
scripts: perStage(options, (stage) => `gkm deploy --provider dokploy --stage ${stage}`),
|
|
9817
|
+
dependencies: {},
|
|
9818
|
+
devDependencies: {}
|
|
9819
|
+
};
|
|
9820
|
+
case "sst": {
|
|
9821
|
+
const v = GEEKMIDAS_VERSIONS;
|
|
9822
|
+
return {
|
|
9823
|
+
scripts: perStage(options, (stage) => `gkm build --provider aws && sst deploy --stage ${stage}`),
|
|
9824
|
+
dependencies: {
|
|
9825
|
+
"@geekmidas/cloud": v["@geekmidas/cloud"],
|
|
9826
|
+
"@geekmidas/db": v["@geekmidas/db"],
|
|
9827
|
+
"@geekmidas/envkit": v["@geekmidas/envkit"],
|
|
9828
|
+
"@geekmidas/events": v["@geekmidas/events"],
|
|
9829
|
+
"@geekmidas/manifest": v["@geekmidas/manifest"],
|
|
9830
|
+
"@geekmidas/storage": v["@geekmidas/storage"],
|
|
9831
|
+
pg: DEPENDENCY_VERSIONS.pg
|
|
9832
|
+
},
|
|
9833
|
+
devDependencies: { sst: DEPENDENCY_VERSIONS.sst }
|
|
9834
|
+
};
|
|
9835
|
+
}
|
|
9836
|
+
default: return {
|
|
9837
|
+
scripts: {},
|
|
9838
|
+
dependencies: {},
|
|
9839
|
+
devDependencies: {}
|
|
9840
|
+
};
|
|
9841
|
+
}
|
|
9842
|
+
}
|
|
9843
|
+
function generateDeployFiles(options) {
|
|
9844
|
+
if (options.deployTarget !== "sst") return [];
|
|
9845
|
+
if (!options.region) throw new Error("An SST deploy needs a region.");
|
|
9846
|
+
const db = databaseFor();
|
|
9847
|
+
const mail = emailFor();
|
|
9848
|
+
const hasDatabase = options.template === "fullstack" || options.services.db;
|
|
9849
|
+
const inputs = [...hasDatabase ? [` // RDS needs a network. Created here rather than assumed, because a
|
|
9850
|
+
// VPC with NAT is a monthly cost in an account that may already
|
|
9851
|
+
// have one — replace it with \`sst.aws.Vpc.get(…)\` if so.
|
|
9852
|
+
${db.id}: { vpc },`] : [], ...options.services.mail ? [` // Every provider rejects an unverified sender, so there is no
|
|
9853
|
+
// default; the synth stops with \`EmailNeedsSender\` until it is set.
|
|
9854
|
+
${mail.id}: { from: process.env.MAIL_FROM as string },`] : []];
|
|
9855
|
+
return [{
|
|
9856
|
+
path: "sst.config.ts",
|
|
9857
|
+
content: `/// <reference path="./.sst/platform/config.d.ts" />
|
|
9858
|
+
|
|
9859
|
+
/**
|
|
9860
|
+
* ${options.name} on AWS.
|
|
9861
|
+
*
|
|
9862
|
+
* No bucket, cluster, queue or IAM is listed here: they come from the
|
|
9863
|
+
* constructs the application declares, through \`fromManifest\`. What is here
|
|
9864
|
+
* is what varies by stage, and the inputs a declaration cannot carry.
|
|
9865
|
+
*
|
|
9866
|
+
* Deploy with \`pnpm run deploy:<stage>\`, which builds the manifest first.
|
|
9867
|
+
*/
|
|
9868
|
+
const region = '${options.region}';
|
|
9869
|
+
|
|
9870
|
+
/** From \`stages.protected\` in gkm.config.ts. */
|
|
9871
|
+
const PROTECTED: string[] = ${JSON.stringify(options.stages.protected ?? []).replace(/"/g, "'")};
|
|
9872
|
+
|
|
9873
|
+
export default $config({
|
|
9874
|
+
app(input) {
|
|
9875
|
+
return {
|
|
9876
|
+
name: '${options.name}',
|
|
9877
|
+
// Any other stage is disposable; a protected one's database should not
|
|
9878
|
+
// vanish because somebody removed a stack.
|
|
9879
|
+
removal: PROTECTED.includes(input?.stage) ? 'retain' : 'remove',
|
|
9880
|
+
protect: PROTECTED.includes(input?.stage),
|
|
9881
|
+
home: 'aws',
|
|
9882
|
+
providers: { aws: { region } },
|
|
9883
|
+
};
|
|
9884
|
+
},
|
|
9885
|
+
|
|
9886
|
+
async run() {
|
|
9887
|
+
const { App, fromManifest, Stack } = await import('@geekmidas/cloud/sst');
|
|
9888
|
+
// Written by \`gkm build --provider aws\`. Imported rather than discovered,
|
|
9889
|
+
// so SST's toolchain never evaluates the application's own modules.
|
|
9890
|
+
const { backends, constructs } = await import('./.gkm/manifest/aws.js');
|
|
9891
|
+
|
|
9892
|
+
${hasDatabase ? " const vpc = new sst.aws.Vpc('Vpc', { nat: 'ec2' });\n\n" : ""} const app = new App({
|
|
9893
|
+
name: $app.name,
|
|
9894
|
+
stage: $app.stage,
|
|
9895
|
+
domain: '',
|
|
9896
|
+
region,
|
|
9897
|
+
hostedZoneId: '',
|
|
9898
|
+
});
|
|
9899
|
+
|
|
9900
|
+
return fromManifest(
|
|
9901
|
+
new Stack(app, '${(0, __geekmidas_manifest.canonicalId)(options.name)}'),
|
|
9902
|
+
constructs,
|
|
9903
|
+
{
|
|
9904
|
+
${inputs.join("\n")}
|
|
9905
|
+
},
|
|
9906
|
+
backends,
|
|
9907
|
+
);
|
|
9908
|
+
},
|
|
9909
|
+
});
|
|
9910
|
+
`
|
|
9911
|
+
}];
|
|
9912
|
+
}
|
|
9913
|
+
|
|
9047
9914
|
//#endregion
|
|
9048
9915
|
//#region src/init/generators/monorepo.ts
|
|
9049
9916
|
/**
|
|
9917
|
+
* What the root `constructs/` folder imports, and the peers each construct
|
|
9918
|
+
* needs, for the services this workspace was scaffolded with.
|
|
9919
|
+
*/
|
|
9920
|
+
function rootConstructDependencies(options) {
|
|
9921
|
+
const v = GEEKMIDAS_VERSIONS;
|
|
9922
|
+
return {
|
|
9923
|
+
"@geekmidas/constructs": v["@geekmidas/constructs"],
|
|
9924
|
+
"@geekmidas/logger": v["@geekmidas/logger"],
|
|
9925
|
+
...options.loggerType === "pino" ? { pino: DEPENDENCY_VERSIONS.pino } : {},
|
|
9926
|
+
"@geekmidas/telescope": v["@geekmidas/telescope"],
|
|
9927
|
+
"@geekmidas/auth": v["@geekmidas/auth"],
|
|
9928
|
+
"better-auth": DEPENDENCY_VERSIONS["better-auth"],
|
|
9929
|
+
"@geekmidas/db": v["@geekmidas/db"],
|
|
9930
|
+
kysely: DEPENDENCY_VERSIONS.kysely,
|
|
9931
|
+
pg: DEPENDENCY_VERSIONS.pg,
|
|
9932
|
+
...options.services.storage ? { "@geekmidas/storage": v["@geekmidas/storage"] } : {},
|
|
9933
|
+
...options.services.mail ? { "@geekmidas/emailkit": v["@geekmidas/emailkit"] } : {},
|
|
9934
|
+
...options.services.cache ? { "@geekmidas/cache": v["@geekmidas/cache"] } : {}
|
|
9935
|
+
};
|
|
9936
|
+
}
|
|
9937
|
+
/**
|
|
9050
9938
|
* Generate monorepo root files (pnpm-workspace.yaml, root package.json, etc.)
|
|
9051
9939
|
*/
|
|
9052
9940
|
function generateMonorepoFiles(options, _template) {
|
|
9053
9941
|
if (!options.monorepo) return [];
|
|
9054
9942
|
const isFullstack = options.template === "fullstack";
|
|
9943
|
+
const deploy$1 = deployPackage(options);
|
|
9944
|
+
const isSst = options.deployTarget === "sst";
|
|
9055
9945
|
const rootPackageJson = {
|
|
9056
9946
|
name: options.name,
|
|
9057
9947
|
version: "0.0.1",
|
|
@@ -9068,9 +9958,13 @@ function generateMonorepoFiles(options, _template) {
|
|
|
9068
9958
|
fmt: "biome format . --write",
|
|
9069
9959
|
"fmt:check": "biome format .",
|
|
9070
9960
|
...isFullstack ? { storybook: "pnpm --filter ./packages/ui storybook" } : {},
|
|
9071
|
-
...
|
|
9961
|
+
...deploy$1.scripts
|
|
9962
|
+
},
|
|
9963
|
+
dependencies: {
|
|
9964
|
+
zod: DEPENDENCY_VERSIONS.zod,
|
|
9965
|
+
...isFullstack ? rootConstructDependencies(options) : {},
|
|
9966
|
+
...deploy$1.dependencies
|
|
9072
9967
|
},
|
|
9073
|
-
dependencies: { zod: "~4.1.0" },
|
|
9074
9968
|
devDependencies: {
|
|
9075
9969
|
"@biomejs/biome": "~2.3.0",
|
|
9076
9970
|
"@geekmidas/cli": GEEKMIDAS_VERSIONS["@geekmidas/cli"],
|
|
@@ -9078,7 +9972,8 @@ function generateMonorepoFiles(options, _template) {
|
|
|
9078
9972
|
tsx: "~4.20.0",
|
|
9079
9973
|
turbo: "~2.3.0",
|
|
9080
9974
|
typescript: "~5.8.2",
|
|
9081
|
-
vitest: "~4.0.0"
|
|
9975
|
+
vitest: "~4.0.0",
|
|
9976
|
+
...deploy$1.devDependencies
|
|
9082
9977
|
}
|
|
9083
9978
|
};
|
|
9084
9979
|
const apiPathParts = options.apiPath.split("/");
|
|
@@ -9094,7 +9989,7 @@ function generateMonorepoFiles(options, _template) {
|
|
|
9094
9989
|
clientKind: "git",
|
|
9095
9990
|
useIgnoreFile: true
|
|
9096
9991
|
},
|
|
9097
|
-
|
|
9992
|
+
assist: { actions: { source: { organizeImports: "on" } } },
|
|
9098
9993
|
formatter: {
|
|
9099
9994
|
enabled: true,
|
|
9100
9995
|
indentStyle: "space",
|
|
@@ -9118,11 +10013,14 @@ function generateMonorepoFiles(options, _template) {
|
|
|
9118
10013
|
style: { noNonNullAssertion: "off" }
|
|
9119
10014
|
}
|
|
9120
10015
|
},
|
|
9121
|
-
|
|
9122
|
-
|
|
9123
|
-
"
|
|
9124
|
-
"
|
|
9125
|
-
"
|
|
10016
|
+
css: { parser: { tailwindDirectives: true } },
|
|
10017
|
+
files: { includes: [
|
|
10018
|
+
"**",
|
|
10019
|
+
"!**/node_modules",
|
|
10020
|
+
"!**/dist",
|
|
10021
|
+
"!**/.gkm",
|
|
10022
|
+
"!**/coverage",
|
|
10023
|
+
...isSst ? ["!**/.sst", "!sst-env.d.ts"] : []
|
|
9126
10024
|
] }
|
|
9127
10025
|
};
|
|
9128
10026
|
const turboConfig = {
|
|
@@ -9188,9 +10086,15 @@ coverage/
|
|
|
9188
10086
|
|
|
9189
10087
|
# Turbo
|
|
9190
10088
|
.turbo/
|
|
9191
|
-
`;
|
|
10089
|
+
${isSst ? "\n# SST\n.sst/\n" : ""}`;
|
|
9192
10090
|
const tsConfig = {
|
|
9193
10091
|
compilerOptions: {
|
|
10092
|
+
...isFullstack ? {
|
|
10093
|
+
baseUrl: ".",
|
|
10094
|
+
paths: { [`@${options.name}/constructs/*`]: ["./constructs/*"] },
|
|
10095
|
+
allowImportingTsExtensions: true,
|
|
10096
|
+
noEmit: true
|
|
10097
|
+
} : {},
|
|
9194
10098
|
target: "ES2022",
|
|
9195
10099
|
module: "NodeNext",
|
|
9196
10100
|
moduleResolution: "NodeNext",
|
|
@@ -9201,16 +10105,21 @@ coverage/
|
|
|
9201
10105
|
forceConsistentCasingInFileNames: true,
|
|
9202
10106
|
resolveJsonModule: true
|
|
9203
10107
|
},
|
|
9204
|
-
exclude: [
|
|
10108
|
+
exclude: [
|
|
10109
|
+
"node_modules",
|
|
10110
|
+
"dist",
|
|
10111
|
+
...isSst ? ["sst.config.ts", ".sst"] : []
|
|
10112
|
+
]
|
|
9205
10113
|
};
|
|
9206
10114
|
const vitestConfig = `import { defineConfig } from 'vitest/config';
|
|
9207
10115
|
|
|
9208
10116
|
export default defineConfig({
|
|
9209
10117
|
test: {
|
|
9210
|
-
|
|
9211
|
-
|
|
9212
|
-
|
|
9213
|
-
|
|
10118
|
+
// Each app and package is its own project, so its own config — an app's
|
|
10119
|
+
// \`globalSetup\`, its path aliases — applies to its own tests.
|
|
10120
|
+
projects: ['apps/*', 'packages/*'],
|
|
10121
|
+
// The shared packages ship without tests of their own.
|
|
10122
|
+
passWithNoTests: true,
|
|
9214
10123
|
coverage: {
|
|
9215
10124
|
provider: 'v8',
|
|
9216
10125
|
reporter: ['text', 'json', 'html'],
|
|
@@ -9291,132 +10200,224 @@ export default defineConfig({
|
|
|
9291
10200
|
{
|
|
9292
10201
|
path: ".vscode/extensions.json",
|
|
9293
10202
|
content: `${JSON.stringify(vscodeExtensions, null, " ")}\n`
|
|
9294
|
-
}
|
|
10203
|
+
},
|
|
10204
|
+
...generateDeployFiles(options)
|
|
9295
10205
|
];
|
|
9296
10206
|
if (isFullstack) files$1.push({
|
|
9297
10207
|
path: "gkm.config.ts",
|
|
9298
10208
|
content: generateWorkspaceConfig(options)
|
|
9299
10209
|
});
|
|
9300
|
-
return files$1;
|
|
9301
|
-
}
|
|
10210
|
+
return files$1;
|
|
10211
|
+
}
|
|
10212
|
+
/**
|
|
10213
|
+
* `gkm.config.ts` for the fullstack workspace.
|
|
10214
|
+
*
|
|
10215
|
+
* What is *not* here is the point. There is no `apps` block: the apps are the
|
|
10216
|
+
* constructs that said they have a process — a `RestApi`, a `BetterAuth`, a
|
|
10217
|
+
* `StaticSite` — and naming them again here would be the same sentence written
|
|
10218
|
+
* twice with only one copy checked.
|
|
10219
|
+
*
|
|
10220
|
+
* There is no `services` block either unless something non-default was picked.
|
|
10221
|
+
* Every key defaults from the deploy target: on a server target a cache is a
|
|
10222
|
+
* table in the database that is already there, storage is MinIO, mail is SES.
|
|
10223
|
+
*/
|
|
10224
|
+
function generateWorkspaceConfig(options) {
|
|
10225
|
+
let config = `import { defineWorkspace } from '@geekmidas/cli/config';
|
|
10226
|
+
|
|
10227
|
+
export default defineWorkspace({
|
|
10228
|
+
// The scope every physical name is built from: \`Database\` becomes
|
|
10229
|
+
// \`production-${options.name}-database\` on Dokploy and on AWS alike.
|
|
10230
|
+
name: '${options.name}',
|
|
10231
|
+
${stagesBlock(options.stages)}
|
|
10232
|
+
|
|
10233
|
+
// Every kind, in every app. A declared database is why a Postgres exists, a
|
|
10234
|
+
// declared bucket is why MinIO does, a declared topic is why a broker does —
|
|
10235
|
+
// none of it listed here. It is also where the apps come from: a
|
|
10236
|
+
// \`StaticSite\` is an app, and so is every \`RestApi\`. And it finds each
|
|
10237
|
+
// app's endpoints, crons and subscribers: an endpoint belongs to the surface
|
|
10238
|
+
// it was built from, not to the directory it sits in.
|
|
10239
|
+
constructs: [
|
|
10240
|
+
${workspaceConstructsGlobs(options.routesStructure, (0, node_path.dirname)(options.apiPath)).map((glob) => ` '${glob}',`).join("\n")}
|
|
10241
|
+
],
|
|
10242
|
+
|
|
10243
|
+
secrets: {
|
|
10244
|
+
enabled: true,
|
|
10245
|
+
},
|
|
10246
|
+
});
|
|
10247
|
+
`;
|
|
10248
|
+
if (options.services.events && options.services.events !== "pgboss") config = config.replace(" secrets: {", ` services: {
|
|
10249
|
+
events: '${options.services.events}',
|
|
10250
|
+
},
|
|
10251
|
+
|
|
10252
|
+
secrets: {`);
|
|
10253
|
+
return config;
|
|
10254
|
+
}
|
|
10255
|
+
/**
|
|
10256
|
+
* The workspace's constructs, at its root rather than inside an app.
|
|
10257
|
+
*
|
|
10258
|
+
* They outlive the app that used them first: a site depending on the API is a
|
|
10259
|
+
* fact about the workspace, and a construct reaching across into a sibling
|
|
10260
|
+
* app's `src/` to state it is the shape this layout removes. Apps import them
|
|
10261
|
+
* through the `@<name>/constructs/*` path the root tsconfig maps.
|
|
10262
|
+
*
|
|
10263
|
+
* Every one of these is also an answer config used to give: the database is
|
|
10264
|
+
* why a Postgres exists, the surface and the site are the apps, and the edges
|
|
10265
|
+
* between them are the CORS origins and the build order.
|
|
10266
|
+
*/
|
|
10267
|
+
function generateRootConstructs(options) {
|
|
10268
|
+
if (!options.monorepo || options.template !== "fullstack") return [];
|
|
10269
|
+
const { name: name$1, services, frontendFramework } = options;
|
|
10270
|
+
const db = databaseFor();
|
|
10271
|
+
const files$1 = [];
|
|
10272
|
+
files$1.push({
|
|
10273
|
+
path: "constructs/logger.ts",
|
|
10274
|
+
content: `import { createLogger } from '@geekmidas/logger/${options.loggerType}';
|
|
10275
|
+
|
|
10276
|
+
/**
|
|
10277
|
+
* The logger every endpoint runs with.
|
|
10278
|
+
*
|
|
10279
|
+
* Named once, here, because the surface carries it. It used to be
|
|
10280
|
+
* \`logger: './src/config/logger#logger'\` in config — a module path the build
|
|
10281
|
+
* printed into each generated handler, checked by nothing and wrong the moment
|
|
10282
|
+
* the file moved.
|
|
10283
|
+
*/
|
|
10284
|
+
export const logger = createLogger();
|
|
10285
|
+
`
|
|
10286
|
+
});
|
|
10287
|
+
files$1.push({
|
|
10288
|
+
path: "constructs/database.ts",
|
|
10289
|
+
content: `import { KyselyDatabase } from '@geekmidas/constructs/database/kysely';
|
|
10290
|
+
import type { Generated } from 'kysely';
|
|
10291
|
+
|
|
10292
|
+
/** Your database schema. Add tables here. */
|
|
10293
|
+
export interface Database {
|
|
10294
|
+
users: {
|
|
10295
|
+
id: Generated<string>;
|
|
10296
|
+
name: string;
|
|
10297
|
+
email: string;
|
|
10298
|
+
created_at: Generated<Date>;
|
|
10299
|
+
};
|
|
10300
|
+
}
|
|
10301
|
+
|
|
10302
|
+
/**
|
|
10303
|
+
* The database, declared once.
|
|
10304
|
+
*
|
|
10305
|
+
* The container, the database inside it, its roles and schema, and
|
|
10306
|
+
* \`${db.urlKey}\` all derive from this line — which is why nothing lists
|
|
10307
|
+
* \`postgres\` anywhere.
|
|
10308
|
+
*/
|
|
10309
|
+
export const database = new KyselyDatabase<Database, '${db.id}'>('${db.id}');
|
|
10310
|
+
|
|
10311
|
+
/**
|
|
10312
|
+
* The auth server's own schema in that same Postgres, with its own role.
|
|
10313
|
+
*
|
|
10314
|
+
* A schema tenant rather than a second database: one container, and a role
|
|
10315
|
+
* whose \`search_path\` is pinned, so Better Auth's tables cannot collide with
|
|
10316
|
+
* the application's.
|
|
10317
|
+
*/
|
|
10318
|
+
export const authDb = database.schema<Record<string, never>, 'AuthDb'>('AuthDb');
|
|
10319
|
+
`
|
|
10320
|
+
});
|
|
10321
|
+
files$1.push({
|
|
10322
|
+
path: "constructs/auth.ts",
|
|
10323
|
+
content: `import { BetterAuth } from '@geekmidas/constructs/auth';
|
|
10324
|
+
import { authDb } from './database.ts';
|
|
10325
|
+
|
|
10326
|
+
/**
|
|
10327
|
+
* The auth server.
|
|
10328
|
+
*
|
|
10329
|
+
* There is no \`src/index.ts\` to go with this, and that is the point: the
|
|
10330
|
+
* routes are a wildcard, so no glob finds them and the build generates the
|
|
10331
|
+
* entry from this declaration instead. CORS, the trusted origins and the
|
|
10332
|
+
* cookie domain come from whatever declared an edge to it — nobody writes an
|
|
10333
|
+
* origin down.
|
|
10334
|
+
*/
|
|
10335
|
+
export const auth = new BetterAuth('Auth', {
|
|
10336
|
+
path: 'apps/auth',
|
|
10337
|
+
database: authDb,
|
|
10338
|
+
basePath: '/api/auth',
|
|
10339
|
+
});
|
|
10340
|
+
`
|
|
10341
|
+
});
|
|
10342
|
+
files$1.push({
|
|
10343
|
+
path: "constructs/api.ts",
|
|
10344
|
+
content: `import { RestApi } from '@geekmidas/constructs/rest-api';
|
|
10345
|
+
import { auth } from './auth.ts';
|
|
10346
|
+
import { logger } from './logger.ts';
|
|
10347
|
+
|
|
9302
10348
|
/**
|
|
9303
|
-
*
|
|
9304
|
-
*
|
|
9305
|
-
|
|
9306
|
-
|
|
9307
|
-
|
|
9308
|
-
|
|
9309
|
-
|
|
9310
|
-
|
|
9311
|
-
|
|
9312
|
-
|
|
9313
|
-
|
|
9314
|
-
|
|
9315
|
-
|
|
9316
|
-
|
|
9317
|
-
|
|
9318
|
-
|
|
9319
|
-
|
|
9320
|
-
|
|
9321
|
-
|
|
9322
|
-
|
|
9323
|
-
|
|
9324
|
-
|
|
9325
|
-
|
|
9326
|
-
|
|
9327
|
-
type: 'web',
|
|
9328
|
-
framework: 'nextjs',
|
|
9329
|
-
path: 'apps/web',
|
|
9330
|
-
port: 3001,
|
|
9331
|
-
dependencies: ['api', 'auth'],
|
|
9332
|
-
config: {
|
|
9333
|
-
client: './src/config/client.ts',
|
|
9334
|
-
server: './src/config/server.ts',
|
|
9335
|
-
},
|
|
9336
|
-
},`;
|
|
9337
|
-
}
|
|
9338
|
-
}
|
|
10349
|
+
* The application's HTTP surface — one RestApi, one container.
|
|
10350
|
+
*
|
|
10351
|
+
* \`path\` is where its app lives. Its handlers are found by the workspace's
|
|
10352
|
+
* \`constructs\` glob, and belong to it because they were built from it.
|
|
10353
|
+
*/
|
|
10354
|
+
export const api = new RestApi('Api', {
|
|
10355
|
+
path: '${options.apiPath}',
|
|
10356
|
+
|
|
10357
|
+
// Typed out rather than omitted: an API that ships open because a field was
|
|
10358
|
+
// left off is the one default worth refusing to have.
|
|
10359
|
+
defaultAuthorizer: 'none',
|
|
10360
|
+
|
|
10361
|
+
// The actual logger, not a path to one.
|
|
10362
|
+
logger,
|
|
10363
|
+
}).auth(auth);
|
|
10364
|
+
`
|
|
10365
|
+
});
|
|
10366
|
+
const variant = frontendFramework === "tanstack-start" ? "{ path: 'apps/web', variant: 'tanstack' }" : frontendFramework === "expo" ? void 0 : "{ path: 'apps/web', variant: 'next' }";
|
|
10367
|
+
if (variant !== void 0) files$1.push({
|
|
10368
|
+
path: "constructs/site.ts",
|
|
10369
|
+
content: `import { StaticSite } from '@geekmidas/constructs/site';
|
|
10370
|
+
import { api } from './api.ts';
|
|
10371
|
+
import { auth } from './auth.ts';
|
|
10372
|
+
|
|
9339
10373
|
/**
|
|
9340
|
-
*
|
|
9341
|
-
|
|
9342
|
-
|
|
9343
|
-
|
|
9344
|
-
|
|
9345
|
-
|
|
9346
|
-
|
|
9347
|
-
|
|
9348
|
-
|
|
9349
|
-
|
|
9350
|
-
|
|
9351
|
-
|
|
10374
|
+
* The frontend — a construct like any other, which is what makes it an app.
|
|
10375
|
+
*
|
|
10376
|
+
* \`path\` is where it lives. \`.dependsOn()\` is the single fact
|
|
10377
|
+
* behind four things that are hand-maintained otherwise: this site's
|
|
10378
|
+
* build-time API URL, the API's CORS origins, the auth server's trusted
|
|
10379
|
+
* origins, and which generated client lands here.
|
|
10380
|
+
*/
|
|
10381
|
+
export const web = new StaticSite('Web', ${variant}).dependsOn([api, auth]);
|
|
10382
|
+
`
|
|
10383
|
+
});
|
|
10384
|
+
if (services.storage) {
|
|
10385
|
+
const bucket = storageFor();
|
|
10386
|
+
files$1.push({
|
|
10387
|
+
path: "constructs/storage.ts",
|
|
10388
|
+
content: `import { ObjectStorage } from '@geekmidas/constructs/object-storage';
|
|
9352
10389
|
|
|
9353
|
-
|
|
9354
|
-
|
|
9355
|
-
|
|
9356
|
-
|
|
9357
|
-
type: 'backend',
|
|
9358
|
-
path: 'apps/api',
|
|
9359
|
-
port: 3000,
|
|
9360
|
-
routes: '${getRoutesGlob()}',
|
|
9361
|
-
envParser: './src/config/env#envParser',
|
|
9362
|
-
logger: './src/config/logger#logger',
|
|
9363
|
-
dependencies: ['auth'],`;
|
|
9364
|
-
if (telescope) config += `
|
|
9365
|
-
telescope: {
|
|
9366
|
-
enabled: true,
|
|
9367
|
-
path: '/__telescope',
|
|
9368
|
-
},`;
|
|
9369
|
-
config += `
|
|
9370
|
-
openapi: {
|
|
9371
|
-
enabled: true,
|
|
9372
|
-
},
|
|
9373
|
-
},
|
|
9374
|
-
auth: {
|
|
9375
|
-
type: 'backend',
|
|
9376
|
-
path: 'apps/auth',
|
|
9377
|
-
port: 3002,
|
|
9378
|
-
entry: './src/index.ts',
|
|
9379
|
-
framework: 'better-auth',
|
|
9380
|
-
envParser: './src/config/env#envParser',
|
|
9381
|
-
logger: './src/config/logger#logger',
|
|
9382
|
-
},
|
|
9383
|
-
${generateFrontendAppEntry(options)}
|
|
9384
|
-
},
|
|
9385
|
-
shared: {
|
|
9386
|
-
packages: ['packages/*'],
|
|
9387
|
-
models: {
|
|
9388
|
-
path: 'packages/models',
|
|
9389
|
-
schema: 'zod',
|
|
9390
|
-
},
|
|
9391
|
-
},`;
|
|
9392
|
-
if (services.db || services.cache || services.mail || services.storage || services.events) {
|
|
9393
|
-
config += `
|
|
9394
|
-
services: {`;
|
|
9395
|
-
if (services.db) config += `
|
|
9396
|
-
db: true,`;
|
|
9397
|
-
if (services.cache) config += `
|
|
9398
|
-
cache: true,`;
|
|
9399
|
-
if (services.mail) config += `
|
|
9400
|
-
mail: true,`;
|
|
9401
|
-
if (services.storage) config += `
|
|
9402
|
-
storage: true,`;
|
|
9403
|
-
if (services.events) config += `
|
|
9404
|
-
events: '${services.events}',`;
|
|
9405
|
-
config += `
|
|
9406
|
-
},`;
|
|
10390
|
+
/** A bucket — MinIO locally, S3 deployed, one declaration for both. */
|
|
10391
|
+
export const uploads = new ObjectStorage('${bucket.id}');
|
|
10392
|
+
`
|
|
10393
|
+
});
|
|
9407
10394
|
}
|
|
9408
|
-
if (
|
|
9409
|
-
|
|
9410
|
-
|
|
9411
|
-
|
|
9412
|
-
|
|
9413
|
-
|
|
9414
|
-
|
|
9415
|
-
|
|
9416
|
-
|
|
9417
|
-
});
|
|
9418
|
-
|
|
9419
|
-
|
|
10395
|
+
if (services.mail) {
|
|
10396
|
+
const mail = emailFor();
|
|
10397
|
+
files$1.push({
|
|
10398
|
+
path: "constructs/email.ts",
|
|
10399
|
+
content: `import { Email } from '@geekmidas/constructs/email';
|
|
10400
|
+
|
|
10401
|
+
/** Outbound mail — Mailpit locally, SES deployed. */
|
|
10402
|
+
export const email = new Email('${mail.id}', { templates: {} });
|
|
10403
|
+
`
|
|
10404
|
+
});
|
|
10405
|
+
}
|
|
10406
|
+
if (services.cache) {
|
|
10407
|
+
const kv = cacheFor();
|
|
10408
|
+
files$1.push({
|
|
10409
|
+
path: "constructs/cache.ts",
|
|
10410
|
+
content: `import { Cache } from '@geekmidas/constructs/cache';
|
|
10411
|
+
|
|
10412
|
+
/**
|
|
10413
|
+
* A cache. Which backend serves it follows the deploy target — a table in the
|
|
10414
|
+
* database on a server, Upstash on AWS — so nothing here says where it lives.
|
|
10415
|
+
*/
|
|
10416
|
+
export const cache = new Cache('${kv.id}');
|
|
10417
|
+
`
|
|
10418
|
+
});
|
|
10419
|
+
}
|
|
10420
|
+
return files$1;
|
|
9420
10421
|
}
|
|
9421
10422
|
|
|
9422
10423
|
//#endregion
|
|
@@ -9436,9 +10437,9 @@ const apiTemplate = {
|
|
|
9436
10437
|
"@geekmidas/errors": GEEKMIDAS_VERSIONS["@geekmidas/errors"],
|
|
9437
10438
|
"@geekmidas/auth": GEEKMIDAS_VERSIONS["@geekmidas/auth"],
|
|
9438
10439
|
"@hono/node-server": "~1.14.1",
|
|
9439
|
-
hono:
|
|
9440
|
-
pino:
|
|
9441
|
-
zod:
|
|
10440
|
+
hono: DEPENDENCY_VERSIONS.hono,
|
|
10441
|
+
pino: DEPENDENCY_VERSIONS.pino,
|
|
10442
|
+
zod: DEPENDENCY_VERSIONS.zod
|
|
9442
10443
|
},
|
|
9443
10444
|
devDependencies: {
|
|
9444
10445
|
"@biomejs/biome": "~2.3.0",
|
|
@@ -9462,10 +10463,10 @@ const apiTemplate = {
|
|
|
9462
10463
|
},
|
|
9463
10464
|
files: (options) => {
|
|
9464
10465
|
const { loggerType, routesStructure, monorepo, name: name$1, services } = options;
|
|
9465
|
-
const bucket = storageFor(
|
|
9466
|
-
const kv = cacheFor(
|
|
9467
|
-
const db = databaseFor(
|
|
9468
|
-
const mail = emailFor(
|
|
10466
|
+
const bucket = storageFor();
|
|
10467
|
+
const kv = cacheFor();
|
|
10468
|
+
const db = databaseFor();
|
|
10469
|
+
const mail = emailFor();
|
|
9469
10470
|
const src = (path) => monorepo ? `~/${path}` : `./${path}`;
|
|
9470
10471
|
const declares = !monorepo;
|
|
9471
10472
|
const loggerContent = `import { createLogger } from '@geekmidas/logger/${loggerType}';
|
|
@@ -9484,14 +10485,24 @@ export const logger = createLogger();
|
|
|
9484
10485
|
}
|
|
9485
10486
|
}
|
|
9486
10487
|
};
|
|
9487
|
-
|
|
10488
|
+
/**
|
|
10489
|
+
* Where a file at the app's `src/` root reaches a construct.
|
|
10490
|
+
*
|
|
10491
|
+
* A workspace keeps them at its own root, so the app goes through the
|
|
10492
|
+
* `@<name>/constructs/*` alias rather than climbing out with `../../`.
|
|
10493
|
+
*/
|
|
10494
|
+
const constructsImport = (file) => monorepo ? `@${name$1}/constructs/${file}.ts` : `./constructs/${file}.ts`;
|
|
10495
|
+
const apiImport = monorepo ? `@${name$1}/constructs/api.ts` : routesStructure === "domain-based" ? "../../constructs/api.js" : "../constructs/api.js";
|
|
9488
10496
|
const files$1 = [
|
|
9489
|
-
{
|
|
10497
|
+
...declares ? [{
|
|
9490
10498
|
path: "src/constructs/api.ts",
|
|
9491
10499
|
content: `import { RestApi } from '@geekmidas/constructs/rest-api';
|
|
9492
10500
|
import { logger } from '../config/logger.ts';
|
|
9493
10501
|
|
|
9494
10502
|
export const api = new RestApi('Api', {
|
|
10503
|
+
// The project is the app.
|
|
10504
|
+
path: '.',
|
|
10505
|
+
|
|
9495
10506
|
// Typed out rather than omitted: an API that ships open because a field was
|
|
9496
10507
|
// left off is the one default worth refusing to have.
|
|
9497
10508
|
defaultAuthorizer: 'none',
|
|
@@ -9501,7 +10512,7 @@ export const api = new RestApi('Api', {
|
|
|
9501
10512
|
logger,
|
|
9502
10513
|
});
|
|
9503
10514
|
`
|
|
9504
|
-
},
|
|
10515
|
+
}] : [],
|
|
9505
10516
|
{
|
|
9506
10517
|
path: "src/config/env.ts",
|
|
9507
10518
|
content: `import { Credentials } from '@geekmidas/envkit/credentials';
|
|
@@ -9514,7 +10525,7 @@ export const envParser = new EnvironmentParser({ ...process.env, ...Credentials
|
|
|
9514
10525
|
export const config = envParser
|
|
9515
10526
|
.create((get) => ({
|
|
9516
10527
|
nodeEnv: get('NODE_ENV').enum(['development', 'test', 'production']).default('development'),
|
|
9517
|
-
stage:
|
|
10528
|
+
stage: ${stageEnv(options.stages)},
|
|
9518
10529
|
}))
|
|
9519
10530
|
.parse();
|
|
9520
10531
|
`
|
|
@@ -9666,7 +10677,7 @@ export const authService = {
|
|
|
9666
10677
|
headers: { cookie },
|
|
9667
10678
|
});
|
|
9668
10679
|
if (!res.ok) return null;
|
|
9669
|
-
return res.json();
|
|
10680
|
+
return (await res.json()) as Session | null;
|
|
9670
10681
|
},
|
|
9671
10682
|
};
|
|
9672
10683
|
},
|
|
@@ -9676,10 +10687,9 @@ export const authService = {
|
|
|
9676
10687
|
files$1.push({
|
|
9677
10688
|
path: "src/router.ts",
|
|
9678
10689
|
content: `import { UnauthorizedError } from '@geekmidas/errors';
|
|
9679
|
-
import { api } from '
|
|
9680
|
-
import { database } from '
|
|
10690
|
+
import { api } from '${constructsImport("api")}';${options.database ? `
|
|
10691
|
+
import { database } from '${constructsImport("database")}';` : ""}
|
|
9681
10692
|
import { authService, type Session } from './services/auth.ts';
|
|
9682
|
-
import { logger } from './config/logger.ts';
|
|
9683
10693
|
|
|
9684
10694
|
/**
|
|
9685
10695
|
* The shared endpoint factory — no session required.
|
|
@@ -9724,8 +10734,8 @@ export const profileEndpoint = sessionRouter
|
|
|
9724
10734
|
}
|
|
9725
10735
|
if (!monorepo) files$1.push({
|
|
9726
10736
|
path: "src/router.ts",
|
|
9727
|
-
content: `import { api } from '
|
|
9728
|
-
import { database } from '
|
|
10737
|
+
content: `import { api } from '${constructsImport("api")}';${options.database ? `
|
|
10738
|
+
import { database } from '${constructsImport("database")}';` : ""}
|
|
9729
10739
|
|
|
9730
10740
|
/**
|
|
9731
10741
|
* The shared endpoint factory.
|
|
@@ -9737,7 +10747,8 @@ import { database } from './constructs/database.ts';` : ""}
|
|
|
9737
10747
|
export const router = api.endpoints${options.database ? ".database(database)" : ""};
|
|
9738
10748
|
`
|
|
9739
10749
|
});
|
|
9740
|
-
if (options.database && declares) files$1.push(...databaseFiles(
|
|
10750
|
+
if (options.database && declares) files$1.push(...databaseFiles());
|
|
10751
|
+
else if (options.database) files$1.push(usersMigration());
|
|
9741
10752
|
if (services.storage && declares) files$1.push({
|
|
9742
10753
|
path: "src/constructs/storage.ts",
|
|
9743
10754
|
content: `import { ObjectStorage } from '@geekmidas/constructs/object-storage';
|
|
@@ -9887,9 +10898,9 @@ const minimalTemplate = {
|
|
|
9887
10898
|
"@geekmidas/rate-limit": GEEKMIDAS_VERSIONS["@geekmidas/rate-limit"],
|
|
9888
10899
|
"@geekmidas/schema": GEEKMIDAS_VERSIONS["@geekmidas/schema"],
|
|
9889
10900
|
"@hono/node-server": "~1.14.1",
|
|
9890
|
-
hono:
|
|
9891
|
-
pino:
|
|
9892
|
-
zod:
|
|
10901
|
+
hono: DEPENDENCY_VERSIONS.hono,
|
|
10902
|
+
pino: DEPENDENCY_VERSIONS.pino,
|
|
10903
|
+
zod: DEPENDENCY_VERSIONS.zod
|
|
9893
10904
|
},
|
|
9894
10905
|
devDependencies: {
|
|
9895
10906
|
"@biomejs/biome": "~2.3.0",
|
|
@@ -9913,7 +10924,7 @@ const minimalTemplate = {
|
|
|
9913
10924
|
},
|
|
9914
10925
|
files: (options) => {
|
|
9915
10926
|
const { loggerType, routesStructure, name: name$1 } = options;
|
|
9916
|
-
const db = databaseFor(
|
|
10927
|
+
const db = databaseFor();
|
|
9917
10928
|
const loggerContent = `import { createLogger } from '@geekmidas/logger/${loggerType}';
|
|
9918
10929
|
|
|
9919
10930
|
export const logger = createLogger();
|
|
@@ -9933,6 +10944,9 @@ export const logger = createLogger();
|
|
|
9933
10944
|
import { logger } from '../config/logger.ts';
|
|
9934
10945
|
|
|
9935
10946
|
export const api = new RestApi('Api', {
|
|
10947
|
+
// The project is the app.
|
|
10948
|
+
path: '.',
|
|
10949
|
+
|
|
9936
10950
|
// Typed out rather than omitted: an API that ships open because a field was
|
|
9937
10951
|
// left off is the one default worth refusing to have.
|
|
9938
10952
|
defaultAuthorizer: 'none',
|
|
@@ -9955,7 +10969,7 @@ export const envParser = new EnvironmentParser({ ...process.env, ...Credentials
|
|
|
9955
10969
|
export const config = envParser
|
|
9956
10970
|
.create((get) => ({
|
|
9957
10971
|
nodeEnv: get('NODE_ENV').enum(['development', 'test', 'production']).default('development'),
|
|
9958
|
-
stage:
|
|
10972
|
+
stage: ${stageEnv(options.stages)},
|
|
9959
10973
|
}))
|
|
9960
10974
|
.parse();
|
|
9961
10975
|
`
|
|
@@ -9982,7 +10996,7 @@ export const healthEndpoint = api
|
|
|
9982
10996
|
`
|
|
9983
10997
|
}
|
|
9984
10998
|
];
|
|
9985
|
-
if (options.database) files$1.push(...databaseFiles(
|
|
10999
|
+
if (options.database) files$1.push(...databaseFiles());
|
|
9986
11000
|
if (options.telescope) files$1.push({
|
|
9987
11001
|
path: "src/config/telescope.ts",
|
|
9988
11002
|
content: `import { Telescope } from '@geekmidas/telescope';
|
|
@@ -10046,8 +11060,8 @@ const serverlessTemplate = {
|
|
|
10046
11060
|
"@geekmidas/rate-limit": GEEKMIDAS_VERSIONS["@geekmidas/rate-limit"],
|
|
10047
11061
|
"@geekmidas/schema": GEEKMIDAS_VERSIONS["@geekmidas/schema"],
|
|
10048
11062
|
"@hono/node-server": "~1.14.1",
|
|
10049
|
-
hono:
|
|
10050
|
-
pino:
|
|
11063
|
+
hono: DEPENDENCY_VERSIONS.hono,
|
|
11064
|
+
pino: DEPENDENCY_VERSIONS.pino
|
|
10051
11065
|
},
|
|
10052
11066
|
devDependencies: {
|
|
10053
11067
|
"@biomejs/biome": "~2.3.0",
|
|
@@ -10090,6 +11104,9 @@ export const logger = createLogger();
|
|
|
10090
11104
|
import { logger } from '../config/logger.ts';
|
|
10091
11105
|
|
|
10092
11106
|
export const api = new RestApi('Api', {
|
|
11107
|
+
// The project is the app.
|
|
11108
|
+
path: '.',
|
|
11109
|
+
|
|
10093
11110
|
// Typed out rather than omitted: an API that ships open because a field was
|
|
10094
11111
|
// left off is the one default worth refusing to have.
|
|
10095
11112
|
defaultAuthorizer: 'none',
|
|
@@ -10112,7 +11129,7 @@ export const envParser = new EnvironmentParser({ ...process.env, ...Credentials
|
|
|
10112
11129
|
export const config = envParser
|
|
10113
11130
|
.create((get) => ({
|
|
10114
11131
|
nodeEnv: get('NODE_ENV').enum(['development', 'test', 'production']).default('development'),
|
|
10115
|
-
stage:
|
|
11132
|
+
stage: ${stageEnv(options.stages)},
|
|
10116
11133
|
}))
|
|
10117
11134
|
.parse();
|
|
10118
11135
|
`
|
|
@@ -10142,19 +11159,27 @@ export const healthEndpoint = api
|
|
|
10142
11159
|
},
|
|
10143
11160
|
{
|
|
10144
11161
|
path: "src/functions/hello.ts",
|
|
10145
|
-
content: `import {
|
|
10146
|
-
import {
|
|
11162
|
+
content: `import { z } from 'zod';
|
|
11163
|
+
import { worker } from '~/constructs/worker.ts';
|
|
10147
11164
|
|
|
10148
|
-
export const helloFunction =
|
|
11165
|
+
export const helloFunction = worker
|
|
10149
11166
|
.input(z.object({ name: z.string() }))
|
|
10150
11167
|
.output(z.object({ message: z.string() }))
|
|
10151
11168
|
.handle(async ({ input }) => ({
|
|
10152
11169
|
message: \`Hello, \${input.name}!\`,
|
|
10153
11170
|
}));
|
|
11171
|
+
`
|
|
11172
|
+
},
|
|
11173
|
+
{
|
|
11174
|
+
path: "src/constructs/worker.ts",
|
|
11175
|
+
content: `import { Worker } from '@geekmidas/constructs/worker';
|
|
11176
|
+
import { logger } from '../config/logger.ts';
|
|
11177
|
+
|
|
11178
|
+
export const worker = new Worker('Jobs', { logger });
|
|
10154
11179
|
`
|
|
10155
11180
|
}
|
|
10156
11181
|
];
|
|
10157
|
-
if (options.database) files$1.push(...databaseFiles(
|
|
11182
|
+
if (options.database) files$1.push(...databaseFiles());
|
|
10158
11183
|
if (options.telescope) files$1.push({
|
|
10159
11184
|
path: "src/config/telescope.ts",
|
|
10160
11185
|
content: `import { Telescope } from '@geekmidas/telescope';
|
|
@@ -10163,7 +11188,7 @@ import { InMemoryStorage } from '@geekmidas/telescope/storage/memory';
|
|
|
10163
11188
|
// Note: For production Lambda, consider using a persistent storage
|
|
10164
11189
|
export const telescope = new Telescope({
|
|
10165
11190
|
storage: new InMemoryStorage({ maxEntries: 50 }),
|
|
10166
|
-
enabled: process.env.STAGE === '
|
|
11191
|
+
enabled: process.env.STAGE === '${options.stages.local}',
|
|
10167
11192
|
});
|
|
10168
11193
|
`
|
|
10169
11194
|
});
|
|
@@ -10173,6 +11198,22 @@ export const telescope = new Telescope({
|
|
|
10173
11198
|
|
|
10174
11199
|
//#endregion
|
|
10175
11200
|
//#region src/init/templates/worker.ts
|
|
11201
|
+
/**
|
|
11202
|
+
* A project that is background work and nothing else.
|
|
11203
|
+
*
|
|
11204
|
+
* It used to scaffold a `RestApi` with no endpoints on it — an HTTP surface
|
|
11205
|
+
* declared by a project whose premise is that nothing calls it over HTTP —
|
|
11206
|
+
* because a surface was the only construct that made an app exist and the only
|
|
11207
|
+
* way to get a logger into the generated runtime. `Worker` is the construct
|
|
11208
|
+
* that was missing: it takes no authorizer, publishes no address, and hands out
|
|
11209
|
+
* factories already carrying its logger.
|
|
11210
|
+
*
|
|
11211
|
+
* It scaffolds a cron when the project has a database, and not otherwise. A
|
|
11212
|
+
* server fires its own crons, and the schedule lives in Postgres so that
|
|
11213
|
+
* running more than one replica still fires each job once — so a worker names
|
|
11214
|
+
* the database it keeps schedules in. Without one there is nowhere to keep
|
|
11215
|
+
* them, and an example that could not run is worse than none.
|
|
11216
|
+
*/
|
|
10176
11217
|
const workerTemplate = {
|
|
10177
11218
|
name: "worker",
|
|
10178
11219
|
description: "Background job processing",
|
|
@@ -10182,11 +11223,8 @@ const workerTemplate = {
|
|
|
10182
11223
|
"@geekmidas/envkit": GEEKMIDAS_VERSIONS["@geekmidas/envkit"],
|
|
10183
11224
|
"@geekmidas/events": GEEKMIDAS_VERSIONS["@geekmidas/events"],
|
|
10184
11225
|
"@geekmidas/logger": GEEKMIDAS_VERSIONS["@geekmidas/logger"],
|
|
10185
|
-
"@geekmidas/rate-limit": GEEKMIDAS_VERSIONS["@geekmidas/rate-limit"],
|
|
10186
11226
|
"@geekmidas/schema": GEEKMIDAS_VERSIONS["@geekmidas/schema"],
|
|
10187
|
-
|
|
10188
|
-
hono: "~4.8.2",
|
|
10189
|
-
pino: "~9.6.0"
|
|
11227
|
+
pino: DEPENDENCY_VERSIONS.pino
|
|
10190
11228
|
},
|
|
10191
11229
|
devDependencies: {
|
|
10192
11230
|
"@biomejs/biome": "~2.3.0",
|
|
@@ -10208,34 +11246,18 @@ const workerTemplate = {
|
|
|
10208
11246
|
"fmt:check": "biome format ."
|
|
10209
11247
|
},
|
|
10210
11248
|
files: (options) => {
|
|
10211
|
-
const { loggerType,
|
|
11249
|
+
const { loggerType, name: name$1, database } = options;
|
|
10212
11250
|
const loggerContent = `import { createLogger } from '@geekmidas/logger/${loggerType}';
|
|
10213
11251
|
|
|
10214
11252
|
export const logger = createLogger();
|
|
10215
11253
|
`;
|
|
10216
|
-
const getRoutePath = (file) => {
|
|
10217
|
-
switch (routesStructure) {
|
|
10218
|
-
case "centralized-endpoints": return `src/endpoints/${file}`;
|
|
10219
|
-
case "centralized-routes": return `src/routes/${file}`;
|
|
10220
|
-
case "domain-based": return `src/${file.replace(".ts", "")}/routes/index.ts`;
|
|
10221
|
-
}
|
|
10222
|
-
};
|
|
10223
|
-
const apiImport = routesStructure === "domain-based" ? "../../constructs/api.js" : "../constructs/api.js";
|
|
10224
11254
|
const files$1 = [
|
|
10225
11255
|
{
|
|
10226
|
-
path: "src/constructs/
|
|
10227
|
-
content: `import {
|
|
10228
|
-
import { logger } from '../config/logger.ts';
|
|
10229
|
-
|
|
10230
|
-
export const api = new RestApi('Api', {
|
|
10231
|
-
// Typed out rather than omitted: an API that ships open because a field was
|
|
10232
|
-
// left off is the one default worth refusing to have.
|
|
10233
|
-
defaultAuthorizer: 'none',
|
|
11256
|
+
path: "src/constructs/worker.ts",
|
|
11257
|
+
content: `import { Worker } from '@geekmidas/constructs/worker';
|
|
11258
|
+
import { logger } from '../config/logger.ts';${database ? "\nimport { database } from './database.ts';" : ""}
|
|
10234
11259
|
|
|
10235
|
-
|
|
10236
|
-
// surface runs with it, and so does the generated entry.
|
|
10237
|
-
logger,
|
|
10238
|
-
});
|
|
11260
|
+
export const worker = new Worker('Jobs', { logger })${database ? ".database(database)" : ""};
|
|
10239
11261
|
`
|
|
10240
11262
|
},
|
|
10241
11263
|
{
|
|
@@ -10250,7 +11272,7 @@ export const envParser = new EnvironmentParser({ ...process.env, ...Credentials
|
|
|
10250
11272
|
export const config = envParser
|
|
10251
11273
|
.create((get) => ({
|
|
10252
11274
|
nodeEnv: get('NODE_ENV').enum(['development', 'test', 'production']).default('development'),
|
|
10253
|
-
stage:
|
|
11275
|
+
stage: ${stageEnv(options.stages)},
|
|
10254
11276
|
}))
|
|
10255
11277
|
.parse();
|
|
10256
11278
|
`
|
|
@@ -10259,23 +11281,6 @@ export const config = envParser
|
|
|
10259
11281
|
path: "src/config/logger.ts",
|
|
10260
11282
|
content: loggerContent
|
|
10261
11283
|
},
|
|
10262
|
-
{
|
|
10263
|
-
path: getRoutePath("health.ts"),
|
|
10264
|
-
content: `import { z } from 'zod';
|
|
10265
|
-
import { api } from '${apiImport}';
|
|
10266
|
-
|
|
10267
|
-
export const healthEndpoint = api
|
|
10268
|
-
.get('/health')
|
|
10269
|
-
.output(z.object({
|
|
10270
|
-
status: z.string(),
|
|
10271
|
-
timestamp: z.string(),
|
|
10272
|
-
}))
|
|
10273
|
-
.handle(async () => ({
|
|
10274
|
-
status: 'ok',
|
|
10275
|
-
timestamp: new Date().toISOString(),
|
|
10276
|
-
}));
|
|
10277
|
-
`
|
|
10278
|
-
},
|
|
10279
11284
|
{
|
|
10280
11285
|
path: "src/events/types.ts",
|
|
10281
11286
|
content: `import type { PublishableMessage } from '@geekmidas/events';
|
|
@@ -10317,46 +11322,47 @@ export const eventsPublisherService = {
|
|
|
10317
11322
|
},
|
|
10318
11323
|
{
|
|
10319
11324
|
path: "src/subscribers/user-events.ts",
|
|
10320
|
-
content: `import {
|
|
11325
|
+
content: `import { worker } from '~/constructs/worker.ts';
|
|
10321
11326
|
import { eventsPublisherService } from '~/events/publisher.ts';
|
|
10322
11327
|
|
|
10323
|
-
export const userEventsSubscriber =
|
|
11328
|
+
export const userEventsSubscriber = worker
|
|
10324
11329
|
.publisher(eventsPublisherService)
|
|
10325
11330
|
.subscribe(['user.created', 'user.updated'])
|
|
10326
|
-
.handle(async ({
|
|
10327
|
-
|
|
10328
|
-
|
|
10329
|
-
|
|
10330
|
-
|
|
10331
|
-
|
|
10332
|
-
|
|
10333
|
-
|
|
10334
|
-
|
|
10335
|
-
|
|
10336
|
-
|
|
10337
|
-
|
|
11331
|
+
.handle(async ({ events, logger }) => {
|
|
11332
|
+
// A batch, not a single event. Both transports deliver in batches, so
|
|
11333
|
+
// handling them one at a time is a round trip per event for no reason.
|
|
11334
|
+
logger.info({ count: events.length }, 'Processing user events');
|
|
11335
|
+
|
|
11336
|
+
for (const event of events) {
|
|
11337
|
+
switch (event.type) {
|
|
11338
|
+
case 'user.created':
|
|
11339
|
+
logger.info({ userId: event.payload.userId }, 'New user created');
|
|
11340
|
+
break;
|
|
11341
|
+
case 'user.updated':
|
|
11342
|
+
logger.info({ userId: event.payload.userId }, 'User updated');
|
|
11343
|
+
break;
|
|
11344
|
+
}
|
|
10338
11345
|
}
|
|
10339
11346
|
});
|
|
10340
11347
|
`
|
|
10341
|
-
}
|
|
10342
|
-
|
|
10343
|
-
|
|
10344
|
-
|
|
11348
|
+
}
|
|
11349
|
+
];
|
|
11350
|
+
if (database) files$1.push({
|
|
11351
|
+
path: "src/crons/cleanup.ts",
|
|
11352
|
+
content: `import { worker } from '~/constructs/worker.ts';
|
|
10345
11353
|
|
|
10346
|
-
|
|
10347
|
-
|
|
11354
|
+
export const cleanup = worker
|
|
11355
|
+
.cron('rate(1 day)')
|
|
10348
11356
|
.handle(async ({ logger }) => {
|
|
10349
|
-
logger.info('Running cleanup
|
|
11357
|
+
logger.info('Running cleanup');
|
|
10350
11358
|
|
|
10351
|
-
//
|
|
10352
|
-
// e.g., delete old sessions, clean up temp files, etc.
|
|
11359
|
+
// Delete old sessions, clear temp files, whatever falls out of date.
|
|
10353
11360
|
|
|
10354
|
-
logger.info('Cleanup
|
|
11361
|
+
logger.info('Cleanup complete');
|
|
10355
11362
|
});
|
|
10356
11363
|
`
|
|
10357
|
-
|
|
10358
|
-
|
|
10359
|
-
if (options.database) files$1.push(...databaseFiles(name$1));
|
|
11364
|
+
});
|
|
11365
|
+
if (options.database) files$1.push(...databaseFiles());
|
|
10360
11366
|
if (options.telescope) files$1.push({
|
|
10361
11367
|
path: "src/config/telescope.ts",
|
|
10362
11368
|
content: `import { Telescope } from '@geekmidas/telescope';
|
|
@@ -10377,7 +11383,7 @@ export const telescope = new Telescope({
|
|
|
10377
11383
|
/**
|
|
10378
11384
|
* OpenAPI output path (fixed, not configurable)
|
|
10379
11385
|
*/
|
|
10380
|
-
const OPENAPI_OUTPUT_PATH$1 = "./.gkm/openapi.ts";
|
|
11386
|
+
const OPENAPI_OUTPUT_PATH$1 = "./.gkm/openapi/api.ts";
|
|
10381
11387
|
/**
|
|
10382
11388
|
* Endpoints output path (generated by gkm dev/build)
|
|
10383
11389
|
*/
|
|
@@ -10483,15 +11489,23 @@ const packageManagerChoices = [
|
|
|
10483
11489
|
/**
|
|
10484
11490
|
* Deploy target choices for prompts
|
|
10485
11491
|
*/
|
|
10486
|
-
const deployTargetChoices = [
|
|
10487
|
-
|
|
10488
|
-
|
|
10489
|
-
|
|
10490
|
-
|
|
10491
|
-
|
|
10492
|
-
|
|
10493
|
-
|
|
10494
|
-
|
|
11492
|
+
const deployTargetChoices = [
|
|
11493
|
+
{
|
|
11494
|
+
title: "Dokploy",
|
|
11495
|
+
value: "dokploy",
|
|
11496
|
+
description: "Deploy to Dokploy (Docker-based hosting)"
|
|
11497
|
+
},
|
|
11498
|
+
{
|
|
11499
|
+
title: "AWS (SST)",
|
|
11500
|
+
value: "sst",
|
|
11501
|
+
description: "Deploy to your AWS account through SST"
|
|
11502
|
+
},
|
|
11503
|
+
{
|
|
11504
|
+
title: "Configure later",
|
|
11505
|
+
value: "none",
|
|
11506
|
+
description: "Skip deployment setup for now"
|
|
11507
|
+
}
|
|
11508
|
+
];
|
|
10495
11509
|
/**
|
|
10496
11510
|
* Services choices for multi-select prompt
|
|
10497
11511
|
*/
|
|
@@ -10584,12 +11598,12 @@ function generatePackageJson(options, template) {
|
|
|
10584
11598
|
if (declares && services.mail) dependencies$1["@geekmidas/emailkit"] = GEEKMIDAS_VERSIONS["@geekmidas/emailkit"];
|
|
10585
11599
|
if (database) {
|
|
10586
11600
|
dependencies$1["@geekmidas/db"] = GEEKMIDAS_VERSIONS["@geekmidas/db"];
|
|
10587
|
-
dependencies$1.kysely =
|
|
10588
|
-
dependencies$1.pg =
|
|
10589
|
-
devDependencies$1["@types/pg"] = "~8.
|
|
11601
|
+
dependencies$1.kysely = DEPENDENCY_VERSIONS.kysely;
|
|
11602
|
+
dependencies$1.pg = DEPENDENCY_VERSIONS.pg;
|
|
11603
|
+
devDependencies$1["@types/pg"] = "~8.23.1";
|
|
10590
11604
|
devDependencies$1["@geekmidas/testkit"] = GEEKMIDAS_VERSIONS["@geekmidas/testkit"];
|
|
10591
11605
|
devDependencies$1["@faker-js/faker"] = "~9.8.0";
|
|
10592
|
-
devDependencies$1["kysely-ctl"] = "
|
|
11606
|
+
devDependencies$1["kysely-ctl"] = DEPENDENCY_VERSIONS["kysely-ctl"];
|
|
10593
11607
|
devDependencies$1["vite-tsconfig-paths"] = "~5.1.0";
|
|
10594
11608
|
}
|
|
10595
11609
|
if (monorepo) {
|
|
@@ -10609,6 +11623,8 @@ function generatePackageJson(options, template) {
|
|
|
10609
11623
|
const appName = pathParts[pathParts.length - 1] || "api";
|
|
10610
11624
|
packageName = `@${name$1}/${appName}`;
|
|
10611
11625
|
}
|
|
11626
|
+
dependencies$1["@geekmidas/client"] = GEEKMIDAS_VERSIONS["@geekmidas/client"];
|
|
11627
|
+
dependencies$1["@tanstack/react-query"] = DEPENDENCY_VERSIONS["@tanstack/react-query"];
|
|
10612
11628
|
const packageJson = {
|
|
10613
11629
|
name: packageName,
|
|
10614
11630
|
version: "0.0.1",
|
|
@@ -10646,7 +11662,7 @@ function generateSourceFiles(options, template) {
|
|
|
10646
11662
|
*/
|
|
10647
11663
|
function generateTestFiles(options, _template) {
|
|
10648
11664
|
if (!options.database) return [];
|
|
10649
|
-
const db = databaseFor(
|
|
11665
|
+
const db = databaseFor();
|
|
10650
11666
|
const declares = !options.monorepo;
|
|
10651
11667
|
const schema = declares ? "../src/constructs/database.ts" : "~/services/database.ts";
|
|
10652
11668
|
const runtimeUrl = declares ? db.urlKey : "DATABASE_URL";
|
|
@@ -10682,14 +11698,16 @@ import pg from 'pg';
|
|
|
10682
11698
|
import { wrapVitestKyselyTransaction } from '@geekmidas/testkit/kysely';
|
|
10683
11699
|
import type { Database } from '${schema}';
|
|
10684
11700
|
|
|
10685
|
-
const
|
|
11701
|
+
const db = new Kysely<Database>({
|
|
10686
11702
|
dialect: new PostgresDialect({
|
|
10687
11703
|
pool: new pg.Pool({ connectionString: process.env.${runtimeUrl} }),
|
|
10688
11704
|
}),
|
|
10689
11705
|
});
|
|
10690
11706
|
|
|
10691
11707
|
export const it = wrapVitestKyselyTransaction<Database>(itVitest, {
|
|
10692
|
-
|
|
11708
|
+
// A function, or a construct: an instance is taken for a construct and has
|
|
11709
|
+
// no service to register.
|
|
11710
|
+
connection: () => db,
|
|
10693
11711
|
});
|
|
10694
11712
|
`
|
|
10695
11713
|
},
|
|
@@ -10774,10 +11792,10 @@ export const usersBuilder = KyselyFactory.createBuilder<Database, 'users'>(
|
|
|
10774
11792
|
import { it } from './config.ts';
|
|
10775
11793
|
|
|
10776
11794
|
describe('example', () => {
|
|
10777
|
-
it('should have a working test setup', async ({
|
|
10778
|
-
//
|
|
10779
|
-
//
|
|
10780
|
-
expect(
|
|
11795
|
+
it('should have a working test setup', async ({ trx }) => {
|
|
11796
|
+
// trx is a Kysely transaction; everything in it is rolled back after the
|
|
11797
|
+
// test
|
|
11798
|
+
expect(trx).toBeDefined();
|
|
10781
11799
|
});
|
|
10782
11800
|
});
|
|
10783
11801
|
`
|
|
@@ -12028,8 +13046,8 @@ export const Alert: Story = {
|
|
|
12028
13046
|
),
|
|
12029
13047
|
};
|
|
12030
13048
|
`;
|
|
12031
|
-
const componentsUiIndex = `export { Button, type ButtonProps, buttonVariants } from './button.tsx';
|
|
12032
|
-
export { Input } from './input.tsx';
|
|
13049
|
+
const componentsUiIndex = `export { Button, type ButtonProps, buttonVariants } from './button/index.tsx';
|
|
13050
|
+
export { Input } from './input/index.tsx';
|
|
12033
13051
|
export {
|
|
12034
13052
|
Card,
|
|
12035
13053
|
CardHeader,
|
|
@@ -12037,17 +13055,17 @@ export {
|
|
|
12037
13055
|
CardTitle,
|
|
12038
13056
|
CardDescription,
|
|
12039
13057
|
CardContent,
|
|
12040
|
-
} from './card.tsx';
|
|
12041
|
-
export { Label } from './label.tsx';
|
|
12042
|
-
export { Badge, type BadgeProps, badgeVariants } from './badge.tsx';
|
|
12043
|
-
export { Separator } from './separator.tsx';
|
|
12044
|
-
export { Tabs, TabsList, TabsTrigger, TabsContent } from './tabs.tsx';
|
|
13058
|
+
} from './card/index.tsx';
|
|
13059
|
+
export { Label } from './label/index.tsx';
|
|
13060
|
+
export { Badge, type BadgeProps, badgeVariants } from './badge/index.tsx';
|
|
13061
|
+
export { Separator } from './separator/index.tsx';
|
|
13062
|
+
export { Tabs, TabsList, TabsTrigger, TabsContent } from './tabs/index.tsx';
|
|
12045
13063
|
export {
|
|
12046
13064
|
Tooltip,
|
|
12047
13065
|
TooltipTrigger,
|
|
12048
13066
|
TooltipContent,
|
|
12049
13067
|
TooltipProvider,
|
|
12050
|
-
} from './tooltip.tsx';
|
|
13068
|
+
} from './tooltip/index.tsx';
|
|
12051
13069
|
export {
|
|
12052
13070
|
Dialog,
|
|
12053
13071
|
DialogPortal,
|
|
@@ -12059,7 +13077,7 @@ export {
|
|
|
12059
13077
|
DialogFooter,
|
|
12060
13078
|
DialogTitle,
|
|
12061
13079
|
DialogDescription,
|
|
12062
|
-
} from './dialog.tsx';
|
|
13080
|
+
} from './dialog/index.tsx';
|
|
12063
13081
|
`;
|
|
12064
13082
|
const buttonIndexTsx = buttonTsx;
|
|
12065
13083
|
const inputIndexTsx = inputTsx;
|
|
@@ -12227,8 +13245,8 @@ function generateWebAppFiles(options) {
|
|
|
12227
13245
|
[uiPackage]: "workspace:*",
|
|
12228
13246
|
"@geekmidas/client": GEEKMIDAS_VERSIONS["@geekmidas/client"],
|
|
12229
13247
|
"@geekmidas/envkit": GEEKMIDAS_VERSIONS["@geekmidas/envkit"],
|
|
12230
|
-
"@tanstack/react-query": "
|
|
12231
|
-
"better-auth": "
|
|
13248
|
+
"@tanstack/react-query": DEPENDENCY_VERSIONS["@tanstack/react-query"],
|
|
13249
|
+
"better-auth": DEPENDENCY_VERSIONS["better-auth"],
|
|
12232
13250
|
next: "~16.1.0",
|
|
12233
13251
|
react: "~19.2.0",
|
|
12234
13252
|
"react-dom": "~19.2.0"
|
|
@@ -12284,7 +13302,7 @@ export default nextConfig;
|
|
|
12284
13302
|
baseUrl: ".",
|
|
12285
13303
|
paths: {
|
|
12286
13304
|
"~/*": ["./src/*", "../../packages/ui/src/*"],
|
|
12287
|
-
[`${apiPackage}/client`]: ["../../apps/api/.gkm/openapi.ts"],
|
|
13305
|
+
[`${apiPackage}/client`]: ["../../apps/api/.gkm/openapi/api.ts"],
|
|
12288
13306
|
[`${modelsPackage}`]: ["../../packages/models/src"],
|
|
12289
13307
|
[`${modelsPackage}/*`]: ["../../packages/models/src/*"],
|
|
12290
13308
|
[`${uiPackage}`]: ["../../packages/ui/src"],
|
|
@@ -12297,12 +13315,7 @@ export default nextConfig;
|
|
|
12297
13315
|
"**/*.tsx",
|
|
12298
13316
|
".next/types/**/*.ts"
|
|
12299
13317
|
],
|
|
12300
|
-
exclude: ["node_modules"]
|
|
12301
|
-
references: [
|
|
12302
|
-
{ path: "../../apps/api" },
|
|
12303
|
-
{ path: "../../packages/ui" },
|
|
12304
|
-
{ path: "../../packages/models" }
|
|
12305
|
-
]
|
|
13318
|
+
exclude: ["node_modules"]
|
|
12306
13319
|
};
|
|
12307
13320
|
const queryClientTs = `import { QueryClient } from '@tanstack/react-query';
|
|
12308
13321
|
|
|
@@ -12565,10 +13578,10 @@ function generateTanStackWebFiles(options) {
|
|
|
12565
13578
|
[uiPackage]: "workspace:*",
|
|
12566
13579
|
"@geekmidas/client": GEEKMIDAS_VERSIONS["@geekmidas/client"],
|
|
12567
13580
|
"@geekmidas/envkit": GEEKMIDAS_VERSIONS["@geekmidas/envkit"],
|
|
12568
|
-
"@tanstack/react-query": "
|
|
13581
|
+
"@tanstack/react-query": DEPENDENCY_VERSIONS["@tanstack/react-query"],
|
|
12569
13582
|
"@tanstack/react-router": "^1.87.0",
|
|
12570
13583
|
"@tanstack/react-start": "^1.87.0",
|
|
12571
|
-
"better-auth": "
|
|
13584
|
+
"better-auth": DEPENDENCY_VERSIONS["better-auth"],
|
|
12572
13585
|
react: "~19.2.0",
|
|
12573
13586
|
"react-dom": "~19.2.0"
|
|
12574
13587
|
},
|
|
@@ -12636,7 +13649,7 @@ export default defineConfig({
|
|
|
12636
13649
|
[`${modelsPackage}/*`]: ["../../packages/models/src/*"],
|
|
12637
13650
|
[uiPackage]: ["../../packages/ui/src"],
|
|
12638
13651
|
[`${uiPackage}/*`]: ["../../packages/ui/src/*"],
|
|
12639
|
-
[`${apiPackage}/client`]: ["../../apps/api/.gkm/openapi.ts"]
|
|
13652
|
+
[`${apiPackage}/client`]: ["../../apps/api/.gkm/openapi/api.ts"]
|
|
12640
13653
|
}
|
|
12641
13654
|
},
|
|
12642
13655
|
include: [
|
|
@@ -12926,12 +13939,27 @@ function detectPackageManager(cwd = process.cwd()) {
|
|
|
12926
13939
|
if (userAgent.includes("bun")) return "bun";
|
|
12927
13940
|
return "npm";
|
|
12928
13941
|
}
|
|
13942
|
+
/** `gkm init`'s flags, which a name can swallow when a space goes missing. */
|
|
13943
|
+
const INIT_FLAGS = [
|
|
13944
|
+
"yes",
|
|
13945
|
+
"name",
|
|
13946
|
+
"template",
|
|
13947
|
+
"monorepo",
|
|
13948
|
+
"api-path",
|
|
13949
|
+
"pm",
|
|
13950
|
+
"skip-install"
|
|
13951
|
+
];
|
|
12929
13952
|
/**
|
|
12930
13953
|
* Validate project name for npm package naming conventions
|
|
12931
13954
|
*/
|
|
12932
13955
|
function validateProjectName(name$1) {
|
|
12933
13956
|
if (!name$1) return "Project name is required";
|
|
12934
13957
|
if (!/^[a-z0-9-_@/.]+$/i.test(name$1)) return "Project name can only contain letters, numbers, hyphens, underscores, @, /, and .";
|
|
13958
|
+
const flag = name$1.match(/--(\w[\w-]*)$/)?.[1];
|
|
13959
|
+
if (flag && INIT_FLAGS.includes(flag)) {
|
|
13960
|
+
const intended = name$1.slice(0, -(flag.length + 2));
|
|
13961
|
+
return `"${name$1}" ends in the --${flag} flag. Did you mean \`${intended} --${flag}\`?`;
|
|
13962
|
+
}
|
|
12935
13963
|
const reserved = [
|
|
12936
13964
|
"node_modules",
|
|
12937
13965
|
".git",
|
|
@@ -12971,6 +13999,18 @@ function getRunCommand(pkgManager, script) {
|
|
|
12971
13999
|
default: return `npm run ${script}`;
|
|
12972
14000
|
}
|
|
12973
14001
|
}
|
|
14002
|
+
/**
|
|
14003
|
+
* Run a binary the project installed, through its package manager — so it is
|
|
14004
|
+
* the project's own version, not whatever \`npx\` would fetch.
|
|
14005
|
+
*/
|
|
14006
|
+
function getExecCommand(pkgManager, command) {
|
|
14007
|
+
switch (pkgManager) {
|
|
14008
|
+
case "pnpm": return `pnpm exec ${command}`;
|
|
14009
|
+
case "yarn": return `yarn ${command}`;
|
|
14010
|
+
case "bun": return `bunx ${command}`;
|
|
14011
|
+
default: return `npx --no-install ${command}`;
|
|
14012
|
+
}
|
|
14013
|
+
}
|
|
12974
14014
|
const lockfileByPm = {
|
|
12975
14015
|
pnpm: "pnpm-lock.yaml",
|
|
12976
14016
|
yarn: "yarn.lock",
|
|
@@ -13041,12 +14081,31 @@ function findWorkspacePackages(cwd, pm) {
|
|
|
13041
14081
|
|
|
13042
14082
|
//#endregion
|
|
13043
14083
|
//#region src/init/index.ts
|
|
14084
|
+
/** `' staging, prod '` → `['staging', 'prod']`. */
|
|
14085
|
+
const stageList = (value) => value.split(",").map((name$1) => name$1.trim()).filter(Boolean);
|
|
14086
|
+
/** `eu-west-1`, `us-gov-west-1`, `ap-southeast-2`. */
|
|
14087
|
+
const AWS_REGION = /^[a-z]{2}(-gov)?-[a-z]+-\d$/;
|
|
13044
14088
|
/**
|
|
13045
14089
|
* Main init command - scaffolds a new project
|
|
13046
14090
|
*/
|
|
13047
14091
|
async function initCommand(projectName, options = {}) {
|
|
13048
14092
|
const cwd = process.cwd();
|
|
13049
14093
|
const detectedPkgManager = detectPackageManager(cwd);
|
|
14094
|
+
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(", ")}.`);
|
|
14095
|
+
if (options.region && !AWS_REGION.test(options.region)) throw new Error(`"${options.region}" is not an AWS region. Use one like eu-west-1.`);
|
|
14096
|
+
if (options.stages || options.localStage || options.protectedStage) {
|
|
14097
|
+
const deployed = stageList(options.stages ?? "");
|
|
14098
|
+
const problems = [
|
|
14099
|
+
...require_workspace.deployedProblems(deployed),
|
|
14100
|
+
...options.localStage ? require_workspace.stageProblems({
|
|
14101
|
+
local: options.localStage,
|
|
14102
|
+
deployed
|
|
14103
|
+
}) : [],
|
|
14104
|
+
...options.protectedStage && !deployed.includes(options.protectedStage) ? [`--protected-stage "${options.protectedStage}" is not in --stages`] : []
|
|
14105
|
+
];
|
|
14106
|
+
if (problems.length) throw new Error([...new Set(problems)].join("\n"));
|
|
14107
|
+
}
|
|
14108
|
+
let deployedSoFar = [];
|
|
13050
14109
|
prompts.default.override({});
|
|
13051
14110
|
const onCancel = () => {
|
|
13052
14111
|
process.exit(0);
|
|
@@ -13097,12 +14156,49 @@ async function initCommand(projectName, options = {}) {
|
|
|
13097
14156
|
initial: packageManagerChoices.findIndex((c) => c.value === detectedPkgManager)
|
|
13098
14157
|
},
|
|
13099
14158
|
{
|
|
13100
|
-
type: options.yes ? null : "select",
|
|
14159
|
+
type: options.yes || options.deploy ? null : "select",
|
|
13101
14160
|
name: "deployTarget",
|
|
13102
14161
|
message: "Deployment target:",
|
|
13103
14162
|
choices: deployTargetChoices,
|
|
13104
14163
|
initial: 0
|
|
13105
14164
|
},
|
|
14165
|
+
{
|
|
14166
|
+
type: (_prev, values) => !options.yes && !options.region && (options.deploy ?? values.deployTarget) === "sst" ? "text" : null,
|
|
14167
|
+
name: "region",
|
|
14168
|
+
message: "AWS region (e.g. eu-west-1):",
|
|
14169
|
+
validate: (value) => AWS_REGION.test(value.trim()) || "An AWS region, like eu-west-1"
|
|
14170
|
+
},
|
|
14171
|
+
{
|
|
14172
|
+
type: options.yes || options.stages ? null : "text",
|
|
14173
|
+
name: "deployedStages",
|
|
14174
|
+
message: "Deployed stages, comma-separated (e.g. staging, prod):",
|
|
14175
|
+
validate: (value) => {
|
|
14176
|
+
const deployed = stageList(value);
|
|
14177
|
+
if (deployed.length === 0) return "At least one stage";
|
|
14178
|
+
return require_workspace.deployedProblems(deployed)[0] ?? true;
|
|
14179
|
+
}
|
|
14180
|
+
},
|
|
14181
|
+
{
|
|
14182
|
+
type: (_prev, values) => options.yes || options.protectedStage ? null : stageList(options.stages ?? values.deployedStages ?? "").length > 1 ? "select" : null,
|
|
14183
|
+
name: "protectedStage",
|
|
14184
|
+
message: "Which one is production (retained and protected)?",
|
|
14185
|
+
choices: (_prev, values) => stageList(options.stages ?? values.deployedStages ?? "").map((name$2) => ({
|
|
14186
|
+
title: name$2,
|
|
14187
|
+
value: name$2
|
|
14188
|
+
}))
|
|
14189
|
+
},
|
|
14190
|
+
{
|
|
14191
|
+
type: (_prev, values) => {
|
|
14192
|
+
deployedSoFar = stageList(options.stages ?? values.deployedStages ?? "");
|
|
14193
|
+
return options.yes || options.localStage ? null : "text";
|
|
14194
|
+
},
|
|
14195
|
+
name: "localStage",
|
|
14196
|
+
message: "Local stage, what gkm dev runs as (e.g. dev):",
|
|
14197
|
+
validate: (value) => require_workspace.stageProblems({
|
|
14198
|
+
local: value.trim(),
|
|
14199
|
+
deployed: deployedSoFar
|
|
14200
|
+
})[0] ?? true
|
|
14201
|
+
},
|
|
13106
14202
|
{
|
|
13107
14203
|
type: options.yes ? null : "confirm",
|
|
13108
14204
|
name: "telescope",
|
|
@@ -13168,7 +14264,7 @@ async function initCommand(projectName, options = {}) {
|
|
|
13168
14264
|
};
|
|
13169
14265
|
if (services.events === "pgboss") services.db = true;
|
|
13170
14266
|
const pkgManager = options.pm ? options.pm : options.yes ? "pnpm" : answers.packageManager ?? detectedPkgManager;
|
|
13171
|
-
const deployTarget = options.yes ? "
|
|
14267
|
+
const deployTarget = options.deploy ?? (options.yes ? "none" : answers.deployTarget ?? "none");
|
|
13172
14268
|
const database = services.db;
|
|
13173
14269
|
const frontendFramework = isFullstack ? options.yes ? "nextjs" : answers.frontendFramework ?? "nextjs" : void 0;
|
|
13174
14270
|
const templateOptions = {
|
|
@@ -13183,6 +14279,8 @@ async function initCommand(projectName, options = {}) {
|
|
|
13183
14279
|
apiPath: monorepo ? options.apiPath ?? "apps/api" : "",
|
|
13184
14280
|
packageManager: pkgManager,
|
|
13185
14281
|
deployTarget,
|
|
14282
|
+
stages: resolveStages(options, answers),
|
|
14283
|
+
...deployTarget === "sst" ? { region: options.region ?? (options.yes ? "eu-west-1" : answers.region?.trim()) } : {},
|
|
13186
14284
|
services,
|
|
13187
14285
|
frontendFramework
|
|
13188
14286
|
};
|
|
@@ -13211,7 +14309,13 @@ async function initCommand(projectName, options = {}) {
|
|
|
13211
14309
|
...isMonorepo$1 ? [] : generateDockerFiles(templateOptions, baseTemplate, dbApps)
|
|
13212
14310
|
] : [];
|
|
13213
14311
|
const dockerFiles = isMonorepo$1 && baseTemplate ? generateDockerFiles(templateOptions, baseTemplate, dbApps) : [];
|
|
13214
|
-
const rootFiles = baseTemplate ? [
|
|
14312
|
+
const rootFiles = baseTemplate ? [
|
|
14313
|
+
...generateAgentFiles(templateOptions, baseTemplate),
|
|
14314
|
+
...generateGithubFiles(templateOptions),
|
|
14315
|
+
...generateMonorepoFiles(templateOptions, baseTemplate),
|
|
14316
|
+
...generateRootConstructs(templateOptions),
|
|
14317
|
+
...generateModelsPackage(templateOptions)
|
|
14318
|
+
] : [];
|
|
13215
14319
|
const webAppFiles = isFullstack ? frontendFramework === "tanstack-start" ? generateTanStackWebFiles(templateOptions) : frontendFramework === "expo" ? generateExpoAppFiles(templateOptions) : generateWebAppFiles(templateOptions) : [];
|
|
13216
14320
|
const authAppFiles = isFullstack ? generateAuthAppFiles(templateOptions) : [];
|
|
13217
14321
|
const uiPackageFiles = isFullstack ? generateUiPackageFiles(templateOptions) : [];
|
|
@@ -13253,12 +14357,12 @@ async function initCommand(projectName, options = {}) {
|
|
|
13253
14357
|
if (services.mail) secretServices.push("mailpit");
|
|
13254
14358
|
if (services.events === "sns") secretServices.push("localstack");
|
|
13255
14359
|
if (services.events === "rabbitmq") secretServices.push("rabbitmq");
|
|
13256
|
-
const
|
|
14360
|
+
const local = templateOptions.stages.local;
|
|
14361
|
+
const devSecrets = require_generator.createStageSecrets(local, secretServices, {
|
|
13257
14362
|
projectName: name$1,
|
|
13258
14363
|
eventsBackend: services.events
|
|
13259
14364
|
});
|
|
13260
14365
|
const customSecrets = {
|
|
13261
|
-
NODE_ENV: "development",
|
|
13262
14366
|
PORT: "3000",
|
|
13263
14367
|
LOG_LEVEL: "debug",
|
|
13264
14368
|
JWT_SECRET: `dev-${Date.now()}-${Math.random().toString(36).slice(2)}`
|
|
@@ -13278,8 +14382,8 @@ async function initCommand(projectName, options = {}) {
|
|
|
13278
14382
|
}
|
|
13279
14383
|
devSecrets.custom = customSecrets;
|
|
13280
14384
|
await require_storage.writeStageSecrets(devSecrets, targetDir);
|
|
13281
|
-
const keyPath = require_storage.getKeyPath(
|
|
13282
|
-
console.log(` Secrets: .gkm/secrets
|
|
14385
|
+
const keyPath = require_storage.getKeyPath(local, name$1);
|
|
14386
|
+
console.log(` Secrets: .gkm/secrets/${local}.json (encrypted)`);
|
|
13283
14387
|
console.log(` Key: ${keyPath}\n`);
|
|
13284
14388
|
if (!options.skipInstall) {
|
|
13285
14389
|
console.log("\n📦 Installing dependencies...\n");
|
|
@@ -13292,11 +14396,13 @@ async function initCommand(projectName, options = {}) {
|
|
|
13292
14396
|
console.error("Failed to install dependencies");
|
|
13293
14397
|
}
|
|
13294
14398
|
try {
|
|
13295
|
-
(0, node_child_process.execSync)("
|
|
14399
|
+
(0, node_child_process.execSync)(getExecCommand(pkgManager, "biome check --write ."), {
|
|
13296
14400
|
cwd: targetDir,
|
|
13297
14401
|
stdio: "inherit"
|
|
13298
14402
|
});
|
|
13299
|
-
} catch {
|
|
14403
|
+
} catch {
|
|
14404
|
+
console.warn(`\n⚠️ Formatting did not finish cleanly. Run ${getRunCommand(pkgManager, "fmt")} to see why.`);
|
|
14405
|
+
}
|
|
13300
14406
|
}
|
|
13301
14407
|
console.log("\n📦 Initializing git repository...\n");
|
|
13302
14408
|
try {
|
|
@@ -13368,19 +14474,46 @@ function printNextSteps(projectName, options, pkgManager) {
|
|
|
13368
14474
|
console.log(` └── turbo.json # Turbo config`);
|
|
13369
14475
|
console.log("");
|
|
13370
14476
|
}
|
|
14477
|
+
const { local, deployed } = options.stages;
|
|
13371
14478
|
console.log("🔐 Secrets management:");
|
|
13372
|
-
console.log(` gkm secrets:show --stage
|
|
13373
|
-
console.log(` gkm secrets:set KEY VALUE --stage
|
|
13374
|
-
console.log(` gkm secrets:init --stage
|
|
14479
|
+
console.log(` gkm secrets:show --stage ${local} # View secrets`);
|
|
14480
|
+
console.log(` gkm secrets:set KEY VALUE --stage ${local} # Add secret`);
|
|
14481
|
+
for (const stage of deployed) console.log(` gkm secrets:init --stage ${stage} # Create ${stage} secrets`);
|
|
13375
14482
|
console.log("");
|
|
13376
14483
|
if (options.deployTarget === "dokploy") {
|
|
13377
14484
|
console.log("🚀 Deployment:");
|
|
13378
|
-
console.log(` ${getRunCommand(pkgManager,
|
|
14485
|
+
for (const stage of deployed) console.log(` ${getRunCommand(pkgManager, `deploy:${stage}`)}`);
|
|
14486
|
+
console.log("");
|
|
14487
|
+
}
|
|
14488
|
+
if (options.deployTarget === "sst") {
|
|
14489
|
+
console.log("🚀 Deployment (AWS, through SST):");
|
|
14490
|
+
for (const stage of deployed) console.log(` ${getRunCommand(pkgManager, `deploy:${stage}`)} # gkm build --provider aws, then sst deploy`);
|
|
14491
|
+
console.log(` Uses your AWS credentials, in ${options.region}.`);
|
|
14492
|
+
if (options.services.mail) console.log(" Set MAIL_FROM to a sender verified in SES first.");
|
|
13379
14493
|
console.log("");
|
|
13380
14494
|
}
|
|
13381
14495
|
console.log("📚 Documentation: https://geekmidas.github.io/toolbox/");
|
|
13382
14496
|
console.log("");
|
|
13383
14497
|
}
|
|
14498
|
+
/**
|
|
14499
|
+
* The stages the project declares, from the answers or the flags.
|
|
14500
|
+
*
|
|
14501
|
+
* `--yes` with no stage flags has nobody to ask and needs an answer, so it
|
|
14502
|
+
* takes `local`, and one deployed stage, `production`, that is protected.
|
|
14503
|
+
*/
|
|
14504
|
+
function resolveStages(options, answers) {
|
|
14505
|
+
const deployed = stageList(options.stages ?? answers.deployedStages ?? (options.yes ? "production" : ""));
|
|
14506
|
+
const kept = options.protectedStage ?? answers.protectedStage ?? (deployed.length === 1 || options.yes ? deployed[0] : void 0);
|
|
14507
|
+
const local = (options.localStage ?? answers.localStage ?? (options.yes ? "local" : "")).trim();
|
|
14508
|
+
const stages = {
|
|
14509
|
+
local,
|
|
14510
|
+
deployed,
|
|
14511
|
+
...kept ? { protected: [kept] } : {}
|
|
14512
|
+
};
|
|
14513
|
+
const problems = require_workspace.stageProblems(stages);
|
|
14514
|
+
if (problems.length) throw new Error(problems.join("\n"));
|
|
14515
|
+
return stages;
|
|
14516
|
+
}
|
|
13384
14517
|
|
|
13385
14518
|
//#endregion
|
|
13386
14519
|
//#region src/secrets/index.ts
|
|
@@ -13591,19 +14724,20 @@ function maskUrl(url) {
|
|
|
13591
14724
|
* Environment variables are sniffed to inject only what the app needs.
|
|
13592
14725
|
*/
|
|
13593
14726
|
async function testCommand(options = {}) {
|
|
13594
|
-
const stage = options.stage ?? "development";
|
|
13595
14727
|
const cwd = process.cwd();
|
|
14728
|
+
const stage = options.stage ?? await require_config.loadWorkspaceConfig(cwd).then((loaded) => loaded.workspace.stages.local).catch(() => void 0);
|
|
14729
|
+
if (!stage) throw new Error("No stage to test with: add `stages` to gkm.config.ts, or pass --stage.");
|
|
13596
14730
|
console.log(`\n🧪 Running tests with ${stage} environment...\n`);
|
|
13597
14731
|
const autoSetup = options.autoSetup || Boolean(process.env.GKM_AUTO_SETUP);
|
|
13598
14732
|
if (autoSetup) {
|
|
13599
|
-
const { ensureStageSecrets } = await Promise.resolve().then(() => require("./setup-
|
|
14733
|
+
const { ensureStageSecrets } = await Promise.resolve().then(() => require("./setup-BsZvZP6f.cjs"));
|
|
13600
14734
|
const generated = await ensureStageSecrets(stage, cwd);
|
|
13601
14735
|
if (generated) console.log(` 🔐 Generated fresh ${stage} secrets (auto-setup)`);
|
|
13602
14736
|
}
|
|
13603
14737
|
const defaultEnv = require_credentials$1.loadEnvFiles(".env");
|
|
13604
14738
|
if (defaultEnv.loaded.length > 0) console.log(` 📦 Loaded env: ${defaultEnv.loaded.join(", ")}`);
|
|
13605
14739
|
const result = await require_credentials$1.prepareEntryCredentials({
|
|
13606
|
-
|
|
14740
|
+
stage,
|
|
13607
14741
|
startDocker: true,
|
|
13608
14742
|
secretsFileName: "test-secrets.json",
|
|
13609
14743
|
resolveDockerPorts: "full",
|
|
@@ -13684,145 +14818,305 @@ function rewriteDatabaseUrlForTests(env) {
|
|
|
13684
14818
|
return result;
|
|
13685
14819
|
}
|
|
13686
14820
|
|
|
14821
|
+
//#endregion
|
|
14822
|
+
//#region src/upgrade/version.ts
|
|
14823
|
+
/**
|
|
14824
|
+
* Just enough semver for `gkm upgrade`: ordering versions, and reading the
|
|
14825
|
+
* floor out of a simple range. The same ordering the release pipeline uses to
|
|
14826
|
+
* decide what is published (`scripts/release-state.mjs`).
|
|
14827
|
+
*/
|
|
14828
|
+
const VERSION = /(\d+)\.(\d+)\.(\d+)(?:-([0-9A-Za-z.-]+))?/;
|
|
14829
|
+
function parse(version$1) {
|
|
14830
|
+
const match = version$1.match(VERSION);
|
|
14831
|
+
if (!match) return void 0;
|
|
14832
|
+
const [, major, minor, patch, pre] = match;
|
|
14833
|
+
return {
|
|
14834
|
+
core: [
|
|
14835
|
+
Number(major),
|
|
14836
|
+
Number(minor),
|
|
14837
|
+
Number(patch)
|
|
14838
|
+
],
|
|
14839
|
+
ids: pre ? pre.split(".").map((id) => /^\d+$/.test(id) ? Number(id) : id) : []
|
|
14840
|
+
};
|
|
14841
|
+
}
|
|
14842
|
+
/** Negative, zero or positive, as `a` is below, equal to or above `b`. */
|
|
14843
|
+
function compareVersions(a, b) {
|
|
14844
|
+
const x = parse(a);
|
|
14845
|
+
const y = parse(b);
|
|
14846
|
+
if (!x || !y) return 0;
|
|
14847
|
+
for (let i = 0; i < 3; i++) if (x.core[i] !== y.core[i]) return x.core[i] - y.core[i];
|
|
14848
|
+
if (!x.ids.length || !y.ids.length) return y.ids.length - x.ids.length;
|
|
14849
|
+
for (let i = 0; i < Math.max(x.ids.length, y.ids.length); i++) {
|
|
14850
|
+
const p = x.ids[i];
|
|
14851
|
+
const q = y.ids[i];
|
|
14852
|
+
if (p === void 0) return -1;
|
|
14853
|
+
if (q === void 0) return 1;
|
|
14854
|
+
if (p === q) continue;
|
|
14855
|
+
if (typeof p === "number" && typeof q === "number") return p - q;
|
|
14856
|
+
if (typeof p === "number") return -1;
|
|
14857
|
+
if (typeof q === "number") return 1;
|
|
14858
|
+
return p < q ? -1 : 1;
|
|
14859
|
+
}
|
|
14860
|
+
return 0;
|
|
14861
|
+
}
|
|
14862
|
+
/** `alpha` for `10.0.0-alpha.6`; undefined for a release. */
|
|
14863
|
+
function prereleaseTag(version$1) {
|
|
14864
|
+
const id = parse(version$1)?.ids[0];
|
|
14865
|
+
return typeof id === "string" ? id : void 0;
|
|
14866
|
+
}
|
|
14867
|
+
/**
|
|
14868
|
+
* A range that is one operator and one version — `~1.2.3`, `^1.2.3`, `>=1.2.3`,
|
|
14869
|
+
* `1.2.3` — split into the two. Anything else (`>=8 <10`, `a || b`) is not
|
|
14870
|
+
* something to rewrite mechanically.
|
|
14871
|
+
*/
|
|
14872
|
+
function simpleRange(range) {
|
|
14873
|
+
const match = range.trim().match(/^(\^|~|>=|)v?(\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?)$/);
|
|
14874
|
+
return match ? {
|
|
14875
|
+
operator: match[1],
|
|
14876
|
+
version: match[2]
|
|
14877
|
+
} : void 0;
|
|
14878
|
+
}
|
|
14879
|
+
|
|
13687
14880
|
//#endregion
|
|
13688
14881
|
//#region src/upgrade/index.ts
|
|
13689
14882
|
const logger = console;
|
|
14883
|
+
const SCOPE = "@geekmidas/";
|
|
14884
|
+
const DEP_FIELDS = [
|
|
14885
|
+
"dependencies",
|
|
14886
|
+
"devDependencies",
|
|
14887
|
+
"peerDependencies",
|
|
14888
|
+
"optionalDependencies"
|
|
14889
|
+
];
|
|
14890
|
+
async function packument(name$1) {
|
|
14891
|
+
const res = await fetch(`https://registry.npmjs.org/${name$1.replace("/", "%2F")}`, { headers: { accept: "application/vnd.npm.install-v1+json" } });
|
|
14892
|
+
if (!res.ok) throw new RegistryUnavailable(name$1, res.status);
|
|
14893
|
+
return await res.json();
|
|
14894
|
+
}
|
|
14895
|
+
/** Every registry range in the workspace's package.json files. */
|
|
14896
|
+
function packageOccurrences(paths) {
|
|
14897
|
+
const found = [];
|
|
14898
|
+
for (const file of paths) {
|
|
14899
|
+
const pkg$1 = JSON.parse((0, node_fs.readFileSync)(file, "utf-8"));
|
|
14900
|
+
for (const field of DEP_FIELDS) for (const [name$1, range] of Object.entries(pkg$1[field] ?? {})) found.push({
|
|
14901
|
+
name: name$1,
|
|
14902
|
+
range,
|
|
14903
|
+
file,
|
|
14904
|
+
field
|
|
14905
|
+
});
|
|
14906
|
+
}
|
|
14907
|
+
return found;
|
|
14908
|
+
}
|
|
14909
|
+
/** pnpm catalog entries, which is where `catalog:` references resolve. */
|
|
14910
|
+
function catalogOccurrences(root) {
|
|
14911
|
+
const file = (0, node_path.join)(root, "pnpm-workspace.yaml");
|
|
14912
|
+
if (!(0, node_fs.existsSync)(file)) return [];
|
|
14913
|
+
const doc = (0, yaml.parseDocument)((0, node_fs.readFileSync)(file, "utf-8")).toJS() ?? {};
|
|
14914
|
+
const found = [];
|
|
14915
|
+
for (const [name$1, range] of Object.entries(doc.catalog ?? {})) found.push({
|
|
14916
|
+
name: name$1,
|
|
14917
|
+
range: String(range),
|
|
14918
|
+
file,
|
|
14919
|
+
field: "catalog"
|
|
14920
|
+
});
|
|
14921
|
+
for (const [catalog, entries] of Object.entries(doc.catalogs ?? {})) for (const [name$1, range] of Object.entries(entries ?? {})) found.push({
|
|
14922
|
+
name: name$1,
|
|
14923
|
+
range: String(range),
|
|
14924
|
+
file,
|
|
14925
|
+
field: "catalog",
|
|
14926
|
+
catalog
|
|
14927
|
+
});
|
|
14928
|
+
return found;
|
|
14929
|
+
}
|
|
14930
|
+
/** Not a registry range: resolved elsewhere, so not ours to move. */
|
|
14931
|
+
const isIndirect = (range) => /^(workspace|catalog|link|file|npm|git|github|https?):/.test(range);
|
|
14932
|
+
/**
|
|
14933
|
+
* The version every `@geekmidas` package moves to.
|
|
14934
|
+
*
|
|
14935
|
+
* All of them share one version, so the CLI's line answers for the rest. The
|
|
14936
|
+
* tag is the one the project is already on — a project on `10.0.0-alpha.6`
|
|
14937
|
+
* follows `alpha` — because `latest` names a different major while a new one
|
|
14938
|
+
* is in prerelease, and following it would move the project backwards.
|
|
14939
|
+
*/
|
|
14940
|
+
function resolveTarget(distTags, installed, tag) {
|
|
14941
|
+
const highest = [...installed].sort(compareVersions).at(-1);
|
|
14942
|
+
const chosen = tag ?? (highest && prereleaseTag(highest)) ?? "latest";
|
|
14943
|
+
const version$1 = distTags[chosen];
|
|
14944
|
+
if (!version$1) throw new NoReleaseOnTag(chosen, Object.keys(distTags));
|
|
14945
|
+
return {
|
|
14946
|
+
tag: chosen,
|
|
14947
|
+
version: version$1
|
|
14948
|
+
};
|
|
14949
|
+
}
|
|
14950
|
+
/** The same operator on the new version — `^`, `~`, `>=` or none, as written. */
|
|
14951
|
+
function withVersion(range, version$1) {
|
|
14952
|
+
return `${simpleRange(range)?.operator ?? ""}${version$1}`;
|
|
14953
|
+
}
|
|
13690
14954
|
async function upgradeCommand(options = {}) {
|
|
13691
14955
|
const cwd = process.cwd();
|
|
13692
|
-
logger.log("\n📦 Scanning workspace for @geekmidas packages...\n");
|
|
13693
14956
|
const pm = detectPackageManager(cwd);
|
|
14957
|
+
const root = findWorkspaceRoot(cwd, pm);
|
|
14958
|
+
logger.log("\n📦 Scanning workspace for @geekmidas packages...\n");
|
|
13694
14959
|
logger.log(` Package manager: ${pm}`);
|
|
13695
|
-
const
|
|
13696
|
-
|
|
13697
|
-
|
|
13698
|
-
|
|
14960
|
+
const packageJsons = findWorkspacePackages(cwd, pm);
|
|
14961
|
+
const occurrences = [...packageOccurrences(packageJsons), ...catalogOccurrences(root)];
|
|
14962
|
+
logger.log(` Found ${packageJsons.length} package(s) in workspace\n`);
|
|
14963
|
+
const ours = occurrences.filter((o) => o.name.startsWith(SCOPE) && !isIndirect(o.range));
|
|
14964
|
+
if (ours.length === 0) {
|
|
13699
14965
|
logger.log(" No @geekmidas packages found.\n");
|
|
13700
14966
|
return;
|
|
13701
14967
|
}
|
|
13702
|
-
const
|
|
13703
|
-
|
|
13704
|
-
const
|
|
13705
|
-
|
|
13706
|
-
|
|
13707
|
-
|
|
13708
|
-
|
|
13709
|
-
|
|
14968
|
+
const installed = ours.map((o) => simpleRange(o.range)?.version).filter((v) => Boolean(v));
|
|
14969
|
+
const cli = await packument(`${SCOPE}cli`);
|
|
14970
|
+
const target = resolveTarget(cli["dist-tags"], installed, options.tag);
|
|
14971
|
+
logger.log(` Target: ${target.version} (npm "${target.tag}")\n`);
|
|
14972
|
+
const highest = installed.sort(compareVersions).at(-1);
|
|
14973
|
+
if (compareVersions(target.version, highest) < 0) throw new WouldDowngrade(highest, target.tag, target.version);
|
|
14974
|
+
const moving = options.all ? ours : ours.filter((o) => o.name === `${SCOPE}cli`);
|
|
14975
|
+
const changes = [];
|
|
14976
|
+
for (const o of moving) {
|
|
14977
|
+
if (!simpleRange(o.range)) {
|
|
14978
|
+
logger.log(` ⚠ ${o.name} ${o.range} is not a simple range; left as is (target ${target.version}).`);
|
|
14979
|
+
continue;
|
|
14980
|
+
}
|
|
14981
|
+
const next = withVersion(o.range, target.version);
|
|
14982
|
+
if (next !== o.range) changes.push({
|
|
14983
|
+
...o,
|
|
14984
|
+
next
|
|
14985
|
+
});
|
|
14986
|
+
}
|
|
14987
|
+
if (options.all) changes.push(...await peerChanges(occurrences, ours, target.version));
|
|
14988
|
+
if (changes.length === 0) {
|
|
14989
|
+
logger.log(" Everything is already on the target version.\n");
|
|
13710
14990
|
return;
|
|
13711
14991
|
}
|
|
13712
|
-
|
|
14992
|
+
printChanges(changes, root);
|
|
14993
|
+
if (!options.all && ours.some((o) => o.name !== `${SCOPE}cli`)) logger.log("\n Only @geekmidas/cli moves without --all. Afterwards, run `gkm upgrade --all` with the new CLI.");
|
|
13713
14994
|
if (options.dryRun) {
|
|
13714
|
-
logger.log(" --dry-run:
|
|
13715
|
-
printUpgradeCommands(upgradable, pm);
|
|
14995
|
+
logger.log("\n --dry-run: no changes made.\n");
|
|
13716
14996
|
return;
|
|
13717
14997
|
}
|
|
13718
|
-
|
|
13719
|
-
|
|
14998
|
+
applyChanges(changes);
|
|
14999
|
+
install(pm, root);
|
|
15000
|
+
logger.log("\n ✅ Upgrade complete. Run your tests to verify.\n");
|
|
13720
15001
|
}
|
|
13721
|
-
|
|
13722
|
-
|
|
13723
|
-
|
|
13724
|
-
|
|
13725
|
-
|
|
13726
|
-
|
|
13727
|
-
|
|
13728
|
-
|
|
13729
|
-
|
|
13730
|
-
|
|
13731
|
-
|
|
13732
|
-
|
|
13733
|
-
|
|
13734
|
-
|
|
13735
|
-
|
|
13736
|
-
|
|
13737
|
-
|
|
13738
|
-
|
|
13739
|
-
|
|
13740
|
-
|
|
13741
|
-
|
|
13742
|
-
|
|
13743
|
-
|
|
15002
|
+
/**
|
|
15003
|
+
* Third-party packages the project already lists, raised to the floor of the
|
|
15004
|
+
* peer range the target `@geekmidas` versions declare.
|
|
15005
|
+
*
|
|
15006
|
+
* Only raised, never lowered, and only where both the peer range and the
|
|
15007
|
+
* project's range are one operator and one version; anything else is reported
|
|
15008
|
+
* for a person to decide.
|
|
15009
|
+
*/
|
|
15010
|
+
async function peerChanges(occurrences, ours, version$1) {
|
|
15011
|
+
const floors = /* @__PURE__ */ new Map();
|
|
15012
|
+
const names = [...new Set(ours.map((o) => o.name))];
|
|
15013
|
+
const docs = await Promise.all(names.map(packument));
|
|
15014
|
+
for (const doc of docs) {
|
|
15015
|
+
const peers = doc.versions[version$1]?.peerDependencies ?? {};
|
|
15016
|
+
for (const [peer, range] of Object.entries(peers)) {
|
|
15017
|
+
if (peer.startsWith(SCOPE)) continue;
|
|
15018
|
+
const floor = simpleRange(range)?.version;
|
|
15019
|
+
if (!floor) continue;
|
|
15020
|
+
const known = floors.get(peer);
|
|
15021
|
+
if (!known || compareVersions(floor, known) > 0) floors.set(peer, floor);
|
|
15022
|
+
}
|
|
15023
|
+
}
|
|
15024
|
+
const changes = [];
|
|
15025
|
+
for (const o of occurrences) {
|
|
15026
|
+
const floor = floors.get(o.name);
|
|
15027
|
+
if (!floor || isIndirect(o.range) || o.field === "peerDependencies") continue;
|
|
15028
|
+
const current = simpleRange(o.range);
|
|
15029
|
+
if (!current) {
|
|
15030
|
+
logger.log(` ⚠ ${o.name} ${o.range} is not a simple range; @geekmidas needs at least ${floor}.`);
|
|
15031
|
+
continue;
|
|
13744
15032
|
}
|
|
15033
|
+
if (compareVersions(current.version, floor) < 0) changes.push({
|
|
15034
|
+
...o,
|
|
15035
|
+
next: withVersion(o.range, floor)
|
|
15036
|
+
});
|
|
13745
15037
|
}
|
|
13746
|
-
return
|
|
13747
|
-
}
|
|
13748
|
-
|
|
13749
|
-
|
|
13750
|
-
const
|
|
13751
|
-
const
|
|
13752
|
-
|
|
13753
|
-
|
|
13754
|
-
|
|
13755
|
-
|
|
13756
|
-
|
|
13757
|
-
|
|
13758
|
-
|
|
13759
|
-
|
|
13760
|
-
|
|
13761
|
-
|
|
13762
|
-
|
|
13763
|
-
|
|
13764
|
-
|
|
13765
|
-
|
|
13766
|
-
|
|
13767
|
-
|
|
13768
|
-
|
|
13769
|
-
|
|
13770
|
-
|
|
13771
|
-
|
|
13772
|
-
|
|
13773
|
-
|
|
13774
|
-
|
|
13775
|
-
|
|
13776
|
-
|
|
13777
|
-
const nameWidth = 33;
|
|
13778
|
-
const verWidth = 14;
|
|
13779
|
-
const statusWidth = 14;
|
|
13780
|
-
const hr = ` ${"─".repeat(nameWidth + verWidth * 2 + statusWidth + 5)}`;
|
|
13781
|
-
logger.log(hr);
|
|
13782
|
-
logger.log(` ${"Package".padEnd(nameWidth)} ${"Current".padEnd(verWidth)} ${"Latest".padEnd(verWidth)} ${"Status".padEnd(statusWidth)}`);
|
|
13783
|
-
logger.log(hr);
|
|
13784
|
-
for (const [, info] of byPackage) {
|
|
13785
|
-
const name$1 = info.packageName.padEnd(nameWidth);
|
|
13786
|
-
const current = info.currentVersion.padEnd(verWidth);
|
|
13787
|
-
const latest = info.latestVersion.padEnd(verWidth);
|
|
13788
|
-
let status;
|
|
13789
|
-
if (info.isWorkspaceRef) status = "workspace";
|
|
13790
|
-
else if (info.needsUpgrade) status = "⬆ upgrade";
|
|
13791
|
-
else status = "✓ up-to-date";
|
|
13792
|
-
logger.log(` ${name$1} ${current} ${latest} ${status}`);
|
|
13793
|
-
}
|
|
13794
|
-
logger.log(hr);
|
|
13795
|
-
}
|
|
13796
|
-
function printUpgradeCommands(upgradable, pm) {
|
|
13797
|
-
logger.log(" Commands that would be run:\n");
|
|
13798
|
-
const uniquePackages = [...new Set(upgradable.map((i) => i.packageName))];
|
|
13799
|
-
const cmd = getWorkspaceUpgradeCommand(pm, uniquePackages);
|
|
13800
|
-
logger.log(` ${cmd}\n`);
|
|
13801
|
-
}
|
|
13802
|
-
function getWorkspaceUpgradeCommand(pm, packages) {
|
|
13803
|
-
const pkgList = packages.join(" ");
|
|
13804
|
-
switch (pm) {
|
|
13805
|
-
case "pnpm": return `pnpm update -r ${pkgList} --latest`;
|
|
13806
|
-
case "yarn": return `yarn upgrade ${pkgList}`;
|
|
13807
|
-
case "bun": return `bun update ${pkgList}`;
|
|
13808
|
-
case "npm": return `npm update ${pkgList} --workspaces`;
|
|
13809
|
-
default: return `npm update ${pkgList}`;
|
|
15038
|
+
return changes;
|
|
15039
|
+
}
|
|
15040
|
+
function printChanges(changes, root) {
|
|
15041
|
+
logger.log(" Changes:\n");
|
|
15042
|
+
for (const c of changes) {
|
|
15043
|
+
const where = c.field === "catalog" ? `pnpm-workspace.yaml (${c.catalog ? `catalogs.${c.catalog}` : "catalog"})` : `${(0, node_path.relative)(root, c.file) || "package.json"} (${c.field})`;
|
|
15044
|
+
logger.log(` ${c.name.padEnd(34)} ${c.range} → ${c.next} ${where}`);
|
|
15045
|
+
}
|
|
15046
|
+
}
|
|
15047
|
+
function applyChanges(changes) {
|
|
15048
|
+
const byFile = /* @__PURE__ */ new Map();
|
|
15049
|
+
for (const c of changes) byFile.set(c.file, [...byFile.get(c.file) ?? [], c]);
|
|
15050
|
+
for (const [file, fileChanges] of byFile) {
|
|
15051
|
+
const text = (0, node_fs.readFileSync)(file, "utf-8");
|
|
15052
|
+
if (file.endsWith(".yaml")) {
|
|
15053
|
+
const doc = (0, yaml.parseDocument)(text);
|
|
15054
|
+
for (const c of fileChanges) {
|
|
15055
|
+
const path = c.catalog ? [
|
|
15056
|
+
"catalogs",
|
|
15057
|
+
c.catalog,
|
|
15058
|
+
c.name
|
|
15059
|
+
] : ["catalog", c.name];
|
|
15060
|
+
doc.setIn(path, c.next);
|
|
15061
|
+
}
|
|
15062
|
+
(0, node_fs.writeFileSync)(file, doc.toString());
|
|
15063
|
+
continue;
|
|
15064
|
+
}
|
|
15065
|
+
const pkg$1 = JSON.parse(text);
|
|
15066
|
+
for (const c of fileChanges) pkg$1[c.field][c.name] = c.next;
|
|
15067
|
+
const indent = text.match(/^[ \t]+(?=")/m)?.[0] ?? " ";
|
|
15068
|
+
(0, node_fs.writeFileSync)(file, `${JSON.stringify(pkg$1, null, indent)}\n`);
|
|
13810
15069
|
}
|
|
13811
15070
|
}
|
|
13812
|
-
function
|
|
13813
|
-
const
|
|
13814
|
-
|
|
13815
|
-
logger.log(` Running: ${cmd}\n`);
|
|
15071
|
+
function install(pm, cwd) {
|
|
15072
|
+
const command = getInstallCommand(pm);
|
|
15073
|
+
logger.log(`\n Running: ${command}\n`);
|
|
13816
15074
|
try {
|
|
13817
|
-
(0, node_child_process.execSync)(
|
|
15075
|
+
(0, node_child_process.execSync)(command, {
|
|
13818
15076
|
cwd,
|
|
13819
15077
|
stdio: "inherit",
|
|
13820
|
-
timeout:
|
|
15078
|
+
timeout: 3e5
|
|
13821
15079
|
});
|
|
13822
15080
|
} catch {
|
|
13823
|
-
throw new
|
|
15081
|
+
throw new InstallFailed(command);
|
|
13824
15082
|
}
|
|
13825
15083
|
}
|
|
15084
|
+
/** npm did not answer for a package, so there is no version to compare. */
|
|
15085
|
+
var RegistryUnavailable = class extends Error {
|
|
15086
|
+
constructor(packageName, status) {
|
|
15087
|
+
super(`npm answered ${status} for ${packageName}. Check the name and your network.`);
|
|
15088
|
+
this.packageName = packageName;
|
|
15089
|
+
this.status = status;
|
|
15090
|
+
this.name = "RegistryUnavailable";
|
|
15091
|
+
}
|
|
15092
|
+
};
|
|
15093
|
+
/** A dist-tag npm does not have, e.g. `--tag beta` before any beta. */
|
|
15094
|
+
var NoReleaseOnTag = class extends Error {
|
|
15095
|
+
constructor(tag, tags) {
|
|
15096
|
+
super(`npm has no "${tag}" release of @geekmidas/cli. Tags: ${tags.join(", ")}.`);
|
|
15097
|
+
this.tag = tag;
|
|
15098
|
+
this.tags = tags;
|
|
15099
|
+
this.name = "NoReleaseOnTag";
|
|
15100
|
+
}
|
|
15101
|
+
};
|
|
15102
|
+
/** The line asked for is behind what is installed; upgrade never goes back. */
|
|
15103
|
+
var WouldDowngrade = class extends Error {
|
|
15104
|
+
constructor(installed, tag, target) {
|
|
15105
|
+
super(`The project is already on ${installed}, ahead of npm's "${tag}" (${target}). Pass --tag to follow another line.`);
|
|
15106
|
+
this.installed = installed;
|
|
15107
|
+
this.tag = tag;
|
|
15108
|
+
this.target = target;
|
|
15109
|
+
this.name = "WouldDowngrade";
|
|
15110
|
+
}
|
|
15111
|
+
};
|
|
15112
|
+
/** The install after rewriting versions failed; its own output says why. */
|
|
15113
|
+
var InstallFailed = class extends Error {
|
|
15114
|
+
constructor(command) {
|
|
15115
|
+
super(`\`${command}\` failed. Check the output above for details.`);
|
|
15116
|
+
this.command = command;
|
|
15117
|
+
this.name = "InstallFailed";
|
|
15118
|
+
}
|
|
15119
|
+
};
|
|
13826
15120
|
|
|
13827
15121
|
//#endregion
|
|
13828
15122
|
//#region src/index.ts
|
|
@@ -13831,7 +15125,7 @@ program.name("gkm").description("GeekMidas backend framework CLI").version(packa
|
|
|
13831
15125
|
const opts = program.opts();
|
|
13832
15126
|
if (opts.debug) enableDebug();
|
|
13833
15127
|
});
|
|
13834
|
-
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) => {
|
|
15128
|
+
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) => {
|
|
13835
15129
|
try {
|
|
13836
15130
|
const globalOptions = program.opts();
|
|
13837
15131
|
if (globalOptions.cwd) process.chdir(globalOptions.cwd);
|
|
@@ -13851,7 +15145,7 @@ program.command("trust").description("Trust the local edge's certificate authori
|
|
|
13851
15145
|
process.exit(1);
|
|
13852
15146
|
}
|
|
13853
15147
|
});
|
|
13854
|
-
program.command("setup").description("Reconcile declared constructs — containers, databases, buckets, secrets").option("--stage <stage>", "Stage name
|
|
15148
|
+
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) => {
|
|
13855
15149
|
try {
|
|
13856
15150
|
const globalOptions = program.opts();
|
|
13857
15151
|
if (globalOptions.cwd) process.chdir(globalOptions.cwd);
|
|
@@ -13923,7 +15217,7 @@ program.command("exec").description("Run a command with secrets injected into Cr
|
|
|
13923
15217
|
process.exit(1);
|
|
13924
15218
|
}
|
|
13925
15219
|
});
|
|
13926
|
-
program.command("test").description("Run tests with secrets loaded from environment").option("--stage <stage>", "Stage to load secrets from
|
|
15220
|
+
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) => {
|
|
13927
15221
|
try {
|
|
13928
15222
|
const globalOptions = program.opts();
|
|
13929
15223
|
if (globalOptions.cwd) process.chdir(globalOptions.cwd);
|
|
@@ -14071,7 +15365,7 @@ program.command("secrets:push").description("Push secrets to remote provider (SS
|
|
|
14071
15365
|
const { workspace } = await loadWorkspaceConfig$1();
|
|
14072
15366
|
const secrets = await readStageSecrets$1(options.stage, workspace.root);
|
|
14073
15367
|
if (secrets) {
|
|
14074
|
-
const { derivedContainers: derivedContainers$1 } = await Promise.resolve().then(() => require("./workspace-
|
|
15368
|
+
const { derivedContainers: derivedContainers$1 } = await Promise.resolve().then(() => require("./workspace-XXtNhqJl.cjs"));
|
|
14075
15369
|
const result = reconcileMissingSecrets(secrets, workspace, await derivedContainers$1(workspace, options.stage));
|
|
14076
15370
|
if (result) {
|
|
14077
15371
|
await writeStageSecrets$1(result.secrets, workspace.root);
|
|
@@ -14100,7 +15394,7 @@ program.command("secrets:pull").description("Pull secrets from remote provider (
|
|
|
14100
15394
|
console.error(`No remote secrets found for stage "${options.stage}".`);
|
|
14101
15395
|
process.exit(1);
|
|
14102
15396
|
}
|
|
14103
|
-
const { derivedContainers: derivedContainers$1 } = await Promise.resolve().then(() => require("./workspace-
|
|
15397
|
+
const { derivedContainers: derivedContainers$1 } = await Promise.resolve().then(() => require("./workspace-XXtNhqJl.cjs"));
|
|
14104
15398
|
const result = reconcileMissingSecrets(secrets, workspace, await derivedContainers$1(workspace, options.stage));
|
|
14105
15399
|
if (result) {
|
|
14106
15400
|
secrets = result.secrets;
|
|
@@ -14114,7 +15408,7 @@ program.command("secrets:pull").description("Pull secrets from remote provider (
|
|
|
14114
15408
|
process.exit(1);
|
|
14115
15409
|
}
|
|
14116
15410
|
});
|
|
14117
|
-
program.command("secrets:reconcile").description("Backfill missing custom secrets from workspace config").option("--stage <stage>", "Stage name
|
|
15411
|
+
program.command("secrets:reconcile").description("Backfill missing custom secrets from workspace config").option("--stage <stage>", "Stage name (default: stages.local)").action(async (options) => {
|
|
14118
15412
|
try {
|
|
14119
15413
|
const globalOptions = program.opts();
|
|
14120
15414
|
if (globalOptions.cwd) process.chdir(globalOptions.cwd);
|
|
@@ -14122,19 +15416,20 @@ program.command("secrets:reconcile").description("Backfill missing custom secret
|
|
|
14122
15416
|
const { reconcileMissingSecrets } = await Promise.resolve().then(() => require("./reconcile-D0pimNiy.cjs"));
|
|
14123
15417
|
const { readStageSecrets: readStageSecrets$1, writeStageSecrets: writeStageSecrets$1 } = await Promise.resolve().then(() => require("./storage-BM9hGpgv.cjs"));
|
|
14124
15418
|
const { workspace } = await loadWorkspaceConfig$1();
|
|
14125
|
-
const
|
|
15419
|
+
const stage = options.stage ?? workspace.stages.local;
|
|
15420
|
+
const secrets = await readStageSecrets$1(stage, workspace.root);
|
|
14126
15421
|
if (!secrets) {
|
|
14127
|
-
console.error(`No secrets found for stage "${
|
|
15422
|
+
console.error(`No secrets found for stage "${stage}". Run "gkm secrets:init --stage ${stage}" first.`);
|
|
14128
15423
|
process.exit(1);
|
|
14129
15424
|
}
|
|
14130
|
-
const { derivedContainers: derivedContainers$1 } = await Promise.resolve().then(() => require("./workspace-
|
|
14131
|
-
const result = reconcileMissingSecrets(secrets, workspace, await derivedContainers$1(workspace,
|
|
15425
|
+
const { derivedContainers: derivedContainers$1 } = await Promise.resolve().then(() => require("./workspace-XXtNhqJl.cjs"));
|
|
15426
|
+
const result = reconcileMissingSecrets(secrets, workspace, await derivedContainers$1(workspace, stage));
|
|
14132
15427
|
if (!result) {
|
|
14133
|
-
console.log(`\n✓ Secrets for stage "${
|
|
15428
|
+
console.log(`\n✓ Secrets for stage "${stage}" are up-to-date`);
|
|
14134
15429
|
return;
|
|
14135
15430
|
}
|
|
14136
15431
|
await writeStageSecrets$1(result.secrets, workspace.root);
|
|
14137
|
-
console.log(`\n✓ Reconciled ${result.addedKeys.length} missing secret(s) for stage "${
|
|
15432
|
+
console.log(`\n✓ Reconciled ${result.addedKeys.length} missing secret(s) for stage "${stage}":`);
|
|
14138
15433
|
for (const key of result.addedKeys) console.log(` + ${key}`);
|
|
14139
15434
|
} catch (error) {
|
|
14140
15435
|
console.error(formatError(error));
|
|
@@ -14288,7 +15583,7 @@ program.command("state:diff").description("Compare local and remote deployment s
|
|
|
14288
15583
|
process.exit(1);
|
|
14289
15584
|
}
|
|
14290
15585
|
});
|
|
14291
|
-
program.command("upgrade").description("Upgrade all @geekmidas
|
|
15586
|
+
program.command("upgrade").description("Upgrade @geekmidas/cli (with --all, every @geekmidas package) along the release line the project is on").option("--dry-run", "Show what would be upgraded without making changes").option("--all", "Every @geekmidas package, and the third-party packages their peers require").option("--tag <tag>", "npm dist-tag to follow (default: the line you are on)").action(async (options) => {
|
|
14292
15587
|
try {
|
|
14293
15588
|
const globalOptions = program.opts();
|
|
14294
15589
|
if (globalOptions.cwd) process.chdir(globalOptions.cwd);
|