@geekmidas/cli 10.0.0-alpha.1 → 10.0.0-alpha.11
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/{CachedStateProvider-CqPdW7CB.cjs → CachedStateProvider-BEBl2wQT.cjs} +1 -1
- package/dist/{CachedStateProvider-CqPdW7CB.cjs.map → CachedStateProvider-BEBl2wQT.cjs.map} +1 -1
- package/dist/CachedStateProvider-C-8wtlto.mjs +3 -0
- package/dist/{CachedStateProvider-DTniehMP.mjs → CachedStateProvider-Co4n5Wvs.mjs} +1 -1
- package/dist/{CachedStateProvider-DTniehMP.mjs.map → CachedStateProvider-Co4n5Wvs.mjs.map} +1 -1
- package/dist/{CachedStateProvider-BLo7zv7S.cjs → CachedStateProvider-DqhGpzsm.cjs} +1 -1
- package/dist/{HostingerProvider-h3EbEp-Z.cjs → HostingerProvider-BOvsgATO.cjs} +1 -1
- package/dist/{HostingerProvider-h3EbEp-Z.cjs.map → HostingerProvider-BOvsgATO.cjs.map} +1 -1
- package/dist/{HostingerProvider-Cft7lEYr.mjs → HostingerProvider-BcyFRbqV.mjs} +1 -1
- package/dist/{HostingerProvider-Cft7lEYr.mjs.map → HostingerProvider-BcyFRbqV.mjs.map} +1 -1
- package/dist/{LocalStateProvider-Bn6tS-oL.mjs → LocalStateProvider-Bq-0C0sg.mjs} +1 -1
- package/dist/{LocalStateProvider-Bn6tS-oL.mjs.map → LocalStateProvider-Bq-0C0sg.mjs.map} +1 -1
- package/dist/{LocalStateProvider-Db9SEjyH.cjs → LocalStateProvider-yjXZXZDM.cjs} +1 -1
- package/dist/{LocalStateProvider-Db9SEjyH.cjs.map → LocalStateProvider-yjXZXZDM.cjs.map} +1 -1
- package/dist/{Route53Provider-RdpUDJQk.mjs → Route53Provider-C0xmer8U.mjs} +1 -1
- package/dist/{Route53Provider-RdpUDJQk.mjs.map → Route53Provider-C0xmer8U.mjs.map} +1 -1
- package/dist/{Route53Provider-di2fMM5N.cjs → Route53Provider-CH9XH7bz.cjs} +1 -1
- package/dist/{Route53Provider-di2fMM5N.cjs.map → Route53Provider-CH9XH7bz.cjs.map} +1 -1
- package/dist/{SSMStateProvider-DyX8s9HI.cjs → SSMStateProvider-CXtNgPfy.cjs} +1 -1
- package/dist/{SSMStateProvider-DyX8s9HI.cjs.map → SSMStateProvider-CXtNgPfy.cjs.map} +1 -1
- package/dist/{SSMStateProvider-BRt2py-A.mjs → SSMStateProvider-DOZifZfE.mjs} +1 -1
- package/dist/{SSMStateProvider-BRt2py-A.mjs.map → SSMStateProvider-DOZifZfE.mjs.map} +1 -1
- package/dist/{backup-provisioner-Bgj5Vi_L.mjs → backup-provisioner-DXXg6FIL.mjs} +1 -1
- package/dist/{backup-provisioner-Bgj5Vi_L.mjs.map → backup-provisioner-DXXg6FIL.mjs.map} +1 -1
- package/dist/{backup-provisioner-BrsHwYG0.cjs → backup-provisioner-cE2PuCTb.cjs} +1 -1
- package/dist/{backup-provisioner-BrsHwYG0.cjs.map → backup-provisioner-cE2PuCTb.cjs.map} +1 -1
- package/dist/{bundler-BY1OmV1e.cjs → bundler-CaTGtiaO.cjs} +3 -3
- package/dist/{bundler-BY1OmV1e.cjs.map → bundler-CaTGtiaO.cjs.map} +1 -1
- package/dist/{bundler-Be8Zg4Wd.mjs → bundler-S3ek7ea-.mjs} +3 -3
- package/dist/{bundler-Be8Zg4Wd.mjs.map → bundler-S3ek7ea-.mjs.map} +1 -1
- package/dist/{config-D8o6I_7Z.mjs → config-B0L2tvvC.mjs} +9 -76
- package/dist/config-B0L2tvvC.mjs.map +1 -0
- package/dist/{config-DbPE1KXF.cjs → config-HhDpJmLO.cjs} +7 -80
- package/dist/config-HhDpJmLO.cjs.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-B2Pyh3bJ.cjs} +21 -20
- package/dist/credentials-B2Pyh3bJ.cjs.map +1 -0
- package/dist/{credentials-Dh0Q6HzY.mjs → credentials-CZrys9C5.mjs} +21 -20
- package/dist/credentials-CZrys9C5.mjs.map +1 -0
- package/dist/discover-CAlFgYec.mjs.map +1 -1
- package/dist/discover-DTmFeloL.cjs.map +1 -1
- package/dist/dokploy-api-5cK2InBm.cjs +3 -0
- package/dist/dokploy-api-D2Ny4Jrc.mjs +3 -0
- package/dist/{dokploy-api-aTFPz7_4.mjs → dokploy-api-pdaBryoY.mjs} +1 -1
- package/dist/{dokploy-api-aTFPz7_4.mjs.map → dokploy-api-pdaBryoY.mjs.map} +1 -1
- package/dist/{dokploy-api-BsVH0y_s.cjs → dokploy-api-zqKe9lHH.cjs} +1 -1
- package/dist/{dokploy-api-BsVH0y_s.cjs.map → dokploy-api-zqKe9lHH.cjs.map} +1 -1
- package/dist/{emit-CiZS_5Ub.mjs → emit-CIlvnDvV.mjs} +1 -1
- package/dist/{emit-CiZS_5Ub.mjs.map → emit-CIlvnDvV.mjs.map} +1 -1
- package/dist/{emit-CbcRolsa.cjs → emit-CUD1BWls.cjs} +1 -1
- package/dist/{emit-CbcRolsa.cjs.map → emit-CUD1BWls.cjs.map} +1 -1
- package/dist/{encryption-C6jaU0hE.cjs → encryption-D2pL3tHH.cjs} +1 -1
- package/dist/{encryption-C6jaU0hE.cjs.map → encryption-D2pL3tHH.cjs.map} +1 -1
- package/dist/encryption-YDsXXi6w.mjs +3 -0
- package/dist/{encryption-D-abyhZx.mjs → encryption-_4zwjOAR.mjs} +1 -1
- package/dist/{encryption-D-abyhZx.mjs.map → encryption-_4zwjOAR.mjs.map} +1 -1
- package/dist/{encryption-CHzzeR8a.cjs → encryption-sQ2Oxx0d.cjs} +1 -1
- package/dist/{fullstack-secrets-C-GGohke.cjs → fullstack-secrets-D6MyHhQP.cjs} +1 -1
- package/dist/{fullstack-secrets-C-GGohke.cjs.map → fullstack-secrets-D6MyHhQP.cjs.map} +1 -1
- package/dist/{fullstack-secrets-D1o9-G5l.mjs → fullstack-secrets-METaRLS1.mjs} +1 -1
- package/dist/{fullstack-secrets-D1o9-G5l.mjs.map → fullstack-secrets-METaRLS1.mjs.map} +1 -1
- package/dist/github-BrvaPkFf.cjs +197 -0
- package/dist/github-BrvaPkFf.cjs.map +1 -0
- package/dist/github-CrpsXSfV.mjs +196 -0
- package/dist/github-CrpsXSfV.mjs.map +1 -0
- package/dist/{index-CHB4tSzX.d.cts → index-Ayxg_DQY.d.mts} +2 -2
- package/dist/index-Ayxg_DQY.d.mts.map +1 -0
- package/dist/{index-BThUzEIp.d.mts → index-CR5urhWX.d.cts} +2 -2
- package/dist/index-CR5urhWX.d.cts.map +1 -0
- package/dist/index.cjs +1989 -623
- package/dist/index.cjs.map +1 -1
- package/dist/index.mjs +1991 -625
- package/dist/index.mjs.map +1 -1
- package/dist/keystore-C9rint2E.cjs +101 -0
- package/dist/keystore-C9rint2E.cjs.map +1 -0
- package/dist/keystore-TxeYM7an.mjs +83 -0
- package/dist/keystore-TxeYM7an.mjs.map +1 -0
- package/dist/{openapi-CljONd_M.cjs → openapi-DaKlTKxz.cjs} +127 -50
- package/dist/openapi-DaKlTKxz.cjs.map +1 -0
- package/dist/{openapi-react-query-CmJ1xDbn.cjs → openapi-react-query-BKH334Ug.cjs} +1 -1
- package/dist/{openapi-react-query-CmJ1xDbn.cjs.map → openapi-react-query-BKH334Ug.cjs.map} +1 -1
- package/dist/{openapi-react-query-Byltvm_7.mjs → openapi-react-query-DoR5CmzQ.mjs} +1 -1
- package/dist/{openapi-react-query-Byltvm_7.mjs.map → openapi-react-query-DoR5CmzQ.mjs.map} +1 -1
- package/dist/openapi-react-query.cjs +1 -1
- package/dist/openapi-react-query.mjs +1 -1
- package/dist/{openapi-CgpMWMmM.mjs → openapi-y1d7Kalt.mjs} +109 -50
- package/dist/openapi-y1d7Kalt.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.cjs +1 -1
- package/dist/reconcile/public.d.cts +1 -1
- package/dist/reconcile/public.d.mts +1 -1
- package/dist/reconcile/public.mjs +1 -1
- package/dist/{reconcile-CFs8C_1a.mjs → reconcile-C13ERt1U.mjs} +2 -2
- package/dist/{reconcile-CFs8C_1a.mjs.map → reconcile-C13ERt1U.mjs.map} +1 -1
- package/dist/{reconcile-D0pimNiy.cjs → reconcile-CYMhFSYy.cjs} +2 -2
- package/dist/{reconcile-D0pimNiy.cjs.map → reconcile-CYMhFSYy.cjs.map} +1 -1
- package/dist/setup-BWgMs4gg.mjs +12 -0
- package/dist/{setup-CJiQM2pX.cjs → setup-DOv92pIu.cjs} +8 -8
- package/dist/setup-DOv92pIu.cjs.map +1 -0
- package/dist/{setup-DOsYueeQ.mjs → setup-DQo_h_yK.mjs} +8 -8
- package/dist/setup-DQo_h_yK.mjs.map +1 -0
- package/dist/setup-lCx_DcUo.cjs +12 -0
- package/dist/{storage-Bi24WfoW.mjs → storage-B020InPW.mjs} +4 -79
- package/dist/storage-B020InPW.mjs.map +1 -0
- package/dist/{storage-BJmzYxt8.cjs → storage-CSNplaJn.cjs} +4 -85
- package/dist/storage-CSNplaJn.cjs.map +1 -0
- package/dist/{storage-CYlGwWTj.mjs → storage-DGBzMLZa.mjs} +2 -1
- package/dist/{storage-BM9hGpgv.cjs → storage-DlVol_oj.cjs} +2 -1
- package/dist/{sync-SizaChmC.cjs → sync-0ILCb-1S.cjs} +2 -2
- package/dist/{sync-SizaChmC.cjs.map → sync-0ILCb-1S.cjs.map} +1 -1
- package/dist/{sync-CmEuNZsB.mjs → sync-CzHXiR9c.mjs} +2 -2
- package/dist/{sync-CmEuNZsB.mjs.map → sync-CzHXiR9c.mjs.map} +1 -1
- package/dist/sync-DYENfOt4.cjs +4 -0
- package/dist/{sync-hOqAWHR6.mjs → sync-HAau6h_R.mjs} +1 -1
- package/dist/{types-C-arnQ1r.d.mts → types-CL7jPFM2.d.mts} +46 -10
- package/dist/types-CL7jPFM2.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-C-a3G_U0.cjs +6871 -0
- package/dist/workspace-C-a3G_U0.cjs.map +1 -0
- package/dist/workspace-DQxxs7Jk.cjs +10 -0
- package/dist/workspace-DaEX7FEg.mjs +6698 -0
- package/dist/workspace-DaEX7FEg.mjs.map +1 -0
- package/dist/workspace-DbI580qS.mjs +9 -0
- package/package.json +33 -32
- package/dist/CachedStateProvider-BxoAm-B2.mjs +0 -3
- 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/dokploy-api-BS2xnJLZ.mjs +0 -3
- package/dist/dokploy-api-BuxDRo3l.cjs +0 -3
- package/dist/encryption-CQUncu7v.mjs +0 -3
- 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/setup-DSutDAlt.mjs +0 -11
- package/dist/storage-BJmzYxt8.cjs.map +0 -1
- package/dist/storage-Bi24WfoW.mjs.map +0 -1
- package/dist/sync-D306yOWu.cjs +0 -4
- 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-8acupeaI.cjs +0 -9
- 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.mjs
CHANGED
|
@@ -1,23 +1,24 @@
|
|
|
1
1
|
#!/usr/bin/env -S npx tsx
|
|
2
2
|
import { __require } from "./chunk-Duj1WY3L.mjs";
|
|
3
3
|
import { ConstructGenerator, DEFAULT_CACHE, discover, isPartitionedRoutes } from "./discover-CAlFgYec.mjs";
|
|
4
|
-
import { derivedApps, getAppNameFromCwd, loadAppConfig, loadConfig, loadWorkspaceConfig, parseModuleConfig
|
|
5
|
-
import { allConstructGlobs, getAppBuildOrder, getDependencyEnvVars, getDeployTargetError, getPublicEnvPrefix, isDeployTargetSupported, stripPublicPrefix } from "./workspace-
|
|
4
|
+
import { derivedApps, getAppNameFromCwd, loadAppConfig, loadConfig, loadWorkspaceConfig, parseModuleConfig } from "./config-B0L2tvvC.mjs";
|
|
5
|
+
import { InvalidStages, allConstructGlobs, assertDeployedStage, deployedProblems, getAppBuildOrder, getAppGkmConfig, getDependencyEnvVars, getDeployTargetError, getPublicEnvPrefix, isDeployTargetSupported, stageProblems, stripPublicPrefix } from "./workspace-DaEX7FEg.mjs";
|
|
6
6
|
import { getCredentialsPath, getDokployCredentials, getDokployRegistryId, getDokployToken, removeDokployCredentials, storeDokployCredentials, storeDokployRegistryId, storeHostingerToken } from "./credentials-B5zRHdAl.mjs";
|
|
7
|
-
import { applyPostgres, cacheBackendOf, constructGlobs, createCredentialsPreload, createEntryWrapper, derivedContainers, emailBackendOf, findAvailablePort, isPortAvailable, loadEnvFiles, loadSecretsForApp, prepareEntryCredentials, providerOf, reconcileWorkspace, resolveServicePorts, resourceName, rewriteUrlsWithPorts, startWorkspaceServices, storageBackendOf, usesConstructs } from "./credentials-
|
|
7
|
+
import { applyPostgres, cacheBackendOf, constructGlobs, createCredentialsPreload, createEntryWrapper, derivedContainers, emailBackendOf, findAvailablePort, isPortAvailable, loadEnvFiles, loadSecretsForApp, prepareEntryCredentials, providerOf, reconcileWorkspace, resolveServicePorts, resourceName, rewriteUrlsWithPorts, startWorkspaceServices, storageBackendOf, usesConstructs } from "./credentials-CZrys9C5.mjs";
|
|
8
8
|
import { createStageSecrets, rotateServicePassword } from "./generator-VE2JrFlY.mjs";
|
|
9
|
-
import { getKeyPath
|
|
10
|
-
import {
|
|
11
|
-
import {
|
|
12
|
-
import {
|
|
13
|
-
import {
|
|
14
|
-
import {
|
|
15
|
-
import {
|
|
16
|
-
import {
|
|
17
|
-
import "./
|
|
18
|
-
import
|
|
9
|
+
import { getKeyPath } from "./keystore-TxeYM7an.mjs";
|
|
10
|
+
import { maskPassword, readStageSecrets, secretsExist, setCustomSecret, toEmbeddableSecrets, writeStageSecrets } from "./storage-B020InPW.mjs";
|
|
11
|
+
import { EndpointGenerator, OPENAPI_OUTPUT_PATH, generateOpenApi, generateOpenApiFrom, normalizeRoutes, openapiCommand, resolveOpenApiConfig, runtimeFor } from "./openapi-y1d7Kalt.mjs";
|
|
12
|
+
import { withCompute, withRoutes } from "./emit-CIlvnDvV.mjs";
|
|
13
|
+
import { DokployApi } from "./dokploy-api-pdaBryoY.mjs";
|
|
14
|
+
import { encryptSecrets } from "./encryption-_4zwjOAR.mjs";
|
|
15
|
+
import { CachedStateProvider } from "./CachedStateProvider-Co4n5Wvs.mjs";
|
|
16
|
+
import { generateDbPassword, generateDbUrl, generateFullstackCustomSecrets } from "./fullstack-secrets-METaRLS1.mjs";
|
|
17
|
+
import { generateReactQueryCommand } from "./openapi-react-query-DoR5CmzQ.mjs";
|
|
18
|
+
import "./sync-CzHXiR9c.mjs";
|
|
19
|
+
import { setupCommand, trustCommand } from "./setup-DQo_h_yK.mjs";
|
|
19
20
|
import { createRequire } from "node:module";
|
|
20
|
-
import { copyFileSync, existsSync, readFileSync, unlinkSync } from "node:fs";
|
|
21
|
+
import { copyFileSync, existsSync, readFileSync, unlinkSync, writeFileSync } from "node:fs";
|
|
21
22
|
import { basename, dirname, isAbsolute, join, parse, relative, resolve } from "node:path";
|
|
22
23
|
import { DEFAULT_POSTGRES_VERSION, cacheTable, canonicalId, cloudName, cookieDomain, dependentsOf, provideKey, provisionOrder, scopedName, serviceKey } from "@geekmidas/manifest";
|
|
23
24
|
import fg from "fast-glob";
|
|
@@ -27,7 +28,7 @@ import * as readline from "node:readline/promises";
|
|
|
27
28
|
import { mkdir, readFile, writeFile } from "node:fs/promises";
|
|
28
29
|
import { execSync, spawn } from "node:child_process";
|
|
29
30
|
import chokidar from "chokidar";
|
|
30
|
-
import { parse as parse$1 } from "yaml";
|
|
31
|
+
import { parse as parse$1, parseDocument } from "yaml";
|
|
31
32
|
import { Client } from "pg";
|
|
32
33
|
import { createHash, randomBytes } from "node:crypto";
|
|
33
34
|
import { ownerRole, readerRole, roleStatements } from "@geekmidas/db/pg/roles";
|
|
@@ -43,7 +44,7 @@ import prompts from "prompts";
|
|
|
43
44
|
|
|
44
45
|
//#region package.json
|
|
45
46
|
var name = "@geekmidas/cli";
|
|
46
|
-
var version = "10.0.0-alpha.
|
|
47
|
+
var version = "10.0.0-alpha.11";
|
|
47
48
|
var description = "CLI tools for building Lambda handlers, server applications, and generating OpenAPI specs";
|
|
48
49
|
var private$1 = false;
|
|
49
50
|
var type = "module";
|
|
@@ -124,12 +125,12 @@ var repository = {
|
|
|
124
125
|
"url": "https://github.com/geekmidas/toolbox"
|
|
125
126
|
};
|
|
126
127
|
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.
|
|
128
|
+
"@apidevtools/swagger-parser": "~13.1.0",
|
|
129
|
+
"@aws-sdk/client-iam": "~3.1136.0",
|
|
130
|
+
"@aws-sdk/client-route-53": "~3.1136.0",
|
|
131
|
+
"@aws-sdk/client-s3": "~3.1136.0",
|
|
132
|
+
"@aws-sdk/client-ssm": "~3.1136.0",
|
|
133
|
+
"@aws-sdk/credential-providers": "~3.1136.0",
|
|
133
134
|
"@geekmidas/cache": "workspace:*",
|
|
134
135
|
"@geekmidas/constructs": "workspace:~",
|
|
135
136
|
"@geekmidas/db": "workspace:*",
|
|
@@ -138,28 +139,29 @@ var dependencies = {
|
|
|
138
139
|
"@geekmidas/logger": "workspace:~",
|
|
139
140
|
"@geekmidas/manifest": "workspace:*",
|
|
140
141
|
"@geekmidas/schema": "workspace:~",
|
|
141
|
-
"chokidar": "~
|
|
142
|
-
"commander": "
|
|
143
|
-
"dotenv": "~
|
|
144
|
-
"esbuild": "
|
|
145
|
-
"fast-glob": "
|
|
146
|
-
"
|
|
147
|
-
"
|
|
148
|
-
"
|
|
149
|
-
"
|
|
142
|
+
"chokidar": "~5.0.0",
|
|
143
|
+
"commander": "~15.0.0",
|
|
144
|
+
"dotenv": "~18.0.1",
|
|
145
|
+
"esbuild": "~0.28.2",
|
|
146
|
+
"fast-glob": "~3.3.3",
|
|
147
|
+
"get-tsconfig": "~4.10.1",
|
|
148
|
+
"hono": "~4.13.8",
|
|
149
|
+
"lodash.kebabcase": "~4.1.1",
|
|
150
|
+
"openapi-typescript": "~7.13.0",
|
|
151
|
+
"pg": "~8.23.0",
|
|
150
152
|
"prompts": "~2.4.2",
|
|
151
153
|
"tsx": "~4.20.3",
|
|
152
|
-
"yaml": "~2.
|
|
154
|
+
"yaml": "~2.9.1"
|
|
153
155
|
};
|
|
154
156
|
var devDependencies = {
|
|
155
157
|
"@geekmidas/testkit": "workspace:*",
|
|
156
|
-
"@types/lodash.kebabcase": "
|
|
158
|
+
"@types/lodash.kebabcase": "~4.1.9",
|
|
157
159
|
"@types/node": "~24.9.1",
|
|
158
|
-
"@types/pg": "~8.
|
|
160
|
+
"@types/pg": "~8.23.1",
|
|
159
161
|
"@types/prompts": "~2.4.9",
|
|
160
|
-
"typescript": "
|
|
161
|
-
"vitest": "
|
|
162
|
-
"zod": "~4.
|
|
162
|
+
"typescript": "~5.8.2",
|
|
163
|
+
"vitest": "~3.2.4",
|
|
164
|
+
"zod": "~4.6.5"
|
|
163
165
|
};
|
|
164
166
|
var peerDependencies = { "@geekmidas/telescope": "workspace:~" };
|
|
165
167
|
var peerDependenciesMeta = { "@geekmidas/telescope": { "optional": true } };
|
|
@@ -187,7 +189,7 @@ const logger$11 = console;
|
|
|
187
189
|
* Validate Dokploy token by making a test API call
|
|
188
190
|
*/
|
|
189
191
|
async function validateDokployToken(endpoint, token) {
|
|
190
|
-
const { DokployApi: DokployApi$1 } = await import("./dokploy-api-
|
|
192
|
+
const { DokployApi: DokployApi$1 } = await import("./dokploy-api-D2Ny4Jrc.mjs");
|
|
191
193
|
const api = new DokployApi$1({
|
|
192
194
|
baseUrl: endpoint,
|
|
193
195
|
token
|
|
@@ -415,11 +417,16 @@ var CronGenerator = class extends ConstructGenerator {
|
|
|
415
417
|
const provider = options?.provider || "aws-lambda";
|
|
416
418
|
const logger$12 = console;
|
|
417
419
|
const cronInfos = [];
|
|
420
|
+
if (provider === "server") {
|
|
421
|
+
await this.generateServerCronsFile(outputDir, constructs);
|
|
422
|
+
logger$12.log(`Generated server crons file with ${constructs.length} crons`);
|
|
423
|
+
return cronInfos;
|
|
424
|
+
}
|
|
418
425
|
if (constructs.length === 0 || provider !== "aws-lambda") return cronInfos;
|
|
419
426
|
const cronsDir = join(outputDir, "crons");
|
|
420
427
|
await mkdir(cronsDir, { recursive: true });
|
|
421
428
|
for (const { key, construct, path } of constructs) {
|
|
422
|
-
const handlerFile = await this.generateCronHandler(cronsDir, path.relative, key, context);
|
|
429
|
+
const handlerFile = await this.generateCronHandler(cronsDir, path.relative, key, context, construct.owner);
|
|
423
430
|
cronInfos.push({
|
|
424
431
|
name: key,
|
|
425
432
|
handler: relative(process.cwd(), handlerFile).replace(/\.ts$/, ".handler"),
|
|
@@ -436,17 +443,173 @@ var CronGenerator = class extends ConstructGenerator {
|
|
|
436
443
|
isConstruct(value) {
|
|
437
444
|
return Cron.isCron(value);
|
|
438
445
|
}
|
|
439
|
-
|
|
446
|
+
/**
|
|
447
|
+
* The crons file a server entry imports.
|
|
448
|
+
*
|
|
449
|
+
* Shaped after `generateServerSubscribersFile`: one module, every cron, a
|
|
450
|
+
* `setupCrons` the generated entry calls beside `setupSubscribers`.
|
|
451
|
+
*
|
|
452
|
+
* It picks its scheduler from what the process can reach. With a Postgres it
|
|
453
|
+
* uses pg-boss, whose schedules live in the database — so a deployment
|
|
454
|
+
* running four replicas fires each job once, which is the thing a timer in
|
|
455
|
+
* every process gets wrong and does not report. With no database it falls
|
|
456
|
+
* back to timers and says plainly that it is doing so.
|
|
457
|
+
*/
|
|
458
|
+
async generateServerCronsFile(outputDir, crons) {
|
|
459
|
+
await mkdir(outputDir, { recursive: true });
|
|
460
|
+
const cronsPath = join(outputDir, "crons.ts");
|
|
461
|
+
const importsByFile = /* @__PURE__ */ new Map();
|
|
462
|
+
for (const { path, key } of crons) {
|
|
463
|
+
const relativePath = relative(dirname(cronsPath), path.relative);
|
|
464
|
+
const importPath = relativePath.replace(/\.ts$/, ".js");
|
|
465
|
+
if (!importsByFile.has(importPath)) importsByFile.set(importPath, []);
|
|
466
|
+
importsByFile.get(importPath)?.push(key);
|
|
467
|
+
}
|
|
468
|
+
const imports = Array.from(importsByFile.entries()).map(([importPath, exports$1]) => `import { ${exports$1.join(", ")} } from '${importPath}';`).join("\n");
|
|
469
|
+
const entries = crons.map(({ key }) => ` { name: '${key}', cron: ${key} }`).join(",\n");
|
|
470
|
+
const content = `/**
|
|
471
|
+
* Generated cron setup.
|
|
472
|
+
*
|
|
473
|
+
* Schedules every cron this app declares, in the process that serves it.
|
|
474
|
+
*
|
|
475
|
+
* The schedule lives in Postgres, through pg-boss, in the database the worker
|
|
476
|
+
* named with \`.database(db)\`. That is what makes running several replicas
|
|
477
|
+
* safe: each job fires once, where a timer in every process fires it once per
|
|
478
|
+
* replica and reports nothing.
|
|
479
|
+
*
|
|
480
|
+
* No connection string appears here. The construct that owns the database is
|
|
481
|
+
* the only thing that knows its key, and it is resolved through service
|
|
482
|
+
* discovery like any other dependency.
|
|
483
|
+
*/
|
|
484
|
+
import type { EnvironmentParser } from '@geekmidas/envkit';
|
|
485
|
+
import type { Logger } from '@geekmidas/logger';
|
|
486
|
+
import { toCronExpression } from '@geekmidas/constructs/crons';
|
|
487
|
+
import { ServiceDiscovery } from '@geekmidas/services';
|
|
488
|
+
${imports}
|
|
489
|
+
|
|
490
|
+
const crons = [
|
|
491
|
+
${entries}
|
|
492
|
+
];
|
|
493
|
+
|
|
494
|
+
type Stoppable = { stop: () => Promise<void> | void };
|
|
495
|
+
const running: Stoppable[] = [];
|
|
496
|
+
|
|
497
|
+
export async function setupCrons(
|
|
498
|
+
envParser: EnvironmentParser<any>,
|
|
499
|
+
logger: Logger,
|
|
500
|
+
): Promise<void> {
|
|
501
|
+
if (crons.length === 0) {
|
|
502
|
+
return;
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
const serviceDiscovery = ServiceDiscovery.getInstance(envParser);
|
|
506
|
+
|
|
507
|
+
// Resolved once per cron rather than per firing: registering services on
|
|
508
|
+
// every tick would reconnect a database every minute.
|
|
509
|
+
const prepared = [];
|
|
510
|
+
for (const { name, cron } of crons) {
|
|
511
|
+
try {
|
|
512
|
+
const schedule = toCronExpression(cron.schedule);
|
|
513
|
+
const services = cron.services.length > 0
|
|
514
|
+
? await serviceDiscovery.register(cron.services)
|
|
515
|
+
: {};
|
|
516
|
+
|
|
517
|
+
prepared.push({ name, cron, schedule, services });
|
|
518
|
+
} catch (error) {
|
|
519
|
+
// An unrepresentable schedule is a build-time mistake, and failing the
|
|
520
|
+
// process for it would take the HTTP server down with it.
|
|
521
|
+
logger.error({ error, cron: name }, 'Cron has no runnable schedule, skipping');
|
|
522
|
+
}
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
const run = async (entry: (typeof prepared)[number]) => {
|
|
526
|
+
try {
|
|
527
|
+
await entry.cron.handler({
|
|
528
|
+
input: undefined,
|
|
529
|
+
services: entry.services,
|
|
530
|
+
logger: entry.cron.logger,
|
|
531
|
+
});
|
|
532
|
+
} catch (error) {
|
|
533
|
+
logger.error({ error, cron: entry.name }, 'Cron failed');
|
|
534
|
+
}
|
|
535
|
+
};
|
|
536
|
+
|
|
537
|
+
// Declared once on the worker, carried onto every cron it built — so this
|
|
538
|
+
// reads it from any of them rather than from config or the environment.
|
|
539
|
+
const scheduleStore = prepared.find(({ cron }) => cron.scheduleStore)?.cron
|
|
540
|
+
.scheduleStore;
|
|
541
|
+
|
|
542
|
+
if (!scheduleStore) {
|
|
543
|
+
// Declared, not discovered. A worker says where its schedules live with
|
|
544
|
+
// \`.database(db)\`; inferring one from whatever database happened to be
|
|
545
|
+
// around would move the schedules the day an app declared a second.
|
|
546
|
+
logger.error(
|
|
547
|
+
{ crons: prepared.map(({ name }) => name) },
|
|
548
|
+
'These crons have nowhere to keep their schedule. A server fires its own ' +
|
|
549
|
+
'crons, and a process scheduling in memory fires each job once per ' +
|
|
550
|
+
'replica — so the schedule lives in Postgres. Say where with ' +
|
|
551
|
+
'\`new Worker(…).database(db)\`.',
|
|
552
|
+
);
|
|
553
|
+
return;
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
const [store] = await serviceDiscovery
|
|
557
|
+
.register([scheduleStore])
|
|
558
|
+
.then((s) => Object.values(s));
|
|
559
|
+
|
|
560
|
+
const { PgBoss } = await import('pg-boss');
|
|
561
|
+
// The pool the construct already opened — no connection string is read here,
|
|
562
|
+
// and none is named anywhere outside the construct that owns it.
|
|
563
|
+
const boss = new PgBoss({ db: store as never });
|
|
564
|
+
await boss.start();
|
|
565
|
+
|
|
566
|
+
// What this app declares now. Anything else under the prefix belonged to a
|
|
567
|
+
// cron since renamed or deleted, and would keep firing at a handler that has
|
|
568
|
+
// gone.
|
|
569
|
+
//
|
|
570
|
+
// The prefix is \`cron.\` rather than \`cron:\`: pg-boss accepts
|
|
571
|
+
// alphanumerics, underscores, hyphens, periods and slashes in a queue name,
|
|
572
|
+
// and rejects a colon outright.
|
|
573
|
+
const declared = new Set(prepared.map(({ name }) => \`cron.\${name}\`));
|
|
574
|
+
for (const existing of await boss.getSchedules()) {
|
|
575
|
+
if (existing.name.startsWith('cron.') && !declared.has(existing.name)) {
|
|
576
|
+
await boss.unschedule(existing.name);
|
|
577
|
+
logger.info({ cron: existing.name }, 'Removed a schedule nothing declares');
|
|
578
|
+
}
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
for (const entry of prepared) {
|
|
582
|
+
const queue = \`cron.\${entry.name}\`;
|
|
583
|
+
await boss.createQueue(queue);
|
|
584
|
+
// UTC, so this agrees with the schedule a deploy target would have made.
|
|
585
|
+
await boss.schedule(queue, entry.schedule, undefined, { tz: 'UTC' });
|
|
586
|
+
await boss.work(queue, () => run(entry));
|
|
587
|
+
|
|
588
|
+
logger.info({ cron: entry.name, schedule: entry.schedule }, 'Cron scheduled');
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
running.push({ stop: () => boss.stop() });
|
|
592
|
+
logger.info({ count: prepared.length }, 'Crons scheduled');
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
export async function stopCrons(): Promise<void> {
|
|
596
|
+
await Promise.all(running.map((r) => r.stop()));
|
|
597
|
+
running.length = 0;
|
|
598
|
+
}
|
|
599
|
+
`;
|
|
600
|
+
await writeFile(cronsPath, content);
|
|
601
|
+
return cronsPath;
|
|
602
|
+
}
|
|
603
|
+
async generateCronHandler(outputDir, sourceFile, exportName, context, owner) {
|
|
440
604
|
const handlerFileName = `${exportName}.ts`;
|
|
441
605
|
const handlerPath = join(outputDir, handlerFileName);
|
|
442
606
|
const relativePath = relative(dirname(handlerPath), sourceFile);
|
|
443
607
|
const importPath = relativePath.replace(/\.ts$/, ".js");
|
|
444
|
-
const
|
|
445
|
-
const
|
|
446
|
-
const content = `import { AWSScheduledFunction } from '@geekmidas/constructs/crons';
|
|
608
|
+
const runtime = runtimeFor(context, dirname(handlerPath), owner);
|
|
609
|
+
const content = `import { AWSScheduledFunction } from '@geekmidas/constructs/aws';
|
|
447
610
|
import { ${exportName} } from '${importPath}';
|
|
448
|
-
|
|
449
|
-
|
|
611
|
+
${runtime.imports}
|
|
612
|
+
${runtime.bindings}
|
|
450
613
|
|
|
451
614
|
const adapter = new AWSScheduledFunction(envParser, ${exportName});
|
|
452
615
|
|
|
@@ -562,7 +725,7 @@ var FunctionGenerator = class extends ConstructGenerator {
|
|
|
562
725
|
const functionsDir = join(outputDir, "functions");
|
|
563
726
|
await mkdir(functionsDir, { recursive: true });
|
|
564
727
|
for (const { key, construct, path } of constructs) {
|
|
565
|
-
const handlerFile = await this.generateFunctionHandler(functionsDir, path.relative, key, context);
|
|
728
|
+
const handlerFile = await this.generateFunctionHandler(functionsDir, path.relative, key, context, construct.owner);
|
|
566
729
|
functionInfos.push({
|
|
567
730
|
name: key,
|
|
568
731
|
handler: relative(process.cwd(), handlerFile).replace(/\.ts$/, ".handler"),
|
|
@@ -575,17 +738,16 @@ var FunctionGenerator = class extends ConstructGenerator {
|
|
|
575
738
|
}
|
|
576
739
|
return functionInfos;
|
|
577
740
|
}
|
|
578
|
-
async generateFunctionHandler(outputDir, sourceFile, exportName, context) {
|
|
741
|
+
async generateFunctionHandler(outputDir, sourceFile, exportName, context, owner) {
|
|
579
742
|
const handlerFileName = `${exportName}.ts`;
|
|
580
743
|
const handlerPath = join(outputDir, handlerFileName);
|
|
581
744
|
const relativePath = relative(dirname(handlerPath), sourceFile);
|
|
582
745
|
const importPath = relativePath.replace(/\.ts$/, ".js");
|
|
583
|
-
const
|
|
584
|
-
const relativeLoggerPath = relative(dirname(handlerPath), context.loggerPath);
|
|
746
|
+
const runtime = runtimeFor(context, dirname(handlerPath), owner);
|
|
585
747
|
const content = `import { AWSLambdaFunction } from '@geekmidas/constructs/aws';
|
|
586
748
|
import { ${exportName} } from '${importPath}';
|
|
587
|
-
|
|
588
|
-
|
|
749
|
+
${runtime.imports}
|
|
750
|
+
${runtime.bindings}
|
|
589
751
|
|
|
590
752
|
const adapter = new AWSLambdaFunction(envParser, ${exportName});
|
|
591
753
|
|
|
@@ -624,7 +786,7 @@ var QueueGenerator = class extends ConstructGenerator {
|
|
|
624
786
|
const queuesDir = join(outputDir, "queues");
|
|
625
787
|
await mkdir(queuesDir, { recursive: true });
|
|
626
788
|
for (const { key, construct, path } of constructs) {
|
|
627
|
-
const handlerFile = await this.generateQueueHandler(queuesDir, path.relative, key, context);
|
|
789
|
+
const handlerFile = await this.generateQueueHandler(queuesDir, path.relative, key, context, construct.owner);
|
|
628
790
|
queueInfos.push({
|
|
629
791
|
name: construct.name,
|
|
630
792
|
handler: relative(process.cwd(), handlerFile).replace(/\.ts$/, ".handler"),
|
|
@@ -638,13 +800,14 @@ var QueueGenerator = class extends ConstructGenerator {
|
|
|
638
800
|
}
|
|
639
801
|
return queueInfos;
|
|
640
802
|
}
|
|
641
|
-
async generateQueueHandler(outputDir, sourceFile, exportName, context) {
|
|
803
|
+
async generateQueueHandler(outputDir, sourceFile, exportName, context, owner) {
|
|
642
804
|
const handlerPath = join(outputDir, `${exportName}.ts`);
|
|
643
805
|
const importPath = relative(dirname(handlerPath), sourceFile).replace(/\.ts$/, ".js");
|
|
644
|
-
const
|
|
806
|
+
const runtime = runtimeFor(context, dirname(handlerPath), owner);
|
|
645
807
|
const content = `import { AWSLambdaQueue } from '@geekmidas/constructs/aws';
|
|
646
808
|
import { ${exportName} } from '${importPath}';
|
|
647
|
-
|
|
809
|
+
${runtime.imports}
|
|
810
|
+
${runtime.bindings}
|
|
648
811
|
|
|
649
812
|
const adapter = new AWSLambdaQueue(envParser, ${exportName});
|
|
650
813
|
|
|
@@ -787,7 +950,7 @@ var SubscriberGenerator = class extends ConstructGenerator {
|
|
|
787
950
|
const subscribersDir = join(outputDir, "subscribers");
|
|
788
951
|
await mkdir(subscribersDir, { recursive: true });
|
|
789
952
|
for (const { key, construct, path } of constructs) {
|
|
790
|
-
const handlerFile = await this.generateSubscriberHandler(subscribersDir, path.relative, key, construct, context);
|
|
953
|
+
const handlerFile = await this.generateSubscriberHandler(subscribersDir, path.relative, key, construct, context, construct.owner);
|
|
791
954
|
subscriberInfos.push({
|
|
792
955
|
name: key,
|
|
793
956
|
handler: relative(process.cwd(), handlerFile).replace(/\.ts$/, ".handler"),
|
|
@@ -805,15 +968,16 @@ var SubscriberGenerator = class extends ConstructGenerator {
|
|
|
805
968
|
}
|
|
806
969
|
return subscriberInfos;
|
|
807
970
|
}
|
|
808
|
-
async generateSubscriberHandler(outputDir, sourceFile, exportName, _subscriber, context) {
|
|
971
|
+
async generateSubscriberHandler(outputDir, sourceFile, exportName, _subscriber, context, owner) {
|
|
809
972
|
const handlerFileName = `${exportName}.ts`;
|
|
810
973
|
const handlerPath = join(outputDir, handlerFileName);
|
|
811
974
|
const relativePath = relative(dirname(handlerPath), sourceFile);
|
|
812
975
|
const importPath = relativePath.replace(/\.ts$/, ".js");
|
|
813
|
-
const
|
|
976
|
+
const runtime = runtimeFor(context, dirname(handlerPath), owner);
|
|
814
977
|
const content = `import { AWSLambdaSubscriber } from '@geekmidas/constructs/aws';
|
|
815
978
|
import { ${exportName} } from '${importPath}';
|
|
816
|
-
|
|
979
|
+
${runtime.imports}
|
|
980
|
+
${runtime.bindings}
|
|
817
981
|
|
|
818
982
|
const adapter = new AWSLambdaSubscriber(envParser, ${exportName});
|
|
819
983
|
|
|
@@ -1196,13 +1360,7 @@ async function devCommand(options) {
|
|
|
1196
1360
|
}
|
|
1197
1361
|
const resolved = resolveProviders(config, { provider: "server" });
|
|
1198
1362
|
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 } = parseModuleConfig(config.envParser, "envParser");
|
|
1205
|
-
const { path: loggerPath, importPattern: loggerImportPattern } = parseModuleConfig(config.logger, "logger");
|
|
1363
|
+
logger$9.log(`Loading constructs from: ${config.constructs}`);
|
|
1206
1364
|
const telescope = normalizeTelescopeConfig(config.telescope);
|
|
1207
1365
|
if (telescope) logger$9.log(`🔭 Telescope enabled at ${telescope.path}`);
|
|
1208
1366
|
const studio = normalizeStudioConfig(config.studio);
|
|
@@ -1213,21 +1371,17 @@ async function devCommand(options) {
|
|
|
1213
1371
|
const enableOpenApi = openApiConfig.enabled || resolved.enableOpenApi;
|
|
1214
1372
|
if (enableOpenApi) logger$9.log(`📄 OpenAPI output: ${OPENAPI_OUTPUT_PATH}`);
|
|
1215
1373
|
const buildContext = {
|
|
1216
|
-
envParserPath,
|
|
1217
|
-
envParserImportPattern,
|
|
1218
|
-
loggerPath,
|
|
1219
|
-
loggerImportPattern,
|
|
1220
1374
|
telescope,
|
|
1221
1375
|
studio,
|
|
1222
1376
|
hooks
|
|
1223
1377
|
};
|
|
1224
1378
|
await buildServer(config, buildContext, resolved.providers[0], enableOpenApi, appRoot);
|
|
1225
|
-
if (enableOpenApi) await
|
|
1379
|
+
if (enableOpenApi) await generateOpenApiFrom(config.constructs, { openapi: config.openapi });
|
|
1226
1380
|
const runtime = config.runtime ?? "node";
|
|
1227
1381
|
let secretsJsonPath;
|
|
1228
|
-
const appSecrets = await loadSecretsForApp(secretsRoot, workspaceAppName);
|
|
1382
|
+
const appSecrets = await loadSecretsForApp(secretsRoot, config.stages.local, workspaceAppName);
|
|
1229
1383
|
if (workspace && usesConstructs(workspace)) {
|
|
1230
|
-
const reconciled = await reconcileWorkspace(workspace, { stage:
|
|
1384
|
+
const reconciled = await reconcileWorkspace(workspace, { stage: workspace.stages.local });
|
|
1231
1385
|
if (reconciled.changed && reconciled.plan.containers.length > 0) {
|
|
1232
1386
|
logger$9.log(`🐳 Services: ${reconciled.plan.containers.join(", ")}`);
|
|
1233
1387
|
for (const [container, address] of Object.entries(reconciled.addresses)) logger$9.log(` ${container}: ${address}`);
|
|
@@ -1255,19 +1409,9 @@ async function devCommand(options) {
|
|
|
1255
1409
|
}
|
|
1256
1410
|
const devServer = new DevServer(resolved.providers[0], options.port ?? workspaceAppPort ?? 3e3, options.portExplicit ?? false, enableOpenApi, telescope, studio, runtime, appRoot, secretsJsonPath);
|
|
1257
1411
|
await devServer.start();
|
|
1258
|
-
const envParserFile = config.envParser.split("#")[0] ?? config.envParser;
|
|
1259
|
-
const loggerFile = config.logger.split("#")[0] ?? config.logger;
|
|
1260
1412
|
const hooksFileParts = config.hooks?.server?.split("#");
|
|
1261
1413
|
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");
|
|
1414
|
+
const watchPatterns = [config.constructs, ...hooksFile ? [hooksFile.endsWith(".ts") ? hooksFile : `${hooksFile}.ts`] : []].flat().filter((p) => typeof p === "string");
|
|
1271
1415
|
const normalizedPatterns = watchPatterns.map((p) => p.startsWith("./") ? p.slice(2) : p);
|
|
1272
1416
|
logger$9.log(`👀 Watching for changes in: ${normalizedPatterns.join(", ")}`);
|
|
1273
1417
|
const resolvedFiles = await fg(normalizedPatterns, {
|
|
@@ -1300,7 +1444,8 @@ async function devCommand(options) {
|
|
|
1300
1444
|
try {
|
|
1301
1445
|
logger$9.log("🔄 Rebuilding...");
|
|
1302
1446
|
await buildServer(config, buildContext, resolved.providers[0], enableOpenApi, appRoot, true);
|
|
1303
|
-
if (enableOpenApi) await
|
|
1447
|
+
if (enableOpenApi) await generateOpenApiFrom(config.constructs, {
|
|
1448
|
+
openapi: config.openapi,
|
|
1304
1449
|
silent: true,
|
|
1305
1450
|
bustCache: true
|
|
1306
1451
|
});
|
|
@@ -1518,15 +1663,15 @@ async function validateFrontendApps(workspace) {
|
|
|
1518
1663
|
*/
|
|
1519
1664
|
async function loadDevSecrets(workspace) {
|
|
1520
1665
|
if (!workspace.secrets.enabled) return {};
|
|
1521
|
-
const
|
|
1522
|
-
|
|
1666
|
+
const stage = workspace.stages.local;
|
|
1667
|
+
if (secretsExist(stage, workspace.root)) {
|
|
1523
1668
|
const secrets = await readStageSecrets(stage, workspace.root);
|
|
1524
1669
|
if (secrets) {
|
|
1525
1670
|
logger$9.log(`🔐 Loading secrets from stage: ${stage}`);
|
|
1526
1671
|
return toEmbeddableSecrets(secrets);
|
|
1527
1672
|
}
|
|
1528
1673
|
}
|
|
1529
|
-
logger$9.warn(
|
|
1674
|
+
logger$9.warn(`⚠️ Secrets enabled but no "${stage}" secrets found. Run "gkm setup" to initialize the local stage`);
|
|
1530
1675
|
return {};
|
|
1531
1676
|
}
|
|
1532
1677
|
/**
|
|
@@ -1570,7 +1715,7 @@ async function workspaceDevCommand(workspace, options) {
|
|
|
1570
1715
|
const rawSecrets = await loadDevSecrets(workspace);
|
|
1571
1716
|
let secretsEnv;
|
|
1572
1717
|
if (usesConstructs(workspace)) {
|
|
1573
|
-
const reconciled = await reconcileWorkspace(workspace, { stage:
|
|
1718
|
+
const reconciled = await reconcileWorkspace(workspace, { stage: workspace.stages.local });
|
|
1574
1719
|
if (reconciled.changed && reconciled.plan.containers.length > 0) {
|
|
1575
1720
|
logger$9.log(`🐳 Services: ${reconciled.plan.containers.join(", ")}`);
|
|
1576
1721
|
for (const [container, address] of Object.entries(reconciled.addresses)) logger$9.log(` ${container}: ${address}`);
|
|
@@ -1714,7 +1859,7 @@ async function buildServer(config, context, provider, enableOpenApi, appRoot = p
|
|
|
1714
1859
|
const queueGenerator = new QueueGenerator();
|
|
1715
1860
|
const topicGenerator = new TopicGenerator();
|
|
1716
1861
|
const [allEndpoints, allFunctions, allCrons, allSubscribers, allQueues, allTopics] = await Promise.all([
|
|
1717
|
-
endpointGenerator.load(config.
|
|
1862
|
+
endpointGenerator.load(config.constructs, appRoot, bustCache),
|
|
1718
1863
|
config.functions ? functionGenerator.load(config.functions, appRoot, bustCache) : [],
|
|
1719
1864
|
config.crons ? cronGenerator.load(config.crons, appRoot, bustCache) : [],
|
|
1720
1865
|
config.subscribers ? subscriberGenerator.load(config.subscribers, appRoot, bustCache) : [],
|
|
@@ -2302,18 +2447,36 @@ export default app;
|
|
|
2302
2447
|
`);
|
|
2303
2448
|
}
|
|
2304
2449
|
const logger$7 = console;
|
|
2450
|
+
/**
|
|
2451
|
+
* What a build run at the workspace root reads.
|
|
2452
|
+
*
|
|
2453
|
+
* One backend app there, and it is that app's config, so its own `openapi`
|
|
2454
|
+
* and `telescope` settings still apply. Otherwise it is the workspace's: every
|
|
2455
|
+
* construct glob, and the backends they resolve to.
|
|
2456
|
+
*/
|
|
2457
|
+
function rootGkmConfig(workspace) {
|
|
2458
|
+
const backends = Object.entries(workspace.apps).filter(([, app]) => app.type === "backend");
|
|
2459
|
+
const only = backends.length === 1 ? backends[0] : void 0;
|
|
2460
|
+
const own = only ? getAppGkmConfig(workspace, only[0]) : void 0;
|
|
2461
|
+
return own ?? {
|
|
2462
|
+
services: workspace.services,
|
|
2463
|
+
stages: workspace.stages,
|
|
2464
|
+
constructs: allConstructGlobs(workspace)
|
|
2465
|
+
};
|
|
2466
|
+
}
|
|
2305
2467
|
async function buildCommand(options) {
|
|
2306
2468
|
const loadedConfig = await loadWorkspaceConfig();
|
|
2307
2469
|
if (loadedConfig.type === "workspace") {
|
|
2308
2470
|
const cwd = resolve(process.cwd());
|
|
2309
|
-
const workspaceRoot = resolve(loadedConfig.workspace.root);
|
|
2310
|
-
const isAtWorkspaceRoot = cwd === workspaceRoot;
|
|
2311
|
-
|
|
2471
|
+
const workspaceRoot$1 = resolve(loadedConfig.workspace.root);
|
|
2472
|
+
const isAtWorkspaceRoot = cwd === workspaceRoot$1;
|
|
2473
|
+
const someAppIsElsewhere = Object.values(loadedConfig.workspace.apps).some((app) => resolve(workspaceRoot$1, app.path) !== workspaceRoot$1);
|
|
2474
|
+
if (isAtWorkspaceRoot && someAppIsElsewhere) {
|
|
2312
2475
|
logger$7.log("📦 Detected workspace configuration");
|
|
2313
2476
|
return workspaceBuildCommand(loadedConfig.workspace, options);
|
|
2314
2477
|
}
|
|
2315
2478
|
}
|
|
2316
|
-
const config = loadedConfig.type
|
|
2479
|
+
const config = loadedConfig.type !== "workspace" ? await loadConfig() : resolve(process.cwd()) === resolve(loadedConfig.workspace.root) ? rootGkmConfig(loadedConfig.workspace) : (await loadAppConfig()).gkmConfig;
|
|
2317
2480
|
const resolved = resolveProviders(config, options);
|
|
2318
2481
|
const backendConfig = config.services ?? loadedConfig.workspace?.services;
|
|
2319
2482
|
const cacheBackend = cacheBackendOf(backendConfig?.cache);
|
|
@@ -2321,13 +2484,7 @@ async function buildCommand(options) {
|
|
|
2321
2484
|
const production = normalizeProductionConfig(options.production ?? false, productionConfigFromGkm);
|
|
2322
2485
|
if (production) logger$7.log(`🏭 Building for PRODUCTION`);
|
|
2323
2486
|
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 } = parseModuleConfig(config.envParser, "envParser");
|
|
2330
|
-
const { path: loggerPath, importPattern: loggerImportPattern } = parseModuleConfig(config.logger, "logger");
|
|
2487
|
+
logger$7.log(`Loading constructs from: ${formatRoutes(config.constructs)}`);
|
|
2331
2488
|
const telescope = production ? void 0 : normalizeTelescopeConfig(config.telescope);
|
|
2332
2489
|
if (telescope) logger$7.log(`🔭 Telescope enabled at ${telescope.path}`);
|
|
2333
2490
|
const studio = production ? void 0 : normalizeStudioConfig(config.studio);
|
|
@@ -2352,10 +2509,23 @@ async function buildCommand(options) {
|
|
|
2352
2509
|
sources: constructSources
|
|
2353
2510
|
}) : {};
|
|
2354
2511
|
const surfaces = Object.values(declared).filter((d) => d.kind === "rest-api");
|
|
2355
|
-
const
|
|
2512
|
+
const workspaceRoot = loadedConfig.workspace.root;
|
|
2513
|
+
const served = surfaces.find((d) => resolve(workspaceRoot, d.path) === resolve(process.cwd()));
|
|
2514
|
+
const primary = served ?? surfaces.find((d) => d.endpoints.length === 0) ?? surfaces[0];
|
|
2356
2515
|
const browsable = Object.entries(declared).find(([, d]) => d.kind === "database");
|
|
2357
2516
|
const browsableSource = browsable ? constructSources[browsable[0]] : void 0;
|
|
2517
|
+
const owners = {};
|
|
2518
|
+
for (const [id, declaration] of Object.entries(declared)) {
|
|
2519
|
+
if (declaration.kind !== "rest-api" && declaration.kind !== "worker") continue;
|
|
2520
|
+
const source = constructSources[id];
|
|
2521
|
+
if (!source) continue;
|
|
2522
|
+
owners[id] = {
|
|
2523
|
+
specifier: source.file,
|
|
2524
|
+
exportName: source.exportName
|
|
2525
|
+
};
|
|
2526
|
+
}
|
|
2358
2527
|
const buildContext = {
|
|
2528
|
+
owners,
|
|
2359
2529
|
...primary ? { surface: {
|
|
2360
2530
|
id: primary.id,
|
|
2361
2531
|
trustedOriginsKey: provideKey(primary.id, "trustedOrigins"),
|
|
@@ -2365,10 +2535,6 @@ async function buildCommand(options) {
|
|
|
2365
2535
|
exportName: constructSources[primary.id].exportName
|
|
2366
2536
|
} } : {}
|
|
2367
2537
|
} } : {},
|
|
2368
|
-
envParserPath,
|
|
2369
|
-
envParserImportPattern,
|
|
2370
|
-
loggerPath,
|
|
2371
|
-
loggerImportPattern,
|
|
2372
2538
|
telescope,
|
|
2373
2539
|
studio: studio && browsableSource ? {
|
|
2374
2540
|
...studio,
|
|
@@ -2395,14 +2561,16 @@ async function buildCommand(options) {
|
|
|
2395
2561
|
const subscriberGenerator = new SubscriberGenerator();
|
|
2396
2562
|
const queueGenerator = new QueueGenerator();
|
|
2397
2563
|
const topicGenerator = new TopicGenerator();
|
|
2398
|
-
const
|
|
2399
|
-
|
|
2400
|
-
|
|
2401
|
-
|
|
2402
|
-
|
|
2403
|
-
|
|
2404
|
-
|
|
2564
|
+
const code = constructGlobs$1;
|
|
2565
|
+
const [loadedEndpoints, allFunctions, allCrons, allSubscribers, allQueues, allTopics] = await Promise.all([
|
|
2566
|
+
endpointGenerator.load(code),
|
|
2567
|
+
functionGenerator.load(code),
|
|
2568
|
+
cronGenerator.load(code),
|
|
2569
|
+
subscriberGenerator.load(code),
|
|
2570
|
+
queueGenerator.load(code),
|
|
2571
|
+
topicGenerator.load(code)
|
|
2405
2572
|
]);
|
|
2573
|
+
const allEndpoints = primary ? loadedEndpoints.filter(({ construct }) => !construct.surface || construct.surface.id === primary.id) : loadedEndpoints;
|
|
2406
2574
|
logger$7.log(`Found ${allEndpoints.length} endpoints`);
|
|
2407
2575
|
logger$7.log(`Found ${allFunctions.length} functions`);
|
|
2408
2576
|
logger$7.log(`Found ${allCrons.length} crons`);
|
|
@@ -2410,7 +2578,7 @@ async function buildCommand(options) {
|
|
|
2410
2578
|
logger$7.log(`Found ${allQueues.length} queues`);
|
|
2411
2579
|
logger$7.log(`Found ${allTopics.length} topics`);
|
|
2412
2580
|
if (allEndpoints.length === 0 && allFunctions.length === 0 && allCrons.length === 0 && allSubscribers.length === 0 && allQueues.length === 0 && allTopics.length === 0) {
|
|
2413
|
-
const selfServing =
|
|
2581
|
+
const selfServing = served && served.endpoints.length > 0 && constructSources[served.id] !== void 0 ? [served.id, served] : void 0;
|
|
2414
2582
|
if (selfServing) {
|
|
2415
2583
|
const [id] = selfServing;
|
|
2416
2584
|
await writeSurfaceEntry(join(process.cwd(), ".gkm", "server"), constructSources[id]);
|
|
@@ -2427,7 +2595,7 @@ async function buildCommand(options) {
|
|
|
2427
2595
|
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
2596
|
if (providerResult.masterKey) result = providerResult;
|
|
2429
2597
|
}
|
|
2430
|
-
await generateOpenApi(
|
|
2598
|
+
await generateOpenApi(allEndpoints.map(({ construct }) => construct), { openapi: config.openapi });
|
|
2431
2599
|
return result;
|
|
2432
2600
|
}
|
|
2433
2601
|
async function buildForProvider(provider, context, rootOutputDir, endpointGenerator, functionGenerator, cronGenerator, subscriberGenerator, queueGenerator, topicGenerator, endpoints, functions, crons, subscribers, queues, topics, enableOpenApi, skipBundle, stage) {
|
|
@@ -2472,7 +2640,7 @@ async function buildForProvider(provider, context, rootOutputDir, endpointGenera
|
|
|
2472
2640
|
let masterKey;
|
|
2473
2641
|
if (context.production?.bundle && !skipBundle) {
|
|
2474
2642
|
logger$7.log(`\n📦 Bundling production server...`);
|
|
2475
|
-
const { bundleServer } = await import("./bundler-
|
|
2643
|
+
const { bundleServer } = await import("./bundler-S3ek7ea-.mjs");
|
|
2476
2644
|
const allConstructs = [
|
|
2477
2645
|
...endpoints.map((e) => e.construct),
|
|
2478
2646
|
...functions.map((f) => f.construct),
|
|
@@ -2681,7 +2849,6 @@ async function workspaceBuildCommand(workspace, options) {
|
|
|
2681
2849
|
});
|
|
2682
2850
|
}
|
|
2683
2851
|
logger$7.log(`\n✅ Workspace build complete!`);
|
|
2684
|
-
await openapiCommand({ cwd: workspace.root });
|
|
2685
2852
|
logger$7.log(`\n📋 Build Summary:`);
|
|
2686
2853
|
for (const result of results) {
|
|
2687
2854
|
const icon = result.type === "backend" ? "⚙️" : result.type === "mobile" ? "📱" : "🌐";
|
|
@@ -3051,7 +3218,7 @@ function minioCompose(options) {
|
|
|
3051
3218
|
return [
|
|
3052
3219
|
"services:",
|
|
3053
3220
|
` ${service}:`,
|
|
3054
|
-
" image:
|
|
3221
|
+
" image: pgsty/minio:RELEASE.2026-08-04T00-00-00Z",
|
|
3055
3222
|
` command: server /data --console-address ":9001"`,
|
|
3056
3223
|
" environment:",
|
|
3057
3224
|
` MINIO_ROOT_USER: ${user}`,
|
|
@@ -3426,11 +3593,11 @@ async function createDnsProvider(options) {
|
|
|
3426
3593
|
if (isDnsProvider(config.provider)) return config.provider;
|
|
3427
3594
|
const provider = config.provider;
|
|
3428
3595
|
if (provider === "hostinger") {
|
|
3429
|
-
const { HostingerProvider } = await import("./HostingerProvider-
|
|
3596
|
+
const { HostingerProvider } = await import("./HostingerProvider-BcyFRbqV.mjs");
|
|
3430
3597
|
return new HostingerProvider();
|
|
3431
3598
|
}
|
|
3432
3599
|
if (provider === "route53") {
|
|
3433
|
-
const { Route53Provider } = await import("./Route53Provider-
|
|
3600
|
+
const { Route53Provider } = await import("./Route53Provider-C0xmer8U.mjs");
|
|
3434
3601
|
const route53Config = config;
|
|
3435
3602
|
return new Route53Provider({
|
|
3436
3603
|
region: route53Config.region,
|
|
@@ -3870,7 +4037,7 @@ const DEFAULT_SERVICE_IMAGES = {
|
|
|
3870
4037
|
postgres: "postgres",
|
|
3871
4038
|
redis: "redis",
|
|
3872
4039
|
rabbitmq: "rabbitmq",
|
|
3873
|
-
minio: "
|
|
4040
|
+
minio: "pgsty/minio",
|
|
3874
4041
|
mailpit: "axllent/mailpit",
|
|
3875
4042
|
localstack: "floci/floci"
|
|
3876
4043
|
};
|
|
@@ -3879,7 +4046,7 @@ const DEFAULT_SERVICE_VERSIONS = {
|
|
|
3879
4046
|
postgres: `${DEFAULT_POSTGRES_VERSION}-alpine`,
|
|
3880
4047
|
redis: "7-alpine",
|
|
3881
4048
|
rabbitmq: "3-management-alpine",
|
|
3882
|
-
minio: "RELEASE.
|
|
4049
|
+
minio: "RELEASE.2026-08-04T00-00-00Z",
|
|
3883
4050
|
mailpit: "latest",
|
|
3884
4051
|
localstack: "latest"
|
|
3885
4052
|
};
|
|
@@ -4331,7 +4498,7 @@ function getInfraServiceImage(serviceName$1, pin) {
|
|
|
4331
4498
|
const defaults = {
|
|
4332
4499
|
postgres: "postgres:18-alpine",
|
|
4333
4500
|
redis: "redis:7-alpine",
|
|
4334
|
-
minio: "
|
|
4501
|
+
minio: "pgsty/minio:RELEASE.2026-08-04T00-00-00Z"
|
|
4335
4502
|
};
|
|
4336
4503
|
return pin || defaults[serviceName$1];
|
|
4337
4504
|
}
|
|
@@ -5531,7 +5698,7 @@ async function workspaceDockerCommand(workspace, options) {
|
|
|
5531
5698
|
logger$5.log(`\n Generated: .dockerignore (workspace root)`);
|
|
5532
5699
|
const dockerCompose = generateWorkspaceCompose(workspace, {
|
|
5533
5700
|
registry: options.registry,
|
|
5534
|
-
containers: await derivedContainers(workspace,
|
|
5701
|
+
containers: await derivedContainers(workspace, workspace.stages.local)
|
|
5535
5702
|
});
|
|
5536
5703
|
const composePath = join(dockerDir, "docker-compose.yml");
|
|
5537
5704
|
await writeFile(composePath, dockerCompose);
|
|
@@ -5757,20 +5924,20 @@ function isStateProvider(value) {
|
|
|
5757
5924
|
async function createStateProvider(options) {
|
|
5758
5925
|
const { config, workspaceRoot, workspaceName } = options;
|
|
5759
5926
|
if (!config) {
|
|
5760
|
-
const { LocalStateProvider } = await import("./LocalStateProvider-
|
|
5927
|
+
const { LocalStateProvider } = await import("./LocalStateProvider-Bq-0C0sg.mjs");
|
|
5761
5928
|
return new LocalStateProvider(workspaceRoot);
|
|
5762
5929
|
}
|
|
5763
5930
|
if (isStateProvider(config.provider)) return config.provider;
|
|
5764
5931
|
const provider = config.provider;
|
|
5765
5932
|
if (provider === "local") {
|
|
5766
|
-
const { LocalStateProvider } = await import("./LocalStateProvider-
|
|
5933
|
+
const { LocalStateProvider } = await import("./LocalStateProvider-Bq-0C0sg.mjs");
|
|
5767
5934
|
return new LocalStateProvider(workspaceRoot);
|
|
5768
5935
|
}
|
|
5769
5936
|
if (provider === "ssm") {
|
|
5770
5937
|
if (!workspaceName) throw new Error("Workspace name is required for SSM state provider. Set \"name\" in gkm.config.ts.");
|
|
5771
|
-
const { LocalStateProvider } = await import("./LocalStateProvider-
|
|
5772
|
-
const { SSMStateProvider } = await import("./SSMStateProvider-
|
|
5773
|
-
const { CachedStateProvider: CachedStateProvider$1 } = await import("./CachedStateProvider-
|
|
5938
|
+
const { LocalStateProvider } = await import("./LocalStateProvider-Bq-0C0sg.mjs");
|
|
5939
|
+
const { SSMStateProvider } = await import("./SSMStateProvider-DOZifZfE.mjs");
|
|
5940
|
+
const { CachedStateProvider: CachedStateProvider$1 } = await import("./CachedStateProvider-C-8wtlto.mjs");
|
|
5774
5941
|
const ssmConfig = config;
|
|
5775
5942
|
const local = new LocalStateProvider(workspaceRoot);
|
|
5776
5943
|
const ssm = SSMStateProvider.create({
|
|
@@ -6646,7 +6813,7 @@ async function workspaceDeployCommand(configured, options) {
|
|
|
6646
6813
|
const backupCluster = Object.values(declaredClusters)[0];
|
|
6647
6814
|
if (workspace.deploy?.backups && backupCluster) {
|
|
6648
6815
|
logger$3.log("\n💾 Provisioning backup destination...");
|
|
6649
|
-
const { provisionBackupDestination } = await import("./backup-provisioner-
|
|
6816
|
+
const { provisionBackupDestination } = await import("./backup-provisioner-DXXg6FIL.mjs");
|
|
6650
6817
|
const backupState = await provisionBackupDestination({
|
|
6651
6818
|
api,
|
|
6652
6819
|
projectId: project.projectId,
|
|
@@ -6977,6 +7144,7 @@ async function workspaceDeployCommand(configured, options) {
|
|
|
6977
7144
|
*/
|
|
6978
7145
|
async function deployCommand(options) {
|
|
6979
7146
|
const loadedConfig = await loadWorkspaceConfig();
|
|
7147
|
+
assertDeployedStage(loadedConfig.workspace.stages, options.stage);
|
|
6980
7148
|
return workspaceDeployCommand(loadedConfig.workspace, options);
|
|
6981
7149
|
}
|
|
6982
7150
|
|
|
@@ -7342,6 +7510,501 @@ function printStateDetails(state) {
|
|
|
7342
7510
|
}
|
|
7343
7511
|
}
|
|
7344
7512
|
|
|
7513
|
+
//#endregion
|
|
7514
|
+
//#region src/init/constructs.ts
|
|
7515
|
+
/** The glob every generated config points at. One glob, every kind. */
|
|
7516
|
+
const CONSTRUCTS_GLOB = "./src/constructs/**/*.ts";
|
|
7517
|
+
/**
|
|
7518
|
+
* Where a workspace keeps its constructs: at the root, beside the apps that
|
|
7519
|
+
* share them, rather than inside any one of them.
|
|
7520
|
+
*/
|
|
7521
|
+
const WORKSPACE_CONSTRUCTS_GLOB = "./constructs/**/*.ts";
|
|
7522
|
+
/**
|
|
7523
|
+
* Where each routes structure puts an app's handlers, relative to the app.
|
|
7524
|
+
*
|
|
7525
|
+
* `.ts` only, never `.tsx`: a TanStack site keeps its own `src/routes/`, and
|
|
7526
|
+
* this glob must not import a frontend's route components.
|
|
7527
|
+
*/
|
|
7528
|
+
function routesGlob(structure) {
|
|
7529
|
+
switch (structure) {
|
|
7530
|
+
case "centralized-endpoints": return "./src/endpoints/**/*.ts";
|
|
7531
|
+
case "centralized-routes": return "./src/routes/**/*.ts";
|
|
7532
|
+
case "domain-based": return "./src/**/routes/*.ts";
|
|
7533
|
+
}
|
|
7534
|
+
}
|
|
7535
|
+
/**
|
|
7536
|
+
* Plain ids — `Database`, not `Beetlefit`.
|
|
7537
|
+
*
|
|
7538
|
+
* The workspace `name` already scopes every physical name
|
|
7539
|
+
* (`production-beetlefit-database`), so an id that carried the project too
|
|
7540
|
+
* would say it twice: `production-beetlefit-beetlefit-cache`. Within a
|
|
7541
|
+
* workspace the constructs are declared once, at its root, so there is no
|
|
7542
|
+
* second app's `Database` to collide with.
|
|
7543
|
+
*/
|
|
7544
|
+
function scaffolded(id) {
|
|
7545
|
+
return {
|
|
7546
|
+
id,
|
|
7547
|
+
service: serviceKey(id),
|
|
7548
|
+
urlKey: provideKey(id, "url")
|
|
7549
|
+
};
|
|
7550
|
+
}
|
|
7551
|
+
/** The database a project declares. */
|
|
7552
|
+
function databaseFor() {
|
|
7553
|
+
return {
|
|
7554
|
+
...scaffolded("Database"),
|
|
7555
|
+
ownerUrlKey: provideKey("Database", "ownerUrl")
|
|
7556
|
+
};
|
|
7557
|
+
}
|
|
7558
|
+
/** The bucket a project declares. */
|
|
7559
|
+
const storageFor = () => scaffolded("Uploads");
|
|
7560
|
+
/** The mail sender a project declares. */
|
|
7561
|
+
const emailFor = () => scaffolded("Mail");
|
|
7562
|
+
/** The cache a project declares. */
|
|
7563
|
+
const cacheFor = () => scaffolded("Cache");
|
|
7564
|
+
/**
|
|
7565
|
+
* The files a project's declared database needs.
|
|
7566
|
+
*
|
|
7567
|
+
* Shared by every template, so a worker and an API declare the same database
|
|
7568
|
+
* the same way — and so the schema type, the migration, and the id the test
|
|
7569
|
+
* setup reads all come from one place.
|
|
7570
|
+
*/
|
|
7571
|
+
function databaseFiles() {
|
|
7572
|
+
const db = databaseFor();
|
|
7573
|
+
return [{
|
|
7574
|
+
path: "src/constructs/database.ts",
|
|
7575
|
+
content: `import { KyselyDatabase } from '@geekmidas/constructs/database/kysely';
|
|
7576
|
+
import type { Generated } from 'kysely';
|
|
7577
|
+
|
|
7578
|
+
/** Your database schema. Add tables here. */
|
|
7579
|
+
export interface Database {
|
|
7580
|
+
users: {
|
|
7581
|
+
id: Generated<string>;
|
|
7582
|
+
name: string;
|
|
7583
|
+
email: string;
|
|
7584
|
+
created_at: Generated<Date>;
|
|
7585
|
+
};
|
|
7586
|
+
}
|
|
7587
|
+
|
|
7588
|
+
/**
|
|
7589
|
+
* The app's database, declared once.
|
|
7590
|
+
*
|
|
7591
|
+
* The container, the database inside it, its roles and schema, and
|
|
7592
|
+
* \`${db.urlKey}\` all derive from this line — \`gkm dev\` reconciles them
|
|
7593
|
+
* before the server starts, which is why nothing lists \`postgres\` anywhere.
|
|
7594
|
+
*
|
|
7595
|
+
* Both type arguments or neither: TypeScript has no partial type-argument
|
|
7596
|
+
* inference, so passing only \`Database\` would leave the name at \`string\`
|
|
7597
|
+
* and widen the service key away from \`${db.service}\`.
|
|
7598
|
+
*/
|
|
7599
|
+
export const database = new KyselyDatabase<Database, '${db.id}'>('${db.id}');
|
|
7600
|
+
`
|
|
7601
|
+
}, usersMigration()];
|
|
7602
|
+
}
|
|
7603
|
+
/**
|
|
7604
|
+
* The table the scaffolded endpoints and factories expect.
|
|
7605
|
+
*
|
|
7606
|
+
* Its own function because a workspace's API needs it without the database
|
|
7607
|
+
* construct beside it — that one lives at the workspace root.
|
|
7608
|
+
*/
|
|
7609
|
+
function usersMigration() {
|
|
7610
|
+
return {
|
|
7611
|
+
path: "src/db/migrations/001_create_users.ts",
|
|
7612
|
+
content: `import type { Kysely } from 'kysely';
|
|
7613
|
+
|
|
7614
|
+
export async function up(db: Kysely<unknown>): Promise<void> {
|
|
7615
|
+
await db.schema
|
|
7616
|
+
.createTable('users')
|
|
7617
|
+
.addColumn('id', 'uuid', (col) =>
|
|
7618
|
+
col.primaryKey().defaultTo(db.fn('gen_random_uuid')),
|
|
7619
|
+
)
|
|
7620
|
+
.addColumn('name', 'varchar(255)', (col) => col.notNull())
|
|
7621
|
+
.addColumn('email', 'varchar(255)', (col) => col.notNull().unique())
|
|
7622
|
+
.addColumn('created_at', 'timestamptz', (col) =>
|
|
7623
|
+
col.notNull().defaultTo(db.fn('now')),
|
|
7624
|
+
)
|
|
7625
|
+
.execute();
|
|
7626
|
+
|
|
7627
|
+
await db.schema
|
|
7628
|
+
.createIndex('users_email_idx')
|
|
7629
|
+
.on('users')
|
|
7630
|
+
.column('email')
|
|
7631
|
+
.execute();
|
|
7632
|
+
}
|
|
7633
|
+
|
|
7634
|
+
export async function down(db: Kysely<unknown>): Promise<void> {
|
|
7635
|
+
await db.schema.dropTable('users').execute();
|
|
7636
|
+
}
|
|
7637
|
+
`
|
|
7638
|
+
};
|
|
7639
|
+
}
|
|
7640
|
+
/**
|
|
7641
|
+
* What a workspace's `constructs` glob reaches: the root constructs, and the
|
|
7642
|
+
* handlers of every app laid out the way `init` was told.
|
|
7643
|
+
*
|
|
7644
|
+
* Every app in `appsDir` (`apps/*`) rather than the API's own directory.
|
|
7645
|
+
* Endpoints are split by the surface they were built from, so naming one app
|
|
7646
|
+
* here would only decide which apps' endpoints are silently never loaded.
|
|
7647
|
+
*/
|
|
7648
|
+
function workspaceConstructsGlobs(structure, appsDir) {
|
|
7649
|
+
return [WORKSPACE_CONSTRUCTS_GLOB, `./${appsDir}/*/${routesGlob(structure).replace(/^\.\//, "")}`];
|
|
7650
|
+
}
|
|
7651
|
+
|
|
7652
|
+
//#endregion
|
|
7653
|
+
//#region src/init/generators/agents.ts
|
|
7654
|
+
/**
|
|
7655
|
+
* `AGENTS.md` and `CLAUDE.md` for a scaffolded project.
|
|
7656
|
+
*
|
|
7657
|
+
* A generated project is the first thing an agent sees, and without this it
|
|
7658
|
+
* sees only the output: a `gkm.config.ts` naming three keys, a `constructs/`
|
|
7659
|
+
* directory, and a build step that reads both. None of that says what the
|
|
7660
|
+
* shape *is*, so an agent reconstructs it from the framework it recognises —
|
|
7661
|
+
* and writes an Express app beside a `RestApi`, or restates in config what a
|
|
7662
|
+
* construct already declared, which is the exact mistake v10 exists to make
|
|
7663
|
+
* impossible.
|
|
7664
|
+
*
|
|
7665
|
+
* Instructions live in `AGENTS.md` because that is the file every tool reads.
|
|
7666
|
+
* `CLAUDE.md` points at it rather than copying it: two files saying the same
|
|
7667
|
+
* thing is two files to keep in step, and the copy that drifts is always the
|
|
7668
|
+
* one nobody opened.
|
|
7669
|
+
*
|
|
7670
|
+
* Content is derived from the same `TemplateOptions` the rest of the scaffold
|
|
7671
|
+
* is built from, so a project with no database gets no database section, and a
|
|
7672
|
+
* worker gets no endpoint guide. A guide that documents what was not generated
|
|
7673
|
+
* teaches the reader to distrust it.
|
|
7674
|
+
*/
|
|
7675
|
+
function generateAgentFiles(options, template) {
|
|
7676
|
+
return [{
|
|
7677
|
+
path: "AGENTS.md",
|
|
7678
|
+
content: agentsContent(options, template)
|
|
7679
|
+
}, {
|
|
7680
|
+
path: "CLAUDE.md",
|
|
7681
|
+
content: claudeContent()
|
|
7682
|
+
}];
|
|
7683
|
+
}
|
|
7684
|
+
/**
|
|
7685
|
+
* The pointer.
|
|
7686
|
+
*
|
|
7687
|
+
* Deliberately short and deliberately not a summary — a summary is a second
|
|
7688
|
+
* copy of the conventions that ages independently of the first.
|
|
7689
|
+
*/
|
|
7690
|
+
function claudeContent() {
|
|
7691
|
+
return `# CLAUDE.md
|
|
7692
|
+
|
|
7693
|
+
The conventions for this project live in [AGENTS.md](./AGENTS.md).
|
|
7694
|
+
|
|
7695
|
+
Read that file before writing code here. It is the single copy, kept that way
|
|
7696
|
+
on purpose: anything repeated here would be a second thing to keep in step, and
|
|
7697
|
+
the stale one is always the copy nobody opened.
|
|
7698
|
+
`;
|
|
7699
|
+
}
|
|
7700
|
+
function agentsContent(options, template) {
|
|
7701
|
+
const { name: name$1, monorepo, database, telescope, studio, packageManager, services } = options;
|
|
7702
|
+
const constructsGlob = monorepo ? WORKSPACE_CONSTRUCTS_GLOB : CONSTRUCTS_GLOB;
|
|
7703
|
+
const isWorker = template.name === "worker";
|
|
7704
|
+
const isServerless = template.name === "serverless";
|
|
7705
|
+
const run = packageManager === "npm" ? "npx" : `${packageManager} exec`;
|
|
7706
|
+
const sections = [];
|
|
7707
|
+
sections.push(`# Agent conventions — ${name$1}
|
|
7708
|
+
|
|
7709
|
+
Rules for anyone, human or agent, writing code in this project. Built with
|
|
7710
|
+
[\`@geekmidas/toolbox\`](https://github.com/geekmidas/toolbox).
|
|
7711
|
+
|
|
7712
|
+
Read this before adding a file. Most of what follows exists because the obvious
|
|
7713
|
+
approach from another framework produces something that compiles, runs, and is
|
|
7714
|
+
wrong in a way nothing catches.`);
|
|
7715
|
+
sections.push(`## The one idea
|
|
7716
|
+
|
|
7717
|
+
**Constructs declare; config does not restate.**
|
|
7718
|
+
|
|
7719
|
+
A construct — a \`RestApi\`, a database, a bucket, a topic — is the single
|
|
7720
|
+
declaration of a thing. The build reads those declarations and derives
|
|
7721
|
+
everything downstream from them: which containers exist, which apps get built,
|
|
7722
|
+
what the OpenAPI spec says, which env vars a process needs.
|
|
7723
|
+
|
|
7724
|
+
\`gkm.config.ts\` holds only what no construct can answer:
|
|
7725
|
+
|
|
7726
|
+
\`\`\`typescript
|
|
7727
|
+
export default defineWorkspace({
|
|
7728
|
+
name: '${name$1}',
|
|
7729
|
+
stages: { local: 'dev', deployed: ['staging', 'prod'], protected: ['prod'] },
|
|
7730
|
+
constructs: '${constructsGlob}',
|
|
7731
|
+
secrets: { enabled: true },
|
|
7732
|
+
});
|
|
7733
|
+
\`\`\`
|
|
7734
|
+
|
|
7735
|
+
If you find yourself adding a key to config that names something already
|
|
7736
|
+
declared in a construct — a path, a port, a route, a container — stop. That is
|
|
7737
|
+
the drift this design removes. The second copy is the one that goes wrong.`);
|
|
7738
|
+
sections.push(`## Layout
|
|
7739
|
+
|
|
7740
|
+
\`\`\`
|
|
7741
|
+
${layoutBlock(options, template)}
|
|
7742
|
+
\`\`\`
|
|
7743
|
+
|
|
7744
|
+
Handlers are found by **one glob**, and what kind a file exports is decided by
|
|
7745
|
+
the value it exports, not by the directory it sits in.`);
|
|
7746
|
+
sections.push(`## Commands
|
|
7747
|
+
|
|
7748
|
+
\`\`\`bash
|
|
7749
|
+
${run} gkm dev # dev server, hot reload, services up, secrets injected
|
|
7750
|
+
${run} gkm build # production build
|
|
7751
|
+
${run} gkm test # tests, with secrets injected
|
|
7752
|
+
${run} gkm exec -- <cmd> # run any command with secrets injected
|
|
7753
|
+
${run} gkm openapi # regenerate the OpenAPI spec
|
|
7754
|
+
\`\`\`
|
|
7755
|
+
|
|
7756
|
+
Secrets are encrypted at rest and injected at run time:
|
|
7757
|
+
|
|
7758
|
+
\`\`\`bash
|
|
7759
|
+
${run} gkm secrets:set DATABASE_URL postgres://...
|
|
7760
|
+
${run} gkm secrets:show
|
|
7761
|
+
\`\`\`
|
|
7762
|
+
|
|
7763
|
+
**Never run a tool that needs project env vars directly.** It will see an empty
|
|
7764
|
+
environment and fail in a way that looks like a config bug. Go through
|
|
7765
|
+
\`gkm exec --\`, which is what injects them.`);
|
|
7766
|
+
if (!isWorker) sections.push(endpointSection(options));
|
|
7767
|
+
if (isWorker || isServerless || services.events) sections.push(backgroundSection(template));
|
|
7768
|
+
if (database) sections.push(databaseSection());
|
|
7769
|
+
sections.push(`## Environment
|
|
7770
|
+
|
|
7771
|
+
Environment is parsed once, through \`envkit\`, and never read from
|
|
7772
|
+
\`process.env\` directly:
|
|
7773
|
+
|
|
7774
|
+
\`\`\`typescript
|
|
7775
|
+
import { EnvironmentParser } from '@geekmidas/envkit';
|
|
7776
|
+
import { Credentials } from '@geekmidas/envkit/credentials';
|
|
7777
|
+
|
|
7778
|
+
export const envParser = new EnvironmentParser({
|
|
7779
|
+
...process.env,
|
|
7780
|
+
...Credentials,
|
|
7781
|
+
});
|
|
7782
|
+
|
|
7783
|
+
export const config = envParser
|
|
7784
|
+
.create((get) => ({
|
|
7785
|
+
port: get('PORT').string().transform(Number).default(3000),
|
|
7786
|
+
}))
|
|
7787
|
+
.parse();
|
|
7788
|
+
\`\`\`
|
|
7789
|
+
|
|
7790
|
+
\`Credentials\` is what \`gkm dev\` and \`gkm exec\` inject. A bare
|
|
7791
|
+
\`process.env.FOO\` bypasses both the decryption and the validation, so it reads
|
|
7792
|
+
as \`undefined\` in exactly the environments that matter.`);
|
|
7793
|
+
sections.push(conventionsSection());
|
|
7794
|
+
sections.push(testingSection(database));
|
|
7795
|
+
if (telescope || studio) {
|
|
7796
|
+
const tools = [telescope && "**Telescope** — request and exception monitoring", studio && "**Studio** — dev dashboard and database browser"].filter(Boolean);
|
|
7797
|
+
sections.push(`## Dev tools
|
|
7798
|
+
|
|
7799
|
+
${tools.map((t) => `- ${t}`).join("\n")}
|
|
7800
|
+
|
|
7801
|
+
Both are derived from declared constructs. Neither needs wiring up by hand.`);
|
|
7802
|
+
}
|
|
7803
|
+
sections.push(`## Before you say it works
|
|
7804
|
+
|
|
7805
|
+
\`\`\`bash
|
|
7806
|
+
${run} gkm build
|
|
7807
|
+
\`\`\`
|
|
7808
|
+
|
|
7809
|
+
A build is the only thing that proves the manifest still resolves. Code that
|
|
7810
|
+
typechecks can still declare two constructs with one id, or a handler no glob
|
|
7811
|
+
reaches — and neither shows up until something reads the graph.`);
|
|
7812
|
+
return `${sections.join("\n\n")}\n`;
|
|
7813
|
+
}
|
|
7814
|
+
function layoutBlock(options, template) {
|
|
7815
|
+
const { monorepo, apiPath, database } = options;
|
|
7816
|
+
const isWorker = template.name === "worker";
|
|
7817
|
+
if (monorepo) return `constructs/ # every construct: the surface, the database, topics
|
|
7818
|
+
${apiPath}/ # the API app
|
|
7819
|
+
gkm.config.ts # name, constructs glob, secrets`;
|
|
7820
|
+
const lines = ["src/constructs/ # the surface, and anything it declares", "src/config/ # env parser, logger"];
|
|
7821
|
+
if (!isWorker) lines.push("src/endpoints/ # HTTP handlers");
|
|
7822
|
+
if (isWorker) {
|
|
7823
|
+
lines.push("src/crons/ # scheduled work");
|
|
7824
|
+
lines.push("src/subscribers/ # topic consumers");
|
|
7825
|
+
}
|
|
7826
|
+
if (database) lines.push("src/db/ # migrations, schema");
|
|
7827
|
+
lines.push("gkm.config.ts # name, constructs glob, secrets");
|
|
7828
|
+
return lines.join("\n");
|
|
7829
|
+
}
|
|
7830
|
+
function endpointSection(options) {
|
|
7831
|
+
const { database } = options;
|
|
7832
|
+
return `## Adding an endpoint
|
|
7833
|
+
|
|
7834
|
+
An endpoint is built **from the surface that serves it**. That is where its
|
|
7835
|
+
logger, environment parser and authorizers come from, so none of them is passed
|
|
7836
|
+
per endpoint:
|
|
7837
|
+
|
|
7838
|
+
\`\`\`typescript
|
|
7839
|
+
import { z } from 'zod';
|
|
7840
|
+
import { router } from '../constructs/router.ts';
|
|
7841
|
+
|
|
7842
|
+
export const createUser = router
|
|
7843
|
+
.post('/users')
|
|
7844
|
+
.body(z.object({ name: z.string(), email: z.email() }))
|
|
7845
|
+
.output(z.object({ id: z.uuid() }))
|
|
7846
|
+
.handle(async ({ body, logger${database ? ", services" : ""} }) => {
|
|
7847
|
+
logger.info({ name: body.name }, 'creating user');
|
|
7848
|
+
${database ? ` const user = await services.database
|
|
7849
|
+
.insertInto('users')
|
|
7850
|
+
.values({ name: body.name, email: body.email })
|
|
7851
|
+
.returningAll()
|
|
7852
|
+
.executeTakeFirstOrThrow();
|
|
7853
|
+
|
|
7854
|
+
return { id: user.id };` : ` return { id: crypto.randomUUID() };`}
|
|
7855
|
+
});
|
|
7856
|
+
\`\`\`
|
|
7857
|
+
|
|
7858
|
+
**Export it.** An endpoint that is never exported is never found — the glob
|
|
7859
|
+
loads the module and filters by value, so a handler assigned to a local
|
|
7860
|
+
\`const\` simply does not exist as far as the build is concerned.
|
|
7861
|
+
|
|
7862
|
+
**Do not write an HTTP server.** No \`express()\`, no \`new Hono()\`, no
|
|
7863
|
+
\`app.listen\`. The entry is generated from the surface; a hand-written server
|
|
7864
|
+
beside it is a second app that shadows the real one.
|
|
7865
|
+
|
|
7866
|
+
### Branching the factory
|
|
7867
|
+
|
|
7868
|
+
Share what a *group* of endpoints needs by branching once, in its own file:
|
|
7869
|
+
|
|
7870
|
+
\`\`\`typescript
|
|
7871
|
+
export const router = api.endpoints${database ? ".database(database)" : ""};
|
|
7872
|
+
export const sessionRouter = router.session(/* … */);
|
|
7873
|
+
\`\`\`
|
|
7874
|
+
|
|
7875
|
+
Branch for a group, never for a single endpoint — a one-endpoint branch is
|
|
7876
|
+
indirection with nothing on the other side of it.`;
|
|
7877
|
+
}
|
|
7878
|
+
function backgroundSection(template) {
|
|
7879
|
+
const isWorker = template.name === "worker";
|
|
7880
|
+
return `## Background work
|
|
7881
|
+
|
|
7882
|
+
Crons, subscribers and functions are declared the same way endpoints are: as
|
|
7883
|
+
exported values the build discovers.
|
|
7884
|
+
|
|
7885
|
+
\`\`\`typescript
|
|
7886
|
+
// a scheduled job, built from the worker that runs it
|
|
7887
|
+
import { worker } from '../constructs/worker.ts';
|
|
7888
|
+
|
|
7889
|
+
export const cleanup = worker
|
|
7890
|
+
.cron('rate(1 day)')
|
|
7891
|
+
.handle(async ({ logger }) => {
|
|
7892
|
+
logger.info('cleaning up');
|
|
7893
|
+
});
|
|
7894
|
+
\`\`\`
|
|
7895
|
+
|
|
7896
|
+
\`\`\`typescript
|
|
7897
|
+
// a topic subscriber
|
|
7898
|
+
import { userEvents } from '../constructs/topics.ts';
|
|
7899
|
+
import { worker } from '../constructs/worker.ts';
|
|
7900
|
+
|
|
7901
|
+
export const onUserCreated = worker
|
|
7902
|
+
.topic(userEvents)
|
|
7903
|
+
.subscribe(['user.created'])
|
|
7904
|
+
.handle(async ({ events, logger }) => {
|
|
7905
|
+
logger.info({ count: events.length }, 'handling user events');
|
|
7906
|
+
});
|
|
7907
|
+
\`\`\`
|
|
7908
|
+
|
|
7909
|
+
Handlers receive a **batch**, not one event. Both transports deliver in
|
|
7910
|
+
batches, so a per-event round trip is a round trip per event for no reason.
|
|
7911
|
+
|
|
7912
|
+
${isWorker ? `This project is background work and nothing else — there is no HTTP surface
|
|
7913
|
+
to add routes to.` : `Declaring a topic is what makes the broker exist. Nothing lists it in config.`}`;
|
|
7914
|
+
}
|
|
7915
|
+
function databaseSection() {
|
|
7916
|
+
return `## Database
|
|
7917
|
+
|
|
7918
|
+
The database is a **construct**, not a hand-rolled service. Declaring it is what
|
|
7919
|
+
makes a Postgres exist locally and a database exist on deploy — nothing names
|
|
7920
|
+
\`postgres\` in config.
|
|
7921
|
+
|
|
7922
|
+
An endpoint reaches it by declaring the dependency, and gets it typed:
|
|
7923
|
+
|
|
7924
|
+
\`\`\`typescript
|
|
7925
|
+
export const listUsers = router
|
|
7926
|
+
.get('/users')
|
|
7927
|
+
.output(z.array(z.object({ id: z.uuid() })))
|
|
7928
|
+
.handle(async ({ services }) => {
|
|
7929
|
+
return services.database.selectFrom('users').selectAll().execute();
|
|
7930
|
+
});
|
|
7931
|
+
\`\`\`
|
|
7932
|
+
|
|
7933
|
+
Queries go through Kysely. Never open your own connection pool — the one the
|
|
7934
|
+
construct provides is the one that gets credentials, pooling and shutdown.`;
|
|
7935
|
+
}
|
|
7936
|
+
function conventionsSection() {
|
|
7937
|
+
return `## Conventions
|
|
7938
|
+
|
|
7939
|
+
**Zod formats are top-level schemas.**
|
|
7940
|
+
|
|
7941
|
+
\`\`\`typescript
|
|
7942
|
+
// yes
|
|
7943
|
+
z.email()
|
|
7944
|
+
z.url()
|
|
7945
|
+
z.uuid()
|
|
7946
|
+
|
|
7947
|
+
// no
|
|
7948
|
+
z.string().email()
|
|
7949
|
+
z.string().url()
|
|
7950
|
+
\`\`\`
|
|
7951
|
+
|
|
7952
|
+
Zod 4 made each format a schema in its own right. The chained form still runs,
|
|
7953
|
+
but it types as a \`ZodString\` carrying a check, so it cannot be composed or
|
|
7954
|
+
narrowed — and it emits different JSON Schema, which means a published API
|
|
7955
|
+
contract would depend on which spelling someone reached for.
|
|
7956
|
+
|
|
7957
|
+
\`envkit\` is not affected: \`get('ADMIN_EMAIL').string().email()\` is its own
|
|
7958
|
+
builder, not Zod's. Leave it alone.
|
|
7959
|
+
|
|
7960
|
+
**Style.** Two-space indent, single quotes, semicolons, trailing commas, 80
|
|
7961
|
+
columns. \`import type\` for type-only imports. Run the formatter rather than
|
|
7962
|
+
matching it by hand.
|
|
7963
|
+
|
|
7964
|
+
**Construct ids are unique.** Two constructs sharing an id fails discovery for
|
|
7965
|
+
the whole project, not just the second one.`;
|
|
7966
|
+
}
|
|
7967
|
+
function testingSection(database) {
|
|
7968
|
+
return `## Testing
|
|
7969
|
+
|
|
7970
|
+
**Integration over unit.** Prefer the real dependency to a mock — a real
|
|
7971
|
+
database, a real cache. Mocks are for things you genuinely cannot run:
|
|
7972
|
+
the filesystem, the clock, external HTTP (use MSW for that).
|
|
7973
|
+
|
|
7974
|
+
**Test behaviour, not implementation.** Assert what the code did, not which
|
|
7975
|
+
functions it called on the way.
|
|
7976
|
+
|
|
7977
|
+
\`\`\`bash
|
|
7978
|
+
${database ? "gkm test # starts services, injects secrets, then runs vitest" : "gkm test"}
|
|
7979
|
+
\`\`\`
|
|
7980
|
+
|
|
7981
|
+
Run tests through \`gkm test\`, not \`vitest\` directly${database ? " — it is what starts the database and injects the secrets the suite needs" : ""}.`;
|
|
7982
|
+
}
|
|
7983
|
+
|
|
7984
|
+
//#endregion
|
|
7985
|
+
//#region src/init/dependencies.ts
|
|
7986
|
+
/**
|
|
7987
|
+
* Third-party ranges a scaffold installs beside the `@geekmidas` packages.
|
|
7988
|
+
*
|
|
7989
|
+
* Held to the peer ranges those packages declare, in one place, because a
|
|
7990
|
+
* template that pinned its own copy drifted below them: `kysely ~0.28` against
|
|
7991
|
+
* a `~0.29.6` peer is two copies of Kysely and a `Kysely<DB>` from one that is
|
|
7992
|
+
* not assignable to the other's.
|
|
7993
|
+
*
|
|
7994
|
+
* Hand-maintained, unlike `versions.ts`, which `sync-versions` rewrites whole.
|
|
7995
|
+
*/
|
|
7996
|
+
const DEPENDENCY_VERSIONS = {
|
|
7997
|
+
"@tanstack/react-query": "~5.80.0",
|
|
7998
|
+
"better-auth": "~1.7.5",
|
|
7999
|
+
hono: "~4.13.8",
|
|
8000
|
+
kysely: "~0.29.6",
|
|
8001
|
+
"kysely-ctl": "~0.21.0",
|
|
8002
|
+
pg: "~8.23.0",
|
|
8003
|
+
pino: "~10.3.1",
|
|
8004
|
+
sst: "~4.17.1",
|
|
8005
|
+
zod: "~4.6.5"
|
|
8006
|
+
};
|
|
8007
|
+
|
|
7345
8008
|
//#endregion
|
|
7346
8009
|
//#region src/init/versions.ts
|
|
7347
8010
|
const require$1 = createRequire(import.meta.url);
|
|
@@ -7364,26 +8027,26 @@ const CLI_VERSION = `~${pkg.version}`;
|
|
|
7364
8027
|
* Run: pnpm --filter @geekmidas/cli sync-versions
|
|
7365
8028
|
*/
|
|
7366
8029
|
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.
|
|
8030
|
+
"@geekmidas/audit": "~10.0.0-alpha.11",
|
|
8031
|
+
"@geekmidas/auth": "~10.0.0-alpha.11",
|
|
8032
|
+
"@geekmidas/cache": "~10.0.0-alpha.11",
|
|
8033
|
+
"@geekmidas/client": "~10.0.0-alpha.11",
|
|
8034
|
+
"@geekmidas/cloud": "~10.0.0-alpha.11",
|
|
8035
|
+
"@geekmidas/constructs": "~10.0.0-alpha.11",
|
|
8036
|
+
"@geekmidas/db": "~10.0.0-alpha.11",
|
|
8037
|
+
"@geekmidas/emailkit": "~10.0.0-alpha.11",
|
|
8038
|
+
"@geekmidas/envkit": "~10.0.0-alpha.11",
|
|
8039
|
+
"@geekmidas/errors": "~10.0.0-alpha.11",
|
|
8040
|
+
"@geekmidas/events": "~10.0.0-alpha.11",
|
|
8041
|
+
"@geekmidas/logger": "~10.0.0-alpha.11",
|
|
8042
|
+
"@geekmidas/manifest": "~10.0.0-alpha.11",
|
|
8043
|
+
"@geekmidas/rate-limit": "~10.0.0-alpha.11",
|
|
8044
|
+
"@geekmidas/schema": "~10.0.0-alpha.11",
|
|
8045
|
+
"@geekmidas/services": "~10.0.0-alpha.11",
|
|
8046
|
+
"@geekmidas/storage": "~10.0.0-alpha.11",
|
|
8047
|
+
"@geekmidas/studio": "~10.0.0-alpha.11",
|
|
8048
|
+
"@geekmidas/telescope": "~10.0.0-alpha.11",
|
|
8049
|
+
"@geekmidas/testkit": "~10.0.0-alpha.11",
|
|
7387
8050
|
"@geekmidas/cli": CLI_VERSION
|
|
7388
8051
|
};
|
|
7389
8052
|
|
|
@@ -7403,27 +8066,28 @@ function generateAuthAppFiles(options) {
|
|
|
7403
8066
|
private: true,
|
|
7404
8067
|
type: "module",
|
|
7405
8068
|
scripts: {
|
|
7406
|
-
dev: "gkm dev
|
|
7407
|
-
build: "
|
|
7408
|
-
start: "node dist/index.js",
|
|
7409
|
-
typecheck: "tsc --noEmit",
|
|
8069
|
+
dev: "gkm dev",
|
|
8070
|
+
build: "gkm build",
|
|
7410
8071
|
"db:migrate": "gkm exec -- npx @better-auth/cli migrate",
|
|
7411
8072
|
"db:generate": "gkm exec -- npx @better-auth/cli generate"
|
|
7412
8073
|
},
|
|
7413
8074
|
dependencies: {
|
|
7414
8075
|
[modelsPackage]: "workspace:*",
|
|
8076
|
+
"@geekmidas/auth": GEEKMIDAS_VERSIONS["@geekmidas/auth"],
|
|
8077
|
+
"@geekmidas/constructs": GEEKMIDAS_VERSIONS["@geekmidas/constructs"],
|
|
8078
|
+
"@geekmidas/db": GEEKMIDAS_VERSIONS["@geekmidas/db"],
|
|
7415
8079
|
"@geekmidas/envkit": GEEKMIDAS_VERSIONS["@geekmidas/envkit"],
|
|
7416
8080
|
"@geekmidas/logger": GEEKMIDAS_VERSIONS["@geekmidas/logger"],
|
|
7417
|
-
"@hono/node-server": "~1.
|
|
7418
|
-
"better-auth": "
|
|
7419
|
-
hono:
|
|
7420
|
-
kysely:
|
|
7421
|
-
pg:
|
|
8081
|
+
"@hono/node-server": "~1.14.1",
|
|
8082
|
+
"better-auth": DEPENDENCY_VERSIONS["better-auth"],
|
|
8083
|
+
hono: DEPENDENCY_VERSIONS.hono,
|
|
8084
|
+
kysely: DEPENDENCY_VERSIONS.kysely,
|
|
8085
|
+
pg: DEPENDENCY_VERSIONS.pg
|
|
7422
8086
|
},
|
|
7423
8087
|
devDependencies: {
|
|
7424
8088
|
"@geekmidas/cli": GEEKMIDAS_VERSIONS["@geekmidas/cli"],
|
|
7425
8089
|
"@types/node": "~22.0.0",
|
|
7426
|
-
"@types/pg": "~8.
|
|
8090
|
+
"@types/pg": "~8.23.1",
|
|
7427
8091
|
tsx: "~4.20.0",
|
|
7428
8092
|
typescript: "~5.8.2"
|
|
7429
8093
|
}
|
|
@@ -7464,117 +8128,27 @@ dist/
|
|
|
7464
8128
|
}
|
|
7465
8129
|
|
|
7466
8130
|
//#endregion
|
|
7467
|
-
//#region src/init/
|
|
7468
|
-
/** The
|
|
7469
|
-
|
|
8131
|
+
//#region src/init/generators/stages.ts
|
|
8132
|
+
/** The `stages` block a generated gkm.config.ts declares, indented two spaces. */
|
|
8133
|
+
function stagesBlock(stages) {
|
|
8134
|
+
const list = (names) => `[${names.map((name$1) => `'${name$1}'`).join(", ")}]`;
|
|
8135
|
+
return `
|
|
8136
|
+
// Named by this project and read by every command: \`gkm dev\`, \`exec\` and
|
|
8137
|
+
// \`test\` run as \`local\`, and \`gkm deploy --stage\` accepts only \`deployed\`.
|
|
8138
|
+
stages: {
|
|
8139
|
+
local: '${stages.local}',
|
|
8140
|
+
deployed: ${list(stages.deployed)},${stages.protected?.length ? `
|
|
8141
|
+
// Retained and protected on removal.
|
|
8142
|
+
protected: ${list(stages.protected)},` : ""}
|
|
8143
|
+
},`;
|
|
8144
|
+
}
|
|
7470
8145
|
/**
|
|
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.
|
|
8146
|
+
* The `STAGE` the scaffolded env config parses: one of the declared stages,
|
|
8147
|
+
* defaulting to the local one.
|
|
7475
8148
|
*/
|
|
7476
|
-
function
|
|
7477
|
-
const
|
|
7478
|
-
return {
|
|
7479
|
-
id,
|
|
7480
|
-
service: serviceKey(id),
|
|
7481
|
-
urlKey: provideKey(id, "url"),
|
|
7482
|
-
ownerUrlKey: provideKey(id, "ownerUrl")
|
|
7483
|
-
};
|
|
7484
|
-
}
|
|
7485
|
-
/** The bucket a project named `name` declares. */
|
|
7486
|
-
function storageFor(name$1) {
|
|
7487
|
-
const id = canonicalId(`${name$1}-uploads`);
|
|
7488
|
-
return {
|
|
7489
|
-
id,
|
|
7490
|
-
service: serviceKey(id),
|
|
7491
|
-
urlKey: provideKey(id, "url")
|
|
7492
|
-
};
|
|
7493
|
-
}
|
|
7494
|
-
/** The mail sender a project named `name` declares. */
|
|
7495
|
-
function emailFor(name$1) {
|
|
7496
|
-
const id = canonicalId(`${name$1}-mail`);
|
|
7497
|
-
return {
|
|
7498
|
-
id,
|
|
7499
|
-
service: serviceKey(id),
|
|
7500
|
-
urlKey: provideKey(id, "url")
|
|
7501
|
-
};
|
|
7502
|
-
}
|
|
7503
|
-
/** The cache a project named `name` declares. */
|
|
7504
|
-
function cacheFor(name$1) {
|
|
7505
|
-
const id = canonicalId(`${name$1}-cache`);
|
|
7506
|
-
return {
|
|
7507
|
-
id,
|
|
7508
|
-
service: serviceKey(id),
|
|
7509
|
-
urlKey: 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
|
-
}];
|
|
8149
|
+
function stageEnv(stages) {
|
|
8150
|
+
const names = [stages.local, ...stages.deployed].map((name$1) => `'${name$1}'`).join(", ");
|
|
8151
|
+
return `get('STAGE').enum([${names}]).default('${stages.local}')`;
|
|
7578
8152
|
}
|
|
7579
8153
|
|
|
7580
8154
|
//#endregion
|
|
@@ -7601,13 +8175,7 @@ function generateConfigFiles(options, template) {
|
|
|
7601
8175
|
const isServerless = template.name === "serverless";
|
|
7602
8176
|
const hasWorker = template.name === "worker";
|
|
7603
8177
|
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
|
-
};
|
|
8178
|
+
const getRoutesGlob = () => routesGlob(routesStructure);
|
|
7611
8179
|
if (isFullstack) return generateSingleAppConfigFiles(options, template, {
|
|
7612
8180
|
telescope,
|
|
7613
8181
|
studio,
|
|
@@ -7618,7 +8186,7 @@ function generateConfigFiles(options, template) {
|
|
|
7618
8186
|
});
|
|
7619
8187
|
let gkmConfig = `import { defineConfig } from '@geekmidas/cli/config';
|
|
7620
8188
|
|
|
7621
|
-
export default defineConfig({${options.monorepo ? "" : `
|
|
8189
|
+
export default defineConfig({${stagesBlock(options.stages)}${options.monorepo ? "" : `
|
|
7622
8190
|
// One glob, every kind. What reconcile reads to derive the containers this
|
|
7623
8191
|
// app needs: a declared database is why a Postgres exists at all, so nothing
|
|
7624
8192
|
// lists \`postgres\` anywhere.
|
|
@@ -7660,21 +8228,7 @@ export default defineConfig({${options.monorepo ? "" : `
|
|
|
7660
8228
|
gkmConfig += `
|
|
7661
8229
|
});
|
|
7662
8230
|
`;
|
|
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
|
-
} : {
|
|
8231
|
+
const tsConfig = options.monorepo ? workspaceApiTsConfig(options.name) : {
|
|
7678
8232
|
compilerOptions: {
|
|
7679
8233
|
target: "ES2022",
|
|
7680
8234
|
module: "NodeNext",
|
|
@@ -7686,7 +8240,9 @@ export default defineConfig({${options.monorepo ? "" : `
|
|
|
7686
8240
|
forceConsistentCasingInFileNames: true,
|
|
7687
8241
|
resolveJsonModule: true,
|
|
7688
8242
|
noEmit: true,
|
|
7689
|
-
allowImportingTsExtensions: true
|
|
8243
|
+
allowImportingTsExtensions: true,
|
|
8244
|
+
baseUrl: ".",
|
|
8245
|
+
paths: { "~/*": ["./src/*"] }
|
|
7690
8246
|
},
|
|
7691
8247
|
include: ["src/**/*.ts"],
|
|
7692
8248
|
exclude: ["node_modules", "dist"]
|
|
@@ -7712,7 +8268,7 @@ export default defineConfig({${options.monorepo ? "" : `
|
|
|
7712
8268
|
clientKind: "git",
|
|
7713
8269
|
useIgnoreFile: true
|
|
7714
8270
|
},
|
|
7715
|
-
|
|
8271
|
+
assist: { actions: { source: { organizeImports: "on" } } },
|
|
7716
8272
|
formatter: {
|
|
7717
8273
|
enabled: true,
|
|
7718
8274
|
indentStyle: "space",
|
|
@@ -7736,11 +8292,13 @@ export default defineConfig({${options.monorepo ? "" : `
|
|
|
7736
8292
|
style: { noNonNullAssertion: "off" }
|
|
7737
8293
|
}
|
|
7738
8294
|
},
|
|
7739
|
-
|
|
7740
|
-
|
|
7741
|
-
"
|
|
7742
|
-
"
|
|
7743
|
-
"
|
|
8295
|
+
css: { parser: { tailwindDirectives: true } },
|
|
8296
|
+
files: { includes: [
|
|
8297
|
+
"**",
|
|
8298
|
+
"!**/node_modules",
|
|
8299
|
+
"!**/dist",
|
|
8300
|
+
"!**/.gkm",
|
|
8301
|
+
"!**/coverage"
|
|
7744
8302
|
] }
|
|
7745
8303
|
};
|
|
7746
8304
|
const turboConfig = {
|
|
@@ -7794,8 +8352,16 @@ export default defineConfig({${options.monorepo ? "" : `
|
|
|
7794
8352
|
});
|
|
7795
8353
|
return files$1;
|
|
7796
8354
|
}
|
|
7797
|
-
|
|
7798
|
-
|
|
8355
|
+
/**
|
|
8356
|
+
* The tsconfig of an API inside a workspace.
|
|
8357
|
+
*
|
|
8358
|
+
* One definition for both ways of getting one — the fullstack template and
|
|
8359
|
+
* `--monorepo` — because the two copies had drifted: only one of them mapped
|
|
8360
|
+
* the root constructs, so in the other `@<name>/constructs/api.ts` fell through
|
|
8361
|
+
* to the `packages/` mapping and nothing could load the surface.
|
|
8362
|
+
*/
|
|
8363
|
+
function workspaceApiTsConfig(name$1) {
|
|
8364
|
+
return {
|
|
7799
8365
|
extends: "../../tsconfig.json",
|
|
7800
8366
|
compilerOptions: {
|
|
7801
8367
|
noEmit: true,
|
|
@@ -7803,12 +8369,16 @@ function generateSingleAppConfigFiles(options, _template, _helpers) {
|
|
|
7803
8369
|
baseUrl: ".",
|
|
7804
8370
|
paths: {
|
|
7805
8371
|
"~/*": ["./src/*"],
|
|
7806
|
-
[`@${
|
|
8372
|
+
[`@${name$1}/constructs/*`]: ["../../constructs/*"],
|
|
8373
|
+
[`@${name$1}/*`]: ["../../packages/*/src"]
|
|
7807
8374
|
}
|
|
7808
8375
|
},
|
|
7809
8376
|
include: ["src/**/*.ts"],
|
|
7810
8377
|
exclude: ["node_modules", "dist"]
|
|
7811
8378
|
};
|
|
8379
|
+
}
|
|
8380
|
+
function generateSingleAppConfigFiles(options, _template, _helpers) {
|
|
8381
|
+
const tsConfig = workspaceApiTsConfig(options.name);
|
|
7812
8382
|
const files$1 = [{
|
|
7813
8383
|
path: "tsconfig.json",
|
|
7814
8384
|
content: `${JSON.stringify(tsConfig, null, 2)}\n`
|
|
@@ -7944,7 +8514,7 @@ function generateDockerFiles(options, template, dbApps) {
|
|
|
7944
8514
|
MP_SMTP_AUTH: \${SMTP_USER:-${options.name}}:\${SMTP_PASS:-${options.name}}`);
|
|
7945
8515
|
if (options.services?.storage) {
|
|
7946
8516
|
services.push(` minio:
|
|
7947
|
-
image:
|
|
8517
|
+
image: pgsty/minio:RELEASE.2026-08-04T00-00-00Z
|
|
7948
8518
|
container_name: ${options.name}-minio
|
|
7949
8519
|
restart: unless-stopped
|
|
7950
8520
|
entrypoint: sh
|
|
@@ -8185,6 +8755,327 @@ coverage/
|
|
|
8185
8755
|
return files$1;
|
|
8186
8756
|
}
|
|
8187
8757
|
|
|
8758
|
+
//#endregion
|
|
8759
|
+
//#region src/init/generators/github.ts
|
|
8760
|
+
/** How each package manager is set up, installs, and runs a script. */
|
|
8761
|
+
const TOOLING = {
|
|
8762
|
+
pnpm: {
|
|
8763
|
+
setup: ` - uses: pnpm/action-setup@v4
|
|
8764
|
+
PNPM_VERSION
|
|
8765
|
+
- uses: actions/setup-node@v4
|
|
8766
|
+
with:
|
|
8767
|
+
node-version: 22
|
|
8768
|
+
cache: pnpm
|
|
8769
|
+
`,
|
|
8770
|
+
install: "pnpm install --frozen-lockfile",
|
|
8771
|
+
run: "pnpm run",
|
|
8772
|
+
exec: "pnpm exec"
|
|
8773
|
+
},
|
|
8774
|
+
npm: {
|
|
8775
|
+
setup: ` - uses: actions/setup-node@v4
|
|
8776
|
+
with:
|
|
8777
|
+
node-version: 22
|
|
8778
|
+
cache: npm
|
|
8779
|
+
`,
|
|
8780
|
+
install: "npm ci",
|
|
8781
|
+
run: "npm run",
|
|
8782
|
+
exec: "npx --no-install"
|
|
8783
|
+
},
|
|
8784
|
+
yarn: {
|
|
8785
|
+
setup: ` - run: corepack enable
|
|
8786
|
+
|
|
8787
|
+
- uses: actions/setup-node@v4
|
|
8788
|
+
with:
|
|
8789
|
+
node-version: 22
|
|
8790
|
+
cache: yarn
|
|
8791
|
+
`,
|
|
8792
|
+
install: "yarn install --immutable",
|
|
8793
|
+
run: "yarn run",
|
|
8794
|
+
exec: "yarn"
|
|
8795
|
+
},
|
|
8796
|
+
bun: {
|
|
8797
|
+
setup: ` - uses: oven-sh/setup-bun@v2
|
|
8798
|
+
`,
|
|
8799
|
+
install: "bun install --frozen-lockfile",
|
|
8800
|
+
run: "bun run",
|
|
8801
|
+
exec: "bunx"
|
|
8802
|
+
}
|
|
8803
|
+
};
|
|
8804
|
+
/** The package manager's steps, with pnpm pinned where package.json does not. */
|
|
8805
|
+
function tooling(options) {
|
|
8806
|
+
const pm = TOOLING[options.packageManager];
|
|
8807
|
+
return {
|
|
8808
|
+
...pm,
|
|
8809
|
+
setup: pm.setup.replace("PNPM_VERSION\n", options.monorepo ? "" : " with:\n version: 10\n")
|
|
8810
|
+
};
|
|
8811
|
+
}
|
|
8812
|
+
function ci(options) {
|
|
8813
|
+
const pm = tooling(options);
|
|
8814
|
+
return `name: CI
|
|
8815
|
+
|
|
8816
|
+
on:
|
|
8817
|
+
pull_request:
|
|
8818
|
+
branches: [main]
|
|
8819
|
+
|
|
8820
|
+
concurrency:
|
|
8821
|
+
group: \${{ github.workflow }}-\${{ github.ref }}
|
|
8822
|
+
cancel-in-progress: true
|
|
8823
|
+
|
|
8824
|
+
permissions:
|
|
8825
|
+
contents: read
|
|
8826
|
+
|
|
8827
|
+
jobs:
|
|
8828
|
+
validate:
|
|
8829
|
+
name: Validate
|
|
8830
|
+
runs-on: ubuntu-latest
|
|
8831
|
+
steps:
|
|
8832
|
+
- uses: actions/checkout@v4
|
|
8833
|
+
|
|
8834
|
+
${pm.setup}
|
|
8835
|
+
- name: Install
|
|
8836
|
+
run: ${pm.install}
|
|
8837
|
+
|
|
8838
|
+
- name: Build
|
|
8839
|
+
run: ${pm.run} build
|
|
8840
|
+
|
|
8841
|
+
- name: Lint
|
|
8842
|
+
run: ${pm.run} lint
|
|
8843
|
+
|
|
8844
|
+
- name: Typecheck
|
|
8845
|
+
run: ${pm.run} typecheck
|
|
8846
|
+
|
|
8847
|
+
# \`gkm test\` starts the containers the declared constructs need (the
|
|
8848
|
+
# runner has Docker) and, with GKM_AUTO_SETUP, generates throwaway
|
|
8849
|
+
# secrets for the test stage — there is no key to share with CI.
|
|
8850
|
+
- name: Test
|
|
8851
|
+
timeout-minutes: 15
|
|
8852
|
+
run: ${pm.run} test:once
|
|
8853
|
+
env:
|
|
8854
|
+
CI: true
|
|
8855
|
+
GKM_AUTO_SETUP: 1
|
|
8856
|
+
`;
|
|
8857
|
+
}
|
|
8858
|
+
const releaseDrafterWorkflow = `name: Release Drafter
|
|
8859
|
+
|
|
8860
|
+
on:
|
|
8861
|
+
push:
|
|
8862
|
+
branches: [main]
|
|
8863
|
+
pull_request:
|
|
8864
|
+
types: [opened, reopened, synchronize, edited]
|
|
8865
|
+
|
|
8866
|
+
permissions:
|
|
8867
|
+
contents: read
|
|
8868
|
+
|
|
8869
|
+
jobs:
|
|
8870
|
+
draft:
|
|
8871
|
+
name: Update release draft
|
|
8872
|
+
runs-on: ubuntu-latest
|
|
8873
|
+
permissions:
|
|
8874
|
+
contents: write
|
|
8875
|
+
pull-requests: write
|
|
8876
|
+
steps:
|
|
8877
|
+
- uses: release-drafter/release-drafter@v6
|
|
8878
|
+
with:
|
|
8879
|
+
commitish: main
|
|
8880
|
+
env:
|
|
8881
|
+
GITHUB_TOKEN: \${{ secrets.GITHUB_TOKEN }}
|
|
8882
|
+
`;
|
|
8883
|
+
function releaseDrafterConfig(options) {
|
|
8884
|
+
const mobile = options.frontendFramework === "expo" ? ` - title: "📱 Mobile"
|
|
8885
|
+
labels: [mobile, ios, android]
|
|
8886
|
+
collapse-after: 10
|
|
8887
|
+
` : "";
|
|
8888
|
+
return `name-template: "v$RESOLVED_VERSION"
|
|
8889
|
+
tag-template: "v$RESOLVED_VERSION"
|
|
8890
|
+
|
|
8891
|
+
categories:
|
|
8892
|
+
- title: "✨ New Features"
|
|
8893
|
+
labels: [feature, enhancement]
|
|
8894
|
+
collapse-after: 10
|
|
8895
|
+
- title: "🐛 Bug Fixes"
|
|
8896
|
+
labels: [bug, fix]
|
|
8897
|
+
collapse-after: 10
|
|
8898
|
+
${mobile} - title: "🏗 Infrastructure"
|
|
8899
|
+
labels: [infrastructure, ci]
|
|
8900
|
+
collapse-after: 10
|
|
8901
|
+
- title: "📦 Dependencies"
|
|
8902
|
+
labels: [dependencies]
|
|
8903
|
+
collapse-after: 5
|
|
8904
|
+
- title: "🔒 Security"
|
|
8905
|
+
labels: [security]
|
|
8906
|
+
- title: "🧹 Maintenance"
|
|
8907
|
+
labels: [chore, refactor]
|
|
8908
|
+
collapse-after: 5
|
|
8909
|
+
|
|
8910
|
+
change-template: "- $TITLE @$AUTHOR (#$NUMBER)"
|
|
8911
|
+
change-title-escapes: '\\<*_&'
|
|
8912
|
+
|
|
8913
|
+
version-resolver:
|
|
8914
|
+
major:
|
|
8915
|
+
labels: [breaking-change, major]
|
|
8916
|
+
minor:
|
|
8917
|
+
labels: [feature, enhancement, minor]
|
|
8918
|
+
patch:
|
|
8919
|
+
labels: [bug, fix, chore, dependencies, patch]
|
|
8920
|
+
default: patch
|
|
8921
|
+
|
|
8922
|
+
# Labelled from the conventional-commit prefix of the pull request title.
|
|
8923
|
+
autolabeler:
|
|
8924
|
+
- label: feature
|
|
8925
|
+
title: ["/^feat/i"]
|
|
8926
|
+
- label: bug
|
|
8927
|
+
title: ["/^fix/i"]
|
|
8928
|
+
- label: chore
|
|
8929
|
+
title: ["/^chore/i", "/^refactor/i"]
|
|
8930
|
+
- label: dependencies
|
|
8931
|
+
title: ["/^deps/i", "/^chore\\\\(deps\\\\)/i"]
|
|
8932
|
+
- label: ci
|
|
8933
|
+
title: ["/^ci/i"]
|
|
8934
|
+
- label: breaking-change
|
|
8935
|
+
title: ["/^\\\\w+(\\\\(.+\\\\))?!:/"]
|
|
8936
|
+
|
|
8937
|
+
template: |
|
|
8938
|
+
## What's Changed
|
|
8939
|
+
|
|
8940
|
+
$CHANGES
|
|
8941
|
+
|
|
8942
|
+
**Full Changelog**: https://github.com/$OWNER/$REPOSITORY/compare/$PREVIOUS_TAG...v$RESOLVED_VERSION
|
|
8943
|
+
`;
|
|
8944
|
+
}
|
|
8945
|
+
function deploy(options) {
|
|
8946
|
+
const pm = tooling(options);
|
|
8947
|
+
const sst = options.deployTarget === "sst";
|
|
8948
|
+
const credentials = sst ? `
|
|
8949
|
+
# An IAM role GitHub assumes through OIDC — no long-lived AWS keys. Set
|
|
8950
|
+
# AWS_ROLE_ARN as a variable on each environment.
|
|
8951
|
+
- uses: aws-actions/configure-aws-credentials@v4
|
|
8952
|
+
with:
|
|
8953
|
+
role-to-assume: \${{ vars.AWS_ROLE_ARN }}
|
|
8954
|
+
aws-region: ${options.region}
|
|
8955
|
+
` : "";
|
|
8956
|
+
const deployEnv = sst ? "" : `
|
|
8957
|
+
DOKPLOY_API_TOKEN: \${{ secrets.DOKPLOY_API_TOKEN }}
|
|
8958
|
+
DOKPLOY_ENDPOINT: \${{ vars.DOKPLOY_ENDPOINT }}`;
|
|
8959
|
+
return `name: Deploy
|
|
8960
|
+
|
|
8961
|
+
# Which stages exist, and which are protected, is read from \`stages\` in
|
|
8962
|
+
# gkm.config.ts — nothing here names one:
|
|
8963
|
+
#
|
|
8964
|
+
# push to main every deployed stage that is not protected
|
|
8965
|
+
# release published the protected stages (publish the drafted release)
|
|
8966
|
+
# run workflow the one stage you name
|
|
8967
|
+
on:
|
|
8968
|
+
push:
|
|
8969
|
+
branches: [main]
|
|
8970
|
+
release:
|
|
8971
|
+
types: [published]
|
|
8972
|
+
workflow_dispatch:
|
|
8973
|
+
inputs:
|
|
8974
|
+
stage:
|
|
8975
|
+
description: A deployed stage from gkm.config.ts
|
|
8976
|
+
required: true
|
|
8977
|
+
|
|
8978
|
+
permissions:
|
|
8979
|
+
contents: read
|
|
8980
|
+
id-token: write
|
|
8981
|
+
|
|
8982
|
+
jobs:
|
|
8983
|
+
stages:
|
|
8984
|
+
name: Pick stages
|
|
8985
|
+
runs-on: ubuntu-latest
|
|
8986
|
+
outputs:
|
|
8987
|
+
stages: \${{ steps.pick.outputs.stages }}
|
|
8988
|
+
steps:
|
|
8989
|
+
- uses: actions/checkout@v4
|
|
8990
|
+
|
|
8991
|
+
${pm.setup}
|
|
8992
|
+
- name: Install
|
|
8993
|
+
run: ${pm.install}
|
|
8994
|
+
|
|
8995
|
+
- name: Read stages from gkm.config.ts
|
|
8996
|
+
id: pick
|
|
8997
|
+
run: |
|
|
8998
|
+
${pm.exec} tsx -e "
|
|
8999
|
+
import config from './gkm.config.ts';
|
|
9000
|
+
const { deployed, protected: kept = [] } = config.stages;
|
|
9001
|
+
const event = process.env.EVENT;
|
|
9002
|
+
if (event === 'workflow_dispatch' && !deployed.includes(process.env.STAGE)) {
|
|
9003
|
+
console.error('Not a deployed stage: ' + process.env.STAGE + ' (deployed: ' + deployed.join(', ') + ')');
|
|
9004
|
+
process.exit(1);
|
|
9005
|
+
}
|
|
9006
|
+
const stages =
|
|
9007
|
+
event === 'workflow_dispatch' ? [process.env.STAGE]
|
|
9008
|
+
: event === 'release' ? kept
|
|
9009
|
+
: deployed.filter((stage) => !kept.includes(stage));
|
|
9010
|
+
console.log('stages=' + JSON.stringify(stages));
|
|
9011
|
+
" >> "$GITHUB_OUTPUT"
|
|
9012
|
+
env:
|
|
9013
|
+
EVENT: \${{ github.event_name }}
|
|
9014
|
+
STAGE: \${{ inputs.stage }}
|
|
9015
|
+
|
|
9016
|
+
deploy:
|
|
9017
|
+
name: Deploy \${{ matrix.stage }}
|
|
9018
|
+
needs: stages
|
|
9019
|
+
if: needs.stages.outputs.stages != '[]'
|
|
9020
|
+
runs-on: ubuntu-latest
|
|
9021
|
+
strategy:
|
|
9022
|
+
fail-fast: false
|
|
9023
|
+
matrix:
|
|
9024
|
+
stage: \${{ fromJSON(needs.stages.outputs.stages) }}
|
|
9025
|
+
# One GitHub environment per stage: its secrets and variables, and any
|
|
9026
|
+
# approval rule you put on a protected one.
|
|
9027
|
+
environment: \${{ matrix.stage }}
|
|
9028
|
+
concurrency:
|
|
9029
|
+
group: deploy-\${{ matrix.stage }}
|
|
9030
|
+
cancel-in-progress: false
|
|
9031
|
+
steps:
|
|
9032
|
+
- uses: actions/checkout@v4
|
|
9033
|
+
|
|
9034
|
+
${pm.setup}
|
|
9035
|
+
- name: Install
|
|
9036
|
+
run: ${pm.install}
|
|
9037
|
+
|
|
9038
|
+
# The stage's secrets are committed encrypted; the key never is. Each
|
|
9039
|
+
# environment holds its own as GKM_SECRETS_KEY.
|
|
9040
|
+
# Owner-only, as the CLI writes its own keys. The stage reaches the shell
|
|
9041
|
+
# as a variable, never pasted into the script: on a manual run it is
|
|
9042
|
+
# whatever was typed.
|
|
9043
|
+
- name: Stage secrets key
|
|
9044
|
+
run: |
|
|
9045
|
+
mkdir -p -m 700 ~/.gkm/${options.name}
|
|
9046
|
+
(umask 077 && printf '%s' "$KEY" > ~/.gkm/${options.name}/"$STAGE".key)
|
|
9047
|
+
env:
|
|
9048
|
+
KEY: \${{ secrets.GKM_SECRETS_KEY }}
|
|
9049
|
+
STAGE: \${{ matrix.stage }}
|
|
9050
|
+
${credentials}
|
|
9051
|
+
- name: Deploy
|
|
9052
|
+
run: ${pm.run} "deploy:$STAGE"
|
|
9053
|
+
env:
|
|
9054
|
+
STAGE: \${{ matrix.stage }}${deployEnv}
|
|
9055
|
+
`;
|
|
9056
|
+
}
|
|
9057
|
+
function generateGithubFiles(options) {
|
|
9058
|
+
const files$1 = [
|
|
9059
|
+
{
|
|
9060
|
+
path: ".github/workflows/ci.yml",
|
|
9061
|
+
content: ci(options)
|
|
9062
|
+
},
|
|
9063
|
+
{
|
|
9064
|
+
path: ".github/workflows/release-drafter.yml",
|
|
9065
|
+
content: releaseDrafterWorkflow
|
|
9066
|
+
},
|
|
9067
|
+
{
|
|
9068
|
+
path: ".github/release-drafter.yml",
|
|
9069
|
+
content: releaseDrafterConfig(options)
|
|
9070
|
+
}
|
|
9071
|
+
];
|
|
9072
|
+
if (options.monorepo && options.deployTarget !== "none") files$1.push({
|
|
9073
|
+
path: ".github/workflows/deploy.yml",
|
|
9074
|
+
content: deploy(options)
|
|
9075
|
+
});
|
|
9076
|
+
return files$1;
|
|
9077
|
+
}
|
|
9078
|
+
|
|
8188
9079
|
//#endregion
|
|
8189
9080
|
//#region src/init/generators/mobile-expo.ts
|
|
8190
9081
|
/**
|
|
@@ -8217,12 +9108,12 @@ function generateExpoAppFiles(options) {
|
|
|
8217
9108
|
dependencies: {
|
|
8218
9109
|
[apiPackage]: "workspace:*",
|
|
8219
9110
|
[modelsPackage]: "workspace:*",
|
|
8220
|
-
"@better-auth/expo": "
|
|
9111
|
+
"@better-auth/expo": DEPENDENCY_VERSIONS["better-auth"],
|
|
8221
9112
|
"@geekmidas/client": GEEKMIDAS_VERSIONS["@geekmidas/client"],
|
|
8222
9113
|
"@geekmidas/envkit": GEEKMIDAS_VERSIONS["@geekmidas/envkit"],
|
|
8223
9114
|
"@react-navigation/native": "^7.1.0",
|
|
8224
|
-
"@tanstack/react-query": "
|
|
8225
|
-
"better-auth": "
|
|
9115
|
+
"@tanstack/react-query": DEPENDENCY_VERSIONS["@tanstack/react-query"],
|
|
9116
|
+
"better-auth": DEPENDENCY_VERSIONS["better-auth"],
|
|
8226
9117
|
expo: "~55.0.0",
|
|
8227
9118
|
"expo-constants": "~55.0.0",
|
|
8228
9119
|
"expo-dev-client": "~55.0.0",
|
|
@@ -8358,7 +9249,7 @@ module.exports = withNativeWind(config, { input: './global.css' });
|
|
|
8358
9249
|
"@/*": ["./*"],
|
|
8359
9250
|
[modelsPackage]: ["../../packages/models/src"],
|
|
8360
9251
|
[`${modelsPackage}/*`]: ["../../packages/models/src/*"],
|
|
8361
|
-
[`${apiPackage}/client`]: ["../../apps/api/.gkm/openapi.ts"]
|
|
9252
|
+
[`${apiPackage}/client`]: ["../../apps/api/.gkm/openapi/api.ts"]
|
|
8362
9253
|
}
|
|
8363
9254
|
},
|
|
8364
9255
|
include: [
|
|
@@ -8916,14 +9807,152 @@ export type ListUsersResponse = z.infer<typeof ListUsersResponseSchema>;
|
|
|
8916
9807
|
];
|
|
8917
9808
|
}
|
|
8918
9809
|
|
|
9810
|
+
//#endregion
|
|
9811
|
+
//#region src/init/generators/deploy.ts
|
|
9812
|
+
/** One `deploy:<stage>` script per deployed stage. */
|
|
9813
|
+
const perStage = (options, command) => Object.fromEntries(options.stages.deployed.map((stage) => [`deploy:${stage}`, command(stage)]));
|
|
9814
|
+
function deployPackage(options) {
|
|
9815
|
+
switch (options.deployTarget) {
|
|
9816
|
+
case "dokploy": return {
|
|
9817
|
+
scripts: perStage(options, (stage) => `gkm deploy --provider dokploy --stage ${stage}`),
|
|
9818
|
+
dependencies: {},
|
|
9819
|
+
devDependencies: {}
|
|
9820
|
+
};
|
|
9821
|
+
case "sst": {
|
|
9822
|
+
const v = GEEKMIDAS_VERSIONS;
|
|
9823
|
+
return {
|
|
9824
|
+
scripts: perStage(options, (stage) => `gkm build --provider aws && sst deploy --stage ${stage}`),
|
|
9825
|
+
dependencies: {
|
|
9826
|
+
"@geekmidas/cloud": v["@geekmidas/cloud"],
|
|
9827
|
+
"@geekmidas/db": v["@geekmidas/db"],
|
|
9828
|
+
"@geekmidas/envkit": v["@geekmidas/envkit"],
|
|
9829
|
+
"@geekmidas/events": v["@geekmidas/events"],
|
|
9830
|
+
"@geekmidas/manifest": v["@geekmidas/manifest"],
|
|
9831
|
+
"@geekmidas/storage": v["@geekmidas/storage"],
|
|
9832
|
+
pg: DEPENDENCY_VERSIONS.pg
|
|
9833
|
+
},
|
|
9834
|
+
devDependencies: { sst: DEPENDENCY_VERSIONS.sst }
|
|
9835
|
+
};
|
|
9836
|
+
}
|
|
9837
|
+
default: return {
|
|
9838
|
+
scripts: {},
|
|
9839
|
+
dependencies: {},
|
|
9840
|
+
devDependencies: {}
|
|
9841
|
+
};
|
|
9842
|
+
}
|
|
9843
|
+
}
|
|
9844
|
+
function generateDeployFiles(options) {
|
|
9845
|
+
if (options.deployTarget !== "sst") return [];
|
|
9846
|
+
if (!options.region) throw new SstNeedsRegion();
|
|
9847
|
+
const db = databaseFor();
|
|
9848
|
+
const mail = emailFor();
|
|
9849
|
+
const hasDatabase = options.template === "fullstack" || options.services.db;
|
|
9850
|
+
const inputs = [...hasDatabase ? [` // RDS needs a network. Created here rather than assumed, because a
|
|
9851
|
+
// VPC with NAT is a monthly cost in an account that may already
|
|
9852
|
+
// have one — replace it with \`sst.aws.Vpc.get(…)\` if so.
|
|
9853
|
+
${db.id}: { vpc },`] : [], ...options.services.mail ? [` // Every provider rejects an unverified sender, so there is no
|
|
9854
|
+
// default; the synth stops with \`EmailNeedsSender\` until it is set.
|
|
9855
|
+
${mail.id}: { from: process.env.MAIL_FROM as string },`] : []];
|
|
9856
|
+
return [{
|
|
9857
|
+
path: "sst.config.ts",
|
|
9858
|
+
content: `/// <reference path="./.sst/platform/config.d.ts" />
|
|
9859
|
+
|
|
9860
|
+
/**
|
|
9861
|
+
* ${options.name} on AWS.
|
|
9862
|
+
*
|
|
9863
|
+
* No bucket, cluster, queue or IAM is listed here: they come from the
|
|
9864
|
+
* constructs the application declares, through \`fromManifest\`. What is here
|
|
9865
|
+
* is what varies by stage, and the inputs a declaration cannot carry.
|
|
9866
|
+
*
|
|
9867
|
+
* Deploy with \`pnpm run deploy:<stage>\`, which builds the manifest first.
|
|
9868
|
+
*/
|
|
9869
|
+
const region = '${options.region}';
|
|
9870
|
+
|
|
9871
|
+
/** From \`stages.protected\` in gkm.config.ts. */
|
|
9872
|
+
const PROTECTED: string[] = ${JSON.stringify(options.stages.protected ?? []).replace(/"/g, "'")};
|
|
9873
|
+
|
|
9874
|
+
export default $config({
|
|
9875
|
+
app(input) {
|
|
9876
|
+
return {
|
|
9877
|
+
name: '${options.name}',
|
|
9878
|
+
// Any other stage is disposable; a protected one's database should not
|
|
9879
|
+
// vanish because somebody removed a stack.
|
|
9880
|
+
removal: PROTECTED.includes(input?.stage) ? 'retain' : 'remove',
|
|
9881
|
+
protect: PROTECTED.includes(input?.stage),
|
|
9882
|
+
home: 'aws',
|
|
9883
|
+
providers: { aws: { region } },
|
|
9884
|
+
};
|
|
9885
|
+
},
|
|
9886
|
+
|
|
9887
|
+
async run() {
|
|
9888
|
+
const { App, fromManifest, Stack } = await import('@geekmidas/cloud/sst');
|
|
9889
|
+
// Written by \`gkm build --provider aws\`. Imported rather than discovered,
|
|
9890
|
+
// so SST's toolchain never evaluates the application's own modules.
|
|
9891
|
+
const { backends, constructs } = await import('./.gkm/manifest/aws.js');
|
|
9892
|
+
|
|
9893
|
+
${hasDatabase ? " const vpc = new sst.aws.Vpc('Vpc', { nat: 'ec2' });\n\n" : ""} const app = new App({
|
|
9894
|
+
name: $app.name,
|
|
9895
|
+
stage: $app.stage,
|
|
9896
|
+
domain: '',
|
|
9897
|
+
region,
|
|
9898
|
+
hostedZoneId: '',
|
|
9899
|
+
});
|
|
9900
|
+
|
|
9901
|
+
return fromManifest(
|
|
9902
|
+
new Stack(app, '${canonicalId(options.name)}'),
|
|
9903
|
+
constructs,
|
|
9904
|
+
{
|
|
9905
|
+
${inputs.join("\n")}
|
|
9906
|
+
},
|
|
9907
|
+
backends,
|
|
9908
|
+
);
|
|
9909
|
+
},
|
|
9910
|
+
});
|
|
9911
|
+
`
|
|
9912
|
+
}];
|
|
9913
|
+
}
|
|
9914
|
+
/**
|
|
9915
|
+
* An SST scaffold asked for without a region. `init` always resolves one —
|
|
9916
|
+
* asked, `--region`, or eu-west-1 under `--yes` — so this is a caller's bug.
|
|
9917
|
+
*/
|
|
9918
|
+
var SstNeedsRegion = class extends Error {
|
|
9919
|
+
constructor() {
|
|
9920
|
+
super("An SST deploy needs a region: pass --region, e.g. eu-west-1.");
|
|
9921
|
+
this.name = "SstNeedsRegion";
|
|
9922
|
+
}
|
|
9923
|
+
};
|
|
9924
|
+
|
|
8919
9925
|
//#endregion
|
|
8920
9926
|
//#region src/init/generators/monorepo.ts
|
|
8921
9927
|
/**
|
|
9928
|
+
* What the root `constructs/` folder imports, and the peers each construct
|
|
9929
|
+
* needs, for the services this workspace was scaffolded with.
|
|
9930
|
+
*/
|
|
9931
|
+
function rootConstructDependencies(options) {
|
|
9932
|
+
const v = GEEKMIDAS_VERSIONS;
|
|
9933
|
+
return {
|
|
9934
|
+
"@geekmidas/constructs": v["@geekmidas/constructs"],
|
|
9935
|
+
"@geekmidas/logger": v["@geekmidas/logger"],
|
|
9936
|
+
...options.loggerType === "pino" ? { pino: DEPENDENCY_VERSIONS.pino } : {},
|
|
9937
|
+
"@geekmidas/telescope": v["@geekmidas/telescope"],
|
|
9938
|
+
"@geekmidas/auth": v["@geekmidas/auth"],
|
|
9939
|
+
"better-auth": DEPENDENCY_VERSIONS["better-auth"],
|
|
9940
|
+
"@geekmidas/db": v["@geekmidas/db"],
|
|
9941
|
+
kysely: DEPENDENCY_VERSIONS.kysely,
|
|
9942
|
+
pg: DEPENDENCY_VERSIONS.pg,
|
|
9943
|
+
...options.services.storage ? { "@geekmidas/storage": v["@geekmidas/storage"] } : {},
|
|
9944
|
+
...options.services.mail ? { "@geekmidas/emailkit": v["@geekmidas/emailkit"] } : {},
|
|
9945
|
+
...options.services.cache ? { "@geekmidas/cache": v["@geekmidas/cache"] } : {}
|
|
9946
|
+
};
|
|
9947
|
+
}
|
|
9948
|
+
/**
|
|
8922
9949
|
* Generate monorepo root files (pnpm-workspace.yaml, root package.json, etc.)
|
|
8923
9950
|
*/
|
|
8924
9951
|
function generateMonorepoFiles(options, _template) {
|
|
8925
9952
|
if (!options.monorepo) return [];
|
|
8926
9953
|
const isFullstack = options.template === "fullstack";
|
|
9954
|
+
const deploy$1 = deployPackage(options);
|
|
9955
|
+
const isSst = options.deployTarget === "sst";
|
|
8927
9956
|
const rootPackageJson = {
|
|
8928
9957
|
name: options.name,
|
|
8929
9958
|
version: "0.0.1",
|
|
@@ -8940,9 +9969,13 @@ function generateMonorepoFiles(options, _template) {
|
|
|
8940
9969
|
fmt: "biome format . --write",
|
|
8941
9970
|
"fmt:check": "biome format .",
|
|
8942
9971
|
...isFullstack ? { storybook: "pnpm --filter ./packages/ui storybook" } : {},
|
|
8943
|
-
...
|
|
9972
|
+
...deploy$1.scripts
|
|
9973
|
+
},
|
|
9974
|
+
dependencies: {
|
|
9975
|
+
zod: DEPENDENCY_VERSIONS.zod,
|
|
9976
|
+
...isFullstack ? rootConstructDependencies(options) : {},
|
|
9977
|
+
...deploy$1.dependencies
|
|
8944
9978
|
},
|
|
8945
|
-
dependencies: { zod: "~4.1.0" },
|
|
8946
9979
|
devDependencies: {
|
|
8947
9980
|
"@biomejs/biome": "~2.3.0",
|
|
8948
9981
|
"@geekmidas/cli": GEEKMIDAS_VERSIONS["@geekmidas/cli"],
|
|
@@ -8950,7 +9983,8 @@ function generateMonorepoFiles(options, _template) {
|
|
|
8950
9983
|
tsx: "~4.20.0",
|
|
8951
9984
|
turbo: "~2.3.0",
|
|
8952
9985
|
typescript: "~5.8.2",
|
|
8953
|
-
vitest: "~4.0.0"
|
|
9986
|
+
vitest: "~4.0.0",
|
|
9987
|
+
...deploy$1.devDependencies
|
|
8954
9988
|
}
|
|
8955
9989
|
};
|
|
8956
9990
|
const apiPathParts = options.apiPath.split("/");
|
|
@@ -8966,7 +10000,7 @@ function generateMonorepoFiles(options, _template) {
|
|
|
8966
10000
|
clientKind: "git",
|
|
8967
10001
|
useIgnoreFile: true
|
|
8968
10002
|
},
|
|
8969
|
-
|
|
10003
|
+
assist: { actions: { source: { organizeImports: "on" } } },
|
|
8970
10004
|
formatter: {
|
|
8971
10005
|
enabled: true,
|
|
8972
10006
|
indentStyle: "space",
|
|
@@ -8990,11 +10024,14 @@ function generateMonorepoFiles(options, _template) {
|
|
|
8990
10024
|
style: { noNonNullAssertion: "off" }
|
|
8991
10025
|
}
|
|
8992
10026
|
},
|
|
8993
|
-
|
|
8994
|
-
|
|
8995
|
-
"
|
|
8996
|
-
"
|
|
8997
|
-
"
|
|
10027
|
+
css: { parser: { tailwindDirectives: true } },
|
|
10028
|
+
files: { includes: [
|
|
10029
|
+
"**",
|
|
10030
|
+
"!**/node_modules",
|
|
10031
|
+
"!**/dist",
|
|
10032
|
+
"!**/.gkm",
|
|
10033
|
+
"!**/coverage",
|
|
10034
|
+
...isSst ? ["!**/.sst", "!sst-env.d.ts"] : []
|
|
8998
10035
|
] }
|
|
8999
10036
|
};
|
|
9000
10037
|
const turboConfig = {
|
|
@@ -9060,12 +10097,14 @@ coverage/
|
|
|
9060
10097
|
|
|
9061
10098
|
# Turbo
|
|
9062
10099
|
.turbo/
|
|
9063
|
-
`;
|
|
10100
|
+
${isSst ? "\n# SST\n.sst/\n" : ""}`;
|
|
9064
10101
|
const tsConfig = {
|
|
9065
10102
|
compilerOptions: {
|
|
9066
10103
|
...isFullstack ? {
|
|
9067
10104
|
baseUrl: ".",
|
|
9068
|
-
paths: { [`@${options.name}/constructs/*`]: ["./constructs/*"] }
|
|
10105
|
+
paths: { [`@${options.name}/constructs/*`]: ["./constructs/*"] },
|
|
10106
|
+
allowImportingTsExtensions: true,
|
|
10107
|
+
noEmit: true
|
|
9069
10108
|
} : {},
|
|
9070
10109
|
target: "ES2022",
|
|
9071
10110
|
module: "NodeNext",
|
|
@@ -9077,16 +10116,21 @@ coverage/
|
|
|
9077
10116
|
forceConsistentCasingInFileNames: true,
|
|
9078
10117
|
resolveJsonModule: true
|
|
9079
10118
|
},
|
|
9080
|
-
exclude: [
|
|
10119
|
+
exclude: [
|
|
10120
|
+
"node_modules",
|
|
10121
|
+
"dist",
|
|
10122
|
+
...isSst ? ["sst.config.ts", ".sst"] : []
|
|
10123
|
+
]
|
|
9081
10124
|
};
|
|
9082
10125
|
const vitestConfig = `import { defineConfig } from 'vitest/config';
|
|
9083
10126
|
|
|
9084
10127
|
export default defineConfig({
|
|
9085
10128
|
test: {
|
|
9086
|
-
|
|
9087
|
-
|
|
9088
|
-
|
|
9089
|
-
|
|
10129
|
+
// Each app and package is its own project, so its own config — an app's
|
|
10130
|
+
// \`globalSetup\`, its path aliases — applies to its own tests.
|
|
10131
|
+
projects: ['apps/*', 'packages/*'],
|
|
10132
|
+
// The shared packages ship without tests of their own.
|
|
10133
|
+
passWithNoTests: true,
|
|
9090
10134
|
coverage: {
|
|
9091
10135
|
provider: 'v8',
|
|
9092
10136
|
reporter: ['text', 'json', 'html'],
|
|
@@ -9167,7 +10211,8 @@ export default defineConfig({
|
|
|
9167
10211
|
{
|
|
9168
10212
|
path: ".vscode/extensions.json",
|
|
9169
10213
|
content: `${JSON.stringify(vscodeExtensions, null, " ")}\n`
|
|
9170
|
-
}
|
|
10214
|
+
},
|
|
10215
|
+
...generateDeployFiles(options)
|
|
9171
10216
|
];
|
|
9172
10217
|
if (isFullstack) files$1.push({
|
|
9173
10218
|
path: "gkm.config.ts",
|
|
@@ -9194,12 +10239,17 @@ export default defineWorkspace({
|
|
|
9194
10239
|
// The scope every physical name is built from: \`Database\` becomes
|
|
9195
10240
|
// \`production-${options.name}-database\` on Dokploy and on AWS alike.
|
|
9196
10241
|
name: '${options.name}',
|
|
10242
|
+
${stagesBlock(options.stages)}
|
|
9197
10243
|
|
|
9198
|
-
//
|
|
10244
|
+
// Every kind, in every app. A declared database is why a Postgres exists, a
|
|
9199
10245
|
// declared bucket is why MinIO does, a declared topic is why a broker does —
|
|
9200
10246
|
// 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
|
-
|
|
10247
|
+
// \`StaticSite\` is an app, and so is every \`RestApi\`. And it finds each
|
|
10248
|
+
// app's endpoints, crons and subscribers: an endpoint belongs to the surface
|
|
10249
|
+
// it was built from, not to the directory it sits in.
|
|
10250
|
+
constructs: [
|
|
10251
|
+
${workspaceConstructsGlobs(options.routesStructure, dirname(options.apiPath)).map((glob) => ` '${glob}',`).join("\n")}
|
|
10252
|
+
],
|
|
9203
10253
|
|
|
9204
10254
|
secrets: {
|
|
9205
10255
|
enabled: true,
|
|
@@ -9228,7 +10278,7 @@ export default defineWorkspace({
|
|
|
9228
10278
|
function generateRootConstructs(options) {
|
|
9229
10279
|
if (!options.monorepo || options.template !== "fullstack") return [];
|
|
9230
10280
|
const { name: name$1, services, frontendFramework } = options;
|
|
9231
|
-
const db = databaseFor(
|
|
10281
|
+
const db = databaseFor();
|
|
9232
10282
|
const files$1 = [];
|
|
9233
10283
|
files$1.push({
|
|
9234
10284
|
path: "constructs/logger.ts",
|
|
@@ -9294,6 +10344,7 @@ import { authDb } from './database.ts';
|
|
|
9294
10344
|
* origin down.
|
|
9295
10345
|
*/
|
|
9296
10346
|
export const auth = new BetterAuth('Auth', {
|
|
10347
|
+
path: 'apps/auth',
|
|
9297
10348
|
database: authDb,
|
|
9298
10349
|
basePath: '/api/auth',
|
|
9299
10350
|
});
|
|
@@ -9308,25 +10359,22 @@ import { logger } from './logger.ts';
|
|
|
9308
10359
|
/**
|
|
9309
10360
|
* The application's HTTP surface — one RestApi, one container.
|
|
9310
10361
|
*
|
|
9311
|
-
*
|
|
9312
|
-
* \`
|
|
9313
|
-
* \`src/\`, where the conventional layout has them at the app root.
|
|
10362
|
+
* \`path\` is where its app lives. Its handlers are found by the workspace's
|
|
10363
|
+
* \`constructs\` glob, and belong to it because they were built from it.
|
|
9314
10364
|
*/
|
|
9315
10365
|
export const api = new RestApi('Api', {
|
|
10366
|
+
path: '${options.apiPath}',
|
|
10367
|
+
|
|
9316
10368
|
// Typed out rather than omitted: an API that ships open because a field was
|
|
9317
10369
|
// left off is the one default worth refusing to have.
|
|
9318
10370
|
defaultAuthorizer: 'none',
|
|
9319
10371
|
|
|
9320
10372
|
// The actual logger, not a path to one.
|
|
9321
10373
|
logger,
|
|
9322
|
-
|
|
9323
|
-
app: {
|
|
9324
|
-
code: './src/{endpoints,functions,crons,queues,topics,subscribers}/**/*.ts',
|
|
9325
|
-
},
|
|
9326
10374
|
}).auth(auth);
|
|
9327
10375
|
`
|
|
9328
10376
|
});
|
|
9329
|
-
const variant = frontendFramework === "tanstack-start" ? "{ variant: 'tanstack' }" : frontendFramework === "expo" ? void 0 : "{ variant: 'next' }";
|
|
10377
|
+
const variant = frontendFramework === "tanstack-start" ? "{ path: 'apps/web', variant: 'tanstack' }" : frontendFramework === "expo" ? void 0 : "{ path: 'apps/web', variant: 'next' }";
|
|
9330
10378
|
if (variant !== void 0) files$1.push({
|
|
9331
10379
|
path: "constructs/site.ts",
|
|
9332
10380
|
content: `import { StaticSite } from '@geekmidas/constructs/site';
|
|
@@ -9336,7 +10384,7 @@ import { auth } from './auth.ts';
|
|
|
9336
10384
|
/**
|
|
9337
10385
|
* The frontend — a construct like any other, which is what makes it an app.
|
|
9338
10386
|
*
|
|
9339
|
-
*
|
|
10387
|
+
* \`path\` is where it lives. \`.dependsOn()\` is the single fact
|
|
9340
10388
|
* behind four things that are hand-maintained otherwise: this site's
|
|
9341
10389
|
* build-time API URL, the API's CORS origins, the auth server's trusted
|
|
9342
10390
|
* origins, and which generated client lands here.
|
|
@@ -9345,7 +10393,7 @@ export const web = new StaticSite('Web', ${variant}).dependsOn([api, auth]);
|
|
|
9345
10393
|
`
|
|
9346
10394
|
});
|
|
9347
10395
|
if (services.storage) {
|
|
9348
|
-
const bucket = storageFor(
|
|
10396
|
+
const bucket = storageFor();
|
|
9349
10397
|
files$1.push({
|
|
9350
10398
|
path: "constructs/storage.ts",
|
|
9351
10399
|
content: `import { ObjectStorage } from '@geekmidas/constructs/object-storage';
|
|
@@ -9356,7 +10404,7 @@ export const uploads = new ObjectStorage('${bucket.id}');
|
|
|
9356
10404
|
});
|
|
9357
10405
|
}
|
|
9358
10406
|
if (services.mail) {
|
|
9359
|
-
const mail = emailFor(
|
|
10407
|
+
const mail = emailFor();
|
|
9360
10408
|
files$1.push({
|
|
9361
10409
|
path: "constructs/email.ts",
|
|
9362
10410
|
content: `import { Email } from '@geekmidas/constructs/email';
|
|
@@ -9367,7 +10415,7 @@ export const email = new Email('${mail.id}', { templates: {} });
|
|
|
9367
10415
|
});
|
|
9368
10416
|
}
|
|
9369
10417
|
if (services.cache) {
|
|
9370
|
-
const kv = cacheFor(
|
|
10418
|
+
const kv = cacheFor();
|
|
9371
10419
|
files$1.push({
|
|
9372
10420
|
path: "constructs/cache.ts",
|
|
9373
10421
|
content: `import { Cache } from '@geekmidas/constructs/cache';
|
|
@@ -9400,9 +10448,9 @@ const apiTemplate = {
|
|
|
9400
10448
|
"@geekmidas/errors": GEEKMIDAS_VERSIONS["@geekmidas/errors"],
|
|
9401
10449
|
"@geekmidas/auth": GEEKMIDAS_VERSIONS["@geekmidas/auth"],
|
|
9402
10450
|
"@hono/node-server": "~1.14.1",
|
|
9403
|
-
hono:
|
|
9404
|
-
pino:
|
|
9405
|
-
zod:
|
|
10451
|
+
hono: DEPENDENCY_VERSIONS.hono,
|
|
10452
|
+
pino: DEPENDENCY_VERSIONS.pino,
|
|
10453
|
+
zod: DEPENDENCY_VERSIONS.zod
|
|
9406
10454
|
},
|
|
9407
10455
|
devDependencies: {
|
|
9408
10456
|
"@biomejs/biome": "~2.3.0",
|
|
@@ -9426,10 +10474,10 @@ const apiTemplate = {
|
|
|
9426
10474
|
},
|
|
9427
10475
|
files: (options) => {
|
|
9428
10476
|
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(
|
|
10477
|
+
const bucket = storageFor();
|
|
10478
|
+
const kv = cacheFor();
|
|
10479
|
+
const db = databaseFor();
|
|
10480
|
+
const mail = emailFor();
|
|
9433
10481
|
const src = (path) => monorepo ? `~/${path}` : `./${path}`;
|
|
9434
10482
|
const declares = !monorepo;
|
|
9435
10483
|
const loggerContent = `import { createLogger } from '@geekmidas/logger/${loggerType}';
|
|
@@ -9463,6 +10511,9 @@ export const logger = createLogger();
|
|
|
9463
10511
|
import { logger } from '../config/logger.ts';
|
|
9464
10512
|
|
|
9465
10513
|
export const api = new RestApi('Api', {
|
|
10514
|
+
// The project is the app.
|
|
10515
|
+
path: '.',
|
|
10516
|
+
|
|
9466
10517
|
// Typed out rather than omitted: an API that ships open because a field was
|
|
9467
10518
|
// left off is the one default worth refusing to have.
|
|
9468
10519
|
defaultAuthorizer: 'none',
|
|
@@ -9485,7 +10536,7 @@ export const envParser = new EnvironmentParser({ ...process.env, ...Credentials
|
|
|
9485
10536
|
export const config = envParser
|
|
9486
10537
|
.create((get) => ({
|
|
9487
10538
|
nodeEnv: get('NODE_ENV').enum(['development', 'test', 'production']).default('development'),
|
|
9488
|
-
stage:
|
|
10539
|
+
stage: ${stageEnv(options.stages)},
|
|
9489
10540
|
}))
|
|
9490
10541
|
.parse();
|
|
9491
10542
|
`
|
|
@@ -9637,7 +10688,7 @@ export const authService = {
|
|
|
9637
10688
|
headers: { cookie },
|
|
9638
10689
|
});
|
|
9639
10690
|
if (!res.ok) return null;
|
|
9640
|
-
return res.json();
|
|
10691
|
+
return (await res.json()) as Session | null;
|
|
9641
10692
|
},
|
|
9642
10693
|
};
|
|
9643
10694
|
},
|
|
@@ -9650,7 +10701,6 @@ export const authService = {
|
|
|
9650
10701
|
import { api } from '${constructsImport("api")}';${options.database ? `
|
|
9651
10702
|
import { database } from '${constructsImport("database")}';` : ""}
|
|
9652
10703
|
import { authService, type Session } from './services/auth.ts';
|
|
9653
|
-
import { logger } from './config/logger.ts';
|
|
9654
10704
|
|
|
9655
10705
|
/**
|
|
9656
10706
|
* The shared endpoint factory — no session required.
|
|
@@ -9708,7 +10758,8 @@ import { database } from '${constructsImport("database")}';` : ""}
|
|
|
9708
10758
|
export const router = api.endpoints${options.database ? ".database(database)" : ""};
|
|
9709
10759
|
`
|
|
9710
10760
|
});
|
|
9711
|
-
if (options.database && declares) files$1.push(...databaseFiles(
|
|
10761
|
+
if (options.database && declares) files$1.push(...databaseFiles());
|
|
10762
|
+
else if (options.database) files$1.push(usersMigration());
|
|
9712
10763
|
if (services.storage && declares) files$1.push({
|
|
9713
10764
|
path: "src/constructs/storage.ts",
|
|
9714
10765
|
content: `import { ObjectStorage } from '@geekmidas/constructs/object-storage';
|
|
@@ -9858,9 +10909,9 @@ const minimalTemplate = {
|
|
|
9858
10909
|
"@geekmidas/rate-limit": GEEKMIDAS_VERSIONS["@geekmidas/rate-limit"],
|
|
9859
10910
|
"@geekmidas/schema": GEEKMIDAS_VERSIONS["@geekmidas/schema"],
|
|
9860
10911
|
"@hono/node-server": "~1.14.1",
|
|
9861
|
-
hono:
|
|
9862
|
-
pino:
|
|
9863
|
-
zod:
|
|
10912
|
+
hono: DEPENDENCY_VERSIONS.hono,
|
|
10913
|
+
pino: DEPENDENCY_VERSIONS.pino,
|
|
10914
|
+
zod: DEPENDENCY_VERSIONS.zod
|
|
9864
10915
|
},
|
|
9865
10916
|
devDependencies: {
|
|
9866
10917
|
"@biomejs/biome": "~2.3.0",
|
|
@@ -9884,7 +10935,7 @@ const minimalTemplate = {
|
|
|
9884
10935
|
},
|
|
9885
10936
|
files: (options) => {
|
|
9886
10937
|
const { loggerType, routesStructure, name: name$1 } = options;
|
|
9887
|
-
const db = databaseFor(
|
|
10938
|
+
const db = databaseFor();
|
|
9888
10939
|
const loggerContent = `import { createLogger } from '@geekmidas/logger/${loggerType}';
|
|
9889
10940
|
|
|
9890
10941
|
export const logger = createLogger();
|
|
@@ -9904,6 +10955,9 @@ export const logger = createLogger();
|
|
|
9904
10955
|
import { logger } from '../config/logger.ts';
|
|
9905
10956
|
|
|
9906
10957
|
export const api = new RestApi('Api', {
|
|
10958
|
+
// The project is the app.
|
|
10959
|
+
path: '.',
|
|
10960
|
+
|
|
9907
10961
|
// Typed out rather than omitted: an API that ships open because a field was
|
|
9908
10962
|
// left off is the one default worth refusing to have.
|
|
9909
10963
|
defaultAuthorizer: 'none',
|
|
@@ -9926,7 +10980,7 @@ export const envParser = new EnvironmentParser({ ...process.env, ...Credentials
|
|
|
9926
10980
|
export const config = envParser
|
|
9927
10981
|
.create((get) => ({
|
|
9928
10982
|
nodeEnv: get('NODE_ENV').enum(['development', 'test', 'production']).default('development'),
|
|
9929
|
-
stage:
|
|
10983
|
+
stage: ${stageEnv(options.stages)},
|
|
9930
10984
|
}))
|
|
9931
10985
|
.parse();
|
|
9932
10986
|
`
|
|
@@ -9953,7 +11007,7 @@ export const healthEndpoint = api
|
|
|
9953
11007
|
`
|
|
9954
11008
|
}
|
|
9955
11009
|
];
|
|
9956
|
-
if (options.database) files$1.push(...databaseFiles(
|
|
11010
|
+
if (options.database) files$1.push(...databaseFiles());
|
|
9957
11011
|
if (options.telescope) files$1.push({
|
|
9958
11012
|
path: "src/config/telescope.ts",
|
|
9959
11013
|
content: `import { Telescope } from '@geekmidas/telescope';
|
|
@@ -10017,8 +11071,8 @@ const serverlessTemplate = {
|
|
|
10017
11071
|
"@geekmidas/rate-limit": GEEKMIDAS_VERSIONS["@geekmidas/rate-limit"],
|
|
10018
11072
|
"@geekmidas/schema": GEEKMIDAS_VERSIONS["@geekmidas/schema"],
|
|
10019
11073
|
"@hono/node-server": "~1.14.1",
|
|
10020
|
-
hono:
|
|
10021
|
-
pino:
|
|
11074
|
+
hono: DEPENDENCY_VERSIONS.hono,
|
|
11075
|
+
pino: DEPENDENCY_VERSIONS.pino
|
|
10022
11076
|
},
|
|
10023
11077
|
devDependencies: {
|
|
10024
11078
|
"@biomejs/biome": "~2.3.0",
|
|
@@ -10061,6 +11115,9 @@ export const logger = createLogger();
|
|
|
10061
11115
|
import { logger } from '../config/logger.ts';
|
|
10062
11116
|
|
|
10063
11117
|
export const api = new RestApi('Api', {
|
|
11118
|
+
// The project is the app.
|
|
11119
|
+
path: '.',
|
|
11120
|
+
|
|
10064
11121
|
// Typed out rather than omitted: an API that ships open because a field was
|
|
10065
11122
|
// left off is the one default worth refusing to have.
|
|
10066
11123
|
defaultAuthorizer: 'none',
|
|
@@ -10083,7 +11140,7 @@ export const envParser = new EnvironmentParser({ ...process.env, ...Credentials
|
|
|
10083
11140
|
export const config = envParser
|
|
10084
11141
|
.create((get) => ({
|
|
10085
11142
|
nodeEnv: get('NODE_ENV').enum(['development', 'test', 'production']).default('development'),
|
|
10086
|
-
stage:
|
|
11143
|
+
stage: ${stageEnv(options.stages)},
|
|
10087
11144
|
}))
|
|
10088
11145
|
.parse();
|
|
10089
11146
|
`
|
|
@@ -10113,19 +11170,27 @@ export const healthEndpoint = api
|
|
|
10113
11170
|
},
|
|
10114
11171
|
{
|
|
10115
11172
|
path: "src/functions/hello.ts",
|
|
10116
|
-
content: `import {
|
|
10117
|
-
import {
|
|
11173
|
+
content: `import { z } from 'zod';
|
|
11174
|
+
import { worker } from '~/constructs/worker.ts';
|
|
10118
11175
|
|
|
10119
|
-
export const helloFunction =
|
|
11176
|
+
export const helloFunction = worker
|
|
10120
11177
|
.input(z.object({ name: z.string() }))
|
|
10121
11178
|
.output(z.object({ message: z.string() }))
|
|
10122
11179
|
.handle(async ({ input }) => ({
|
|
10123
11180
|
message: \`Hello, \${input.name}!\`,
|
|
10124
11181
|
}));
|
|
11182
|
+
`
|
|
11183
|
+
},
|
|
11184
|
+
{
|
|
11185
|
+
path: "src/constructs/worker.ts",
|
|
11186
|
+
content: `import { Worker } from '@geekmidas/constructs/worker';
|
|
11187
|
+
import { logger } from '../config/logger.ts';
|
|
11188
|
+
|
|
11189
|
+
export const worker = new Worker('Jobs', { logger });
|
|
10125
11190
|
`
|
|
10126
11191
|
}
|
|
10127
11192
|
];
|
|
10128
|
-
if (options.database) files$1.push(...databaseFiles(
|
|
11193
|
+
if (options.database) files$1.push(...databaseFiles());
|
|
10129
11194
|
if (options.telescope) files$1.push({
|
|
10130
11195
|
path: "src/config/telescope.ts",
|
|
10131
11196
|
content: `import { Telescope } from '@geekmidas/telescope';
|
|
@@ -10134,7 +11199,7 @@ import { InMemoryStorage } from '@geekmidas/telescope/storage/memory';
|
|
|
10134
11199
|
// Note: For production Lambda, consider using a persistent storage
|
|
10135
11200
|
export const telescope = new Telescope({
|
|
10136
11201
|
storage: new InMemoryStorage({ maxEntries: 50 }),
|
|
10137
|
-
enabled: process.env.STAGE === '
|
|
11202
|
+
enabled: process.env.STAGE === '${options.stages.local}',
|
|
10138
11203
|
});
|
|
10139
11204
|
`
|
|
10140
11205
|
});
|
|
@@ -10144,6 +11209,22 @@ export const telescope = new Telescope({
|
|
|
10144
11209
|
|
|
10145
11210
|
//#endregion
|
|
10146
11211
|
//#region src/init/templates/worker.ts
|
|
11212
|
+
/**
|
|
11213
|
+
* A project that is background work and nothing else.
|
|
11214
|
+
*
|
|
11215
|
+
* It used to scaffold a `RestApi` with no endpoints on it — an HTTP surface
|
|
11216
|
+
* declared by a project whose premise is that nothing calls it over HTTP —
|
|
11217
|
+
* because a surface was the only construct that made an app exist and the only
|
|
11218
|
+
* way to get a logger into the generated runtime. `Worker` is the construct
|
|
11219
|
+
* that was missing: it takes no authorizer, publishes no address, and hands out
|
|
11220
|
+
* factories already carrying its logger.
|
|
11221
|
+
*
|
|
11222
|
+
* It scaffolds a cron when the project has a database, and not otherwise. A
|
|
11223
|
+
* server fires its own crons, and the schedule lives in Postgres so that
|
|
11224
|
+
* running more than one replica still fires each job once — so a worker names
|
|
11225
|
+
* the database it keeps schedules in. Without one there is nowhere to keep
|
|
11226
|
+
* them, and an example that could not run is worse than none.
|
|
11227
|
+
*/
|
|
10147
11228
|
const workerTemplate = {
|
|
10148
11229
|
name: "worker",
|
|
10149
11230
|
description: "Background job processing",
|
|
@@ -10153,11 +11234,8 @@ const workerTemplate = {
|
|
|
10153
11234
|
"@geekmidas/envkit": GEEKMIDAS_VERSIONS["@geekmidas/envkit"],
|
|
10154
11235
|
"@geekmidas/events": GEEKMIDAS_VERSIONS["@geekmidas/events"],
|
|
10155
11236
|
"@geekmidas/logger": GEEKMIDAS_VERSIONS["@geekmidas/logger"],
|
|
10156
|
-
"@geekmidas/rate-limit": GEEKMIDAS_VERSIONS["@geekmidas/rate-limit"],
|
|
10157
11237
|
"@geekmidas/schema": GEEKMIDAS_VERSIONS["@geekmidas/schema"],
|
|
10158
|
-
|
|
10159
|
-
hono: "~4.8.2",
|
|
10160
|
-
pino: "~9.6.0"
|
|
11238
|
+
pino: DEPENDENCY_VERSIONS.pino
|
|
10161
11239
|
},
|
|
10162
11240
|
devDependencies: {
|
|
10163
11241
|
"@biomejs/biome": "~2.3.0",
|
|
@@ -10179,34 +11257,18 @@ const workerTemplate = {
|
|
|
10179
11257
|
"fmt:check": "biome format ."
|
|
10180
11258
|
},
|
|
10181
11259
|
files: (options) => {
|
|
10182
|
-
const { loggerType,
|
|
11260
|
+
const { loggerType, name: name$1, database } = options;
|
|
10183
11261
|
const loggerContent = `import { createLogger } from '@geekmidas/logger/${loggerType}';
|
|
10184
11262
|
|
|
10185
11263
|
export const logger = createLogger();
|
|
10186
11264
|
`;
|
|
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
11265
|
const files$1 = [
|
|
10196
11266
|
{
|
|
10197
|
-
path: "src/constructs/
|
|
10198
|
-
content: `import {
|
|
10199
|
-
import { logger } from '../config/logger.ts';
|
|
11267
|
+
path: "src/constructs/worker.ts",
|
|
11268
|
+
content: `import { Worker } from '@geekmidas/constructs/worker';
|
|
11269
|
+
import { logger } from '../config/logger.ts';${database ? "\nimport { database } from './database.ts';" : ""}
|
|
10200
11270
|
|
|
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
|
-
});
|
|
11271
|
+
export const worker = new Worker('Jobs', { logger })${database ? ".database(database)" : ""};
|
|
10210
11272
|
`
|
|
10211
11273
|
},
|
|
10212
11274
|
{
|
|
@@ -10221,7 +11283,7 @@ export const envParser = new EnvironmentParser({ ...process.env, ...Credentials
|
|
|
10221
11283
|
export const config = envParser
|
|
10222
11284
|
.create((get) => ({
|
|
10223
11285
|
nodeEnv: get('NODE_ENV').enum(['development', 'test', 'production']).default('development'),
|
|
10224
|
-
stage:
|
|
11286
|
+
stage: ${stageEnv(options.stages)},
|
|
10225
11287
|
}))
|
|
10226
11288
|
.parse();
|
|
10227
11289
|
`
|
|
@@ -10230,23 +11292,6 @@ export const config = envParser
|
|
|
10230
11292
|
path: "src/config/logger.ts",
|
|
10231
11293
|
content: loggerContent
|
|
10232
11294
|
},
|
|
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
11295
|
{
|
|
10251
11296
|
path: "src/events/types.ts",
|
|
10252
11297
|
content: `import type { PublishableMessage } from '@geekmidas/events';
|
|
@@ -10288,46 +11333,47 @@ export const eventsPublisherService = {
|
|
|
10288
11333
|
},
|
|
10289
11334
|
{
|
|
10290
11335
|
path: "src/subscribers/user-events.ts",
|
|
10291
|
-
content: `import {
|
|
11336
|
+
content: `import { worker } from '~/constructs/worker.ts';
|
|
10292
11337
|
import { eventsPublisherService } from '~/events/publisher.ts';
|
|
10293
11338
|
|
|
10294
|
-
export const userEventsSubscriber =
|
|
11339
|
+
export const userEventsSubscriber = worker
|
|
10295
11340
|
.publisher(eventsPublisherService)
|
|
10296
11341
|
.subscribe(['user.created', 'user.updated'])
|
|
10297
|
-
.handle(async ({
|
|
10298
|
-
|
|
10299
|
-
|
|
10300
|
-
|
|
10301
|
-
|
|
10302
|
-
|
|
10303
|
-
|
|
10304
|
-
|
|
10305
|
-
|
|
10306
|
-
|
|
10307
|
-
|
|
10308
|
-
|
|
11342
|
+
.handle(async ({ events, logger }) => {
|
|
11343
|
+
// A batch, not a single event. Both transports deliver in batches, so
|
|
11344
|
+
// handling them one at a time is a round trip per event for no reason.
|
|
11345
|
+
logger.info({ count: events.length }, 'Processing user events');
|
|
11346
|
+
|
|
11347
|
+
for (const event of events) {
|
|
11348
|
+
switch (event.type) {
|
|
11349
|
+
case 'user.created':
|
|
11350
|
+
logger.info({ userId: event.payload.userId }, 'New user created');
|
|
11351
|
+
break;
|
|
11352
|
+
case 'user.updated':
|
|
11353
|
+
logger.info({ userId: event.payload.userId }, 'User updated');
|
|
11354
|
+
break;
|
|
11355
|
+
}
|
|
10309
11356
|
}
|
|
10310
11357
|
});
|
|
10311
11358
|
`
|
|
10312
|
-
}
|
|
10313
|
-
|
|
10314
|
-
|
|
10315
|
-
|
|
11359
|
+
}
|
|
11360
|
+
];
|
|
11361
|
+
if (database) files$1.push({
|
|
11362
|
+
path: "src/crons/cleanup.ts",
|
|
11363
|
+
content: `import { worker } from '~/constructs/worker.ts';
|
|
10316
11364
|
|
|
10317
|
-
|
|
10318
|
-
|
|
11365
|
+
export const cleanup = worker
|
|
11366
|
+
.cron('rate(1 day)')
|
|
10319
11367
|
.handle(async ({ logger }) => {
|
|
10320
|
-
logger.info('Running cleanup
|
|
11368
|
+
logger.info('Running cleanup');
|
|
10321
11369
|
|
|
10322
|
-
//
|
|
10323
|
-
// e.g., delete old sessions, clean up temp files, etc.
|
|
11370
|
+
// Delete old sessions, clear temp files, whatever falls out of date.
|
|
10324
11371
|
|
|
10325
|
-
logger.info('Cleanup
|
|
11372
|
+
logger.info('Cleanup complete');
|
|
10326
11373
|
});
|
|
10327
11374
|
`
|
|
10328
|
-
|
|
10329
|
-
|
|
10330
|
-
if (options.database) files$1.push(...databaseFiles(name$1));
|
|
11375
|
+
});
|
|
11376
|
+
if (options.database) files$1.push(...databaseFiles());
|
|
10331
11377
|
if (options.telescope) files$1.push({
|
|
10332
11378
|
path: "src/config/telescope.ts",
|
|
10333
11379
|
content: `import { Telescope } from '@geekmidas/telescope';
|
|
@@ -10348,7 +11394,7 @@ export const telescope = new Telescope({
|
|
|
10348
11394
|
/**
|
|
10349
11395
|
* OpenAPI output path (fixed, not configurable)
|
|
10350
11396
|
*/
|
|
10351
|
-
const OPENAPI_OUTPUT_PATH$1 = "./.gkm/openapi.ts";
|
|
11397
|
+
const OPENAPI_OUTPUT_PATH$1 = "./.gkm/openapi/api.ts";
|
|
10352
11398
|
/**
|
|
10353
11399
|
* Endpoints output path (generated by gkm dev/build)
|
|
10354
11400
|
*/
|
|
@@ -10454,15 +11500,23 @@ const packageManagerChoices = [
|
|
|
10454
11500
|
/**
|
|
10455
11501
|
* Deploy target choices for prompts
|
|
10456
11502
|
*/
|
|
10457
|
-
const deployTargetChoices = [
|
|
10458
|
-
|
|
10459
|
-
|
|
10460
|
-
|
|
10461
|
-
|
|
10462
|
-
|
|
10463
|
-
|
|
10464
|
-
|
|
10465
|
-
|
|
11503
|
+
const deployTargetChoices = [
|
|
11504
|
+
{
|
|
11505
|
+
title: "Dokploy",
|
|
11506
|
+
value: "dokploy",
|
|
11507
|
+
description: "Deploy to Dokploy (Docker-based hosting)"
|
|
11508
|
+
},
|
|
11509
|
+
{
|
|
11510
|
+
title: "AWS (SST)",
|
|
11511
|
+
value: "sst",
|
|
11512
|
+
description: "Deploy to your AWS account through SST"
|
|
11513
|
+
},
|
|
11514
|
+
{
|
|
11515
|
+
title: "Configure later",
|
|
11516
|
+
value: "none",
|
|
11517
|
+
description: "Skip deployment setup for now"
|
|
11518
|
+
}
|
|
11519
|
+
];
|
|
10466
11520
|
/**
|
|
10467
11521
|
* Services choices for multi-select prompt
|
|
10468
11522
|
*/
|
|
@@ -10555,12 +11609,12 @@ function generatePackageJson(options, template) {
|
|
|
10555
11609
|
if (declares && services.mail) dependencies$1["@geekmidas/emailkit"] = GEEKMIDAS_VERSIONS["@geekmidas/emailkit"];
|
|
10556
11610
|
if (database) {
|
|
10557
11611
|
dependencies$1["@geekmidas/db"] = GEEKMIDAS_VERSIONS["@geekmidas/db"];
|
|
10558
|
-
dependencies$1.kysely =
|
|
10559
|
-
dependencies$1.pg =
|
|
10560
|
-
devDependencies$1["@types/pg"] = "~8.
|
|
11612
|
+
dependencies$1.kysely = DEPENDENCY_VERSIONS.kysely;
|
|
11613
|
+
dependencies$1.pg = DEPENDENCY_VERSIONS.pg;
|
|
11614
|
+
devDependencies$1["@types/pg"] = "~8.23.1";
|
|
10561
11615
|
devDependencies$1["@geekmidas/testkit"] = GEEKMIDAS_VERSIONS["@geekmidas/testkit"];
|
|
10562
11616
|
devDependencies$1["@faker-js/faker"] = "~9.8.0";
|
|
10563
|
-
devDependencies$1["kysely-ctl"] = "
|
|
11617
|
+
devDependencies$1["kysely-ctl"] = DEPENDENCY_VERSIONS["kysely-ctl"];
|
|
10564
11618
|
devDependencies$1["vite-tsconfig-paths"] = "~5.1.0";
|
|
10565
11619
|
}
|
|
10566
11620
|
if (monorepo) {
|
|
@@ -10580,6 +11634,8 @@ function generatePackageJson(options, template) {
|
|
|
10580
11634
|
const appName = pathParts[pathParts.length - 1] || "api";
|
|
10581
11635
|
packageName = `@${name$1}/${appName}`;
|
|
10582
11636
|
}
|
|
11637
|
+
dependencies$1["@geekmidas/client"] = GEEKMIDAS_VERSIONS["@geekmidas/client"];
|
|
11638
|
+
dependencies$1["@tanstack/react-query"] = DEPENDENCY_VERSIONS["@tanstack/react-query"];
|
|
10583
11639
|
const packageJson = {
|
|
10584
11640
|
name: packageName,
|
|
10585
11641
|
version: "0.0.1",
|
|
@@ -10617,7 +11673,7 @@ function generateSourceFiles(options, template) {
|
|
|
10617
11673
|
*/
|
|
10618
11674
|
function generateTestFiles(options, _template) {
|
|
10619
11675
|
if (!options.database) return [];
|
|
10620
|
-
const db = databaseFor(
|
|
11676
|
+
const db = databaseFor();
|
|
10621
11677
|
const declares = !options.monorepo;
|
|
10622
11678
|
const schema = declares ? "../src/constructs/database.ts" : "~/services/database.ts";
|
|
10623
11679
|
const runtimeUrl = declares ? db.urlKey : "DATABASE_URL";
|
|
@@ -10653,14 +11709,16 @@ import pg from 'pg';
|
|
|
10653
11709
|
import { wrapVitestKyselyTransaction } from '@geekmidas/testkit/kysely';
|
|
10654
11710
|
import type { Database } from '${schema}';
|
|
10655
11711
|
|
|
10656
|
-
const
|
|
11712
|
+
const db = new Kysely<Database>({
|
|
10657
11713
|
dialect: new PostgresDialect({
|
|
10658
11714
|
pool: new pg.Pool({ connectionString: process.env.${runtimeUrl} }),
|
|
10659
11715
|
}),
|
|
10660
11716
|
});
|
|
10661
11717
|
|
|
10662
11718
|
export const it = wrapVitestKyselyTransaction<Database>(itVitest, {
|
|
10663
|
-
|
|
11719
|
+
// A function, or a construct: an instance is taken for a construct and has
|
|
11720
|
+
// no service to register.
|
|
11721
|
+
connection: () => db,
|
|
10664
11722
|
});
|
|
10665
11723
|
`
|
|
10666
11724
|
},
|
|
@@ -10745,10 +11803,10 @@ export const usersBuilder = KyselyFactory.createBuilder<Database, 'users'>(
|
|
|
10745
11803
|
import { it } from './config.ts';
|
|
10746
11804
|
|
|
10747
11805
|
describe('example', () => {
|
|
10748
|
-
it('should have a working test setup', async ({
|
|
10749
|
-
//
|
|
10750
|
-
//
|
|
10751
|
-
expect(
|
|
11806
|
+
it('should have a working test setup', async ({ trx }) => {
|
|
11807
|
+
// trx is a Kysely transaction; everything in it is rolled back after the
|
|
11808
|
+
// test
|
|
11809
|
+
expect(trx).toBeDefined();
|
|
10752
11810
|
});
|
|
10753
11811
|
});
|
|
10754
11812
|
`
|
|
@@ -11999,8 +13057,8 @@ export const Alert: Story = {
|
|
|
11999
13057
|
),
|
|
12000
13058
|
};
|
|
12001
13059
|
`;
|
|
12002
|
-
const componentsUiIndex = `export { Button, type ButtonProps, buttonVariants } from './button.tsx';
|
|
12003
|
-
export { Input } from './input.tsx';
|
|
13060
|
+
const componentsUiIndex = `export { Button, type ButtonProps, buttonVariants } from './button/index.tsx';
|
|
13061
|
+
export { Input } from './input/index.tsx';
|
|
12004
13062
|
export {
|
|
12005
13063
|
Card,
|
|
12006
13064
|
CardHeader,
|
|
@@ -12008,17 +13066,17 @@ export {
|
|
|
12008
13066
|
CardTitle,
|
|
12009
13067
|
CardDescription,
|
|
12010
13068
|
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';
|
|
13069
|
+
} from './card/index.tsx';
|
|
13070
|
+
export { Label } from './label/index.tsx';
|
|
13071
|
+
export { Badge, type BadgeProps, badgeVariants } from './badge/index.tsx';
|
|
13072
|
+
export { Separator } from './separator/index.tsx';
|
|
13073
|
+
export { Tabs, TabsList, TabsTrigger, TabsContent } from './tabs/index.tsx';
|
|
12016
13074
|
export {
|
|
12017
13075
|
Tooltip,
|
|
12018
13076
|
TooltipTrigger,
|
|
12019
13077
|
TooltipContent,
|
|
12020
13078
|
TooltipProvider,
|
|
12021
|
-
} from './tooltip.tsx';
|
|
13079
|
+
} from './tooltip/index.tsx';
|
|
12022
13080
|
export {
|
|
12023
13081
|
Dialog,
|
|
12024
13082
|
DialogPortal,
|
|
@@ -12030,7 +13088,7 @@ export {
|
|
|
12030
13088
|
DialogFooter,
|
|
12031
13089
|
DialogTitle,
|
|
12032
13090
|
DialogDescription,
|
|
12033
|
-
} from './dialog.tsx';
|
|
13091
|
+
} from './dialog/index.tsx';
|
|
12034
13092
|
`;
|
|
12035
13093
|
const buttonIndexTsx = buttonTsx;
|
|
12036
13094
|
const inputIndexTsx = inputTsx;
|
|
@@ -12198,8 +13256,8 @@ function generateWebAppFiles(options) {
|
|
|
12198
13256
|
[uiPackage]: "workspace:*",
|
|
12199
13257
|
"@geekmidas/client": GEEKMIDAS_VERSIONS["@geekmidas/client"],
|
|
12200
13258
|
"@geekmidas/envkit": GEEKMIDAS_VERSIONS["@geekmidas/envkit"],
|
|
12201
|
-
"@tanstack/react-query": "
|
|
12202
|
-
"better-auth": "
|
|
13259
|
+
"@tanstack/react-query": DEPENDENCY_VERSIONS["@tanstack/react-query"],
|
|
13260
|
+
"better-auth": DEPENDENCY_VERSIONS["better-auth"],
|
|
12203
13261
|
next: "~16.1.0",
|
|
12204
13262
|
react: "~19.2.0",
|
|
12205
13263
|
"react-dom": "~19.2.0"
|
|
@@ -12255,7 +13313,7 @@ export default nextConfig;
|
|
|
12255
13313
|
baseUrl: ".",
|
|
12256
13314
|
paths: {
|
|
12257
13315
|
"~/*": ["./src/*", "../../packages/ui/src/*"],
|
|
12258
|
-
[`${apiPackage}/client`]: ["../../apps/api/.gkm/openapi.ts"],
|
|
13316
|
+
[`${apiPackage}/client`]: ["../../apps/api/.gkm/openapi/api.ts"],
|
|
12259
13317
|
[`${modelsPackage}`]: ["../../packages/models/src"],
|
|
12260
13318
|
[`${modelsPackage}/*`]: ["../../packages/models/src/*"],
|
|
12261
13319
|
[`${uiPackage}`]: ["../../packages/ui/src"],
|
|
@@ -12268,12 +13326,7 @@ export default nextConfig;
|
|
|
12268
13326
|
"**/*.tsx",
|
|
12269
13327
|
".next/types/**/*.ts"
|
|
12270
13328
|
],
|
|
12271
|
-
exclude: ["node_modules"]
|
|
12272
|
-
references: [
|
|
12273
|
-
{ path: "../../apps/api" },
|
|
12274
|
-
{ path: "../../packages/ui" },
|
|
12275
|
-
{ path: "../../packages/models" }
|
|
12276
|
-
]
|
|
13329
|
+
exclude: ["node_modules"]
|
|
12277
13330
|
};
|
|
12278
13331
|
const queryClientTs = `import { QueryClient } from '@tanstack/react-query';
|
|
12279
13332
|
|
|
@@ -12536,10 +13589,10 @@ function generateTanStackWebFiles(options) {
|
|
|
12536
13589
|
[uiPackage]: "workspace:*",
|
|
12537
13590
|
"@geekmidas/client": GEEKMIDAS_VERSIONS["@geekmidas/client"],
|
|
12538
13591
|
"@geekmidas/envkit": GEEKMIDAS_VERSIONS["@geekmidas/envkit"],
|
|
12539
|
-
"@tanstack/react-query": "
|
|
13592
|
+
"@tanstack/react-query": DEPENDENCY_VERSIONS["@tanstack/react-query"],
|
|
12540
13593
|
"@tanstack/react-router": "^1.87.0",
|
|
12541
13594
|
"@tanstack/react-start": "^1.87.0",
|
|
12542
|
-
"better-auth": "
|
|
13595
|
+
"better-auth": DEPENDENCY_VERSIONS["better-auth"],
|
|
12543
13596
|
react: "~19.2.0",
|
|
12544
13597
|
"react-dom": "~19.2.0"
|
|
12545
13598
|
},
|
|
@@ -12607,7 +13660,7 @@ export default defineConfig({
|
|
|
12607
13660
|
[`${modelsPackage}/*`]: ["../../packages/models/src/*"],
|
|
12608
13661
|
[uiPackage]: ["../../packages/ui/src"],
|
|
12609
13662
|
[`${uiPackage}/*`]: ["../../packages/ui/src/*"],
|
|
12610
|
-
[`${apiPackage}/client`]: ["../../apps/api/.gkm/openapi.ts"]
|
|
13663
|
+
[`${apiPackage}/client`]: ["../../apps/api/.gkm/openapi/api.ts"]
|
|
12611
13664
|
}
|
|
12612
13665
|
},
|
|
12613
13666
|
include: [
|
|
@@ -12897,12 +13950,27 @@ function detectPackageManager(cwd = process.cwd()) {
|
|
|
12897
13950
|
if (userAgent.includes("bun")) return "bun";
|
|
12898
13951
|
return "npm";
|
|
12899
13952
|
}
|
|
13953
|
+
/** `gkm init`'s flags, which a name can swallow when a space goes missing. */
|
|
13954
|
+
const INIT_FLAGS = [
|
|
13955
|
+
"yes",
|
|
13956
|
+
"name",
|
|
13957
|
+
"template",
|
|
13958
|
+
"monorepo",
|
|
13959
|
+
"api-path",
|
|
13960
|
+
"pm",
|
|
13961
|
+
"skip-install"
|
|
13962
|
+
];
|
|
12900
13963
|
/**
|
|
12901
13964
|
* Validate project name for npm package naming conventions
|
|
12902
13965
|
*/
|
|
12903
13966
|
function validateProjectName(name$1) {
|
|
12904
13967
|
if (!name$1) return "Project name is required";
|
|
12905
13968
|
if (!/^[a-z0-9-_@/.]+$/i.test(name$1)) return "Project name can only contain letters, numbers, hyphens, underscores, @, /, and .";
|
|
13969
|
+
const flag = name$1.match(/--(\w[\w-]*)$/)?.[1];
|
|
13970
|
+
if (flag && INIT_FLAGS.includes(flag)) {
|
|
13971
|
+
const intended = name$1.slice(0, -(flag.length + 2));
|
|
13972
|
+
return `"${name$1}" ends in the --${flag} flag. Did you mean \`${intended} --${flag}\`?`;
|
|
13973
|
+
}
|
|
12906
13974
|
const reserved = [
|
|
12907
13975
|
"node_modules",
|
|
12908
13976
|
".git",
|
|
@@ -12942,6 +14010,18 @@ function getRunCommand(pkgManager, script) {
|
|
|
12942
14010
|
default: return `npm run ${script}`;
|
|
12943
14011
|
}
|
|
12944
14012
|
}
|
|
14013
|
+
/**
|
|
14014
|
+
* Run a binary the project installed, through its package manager — so it is
|
|
14015
|
+
* the project's own version, not whatever \`npx\` would fetch.
|
|
14016
|
+
*/
|
|
14017
|
+
function getExecCommand(pkgManager, command) {
|
|
14018
|
+
switch (pkgManager) {
|
|
14019
|
+
case "pnpm": return `pnpm exec ${command}`;
|
|
14020
|
+
case "yarn": return `yarn ${command}`;
|
|
14021
|
+
case "bun": return `bunx ${command}`;
|
|
14022
|
+
default: return `npx --no-install ${command}`;
|
|
14023
|
+
}
|
|
14024
|
+
}
|
|
12945
14025
|
const lockfileByPm = {
|
|
12946
14026
|
pnpm: "pnpm-lock.yaml",
|
|
12947
14027
|
yarn: "yarn.lock",
|
|
@@ -13012,12 +14092,31 @@ function findWorkspacePackages(cwd, pm) {
|
|
|
13012
14092
|
|
|
13013
14093
|
//#endregion
|
|
13014
14094
|
//#region src/init/index.ts
|
|
14095
|
+
/** `' staging, prod '` → `['staging', 'prod']`. */
|
|
14096
|
+
const stageList = (value) => value.split(",").map((name$1) => name$1.trim()).filter(Boolean);
|
|
14097
|
+
/** `eu-west-1`, `us-gov-west-1`, `ap-southeast-2`. */
|
|
14098
|
+
const AWS_REGION = /^[a-z]{2}(-gov)?-[a-z]+-\d$/;
|
|
13015
14099
|
/**
|
|
13016
14100
|
* Main init command - scaffolds a new project
|
|
13017
14101
|
*/
|
|
13018
14102
|
async function initCommand(projectName, options = {}) {
|
|
13019
14103
|
const cwd = process.cwd();
|
|
13020
14104
|
const detectedPkgManager = detectPackageManager(cwd);
|
|
14105
|
+
if (options.deploy && !deployTargetChoices.some((choice) => choice.value === options.deploy)) throw new UnknownDeployTarget(options.deploy, deployTargetChoices.map((c) => c.value));
|
|
14106
|
+
if (options.region && !AWS_REGION.test(options.region)) throw new NotAnAwsRegion(options.region);
|
|
14107
|
+
if (options.stages || options.localStage || options.protectedStage) {
|
|
14108
|
+
const deployed = stageList(options.stages ?? "");
|
|
14109
|
+
const problems = [
|
|
14110
|
+
...deployedProblems(deployed),
|
|
14111
|
+
...options.localStage ? stageProblems({
|
|
14112
|
+
local: options.localStage,
|
|
14113
|
+
deployed
|
|
14114
|
+
}) : [],
|
|
14115
|
+
...options.protectedStage && !deployed.includes(options.protectedStage) ? [`--protected-stage "${options.protectedStage}" is not in --stages`] : []
|
|
14116
|
+
];
|
|
14117
|
+
if (problems.length) throw new InvalidStages([...new Set(problems)]);
|
|
14118
|
+
}
|
|
14119
|
+
let deployedSoFar = [];
|
|
13021
14120
|
prompts.override({});
|
|
13022
14121
|
const onCancel = () => {
|
|
13023
14122
|
process.exit(0);
|
|
@@ -13068,12 +14167,49 @@ async function initCommand(projectName, options = {}) {
|
|
|
13068
14167
|
initial: packageManagerChoices.findIndex((c) => c.value === detectedPkgManager)
|
|
13069
14168
|
},
|
|
13070
14169
|
{
|
|
13071
|
-
type: options.yes ? null : "select",
|
|
14170
|
+
type: options.yes || options.deploy ? null : "select",
|
|
13072
14171
|
name: "deployTarget",
|
|
13073
14172
|
message: "Deployment target:",
|
|
13074
14173
|
choices: deployTargetChoices,
|
|
13075
14174
|
initial: 0
|
|
13076
14175
|
},
|
|
14176
|
+
{
|
|
14177
|
+
type: (_prev, values) => !options.yes && !options.region && (options.deploy ?? values.deployTarget) === "sst" ? "text" : null,
|
|
14178
|
+
name: "region",
|
|
14179
|
+
message: "AWS region (e.g. eu-west-1):",
|
|
14180
|
+
validate: (value) => AWS_REGION.test(value.trim()) || "An AWS region, like eu-west-1"
|
|
14181
|
+
},
|
|
14182
|
+
{
|
|
14183
|
+
type: options.yes || options.stages ? null : "text",
|
|
14184
|
+
name: "deployedStages",
|
|
14185
|
+
message: "Deployed stages, comma-separated (e.g. staging, prod):",
|
|
14186
|
+
validate: (value) => {
|
|
14187
|
+
const deployed = stageList(value);
|
|
14188
|
+
if (deployed.length === 0) return "At least one stage";
|
|
14189
|
+
return deployedProblems(deployed)[0] ?? true;
|
|
14190
|
+
}
|
|
14191
|
+
},
|
|
14192
|
+
{
|
|
14193
|
+
type: (_prev, values) => options.yes || options.protectedStage ? null : stageList(options.stages ?? values.deployedStages ?? "").length > 1 ? "select" : null,
|
|
14194
|
+
name: "protectedStage",
|
|
14195
|
+
message: "Which one is production (retained and protected)?",
|
|
14196
|
+
choices: (_prev, values) => stageList(options.stages ?? values.deployedStages ?? "").map((name$2) => ({
|
|
14197
|
+
title: name$2,
|
|
14198
|
+
value: name$2
|
|
14199
|
+
}))
|
|
14200
|
+
},
|
|
14201
|
+
{
|
|
14202
|
+
type: (_prev, values) => {
|
|
14203
|
+
deployedSoFar = stageList(options.stages ?? values.deployedStages ?? "");
|
|
14204
|
+
return options.yes || options.localStage ? null : "text";
|
|
14205
|
+
},
|
|
14206
|
+
name: "localStage",
|
|
14207
|
+
message: "Local stage, what gkm dev runs as (e.g. dev):",
|
|
14208
|
+
validate: (value) => stageProblems({
|
|
14209
|
+
local: value.trim(),
|
|
14210
|
+
deployed: deployedSoFar
|
|
14211
|
+
})[0] ?? true
|
|
14212
|
+
},
|
|
13077
14213
|
{
|
|
13078
14214
|
type: options.yes ? null : "confirm",
|
|
13079
14215
|
name: "telescope",
|
|
@@ -13139,7 +14275,7 @@ async function initCommand(projectName, options = {}) {
|
|
|
13139
14275
|
};
|
|
13140
14276
|
if (services.events === "pgboss") services.db = true;
|
|
13141
14277
|
const pkgManager = options.pm ? options.pm : options.yes ? "pnpm" : answers.packageManager ?? detectedPkgManager;
|
|
13142
|
-
const deployTarget = options.yes ? "
|
|
14278
|
+
const deployTarget = options.deploy ?? (options.yes ? "none" : answers.deployTarget ?? "none");
|
|
13143
14279
|
const database = services.db;
|
|
13144
14280
|
const frontendFramework = isFullstack ? options.yes ? "nextjs" : answers.frontendFramework ?? "nextjs" : void 0;
|
|
13145
14281
|
const templateOptions = {
|
|
@@ -13154,6 +14290,8 @@ async function initCommand(projectName, options = {}) {
|
|
|
13154
14290
|
apiPath: monorepo ? options.apiPath ?? "apps/api" : "",
|
|
13155
14291
|
packageManager: pkgManager,
|
|
13156
14292
|
deployTarget,
|
|
14293
|
+
stages: resolveStages(options, answers),
|
|
14294
|
+
...deployTarget === "sst" ? { region: options.region ?? (options.yes ? "eu-west-1" : answers.region?.trim()) } : {},
|
|
13157
14295
|
services,
|
|
13158
14296
|
frontendFramework
|
|
13159
14297
|
};
|
|
@@ -13183,6 +14321,8 @@ async function initCommand(projectName, options = {}) {
|
|
|
13183
14321
|
] : [];
|
|
13184
14322
|
const dockerFiles = isMonorepo$1 && baseTemplate ? generateDockerFiles(templateOptions, baseTemplate, dbApps) : [];
|
|
13185
14323
|
const rootFiles = baseTemplate ? [
|
|
14324
|
+
...generateAgentFiles(templateOptions, baseTemplate),
|
|
14325
|
+
...generateGithubFiles(templateOptions),
|
|
13186
14326
|
...generateMonorepoFiles(templateOptions, baseTemplate),
|
|
13187
14327
|
...generateRootConstructs(templateOptions),
|
|
13188
14328
|
...generateModelsPackage(templateOptions)
|
|
@@ -13228,12 +14368,12 @@ async function initCommand(projectName, options = {}) {
|
|
|
13228
14368
|
if (services.mail) secretServices.push("mailpit");
|
|
13229
14369
|
if (services.events === "sns") secretServices.push("localstack");
|
|
13230
14370
|
if (services.events === "rabbitmq") secretServices.push("rabbitmq");
|
|
13231
|
-
const
|
|
14371
|
+
const local = templateOptions.stages.local;
|
|
14372
|
+
const devSecrets = createStageSecrets(local, secretServices, {
|
|
13232
14373
|
projectName: name$1,
|
|
13233
14374
|
eventsBackend: services.events
|
|
13234
14375
|
});
|
|
13235
14376
|
const customSecrets = {
|
|
13236
|
-
NODE_ENV: "development",
|
|
13237
14377
|
PORT: "3000",
|
|
13238
14378
|
LOG_LEVEL: "debug",
|
|
13239
14379
|
JWT_SECRET: `dev-${Date.now()}-${Math.random().toString(36).slice(2)}`
|
|
@@ -13253,8 +14393,8 @@ async function initCommand(projectName, options = {}) {
|
|
|
13253
14393
|
}
|
|
13254
14394
|
devSecrets.custom = customSecrets;
|
|
13255
14395
|
await writeStageSecrets(devSecrets, targetDir);
|
|
13256
|
-
const keyPath = getKeyPath(
|
|
13257
|
-
console.log(` Secrets: .gkm/secrets
|
|
14396
|
+
const keyPath = getKeyPath(local, name$1);
|
|
14397
|
+
console.log(` Secrets: .gkm/secrets/${local}.json (encrypted)`);
|
|
13258
14398
|
console.log(` Key: ${keyPath}\n`);
|
|
13259
14399
|
if (!options.skipInstall) {
|
|
13260
14400
|
console.log("\n📦 Installing dependencies...\n");
|
|
@@ -13267,11 +14407,13 @@ async function initCommand(projectName, options = {}) {
|
|
|
13267
14407
|
console.error("Failed to install dependencies");
|
|
13268
14408
|
}
|
|
13269
14409
|
try {
|
|
13270
|
-
execSync("
|
|
14410
|
+
execSync(getExecCommand(pkgManager, "biome check --write ."), {
|
|
13271
14411
|
cwd: targetDir,
|
|
13272
14412
|
stdio: "inherit"
|
|
13273
14413
|
});
|
|
13274
|
-
} catch {
|
|
14414
|
+
} catch {
|
|
14415
|
+
console.warn(`\n⚠️ Formatting did not finish cleanly. Run ${getRunCommand(pkgManager, "fmt")} to see why.`);
|
|
14416
|
+
}
|
|
13275
14417
|
}
|
|
13276
14418
|
console.log("\n📦 Initializing git repository...\n");
|
|
13277
14419
|
try {
|
|
@@ -13343,19 +14485,63 @@ function printNextSteps(projectName, options, pkgManager) {
|
|
|
13343
14485
|
console.log(` └── turbo.json # Turbo config`);
|
|
13344
14486
|
console.log("");
|
|
13345
14487
|
}
|
|
14488
|
+
const { local, deployed } = options.stages;
|
|
13346
14489
|
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
|
|
14490
|
+
console.log(` gkm secrets:show --stage ${local} # View secrets`);
|
|
14491
|
+
console.log(` gkm secrets:set KEY VALUE --stage ${local} # Add secret`);
|
|
14492
|
+
for (const stage of deployed) console.log(` gkm secrets:init --stage ${stage} # Create ${stage} secrets`);
|
|
13350
14493
|
console.log("");
|
|
13351
14494
|
if (options.deployTarget === "dokploy") {
|
|
13352
14495
|
console.log("🚀 Deployment:");
|
|
13353
|
-
console.log(` ${getRunCommand(pkgManager,
|
|
14496
|
+
for (const stage of deployed) console.log(` ${getRunCommand(pkgManager, `deploy:${stage}`)}`);
|
|
14497
|
+
console.log("");
|
|
14498
|
+
}
|
|
14499
|
+
if (options.deployTarget === "sst") {
|
|
14500
|
+
console.log("🚀 Deployment (AWS, through SST):");
|
|
14501
|
+
for (const stage of deployed) console.log(` ${getRunCommand(pkgManager, `deploy:${stage}`)} # gkm build --provider aws, then sst deploy`);
|
|
14502
|
+
console.log(` Uses your AWS credentials, in ${options.region}.`);
|
|
14503
|
+
if (options.services.mail) console.log(" Set MAIL_FROM to a sender verified in SES first.");
|
|
13354
14504
|
console.log("");
|
|
13355
14505
|
}
|
|
13356
14506
|
console.log("📚 Documentation: https://geekmidas.github.io/toolbox/");
|
|
13357
14507
|
console.log("");
|
|
13358
14508
|
}
|
|
14509
|
+
/**
|
|
14510
|
+
* The stages the project declares, from the answers or the flags.
|
|
14511
|
+
*
|
|
14512
|
+
* `--yes` with no stage flags has nobody to ask and needs an answer, so it
|
|
14513
|
+
* takes `local`, and one deployed stage, `production`, that is protected.
|
|
14514
|
+
*/
|
|
14515
|
+
function resolveStages(options, answers) {
|
|
14516
|
+
const deployed = stageList(options.stages ?? answers.deployedStages ?? (options.yes ? "production" : ""));
|
|
14517
|
+
const kept = options.protectedStage ?? answers.protectedStage ?? (deployed.length === 1 || options.yes ? deployed[0] : void 0);
|
|
14518
|
+
const local = (options.localStage ?? answers.localStage ?? (options.yes ? "local" : "")).trim();
|
|
14519
|
+
const stages = {
|
|
14520
|
+
local,
|
|
14521
|
+
deployed,
|
|
14522
|
+
...kept ? { protected: [kept] } : {}
|
|
14523
|
+
};
|
|
14524
|
+
const problems = stageProblems(stages);
|
|
14525
|
+
if (problems.length) throw new InvalidStages(problems);
|
|
14526
|
+
return stages;
|
|
14527
|
+
}
|
|
14528
|
+
/** `--deploy` naming a target init has no scaffold for. */
|
|
14529
|
+
var UnknownDeployTarget = class extends Error {
|
|
14530
|
+
constructor(target, known) {
|
|
14531
|
+
super(`Unknown deploy target "${target}". Use ${known.join(", ")}.`);
|
|
14532
|
+
this.target = target;
|
|
14533
|
+
this.known = known;
|
|
14534
|
+
this.name = "UnknownDeployTarget";
|
|
14535
|
+
}
|
|
14536
|
+
};
|
|
14537
|
+
/** `--region` that is not shaped like one, e.g. `europe` for `eu-west-1`. */
|
|
14538
|
+
var NotAnAwsRegion = class extends Error {
|
|
14539
|
+
constructor(region) {
|
|
14540
|
+
super(`"${region}" is not an AWS region. Use one like eu-west-1.`);
|
|
14541
|
+
this.region = region;
|
|
14542
|
+
this.name = "NotAnAwsRegion";
|
|
14543
|
+
}
|
|
14544
|
+
};
|
|
13359
14545
|
|
|
13360
14546
|
//#endregion
|
|
13361
14547
|
//#region src/secrets/index.ts
|
|
@@ -13566,19 +14752,20 @@ function maskUrl(url) {
|
|
|
13566
14752
|
* Environment variables are sniffed to inject only what the app needs.
|
|
13567
14753
|
*/
|
|
13568
14754
|
async function testCommand(options = {}) {
|
|
13569
|
-
const stage = options.stage ?? "development";
|
|
13570
14755
|
const cwd = process.cwd();
|
|
14756
|
+
const stage = options.stage ?? await loadWorkspaceConfig(cwd).then((loaded) => loaded.workspace.stages.local).catch(() => void 0);
|
|
14757
|
+
if (!stage) throw new NoStageToTest();
|
|
13571
14758
|
console.log(`\n🧪 Running tests with ${stage} environment...\n`);
|
|
13572
14759
|
const autoSetup = options.autoSetup || Boolean(process.env.GKM_AUTO_SETUP);
|
|
13573
14760
|
if (autoSetup) {
|
|
13574
|
-
const { ensureStageSecrets } = await import("./setup-
|
|
14761
|
+
const { ensureStageSecrets } = await import("./setup-BWgMs4gg.mjs");
|
|
13575
14762
|
const generated = await ensureStageSecrets(stage, cwd);
|
|
13576
14763
|
if (generated) console.log(` 🔐 Generated fresh ${stage} secrets (auto-setup)`);
|
|
13577
14764
|
}
|
|
13578
14765
|
const defaultEnv = loadEnvFiles(".env");
|
|
13579
14766
|
if (defaultEnv.loaded.length > 0) console.log(` 📦 Loaded env: ${defaultEnv.loaded.join(", ")}`);
|
|
13580
14767
|
const result = await prepareEntryCredentials({
|
|
13581
|
-
|
|
14768
|
+
stage,
|
|
13582
14769
|
startDocker: true,
|
|
13583
14770
|
secretsFileName: "test-secrets.json",
|
|
13584
14771
|
resolveDockerPorts: "full",
|
|
@@ -13658,146 +14845,313 @@ function rewriteDatabaseUrlForTests(env) {
|
|
|
13658
14845
|
}
|
|
13659
14846
|
return result;
|
|
13660
14847
|
}
|
|
14848
|
+
/** `gkm test` outside a project that declares stages, with none named. */
|
|
14849
|
+
var NoStageToTest = class extends Error {
|
|
14850
|
+
constructor() {
|
|
14851
|
+
super("No stage to test with: add `stages` to gkm.config.ts, or pass --stage.");
|
|
14852
|
+
this.name = "NoStageToTest";
|
|
14853
|
+
}
|
|
14854
|
+
};
|
|
14855
|
+
|
|
14856
|
+
//#endregion
|
|
14857
|
+
//#region src/upgrade/version.ts
|
|
14858
|
+
/**
|
|
14859
|
+
* Just enough semver for `gkm upgrade`: ordering versions, and reading the
|
|
14860
|
+
* floor out of a simple range. The same ordering the release pipeline uses to
|
|
14861
|
+
* decide what is published (`scripts/release-state.mjs`).
|
|
14862
|
+
*/
|
|
14863
|
+
const VERSION = /(\d+)\.(\d+)\.(\d+)(?:-([0-9A-Za-z.-]+))?/;
|
|
14864
|
+
function parse$2(version$1) {
|
|
14865
|
+
const match = version$1.match(VERSION);
|
|
14866
|
+
if (!match) return void 0;
|
|
14867
|
+
const [, major, minor, patch, pre] = match;
|
|
14868
|
+
return {
|
|
14869
|
+
core: [
|
|
14870
|
+
Number(major),
|
|
14871
|
+
Number(minor),
|
|
14872
|
+
Number(patch)
|
|
14873
|
+
],
|
|
14874
|
+
ids: pre ? pre.split(".").map((id) => /^\d+$/.test(id) ? Number(id) : id) : []
|
|
14875
|
+
};
|
|
14876
|
+
}
|
|
14877
|
+
/** Negative, zero or positive, as `a` is below, equal to or above `b`. */
|
|
14878
|
+
function compareVersions(a, b) {
|
|
14879
|
+
const x = parse$2(a);
|
|
14880
|
+
const y = parse$2(b);
|
|
14881
|
+
if (!x || !y) return 0;
|
|
14882
|
+
for (let i = 0; i < 3; i++) if (x.core[i] !== y.core[i]) return x.core[i] - y.core[i];
|
|
14883
|
+
if (!x.ids.length || !y.ids.length) return y.ids.length - x.ids.length;
|
|
14884
|
+
for (let i = 0; i < Math.max(x.ids.length, y.ids.length); i++) {
|
|
14885
|
+
const p = x.ids[i];
|
|
14886
|
+
const q = y.ids[i];
|
|
14887
|
+
if (p === void 0) return -1;
|
|
14888
|
+
if (q === void 0) return 1;
|
|
14889
|
+
if (p === q) continue;
|
|
14890
|
+
if (typeof p === "number" && typeof q === "number") return p - q;
|
|
14891
|
+
if (typeof p === "number") return -1;
|
|
14892
|
+
if (typeof q === "number") return 1;
|
|
14893
|
+
return p < q ? -1 : 1;
|
|
14894
|
+
}
|
|
14895
|
+
return 0;
|
|
14896
|
+
}
|
|
14897
|
+
/** `alpha` for `10.0.0-alpha.6`; undefined for a release. */
|
|
14898
|
+
function prereleaseTag(version$1) {
|
|
14899
|
+
const id = parse$2(version$1)?.ids[0];
|
|
14900
|
+
return typeof id === "string" ? id : void 0;
|
|
14901
|
+
}
|
|
14902
|
+
/**
|
|
14903
|
+
* A range that is one operator and one version — `~1.2.3`, `^1.2.3`, `>=1.2.3`,
|
|
14904
|
+
* `1.2.3` — split into the two. Anything else (`>=8 <10`, `a || b`) is not
|
|
14905
|
+
* something to rewrite mechanically.
|
|
14906
|
+
*/
|
|
14907
|
+
function simpleRange(range) {
|
|
14908
|
+
const match = range.trim().match(/^(\^|~|>=|)v?(\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?)$/);
|
|
14909
|
+
return match ? {
|
|
14910
|
+
operator: match[1],
|
|
14911
|
+
version: match[2]
|
|
14912
|
+
} : void 0;
|
|
14913
|
+
}
|
|
13661
14914
|
|
|
13662
14915
|
//#endregion
|
|
13663
14916
|
//#region src/upgrade/index.ts
|
|
13664
14917
|
const logger = console;
|
|
14918
|
+
const SCOPE = "@geekmidas/";
|
|
14919
|
+
const DEP_FIELDS = [
|
|
14920
|
+
"dependencies",
|
|
14921
|
+
"devDependencies",
|
|
14922
|
+
"peerDependencies",
|
|
14923
|
+
"optionalDependencies"
|
|
14924
|
+
];
|
|
14925
|
+
async function packument(name$1) {
|
|
14926
|
+
const res = await fetch(`https://registry.npmjs.org/${name$1.replace("/", "%2F")}`, { headers: { accept: "application/vnd.npm.install-v1+json" } });
|
|
14927
|
+
if (!res.ok) throw new RegistryUnavailable(name$1, res.status);
|
|
14928
|
+
return await res.json();
|
|
14929
|
+
}
|
|
14930
|
+
/** Every registry range in the workspace's package.json files. */
|
|
14931
|
+
function packageOccurrences(paths) {
|
|
14932
|
+
const found = [];
|
|
14933
|
+
for (const file of paths) {
|
|
14934
|
+
const pkg$1 = JSON.parse(readFileSync(file, "utf-8"));
|
|
14935
|
+
for (const field of DEP_FIELDS) for (const [name$1, range] of Object.entries(pkg$1[field] ?? {})) found.push({
|
|
14936
|
+
name: name$1,
|
|
14937
|
+
range,
|
|
14938
|
+
file,
|
|
14939
|
+
field
|
|
14940
|
+
});
|
|
14941
|
+
}
|
|
14942
|
+
return found;
|
|
14943
|
+
}
|
|
14944
|
+
/** pnpm catalog entries, which is where `catalog:` references resolve. */
|
|
14945
|
+
function catalogOccurrences(root) {
|
|
14946
|
+
const file = join(root, "pnpm-workspace.yaml");
|
|
14947
|
+
if (!existsSync(file)) return [];
|
|
14948
|
+
const doc = parseDocument(readFileSync(file, "utf-8")).toJS() ?? {};
|
|
14949
|
+
const found = [];
|
|
14950
|
+
for (const [name$1, range] of Object.entries(doc.catalog ?? {})) found.push({
|
|
14951
|
+
name: name$1,
|
|
14952
|
+
range: String(range),
|
|
14953
|
+
file,
|
|
14954
|
+
field: "catalog"
|
|
14955
|
+
});
|
|
14956
|
+
for (const [catalog, entries] of Object.entries(doc.catalogs ?? {})) for (const [name$1, range] of Object.entries(entries ?? {})) found.push({
|
|
14957
|
+
name: name$1,
|
|
14958
|
+
range: String(range),
|
|
14959
|
+
file,
|
|
14960
|
+
field: "catalog",
|
|
14961
|
+
catalog
|
|
14962
|
+
});
|
|
14963
|
+
return found;
|
|
14964
|
+
}
|
|
14965
|
+
/** Not a registry range: resolved elsewhere, so not ours to move. */
|
|
14966
|
+
const isIndirect = (range) => /^(workspace|catalog|link|file|npm|git|github|https?):/.test(range);
|
|
14967
|
+
/**
|
|
14968
|
+
* The version every `@geekmidas` package moves to.
|
|
14969
|
+
*
|
|
14970
|
+
* All of them share one version, so the CLI's line answers for the rest. The
|
|
14971
|
+
* tag is the one the project is already on — a project on `10.0.0-alpha.6`
|
|
14972
|
+
* follows `alpha` — because `latest` names a different major while a new one
|
|
14973
|
+
* is in prerelease, and following it would move the project backwards.
|
|
14974
|
+
*/
|
|
14975
|
+
function resolveTarget(distTags, installed, tag) {
|
|
14976
|
+
const highest = [...installed].sort(compareVersions).at(-1);
|
|
14977
|
+
const chosen = tag ?? (highest && prereleaseTag(highest)) ?? "latest";
|
|
14978
|
+
const version$1 = distTags[chosen];
|
|
14979
|
+
if (!version$1) throw new NoReleaseOnTag(chosen, Object.keys(distTags));
|
|
14980
|
+
return {
|
|
14981
|
+
tag: chosen,
|
|
14982
|
+
version: version$1
|
|
14983
|
+
};
|
|
14984
|
+
}
|
|
14985
|
+
/** The same operator on the new version — `^`, `~`, `>=` or none, as written. */
|
|
14986
|
+
function withVersion(range, version$1) {
|
|
14987
|
+
return `${simpleRange(range)?.operator ?? ""}${version$1}`;
|
|
14988
|
+
}
|
|
13665
14989
|
async function upgradeCommand(options = {}) {
|
|
13666
14990
|
const cwd = process.cwd();
|
|
13667
|
-
logger.log("\n📦 Scanning workspace for @geekmidas packages...\n");
|
|
13668
14991
|
const pm = detectPackageManager(cwd);
|
|
14992
|
+
const root = findWorkspaceRoot(cwd, pm);
|
|
14993
|
+
logger.log("\n📦 Scanning workspace for @geekmidas packages...\n");
|
|
13669
14994
|
logger.log(` Package manager: ${pm}`);
|
|
13670
|
-
const
|
|
13671
|
-
|
|
13672
|
-
|
|
13673
|
-
|
|
14995
|
+
const packageJsons = findWorkspacePackages(cwd, pm);
|
|
14996
|
+
const occurrences = [...packageOccurrences(packageJsons), ...catalogOccurrences(root)];
|
|
14997
|
+
logger.log(` Found ${packageJsons.length} package(s) in workspace\n`);
|
|
14998
|
+
const ours = occurrences.filter((o) => o.name.startsWith(SCOPE) && !isIndirect(o.range));
|
|
14999
|
+
if (ours.length === 0) {
|
|
13674
15000
|
logger.log(" No @geekmidas packages found.\n");
|
|
13675
15001
|
return;
|
|
13676
15002
|
}
|
|
13677
|
-
const
|
|
13678
|
-
|
|
13679
|
-
const
|
|
13680
|
-
|
|
13681
|
-
|
|
13682
|
-
|
|
13683
|
-
|
|
13684
|
-
|
|
15003
|
+
const installed = ours.map((o) => simpleRange(o.range)?.version).filter((v) => Boolean(v));
|
|
15004
|
+
const cli = await packument(`${SCOPE}cli`);
|
|
15005
|
+
const target = resolveTarget(cli["dist-tags"], installed, options.tag);
|
|
15006
|
+
logger.log(` Target: ${target.version} (npm "${target.tag}")\n`);
|
|
15007
|
+
const highest = installed.sort(compareVersions).at(-1);
|
|
15008
|
+
if (compareVersions(target.version, highest) < 0) throw new WouldDowngrade(highest, target.tag, target.version);
|
|
15009
|
+
const moving = options.all ? ours : ours.filter((o) => o.name === `${SCOPE}cli`);
|
|
15010
|
+
const changes = [];
|
|
15011
|
+
for (const o of moving) {
|
|
15012
|
+
if (!simpleRange(o.range)) {
|
|
15013
|
+
logger.log(` ⚠ ${o.name} ${o.range} is not a simple range; left as is (target ${target.version}).`);
|
|
15014
|
+
continue;
|
|
15015
|
+
}
|
|
15016
|
+
const next = withVersion(o.range, target.version);
|
|
15017
|
+
if (next !== o.range) changes.push({
|
|
15018
|
+
...o,
|
|
15019
|
+
next
|
|
15020
|
+
});
|
|
15021
|
+
}
|
|
15022
|
+
if (options.all) changes.push(...await peerChanges(occurrences, ours, target.version));
|
|
15023
|
+
if (changes.length === 0) {
|
|
15024
|
+
logger.log(" Everything is already on the target version.\n");
|
|
13685
15025
|
return;
|
|
13686
15026
|
}
|
|
13687
|
-
|
|
15027
|
+
printChanges(changes, root);
|
|
15028
|
+
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
15029
|
if (options.dryRun) {
|
|
13689
|
-
logger.log(" --dry-run:
|
|
13690
|
-
printUpgradeCommands(upgradable, pm);
|
|
15030
|
+
logger.log("\n --dry-run: no changes made.\n");
|
|
13691
15031
|
return;
|
|
13692
15032
|
}
|
|
13693
|
-
|
|
13694
|
-
|
|
15033
|
+
applyChanges(changes);
|
|
15034
|
+
install(pm, root);
|
|
15035
|
+
logger.log("\n ✅ Upgrade complete. Run your tests to verify.\n");
|
|
13695
15036
|
}
|
|
13696
|
-
|
|
13697
|
-
|
|
13698
|
-
|
|
13699
|
-
|
|
13700
|
-
|
|
13701
|
-
|
|
13702
|
-
|
|
13703
|
-
|
|
13704
|
-
|
|
13705
|
-
|
|
13706
|
-
|
|
13707
|
-
|
|
13708
|
-
|
|
13709
|
-
|
|
13710
|
-
|
|
13711
|
-
|
|
13712
|
-
|
|
13713
|
-
|
|
13714
|
-
|
|
13715
|
-
|
|
13716
|
-
|
|
13717
|
-
|
|
13718
|
-
|
|
15037
|
+
/**
|
|
15038
|
+
* Third-party packages the project already lists, raised to the floor of the
|
|
15039
|
+
* peer range the target `@geekmidas` versions declare.
|
|
15040
|
+
*
|
|
15041
|
+
* Only raised, never lowered, and only where both the peer range and the
|
|
15042
|
+
* project's range are one operator and one version; anything else is reported
|
|
15043
|
+
* for a person to decide.
|
|
15044
|
+
*/
|
|
15045
|
+
async function peerChanges(occurrences, ours, version$1) {
|
|
15046
|
+
const floors = /* @__PURE__ */ new Map();
|
|
15047
|
+
const names = [...new Set(ours.map((o) => o.name))];
|
|
15048
|
+
const docs = await Promise.all(names.map(packument));
|
|
15049
|
+
for (const doc of docs) {
|
|
15050
|
+
const peers = doc.versions[version$1]?.peerDependencies ?? {};
|
|
15051
|
+
for (const [peer, range] of Object.entries(peers)) {
|
|
15052
|
+
if (peer.startsWith(SCOPE)) continue;
|
|
15053
|
+
const floor = simpleRange(range)?.version;
|
|
15054
|
+
if (!floor) continue;
|
|
15055
|
+
const known = floors.get(peer);
|
|
15056
|
+
if (!known || compareVersions(floor, known) > 0) floors.set(peer, floor);
|
|
15057
|
+
}
|
|
15058
|
+
}
|
|
15059
|
+
const changes = [];
|
|
15060
|
+
for (const o of occurrences) {
|
|
15061
|
+
const floor = floors.get(o.name);
|
|
15062
|
+
if (!floor || isIndirect(o.range) || o.field === "peerDependencies") continue;
|
|
15063
|
+
const current = simpleRange(o.range);
|
|
15064
|
+
if (!current) {
|
|
15065
|
+
logger.log(` ⚠ ${o.name} ${o.range} is not a simple range; @geekmidas needs at least ${floor}.`);
|
|
15066
|
+
continue;
|
|
13719
15067
|
}
|
|
15068
|
+
if (compareVersions(current.version, floor) < 0) changes.push({
|
|
15069
|
+
...o,
|
|
15070
|
+
next: withVersion(o.range, floor)
|
|
15071
|
+
});
|
|
13720
15072
|
}
|
|
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}`;
|
|
15073
|
+
return changes;
|
|
15074
|
+
}
|
|
15075
|
+
function printChanges(changes, root) {
|
|
15076
|
+
logger.log(" Changes:\n");
|
|
15077
|
+
for (const c of changes) {
|
|
15078
|
+
const where = c.field === "catalog" ? `pnpm-workspace.yaml (${c.catalog ? `catalogs.${c.catalog}` : "catalog"})` : `${relative(root, c.file) || "package.json"} (${c.field})`;
|
|
15079
|
+
logger.log(` ${c.name.padEnd(34)} ${c.range} → ${c.next} ${where}`);
|
|
15080
|
+
}
|
|
15081
|
+
}
|
|
15082
|
+
function applyChanges(changes) {
|
|
15083
|
+
const byFile = /* @__PURE__ */ new Map();
|
|
15084
|
+
for (const c of changes) byFile.set(c.file, [...byFile.get(c.file) ?? [], c]);
|
|
15085
|
+
for (const [file, fileChanges] of byFile) {
|
|
15086
|
+
const text = readFileSync(file, "utf-8");
|
|
15087
|
+
if (file.endsWith(".yaml")) {
|
|
15088
|
+
const doc = parseDocument(text);
|
|
15089
|
+
for (const c of fileChanges) {
|
|
15090
|
+
const path = c.catalog ? [
|
|
15091
|
+
"catalogs",
|
|
15092
|
+
c.catalog,
|
|
15093
|
+
c.name
|
|
15094
|
+
] : ["catalog", c.name];
|
|
15095
|
+
doc.setIn(path, c.next);
|
|
15096
|
+
}
|
|
15097
|
+
writeFileSync(file, doc.toString());
|
|
15098
|
+
continue;
|
|
15099
|
+
}
|
|
15100
|
+
const pkg$1 = JSON.parse(text);
|
|
15101
|
+
for (const c of fileChanges) pkg$1[c.field][c.name] = c.next;
|
|
15102
|
+
const indent = text.match(/^[ \t]+(?=")/m)?.[0] ?? " ";
|
|
15103
|
+
writeFileSync(file, `${JSON.stringify(pkg$1, null, indent)}\n`);
|
|
13785
15104
|
}
|
|
13786
15105
|
}
|
|
13787
|
-
function
|
|
13788
|
-
const
|
|
13789
|
-
|
|
13790
|
-
logger.log(` Running: ${cmd}\n`);
|
|
15106
|
+
function install(pm, cwd) {
|
|
15107
|
+
const command = getInstallCommand(pm);
|
|
15108
|
+
logger.log(`\n Running: ${command}\n`);
|
|
13791
15109
|
try {
|
|
13792
|
-
execSync(
|
|
15110
|
+
execSync(command, {
|
|
13793
15111
|
cwd,
|
|
13794
15112
|
stdio: "inherit",
|
|
13795
|
-
timeout:
|
|
15113
|
+
timeout: 3e5
|
|
13796
15114
|
});
|
|
13797
15115
|
} catch {
|
|
13798
|
-
throw new
|
|
15116
|
+
throw new InstallFailed(command);
|
|
13799
15117
|
}
|
|
13800
15118
|
}
|
|
15119
|
+
/** npm did not answer for a package, so there is no version to compare. */
|
|
15120
|
+
var RegistryUnavailable = class extends Error {
|
|
15121
|
+
constructor(packageName, status) {
|
|
15122
|
+
super(`npm answered ${status} for ${packageName}. Check the name and your network.`);
|
|
15123
|
+
this.packageName = packageName;
|
|
15124
|
+
this.status = status;
|
|
15125
|
+
this.name = "RegistryUnavailable";
|
|
15126
|
+
}
|
|
15127
|
+
};
|
|
15128
|
+
/** A dist-tag npm does not have, e.g. `--tag beta` before any beta. */
|
|
15129
|
+
var NoReleaseOnTag = class extends Error {
|
|
15130
|
+
constructor(tag, tags) {
|
|
15131
|
+
super(`npm has no "${tag}" release of @geekmidas/cli. Tags: ${tags.join(", ")}.`);
|
|
15132
|
+
this.tag = tag;
|
|
15133
|
+
this.tags = tags;
|
|
15134
|
+
this.name = "NoReleaseOnTag";
|
|
15135
|
+
}
|
|
15136
|
+
};
|
|
15137
|
+
/** The line asked for is behind what is installed; upgrade never goes back. */
|
|
15138
|
+
var WouldDowngrade = class extends Error {
|
|
15139
|
+
constructor(installed, tag, target) {
|
|
15140
|
+
super(`The project is already on ${installed}, ahead of npm's "${tag}" (${target}). Pass --tag to follow another line.`);
|
|
15141
|
+
this.installed = installed;
|
|
15142
|
+
this.tag = tag;
|
|
15143
|
+
this.target = target;
|
|
15144
|
+
this.name = "WouldDowngrade";
|
|
15145
|
+
}
|
|
15146
|
+
};
|
|
15147
|
+
/** The install after rewriting versions failed; its own output says why. */
|
|
15148
|
+
var InstallFailed = class extends Error {
|
|
15149
|
+
constructor(command) {
|
|
15150
|
+
super(`\`${command}\` failed. Check the output above for details.`);
|
|
15151
|
+
this.command = command;
|
|
15152
|
+
this.name = "InstallFailed";
|
|
15153
|
+
}
|
|
15154
|
+
};
|
|
13801
15155
|
|
|
13802
15156
|
//#endregion
|
|
13803
15157
|
//#region src/index.ts
|
|
@@ -13806,7 +15160,7 @@ program.name("gkm").description("GeekMidas backend framework CLI").version(packa
|
|
|
13806
15160
|
const opts = program.opts();
|
|
13807
15161
|
if (opts.debug) enableDebug();
|
|
13808
15162
|
});
|
|
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) => {
|
|
15163
|
+
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
15164
|
try {
|
|
13811
15165
|
const globalOptions = program.opts();
|
|
13812
15166
|
if (globalOptions.cwd) process.chdir(globalOptions.cwd);
|
|
@@ -13826,7 +15180,7 @@ program.command("trust").description("Trust the local edge's certificate authori
|
|
|
13826
15180
|
process.exit(1);
|
|
13827
15181
|
}
|
|
13828
15182
|
});
|
|
13829
|
-
program.command("setup").description("Reconcile declared constructs — containers, databases, buckets, secrets").option("--stage <stage>", "Stage name
|
|
15183
|
+
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
15184
|
try {
|
|
13831
15185
|
const globalOptions = program.opts();
|
|
13832
15186
|
if (globalOptions.cwd) process.chdir(globalOptions.cwd);
|
|
@@ -13898,7 +15252,7 @@ program.command("exec").description("Run a command with secrets injected into Cr
|
|
|
13898
15252
|
process.exit(1);
|
|
13899
15253
|
}
|
|
13900
15254
|
});
|
|
13901
|
-
program.command("test").description("Run tests with secrets loaded from environment").option("--stage <stage>", "Stage to load secrets from
|
|
15255
|
+
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
15256
|
try {
|
|
13903
15257
|
const globalOptions = program.opts();
|
|
13904
15258
|
if (globalOptions.cwd) process.chdir(globalOptions.cwd);
|
|
@@ -14040,13 +15394,13 @@ program.command("secrets:push").description("Push secrets to remote provider (SS
|
|
|
14040
15394
|
const globalOptions = program.opts();
|
|
14041
15395
|
if (globalOptions.cwd) process.chdir(globalOptions.cwd);
|
|
14042
15396
|
const { loadWorkspaceConfig: loadWorkspaceConfig$1 } = await import("./config.mjs");
|
|
14043
|
-
const { pushSecrets } = await import("./sync-
|
|
14044
|
-
const { reconcileMissingSecrets } = await import("./reconcile-
|
|
14045
|
-
const { readStageSecrets: readStageSecrets$1, writeStageSecrets: writeStageSecrets$1 } = await import("./storage-
|
|
15397
|
+
const { pushSecrets } = await import("./sync-HAau6h_R.mjs");
|
|
15398
|
+
const { reconcileMissingSecrets } = await import("./reconcile-C13ERt1U.mjs");
|
|
15399
|
+
const { readStageSecrets: readStageSecrets$1, writeStageSecrets: writeStageSecrets$1 } = await import("./storage-DGBzMLZa.mjs");
|
|
14046
15400
|
const { workspace } = await loadWorkspaceConfig$1();
|
|
14047
15401
|
const secrets = await readStageSecrets$1(options.stage, workspace.root);
|
|
14048
15402
|
if (secrets) {
|
|
14049
|
-
const { derivedContainers: derivedContainers$1 } = await import("./workspace-
|
|
15403
|
+
const { derivedContainers: derivedContainers$1 } = await import("./workspace-DbI580qS.mjs");
|
|
14050
15404
|
const result = reconcileMissingSecrets(secrets, workspace, await derivedContainers$1(workspace, options.stage));
|
|
14051
15405
|
if (result) {
|
|
14052
15406
|
await writeStageSecrets$1(result.secrets, workspace.root);
|
|
@@ -14066,16 +15420,16 @@ program.command("secrets:pull").description("Pull secrets from remote provider (
|
|
|
14066
15420
|
const globalOptions = program.opts();
|
|
14067
15421
|
if (globalOptions.cwd) process.chdir(globalOptions.cwd);
|
|
14068
15422
|
const { loadWorkspaceConfig: loadWorkspaceConfig$1 } = await import("./config.mjs");
|
|
14069
|
-
const { pullSecrets } = await import("./sync-
|
|
14070
|
-
const { writeStageSecrets: writeStageSecrets$1 } = await import("./storage-
|
|
14071
|
-
const { reconcileMissingSecrets } = await import("./reconcile-
|
|
15423
|
+
const { pullSecrets } = await import("./sync-HAau6h_R.mjs");
|
|
15424
|
+
const { writeStageSecrets: writeStageSecrets$1 } = await import("./storage-DGBzMLZa.mjs");
|
|
15425
|
+
const { reconcileMissingSecrets } = await import("./reconcile-C13ERt1U.mjs");
|
|
14072
15426
|
const { workspace } = await loadWorkspaceConfig$1();
|
|
14073
15427
|
let secrets = await pullSecrets(options.stage, workspace);
|
|
14074
15428
|
if (!secrets) {
|
|
14075
15429
|
console.error(`No remote secrets found for stage "${options.stage}".`);
|
|
14076
15430
|
process.exit(1);
|
|
14077
15431
|
}
|
|
14078
|
-
const { derivedContainers: derivedContainers$1 } = await import("./workspace-
|
|
15432
|
+
const { derivedContainers: derivedContainers$1 } = await import("./workspace-DbI580qS.mjs");
|
|
14079
15433
|
const result = reconcileMissingSecrets(secrets, workspace, await derivedContainers$1(workspace, options.stage));
|
|
14080
15434
|
if (result) {
|
|
14081
15435
|
secrets = result.secrets;
|
|
@@ -14089,27 +15443,28 @@ program.command("secrets:pull").description("Pull secrets from remote provider (
|
|
|
14089
15443
|
process.exit(1);
|
|
14090
15444
|
}
|
|
14091
15445
|
});
|
|
14092
|
-
program.command("secrets:reconcile").description("Backfill missing custom secrets from workspace config").option("--stage <stage>", "Stage name
|
|
15446
|
+
program.command("secrets:reconcile").description("Backfill missing custom secrets from workspace config").option("--stage <stage>", "Stage name (default: stages.local)").action(async (options) => {
|
|
14093
15447
|
try {
|
|
14094
15448
|
const globalOptions = program.opts();
|
|
14095
15449
|
if (globalOptions.cwd) process.chdir(globalOptions.cwd);
|
|
14096
15450
|
const { loadWorkspaceConfig: loadWorkspaceConfig$1 } = await import("./config.mjs");
|
|
14097
|
-
const { reconcileMissingSecrets } = await import("./reconcile-
|
|
14098
|
-
const { readStageSecrets: readStageSecrets$1, writeStageSecrets: writeStageSecrets$1 } = await import("./storage-
|
|
15451
|
+
const { reconcileMissingSecrets } = await import("./reconcile-C13ERt1U.mjs");
|
|
15452
|
+
const { readStageSecrets: readStageSecrets$1, writeStageSecrets: writeStageSecrets$1 } = await import("./storage-DGBzMLZa.mjs");
|
|
14099
15453
|
const { workspace } = await loadWorkspaceConfig$1();
|
|
14100
|
-
const
|
|
15454
|
+
const stage = options.stage ?? workspace.stages.local;
|
|
15455
|
+
const secrets = await readStageSecrets$1(stage, workspace.root);
|
|
14101
15456
|
if (!secrets) {
|
|
14102
|
-
console.error(`No secrets found for stage "${
|
|
15457
|
+
console.error(`No secrets found for stage "${stage}". Run "gkm secrets:init --stage ${stage}" first.`);
|
|
14103
15458
|
process.exit(1);
|
|
14104
15459
|
}
|
|
14105
|
-
const { derivedContainers: derivedContainers$1 } = await import("./workspace-
|
|
14106
|
-
const result = reconcileMissingSecrets(secrets, workspace, await derivedContainers$1(workspace,
|
|
15460
|
+
const { derivedContainers: derivedContainers$1 } = await import("./workspace-DbI580qS.mjs");
|
|
15461
|
+
const result = reconcileMissingSecrets(secrets, workspace, await derivedContainers$1(workspace, stage));
|
|
14107
15462
|
if (!result) {
|
|
14108
|
-
console.log(`\n✓ Secrets for stage "${
|
|
15463
|
+
console.log(`\n✓ Secrets for stage "${stage}" are up-to-date`);
|
|
14109
15464
|
return;
|
|
14110
15465
|
}
|
|
14111
15466
|
await writeStageSecrets$1(result.secrets, workspace.root);
|
|
14112
|
-
console.log(`\n✓ Reconciled ${result.addedKeys.length} missing secret(s) for stage "${
|
|
15467
|
+
console.log(`\n✓ Reconciled ${result.addedKeys.length} missing secret(s) for stage "${stage}":`);
|
|
14113
15468
|
for (const key of result.addedKeys) console.log(` + ${key}`);
|
|
14114
15469
|
} catch (error) {
|
|
14115
15470
|
console.error(formatError(error));
|
|
@@ -14157,6 +15512,17 @@ program.command("deploy:init").description("Initialize Dokploy deployment (creat
|
|
|
14157
15512
|
process.exit(1);
|
|
14158
15513
|
}
|
|
14159
15514
|
});
|
|
15515
|
+
program.command("deploy:github").description("Let GitHub Actions deploy a stage: OIDC role in the stage's AWS account, and the GitHub environment the deploy workflow uses").requiredOption("--stage <stage>", "A deployed stage from gkm.config.ts").option("--profile <profile>", "AWS profile for the stage's account").option("--repo <owner/name>", "GitHub repository (default: this one)").option("--policy-arn <arn>", "Policy for the deploy role (default: AdministratorAccess)").option("--dry-run", "Show the plan without changing AWS or GitHub").action(async (options) => {
|
|
15516
|
+
try {
|
|
15517
|
+
const globalOptions = program.opts();
|
|
15518
|
+
if (globalOptions.cwd) process.chdir(globalOptions.cwd);
|
|
15519
|
+
const { deployGithubCommand } = await import("./github-CrpsXSfV.mjs");
|
|
15520
|
+
await deployGithubCommand(options);
|
|
15521
|
+
} catch (error) {
|
|
15522
|
+
console.error(formatError(error));
|
|
15523
|
+
process.exit(1);
|
|
15524
|
+
}
|
|
15525
|
+
});
|
|
14160
15526
|
program.command("deploy:list").description("List Dokploy resources (projects, registries)").option("--endpoint <url>", "Dokploy server URL (uses stored credentials if logged in)").option("--projects", "List projects").option("--registries", "List registries").action(async (options) => {
|
|
14161
15527
|
try {
|
|
14162
15528
|
const globalOptions = program.opts();
|
|
@@ -14263,7 +15629,7 @@ program.command("state:diff").description("Compare local and remote deployment s
|
|
|
14263
15629
|
process.exit(1);
|
|
14264
15630
|
}
|
|
14265
15631
|
});
|
|
14266
|
-
program.command("upgrade").description("Upgrade all @geekmidas
|
|
15632
|
+
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
15633
|
try {
|
|
14268
15634
|
const globalOptions = program.opts();
|
|
14269
15635
|
if (globalOptions.cwd) process.chdir(globalOptions.cwd);
|