@geekmidas/cli 10.0.0-alpha.1 → 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 -8
- 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 +1917 -597
- package/dist/index.cjs.map +1 -1
- package/dist/index.mjs +1919 -599
- 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 = "10.0.0-alpha.
|
|
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
|
|
|
@@ -7342,6 +7509,501 @@ function printStateDetails(state) {
|
|
|
7342
7509
|
}
|
|
7343
7510
|
}
|
|
7344
7511
|
|
|
7512
|
+
//#endregion
|
|
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";
|
|
7516
|
+
/**
|
|
7517
|
+
* Where a workspace keeps its constructs: at the root, beside the apps that
|
|
7518
|
+
* share them, rather than inside any one of them.
|
|
7519
|
+
*/
|
|
7520
|
+
const WORKSPACE_CONSTRUCTS_GLOB = "./constructs/**/*.ts";
|
|
7521
|
+
/**
|
|
7522
|
+
* Where each routes structure puts an app's handlers, relative to the app.
|
|
7523
|
+
*
|
|
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.
|
|
7526
|
+
*/
|
|
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
|
+
}
|
|
7533
|
+
}
|
|
7534
|
+
/**
|
|
7535
|
+
* Plain ids — `Database`, not `Beetlefit`.
|
|
7536
|
+
*
|
|
7537
|
+
* The workspace `name` already scopes every physical name
|
|
7538
|
+
* (`production-beetlefit-database`), so an id that carried the project too
|
|
7539
|
+
* would say it twice: `production-beetlefit-beetlefit-cache`. Within a
|
|
7540
|
+
* workspace the constructs are declared once, at its root, so there is no
|
|
7541
|
+
* second app's `Database` to collide with.
|
|
7542
|
+
*/
|
|
7543
|
+
function scaffolded(id) {
|
|
7544
|
+
return {
|
|
7545
|
+
id,
|
|
7546
|
+
service: (0, __geekmidas_manifest.serviceKey)(id),
|
|
7547
|
+
urlKey: (0, __geekmidas_manifest.provideKey)(id, "url")
|
|
7548
|
+
};
|
|
7549
|
+
}
|
|
7550
|
+
/** The database a project declares. */
|
|
7551
|
+
function databaseFor() {
|
|
7552
|
+
return {
|
|
7553
|
+
...scaffolded("Database"),
|
|
7554
|
+
ownerUrlKey: (0, __geekmidas_manifest.provideKey)("Database", "ownerUrl")
|
|
7555
|
+
};
|
|
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");
|
|
7563
|
+
/**
|
|
7564
|
+
* The files a project's declared database needs.
|
|
7565
|
+
*
|
|
7566
|
+
* Shared by every template, so a worker and an API declare the same database
|
|
7567
|
+
* the same way — and so the schema type, the migration, and the id the test
|
|
7568
|
+
* setup reads all come from one place.
|
|
7569
|
+
*/
|
|
7570
|
+
function databaseFiles() {
|
|
7571
|
+
const db = databaseFor();
|
|
7572
|
+
return [{
|
|
7573
|
+
path: "src/constructs/database.ts",
|
|
7574
|
+
content: `import { KyselyDatabase } from '@geekmidas/constructs/database/kysely';
|
|
7575
|
+
import type { Generated } from 'kysely';
|
|
7576
|
+
|
|
7577
|
+
/** Your database schema. Add tables here. */
|
|
7578
|
+
export interface Database {
|
|
7579
|
+
users: {
|
|
7580
|
+
id: Generated<string>;
|
|
7581
|
+
name: string;
|
|
7582
|
+
email: string;
|
|
7583
|
+
created_at: Generated<Date>;
|
|
7584
|
+
};
|
|
7585
|
+
}
|
|
7586
|
+
|
|
7587
|
+
/**
|
|
7588
|
+
* The app's database, declared once.
|
|
7589
|
+
*
|
|
7590
|
+
* The container, the database inside it, its roles and schema, and
|
|
7591
|
+
* \`${db.urlKey}\` all derive from this line — \`gkm dev\` reconciles them
|
|
7592
|
+
* before the server starts, which is why nothing lists \`postgres\` anywhere.
|
|
7593
|
+
*
|
|
7594
|
+
* Both type arguments or neither: TypeScript has no partial type-argument
|
|
7595
|
+
* inference, so passing only \`Database\` would leave the name at \`string\`
|
|
7596
|
+
* and widen the service key away from \`${db.service}\`.
|
|
7597
|
+
*/
|
|
7598
|
+
export const database = new KyselyDatabase<Database, '${db.id}'>('${db.id}');
|
|
7599
|
+
`
|
|
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 {
|
|
7610
|
+
path: "src/db/migrations/001_create_users.ts",
|
|
7611
|
+
content: `import type { Kysely } from 'kysely';
|
|
7612
|
+
|
|
7613
|
+
export async function up(db: Kysely<unknown>): Promise<void> {
|
|
7614
|
+
await db.schema
|
|
7615
|
+
.createTable('users')
|
|
7616
|
+
.addColumn('id', 'uuid', (col) =>
|
|
7617
|
+
col.primaryKey().defaultTo(db.fn('gen_random_uuid')),
|
|
7618
|
+
)
|
|
7619
|
+
.addColumn('name', 'varchar(255)', (col) => col.notNull())
|
|
7620
|
+
.addColumn('email', 'varchar(255)', (col) => col.notNull().unique())
|
|
7621
|
+
.addColumn('created_at', 'timestamptz', (col) =>
|
|
7622
|
+
col.notNull().defaultTo(db.fn('now')),
|
|
7623
|
+
)
|
|
7624
|
+
.execute();
|
|
7625
|
+
|
|
7626
|
+
await db.schema
|
|
7627
|
+
.createIndex('users_email_idx')
|
|
7628
|
+
.on('users')
|
|
7629
|
+
.column('email')
|
|
7630
|
+
.execute();
|
|
7631
|
+
}
|
|
7632
|
+
|
|
7633
|
+
export async function down(db: Kysely<unknown>): Promise<void> {
|
|
7634
|
+
await db.schema.dropTable('users').execute();
|
|
7635
|
+
}
|
|
7636
|
+
`
|
|
7637
|
+
};
|
|
7638
|
+
}
|
|
7639
|
+
/**
|
|
7640
|
+
* What a workspace's `constructs` glob reaches: the root constructs, and the
|
|
7641
|
+
* handlers of every app laid out the way `init` was told.
|
|
7642
|
+
*
|
|
7643
|
+
* Every app in `appsDir` (`apps/*`) rather than the API's own directory.
|
|
7644
|
+
* Endpoints are split by the surface they were built from, so naming one app
|
|
7645
|
+
* here would only decide which apps' endpoints are silently never loaded.
|
|
7646
|
+
*/
|
|
7647
|
+
function workspaceConstructsGlobs(structure, appsDir) {
|
|
7648
|
+
return [WORKSPACE_CONSTRUCTS_GLOB, `./${appsDir}/*/${routesGlob(structure).replace(/^\.\//, "")}`];
|
|
7649
|
+
}
|
|
7650
|
+
|
|
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
|
+
|
|
7345
8007
|
//#endregion
|
|
7346
8008
|
//#region src/init/versions.ts
|
|
7347
8009
|
const require$1 = (0, node_module.createRequire)(require("url").pathToFileURL(__filename).href);
|
|
@@ -7364,26 +8026,26 @@ const CLI_VERSION = `~${pkg.version}`;
|
|
|
7364
8026
|
* Run: pnpm --filter @geekmidas/cli sync-versions
|
|
7365
8027
|
*/
|
|
7366
8028
|
const GEEKMIDAS_VERSIONS = {
|
|
7367
|
-
"@geekmidas/audit": "~10.0.0-alpha.
|
|
7368
|
-
"@geekmidas/auth": "~10.0.0-alpha.
|
|
7369
|
-
"@geekmidas/cache": "~10.0.0-alpha.
|
|
7370
|
-
"@geekmidas/client": "~10.0.0-alpha.
|
|
7371
|
-
"@geekmidas/cloud": "~10.0.0-alpha.
|
|
7372
|
-
"@geekmidas/constructs": "~10.0.0-alpha.
|
|
7373
|
-
"@geekmidas/db": "~10.0.0-alpha.
|
|
7374
|
-
"@geekmidas/emailkit": "~10.0.0-alpha.
|
|
7375
|
-
"@geekmidas/envkit": "~10.0.0-alpha.
|
|
7376
|
-
"@geekmidas/errors": "~10.0.0-alpha.
|
|
7377
|
-
"@geekmidas/events": "~10.0.0-alpha.
|
|
7378
|
-
"@geekmidas/logger": "~10.0.0-alpha.
|
|
7379
|
-
"@geekmidas/manifest": "~10.0.0-alpha.
|
|
7380
|
-
"@geekmidas/rate-limit": "~10.0.0-alpha.
|
|
7381
|
-
"@geekmidas/schema": "~10.0.0-alpha.
|
|
7382
|
-
"@geekmidas/services": "~10.0.0-alpha.
|
|
7383
|
-
"@geekmidas/storage": "~10.0.0-alpha.
|
|
7384
|
-
"@geekmidas/studio": "~10.0.0-alpha.
|
|
7385
|
-
"@geekmidas/telescope": "~10.0.0-alpha.
|
|
7386
|
-
"@geekmidas/testkit": "~10.0.0-alpha.
|
|
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",
|
|
7387
8049
|
"@geekmidas/cli": CLI_VERSION
|
|
7388
8050
|
};
|
|
7389
8051
|
|
|
@@ -7403,27 +8065,28 @@ function generateAuthAppFiles(options) {
|
|
|
7403
8065
|
private: true,
|
|
7404
8066
|
type: "module",
|
|
7405
8067
|
scripts: {
|
|
7406
|
-
dev: "gkm dev
|
|
7407
|
-
build: "
|
|
7408
|
-
start: "node dist/index.js",
|
|
7409
|
-
typecheck: "tsc --noEmit",
|
|
8068
|
+
dev: "gkm dev",
|
|
8069
|
+
build: "gkm build",
|
|
7410
8070
|
"db:migrate": "gkm exec -- npx @better-auth/cli migrate",
|
|
7411
8071
|
"db:generate": "gkm exec -- npx @better-auth/cli generate"
|
|
7412
8072
|
},
|
|
7413
8073
|
dependencies: {
|
|
7414
8074
|
[modelsPackage]: "workspace:*",
|
|
8075
|
+
"@geekmidas/auth": GEEKMIDAS_VERSIONS["@geekmidas/auth"],
|
|
8076
|
+
"@geekmidas/constructs": GEEKMIDAS_VERSIONS["@geekmidas/constructs"],
|
|
8077
|
+
"@geekmidas/db": GEEKMIDAS_VERSIONS["@geekmidas/db"],
|
|
7415
8078
|
"@geekmidas/envkit": GEEKMIDAS_VERSIONS["@geekmidas/envkit"],
|
|
7416
8079
|
"@geekmidas/logger": GEEKMIDAS_VERSIONS["@geekmidas/logger"],
|
|
7417
|
-
"@hono/node-server": "~1.
|
|
7418
|
-
"better-auth": "
|
|
7419
|
-
hono:
|
|
7420
|
-
kysely:
|
|
7421
|
-
pg:
|
|
8080
|
+
"@hono/node-server": "~1.14.1",
|
|
8081
|
+
"better-auth": DEPENDENCY_VERSIONS["better-auth"],
|
|
8082
|
+
hono: DEPENDENCY_VERSIONS.hono,
|
|
8083
|
+
kysely: DEPENDENCY_VERSIONS.kysely,
|
|
8084
|
+
pg: DEPENDENCY_VERSIONS.pg
|
|
7422
8085
|
},
|
|
7423
8086
|
devDependencies: {
|
|
7424
8087
|
"@geekmidas/cli": GEEKMIDAS_VERSIONS["@geekmidas/cli"],
|
|
7425
8088
|
"@types/node": "~22.0.0",
|
|
7426
|
-
"@types/pg": "~8.
|
|
8089
|
+
"@types/pg": "~8.23.1",
|
|
7427
8090
|
tsx: "~4.20.0",
|
|
7428
8091
|
typescript: "~5.8.2"
|
|
7429
8092
|
}
|
|
@@ -7464,117 +8127,27 @@ dist/
|
|
|
7464
8127
|
}
|
|
7465
8128
|
|
|
7466
8129
|
//#endregion
|
|
7467
|
-
//#region src/init/
|
|
7468
|
-
/** The
|
|
7469
|
-
|
|
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
|
+
}
|
|
7470
8144
|
/**
|
|
7471
|
-
* The
|
|
7472
|
-
*
|
|
7473
|
-
* Named after the project rather than `Db`, so two apps in one workspace can
|
|
7474
|
-
* declare one each without colliding on an id or on an env key.
|
|
8145
|
+
* The `STAGE` the scaffolded env config parses: one of the declared stages,
|
|
8146
|
+
* defaulting to the local one.
|
|
7475
8147
|
*/
|
|
7476
|
-
function
|
|
7477
|
-
const
|
|
7478
|
-
return {
|
|
7479
|
-
id,
|
|
7480
|
-
service: (0, __geekmidas_manifest.serviceKey)(id),
|
|
7481
|
-
urlKey: (0, __geekmidas_manifest.provideKey)(id, "url"),
|
|
7482
|
-
ownerUrlKey: (0, __geekmidas_manifest.provideKey)(id, "ownerUrl")
|
|
7483
|
-
};
|
|
7484
|
-
}
|
|
7485
|
-
/** The bucket a project named `name` declares. */
|
|
7486
|
-
function storageFor(name$1) {
|
|
7487
|
-
const id = (0, __geekmidas_manifest.canonicalId)(`${name$1}-uploads`);
|
|
7488
|
-
return {
|
|
7489
|
-
id,
|
|
7490
|
-
service: (0, __geekmidas_manifest.serviceKey)(id),
|
|
7491
|
-
urlKey: (0, __geekmidas_manifest.provideKey)(id, "url")
|
|
7492
|
-
};
|
|
7493
|
-
}
|
|
7494
|
-
/** The mail sender a project named `name` declares. */
|
|
7495
|
-
function emailFor(name$1) {
|
|
7496
|
-
const id = (0, __geekmidas_manifest.canonicalId)(`${name$1}-mail`);
|
|
7497
|
-
return {
|
|
7498
|
-
id,
|
|
7499
|
-
service: (0, __geekmidas_manifest.serviceKey)(id),
|
|
7500
|
-
urlKey: (0, __geekmidas_manifest.provideKey)(id, "url")
|
|
7501
|
-
};
|
|
7502
|
-
}
|
|
7503
|
-
/** The cache a project named `name` declares. */
|
|
7504
|
-
function cacheFor(name$1) {
|
|
7505
|
-
const id = (0, __geekmidas_manifest.canonicalId)(`${name$1}-cache`);
|
|
7506
|
-
return {
|
|
7507
|
-
id,
|
|
7508
|
-
service: (0, __geekmidas_manifest.serviceKey)(id),
|
|
7509
|
-
urlKey: (0, __geekmidas_manifest.provideKey)(id, "url")
|
|
7510
|
-
};
|
|
7511
|
-
}
|
|
7512
|
-
/**
|
|
7513
|
-
* The files a project's declared database needs.
|
|
7514
|
-
*
|
|
7515
|
-
* Shared by every template, so a worker and an API declare the same database
|
|
7516
|
-
* the same way — and so the schema type, the migration, and the id the test
|
|
7517
|
-
* setup reads all come from one place.
|
|
7518
|
-
*/
|
|
7519
|
-
function databaseFiles(name$1) {
|
|
7520
|
-
const db = databaseFor(name$1);
|
|
7521
|
-
return [{
|
|
7522
|
-
path: "src/constructs/database.ts",
|
|
7523
|
-
content: `import { KyselyDatabase } from '@geekmidas/constructs/database/kysely';
|
|
7524
|
-
import type { Generated } from 'kysely';
|
|
7525
|
-
|
|
7526
|
-
/** Your database schema. Add tables here. */
|
|
7527
|
-
export interface Database {
|
|
7528
|
-
users: {
|
|
7529
|
-
id: Generated<string>;
|
|
7530
|
-
name: string;
|
|
7531
|
-
email: string;
|
|
7532
|
-
created_at: Generated<Date>;
|
|
7533
|
-
};
|
|
7534
|
-
}
|
|
7535
|
-
|
|
7536
|
-
/**
|
|
7537
|
-
* The app's database, declared once.
|
|
7538
|
-
*
|
|
7539
|
-
* The container, the database inside it, its roles and schema, and
|
|
7540
|
-
* \`${db.urlKey}\` all derive from this line — \`gkm dev\` reconciles them
|
|
7541
|
-
* before the server starts, which is why nothing lists \`postgres\` anywhere.
|
|
7542
|
-
*
|
|
7543
|
-
* Both type arguments or neither: TypeScript has no partial type-argument
|
|
7544
|
-
* inference, so passing only \`Database\` would leave the name at \`string\`
|
|
7545
|
-
* and widen the service key away from \`${db.service}\`.
|
|
7546
|
-
*/
|
|
7547
|
-
export const database = new KyselyDatabase<Database, '${db.id}'>('${db.id}');
|
|
7548
|
-
`
|
|
7549
|
-
}, {
|
|
7550
|
-
path: "src/db/migrations/001_create_users.ts",
|
|
7551
|
-
content: `import type { Kysely } from 'kysely';
|
|
7552
|
-
|
|
7553
|
-
export async function up(db: Kysely<unknown>): Promise<void> {
|
|
7554
|
-
await db.schema
|
|
7555
|
-
.createTable('users')
|
|
7556
|
-
.addColumn('id', 'uuid', (col) =>
|
|
7557
|
-
col.primaryKey().defaultTo(db.fn('gen_random_uuid')),
|
|
7558
|
-
)
|
|
7559
|
-
.addColumn('name', 'varchar(255)', (col) => col.notNull())
|
|
7560
|
-
.addColumn('email', 'varchar(255)', (col) => col.notNull().unique())
|
|
7561
|
-
.addColumn('created_at', 'timestamptz', (col) =>
|
|
7562
|
-
col.notNull().defaultTo(db.fn('now')),
|
|
7563
|
-
)
|
|
7564
|
-
.execute();
|
|
7565
|
-
|
|
7566
|
-
await db.schema
|
|
7567
|
-
.createIndex('users_email_idx')
|
|
7568
|
-
.on('users')
|
|
7569
|
-
.column('email')
|
|
7570
|
-
.execute();
|
|
7571
|
-
}
|
|
7572
|
-
|
|
7573
|
-
export async function down(db: Kysely<unknown>): Promise<void> {
|
|
7574
|
-
await db.schema.dropTable('users').execute();
|
|
7575
|
-
}
|
|
7576
|
-
`
|
|
7577
|
-
}];
|
|
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}')`;
|
|
7578
8151
|
}
|
|
7579
8152
|
|
|
7580
8153
|
//#endregion
|
|
@@ -7601,13 +8174,7 @@ function generateConfigFiles(options, template) {
|
|
|
7601
8174
|
const isServerless = template.name === "serverless";
|
|
7602
8175
|
const hasWorker = template.name === "worker";
|
|
7603
8176
|
const isFullstack = options.template === "fullstack";
|
|
7604
|
-
const getRoutesGlob = () =>
|
|
7605
|
-
switch (routesStructure) {
|
|
7606
|
-
case "centralized-endpoints": return "./src/endpoints/**/*.ts";
|
|
7607
|
-
case "centralized-routes": return "./src/routes/**/*.ts";
|
|
7608
|
-
case "domain-based": return "./src/**/routes/*.ts";
|
|
7609
|
-
}
|
|
7610
|
-
};
|
|
8177
|
+
const getRoutesGlob = () => routesGlob(routesStructure);
|
|
7611
8178
|
if (isFullstack) return generateSingleAppConfigFiles(options, template, {
|
|
7612
8179
|
telescope,
|
|
7613
8180
|
studio,
|
|
@@ -7618,7 +8185,7 @@ function generateConfigFiles(options, template) {
|
|
|
7618
8185
|
});
|
|
7619
8186
|
let gkmConfig = `import { defineConfig } from '@geekmidas/cli/config';
|
|
7620
8187
|
|
|
7621
|
-
export default defineConfig({${options.monorepo ? "" : `
|
|
8188
|
+
export default defineConfig({${stagesBlock(options.stages)}${options.monorepo ? "" : `
|
|
7622
8189
|
// One glob, every kind. What reconcile reads to derive the containers this
|
|
7623
8190
|
// app needs: a declared database is why a Postgres exists at all, so nothing
|
|
7624
8191
|
// lists \`postgres\` anywhere.
|
|
@@ -7660,21 +8227,7 @@ export default defineConfig({${options.monorepo ? "" : `
|
|
|
7660
8227
|
gkmConfig += `
|
|
7661
8228
|
});
|
|
7662
8229
|
`;
|
|
7663
|
-
const tsConfig = options.monorepo ? {
|
|
7664
|
-
extends: "../../tsconfig.json",
|
|
7665
|
-
compilerOptions: {
|
|
7666
|
-
noEmit: true,
|
|
7667
|
-
allowImportingTsExtensions: true,
|
|
7668
|
-
baseUrl: ".",
|
|
7669
|
-
paths: {
|
|
7670
|
-
"~/*": ["./src/*"],
|
|
7671
|
-
[`@${options.name}/constructs/*`]: ["../../constructs/*"],
|
|
7672
|
-
[`@${options.name}/*`]: ["../../packages/*/src"]
|
|
7673
|
-
}
|
|
7674
|
-
},
|
|
7675
|
-
include: ["src/**/*.ts"],
|
|
7676
|
-
exclude: ["node_modules", "dist"]
|
|
7677
|
-
} : {
|
|
8230
|
+
const tsConfig = options.monorepo ? workspaceApiTsConfig(options.name) : {
|
|
7678
8231
|
compilerOptions: {
|
|
7679
8232
|
target: "ES2022",
|
|
7680
8233
|
module: "NodeNext",
|
|
@@ -7686,7 +8239,9 @@ export default defineConfig({${options.monorepo ? "" : `
|
|
|
7686
8239
|
forceConsistentCasingInFileNames: true,
|
|
7687
8240
|
resolveJsonModule: true,
|
|
7688
8241
|
noEmit: true,
|
|
7689
|
-
allowImportingTsExtensions: true
|
|
8242
|
+
allowImportingTsExtensions: true,
|
|
8243
|
+
baseUrl: ".",
|
|
8244
|
+
paths: { "~/*": ["./src/*"] }
|
|
7690
8245
|
},
|
|
7691
8246
|
include: ["src/**/*.ts"],
|
|
7692
8247
|
exclude: ["node_modules", "dist"]
|
|
@@ -7712,7 +8267,7 @@ export default defineConfig({${options.monorepo ? "" : `
|
|
|
7712
8267
|
clientKind: "git",
|
|
7713
8268
|
useIgnoreFile: true
|
|
7714
8269
|
},
|
|
7715
|
-
|
|
8270
|
+
assist: { actions: { source: { organizeImports: "on" } } },
|
|
7716
8271
|
formatter: {
|
|
7717
8272
|
enabled: true,
|
|
7718
8273
|
indentStyle: "space",
|
|
@@ -7736,11 +8291,13 @@ export default defineConfig({${options.monorepo ? "" : `
|
|
|
7736
8291
|
style: { noNonNullAssertion: "off" }
|
|
7737
8292
|
}
|
|
7738
8293
|
},
|
|
7739
|
-
|
|
7740
|
-
|
|
7741
|
-
"
|
|
7742
|
-
"
|
|
7743
|
-
"
|
|
8294
|
+
css: { parser: { tailwindDirectives: true } },
|
|
8295
|
+
files: { includes: [
|
|
8296
|
+
"**",
|
|
8297
|
+
"!**/node_modules",
|
|
8298
|
+
"!**/dist",
|
|
8299
|
+
"!**/.gkm",
|
|
8300
|
+
"!**/coverage"
|
|
7744
8301
|
] }
|
|
7745
8302
|
};
|
|
7746
8303
|
const turboConfig = {
|
|
@@ -7794,8 +8351,16 @@ export default defineConfig({${options.monorepo ? "" : `
|
|
|
7794
8351
|
});
|
|
7795
8352
|
return files$1;
|
|
7796
8353
|
}
|
|
7797
|
-
|
|
7798
|
-
|
|
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 {
|
|
7799
8364
|
extends: "../../tsconfig.json",
|
|
7800
8365
|
compilerOptions: {
|
|
7801
8366
|
noEmit: true,
|
|
@@ -7803,12 +8368,16 @@ function generateSingleAppConfigFiles(options, _template, _helpers) {
|
|
|
7803
8368
|
baseUrl: ".",
|
|
7804
8369
|
paths: {
|
|
7805
8370
|
"~/*": ["./src/*"],
|
|
7806
|
-
[`@${
|
|
8371
|
+
[`@${name$1}/constructs/*`]: ["../../constructs/*"],
|
|
8372
|
+
[`@${name$1}/*`]: ["../../packages/*/src"]
|
|
7807
8373
|
}
|
|
7808
8374
|
},
|
|
7809
8375
|
include: ["src/**/*.ts"],
|
|
7810
8376
|
exclude: ["node_modules", "dist"]
|
|
7811
8377
|
};
|
|
8378
|
+
}
|
|
8379
|
+
function generateSingleAppConfigFiles(options, _template, _helpers) {
|
|
8380
|
+
const tsConfig = workspaceApiTsConfig(options.name);
|
|
7812
8381
|
const files$1 = [{
|
|
7813
8382
|
path: "tsconfig.json",
|
|
7814
8383
|
content: `${JSON.stringify(tsConfig, null, 2)}\n`
|
|
@@ -7944,7 +8513,7 @@ function generateDockerFiles(options, template, dbApps) {
|
|
|
7944
8513
|
MP_SMTP_AUTH: \${SMTP_USER:-${options.name}}:\${SMTP_PASS:-${options.name}}`);
|
|
7945
8514
|
if (options.services?.storage) {
|
|
7946
8515
|
services.push(` minio:
|
|
7947
|
-
image:
|
|
8516
|
+
image: pgsty/minio:RELEASE.2026-08-04T00-00-00Z
|
|
7948
8517
|
container_name: ${options.name}-minio
|
|
7949
8518
|
restart: unless-stopped
|
|
7950
8519
|
entrypoint: sh
|
|
@@ -8185,6 +8754,327 @@ coverage/
|
|
|
8185
8754
|
return files$1;
|
|
8186
8755
|
}
|
|
8187
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
|
+
|
|
8188
9078
|
//#endregion
|
|
8189
9079
|
//#region src/init/generators/mobile-expo.ts
|
|
8190
9080
|
/**
|
|
@@ -8217,12 +9107,12 @@ function generateExpoAppFiles(options) {
|
|
|
8217
9107
|
dependencies: {
|
|
8218
9108
|
[apiPackage]: "workspace:*",
|
|
8219
9109
|
[modelsPackage]: "workspace:*",
|
|
8220
|
-
"@better-auth/expo": "
|
|
9110
|
+
"@better-auth/expo": DEPENDENCY_VERSIONS["better-auth"],
|
|
8221
9111
|
"@geekmidas/client": GEEKMIDAS_VERSIONS["@geekmidas/client"],
|
|
8222
9112
|
"@geekmidas/envkit": GEEKMIDAS_VERSIONS["@geekmidas/envkit"],
|
|
8223
9113
|
"@react-navigation/native": "^7.1.0",
|
|
8224
|
-
"@tanstack/react-query": "
|
|
8225
|
-
"better-auth": "
|
|
9114
|
+
"@tanstack/react-query": DEPENDENCY_VERSIONS["@tanstack/react-query"],
|
|
9115
|
+
"better-auth": DEPENDENCY_VERSIONS["better-auth"],
|
|
8226
9116
|
expo: "~55.0.0",
|
|
8227
9117
|
"expo-constants": "~55.0.0",
|
|
8228
9118
|
"expo-dev-client": "~55.0.0",
|
|
@@ -8358,7 +9248,7 @@ module.exports = withNativeWind(config, { input: './global.css' });
|
|
|
8358
9248
|
"@/*": ["./*"],
|
|
8359
9249
|
[modelsPackage]: ["../../packages/models/src"],
|
|
8360
9250
|
[`${modelsPackage}/*`]: ["../../packages/models/src/*"],
|
|
8361
|
-
[`${apiPackage}/client`]: ["../../apps/api/.gkm/openapi.ts"]
|
|
9251
|
+
[`${apiPackage}/client`]: ["../../apps/api/.gkm/openapi/api.ts"]
|
|
8362
9252
|
}
|
|
8363
9253
|
},
|
|
8364
9254
|
include: [
|
|
@@ -8916,14 +9806,142 @@ export type ListUsersResponse = z.infer<typeof ListUsersResponseSchema>;
|
|
|
8916
9806
|
];
|
|
8917
9807
|
}
|
|
8918
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
|
+
|
|
8919
9914
|
//#endregion
|
|
8920
9915
|
//#region src/init/generators/monorepo.ts
|
|
8921
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
|
+
/**
|
|
8922
9938
|
* Generate monorepo root files (pnpm-workspace.yaml, root package.json, etc.)
|
|
8923
9939
|
*/
|
|
8924
9940
|
function generateMonorepoFiles(options, _template) {
|
|
8925
9941
|
if (!options.monorepo) return [];
|
|
8926
9942
|
const isFullstack = options.template === "fullstack";
|
|
9943
|
+
const deploy$1 = deployPackage(options);
|
|
9944
|
+
const isSst = options.deployTarget === "sst";
|
|
8927
9945
|
const rootPackageJson = {
|
|
8928
9946
|
name: options.name,
|
|
8929
9947
|
version: "0.0.1",
|
|
@@ -8940,9 +9958,13 @@ function generateMonorepoFiles(options, _template) {
|
|
|
8940
9958
|
fmt: "biome format . --write",
|
|
8941
9959
|
"fmt:check": "biome format .",
|
|
8942
9960
|
...isFullstack ? { storybook: "pnpm --filter ./packages/ui storybook" } : {},
|
|
8943
|
-
...
|
|
9961
|
+
...deploy$1.scripts
|
|
9962
|
+
},
|
|
9963
|
+
dependencies: {
|
|
9964
|
+
zod: DEPENDENCY_VERSIONS.zod,
|
|
9965
|
+
...isFullstack ? rootConstructDependencies(options) : {},
|
|
9966
|
+
...deploy$1.dependencies
|
|
8944
9967
|
},
|
|
8945
|
-
dependencies: { zod: "~4.1.0" },
|
|
8946
9968
|
devDependencies: {
|
|
8947
9969
|
"@biomejs/biome": "~2.3.0",
|
|
8948
9970
|
"@geekmidas/cli": GEEKMIDAS_VERSIONS["@geekmidas/cli"],
|
|
@@ -8950,7 +9972,8 @@ function generateMonorepoFiles(options, _template) {
|
|
|
8950
9972
|
tsx: "~4.20.0",
|
|
8951
9973
|
turbo: "~2.3.0",
|
|
8952
9974
|
typescript: "~5.8.2",
|
|
8953
|
-
vitest: "~4.0.0"
|
|
9975
|
+
vitest: "~4.0.0",
|
|
9976
|
+
...deploy$1.devDependencies
|
|
8954
9977
|
}
|
|
8955
9978
|
};
|
|
8956
9979
|
const apiPathParts = options.apiPath.split("/");
|
|
@@ -8966,7 +9989,7 @@ function generateMonorepoFiles(options, _template) {
|
|
|
8966
9989
|
clientKind: "git",
|
|
8967
9990
|
useIgnoreFile: true
|
|
8968
9991
|
},
|
|
8969
|
-
|
|
9992
|
+
assist: { actions: { source: { organizeImports: "on" } } },
|
|
8970
9993
|
formatter: {
|
|
8971
9994
|
enabled: true,
|
|
8972
9995
|
indentStyle: "space",
|
|
@@ -8990,11 +10013,14 @@ function generateMonorepoFiles(options, _template) {
|
|
|
8990
10013
|
style: { noNonNullAssertion: "off" }
|
|
8991
10014
|
}
|
|
8992
10015
|
},
|
|
8993
|
-
|
|
8994
|
-
|
|
8995
|
-
"
|
|
8996
|
-
"
|
|
8997
|
-
"
|
|
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"] : []
|
|
8998
10024
|
] }
|
|
8999
10025
|
};
|
|
9000
10026
|
const turboConfig = {
|
|
@@ -9060,12 +10086,14 @@ coverage/
|
|
|
9060
10086
|
|
|
9061
10087
|
# Turbo
|
|
9062
10088
|
.turbo/
|
|
9063
|
-
`;
|
|
10089
|
+
${isSst ? "\n# SST\n.sst/\n" : ""}`;
|
|
9064
10090
|
const tsConfig = {
|
|
9065
10091
|
compilerOptions: {
|
|
9066
10092
|
...isFullstack ? {
|
|
9067
10093
|
baseUrl: ".",
|
|
9068
|
-
paths: { [`@${options.name}/constructs/*`]: ["./constructs/*"] }
|
|
10094
|
+
paths: { [`@${options.name}/constructs/*`]: ["./constructs/*"] },
|
|
10095
|
+
allowImportingTsExtensions: true,
|
|
10096
|
+
noEmit: true
|
|
9069
10097
|
} : {},
|
|
9070
10098
|
target: "ES2022",
|
|
9071
10099
|
module: "NodeNext",
|
|
@@ -9077,16 +10105,21 @@ coverage/
|
|
|
9077
10105
|
forceConsistentCasingInFileNames: true,
|
|
9078
10106
|
resolveJsonModule: true
|
|
9079
10107
|
},
|
|
9080
|
-
exclude: [
|
|
10108
|
+
exclude: [
|
|
10109
|
+
"node_modules",
|
|
10110
|
+
"dist",
|
|
10111
|
+
...isSst ? ["sst.config.ts", ".sst"] : []
|
|
10112
|
+
]
|
|
9081
10113
|
};
|
|
9082
10114
|
const vitestConfig = `import { defineConfig } from 'vitest/config';
|
|
9083
10115
|
|
|
9084
10116
|
export default defineConfig({
|
|
9085
10117
|
test: {
|
|
9086
|
-
|
|
9087
|
-
|
|
9088
|
-
|
|
9089
|
-
|
|
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,
|
|
9090
10123
|
coverage: {
|
|
9091
10124
|
provider: 'v8',
|
|
9092
10125
|
reporter: ['text', 'json', 'html'],
|
|
@@ -9167,7 +10200,8 @@ export default defineConfig({
|
|
|
9167
10200
|
{
|
|
9168
10201
|
path: ".vscode/extensions.json",
|
|
9169
10202
|
content: `${JSON.stringify(vscodeExtensions, null, " ")}\n`
|
|
9170
|
-
}
|
|
10203
|
+
},
|
|
10204
|
+
...generateDeployFiles(options)
|
|
9171
10205
|
];
|
|
9172
10206
|
if (isFullstack) files$1.push({
|
|
9173
10207
|
path: "gkm.config.ts",
|
|
@@ -9194,12 +10228,17 @@ export default defineWorkspace({
|
|
|
9194
10228
|
// The scope every physical name is built from: \`Database\` becomes
|
|
9195
10229
|
// \`production-${options.name}-database\` on Dokploy and on AWS alike.
|
|
9196
10230
|
name: '${options.name}',
|
|
10231
|
+
${stagesBlock(options.stages)}
|
|
9197
10232
|
|
|
9198
|
-
//
|
|
10233
|
+
// Every kind, in every app. A declared database is why a Postgres exists, a
|
|
9199
10234
|
// declared bucket is why MinIO does, a declared topic is why a broker does —
|
|
9200
10235
|
// none of it listed here. It is also where the apps come from: a
|
|
9201
|
-
// \`StaticSite\` is an app, and so is every \`RestApi\`.
|
|
9202
|
-
|
|
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
|
+
],
|
|
9203
10242
|
|
|
9204
10243
|
secrets: {
|
|
9205
10244
|
enabled: true,
|
|
@@ -9228,7 +10267,7 @@ export default defineWorkspace({
|
|
|
9228
10267
|
function generateRootConstructs(options) {
|
|
9229
10268
|
if (!options.monorepo || options.template !== "fullstack") return [];
|
|
9230
10269
|
const { name: name$1, services, frontendFramework } = options;
|
|
9231
|
-
const db = databaseFor(
|
|
10270
|
+
const db = databaseFor();
|
|
9232
10271
|
const files$1 = [];
|
|
9233
10272
|
files$1.push({
|
|
9234
10273
|
path: "constructs/logger.ts",
|
|
@@ -9294,6 +10333,7 @@ import { authDb } from './database.ts';
|
|
|
9294
10333
|
* origin down.
|
|
9295
10334
|
*/
|
|
9296
10335
|
export const auth = new BetterAuth('Auth', {
|
|
10336
|
+
path: 'apps/auth',
|
|
9297
10337
|
database: authDb,
|
|
9298
10338
|
basePath: '/api/auth',
|
|
9299
10339
|
});
|
|
@@ -9308,25 +10348,22 @@ import { logger } from './logger.ts';
|
|
|
9308
10348
|
/**
|
|
9309
10349
|
* The application's HTTP surface — one RestApi, one container.
|
|
9310
10350
|
*
|
|
9311
|
-
*
|
|
9312
|
-
* \`
|
|
9313
|
-
* \`src/\`, where the conventional layout has them at the app root.
|
|
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.
|
|
9314
10353
|
*/
|
|
9315
10354
|
export const api = new RestApi('Api', {
|
|
10355
|
+
path: '${options.apiPath}',
|
|
10356
|
+
|
|
9316
10357
|
// Typed out rather than omitted: an API that ships open because a field was
|
|
9317
10358
|
// left off is the one default worth refusing to have.
|
|
9318
10359
|
defaultAuthorizer: 'none',
|
|
9319
10360
|
|
|
9320
10361
|
// The actual logger, not a path to one.
|
|
9321
10362
|
logger,
|
|
9322
|
-
|
|
9323
|
-
app: {
|
|
9324
|
-
code: './src/{endpoints,functions,crons,queues,topics,subscribers}/**/*.ts',
|
|
9325
|
-
},
|
|
9326
10363
|
}).auth(auth);
|
|
9327
10364
|
`
|
|
9328
10365
|
});
|
|
9329
|
-
const variant = frontendFramework === "tanstack-start" ? "{ variant: 'tanstack' }" : frontendFramework === "expo" ? void 0 : "{ variant: 'next' }";
|
|
10366
|
+
const variant = frontendFramework === "tanstack-start" ? "{ path: 'apps/web', variant: 'tanstack' }" : frontendFramework === "expo" ? void 0 : "{ path: 'apps/web', variant: 'next' }";
|
|
9330
10367
|
if (variant !== void 0) files$1.push({
|
|
9331
10368
|
path: "constructs/site.ts",
|
|
9332
10369
|
content: `import { StaticSite } from '@geekmidas/constructs/site';
|
|
@@ -9336,7 +10373,7 @@ import { auth } from './auth.ts';
|
|
|
9336
10373
|
/**
|
|
9337
10374
|
* The frontend — a construct like any other, which is what makes it an app.
|
|
9338
10375
|
*
|
|
9339
|
-
*
|
|
10376
|
+
* \`path\` is where it lives. \`.dependsOn()\` is the single fact
|
|
9340
10377
|
* behind four things that are hand-maintained otherwise: this site's
|
|
9341
10378
|
* build-time API URL, the API's CORS origins, the auth server's trusted
|
|
9342
10379
|
* origins, and which generated client lands here.
|
|
@@ -9345,7 +10382,7 @@ export const web = new StaticSite('Web', ${variant}).dependsOn([api, auth]);
|
|
|
9345
10382
|
`
|
|
9346
10383
|
});
|
|
9347
10384
|
if (services.storage) {
|
|
9348
|
-
const bucket = storageFor(
|
|
10385
|
+
const bucket = storageFor();
|
|
9349
10386
|
files$1.push({
|
|
9350
10387
|
path: "constructs/storage.ts",
|
|
9351
10388
|
content: `import { ObjectStorage } from '@geekmidas/constructs/object-storage';
|
|
@@ -9356,7 +10393,7 @@ export const uploads = new ObjectStorage('${bucket.id}');
|
|
|
9356
10393
|
});
|
|
9357
10394
|
}
|
|
9358
10395
|
if (services.mail) {
|
|
9359
|
-
const mail = emailFor(
|
|
10396
|
+
const mail = emailFor();
|
|
9360
10397
|
files$1.push({
|
|
9361
10398
|
path: "constructs/email.ts",
|
|
9362
10399
|
content: `import { Email } from '@geekmidas/constructs/email';
|
|
@@ -9367,7 +10404,7 @@ export const email = new Email('${mail.id}', { templates: {} });
|
|
|
9367
10404
|
});
|
|
9368
10405
|
}
|
|
9369
10406
|
if (services.cache) {
|
|
9370
|
-
const kv = cacheFor(
|
|
10407
|
+
const kv = cacheFor();
|
|
9371
10408
|
files$1.push({
|
|
9372
10409
|
path: "constructs/cache.ts",
|
|
9373
10410
|
content: `import { Cache } from '@geekmidas/constructs/cache';
|
|
@@ -9400,9 +10437,9 @@ const apiTemplate = {
|
|
|
9400
10437
|
"@geekmidas/errors": GEEKMIDAS_VERSIONS["@geekmidas/errors"],
|
|
9401
10438
|
"@geekmidas/auth": GEEKMIDAS_VERSIONS["@geekmidas/auth"],
|
|
9402
10439
|
"@hono/node-server": "~1.14.1",
|
|
9403
|
-
hono:
|
|
9404
|
-
pino:
|
|
9405
|
-
zod:
|
|
10440
|
+
hono: DEPENDENCY_VERSIONS.hono,
|
|
10441
|
+
pino: DEPENDENCY_VERSIONS.pino,
|
|
10442
|
+
zod: DEPENDENCY_VERSIONS.zod
|
|
9406
10443
|
},
|
|
9407
10444
|
devDependencies: {
|
|
9408
10445
|
"@biomejs/biome": "~2.3.0",
|
|
@@ -9426,10 +10463,10 @@ const apiTemplate = {
|
|
|
9426
10463
|
},
|
|
9427
10464
|
files: (options) => {
|
|
9428
10465
|
const { loggerType, routesStructure, monorepo, name: name$1, services } = options;
|
|
9429
|
-
const bucket = storageFor(
|
|
9430
|
-
const kv = cacheFor(
|
|
9431
|
-
const db = databaseFor(
|
|
9432
|
-
const mail = emailFor(
|
|
10466
|
+
const bucket = storageFor();
|
|
10467
|
+
const kv = cacheFor();
|
|
10468
|
+
const db = databaseFor();
|
|
10469
|
+
const mail = emailFor();
|
|
9433
10470
|
const src = (path) => monorepo ? `~/${path}` : `./${path}`;
|
|
9434
10471
|
const declares = !monorepo;
|
|
9435
10472
|
const loggerContent = `import { createLogger } from '@geekmidas/logger/${loggerType}';
|
|
@@ -9463,6 +10500,9 @@ export const logger = createLogger();
|
|
|
9463
10500
|
import { logger } from '../config/logger.ts';
|
|
9464
10501
|
|
|
9465
10502
|
export const api = new RestApi('Api', {
|
|
10503
|
+
// The project is the app.
|
|
10504
|
+
path: '.',
|
|
10505
|
+
|
|
9466
10506
|
// Typed out rather than omitted: an API that ships open because a field was
|
|
9467
10507
|
// left off is the one default worth refusing to have.
|
|
9468
10508
|
defaultAuthorizer: 'none',
|
|
@@ -9485,7 +10525,7 @@ export const envParser = new EnvironmentParser({ ...process.env, ...Credentials
|
|
|
9485
10525
|
export const config = envParser
|
|
9486
10526
|
.create((get) => ({
|
|
9487
10527
|
nodeEnv: get('NODE_ENV').enum(['development', 'test', 'production']).default('development'),
|
|
9488
|
-
stage:
|
|
10528
|
+
stage: ${stageEnv(options.stages)},
|
|
9489
10529
|
}))
|
|
9490
10530
|
.parse();
|
|
9491
10531
|
`
|
|
@@ -9637,7 +10677,7 @@ export const authService = {
|
|
|
9637
10677
|
headers: { cookie },
|
|
9638
10678
|
});
|
|
9639
10679
|
if (!res.ok) return null;
|
|
9640
|
-
return res.json();
|
|
10680
|
+
return (await res.json()) as Session | null;
|
|
9641
10681
|
},
|
|
9642
10682
|
};
|
|
9643
10683
|
},
|
|
@@ -9650,7 +10690,6 @@ export const authService = {
|
|
|
9650
10690
|
import { api } from '${constructsImport("api")}';${options.database ? `
|
|
9651
10691
|
import { database } from '${constructsImport("database")}';` : ""}
|
|
9652
10692
|
import { authService, type Session } from './services/auth.ts';
|
|
9653
|
-
import { logger } from './config/logger.ts';
|
|
9654
10693
|
|
|
9655
10694
|
/**
|
|
9656
10695
|
* The shared endpoint factory — no session required.
|
|
@@ -9708,7 +10747,8 @@ import { database } from '${constructsImport("database")}';` : ""}
|
|
|
9708
10747
|
export const router = api.endpoints${options.database ? ".database(database)" : ""};
|
|
9709
10748
|
`
|
|
9710
10749
|
});
|
|
9711
|
-
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());
|
|
9712
10752
|
if (services.storage && declares) files$1.push({
|
|
9713
10753
|
path: "src/constructs/storage.ts",
|
|
9714
10754
|
content: `import { ObjectStorage } from '@geekmidas/constructs/object-storage';
|
|
@@ -9858,9 +10898,9 @@ const minimalTemplate = {
|
|
|
9858
10898
|
"@geekmidas/rate-limit": GEEKMIDAS_VERSIONS["@geekmidas/rate-limit"],
|
|
9859
10899
|
"@geekmidas/schema": GEEKMIDAS_VERSIONS["@geekmidas/schema"],
|
|
9860
10900
|
"@hono/node-server": "~1.14.1",
|
|
9861
|
-
hono:
|
|
9862
|
-
pino:
|
|
9863
|
-
zod:
|
|
10901
|
+
hono: DEPENDENCY_VERSIONS.hono,
|
|
10902
|
+
pino: DEPENDENCY_VERSIONS.pino,
|
|
10903
|
+
zod: DEPENDENCY_VERSIONS.zod
|
|
9864
10904
|
},
|
|
9865
10905
|
devDependencies: {
|
|
9866
10906
|
"@biomejs/biome": "~2.3.0",
|
|
@@ -9884,7 +10924,7 @@ const minimalTemplate = {
|
|
|
9884
10924
|
},
|
|
9885
10925
|
files: (options) => {
|
|
9886
10926
|
const { loggerType, routesStructure, name: name$1 } = options;
|
|
9887
|
-
const db = databaseFor(
|
|
10927
|
+
const db = databaseFor();
|
|
9888
10928
|
const loggerContent = `import { createLogger } from '@geekmidas/logger/${loggerType}';
|
|
9889
10929
|
|
|
9890
10930
|
export const logger = createLogger();
|
|
@@ -9904,6 +10944,9 @@ export const logger = createLogger();
|
|
|
9904
10944
|
import { logger } from '../config/logger.ts';
|
|
9905
10945
|
|
|
9906
10946
|
export const api = new RestApi('Api', {
|
|
10947
|
+
// The project is the app.
|
|
10948
|
+
path: '.',
|
|
10949
|
+
|
|
9907
10950
|
// Typed out rather than omitted: an API that ships open because a field was
|
|
9908
10951
|
// left off is the one default worth refusing to have.
|
|
9909
10952
|
defaultAuthorizer: 'none',
|
|
@@ -9926,7 +10969,7 @@ export const envParser = new EnvironmentParser({ ...process.env, ...Credentials
|
|
|
9926
10969
|
export const config = envParser
|
|
9927
10970
|
.create((get) => ({
|
|
9928
10971
|
nodeEnv: get('NODE_ENV').enum(['development', 'test', 'production']).default('development'),
|
|
9929
|
-
stage:
|
|
10972
|
+
stage: ${stageEnv(options.stages)},
|
|
9930
10973
|
}))
|
|
9931
10974
|
.parse();
|
|
9932
10975
|
`
|
|
@@ -9953,7 +10996,7 @@ export const healthEndpoint = api
|
|
|
9953
10996
|
`
|
|
9954
10997
|
}
|
|
9955
10998
|
];
|
|
9956
|
-
if (options.database) files$1.push(...databaseFiles(
|
|
10999
|
+
if (options.database) files$1.push(...databaseFiles());
|
|
9957
11000
|
if (options.telescope) files$1.push({
|
|
9958
11001
|
path: "src/config/telescope.ts",
|
|
9959
11002
|
content: `import { Telescope } from '@geekmidas/telescope';
|
|
@@ -10017,8 +11060,8 @@ const serverlessTemplate = {
|
|
|
10017
11060
|
"@geekmidas/rate-limit": GEEKMIDAS_VERSIONS["@geekmidas/rate-limit"],
|
|
10018
11061
|
"@geekmidas/schema": GEEKMIDAS_VERSIONS["@geekmidas/schema"],
|
|
10019
11062
|
"@hono/node-server": "~1.14.1",
|
|
10020
|
-
hono:
|
|
10021
|
-
pino:
|
|
11063
|
+
hono: DEPENDENCY_VERSIONS.hono,
|
|
11064
|
+
pino: DEPENDENCY_VERSIONS.pino
|
|
10022
11065
|
},
|
|
10023
11066
|
devDependencies: {
|
|
10024
11067
|
"@biomejs/biome": "~2.3.0",
|
|
@@ -10061,6 +11104,9 @@ export const logger = createLogger();
|
|
|
10061
11104
|
import { logger } from '../config/logger.ts';
|
|
10062
11105
|
|
|
10063
11106
|
export const api = new RestApi('Api', {
|
|
11107
|
+
// The project is the app.
|
|
11108
|
+
path: '.',
|
|
11109
|
+
|
|
10064
11110
|
// Typed out rather than omitted: an API that ships open because a field was
|
|
10065
11111
|
// left off is the one default worth refusing to have.
|
|
10066
11112
|
defaultAuthorizer: 'none',
|
|
@@ -10083,7 +11129,7 @@ export const envParser = new EnvironmentParser({ ...process.env, ...Credentials
|
|
|
10083
11129
|
export const config = envParser
|
|
10084
11130
|
.create((get) => ({
|
|
10085
11131
|
nodeEnv: get('NODE_ENV').enum(['development', 'test', 'production']).default('development'),
|
|
10086
|
-
stage:
|
|
11132
|
+
stage: ${stageEnv(options.stages)},
|
|
10087
11133
|
}))
|
|
10088
11134
|
.parse();
|
|
10089
11135
|
`
|
|
@@ -10113,19 +11159,27 @@ export const healthEndpoint = api
|
|
|
10113
11159
|
},
|
|
10114
11160
|
{
|
|
10115
11161
|
path: "src/functions/hello.ts",
|
|
10116
|
-
content: `import {
|
|
10117
|
-
import {
|
|
11162
|
+
content: `import { z } from 'zod';
|
|
11163
|
+
import { worker } from '~/constructs/worker.ts';
|
|
10118
11164
|
|
|
10119
|
-
export const helloFunction =
|
|
11165
|
+
export const helloFunction = worker
|
|
10120
11166
|
.input(z.object({ name: z.string() }))
|
|
10121
11167
|
.output(z.object({ message: z.string() }))
|
|
10122
11168
|
.handle(async ({ input }) => ({
|
|
10123
11169
|
message: \`Hello, \${input.name}!\`,
|
|
10124
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 });
|
|
10125
11179
|
`
|
|
10126
11180
|
}
|
|
10127
11181
|
];
|
|
10128
|
-
if (options.database) files$1.push(...databaseFiles(
|
|
11182
|
+
if (options.database) files$1.push(...databaseFiles());
|
|
10129
11183
|
if (options.telescope) files$1.push({
|
|
10130
11184
|
path: "src/config/telescope.ts",
|
|
10131
11185
|
content: `import { Telescope } from '@geekmidas/telescope';
|
|
@@ -10134,7 +11188,7 @@ import { InMemoryStorage } from '@geekmidas/telescope/storage/memory';
|
|
|
10134
11188
|
// Note: For production Lambda, consider using a persistent storage
|
|
10135
11189
|
export const telescope = new Telescope({
|
|
10136
11190
|
storage: new InMemoryStorage({ maxEntries: 50 }),
|
|
10137
|
-
enabled: process.env.STAGE === '
|
|
11191
|
+
enabled: process.env.STAGE === '${options.stages.local}',
|
|
10138
11192
|
});
|
|
10139
11193
|
`
|
|
10140
11194
|
});
|
|
@@ -10144,6 +11198,22 @@ export const telescope = new Telescope({
|
|
|
10144
11198
|
|
|
10145
11199
|
//#endregion
|
|
10146
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
|
+
*/
|
|
10147
11217
|
const workerTemplate = {
|
|
10148
11218
|
name: "worker",
|
|
10149
11219
|
description: "Background job processing",
|
|
@@ -10153,11 +11223,8 @@ const workerTemplate = {
|
|
|
10153
11223
|
"@geekmidas/envkit": GEEKMIDAS_VERSIONS["@geekmidas/envkit"],
|
|
10154
11224
|
"@geekmidas/events": GEEKMIDAS_VERSIONS["@geekmidas/events"],
|
|
10155
11225
|
"@geekmidas/logger": GEEKMIDAS_VERSIONS["@geekmidas/logger"],
|
|
10156
|
-
"@geekmidas/rate-limit": GEEKMIDAS_VERSIONS["@geekmidas/rate-limit"],
|
|
10157
11226
|
"@geekmidas/schema": GEEKMIDAS_VERSIONS["@geekmidas/schema"],
|
|
10158
|
-
|
|
10159
|
-
hono: "~4.8.2",
|
|
10160
|
-
pino: "~9.6.0"
|
|
11227
|
+
pino: DEPENDENCY_VERSIONS.pino
|
|
10161
11228
|
},
|
|
10162
11229
|
devDependencies: {
|
|
10163
11230
|
"@biomejs/biome": "~2.3.0",
|
|
@@ -10179,34 +11246,18 @@ const workerTemplate = {
|
|
|
10179
11246
|
"fmt:check": "biome format ."
|
|
10180
11247
|
},
|
|
10181
11248
|
files: (options) => {
|
|
10182
|
-
const { loggerType,
|
|
11249
|
+
const { loggerType, name: name$1, database } = options;
|
|
10183
11250
|
const loggerContent = `import { createLogger } from '@geekmidas/logger/${loggerType}';
|
|
10184
11251
|
|
|
10185
11252
|
export const logger = createLogger();
|
|
10186
11253
|
`;
|
|
10187
|
-
const getRoutePath = (file) => {
|
|
10188
|
-
switch (routesStructure) {
|
|
10189
|
-
case "centralized-endpoints": return `src/endpoints/${file}`;
|
|
10190
|
-
case "centralized-routes": return `src/routes/${file}`;
|
|
10191
|
-
case "domain-based": return `src/${file.replace(".ts", "")}/routes/index.ts`;
|
|
10192
|
-
}
|
|
10193
|
-
};
|
|
10194
|
-
const apiImport = routesStructure === "domain-based" ? "../../constructs/api.js" : "../constructs/api.js";
|
|
10195
11254
|
const files$1 = [
|
|
10196
11255
|
{
|
|
10197
|
-
path: "src/constructs/
|
|
10198
|
-
content: `import {
|
|
10199
|
-
import { logger } from '../config/logger.ts';
|
|
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';" : ""}
|
|
10200
11259
|
|
|
10201
|
-
export const
|
|
10202
|
-
// Typed out rather than omitted: an API that ships open because a field was
|
|
10203
|
-
// left off is the one default worth refusing to have.
|
|
10204
|
-
defaultAuthorizer: 'none',
|
|
10205
|
-
|
|
10206
|
-
// The actual logger, not a path to one. Every endpoint built from this
|
|
10207
|
-
// surface runs with it, and so does the generated entry.
|
|
10208
|
-
logger,
|
|
10209
|
-
});
|
|
11260
|
+
export const worker = new Worker('Jobs', { logger })${database ? ".database(database)" : ""};
|
|
10210
11261
|
`
|
|
10211
11262
|
},
|
|
10212
11263
|
{
|
|
@@ -10221,7 +11272,7 @@ export const envParser = new EnvironmentParser({ ...process.env, ...Credentials
|
|
|
10221
11272
|
export const config = envParser
|
|
10222
11273
|
.create((get) => ({
|
|
10223
11274
|
nodeEnv: get('NODE_ENV').enum(['development', 'test', 'production']).default('development'),
|
|
10224
|
-
stage:
|
|
11275
|
+
stage: ${stageEnv(options.stages)},
|
|
10225
11276
|
}))
|
|
10226
11277
|
.parse();
|
|
10227
11278
|
`
|
|
@@ -10230,23 +11281,6 @@ export const config = envParser
|
|
|
10230
11281
|
path: "src/config/logger.ts",
|
|
10231
11282
|
content: loggerContent
|
|
10232
11283
|
},
|
|
10233
|
-
{
|
|
10234
|
-
path: getRoutePath("health.ts"),
|
|
10235
|
-
content: `import { z } from 'zod';
|
|
10236
|
-
import { api } from '${apiImport}';
|
|
10237
|
-
|
|
10238
|
-
export const healthEndpoint = api
|
|
10239
|
-
.get('/health')
|
|
10240
|
-
.output(z.object({
|
|
10241
|
-
status: z.string(),
|
|
10242
|
-
timestamp: z.string(),
|
|
10243
|
-
}))
|
|
10244
|
-
.handle(async () => ({
|
|
10245
|
-
status: 'ok',
|
|
10246
|
-
timestamp: new Date().toISOString(),
|
|
10247
|
-
}));
|
|
10248
|
-
`
|
|
10249
|
-
},
|
|
10250
11284
|
{
|
|
10251
11285
|
path: "src/events/types.ts",
|
|
10252
11286
|
content: `import type { PublishableMessage } from '@geekmidas/events';
|
|
@@ -10288,46 +11322,47 @@ export const eventsPublisherService = {
|
|
|
10288
11322
|
},
|
|
10289
11323
|
{
|
|
10290
11324
|
path: "src/subscribers/user-events.ts",
|
|
10291
|
-
content: `import {
|
|
11325
|
+
content: `import { worker } from '~/constructs/worker.ts';
|
|
10292
11326
|
import { eventsPublisherService } from '~/events/publisher.ts';
|
|
10293
11327
|
|
|
10294
|
-
export const userEventsSubscriber =
|
|
11328
|
+
export const userEventsSubscriber = worker
|
|
10295
11329
|
.publisher(eventsPublisherService)
|
|
10296
11330
|
.subscribe(['user.created', 'user.updated'])
|
|
10297
|
-
.handle(async ({
|
|
10298
|
-
|
|
10299
|
-
|
|
10300
|
-
|
|
10301
|
-
|
|
10302
|
-
|
|
10303
|
-
|
|
10304
|
-
|
|
10305
|
-
|
|
10306
|
-
|
|
10307
|
-
|
|
10308
|
-
|
|
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
|
+
}
|
|
10309
11345
|
}
|
|
10310
11346
|
});
|
|
10311
11347
|
`
|
|
10312
|
-
}
|
|
10313
|
-
|
|
10314
|
-
|
|
10315
|
-
|
|
11348
|
+
}
|
|
11349
|
+
];
|
|
11350
|
+
if (database) files$1.push({
|
|
11351
|
+
path: "src/crons/cleanup.ts",
|
|
11352
|
+
content: `import { worker } from '~/constructs/worker.ts';
|
|
10316
11353
|
|
|
10317
|
-
|
|
10318
|
-
|
|
11354
|
+
export const cleanup = worker
|
|
11355
|
+
.cron('rate(1 day)')
|
|
10319
11356
|
.handle(async ({ logger }) => {
|
|
10320
|
-
logger.info('Running cleanup
|
|
11357
|
+
logger.info('Running cleanup');
|
|
10321
11358
|
|
|
10322
|
-
//
|
|
10323
|
-
// e.g., delete old sessions, clean up temp files, etc.
|
|
11359
|
+
// Delete old sessions, clear temp files, whatever falls out of date.
|
|
10324
11360
|
|
|
10325
|
-
logger.info('Cleanup
|
|
11361
|
+
logger.info('Cleanup complete');
|
|
10326
11362
|
});
|
|
10327
11363
|
`
|
|
10328
|
-
|
|
10329
|
-
|
|
10330
|
-
if (options.database) files$1.push(...databaseFiles(name$1));
|
|
11364
|
+
});
|
|
11365
|
+
if (options.database) files$1.push(...databaseFiles());
|
|
10331
11366
|
if (options.telescope) files$1.push({
|
|
10332
11367
|
path: "src/config/telescope.ts",
|
|
10333
11368
|
content: `import { Telescope } from '@geekmidas/telescope';
|
|
@@ -10348,7 +11383,7 @@ export const telescope = new Telescope({
|
|
|
10348
11383
|
/**
|
|
10349
11384
|
* OpenAPI output path (fixed, not configurable)
|
|
10350
11385
|
*/
|
|
10351
|
-
const OPENAPI_OUTPUT_PATH$1 = "./.gkm/openapi.ts";
|
|
11386
|
+
const OPENAPI_OUTPUT_PATH$1 = "./.gkm/openapi/api.ts";
|
|
10352
11387
|
/**
|
|
10353
11388
|
* Endpoints output path (generated by gkm dev/build)
|
|
10354
11389
|
*/
|
|
@@ -10454,15 +11489,23 @@ const packageManagerChoices = [
|
|
|
10454
11489
|
/**
|
|
10455
11490
|
* Deploy target choices for prompts
|
|
10456
11491
|
*/
|
|
10457
|
-
const deployTargetChoices = [
|
|
10458
|
-
|
|
10459
|
-
|
|
10460
|
-
|
|
10461
|
-
|
|
10462
|
-
|
|
10463
|
-
|
|
10464
|
-
|
|
10465
|
-
|
|
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
|
+
];
|
|
10466
11509
|
/**
|
|
10467
11510
|
* Services choices for multi-select prompt
|
|
10468
11511
|
*/
|
|
@@ -10555,12 +11598,12 @@ function generatePackageJson(options, template) {
|
|
|
10555
11598
|
if (declares && services.mail) dependencies$1["@geekmidas/emailkit"] = GEEKMIDAS_VERSIONS["@geekmidas/emailkit"];
|
|
10556
11599
|
if (database) {
|
|
10557
11600
|
dependencies$1["@geekmidas/db"] = GEEKMIDAS_VERSIONS["@geekmidas/db"];
|
|
10558
|
-
dependencies$1.kysely =
|
|
10559
|
-
dependencies$1.pg =
|
|
10560
|
-
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";
|
|
10561
11604
|
devDependencies$1["@geekmidas/testkit"] = GEEKMIDAS_VERSIONS["@geekmidas/testkit"];
|
|
10562
11605
|
devDependencies$1["@faker-js/faker"] = "~9.8.0";
|
|
10563
|
-
devDependencies$1["kysely-ctl"] = "
|
|
11606
|
+
devDependencies$1["kysely-ctl"] = DEPENDENCY_VERSIONS["kysely-ctl"];
|
|
10564
11607
|
devDependencies$1["vite-tsconfig-paths"] = "~5.1.0";
|
|
10565
11608
|
}
|
|
10566
11609
|
if (monorepo) {
|
|
@@ -10580,6 +11623,8 @@ function generatePackageJson(options, template) {
|
|
|
10580
11623
|
const appName = pathParts[pathParts.length - 1] || "api";
|
|
10581
11624
|
packageName = `@${name$1}/${appName}`;
|
|
10582
11625
|
}
|
|
11626
|
+
dependencies$1["@geekmidas/client"] = GEEKMIDAS_VERSIONS["@geekmidas/client"];
|
|
11627
|
+
dependencies$1["@tanstack/react-query"] = DEPENDENCY_VERSIONS["@tanstack/react-query"];
|
|
10583
11628
|
const packageJson = {
|
|
10584
11629
|
name: packageName,
|
|
10585
11630
|
version: "0.0.1",
|
|
@@ -10617,7 +11662,7 @@ function generateSourceFiles(options, template) {
|
|
|
10617
11662
|
*/
|
|
10618
11663
|
function generateTestFiles(options, _template) {
|
|
10619
11664
|
if (!options.database) return [];
|
|
10620
|
-
const db = databaseFor(
|
|
11665
|
+
const db = databaseFor();
|
|
10621
11666
|
const declares = !options.monorepo;
|
|
10622
11667
|
const schema = declares ? "../src/constructs/database.ts" : "~/services/database.ts";
|
|
10623
11668
|
const runtimeUrl = declares ? db.urlKey : "DATABASE_URL";
|
|
@@ -10653,14 +11698,16 @@ import pg from 'pg';
|
|
|
10653
11698
|
import { wrapVitestKyselyTransaction } from '@geekmidas/testkit/kysely';
|
|
10654
11699
|
import type { Database } from '${schema}';
|
|
10655
11700
|
|
|
10656
|
-
const
|
|
11701
|
+
const db = new Kysely<Database>({
|
|
10657
11702
|
dialect: new PostgresDialect({
|
|
10658
11703
|
pool: new pg.Pool({ connectionString: process.env.${runtimeUrl} }),
|
|
10659
11704
|
}),
|
|
10660
11705
|
});
|
|
10661
11706
|
|
|
10662
11707
|
export const it = wrapVitestKyselyTransaction<Database>(itVitest, {
|
|
10663
|
-
|
|
11708
|
+
// A function, or a construct: an instance is taken for a construct and has
|
|
11709
|
+
// no service to register.
|
|
11710
|
+
connection: () => db,
|
|
10664
11711
|
});
|
|
10665
11712
|
`
|
|
10666
11713
|
},
|
|
@@ -10745,10 +11792,10 @@ export const usersBuilder = KyselyFactory.createBuilder<Database, 'users'>(
|
|
|
10745
11792
|
import { it } from './config.ts';
|
|
10746
11793
|
|
|
10747
11794
|
describe('example', () => {
|
|
10748
|
-
it('should have a working test setup', async ({
|
|
10749
|
-
//
|
|
10750
|
-
//
|
|
10751
|
-
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();
|
|
10752
11799
|
});
|
|
10753
11800
|
});
|
|
10754
11801
|
`
|
|
@@ -11999,8 +13046,8 @@ export const Alert: Story = {
|
|
|
11999
13046
|
),
|
|
12000
13047
|
};
|
|
12001
13048
|
`;
|
|
12002
|
-
const componentsUiIndex = `export { Button, type ButtonProps, buttonVariants } from './button.tsx';
|
|
12003
|
-
export { Input } from './input.tsx';
|
|
13049
|
+
const componentsUiIndex = `export { Button, type ButtonProps, buttonVariants } from './button/index.tsx';
|
|
13050
|
+
export { Input } from './input/index.tsx';
|
|
12004
13051
|
export {
|
|
12005
13052
|
Card,
|
|
12006
13053
|
CardHeader,
|
|
@@ -12008,17 +13055,17 @@ export {
|
|
|
12008
13055
|
CardTitle,
|
|
12009
13056
|
CardDescription,
|
|
12010
13057
|
CardContent,
|
|
12011
|
-
} from './card.tsx';
|
|
12012
|
-
export { Label } from './label.tsx';
|
|
12013
|
-
export { Badge, type BadgeProps, badgeVariants } from './badge.tsx';
|
|
12014
|
-
export { Separator } from './separator.tsx';
|
|
12015
|
-
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';
|
|
12016
13063
|
export {
|
|
12017
13064
|
Tooltip,
|
|
12018
13065
|
TooltipTrigger,
|
|
12019
13066
|
TooltipContent,
|
|
12020
13067
|
TooltipProvider,
|
|
12021
|
-
} from './tooltip.tsx';
|
|
13068
|
+
} from './tooltip/index.tsx';
|
|
12022
13069
|
export {
|
|
12023
13070
|
Dialog,
|
|
12024
13071
|
DialogPortal,
|
|
@@ -12030,7 +13077,7 @@ export {
|
|
|
12030
13077
|
DialogFooter,
|
|
12031
13078
|
DialogTitle,
|
|
12032
13079
|
DialogDescription,
|
|
12033
|
-
} from './dialog.tsx';
|
|
13080
|
+
} from './dialog/index.tsx';
|
|
12034
13081
|
`;
|
|
12035
13082
|
const buttonIndexTsx = buttonTsx;
|
|
12036
13083
|
const inputIndexTsx = inputTsx;
|
|
@@ -12198,8 +13245,8 @@ function generateWebAppFiles(options) {
|
|
|
12198
13245
|
[uiPackage]: "workspace:*",
|
|
12199
13246
|
"@geekmidas/client": GEEKMIDAS_VERSIONS["@geekmidas/client"],
|
|
12200
13247
|
"@geekmidas/envkit": GEEKMIDAS_VERSIONS["@geekmidas/envkit"],
|
|
12201
|
-
"@tanstack/react-query": "
|
|
12202
|
-
"better-auth": "
|
|
13248
|
+
"@tanstack/react-query": DEPENDENCY_VERSIONS["@tanstack/react-query"],
|
|
13249
|
+
"better-auth": DEPENDENCY_VERSIONS["better-auth"],
|
|
12203
13250
|
next: "~16.1.0",
|
|
12204
13251
|
react: "~19.2.0",
|
|
12205
13252
|
"react-dom": "~19.2.0"
|
|
@@ -12255,7 +13302,7 @@ export default nextConfig;
|
|
|
12255
13302
|
baseUrl: ".",
|
|
12256
13303
|
paths: {
|
|
12257
13304
|
"~/*": ["./src/*", "../../packages/ui/src/*"],
|
|
12258
|
-
[`${apiPackage}/client`]: ["../../apps/api/.gkm/openapi.ts"],
|
|
13305
|
+
[`${apiPackage}/client`]: ["../../apps/api/.gkm/openapi/api.ts"],
|
|
12259
13306
|
[`${modelsPackage}`]: ["../../packages/models/src"],
|
|
12260
13307
|
[`${modelsPackage}/*`]: ["../../packages/models/src/*"],
|
|
12261
13308
|
[`${uiPackage}`]: ["../../packages/ui/src"],
|
|
@@ -12268,12 +13315,7 @@ export default nextConfig;
|
|
|
12268
13315
|
"**/*.tsx",
|
|
12269
13316
|
".next/types/**/*.ts"
|
|
12270
13317
|
],
|
|
12271
|
-
exclude: ["node_modules"]
|
|
12272
|
-
references: [
|
|
12273
|
-
{ path: "../../apps/api" },
|
|
12274
|
-
{ path: "../../packages/ui" },
|
|
12275
|
-
{ path: "../../packages/models" }
|
|
12276
|
-
]
|
|
13318
|
+
exclude: ["node_modules"]
|
|
12277
13319
|
};
|
|
12278
13320
|
const queryClientTs = `import { QueryClient } from '@tanstack/react-query';
|
|
12279
13321
|
|
|
@@ -12536,10 +13578,10 @@ function generateTanStackWebFiles(options) {
|
|
|
12536
13578
|
[uiPackage]: "workspace:*",
|
|
12537
13579
|
"@geekmidas/client": GEEKMIDAS_VERSIONS["@geekmidas/client"],
|
|
12538
13580
|
"@geekmidas/envkit": GEEKMIDAS_VERSIONS["@geekmidas/envkit"],
|
|
12539
|
-
"@tanstack/react-query": "
|
|
13581
|
+
"@tanstack/react-query": DEPENDENCY_VERSIONS["@tanstack/react-query"],
|
|
12540
13582
|
"@tanstack/react-router": "^1.87.0",
|
|
12541
13583
|
"@tanstack/react-start": "^1.87.0",
|
|
12542
|
-
"better-auth": "
|
|
13584
|
+
"better-auth": DEPENDENCY_VERSIONS["better-auth"],
|
|
12543
13585
|
react: "~19.2.0",
|
|
12544
13586
|
"react-dom": "~19.2.0"
|
|
12545
13587
|
},
|
|
@@ -12607,7 +13649,7 @@ export default defineConfig({
|
|
|
12607
13649
|
[`${modelsPackage}/*`]: ["../../packages/models/src/*"],
|
|
12608
13650
|
[uiPackage]: ["../../packages/ui/src"],
|
|
12609
13651
|
[`${uiPackage}/*`]: ["../../packages/ui/src/*"],
|
|
12610
|
-
[`${apiPackage}/client`]: ["../../apps/api/.gkm/openapi.ts"]
|
|
13652
|
+
[`${apiPackage}/client`]: ["../../apps/api/.gkm/openapi/api.ts"]
|
|
12611
13653
|
}
|
|
12612
13654
|
},
|
|
12613
13655
|
include: [
|
|
@@ -12897,12 +13939,27 @@ function detectPackageManager(cwd = process.cwd()) {
|
|
|
12897
13939
|
if (userAgent.includes("bun")) return "bun";
|
|
12898
13940
|
return "npm";
|
|
12899
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
|
+
];
|
|
12900
13952
|
/**
|
|
12901
13953
|
* Validate project name for npm package naming conventions
|
|
12902
13954
|
*/
|
|
12903
13955
|
function validateProjectName(name$1) {
|
|
12904
13956
|
if (!name$1) return "Project name is required";
|
|
12905
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
|
+
}
|
|
12906
13963
|
const reserved = [
|
|
12907
13964
|
"node_modules",
|
|
12908
13965
|
".git",
|
|
@@ -12942,6 +13999,18 @@ function getRunCommand(pkgManager, script) {
|
|
|
12942
13999
|
default: return `npm run ${script}`;
|
|
12943
14000
|
}
|
|
12944
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
|
+
}
|
|
12945
14014
|
const lockfileByPm = {
|
|
12946
14015
|
pnpm: "pnpm-lock.yaml",
|
|
12947
14016
|
yarn: "yarn.lock",
|
|
@@ -13012,12 +14081,31 @@ function findWorkspacePackages(cwd, pm) {
|
|
|
13012
14081
|
|
|
13013
14082
|
//#endregion
|
|
13014
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$/;
|
|
13015
14088
|
/**
|
|
13016
14089
|
* Main init command - scaffolds a new project
|
|
13017
14090
|
*/
|
|
13018
14091
|
async function initCommand(projectName, options = {}) {
|
|
13019
14092
|
const cwd = process.cwd();
|
|
13020
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 = [];
|
|
13021
14109
|
prompts.default.override({});
|
|
13022
14110
|
const onCancel = () => {
|
|
13023
14111
|
process.exit(0);
|
|
@@ -13068,12 +14156,49 @@ async function initCommand(projectName, options = {}) {
|
|
|
13068
14156
|
initial: packageManagerChoices.findIndex((c) => c.value === detectedPkgManager)
|
|
13069
14157
|
},
|
|
13070
14158
|
{
|
|
13071
|
-
type: options.yes ? null : "select",
|
|
14159
|
+
type: options.yes || options.deploy ? null : "select",
|
|
13072
14160
|
name: "deployTarget",
|
|
13073
14161
|
message: "Deployment target:",
|
|
13074
14162
|
choices: deployTargetChoices,
|
|
13075
14163
|
initial: 0
|
|
13076
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
|
+
},
|
|
13077
14202
|
{
|
|
13078
14203
|
type: options.yes ? null : "confirm",
|
|
13079
14204
|
name: "telescope",
|
|
@@ -13139,7 +14264,7 @@ async function initCommand(projectName, options = {}) {
|
|
|
13139
14264
|
};
|
|
13140
14265
|
if (services.events === "pgboss") services.db = true;
|
|
13141
14266
|
const pkgManager = options.pm ? options.pm : options.yes ? "pnpm" : answers.packageManager ?? detectedPkgManager;
|
|
13142
|
-
const deployTarget = options.yes ? "
|
|
14267
|
+
const deployTarget = options.deploy ?? (options.yes ? "none" : answers.deployTarget ?? "none");
|
|
13143
14268
|
const database = services.db;
|
|
13144
14269
|
const frontendFramework = isFullstack ? options.yes ? "nextjs" : answers.frontendFramework ?? "nextjs" : void 0;
|
|
13145
14270
|
const templateOptions = {
|
|
@@ -13154,6 +14279,8 @@ async function initCommand(projectName, options = {}) {
|
|
|
13154
14279
|
apiPath: monorepo ? options.apiPath ?? "apps/api" : "",
|
|
13155
14280
|
packageManager: pkgManager,
|
|
13156
14281
|
deployTarget,
|
|
14282
|
+
stages: resolveStages(options, answers),
|
|
14283
|
+
...deployTarget === "sst" ? { region: options.region ?? (options.yes ? "eu-west-1" : answers.region?.trim()) } : {},
|
|
13157
14284
|
services,
|
|
13158
14285
|
frontendFramework
|
|
13159
14286
|
};
|
|
@@ -13183,6 +14310,8 @@ async function initCommand(projectName, options = {}) {
|
|
|
13183
14310
|
] : [];
|
|
13184
14311
|
const dockerFiles = isMonorepo$1 && baseTemplate ? generateDockerFiles(templateOptions, baseTemplate, dbApps) : [];
|
|
13185
14312
|
const rootFiles = baseTemplate ? [
|
|
14313
|
+
...generateAgentFiles(templateOptions, baseTemplate),
|
|
14314
|
+
...generateGithubFiles(templateOptions),
|
|
13186
14315
|
...generateMonorepoFiles(templateOptions, baseTemplate),
|
|
13187
14316
|
...generateRootConstructs(templateOptions),
|
|
13188
14317
|
...generateModelsPackage(templateOptions)
|
|
@@ -13228,12 +14357,12 @@ async function initCommand(projectName, options = {}) {
|
|
|
13228
14357
|
if (services.mail) secretServices.push("mailpit");
|
|
13229
14358
|
if (services.events === "sns") secretServices.push("localstack");
|
|
13230
14359
|
if (services.events === "rabbitmq") secretServices.push("rabbitmq");
|
|
13231
|
-
const
|
|
14360
|
+
const local = templateOptions.stages.local;
|
|
14361
|
+
const devSecrets = require_generator.createStageSecrets(local, secretServices, {
|
|
13232
14362
|
projectName: name$1,
|
|
13233
14363
|
eventsBackend: services.events
|
|
13234
14364
|
});
|
|
13235
14365
|
const customSecrets = {
|
|
13236
|
-
NODE_ENV: "development",
|
|
13237
14366
|
PORT: "3000",
|
|
13238
14367
|
LOG_LEVEL: "debug",
|
|
13239
14368
|
JWT_SECRET: `dev-${Date.now()}-${Math.random().toString(36).slice(2)}`
|
|
@@ -13253,8 +14382,8 @@ async function initCommand(projectName, options = {}) {
|
|
|
13253
14382
|
}
|
|
13254
14383
|
devSecrets.custom = customSecrets;
|
|
13255
14384
|
await require_storage.writeStageSecrets(devSecrets, targetDir);
|
|
13256
|
-
const keyPath = require_storage.getKeyPath(
|
|
13257
|
-
console.log(` Secrets: .gkm/secrets
|
|
14385
|
+
const keyPath = require_storage.getKeyPath(local, name$1);
|
|
14386
|
+
console.log(` Secrets: .gkm/secrets/${local}.json (encrypted)`);
|
|
13258
14387
|
console.log(` Key: ${keyPath}\n`);
|
|
13259
14388
|
if (!options.skipInstall) {
|
|
13260
14389
|
console.log("\n📦 Installing dependencies...\n");
|
|
@@ -13267,11 +14396,13 @@ async function initCommand(projectName, options = {}) {
|
|
|
13267
14396
|
console.error("Failed to install dependencies");
|
|
13268
14397
|
}
|
|
13269
14398
|
try {
|
|
13270
|
-
(0, node_child_process.execSync)("
|
|
14399
|
+
(0, node_child_process.execSync)(getExecCommand(pkgManager, "biome check --write ."), {
|
|
13271
14400
|
cwd: targetDir,
|
|
13272
14401
|
stdio: "inherit"
|
|
13273
14402
|
});
|
|
13274
|
-
} catch {
|
|
14403
|
+
} catch {
|
|
14404
|
+
console.warn(`\n⚠️ Formatting did not finish cleanly. Run ${getRunCommand(pkgManager, "fmt")} to see why.`);
|
|
14405
|
+
}
|
|
13275
14406
|
}
|
|
13276
14407
|
console.log("\n📦 Initializing git repository...\n");
|
|
13277
14408
|
try {
|
|
@@ -13343,19 +14474,46 @@ function printNextSteps(projectName, options, pkgManager) {
|
|
|
13343
14474
|
console.log(` └── turbo.json # Turbo config`);
|
|
13344
14475
|
console.log("");
|
|
13345
14476
|
}
|
|
14477
|
+
const { local, deployed } = options.stages;
|
|
13346
14478
|
console.log("🔐 Secrets management:");
|
|
13347
|
-
console.log(` gkm secrets:show --stage
|
|
13348
|
-
console.log(` gkm secrets:set KEY VALUE --stage
|
|
13349
|
-
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`);
|
|
13350
14482
|
console.log("");
|
|
13351
14483
|
if (options.deployTarget === "dokploy") {
|
|
13352
14484
|
console.log("🚀 Deployment:");
|
|
13353
|
-
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.");
|
|
13354
14493
|
console.log("");
|
|
13355
14494
|
}
|
|
13356
14495
|
console.log("📚 Documentation: https://geekmidas.github.io/toolbox/");
|
|
13357
14496
|
console.log("");
|
|
13358
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
|
+
}
|
|
13359
14517
|
|
|
13360
14518
|
//#endregion
|
|
13361
14519
|
//#region src/secrets/index.ts
|
|
@@ -13566,19 +14724,20 @@ function maskUrl(url) {
|
|
|
13566
14724
|
* Environment variables are sniffed to inject only what the app needs.
|
|
13567
14725
|
*/
|
|
13568
14726
|
async function testCommand(options = {}) {
|
|
13569
|
-
const stage = options.stage ?? "development";
|
|
13570
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.");
|
|
13571
14730
|
console.log(`\n🧪 Running tests with ${stage} environment...\n`);
|
|
13572
14731
|
const autoSetup = options.autoSetup || Boolean(process.env.GKM_AUTO_SETUP);
|
|
13573
14732
|
if (autoSetup) {
|
|
13574
|
-
const { ensureStageSecrets } = await Promise.resolve().then(() => require("./setup-
|
|
14733
|
+
const { ensureStageSecrets } = await Promise.resolve().then(() => require("./setup-BsZvZP6f.cjs"));
|
|
13575
14734
|
const generated = await ensureStageSecrets(stage, cwd);
|
|
13576
14735
|
if (generated) console.log(` 🔐 Generated fresh ${stage} secrets (auto-setup)`);
|
|
13577
14736
|
}
|
|
13578
14737
|
const defaultEnv = require_credentials$1.loadEnvFiles(".env");
|
|
13579
14738
|
if (defaultEnv.loaded.length > 0) console.log(` 📦 Loaded env: ${defaultEnv.loaded.join(", ")}`);
|
|
13580
14739
|
const result = await require_credentials$1.prepareEntryCredentials({
|
|
13581
|
-
|
|
14740
|
+
stage,
|
|
13582
14741
|
startDocker: true,
|
|
13583
14742
|
secretsFileName: "test-secrets.json",
|
|
13584
14743
|
resolveDockerPorts: "full",
|
|
@@ -13659,145 +14818,305 @@ function rewriteDatabaseUrlForTests(env) {
|
|
|
13659
14818
|
return result;
|
|
13660
14819
|
}
|
|
13661
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
|
+
|
|
13662
14880
|
//#endregion
|
|
13663
14881
|
//#region src/upgrade/index.ts
|
|
13664
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
|
+
}
|
|
13665
14954
|
async function upgradeCommand(options = {}) {
|
|
13666
14955
|
const cwd = process.cwd();
|
|
13667
|
-
logger.log("\n📦 Scanning workspace for @geekmidas packages...\n");
|
|
13668
14956
|
const pm = detectPackageManager(cwd);
|
|
14957
|
+
const root = findWorkspaceRoot(cwd, pm);
|
|
14958
|
+
logger.log("\n📦 Scanning workspace for @geekmidas packages...\n");
|
|
13669
14959
|
logger.log(` Package manager: ${pm}`);
|
|
13670
|
-
const
|
|
13671
|
-
|
|
13672
|
-
|
|
13673
|
-
|
|
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) {
|
|
13674
14965
|
logger.log(" No @geekmidas packages found.\n");
|
|
13675
14966
|
return;
|
|
13676
14967
|
}
|
|
13677
|
-
const
|
|
13678
|
-
|
|
13679
|
-
const
|
|
13680
|
-
|
|
13681
|
-
|
|
13682
|
-
|
|
13683
|
-
|
|
13684
|
-
|
|
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");
|
|
13685
14990
|
return;
|
|
13686
14991
|
}
|
|
13687
|
-
|
|
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.");
|
|
13688
14994
|
if (options.dryRun) {
|
|
13689
|
-
logger.log(" --dry-run:
|
|
13690
|
-
printUpgradeCommands(upgradable, pm);
|
|
14995
|
+
logger.log("\n --dry-run: no changes made.\n");
|
|
13691
14996
|
return;
|
|
13692
14997
|
}
|
|
13693
|
-
|
|
13694
|
-
|
|
14998
|
+
applyChanges(changes);
|
|
14999
|
+
install(pm, root);
|
|
15000
|
+
logger.log("\n ✅ Upgrade complete. Run your tests to verify.\n");
|
|
13695
15001
|
}
|
|
13696
|
-
|
|
13697
|
-
|
|
13698
|
-
|
|
13699
|
-
|
|
13700
|
-
|
|
13701
|
-
|
|
13702
|
-
|
|
13703
|
-
|
|
13704
|
-
|
|
13705
|
-
|
|
13706
|
-
|
|
13707
|
-
|
|
13708
|
-
|
|
13709
|
-
|
|
13710
|
-
|
|
13711
|
-
|
|
13712
|
-
|
|
13713
|
-
|
|
13714
|
-
|
|
13715
|
-
|
|
13716
|
-
|
|
13717
|
-
|
|
13718
|
-
|
|
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;
|
|
13719
15032
|
}
|
|
15033
|
+
if (compareVersions(current.version, floor) < 0) changes.push({
|
|
15034
|
+
...o,
|
|
15035
|
+
next: withVersion(o.range, floor)
|
|
15036
|
+
});
|
|
13720
15037
|
}
|
|
13721
|
-
return
|
|
13722
|
-
}
|
|
13723
|
-
|
|
13724
|
-
|
|
13725
|
-
const
|
|
13726
|
-
const
|
|
13727
|
-
|
|
13728
|
-
|
|
13729
|
-
|
|
13730
|
-
|
|
13731
|
-
|
|
13732
|
-
|
|
13733
|
-
|
|
13734
|
-
|
|
13735
|
-
|
|
13736
|
-
|
|
13737
|
-
|
|
13738
|
-
|
|
13739
|
-
|
|
13740
|
-
|
|
13741
|
-
|
|
13742
|
-
|
|
13743
|
-
|
|
13744
|
-
|
|
13745
|
-
|
|
13746
|
-
|
|
13747
|
-
|
|
13748
|
-
|
|
13749
|
-
|
|
13750
|
-
|
|
13751
|
-
|
|
13752
|
-
const nameWidth = 33;
|
|
13753
|
-
const verWidth = 14;
|
|
13754
|
-
const statusWidth = 14;
|
|
13755
|
-
const hr = ` ${"─".repeat(nameWidth + verWidth * 2 + statusWidth + 5)}`;
|
|
13756
|
-
logger.log(hr);
|
|
13757
|
-
logger.log(` ${"Package".padEnd(nameWidth)} ${"Current".padEnd(verWidth)} ${"Latest".padEnd(verWidth)} ${"Status".padEnd(statusWidth)}`);
|
|
13758
|
-
logger.log(hr);
|
|
13759
|
-
for (const [, info] of byPackage) {
|
|
13760
|
-
const name$1 = info.packageName.padEnd(nameWidth);
|
|
13761
|
-
const current = info.currentVersion.padEnd(verWidth);
|
|
13762
|
-
const latest = info.latestVersion.padEnd(verWidth);
|
|
13763
|
-
let status;
|
|
13764
|
-
if (info.isWorkspaceRef) status = "workspace";
|
|
13765
|
-
else if (info.needsUpgrade) status = "⬆ upgrade";
|
|
13766
|
-
else status = "✓ up-to-date";
|
|
13767
|
-
logger.log(` ${name$1} ${current} ${latest} ${status}`);
|
|
13768
|
-
}
|
|
13769
|
-
logger.log(hr);
|
|
13770
|
-
}
|
|
13771
|
-
function printUpgradeCommands(upgradable, pm) {
|
|
13772
|
-
logger.log(" Commands that would be run:\n");
|
|
13773
|
-
const uniquePackages = [...new Set(upgradable.map((i) => i.packageName))];
|
|
13774
|
-
const cmd = getWorkspaceUpgradeCommand(pm, uniquePackages);
|
|
13775
|
-
logger.log(` ${cmd}\n`);
|
|
13776
|
-
}
|
|
13777
|
-
function getWorkspaceUpgradeCommand(pm, packages) {
|
|
13778
|
-
const pkgList = packages.join(" ");
|
|
13779
|
-
switch (pm) {
|
|
13780
|
-
case "pnpm": return `pnpm update -r ${pkgList} --latest`;
|
|
13781
|
-
case "yarn": return `yarn upgrade ${pkgList}`;
|
|
13782
|
-
case "bun": return `bun update ${pkgList}`;
|
|
13783
|
-
case "npm": return `npm update ${pkgList} --workspaces`;
|
|
13784
|
-
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`);
|
|
13785
15069
|
}
|
|
13786
15070
|
}
|
|
13787
|
-
function
|
|
13788
|
-
const
|
|
13789
|
-
|
|
13790
|
-
logger.log(` Running: ${cmd}\n`);
|
|
15071
|
+
function install(pm, cwd) {
|
|
15072
|
+
const command = getInstallCommand(pm);
|
|
15073
|
+
logger.log(`\n Running: ${command}\n`);
|
|
13791
15074
|
try {
|
|
13792
|
-
(0, node_child_process.execSync)(
|
|
15075
|
+
(0, node_child_process.execSync)(command, {
|
|
13793
15076
|
cwd,
|
|
13794
15077
|
stdio: "inherit",
|
|
13795
|
-
timeout:
|
|
15078
|
+
timeout: 3e5
|
|
13796
15079
|
});
|
|
13797
15080
|
} catch {
|
|
13798
|
-
throw new
|
|
15081
|
+
throw new InstallFailed(command);
|
|
13799
15082
|
}
|
|
13800
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
|
+
};
|
|
13801
15120
|
|
|
13802
15121
|
//#endregion
|
|
13803
15122
|
//#region src/index.ts
|
|
@@ -13806,7 +15125,7 @@ program.name("gkm").description("GeekMidas backend framework CLI").version(packa
|
|
|
13806
15125
|
const opts = program.opts();
|
|
13807
15126
|
if (opts.debug) enableDebug();
|
|
13808
15127
|
});
|
|
13809
|
-
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) => {
|
|
13810
15129
|
try {
|
|
13811
15130
|
const globalOptions = program.opts();
|
|
13812
15131
|
if (globalOptions.cwd) process.chdir(globalOptions.cwd);
|
|
@@ -13826,7 +15145,7 @@ program.command("trust").description("Trust the local edge's certificate authori
|
|
|
13826
15145
|
process.exit(1);
|
|
13827
15146
|
}
|
|
13828
15147
|
});
|
|
13829
|
-
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) => {
|
|
13830
15149
|
try {
|
|
13831
15150
|
const globalOptions = program.opts();
|
|
13832
15151
|
if (globalOptions.cwd) process.chdir(globalOptions.cwd);
|
|
@@ -13898,7 +15217,7 @@ program.command("exec").description("Run a command with secrets injected into Cr
|
|
|
13898
15217
|
process.exit(1);
|
|
13899
15218
|
}
|
|
13900
15219
|
});
|
|
13901
|
-
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) => {
|
|
13902
15221
|
try {
|
|
13903
15222
|
const globalOptions = program.opts();
|
|
13904
15223
|
if (globalOptions.cwd) process.chdir(globalOptions.cwd);
|
|
@@ -14046,7 +15365,7 @@ program.command("secrets:push").description("Push secrets to remote provider (SS
|
|
|
14046
15365
|
const { workspace } = await loadWorkspaceConfig$1();
|
|
14047
15366
|
const secrets = await readStageSecrets$1(options.stage, workspace.root);
|
|
14048
15367
|
if (secrets) {
|
|
14049
|
-
const { derivedContainers: derivedContainers$1 } = await Promise.resolve().then(() => require("./workspace-
|
|
15368
|
+
const { derivedContainers: derivedContainers$1 } = await Promise.resolve().then(() => require("./workspace-XXtNhqJl.cjs"));
|
|
14050
15369
|
const result = reconcileMissingSecrets(secrets, workspace, await derivedContainers$1(workspace, options.stage));
|
|
14051
15370
|
if (result) {
|
|
14052
15371
|
await writeStageSecrets$1(result.secrets, workspace.root);
|
|
@@ -14075,7 +15394,7 @@ program.command("secrets:pull").description("Pull secrets from remote provider (
|
|
|
14075
15394
|
console.error(`No remote secrets found for stage "${options.stage}".`);
|
|
14076
15395
|
process.exit(1);
|
|
14077
15396
|
}
|
|
14078
|
-
const { derivedContainers: derivedContainers$1 } = await Promise.resolve().then(() => require("./workspace-
|
|
15397
|
+
const { derivedContainers: derivedContainers$1 } = await Promise.resolve().then(() => require("./workspace-XXtNhqJl.cjs"));
|
|
14079
15398
|
const result = reconcileMissingSecrets(secrets, workspace, await derivedContainers$1(workspace, options.stage));
|
|
14080
15399
|
if (result) {
|
|
14081
15400
|
secrets = result.secrets;
|
|
@@ -14089,7 +15408,7 @@ program.command("secrets:pull").description("Pull secrets from remote provider (
|
|
|
14089
15408
|
process.exit(1);
|
|
14090
15409
|
}
|
|
14091
15410
|
});
|
|
14092
|
-
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) => {
|
|
14093
15412
|
try {
|
|
14094
15413
|
const globalOptions = program.opts();
|
|
14095
15414
|
if (globalOptions.cwd) process.chdir(globalOptions.cwd);
|
|
@@ -14097,19 +15416,20 @@ program.command("secrets:reconcile").description("Backfill missing custom secret
|
|
|
14097
15416
|
const { reconcileMissingSecrets } = await Promise.resolve().then(() => require("./reconcile-D0pimNiy.cjs"));
|
|
14098
15417
|
const { readStageSecrets: readStageSecrets$1, writeStageSecrets: writeStageSecrets$1 } = await Promise.resolve().then(() => require("./storage-BM9hGpgv.cjs"));
|
|
14099
15418
|
const { workspace } = await loadWorkspaceConfig$1();
|
|
14100
|
-
const
|
|
15419
|
+
const stage = options.stage ?? workspace.stages.local;
|
|
15420
|
+
const secrets = await readStageSecrets$1(stage, workspace.root);
|
|
14101
15421
|
if (!secrets) {
|
|
14102
|
-
console.error(`No secrets found for stage "${
|
|
15422
|
+
console.error(`No secrets found for stage "${stage}". Run "gkm secrets:init --stage ${stage}" first.`);
|
|
14103
15423
|
process.exit(1);
|
|
14104
15424
|
}
|
|
14105
|
-
const { derivedContainers: derivedContainers$1 } = await Promise.resolve().then(() => require("./workspace-
|
|
14106
|
-
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));
|
|
14107
15427
|
if (!result) {
|
|
14108
|
-
console.log(`\n✓ Secrets for stage "${
|
|
15428
|
+
console.log(`\n✓ Secrets for stage "${stage}" are up-to-date`);
|
|
14109
15429
|
return;
|
|
14110
15430
|
}
|
|
14111
15431
|
await writeStageSecrets$1(result.secrets, workspace.root);
|
|
14112
|
-
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}":`);
|
|
14113
15433
|
for (const key of result.addedKeys) console.log(` + ${key}`);
|
|
14114
15434
|
} catch (error) {
|
|
14115
15435
|
console.error(formatError(error));
|
|
@@ -14263,7 +15583,7 @@ program.command("state:diff").description("Compare local and remote deployment s
|
|
|
14263
15583
|
process.exit(1);
|
|
14264
15584
|
}
|
|
14265
15585
|
});
|
|
14266
|
-
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) => {
|
|
14267
15587
|
try {
|
|
14268
15588
|
const globalOptions = program.opts();
|
|
14269
15589
|
if (globalOptions.cwd) process.chdir(globalOptions.cwd);
|