@geekmidas/cli 10.0.0-alpha.1 → 10.0.0-alpha.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{config-DbPE1KXF.cjs → config-BholeD-0.cjs} +5 -5
- package/dist/config-BholeD-0.cjs.map +1 -0
- package/dist/{config-D8o6I_7Z.mjs → config-CzWnfcXM.mjs} +5 -5
- package/dist/config-CzWnfcXM.mjs.map +1 -0
- package/dist/config.cjs +1 -1
- package/dist/config.mjs +1 -1
- package/dist/{credentials-Dh0Q6HzY.mjs → credentials-BZcp3CGx.mjs} +3 -3
- package/dist/{credentials-Dh0Q6HzY.mjs.map → credentials-BZcp3CGx.mjs.map} +1 -1
- package/dist/{credentials-K961ThG3.cjs → credentials-DQmbRnr1.cjs} +3 -3
- package/dist/{credentials-K961ThG3.cjs.map → credentials-DQmbRnr1.cjs.map} +1 -1
- package/dist/index.cjs +469 -132
- package/dist/index.cjs.map +1 -1
- package/dist/index.mjs +469 -132
- package/dist/index.mjs.map +1 -1
- package/dist/{openapi-CgpMWMmM.mjs → openapi-CY9xFkTH.mjs} +2 -2
- package/dist/{openapi-CgpMWMmM.mjs.map → openapi-CY9xFkTH.mjs.map} +1 -1
- package/dist/{openapi-CljONd_M.cjs → openapi-c9bpieI5.cjs} +2 -2
- package/dist/{openapi-CljONd_M.cjs.map → openapi-c9bpieI5.cjs.map} +1 -1
- package/dist/openapi.cjs +2 -2
- package/dist/openapi.mjs +2 -2
- package/dist/{setup-DOsYueeQ.mjs → setup-Bc73t0ve.mjs} +3 -3
- package/dist/{setup-DOsYueeQ.mjs.map → setup-Bc73t0ve.mjs.map} +1 -1
- package/dist/{setup-CJiQM2pX.cjs → setup-C48C9hto.cjs} +3 -3
- package/dist/{setup-CJiQM2pX.cjs.map → setup-C48C9hto.cjs.map} +1 -1
- package/dist/{setup-DSutDAlt.mjs → setup-DCuudnCl.mjs} +3 -3
- package/dist/{setup-DMtU3p41.cjs → setup-fG_ivuxO.cjs} +3 -3
- package/dist/{workspace-8acupeaI.cjs → workspace-BO_KWohI.cjs} +2 -2
- package/dist/{workspace-yP5AHnrn.mjs → workspace-DK4WWJIn.mjs} +2 -2
- package/package.json +11 -11
- package/dist/config-D8o6I_7Z.mjs.map +0 -1
- package/dist/config-DbPE1KXF.cjs.map +0 -1
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-
|
|
4
|
+
const require_config = require('./config-BholeD-0.cjs');
|
|
5
5
|
const require_workspace = require('./workspace-XvqO9jIv.cjs');
|
|
6
6
|
const require_credentials = require('./credentials-BapzjyV5.cjs');
|
|
7
|
-
const require_credentials$1 = require('./credentials-
|
|
7
|
+
const require_credentials$1 = require('./credentials-DQmbRnr1.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-c9bpieI5.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-C48C9hto.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.2";
|
|
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";
|
|
@@ -443,7 +443,7 @@ var CronGenerator = class extends require_discover.ConstructGenerator {
|
|
|
443
443
|
const importPath = relativePath.replace(/\.ts$/, ".js");
|
|
444
444
|
const relativeEnvParserPath = (0, node_path.relative)((0, node_path.dirname)(handlerPath), context.envParserPath);
|
|
445
445
|
const relativeLoggerPath = (0, node_path.relative)((0, node_path.dirname)(handlerPath), context.loggerPath);
|
|
446
|
-
const content = `import { AWSScheduledFunction } from '@geekmidas/constructs/
|
|
446
|
+
const content = `import { AWSScheduledFunction } from '@geekmidas/constructs/aws';
|
|
447
447
|
import { ${exportName} } from '${importPath}';
|
|
448
448
|
import ${context.envParserImportPattern} from '${relativeEnvParserPath}';
|
|
449
449
|
import ${context.loggerImportPattern} from '${relativeLoggerPath}';
|
|
@@ -7342,132 +7342,16 @@ function printStateDetails(state) {
|
|
|
7342
7342
|
}
|
|
7343
7343
|
}
|
|
7344
7344
|
|
|
7345
|
-
//#endregion
|
|
7346
|
-
//#region src/init/versions.ts
|
|
7347
|
-
const require$1 = (0, node_module.createRequire)(require("url").pathToFileURL(__filename).href);
|
|
7348
|
-
function loadPackageJson() {
|
|
7349
|
-
try {
|
|
7350
|
-
return require$1("../package.json");
|
|
7351
|
-
} catch {
|
|
7352
|
-
return require$1("../../package.json");
|
|
7353
|
-
}
|
|
7354
|
-
}
|
|
7355
|
-
const pkg = loadPackageJson();
|
|
7356
|
-
/**
|
|
7357
|
-
* CLI version resolved from package.json at runtime
|
|
7358
|
-
*/
|
|
7359
|
-
const CLI_VERSION = `~${pkg.version}`;
|
|
7360
|
-
/**
|
|
7361
|
-
* Package versions for @geekmidas packages
|
|
7362
|
-
*
|
|
7363
|
-
* AUTO-GENERATED (except CLI) - Do not edit manually
|
|
7364
|
-
* Run: pnpm --filter @geekmidas/cli sync-versions
|
|
7365
|
-
*/
|
|
7366
|
-
const GEEKMIDAS_VERSIONS = {
|
|
7367
|
-
"@geekmidas/audit": "~10.0.0-alpha.0",
|
|
7368
|
-
"@geekmidas/auth": "~10.0.0-alpha.0",
|
|
7369
|
-
"@geekmidas/cache": "~10.0.0-alpha.0",
|
|
7370
|
-
"@geekmidas/client": "~10.0.0-alpha.0",
|
|
7371
|
-
"@geekmidas/cloud": "~10.0.0-alpha.0",
|
|
7372
|
-
"@geekmidas/constructs": "~10.0.0-alpha.0",
|
|
7373
|
-
"@geekmidas/db": "~10.0.0-alpha.0",
|
|
7374
|
-
"@geekmidas/emailkit": "~10.0.0-alpha.0",
|
|
7375
|
-
"@geekmidas/envkit": "~10.0.0-alpha.0",
|
|
7376
|
-
"@geekmidas/errors": "~10.0.0-alpha.0",
|
|
7377
|
-
"@geekmidas/events": "~10.0.0-alpha.0",
|
|
7378
|
-
"@geekmidas/logger": "~10.0.0-alpha.0",
|
|
7379
|
-
"@geekmidas/manifest": "~10.0.0-alpha.0",
|
|
7380
|
-
"@geekmidas/rate-limit": "~10.0.0-alpha.0",
|
|
7381
|
-
"@geekmidas/schema": "~10.0.0-alpha.0",
|
|
7382
|
-
"@geekmidas/services": "~10.0.0-alpha.0",
|
|
7383
|
-
"@geekmidas/storage": "~10.0.0-alpha.0",
|
|
7384
|
-
"@geekmidas/studio": "~10.0.0-alpha.0",
|
|
7385
|
-
"@geekmidas/telescope": "~10.0.0-alpha.0",
|
|
7386
|
-
"@geekmidas/testkit": "~10.0.0-alpha.0",
|
|
7387
|
-
"@geekmidas/cli": CLI_VERSION
|
|
7388
|
-
};
|
|
7389
|
-
|
|
7390
|
-
//#endregion
|
|
7391
|
-
//#region src/init/generators/auth.ts
|
|
7392
|
-
/**
|
|
7393
|
-
* Generate auth app files for fullstack template
|
|
7394
|
-
* Uses better-auth with magic link authentication
|
|
7395
|
-
*/
|
|
7396
|
-
function generateAuthAppFiles(options) {
|
|
7397
|
-
if (!options.monorepo || options.template !== "fullstack") return [];
|
|
7398
|
-
const packageName = `@${options.name}/auth`;
|
|
7399
|
-
const modelsPackage = `@${options.name}/models`;
|
|
7400
|
-
const packageJson = {
|
|
7401
|
-
name: packageName,
|
|
7402
|
-
version: "0.0.1",
|
|
7403
|
-
private: true,
|
|
7404
|
-
type: "module",
|
|
7405
|
-
scripts: {
|
|
7406
|
-
dev: "gkm dev --entry ./src/index.ts",
|
|
7407
|
-
build: "tsc",
|
|
7408
|
-
start: "node dist/index.js",
|
|
7409
|
-
typecheck: "tsc --noEmit",
|
|
7410
|
-
"db:migrate": "gkm exec -- npx @better-auth/cli migrate",
|
|
7411
|
-
"db:generate": "gkm exec -- npx @better-auth/cli generate"
|
|
7412
|
-
},
|
|
7413
|
-
dependencies: {
|
|
7414
|
-
[modelsPackage]: "workspace:*",
|
|
7415
|
-
"@geekmidas/envkit": GEEKMIDAS_VERSIONS["@geekmidas/envkit"],
|
|
7416
|
-
"@geekmidas/logger": GEEKMIDAS_VERSIONS["@geekmidas/logger"],
|
|
7417
|
-
"@hono/node-server": "~1.13.0",
|
|
7418
|
-
"better-auth": "~1.2.0",
|
|
7419
|
-
hono: "~4.8.0",
|
|
7420
|
-
kysely: "~0.27.0",
|
|
7421
|
-
pg: "~8.13.0"
|
|
7422
|
-
},
|
|
7423
|
-
devDependencies: {
|
|
7424
|
-
"@geekmidas/cli": GEEKMIDAS_VERSIONS["@geekmidas/cli"],
|
|
7425
|
-
"@types/node": "~22.0.0",
|
|
7426
|
-
"@types/pg": "~8.11.0",
|
|
7427
|
-
tsx: "~4.20.0",
|
|
7428
|
-
typescript: "~5.8.2"
|
|
7429
|
-
}
|
|
7430
|
-
};
|
|
7431
|
-
const tsConfig = {
|
|
7432
|
-
extends: "../../tsconfig.json",
|
|
7433
|
-
compilerOptions: {
|
|
7434
|
-
noEmit: true,
|
|
7435
|
-
allowImportingTsExtensions: true,
|
|
7436
|
-
baseUrl: ".",
|
|
7437
|
-
paths: {
|
|
7438
|
-
"~/*": ["./src/*"],
|
|
7439
|
-
[`@${options.name}/*`]: ["../../packages/*/src"]
|
|
7440
|
-
}
|
|
7441
|
-
},
|
|
7442
|
-
include: ["src/**/*.ts"],
|
|
7443
|
-
exclude: ["node_modules", "dist"]
|
|
7444
|
-
};
|
|
7445
|
-
const gitignore = `node_modules/
|
|
7446
|
-
dist/
|
|
7447
|
-
.env.local
|
|
7448
|
-
*.log
|
|
7449
|
-
`;
|
|
7450
|
-
return [
|
|
7451
|
-
{
|
|
7452
|
-
path: "apps/auth/package.json",
|
|
7453
|
-
content: `${JSON.stringify(packageJson, null, 2)}\n`
|
|
7454
|
-
},
|
|
7455
|
-
{
|
|
7456
|
-
path: "apps/auth/tsconfig.json",
|
|
7457
|
-
content: `${JSON.stringify(tsConfig, null, 2)}\n`
|
|
7458
|
-
},
|
|
7459
|
-
{
|
|
7460
|
-
path: "apps/auth/.gitignore",
|
|
7461
|
-
content: gitignore
|
|
7462
|
-
}
|
|
7463
|
-
];
|
|
7464
|
-
}
|
|
7465
|
-
|
|
7466
7345
|
//#endregion
|
|
7467
7346
|
//#region src/init/constructs.ts
|
|
7468
7347
|
/** The glob every generated config points at. One glob, every kind. */
|
|
7469
7348
|
const CONSTRUCTS_GLOB = "./src/constructs/**/*.ts";
|
|
7470
7349
|
/**
|
|
7350
|
+
* Where a workspace keeps its constructs: at the root, beside the apps that
|
|
7351
|
+
* share them, rather than inside any one of them.
|
|
7352
|
+
*/
|
|
7353
|
+
const WORKSPACE_CONSTRUCTS_GLOB = "./constructs/**/*.ts";
|
|
7354
|
+
/**
|
|
7471
7355
|
* The database a project named `name` declares.
|
|
7472
7356
|
*
|
|
7473
7357
|
* Named after the project rather than `Db`, so two apps in one workspace can
|
|
@@ -7577,6 +7461,458 @@ export async function down(db: Kysely<unknown>): Promise<void> {
|
|
|
7577
7461
|
}];
|
|
7578
7462
|
}
|
|
7579
7463
|
|
|
7464
|
+
//#endregion
|
|
7465
|
+
//#region src/init/generators/agents.ts
|
|
7466
|
+
/**
|
|
7467
|
+
* `AGENTS.md` and `CLAUDE.md` for a scaffolded project.
|
|
7468
|
+
*
|
|
7469
|
+
* A generated project is the first thing an agent sees, and without this it
|
|
7470
|
+
* sees only the output: a `gkm.config.ts` naming three keys, a `constructs/`
|
|
7471
|
+
* directory, and a build step that reads both. None of that says what the
|
|
7472
|
+
* shape *is*, so an agent reconstructs it from the framework it recognises —
|
|
7473
|
+
* and writes an Express app beside a `RestApi`, or restates in config what a
|
|
7474
|
+
* construct already declared, which is the exact mistake v10 exists to make
|
|
7475
|
+
* impossible.
|
|
7476
|
+
*
|
|
7477
|
+
* Instructions live in `AGENTS.md` because that is the file every tool reads.
|
|
7478
|
+
* `CLAUDE.md` points at it rather than copying it: two files saying the same
|
|
7479
|
+
* thing is two files to keep in step, and the copy that drifts is always the
|
|
7480
|
+
* one nobody opened.
|
|
7481
|
+
*
|
|
7482
|
+
* Content is derived from the same `TemplateOptions` the rest of the scaffold
|
|
7483
|
+
* is built from, so a project with no database gets no database section, and a
|
|
7484
|
+
* worker gets no endpoint guide. A guide that documents what was not generated
|
|
7485
|
+
* teaches the reader to distrust it.
|
|
7486
|
+
*/
|
|
7487
|
+
function generateAgentFiles(options, template) {
|
|
7488
|
+
return [{
|
|
7489
|
+
path: "AGENTS.md",
|
|
7490
|
+
content: agentsContent(options, template)
|
|
7491
|
+
}, {
|
|
7492
|
+
path: "CLAUDE.md",
|
|
7493
|
+
content: claudeContent()
|
|
7494
|
+
}];
|
|
7495
|
+
}
|
|
7496
|
+
/**
|
|
7497
|
+
* The pointer.
|
|
7498
|
+
*
|
|
7499
|
+
* Deliberately short and deliberately not a summary — a summary is a second
|
|
7500
|
+
* copy of the conventions that ages independently of the first.
|
|
7501
|
+
*/
|
|
7502
|
+
function claudeContent() {
|
|
7503
|
+
return `# CLAUDE.md
|
|
7504
|
+
|
|
7505
|
+
The conventions for this project live in [AGENTS.md](./AGENTS.md).
|
|
7506
|
+
|
|
7507
|
+
Read that file before writing code here. It is the single copy, kept that way
|
|
7508
|
+
on purpose: anything repeated here would be a second thing to keep in step, and
|
|
7509
|
+
the stale one is always the copy nobody opened.
|
|
7510
|
+
`;
|
|
7511
|
+
}
|
|
7512
|
+
function agentsContent(options, template) {
|
|
7513
|
+
const { name: name$1, monorepo, database, telescope, studio, packageManager, services } = options;
|
|
7514
|
+
const constructsGlob = monorepo ? WORKSPACE_CONSTRUCTS_GLOB : CONSTRUCTS_GLOB;
|
|
7515
|
+
const isWorker = template.name === "worker";
|
|
7516
|
+
const isServerless = template.name === "serverless";
|
|
7517
|
+
const run = packageManager === "npm" ? "npx" : `${packageManager} exec`;
|
|
7518
|
+
const sections = [];
|
|
7519
|
+
sections.push(`# Agent conventions — ${name$1}
|
|
7520
|
+
|
|
7521
|
+
Rules for anyone, human or agent, writing code in this project. Built with
|
|
7522
|
+
[\`@geekmidas/toolbox\`](https://github.com/geekmidas/toolbox).
|
|
7523
|
+
|
|
7524
|
+
Read this before adding a file. Most of what follows exists because the obvious
|
|
7525
|
+
approach from another framework produces something that compiles, runs, and is
|
|
7526
|
+
wrong in a way nothing catches.`);
|
|
7527
|
+
sections.push(`## The one idea
|
|
7528
|
+
|
|
7529
|
+
**Constructs declare; config does not restate.**
|
|
7530
|
+
|
|
7531
|
+
A construct — a \`RestApi\`, a database, a bucket, a topic — is the single
|
|
7532
|
+
declaration of a thing. The build reads those declarations and derives
|
|
7533
|
+
everything downstream from them: which containers exist, which apps get built,
|
|
7534
|
+
what the OpenAPI spec says, which env vars a process needs.
|
|
7535
|
+
|
|
7536
|
+
\`gkm.config.ts\` holds only what no construct can answer:
|
|
7537
|
+
|
|
7538
|
+
\`\`\`typescript
|
|
7539
|
+
export default defineWorkspace({
|
|
7540
|
+
name: '${name$1}',
|
|
7541
|
+
constructs: '${constructsGlob}',
|
|
7542
|
+
secrets: { enabled: true },
|
|
7543
|
+
});
|
|
7544
|
+
\`\`\`
|
|
7545
|
+
|
|
7546
|
+
If you find yourself adding a key to config that names something already
|
|
7547
|
+
declared in a construct — a path, a port, a route, a container — stop. That is
|
|
7548
|
+
the drift this design removes. The second copy is the one that goes wrong.`);
|
|
7549
|
+
sections.push(`## Layout
|
|
7550
|
+
|
|
7551
|
+
\`\`\`
|
|
7552
|
+
${layoutBlock(options, template)}
|
|
7553
|
+
\`\`\`
|
|
7554
|
+
|
|
7555
|
+
Handlers are found by **one glob**, and what kind a file exports is decided by
|
|
7556
|
+
the value it exports, not by the directory it sits in.`);
|
|
7557
|
+
sections.push(`## Commands
|
|
7558
|
+
|
|
7559
|
+
\`\`\`bash
|
|
7560
|
+
${run} gkm dev # dev server, hot reload, services up, secrets injected
|
|
7561
|
+
${run} gkm build # production build
|
|
7562
|
+
${run} gkm test # tests, with secrets injected
|
|
7563
|
+
${run} gkm exec -- <cmd> # run any command with secrets injected
|
|
7564
|
+
${run} gkm openapi # regenerate the OpenAPI spec
|
|
7565
|
+
\`\`\`
|
|
7566
|
+
|
|
7567
|
+
Secrets are encrypted at rest and injected at run time:
|
|
7568
|
+
|
|
7569
|
+
\`\`\`bash
|
|
7570
|
+
${run} gkm secrets:set DATABASE_URL postgres://...
|
|
7571
|
+
${run} gkm secrets:show
|
|
7572
|
+
\`\`\`
|
|
7573
|
+
|
|
7574
|
+
**Never run a tool that needs project env vars directly.** It will see an empty
|
|
7575
|
+
environment and fail in a way that looks like a config bug. Go through
|
|
7576
|
+
\`gkm exec --\`, which is what injects them.`);
|
|
7577
|
+
if (!isWorker) sections.push(endpointSection(options));
|
|
7578
|
+
if (isWorker || isServerless || services.events) sections.push(backgroundSection(template));
|
|
7579
|
+
if (database) sections.push(databaseSection());
|
|
7580
|
+
sections.push(`## Environment
|
|
7581
|
+
|
|
7582
|
+
Environment is parsed once, through \`envkit\`, and never read from
|
|
7583
|
+
\`process.env\` directly:
|
|
7584
|
+
|
|
7585
|
+
\`\`\`typescript
|
|
7586
|
+
import { EnvironmentParser } from '@geekmidas/envkit';
|
|
7587
|
+
import { Credentials } from '@geekmidas/envkit/credentials';
|
|
7588
|
+
|
|
7589
|
+
export const envParser = new EnvironmentParser({
|
|
7590
|
+
...process.env,
|
|
7591
|
+
...Credentials,
|
|
7592
|
+
});
|
|
7593
|
+
|
|
7594
|
+
export const config = envParser
|
|
7595
|
+
.create((get) => ({
|
|
7596
|
+
port: get('PORT').string().transform(Number).default(3000),
|
|
7597
|
+
}))
|
|
7598
|
+
.parse();
|
|
7599
|
+
\`\`\`
|
|
7600
|
+
|
|
7601
|
+
\`Credentials\` is what \`gkm dev\` and \`gkm exec\` inject. A bare
|
|
7602
|
+
\`process.env.FOO\` bypasses both the decryption and the validation, so it reads
|
|
7603
|
+
as \`undefined\` in exactly the environments that matter.`);
|
|
7604
|
+
sections.push(conventionsSection());
|
|
7605
|
+
sections.push(testingSection(database));
|
|
7606
|
+
if (telescope || studio) {
|
|
7607
|
+
const tools = [telescope && "**Telescope** — request and exception monitoring", studio && "**Studio** — dev dashboard and database browser"].filter(Boolean);
|
|
7608
|
+
sections.push(`## Dev tools
|
|
7609
|
+
|
|
7610
|
+
${tools.map((t) => `- ${t}`).join("\n")}
|
|
7611
|
+
|
|
7612
|
+
Both are derived from declared constructs. Neither needs wiring up by hand.`);
|
|
7613
|
+
}
|
|
7614
|
+
sections.push(`## Before you say it works
|
|
7615
|
+
|
|
7616
|
+
\`\`\`bash
|
|
7617
|
+
${run} gkm build
|
|
7618
|
+
\`\`\`
|
|
7619
|
+
|
|
7620
|
+
A build is the only thing that proves the manifest still resolves. Code that
|
|
7621
|
+
typechecks can still declare two constructs with one id, or a handler no glob
|
|
7622
|
+
reaches — and neither shows up until something reads the graph.`);
|
|
7623
|
+
return `${sections.join("\n\n")}\n`;
|
|
7624
|
+
}
|
|
7625
|
+
function layoutBlock(options, template) {
|
|
7626
|
+
const { monorepo, apiPath, database } = options;
|
|
7627
|
+
const isWorker = template.name === "worker";
|
|
7628
|
+
if (monorepo) return `constructs/ # every construct: the surface, the database, topics
|
|
7629
|
+
${apiPath}/ # the API app
|
|
7630
|
+
gkm.config.ts # name, constructs glob, secrets`;
|
|
7631
|
+
const lines = ["src/constructs/ # the surface, and anything it declares", "src/config/ # env parser, logger"];
|
|
7632
|
+
if (!isWorker) lines.push("src/endpoints/ # HTTP handlers");
|
|
7633
|
+
if (isWorker) {
|
|
7634
|
+
lines.push("src/crons/ # scheduled work");
|
|
7635
|
+
lines.push("src/subscribers/ # topic consumers");
|
|
7636
|
+
}
|
|
7637
|
+
if (database) lines.push("src/db/ # migrations, schema");
|
|
7638
|
+
lines.push("gkm.config.ts # name, constructs glob, secrets");
|
|
7639
|
+
return lines.join("\n");
|
|
7640
|
+
}
|
|
7641
|
+
function endpointSection(options) {
|
|
7642
|
+
const { database } = options;
|
|
7643
|
+
return `## Adding an endpoint
|
|
7644
|
+
|
|
7645
|
+
An endpoint is built **from the surface that serves it**. That is where its
|
|
7646
|
+
logger, environment parser and authorizers come from, so none of them is passed
|
|
7647
|
+
per endpoint:
|
|
7648
|
+
|
|
7649
|
+
\`\`\`typescript
|
|
7650
|
+
import { z } from 'zod';
|
|
7651
|
+
import { router } from '../constructs/router.ts';
|
|
7652
|
+
|
|
7653
|
+
export const createUser = router
|
|
7654
|
+
.post('/users')
|
|
7655
|
+
.body(z.object({ name: z.string(), email: z.email() }))
|
|
7656
|
+
.output(z.object({ id: z.uuid() }))
|
|
7657
|
+
.handle(async ({ body, logger${database ? ", services" : ""} }) => {
|
|
7658
|
+
logger.info({ name: body.name }, 'creating user');
|
|
7659
|
+
${database ? ` const user = await services.database
|
|
7660
|
+
.insertInto('users')
|
|
7661
|
+
.values({ name: body.name, email: body.email })
|
|
7662
|
+
.returningAll()
|
|
7663
|
+
.executeTakeFirstOrThrow();
|
|
7664
|
+
|
|
7665
|
+
return { id: user.id };` : ` return { id: crypto.randomUUID() };`}
|
|
7666
|
+
});
|
|
7667
|
+
\`\`\`
|
|
7668
|
+
|
|
7669
|
+
**Export it.** An endpoint that is never exported is never found — the glob
|
|
7670
|
+
loads the module and filters by value, so a handler assigned to a local
|
|
7671
|
+
\`const\` simply does not exist as far as the build is concerned.
|
|
7672
|
+
|
|
7673
|
+
**Do not write an HTTP server.** No \`express()\`, no \`new Hono()\`, no
|
|
7674
|
+
\`app.listen\`. The entry is generated from the surface; a hand-written server
|
|
7675
|
+
beside it is a second app that shadows the real one.
|
|
7676
|
+
|
|
7677
|
+
### Branching the factory
|
|
7678
|
+
|
|
7679
|
+
Share what a *group* of endpoints needs by branching once, in its own file:
|
|
7680
|
+
|
|
7681
|
+
\`\`\`typescript
|
|
7682
|
+
export const router = api.endpoints${database ? ".database(database)" : ""};
|
|
7683
|
+
export const sessionRouter = router.session(/* … */);
|
|
7684
|
+
\`\`\`
|
|
7685
|
+
|
|
7686
|
+
Branch for a group, never for a single endpoint — a one-endpoint branch is
|
|
7687
|
+
indirection with nothing on the other side of it.`;
|
|
7688
|
+
}
|
|
7689
|
+
function backgroundSection(template) {
|
|
7690
|
+
const isWorker = template.name === "worker";
|
|
7691
|
+
return `## Background work
|
|
7692
|
+
|
|
7693
|
+
Crons, subscribers and functions are declared the same way endpoints are: as
|
|
7694
|
+
exported values the build discovers.
|
|
7695
|
+
|
|
7696
|
+
\`\`\`typescript
|
|
7697
|
+
// a scheduled job
|
|
7698
|
+
import { c } from '@geekmidas/constructs/crons';
|
|
7699
|
+
|
|
7700
|
+
export const cleanup = c
|
|
7701
|
+
.schedule('rate(1 day)')
|
|
7702
|
+
.handle(async ({ logger }) => {
|
|
7703
|
+
logger.info('cleaning up');
|
|
7704
|
+
});
|
|
7705
|
+
\`\`\`
|
|
7706
|
+
|
|
7707
|
+
\`\`\`typescript
|
|
7708
|
+
// a topic subscriber
|
|
7709
|
+
import { s } from '@geekmidas/constructs/subscribers';
|
|
7710
|
+
import { userEvents } from '../constructs/topics.ts';
|
|
7711
|
+
|
|
7712
|
+
export const onUserCreated = s
|
|
7713
|
+
.topic(userEvents)
|
|
7714
|
+
.subscribe(['user.created'])
|
|
7715
|
+
.handle(async ({ events, logger }) => {
|
|
7716
|
+
logger.info({ count: events.length }, 'handling user events');
|
|
7717
|
+
});
|
|
7718
|
+
\`\`\`
|
|
7719
|
+
|
|
7720
|
+
Handlers receive a **batch**, not one event. Both transports deliver in
|
|
7721
|
+
batches, so a per-event round trip is a round trip per event for no reason.
|
|
7722
|
+
|
|
7723
|
+
${isWorker ? `This project is background work and nothing else — there is no HTTP surface
|
|
7724
|
+
to add routes to.` : `Declaring a topic is what makes the broker exist. Nothing lists it in config.`}`;
|
|
7725
|
+
}
|
|
7726
|
+
function databaseSection() {
|
|
7727
|
+
return `## Database
|
|
7728
|
+
|
|
7729
|
+
The database is a **construct**, not a hand-rolled service. Declaring it is what
|
|
7730
|
+
makes a Postgres exist locally and a database exist on deploy — nothing names
|
|
7731
|
+
\`postgres\` in config.
|
|
7732
|
+
|
|
7733
|
+
An endpoint reaches it by declaring the dependency, and gets it typed:
|
|
7734
|
+
|
|
7735
|
+
\`\`\`typescript
|
|
7736
|
+
export const listUsers = router
|
|
7737
|
+
.get('/users')
|
|
7738
|
+
.output(z.array(z.object({ id: z.uuid() })))
|
|
7739
|
+
.handle(async ({ services }) => {
|
|
7740
|
+
return services.database.selectFrom('users').selectAll().execute();
|
|
7741
|
+
});
|
|
7742
|
+
\`\`\`
|
|
7743
|
+
|
|
7744
|
+
Queries go through Kysely. Never open your own connection pool — the one the
|
|
7745
|
+
construct provides is the one that gets credentials, pooling and shutdown.`;
|
|
7746
|
+
}
|
|
7747
|
+
function conventionsSection() {
|
|
7748
|
+
return `## Conventions
|
|
7749
|
+
|
|
7750
|
+
**Zod formats are top-level schemas.**
|
|
7751
|
+
|
|
7752
|
+
\`\`\`typescript
|
|
7753
|
+
// yes
|
|
7754
|
+
z.email()
|
|
7755
|
+
z.url()
|
|
7756
|
+
z.uuid()
|
|
7757
|
+
|
|
7758
|
+
// no
|
|
7759
|
+
z.string().email()
|
|
7760
|
+
z.string().url()
|
|
7761
|
+
\`\`\`
|
|
7762
|
+
|
|
7763
|
+
Zod 4 made each format a schema in its own right. The chained form still runs,
|
|
7764
|
+
but it types as a \`ZodString\` carrying a check, so it cannot be composed or
|
|
7765
|
+
narrowed — and it emits different JSON Schema, which means a published API
|
|
7766
|
+
contract would depend on which spelling someone reached for.
|
|
7767
|
+
|
|
7768
|
+
\`envkit\` is not affected: \`get('ADMIN_EMAIL').string().email()\` is its own
|
|
7769
|
+
builder, not Zod's. Leave it alone.
|
|
7770
|
+
|
|
7771
|
+
**Style.** Two-space indent, single quotes, semicolons, trailing commas, 80
|
|
7772
|
+
columns. \`import type\` for type-only imports. Run the formatter rather than
|
|
7773
|
+
matching it by hand.
|
|
7774
|
+
|
|
7775
|
+
**Construct ids are unique.** Two constructs sharing an id fails discovery for
|
|
7776
|
+
the whole project, not just the second one.`;
|
|
7777
|
+
}
|
|
7778
|
+
function testingSection(database) {
|
|
7779
|
+
return `## Testing
|
|
7780
|
+
|
|
7781
|
+
**Integration over unit.** Prefer the real dependency to a mock — a real
|
|
7782
|
+
database, a real cache. Mocks are for things you genuinely cannot run:
|
|
7783
|
+
the filesystem, the clock, external HTTP (use MSW for that).
|
|
7784
|
+
|
|
7785
|
+
**Test behaviour, not implementation.** Assert what the code did, not which
|
|
7786
|
+
functions it called on the way.
|
|
7787
|
+
|
|
7788
|
+
\`\`\`bash
|
|
7789
|
+
${database ? "gkm test # starts services, injects secrets, then runs vitest" : "gkm test"}
|
|
7790
|
+
\`\`\`
|
|
7791
|
+
|
|
7792
|
+
Run tests through \`gkm test\`, not \`vitest\` directly${database ? " — it is what starts the database and injects the secrets the suite needs" : ""}.`;
|
|
7793
|
+
}
|
|
7794
|
+
|
|
7795
|
+
//#endregion
|
|
7796
|
+
//#region src/init/versions.ts
|
|
7797
|
+
const require$1 = (0, node_module.createRequire)(require("url").pathToFileURL(__filename).href);
|
|
7798
|
+
function loadPackageJson() {
|
|
7799
|
+
try {
|
|
7800
|
+
return require$1("../package.json");
|
|
7801
|
+
} catch {
|
|
7802
|
+
return require$1("../../package.json");
|
|
7803
|
+
}
|
|
7804
|
+
}
|
|
7805
|
+
const pkg = loadPackageJson();
|
|
7806
|
+
/**
|
|
7807
|
+
* CLI version resolved from package.json at runtime
|
|
7808
|
+
*/
|
|
7809
|
+
const CLI_VERSION = `~${pkg.version}`;
|
|
7810
|
+
/**
|
|
7811
|
+
* Package versions for @geekmidas packages
|
|
7812
|
+
*
|
|
7813
|
+
* AUTO-GENERATED (except CLI) - Do not edit manually
|
|
7814
|
+
* Run: pnpm --filter @geekmidas/cli sync-versions
|
|
7815
|
+
*/
|
|
7816
|
+
const GEEKMIDAS_VERSIONS = {
|
|
7817
|
+
"@geekmidas/audit": "~10.0.0-alpha.2",
|
|
7818
|
+
"@geekmidas/auth": "~10.0.0-alpha.2",
|
|
7819
|
+
"@geekmidas/cache": "~10.0.0-alpha.2",
|
|
7820
|
+
"@geekmidas/client": "~10.0.0-alpha.2",
|
|
7821
|
+
"@geekmidas/cloud": "~10.0.0-alpha.2",
|
|
7822
|
+
"@geekmidas/constructs": "~10.0.0-alpha.2",
|
|
7823
|
+
"@geekmidas/db": "~10.0.0-alpha.2",
|
|
7824
|
+
"@geekmidas/emailkit": "~10.0.0-alpha.2",
|
|
7825
|
+
"@geekmidas/envkit": "~10.0.0-alpha.2",
|
|
7826
|
+
"@geekmidas/errors": "~10.0.0-alpha.2",
|
|
7827
|
+
"@geekmidas/events": "~10.0.0-alpha.2",
|
|
7828
|
+
"@geekmidas/logger": "~10.0.0-alpha.2",
|
|
7829
|
+
"@geekmidas/manifest": "~10.0.0-alpha.2",
|
|
7830
|
+
"@geekmidas/rate-limit": "~10.0.0-alpha.2",
|
|
7831
|
+
"@geekmidas/schema": "~10.0.0-alpha.2",
|
|
7832
|
+
"@geekmidas/services": "~10.0.0-alpha.2",
|
|
7833
|
+
"@geekmidas/storage": "~10.0.0-alpha.2",
|
|
7834
|
+
"@geekmidas/studio": "~10.0.0-alpha.2",
|
|
7835
|
+
"@geekmidas/telescope": "~10.0.0-alpha.2",
|
|
7836
|
+
"@geekmidas/testkit": "~10.0.0-alpha.2",
|
|
7837
|
+
"@geekmidas/cli": CLI_VERSION
|
|
7838
|
+
};
|
|
7839
|
+
|
|
7840
|
+
//#endregion
|
|
7841
|
+
//#region src/init/generators/auth.ts
|
|
7842
|
+
/**
|
|
7843
|
+
* Generate auth app files for fullstack template
|
|
7844
|
+
* Uses better-auth with magic link authentication
|
|
7845
|
+
*/
|
|
7846
|
+
function generateAuthAppFiles(options) {
|
|
7847
|
+
if (!options.monorepo || options.template !== "fullstack") return [];
|
|
7848
|
+
const packageName = `@${options.name}/auth`;
|
|
7849
|
+
const modelsPackage = `@${options.name}/models`;
|
|
7850
|
+
const packageJson = {
|
|
7851
|
+
name: packageName,
|
|
7852
|
+
version: "0.0.1",
|
|
7853
|
+
private: true,
|
|
7854
|
+
type: "module",
|
|
7855
|
+
scripts: {
|
|
7856
|
+
dev: "gkm dev --entry ./src/index.ts",
|
|
7857
|
+
build: "tsc",
|
|
7858
|
+
start: "node dist/index.js",
|
|
7859
|
+
typecheck: "tsc --noEmit",
|
|
7860
|
+
"db:migrate": "gkm exec -- npx @better-auth/cli migrate",
|
|
7861
|
+
"db:generate": "gkm exec -- npx @better-auth/cli generate"
|
|
7862
|
+
},
|
|
7863
|
+
dependencies: {
|
|
7864
|
+
[modelsPackage]: "workspace:*",
|
|
7865
|
+
"@geekmidas/envkit": GEEKMIDAS_VERSIONS["@geekmidas/envkit"],
|
|
7866
|
+
"@geekmidas/logger": GEEKMIDAS_VERSIONS["@geekmidas/logger"],
|
|
7867
|
+
"@hono/node-server": "~1.13.0",
|
|
7868
|
+
"better-auth": "~1.2.0",
|
|
7869
|
+
hono: "~4.8.0",
|
|
7870
|
+
kysely: "~0.27.0",
|
|
7871
|
+
pg: "~8.13.0"
|
|
7872
|
+
},
|
|
7873
|
+
devDependencies: {
|
|
7874
|
+
"@geekmidas/cli": GEEKMIDAS_VERSIONS["@geekmidas/cli"],
|
|
7875
|
+
"@types/node": "~22.0.0",
|
|
7876
|
+
"@types/pg": "~8.11.0",
|
|
7877
|
+
tsx: "~4.20.0",
|
|
7878
|
+
typescript: "~5.8.2"
|
|
7879
|
+
}
|
|
7880
|
+
};
|
|
7881
|
+
const tsConfig = {
|
|
7882
|
+
extends: "../../tsconfig.json",
|
|
7883
|
+
compilerOptions: {
|
|
7884
|
+
noEmit: true,
|
|
7885
|
+
allowImportingTsExtensions: true,
|
|
7886
|
+
baseUrl: ".",
|
|
7887
|
+
paths: {
|
|
7888
|
+
"~/*": ["./src/*"],
|
|
7889
|
+
[`@${options.name}/*`]: ["../../packages/*/src"]
|
|
7890
|
+
}
|
|
7891
|
+
},
|
|
7892
|
+
include: ["src/**/*.ts"],
|
|
7893
|
+
exclude: ["node_modules", "dist"]
|
|
7894
|
+
};
|
|
7895
|
+
const gitignore = `node_modules/
|
|
7896
|
+
dist/
|
|
7897
|
+
.env.local
|
|
7898
|
+
*.log
|
|
7899
|
+
`;
|
|
7900
|
+
return [
|
|
7901
|
+
{
|
|
7902
|
+
path: "apps/auth/package.json",
|
|
7903
|
+
content: `${JSON.stringify(packageJson, null, 2)}\n`
|
|
7904
|
+
},
|
|
7905
|
+
{
|
|
7906
|
+
path: "apps/auth/tsconfig.json",
|
|
7907
|
+
content: `${JSON.stringify(tsConfig, null, 2)}\n`
|
|
7908
|
+
},
|
|
7909
|
+
{
|
|
7910
|
+
path: "apps/auth/.gitignore",
|
|
7911
|
+
content: gitignore
|
|
7912
|
+
}
|
|
7913
|
+
];
|
|
7914
|
+
}
|
|
7915
|
+
|
|
7580
7916
|
//#endregion
|
|
7581
7917
|
//#region src/init/generators/config.ts
|
|
7582
7918
|
/**
|
|
@@ -9199,7 +9535,7 @@ export default defineWorkspace({
|
|
|
9199
9535
|
// declared bucket is why MinIO does, a declared topic is why a broker does —
|
|
9200
9536
|
// none of it listed here. It is also where the apps come from: a
|
|
9201
9537
|
// \`StaticSite\` is an app, and so is every \`RestApi\`.
|
|
9202
|
-
constructs: '
|
|
9538
|
+
constructs: '${WORKSPACE_CONSTRUCTS_GLOB}',
|
|
9203
9539
|
|
|
9204
9540
|
secrets: {
|
|
9205
9541
|
enabled: true,
|
|
@@ -13183,6 +13519,7 @@ async function initCommand(projectName, options = {}) {
|
|
|
13183
13519
|
] : [];
|
|
13184
13520
|
const dockerFiles = isMonorepo$1 && baseTemplate ? generateDockerFiles(templateOptions, baseTemplate, dbApps) : [];
|
|
13185
13521
|
const rootFiles = baseTemplate ? [
|
|
13522
|
+
...generateAgentFiles(templateOptions, baseTemplate),
|
|
13186
13523
|
...generateMonorepoFiles(templateOptions, baseTemplate),
|
|
13187
13524
|
...generateRootConstructs(templateOptions),
|
|
13188
13525
|
...generateModelsPackage(templateOptions)
|
|
@@ -13571,7 +13908,7 @@ async function testCommand(options = {}) {
|
|
|
13571
13908
|
console.log(`\n🧪 Running tests with ${stage} environment...\n`);
|
|
13572
13909
|
const autoSetup = options.autoSetup || Boolean(process.env.GKM_AUTO_SETUP);
|
|
13573
13910
|
if (autoSetup) {
|
|
13574
|
-
const { ensureStageSecrets } = await Promise.resolve().then(() => require("./setup-
|
|
13911
|
+
const { ensureStageSecrets } = await Promise.resolve().then(() => require("./setup-fG_ivuxO.cjs"));
|
|
13575
13912
|
const generated = await ensureStageSecrets(stage, cwd);
|
|
13576
13913
|
if (generated) console.log(` 🔐 Generated fresh ${stage} secrets (auto-setup)`);
|
|
13577
13914
|
}
|
|
@@ -14046,7 +14383,7 @@ program.command("secrets:push").description("Push secrets to remote provider (SS
|
|
|
14046
14383
|
const { workspace } = await loadWorkspaceConfig$1();
|
|
14047
14384
|
const secrets = await readStageSecrets$1(options.stage, workspace.root);
|
|
14048
14385
|
if (secrets) {
|
|
14049
|
-
const { derivedContainers: derivedContainers$1 } = await Promise.resolve().then(() => require("./workspace-
|
|
14386
|
+
const { derivedContainers: derivedContainers$1 } = await Promise.resolve().then(() => require("./workspace-BO_KWohI.cjs"));
|
|
14050
14387
|
const result = reconcileMissingSecrets(secrets, workspace, await derivedContainers$1(workspace, options.stage));
|
|
14051
14388
|
if (result) {
|
|
14052
14389
|
await writeStageSecrets$1(result.secrets, workspace.root);
|
|
@@ -14075,7 +14412,7 @@ program.command("secrets:pull").description("Pull secrets from remote provider (
|
|
|
14075
14412
|
console.error(`No remote secrets found for stage "${options.stage}".`);
|
|
14076
14413
|
process.exit(1);
|
|
14077
14414
|
}
|
|
14078
|
-
const { derivedContainers: derivedContainers$1 } = await Promise.resolve().then(() => require("./workspace-
|
|
14415
|
+
const { derivedContainers: derivedContainers$1 } = await Promise.resolve().then(() => require("./workspace-BO_KWohI.cjs"));
|
|
14079
14416
|
const result = reconcileMissingSecrets(secrets, workspace, await derivedContainers$1(workspace, options.stage));
|
|
14080
14417
|
if (result) {
|
|
14081
14418
|
secrets = result.secrets;
|
|
@@ -14102,7 +14439,7 @@ program.command("secrets:reconcile").description("Backfill missing custom secret
|
|
|
14102
14439
|
console.error(`No secrets found for stage "${options.stage}". Run "gkm secrets:init --stage ${options.stage}" first.`);
|
|
14103
14440
|
process.exit(1);
|
|
14104
14441
|
}
|
|
14105
|
-
const { derivedContainers: derivedContainers$1 } = await Promise.resolve().then(() => require("./workspace-
|
|
14442
|
+
const { derivedContainers: derivedContainers$1 } = await Promise.resolve().then(() => require("./workspace-BO_KWohI.cjs"));
|
|
14106
14443
|
const result = reconcileMissingSecrets(secrets, workspace, await derivedContainers$1(workspace, options.stage));
|
|
14107
14444
|
if (!result) {
|
|
14108
14445
|
console.log(`\n✓ Secrets for stage "${options.stage}" are up-to-date`);
|