@geekmidas/cli 10.0.0-alpha.6 → 10.0.0-alpha.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +12 -8
- package/bin/adjacent-tsconfig.mjs +59 -0
- package/bin/gkm.mjs +7 -1
- package/dist/{config-CNz5KaSR.cjs → config-BZ1F0WuA.cjs} +6 -50
- package/dist/config-BZ1F0WuA.cjs.map +1 -0
- package/dist/{config-CxP7P-qG.mjs → config-DVVph9yz.mjs} +7 -45
- package/dist/config-DVVph9yz.mjs.map +1 -0
- package/dist/config.cjs +2 -2
- package/dist/config.d.cts +2 -2
- package/dist/config.d.mts +2 -2
- package/dist/config.mjs +2 -2
- package/dist/{credentials-DzsXZzkx.cjs → credentials-Xamqe_NJ.cjs} +19 -18
- package/dist/credentials-Xamqe_NJ.cjs.map +1 -0
- package/dist/{credentials-BK3Z93MX.mjs → credentials-dP8vRVRv.mjs} +19 -18
- package/dist/credentials-dP8vRVRv.mjs.map +1 -0
- package/dist/discover-CAlFgYec.mjs.map +1 -1
- package/dist/discover-DTmFeloL.cjs.map +1 -1
- package/dist/{index-BGGdjueF.d.cts → index-C6e-ULLx.d.mts} +2 -2
- package/dist/index-C6e-ULLx.d.mts.map +1 -0
- package/dist/{index-DEbaleny.d.mts → index-CR5urhWX.d.cts} +2 -2
- package/dist/index-CR5urhWX.d.cts.map +1 -0
- package/dist/index.cjs +596 -248
- package/dist/index.cjs.map +1 -1
- package/dist/index.mjs +596 -248
- package/dist/index.mjs.map +1 -1
- package/dist/{openapi-D-dx9Akj.cjs → openapi-SjsgARie.cjs} +4 -4
- package/dist/{openapi-D-dx9Akj.cjs.map → openapi-SjsgARie.cjs.map} +1 -1
- package/dist/{openapi-Bw8DWFWz.mjs → openapi-Xx0A7Wg0.mjs} +4 -4
- package/dist/{openapi-Bw8DWFWz.mjs.map → openapi-Xx0A7Wg0.mjs.map} +1 -1
- package/dist/openapi.cjs +3 -3
- package/dist/openapi.d.cts +3 -3
- package/dist/openapi.d.mts +3 -3
- package/dist/openapi.mjs +3 -3
- package/dist/reconcile/public.d.cts +1 -1
- package/dist/reconcile/public.d.mts +1 -1
- package/dist/{setup-DGIPd2fh.mjs → setup-BA8ciTPU.mjs} +4 -4
- package/dist/setup-BsZvZP6f.cjs +11 -0
- package/dist/{setup-BWMhbW-p.mjs → setup-C8KOx8_8.mjs} +5 -5
- package/dist/setup-C8KOx8_8.mjs.map +1 -0
- package/dist/{setup-D7L4BgBT.cjs → setup-hC6hI86f.cjs} +5 -5
- package/dist/setup-hC6hI86f.cjs.map +1 -0
- package/dist/{types-BLSNdrk0.d.cts → types-6pkp2RRj.d.mts} +48 -4
- package/dist/types-6pkp2RRj.d.mts.map +1 -0
- package/dist/{types-C3POXhw3.d.mts → types-Y3fKQSAK.d.cts} +45 -1
- package/dist/types-Y3fKQSAK.d.cts.map +1 -0
- package/dist/workspace/index.cjs +1 -1
- package/dist/workspace/index.d.cts +2 -2
- package/dist/workspace/index.d.mts +2 -2
- package/dist/workspace/index.mjs +1 -1
- package/dist/workspace-BzrG_2SI.mjs +8 -0
- package/dist/{workspace-LKiNyaDy.cjs → workspace-C6VJQVtG.cjs} +79 -1
- package/dist/workspace-C6VJQVtG.cjs.map +1 -0
- package/dist/{workspace-CGaUAw5A.cjs → workspace-XXtNhqJl.cjs} +3 -3
- package/dist/{workspace-DIuZx-cc.mjs → workspace-zoqRbihZ.mjs} +62 -2
- package/dist/workspace-zoqRbihZ.mjs.map +1 -0
- package/package.json +14 -13
- package/dist/config-CNz5KaSR.cjs.map +0 -1
- package/dist/config-CxP7P-qG.mjs.map +0 -1
- package/dist/credentials-BK3Z93MX.mjs.map +0 -1
- package/dist/credentials-DzsXZzkx.cjs.map +0 -1
- package/dist/index-BGGdjueF.d.cts.map +0 -1
- package/dist/index-DEbaleny.d.mts.map +0 -1
- package/dist/setup-8IWPsRIN.cjs +0 -11
- package/dist/setup-BWMhbW-p.mjs.map +0 -1
- package/dist/setup-D7L4BgBT.cjs.map +0 -1
- package/dist/types-BLSNdrk0.d.cts.map +0 -1
- package/dist/types-C3POXhw3.d.mts.map +0 -1
- package/dist/workspace-CnTqIlB3.mjs +0 -8
- package/dist/workspace-DIuZx-cc.mjs.map +0 -1
- package/dist/workspace-LKiNyaDy.cjs.map +0 -1
package/README.md
CHANGED
|
@@ -54,6 +54,7 @@ Create a `gkm.config.ts` file in your project root:
|
|
|
54
54
|
import { defineConfig } from '@geekmidas/cli/config';
|
|
55
55
|
|
|
56
56
|
export default defineConfig({
|
|
57
|
+
stages: { local: 'dev', deployed: ['prod'] },
|
|
57
58
|
// Glob pattern to find endpoint files
|
|
58
59
|
routes: 'src/routes/**/*.ts',
|
|
59
60
|
|
|
@@ -490,6 +491,7 @@ Configure Docker settings in `gkm.config.ts`:
|
|
|
490
491
|
import { defineConfig } from '@geekmidas/cli/config';
|
|
491
492
|
|
|
492
493
|
export default defineConfig({
|
|
494
|
+
stages: { local: 'dev', deployed: ['prod'] },
|
|
493
495
|
routes: 'src/routes/**/*.ts',
|
|
494
496
|
envParser: './src/env.ts',
|
|
495
497
|
logger: './src/logger.ts',
|
|
@@ -929,6 +931,7 @@ This allows the CLI to validate that all required secrets are configured before
|
|
|
929
931
|
```typescript
|
|
930
932
|
// gkm.config.ts
|
|
931
933
|
export default defineConfig({
|
|
934
|
+
stages: { local: 'dev', deployed: ['prod'] },
|
|
932
935
|
routes: 'src/endpoints/**/*.ts',
|
|
933
936
|
envParser: './src/env.ts',
|
|
934
937
|
logger: './src/logger.ts',
|
|
@@ -1216,6 +1219,7 @@ import { defineWorkspace } from '@geekmidas/cli/config';
|
|
|
1216
1219
|
|
|
1217
1220
|
export default defineWorkspace({
|
|
1218
1221
|
name: 'my-project',
|
|
1222
|
+
stages: { local: 'dev', deployed: ['prod'] },
|
|
1219
1223
|
constructs: './constructs/**/*.ts',
|
|
1220
1224
|
shared: {
|
|
1221
1225
|
packages: ['packages/*'],
|
|
@@ -1239,7 +1243,7 @@ them out of the manifest; none of them is listed in config.
|
|
|
1239
1243
|
|
|
1240
1244
|
```typescript
|
|
1241
1245
|
// constructs/api.ts
|
|
1242
|
-
export const api = new RestApi('Api', { defaultAuthorizer: 'none', logger });
|
|
1246
|
+
export const api = new RestApi('Api', { path: 'apps/api', defaultAuthorizer: 'none', logger });
|
|
1243
1247
|
```
|
|
1244
1248
|
|
|
1245
1249
|
`gkm build --provider server` generates a Hono server from the endpoints built
|
|
@@ -1253,6 +1257,7 @@ An auth server mounts a wildcard, so there is nothing for a glob to find:
|
|
|
1253
1257
|
```typescript
|
|
1254
1258
|
// constructs/auth.ts
|
|
1255
1259
|
export const auth = new BetterAuth('Auth', {
|
|
1260
|
+
path: 'apps/auth',
|
|
1256
1261
|
database: authDb,
|
|
1257
1262
|
basePath: '/api/auth',
|
|
1258
1263
|
});
|
|
@@ -1271,8 +1276,8 @@ from being read by it.
|
|
|
1271
1276
|
|
|
1272
1277
|
```typescript
|
|
1273
1278
|
// constructs/site.ts
|
|
1274
|
-
export const web = new StaticSite('Web').dependsOn([api, auth]);
|
|
1275
|
-
export const admin = new StaticSite('Admin', { variant: 'next' }).dependsOn([api]);
|
|
1279
|
+
export const web = new StaticSite('Web', { path: 'apps/web' }).dependsOn([api, auth]);
|
|
1280
|
+
export const admin = new StaticSite('Admin', { path: 'apps/admin', variant: 'next' }).dependsOn([api]);
|
|
1276
1281
|
```
|
|
1277
1282
|
|
|
1278
1283
|
`.dependsOn()` is the single fact behind four things that are hand-maintained
|
|
@@ -1358,12 +1363,10 @@ interface WorkspaceConfig {
|
|
|
1358
1363
|
What an app is comes from the declaration, not from here:
|
|
1359
1364
|
|
|
1360
1365
|
```typescript
|
|
1361
|
-
/**
|
|
1366
|
+
/** How a site is built and run. A surface declares only its `path`. */
|
|
1362
1367
|
interface AppSpec {
|
|
1363
|
-
/**
|
|
1364
|
-
path
|
|
1365
|
-
/** Default: the conventional directories under `path`. */
|
|
1366
|
-
code?: string;
|
|
1368
|
+
/** Required: where the app lives, relative to the workspace root. */
|
|
1369
|
+
path: string;
|
|
1367
1370
|
/** Default: assigned in a stable order, so adding an app renumbers nothing. */
|
|
1368
1371
|
port?: number;
|
|
1369
1372
|
entry?: string;
|
|
@@ -1883,6 +1886,7 @@ Configure multiple patterns for complex project structures:
|
|
|
1883
1886
|
import { defineConfig } from '@geekmidas/cli/config';
|
|
1884
1887
|
|
|
1885
1888
|
export default defineConfig({
|
|
1889
|
+
stages: { local: 'dev', deployed: ['prod'] },
|
|
1886
1890
|
routes: [
|
|
1887
1891
|
'src/routes/**/*.ts',
|
|
1888
1892
|
'src/api/v1/**/*.ts',
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Resolve a module's path aliases through the tsconfig beside it.
|
|
3
|
+
*
|
|
4
|
+
* tsx applies one tsconfig to the whole process — the one in the directory the
|
|
5
|
+
* command ran from. The workspace's `constructs` glob loads every app's code
|
|
6
|
+
* from wherever a command started, so `~/router.ts` in `apps/api` was being
|
|
7
|
+
* read with `apps/web`'s `~` when the command ran there, and resolved to
|
|
8
|
+
* nothing. TypeScript itself resolves an import through the tsconfig that owns
|
|
9
|
+
* the importing file; this does the same at runtime.
|
|
10
|
+
*
|
|
11
|
+
* Registered after tsx, so it runs first. It only answers a non-relative
|
|
12
|
+
* specifier from TypeScript source outside `node_modules` that the adjacent
|
|
13
|
+
* tsconfig's `paths` actually map — everything else, and anything a mapped
|
|
14
|
+
* candidate fails to resolve to, goes to the next resolver untouched.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import { dirname } from 'node:path';
|
|
18
|
+
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
19
|
+
import { createPathsMatcher, getTsconfig } from 'get-tsconfig';
|
|
20
|
+
|
|
21
|
+
/** Directory → the paths matcher of the tsconfig that owns it, if any. */
|
|
22
|
+
const matchers = new Map();
|
|
23
|
+
/** Shared with `get-tsconfig`, so a config `extends` is read once. */
|
|
24
|
+
const tsconfigCache = new Map();
|
|
25
|
+
|
|
26
|
+
const TS_SOURCE = /\.(?:[cm]?ts|tsx)$/;
|
|
27
|
+
/** Relative, absolute, or carrying a scheme (`node:`, `file:`, `data:`). */
|
|
28
|
+
const NOT_BARE = /^(?:\.{1,2}(?:\/|$)|\/|[a-zA-Z][a-zA-Z\d+.-]*:)/;
|
|
29
|
+
|
|
30
|
+
function matcherFor(directory) {
|
|
31
|
+
if (!matchers.has(directory)) {
|
|
32
|
+
const tsconfig = getTsconfig(directory, 'tsconfig.json', tsconfigCache);
|
|
33
|
+
matchers.set(directory, tsconfig ? createPathsMatcher(tsconfig) : null);
|
|
34
|
+
}
|
|
35
|
+
return matchers.get(directory);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export async function resolve(specifier, context, nextResolve) {
|
|
39
|
+
const parent = context.parentURL;
|
|
40
|
+
if (!parent?.startsWith('file:') || NOT_BARE.test(specifier)) {
|
|
41
|
+
return nextResolve(specifier, context);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const parentPath = fileURLToPath(parent);
|
|
45
|
+
if (!TS_SOURCE.test(parentPath) || parentPath.includes('/node_modules/')) {
|
|
46
|
+
return nextResolve(specifier, context);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const match = matcherFor(dirname(parentPath));
|
|
50
|
+
for (const candidate of match?.(specifier) ?? []) {
|
|
51
|
+
try {
|
|
52
|
+
return await nextResolve(pathToFileURL(candidate).href, context);
|
|
53
|
+
} catch {
|
|
54
|
+
// The next mapping, as TypeScript tries them in order.
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
return nextResolve(specifier, context);
|
|
59
|
+
}
|
package/bin/gkm.mjs
CHANGED
|
@@ -35,5 +35,11 @@ if (
|
|
|
35
35
|
process.exit(0);
|
|
36
36
|
}
|
|
37
37
|
|
|
38
|
-
// tsx is loaded
|
|
38
|
+
// tsx is loaded. Resolve each module's path aliases through its own tsconfig
|
|
39
|
+
// rather than the one tsx took from the cwd — registered after tsx, so it is
|
|
40
|
+
// asked first. See `adjacent-tsconfig.mjs`.
|
|
41
|
+
const { register } = await import('node:module');
|
|
42
|
+
register('./adjacent-tsconfig.mjs', import.meta.url);
|
|
43
|
+
|
|
44
|
+
// Run the CLI
|
|
39
45
|
await import('../dist/index.mjs');
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
const require_chunk = require('./chunk-CUT6urMc.cjs');
|
|
2
2
|
const require_discover = require('./discover-DTmFeloL.cjs');
|
|
3
|
-
const require_workspace = require('./workspace-
|
|
3
|
+
const require_workspace = require('./workspace-C6VJQVtG.cjs');
|
|
4
4
|
const node_fs = require_chunk.__toESM(require("node:fs"));
|
|
5
5
|
const node_path = require_chunk.__toESM(require("node:path"));
|
|
6
6
|
const __geekmidas_manifest = require_chunk.__toESM(require("@geekmidas/manifest"));
|
|
@@ -77,53 +77,15 @@ function markRoot(apps) {
|
|
|
77
77
|
const chosen = sites.find(([name]) => name === "web") ?? (sites.length === 1 ? sites[0] : void 0);
|
|
78
78
|
if (chosen) chosen[1].root = true;
|
|
79
79
|
}
|
|
80
|
-
/**
|
|
81
|
-
* What `app: true` means, and what the object form leaves out.
|
|
82
|
-
*
|
|
83
|
-
* Both fields follow from the construct's own id, so neither was worth making
|
|
84
|
-
* someone write down:
|
|
85
|
-
*
|
|
86
|
-
* - `path` is `apps/<kebab-id>` where that directory exists, and the workspace
|
|
87
|
-
* root otherwise. `Api` means `apps/api` in a monorepo and `.` in a
|
|
88
|
-
* single-app project, and which of the two you are in is answerable by
|
|
89
|
-
* looking.
|
|
90
|
-
* - `code` is the conventional directories under `path`. A glob is worth
|
|
91
|
-
* writing only when the code is somewhere else, which is the case the field
|
|
92
|
-
* still exists for.
|
|
93
|
-
*
|
|
94
|
-
* A site takes no `code`: its build is its framework's, not ours.
|
|
95
|
-
*/
|
|
96
|
-
/**
|
|
97
|
-
* Where an app lives when it did not say.
|
|
98
|
-
*
|
|
99
|
-
* `apps/<kebab-id>`, which is what the id already said — or the project root
|
|
100
|
-
* when there is no such directory, because then there is one app and it is the
|
|
101
|
-
* project.
|
|
102
|
-
*
|
|
103
|
-
* This refused, briefly, when a workspace had an `apps/` directory with nothing
|
|
104
|
-
* under this name — on the grounds that answering `.` makes the app the whole
|
|
105
|
-
* repository. That was the wrong place for the guard twice over. It throws
|
|
106
|
-
* inside discovery, which is caught, so one misplaced app lost the whole
|
|
107
|
-
* manifest and every other app with it. And the hazard it guards — the build
|
|
108
|
-
* filtering turbo onto the root package, whose `build` is `gkm build` — is the
|
|
109
|
-
* build's question, asked where the answer is actionable.
|
|
110
|
-
*/
|
|
111
|
-
function conventionalPath(id, workspaceRoot) {
|
|
112
|
-
const conventional = (0, node_path.join)("apps", appKey(id));
|
|
113
|
-
return (0, node_fs.existsSync)((0, node_path.join)(workspaceRoot, conventional)) ? conventional : ".";
|
|
114
|
-
}
|
|
115
|
-
function resolveAppSpec(id, spec, workspaceRoot) {
|
|
116
|
-
return {
|
|
117
|
-
...spec,
|
|
118
|
-
path: spec.path ?? conventionalPath(id, workspaceRoot)
|
|
119
|
-
};
|
|
120
|
-
}
|
|
121
80
|
function derivedApps(manifest, workspace) {
|
|
122
81
|
const apps = {};
|
|
123
82
|
const defaultTarget = workspace.deploy?.default ?? "dokploy";
|
|
124
83
|
for (const [id, declaration] of Object.entries(manifest)) {
|
|
125
84
|
if (declaration.kind !== "site" && declaration.kind !== "rest-api") continue;
|
|
126
|
-
const spec =
|
|
85
|
+
const spec = declaration.kind === "site" ? declaration.app : {
|
|
86
|
+
path: declaration.path,
|
|
87
|
+
...declaration.telescope ? { telescope: true } : {}
|
|
88
|
+
};
|
|
127
89
|
const name = appKey(id);
|
|
128
90
|
const configured = workspace.apps[name];
|
|
129
91
|
apps[name] = {
|
|
@@ -497,10 +459,4 @@ Object.defineProperty(exports, 'parseModuleConfig', {
|
|
|
497
459
|
return parseModuleConfig;
|
|
498
460
|
}
|
|
499
461
|
});
|
|
500
|
-
|
|
501
|
-
enumerable: true,
|
|
502
|
-
get: function () {
|
|
503
|
-
return resolveAppSpec;
|
|
504
|
-
}
|
|
505
|
-
});
|
|
506
|
-
//# sourceMappingURL=config-CNz5KaSR.cjs.map
|
|
462
|
+
//# sourceMappingURL=config-BZ1F0WuA.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"config-BZ1F0WuA.cjs","names":["FRAMEWORKS: Record<string, Framework>","id: string","manifest: ConstructManifest","apps: Record<string, NormalizedAppConfig>","name: string","app: NormalizedAppConfig","workspace: NormalizedWorkspace","defaultTarget: DeployTarget","spec: AppSpec","config: GkmConfig","configString: string","defaultAlias: string","cwd: string","loaded: LoadedConfig","loadedConfig: LoadedConfig","appName: string"],"sources":["../src/workspace/derive.ts","../src/config.ts"],"sourcesContent":["/**\n * The apps a manifest implies.\n *\n * `gkm.config.ts` used to carry an `apps` block naming every app: its type, its\n * path, its port, its framework, which other apps it depended on. Every one of\n * those facts was already declared. A `StaticSite('Web', { path: 'apps/web' })`\n * and an `apps.web = { type: 'web', path: 'apps/web', framework: 'vite',\n * dependencies: ['api'] }` are the same statement written twice, and only one of\n * them was checked against anything — so the copy in config was free to drift,\n * and a surface config had no entry for simply never deployed.\n *\n * So the list comes from the graph. A `site` is an app, and so is every\n * `rest-api` — one surface, one container, at the `path` it declared.\n *\n * What config still supplies is what no graph can answer: the backends a cache\n * and a mailer resolve to, the deploy endpoint, the stage's domain.\n */\n\nimport {\n\ttype AppSpec,\n\ttype ConstructManifest,\n\tkebabCase,\n} from '@geekmidas/manifest';\nimport type {\n\tAppDomainConfig,\n\tDeployTarget,\n\tFramework,\n\tNormalizedAppConfig,\n\tNormalizedWorkspace,\n} from './types.js';\n\n/** Which frontend framework builds each site variant. */\nconst FRAMEWORKS: Record<string, Framework> = {\n\tstatic: 'vite',\n\tnext: 'nextjs',\n\ttanstack: 'tanstack-start',\n};\n\n/**\n * The app key a construct id becomes.\n *\n * The same kebab form every physical name is built from, so `Api` is `api` in\n * the config, `production-kitchen-sink-api` on Dokploy, and `API_URL` in an\n * environment — one id, three renderings, no second list.\n */\nexport function appKey(id: string): string {\n\treturn kebabCase(id);\n}\n\n/**\n * The app serving a surface: its own, always.\n *\n * Every surface gets a container. There is no arrangement in which one runs\n * inside another — two surfaces in a process share a filesystem, an\n * environment, and every credential either was granted, so an auth server\n * beside an API is one bug in the API away from being read by it.\n */\nexport function hostOf(\n\tmanifest: ConstructManifest,\n\tid: string,\n): string | undefined {\n\tconst declaration = manifest[id];\n\tif (!declaration || declaration.kind !== 'rest-api') return undefined;\n\n\treturn id;\n}\n\n/**\n * Local ports, assigned so that adding a site does not renumber the others.\n *\n * Backends first, then the site holding the base domain, then the rest\n * alphabetically. The ordering is arbitrary but it is *fixed*, which is the\n * property that matters: a port that moves when an unrelated app is added is a\n * port nobody can put in a bookmark. A construct that names its own port keeps\n * it, and is skipped here.\n */\nfunction assignPorts(apps: Record<string, NormalizedAppConfig>): void {\n\tconst rank = (\n\t\tname: string,\n\t\tapp: NormalizedAppConfig,\n\t): [number, number, string] => [\n\t\tapp.type === 'backend' ? 0 : app.type === 'web' ? 1 : 2,\n\t\tapp.root ? 0 : 1,\n\t\tname,\n\t];\n\n\tconst ordered = Object.entries(apps).sort(([aName, a], [bName, b]) => {\n\t\tconst [at, ar, an] = rank(aName, a);\n\t\tconst [bt, br, bn] = rank(bName, b);\n\t\treturn at - bt || ar - br || an.localeCompare(bn);\n\t});\n\n\tconst taken = new Set(\n\t\tordered.map(([, app]) => app.port).filter((p): p is number => !!p),\n\t);\n\n\tlet next = 3000;\n\tfor (const [, app] of ordered) {\n\t\tif (app.port) continue;\n\t\twhile (taken.has(next)) next += 1;\n\t\tapp.port = next;\n\t\ttaken.add(next);\n\t}\n}\n\n/**\n * Whichever site holds the base domain.\n *\n * The convention first — a site named `web` — because it is one people already\n * rely on. Then an explicit `root: true`. A project with one site needs neither.\n */\nfunction markRoot(apps: Record<string, NormalizedAppConfig>): void {\n\tconst sites = Object.entries(apps).filter(([, a]) => a.type === 'web');\n\tif (sites.length === 0) return;\n\tif (sites.some(([, a]) => a.root)) return;\n\n\tconst chosen =\n\t\tsites.find(([name]) => name === 'web') ??\n\t\t(sites.length === 1 ? sites[0] : undefined);\n\tif (chosen) chosen[1].root = true;\n}\n\nexport function derivedApps(\n\tmanifest: ConstructManifest,\n\tworkspace: NormalizedWorkspace,\n): Record<string, NormalizedAppConfig> {\n\tconst apps: Record<string, NormalizedAppConfig> = {};\n\tconst defaultTarget: DeployTarget = workspace.deploy?.default ?? 'dokploy';\n\n\tfor (const [id, declaration] of Object.entries(manifest)) {\n\t\t// Two kinds are apps: a site and an HTTP surface. A worker is not one —\n\t\t// it names the process that runs a cron or a subscriber, and that process\n\t\t// is the app's server, which already exists. Everything else is a\n\t\t// resource one of them uses.\n\t\tif (declaration.kind !== 'site' && declaration.kind !== 'rest-api')\n\t\t\tcontinue;\n\n\t\t// No opt-in to be had. A site is an app and so is a surface, and each\n\t\t// says where it lives.\n\t\tconst spec: AppSpec =\n\t\t\tdeclaration.kind === 'site'\n\t\t\t\t? declaration.app\n\t\t\t\t: {\n\t\t\t\t\t\tpath: declaration.path,\n\t\t\t\t\t\t...(declaration.telescope ? { telescope: true } : {}),\n\t\t\t\t\t};\n\n\t\tconst name = appKey(id);\n\t\t// A config entry of the same name still wins, so a workspace can override\n\t\t// one field without restating the app. Nothing in this repo needs to.\n\t\tconst configured = workspace.apps[name];\n\n\t\tapps[name] = {\n\t\t\t...configured,\n\t\t\ttype: declaration.kind === 'site' ? 'web' : 'backend',\n\t\t\tpath: spec.path,\n\t\t\tport: spec.port ?? configured?.port ?? 0,\n\t\t\tdependencies: [],\n\t\t\tresolvedDeployTarget: configured?.resolvedDeployTarget ?? defaultTarget,\n\t\t\t...(declaration.kind === 'site'\n\t\t\t\t? {\n\t\t\t\t\t\tframework: configured?.framework ?? FRAMEWORKS[declaration.variant],\n\t\t\t\t\t\t...(declaration.root ? { root: true } : {}),\n\t\t\t\t\t\t...(spec.config ? { config: spec.config } : {}),\n\t\t\t\t\t}\n\t\t\t\t: {}),\n\t\t\t// One glob fans out to the six the build reads, because each generator\n\t\t\t// already inspects every export and keeps what it recognises. A\n\t\t\t// per-kind field still wins where one was given.\n\t\t\t...(spec.telescope !== undefined ? { telescope: spec.telescope } : {}),\n\t\t\t...(spec.studio !== undefined ? { studio: spec.studio } : {}),\n\t\t\t...(spec.openapi !== undefined ? { openapi: spec.openapi } : {}),\n\t\t\t...(spec.runtime !== undefined ? { runtime: spec.runtime } : {}),\n\t\t\t...(spec.env !== undefined ? { env: spec.env } : {}),\n\t\t\t...(spec.entry !== undefined ? { entry: spec.entry } : {}),\n\t\t\t...(configured?.domain\n\t\t\t\t? { domain: configured.domain as AppDomainConfig }\n\t\t\t\t: {}),\n\t\t} as NormalizedAppConfig;\n\t}\n\n\t// Build order, from the edges rather than from a hand-kept list. A site that\n\t// depends on a surface depends on the app that serves it — which for a\n\t// mounted auth server is its host, not a container that does not exist.\n\tfor (const [id, declaration] of Object.entries(manifest)) {\n\t\t// Two kinds are apps: a site and an HTTP surface. A worker is not one —\n\t\t// it names the process that runs a cron or a subscriber, and that process\n\t\t// is the app's server, which already exists. Everything else is a\n\t\t// resource one of them uses.\n\t\tif (declaration.kind !== 'site' && declaration.kind !== 'rest-api')\n\t\t\tcontinue;\n\n\t\tconst app = apps[appKey(id)];\n\t\tif (!app) continue;\n\n\t\t// A site names its edges `dependencies`; a surface names them `calls`.\n\t\tconst edges =\n\t\t\tdeclaration.kind === 'rest-api'\n\t\t\t\t? (declaration.calls ?? [])\n\t\t\t\t: (declaration.dependencies ?? []);\n\n\t\tconst names = new Set<string>();\n\t\tfor (const edge of edges) {\n\t\t\tif (edge.kind !== 'rest-api') continue;\n\t\t\tconst host = hostOf(manifest, edge.target);\n\t\t\tif (!host) continue;\n\t\t\tconst key = appKey(host);\n\t\t\tif (key !== appKey(id) && apps[key]) names.add(key);\n\t\t}\n\n\t\tapp.dependencies = [...names].sort();\n\t}\n\n\tmarkRoot(apps);\n\tassignPorts(apps);\n\n\t// Apps config declared that the graph knows nothing about — a `type: 'mobile'`\n\t// entry, or a hand-run process. Kept rather than dropped: config is still the\n\t// escape hatch for what nothing declares.\n\tfor (const [name, app] of Object.entries(workspace.apps)) {\n\t\tif (!apps[name]) apps[name] = app;\n\t}\n\n\treturn apps;\n}\n","import { existsSync, readFileSync } from 'node:fs';\nimport { dirname, join, parse } from 'node:path';\nimport { discover } from './reconcile/discover.js';\nimport type { GkmConfig } from './types.js';\nimport { derivedApps } from './workspace/derive.js';\nimport {\n\tallConstructGlobs,\n\tgetAppGkmConfig,\n\tisWorkspaceConfig,\n\ttype LoadedConfig,\n\ttype NormalizedAppConfig,\n\ttype NormalizedWorkspace,\n\tprocessConfig,\n\ttype WorkspaceConfig,\n} from './workspace/index.js';\n\nexport type { GkmConfig } from './types.js';\nexport type { LoadedConfig, WorkspaceConfig } from './workspace/index.js';\nexport { defineWorkspace } from './workspace/index.js';\n/**\n * Define GKM configuration with full TypeScript support.\n * This is an identity function that provides type safety and autocomplete.\n *\n * @example\n * ```ts\n * // gkm.config.ts\n * import { defineConfig } from '@geekmidas/cli/config';\n *\n * export default defineConfig({\n * routes: './src/endpoints/**\\/*.ts',\n * envParser: './src/config/env',\n * logger: './src/config/logger',\n * telescope: true,\n * });\n * ```\n */\nexport function defineConfig(config: GkmConfig): GkmConfig {\n\treturn config;\n}\n\nexport interface ParsedModuleConfig {\n\tpath: string;\n\timportPattern: string;\n}\n\n/**\n * Parse a module config string into path and import pattern.\n *\n * @param configString - Config string in format \"./path/to/module\" or \"./path/to/module#exportName\"\n * @param defaultAlias - The default alias name to use if no export name specified\n * @returns Object with path and import pattern\n *\n * @example\n * parseModuleConfig('./src/config/env', 'envParser')\n * // { path: './src/config/env', importPattern: 'envParser' }\n *\n * parseModuleConfig('./src/config/env#envParser', 'envParser')\n * // { path: './src/config/env', importPattern: '{ envParser }' }\n *\n * parseModuleConfig('./src/config/env#myEnv', 'envParser')\n * // { path: './src/config/env', importPattern: '{ myEnv as envParser }' }\n */\nexport function parseModuleConfig(\n\tconfigString: string,\n\tdefaultAlias: string,\n): ParsedModuleConfig {\n\tconst parts = configString.split('#');\n\tconst path = parts[0] ?? configString;\n\tconst exportName = parts[1];\n\tconst importPattern = !exportName\n\t\t? defaultAlias\n\t\t: exportName === defaultAlias\n\t\t\t? `{ ${defaultAlias} }`\n\t\t\t: `{ ${exportName} as ${defaultAlias} }`;\n\n\treturn { path, importPattern };\n}\n\nexport interface ConfigDiscoveryResult {\n\tconfigPath: string;\n\tworkspaceRoot: string;\n}\n\n/**\n * Find and return the path to the config file.\n *\n * Resolution order:\n * 1. GKM_CONFIG_PATH env var (set by workspace dev command)\n * 2. Walk up directory tree from cwd\n */\nfunction findConfigPath(cwd: string): ConfigDiscoveryResult {\n\tconst files = ['gkm.config.json', 'gkm.config.ts', 'gkm.config.js'];\n\n\t// Check GKM_CONFIG_PATH env var first (set by workspace dev command)\n\tconst envConfigPath = process.env.GKM_CONFIG_PATH;\n\tif (envConfigPath && existsSync(envConfigPath)) {\n\t\treturn {\n\t\t\tconfigPath: envConfigPath,\n\t\t\tworkspaceRoot: dirname(envConfigPath),\n\t\t};\n\t}\n\n\t// Walk up directory tree to find config\n\tlet currentDir = cwd;\n\tconst { root } = parse(currentDir);\n\n\twhile (currentDir !== root) {\n\t\tfor (const file of files) {\n\t\t\tconst configPath = join(currentDir, file);\n\t\t\tif (existsSync(configPath)) {\n\t\t\t\treturn {\n\t\t\t\t\tconfigPath,\n\t\t\t\t\tworkspaceRoot: currentDir,\n\t\t\t\t};\n\t\t\t}\n\t\t}\n\t\tcurrentDir = dirname(currentDir);\n\t}\n\n\tthrow new Error(\n\t\t'Configuration file not found. Please create gkm.config.json, gkm.config.ts, or gkm.config.js in the project root.',\n\t);\n}\n\n/**\n * Get app name from package.json in the given directory.\n * Handles scoped packages by extracting the name after the scope.\n *\n * @example\n * getAppNameFromCwd('/path/to/apps/api')\n * // package.json: { \"name\": \"@myorg/api\" }\n * // Returns: 'api'\n */\nexport function getAppNameFromCwd(cwd: string = process.cwd()): string | null {\n\tconst packageJsonPath = join(cwd, 'package.json');\n\n\tif (!existsSync(packageJsonPath)) {\n\t\treturn null;\n\t}\n\n\ttry {\n\t\tconst packageJson = JSON.parse(readFileSync(packageJsonPath, 'utf-8'));\n\t\tconst name = packageJson.name as string | undefined;\n\n\t\tif (!name) {\n\t\t\treturn null;\n\t\t}\n\n\t\t// Handle scoped packages: @scope/name -> name\n\t\tif (name.startsWith('@') && name.includes('/')) {\n\t\t\treturn name.split('/')[1] ?? null;\n\t\t}\n\n\t\treturn name;\n\t} catch {\n\t\treturn null;\n\t}\n}\n\ninterface RawConfigResult {\n\tconfig: GkmConfig | WorkspaceConfig;\n\tworkspaceRoot: string;\n}\n\n/**\n * Load raw configuration from file.\n */\nasync function loadRawConfig(cwd: string): Promise<RawConfigResult> {\n\tconst { configPath, workspaceRoot } = findConfigPath(cwd);\n\n\ttry {\n\t\tconst config = await import(configPath);\n\t\treturn {\n\t\t\tconfig: config.default,\n\t\t\tworkspaceRoot,\n\t\t};\n\t} catch (error) {\n\t\tthrow new Error(`Failed to load config: ${(error as Error).message}`);\n\t}\n}\n\n/**\n * Load configuration file (single-app format).\n * For backwards compatibility with existing code.\n *\n * @deprecated Use loadWorkspaceConfig for new code\n */\nexport async function loadConfig(\n\tcwd: string = process.cwd(),\n): Promise<GkmConfig> {\n\tconst { config } = await loadRawConfig(cwd);\n\n\t// If it's a workspace config, throw an error\n\tif (isWorkspaceConfig(config)) {\n\t\tthrow new Error(\n\t\t\t'Workspace configuration detected. Use loadWorkspaceConfig() instead.',\n\t\t);\n\t}\n\n\treturn config;\n}\n\n/**\n * Load configuration file and process it as a workspace.\n * Works with both single-app and workspace configurations.\n *\n * Single-app configs are automatically wrapped as a workspace with one app.\n *\n * @example\n * ```ts\n * const { type, workspace } = await loadWorkspaceConfig();\n *\n * if (type === 'workspace') {\n * console.log('Multi-app workspace:', workspace.apps);\n * } else {\n * console.log('Single app wrapped as workspace');\n * }\n * ```\n */\nexport async function loadWorkspaceConfig(\n\tcwd: string = process.cwd(),\n): Promise<LoadedConfig> {\n\tconst { config, workspaceRoot } = await loadRawConfig(cwd);\n\treturn withDerivedApps(processConfig(config, workspaceRoot));\n}\n\n/**\n * The loaded config, with its apps read off the graph.\n *\n * Here rather than at each call site because every command needs the same\n * answer, and the ones that forgot to ask were exactly the bugs: a site that\n * never deployed, a surface that could not be its own process. `gkm dev`,\n * `gkm build`, `gkm docker` and `gkm deploy` now all see the same apps without\n * any of them knowing a derivation happened.\n *\n * Discovery imports the construct modules — declarations only, not endpoints —\n * which is the same import the reconciler and the build already do. A workspace\n * with no `constructs` glob discovers nothing and keeps whatever its config\n * said, so nothing that worked before changes.\n *\n * A construct file that throws is reported and skipped rather than taking the\n * command down: `gkm dev` failing to start because a half-written declaration\n * cannot be imported is a worse failure than starting with the app it does not\n * describe yet.\n */\nasync function withDerivedApps(loaded: LoadedConfig): Promise<LoadedConfig> {\n\tconst globs = allConstructGlobs(loaded.workspace);\n\tif (globs.length === 0) return loaded;\n\n\ttry {\n\t\tconst manifest = await discover({\n\t\t\tpatterns: globs,\n\t\t\tcwd: loaded.workspace.root,\n\t\t});\n\n\t\treturn {\n\t\t\t...loaded,\n\t\t\tmanifest,\n\t\t\tworkspace: {\n\t\t\t\t...loaded.workspace,\n\t\t\t\tapps: derivedApps(manifest, loaded.workspace),\n\t\t\t},\n\t\t};\n\t} catch (error) {\n\t\tconst message = error instanceof Error ? error.message : String(error);\n\t\tconsole.warn(\n\t\t\t`⚠️ Could not read constructs, so apps come from config alone: ${message}`,\n\t\t);\n\t\treturn loaded;\n\t}\n}\n\nexport interface AppConfigResult {\n\tappName: string;\n\tapp: NormalizedAppConfig;\n\tgkmConfig: GkmConfig;\n\tworkspace: NormalizedWorkspace;\n\tworkspaceRoot: string;\n\tappRoot: string;\n}\n\n/**\n * The app a package directory refers to, and what the workspace calls it.\n *\n * A single-app config is wrapped as a one-app workspace keyed `api`, which is\n * almost never the package name. Matching on the key alone means an app like\n * `@geekmidas/example` resolves to nothing and silently loses everything that\n * comes from the workspace — including the URLs its constructs declare.\n */\nfunction resolveWorkspaceApp(\n\tloadedConfig: LoadedConfig,\n\tappName: string,\n): { key: string; app: NormalizedAppConfig } | undefined {\n\tconst apps = loadedConfig.workspace.apps;\n\n\tconst named = apps[appName];\n\tif (named) return { key: appName, app: named };\n\n\tconst names = Object.keys(apps);\n\tconst only = names[0];\n\tif (loadedConfig.type !== 'single' || names.length !== 1 || !only) {\n\t\treturn undefined;\n\t}\n\n\tconst app = apps[only];\n\n\treturn app ? { key: only, app } : undefined;\n}\n\n/**\n * Load app-specific configuration from workspace.\n * Uses the app name from package.json to find the correct app config.\n *\n * @example\n * ```ts\n * // From apps/api directory with package.json: { \"name\": \"@myorg/api\" }\n * const { app, workspace, workspaceRoot } = await loadAppConfig();\n * console.log(app.routes); // './src/endpoints/**\\/*.ts'\n * ```\n */\nexport async function loadAppConfig(\n\tcwd: string = process.cwd(),\n): Promise<AppConfigResult> {\n\tconst appName = getAppNameFromCwd(cwd);\n\n\tif (!appName) {\n\t\tthrow new Error(\n\t\t\t'Could not determine app name. Ensure package.json exists with a \"name\" field.',\n\t\t);\n\t}\n\n\tconst { config, workspaceRoot } = await loadRawConfig(cwd);\n\tconst loadedConfig = await withDerivedApps(\n\t\tprocessConfig(config, workspaceRoot),\n\t);\n\n\tconst resolved = resolveWorkspaceApp(loadedConfig, appName);\n\n\tif (!resolved) {\n\t\tthrow new Error(\n\t\t\t`App \"${appName}\" not found in workspace config. Available apps: ${Object.keys(\n\t\t\t\tloadedConfig.workspace.apps,\n\t\t\t).join(', ')}. ` +\n\t\t\t\t`Ensure the package.json name matches the app key in gkm.config.ts.`,\n\t\t);\n\t}\n\n\tconst { key, app } = resolved;\n\n\t// Keyed by what the workspace calls the app, not by the package name — for a\n\t// single-app config those differ.\n\tconst gkmConfig = getAppGkmConfig(loadedConfig.workspace, key);\n\n\tif (!gkmConfig) {\n\t\tthrow new Error(\n\t\t\t`App \"${appName}\" is not a backend app and cannot be run with gkm dev.`,\n\t\t);\n\t}\n\n\treturn {\n\t\tappName,\n\t\tapp,\n\t\tgkmConfig,\n\t\tworkspace: loadedConfig.workspace,\n\t\tworkspaceRoot,\n\t\tappRoot: join(workspaceRoot, app.path),\n\t};\n}\n\nexport interface WorkspaceAppInfo {\n\tappName: string;\n\tapp: NormalizedAppConfig;\n\tworkspace: NormalizedWorkspace;\n\tworkspaceRoot: string;\n}\n\n/**\n * Load workspace info for any app (frontend or backend).\n * Unlike loadAppConfig, this does NOT require the app to have a gkm config\n * (routes, entry, etc.), making it suitable for gkm exec/test from frontend apps.\n */\nexport async function loadWorkspaceAppInfo(\n\tcwd: string = process.cwd(),\n): Promise<WorkspaceAppInfo> {\n\tconst appName = getAppNameFromCwd(cwd);\n\n\tif (!appName) {\n\t\tthrow new Error(\n\t\t\t'Could not determine app name. Ensure package.json exists with a \"name\" field.',\n\t\t);\n\t}\n\n\tconst { config, workspaceRoot } = await loadRawConfig(cwd);\n\tconst loadedConfig = await withDerivedApps(\n\t\tprocessConfig(config, workspaceRoot),\n\t);\n\n\tconst resolved = resolveWorkspaceApp(loadedConfig, appName);\n\n\tif (!resolved) {\n\t\tconst availableApps = Object.keys(loadedConfig.workspace.apps).join(', ');\n\t\tthrow new Error(\n\t\t\t`App \"${appName}\" not found in workspace config. Available apps: ${availableApps}. ` +\n\t\t\t\t`Ensure the package.json name matches the app key in gkm.config.ts.`,\n\t\t);\n\t}\n\n\treturn {\n\t\tappName,\n\t\tapp: resolved.app,\n\t\tworkspace: loadedConfig.workspace,\n\t\tworkspaceRoot,\n\t};\n}\n"],"mappings":";;;;;;;;;AAgCA,MAAMA,aAAwC;CAC7C,QAAQ;CACR,MAAM;CACN,UAAU;AACV;;;;;;;;AASD,SAAgB,OAAOC,IAAoB;AAC1C,QAAO,oCAAU,GAAG;AACpB;;;;;;;;;AAUD,SAAgB,OACfC,UACAD,IACqB;CACrB,MAAM,cAAc,SAAS;AAC7B,MAAK,eAAe,YAAY,SAAS,WAAY;AAErD,QAAO;AACP;;;;;;;;;;AAWD,SAAS,YAAYE,MAAiD;CACrE,MAAM,OAAO,CACZC,MACAC,QAC8B;EAC9B,IAAI,SAAS,YAAY,IAAI,IAAI,SAAS,QAAQ,IAAI;EACtD,IAAI,OAAO,IAAI;EACf;CACA;CAED,MAAM,UAAU,OAAO,QAAQ,KAAK,CAAC,KAAK,CAAC,CAAC,OAAO,EAAE,EAAE,CAAC,OAAO,EAAE,KAAK;EACrE,MAAM,CAAC,IAAI,IAAI,GAAG,GAAG,KAAK,OAAO,EAAE;EACnC,MAAM,CAAC,IAAI,IAAI,GAAG,GAAG,KAAK,OAAO,EAAE;AACnC,SAAO,KAAK,MAAM,KAAK,MAAM,GAAG,cAAc,GAAG;CACjD,EAAC;CAEF,MAAM,QAAQ,IAAI,IACjB,QAAQ,IAAI,CAAC,GAAG,IAAI,KAAK,IAAI,KAAK,CAAC,OAAO,CAAC,QAAqB,EAAE;CAGnE,IAAI,OAAO;AACX,MAAK,MAAM,GAAG,IAAI,IAAI,SAAS;AAC9B,MAAI,IAAI,KAAM;AACd,SAAO,MAAM,IAAI,KAAK,CAAE,SAAQ;AAChC,MAAI,OAAO;AACX,QAAM,IAAI,KAAK;CACf;AACD;;;;;;;AAQD,SAAS,SAASF,MAAiD;CAClE,MAAM,QAAQ,OAAO,QAAQ,KAAK,CAAC,OAAO,CAAC,GAAG,EAAE,KAAK,EAAE,SAAS,MAAM;AACtE,KAAI,MAAM,WAAW,EAAG;AACxB,KAAI,MAAM,KAAK,CAAC,GAAG,EAAE,KAAK,EAAE,KAAK,CAAE;CAEnC,MAAM,SACL,MAAM,KAAK,CAAC,CAAC,KAAK,KAAK,SAAS,MAAM,KACrC,MAAM,WAAW,IAAI,MAAM;AAC7B,KAAI,OAAQ,QAAO,GAAG,OAAO;AAC7B;AAED,SAAgB,YACfD,UACAI,WACsC;CACtC,MAAMH,OAA4C,CAAE;CACpD,MAAMI,gBAA8B,UAAU,QAAQ,WAAW;AAEjE,MAAK,MAAM,CAAC,IAAI,YAAY,IAAI,OAAO,QAAQ,SAAS,EAAE;AAKzD,MAAI,YAAY,SAAS,UAAU,YAAY,SAAS,WACvD;EAID,MAAMC,OACL,YAAY,SAAS,SAClB,YAAY,MACZ;GACA,MAAM,YAAY;GAClB,GAAI,YAAY,YAAY,EAAE,WAAW,KAAM,IAAG,CAAE;EACpD;EAEJ,MAAM,OAAO,OAAO,GAAG;EAGvB,MAAM,aAAa,UAAU,KAAK;AAElC,OAAK,QAAQ;GACZ,GAAG;GACH,MAAM,YAAY,SAAS,SAAS,QAAQ;GAC5C,MAAM,KAAK;GACX,MAAM,KAAK,QAAQ,YAAY,QAAQ;GACvC,cAAc,CAAE;GAChB,sBAAsB,YAAY,wBAAwB;GAC1D,GAAI,YAAY,SAAS,SACtB;IACA,WAAW,YAAY,aAAa,WAAW,YAAY;IAC3D,GAAI,YAAY,OAAO,EAAE,MAAM,KAAM,IAAG,CAAE;IAC1C,GAAI,KAAK,SAAS,EAAE,QAAQ,KAAK,OAAQ,IAAG,CAAE;GAC9C,IACA,CAAE;GAIL,GAAI,KAAK,uBAA0B,EAAE,WAAW,KAAK,UAAW,IAAG,CAAE;GACrE,GAAI,KAAK,oBAAuB,EAAE,QAAQ,KAAK,OAAQ,IAAG,CAAE;GAC5D,GAAI,KAAK,qBAAwB,EAAE,SAAS,KAAK,QAAS,IAAG,CAAE;GAC/D,GAAI,KAAK,qBAAwB,EAAE,SAAS,KAAK,QAAS,IAAG,CAAE;GAC/D,GAAI,KAAK,iBAAoB,EAAE,KAAK,KAAK,IAAK,IAAG,CAAE;GACnD,GAAI,KAAK,mBAAsB,EAAE,OAAO,KAAK,MAAO,IAAG,CAAE;GACzD,GAAI,YAAY,SACb,EAAE,QAAQ,WAAW,OAA2B,IAChD,CAAE;EACL;CACD;AAKD,MAAK,MAAM,CAAC,IAAI,YAAY,IAAI,OAAO,QAAQ,SAAS,EAAE;AAKzD,MAAI,YAAY,SAAS,UAAU,YAAY,SAAS,WACvD;EAED,MAAM,MAAM,KAAK,OAAO,GAAG;AAC3B,OAAK,IAAK;EAGV,MAAM,QACL,YAAY,SAAS,aACjB,YAAY,SAAS,CAAE,IACvB,YAAY,gBAAgB,CAAE;EAEnC,MAAM,wBAAQ,IAAI;AAClB,OAAK,MAAM,QAAQ,OAAO;AACzB,OAAI,KAAK,SAAS,WAAY;GAC9B,MAAM,OAAO,OAAO,UAAU,KAAK,OAAO;AAC1C,QAAK,KAAM;GACX,MAAM,MAAM,OAAO,KAAK;AACxB,OAAI,QAAQ,OAAO,GAAG,IAAI,KAAK,KAAM,OAAM,IAAI,IAAI;EACnD;AAED,MAAI,eAAe,CAAC,GAAG,KAAM,EAAC,MAAM;CACpC;AAED,UAAS,KAAK;AACd,aAAY,KAAK;AAKjB,MAAK,MAAM,CAAC,MAAM,IAAI,IAAI,OAAO,QAAQ,UAAU,KAAK,CACvD,MAAK,KAAK,MAAO,MAAK,QAAQ;AAG/B,QAAO;AACP;;;;;;;;;;;;;;;;;;;;;AC5LD,SAAgB,aAAaC,QAA8B;AAC1D,QAAO;AACP;;;;;;;;;;;;;;;;;;AAwBD,SAAgB,kBACfC,cACAC,cACqB;CACrB,MAAM,QAAQ,aAAa,MAAM,IAAI;CACrC,MAAM,OAAO,MAAM,MAAM;CACzB,MAAM,aAAa,MAAM;CACzB,MAAM,iBAAiB,aACpB,eACA,eAAe,gBACb,IAAI,aAAa,OACjB,IAAI,WAAW,MAAM,aAAa;AAEvC,QAAO;EAAE;EAAM;CAAe;AAC9B;;;;;;;;AAcD,SAAS,eAAeC,KAAoC;CAC3D,MAAM,QAAQ;EAAC;EAAmB;EAAiB;CAAgB;CAGnE,MAAM,gBAAgB,QAAQ,IAAI;AAClC,KAAI,iBAAiB,wBAAW,cAAc,CAC7C,QAAO;EACN,YAAY;EACZ,eAAe,uBAAQ,cAAc;CACrC;CAIF,IAAI,aAAa;CACjB,MAAM,EAAE,MAAM,GAAG,qBAAM,WAAW;AAElC,QAAO,eAAe,MAAM;AAC3B,OAAK,MAAM,QAAQ,OAAO;GACzB,MAAM,aAAa,oBAAK,YAAY,KAAK;AACzC,OAAI,wBAAW,WAAW,CACzB,QAAO;IACN;IACA,eAAe;GACf;EAEF;AACD,eAAa,uBAAQ,WAAW;CAChC;AAED,OAAM,IAAI,MACT;AAED;;;;;;;;;;AAWD,SAAgB,kBAAkBA,MAAc,QAAQ,KAAK,EAAiB;CAC7E,MAAM,kBAAkB,oBAAK,KAAK,eAAe;AAEjD,MAAK,wBAAW,gBAAgB,CAC/B,QAAO;AAGR,KAAI;EACH,MAAM,cAAc,KAAK,MAAM,0BAAa,iBAAiB,QAAQ,CAAC;EACtE,MAAM,OAAO,YAAY;AAEzB,OAAK,KACJ,QAAO;AAIR,MAAI,KAAK,WAAW,IAAI,IAAI,KAAK,SAAS,IAAI,CAC7C,QAAO,KAAK,MAAM,IAAI,CAAC,MAAM;AAG9B,SAAO;CACP,QAAO;AACP,SAAO;CACP;AACD;;;;AAUD,eAAe,cAAcA,KAAuC;CACnE,MAAM,EAAE,YAAY,eAAe,GAAG,eAAe,IAAI;AAEzD,KAAI;EACH,MAAM,SAAS,MAAM,OAAO;AAC5B,SAAO;GACN,QAAQ,OAAO;GACf;EACA;CACD,SAAQ,OAAO;AACf,QAAM,IAAI,OAAO,yBAA0B,MAAgB,QAAQ;CACnE;AACD;;;;;;;AAQD,eAAsB,WACrBA,MAAc,QAAQ,KAAK,EACN;CACrB,MAAM,EAAE,QAAQ,GAAG,MAAM,cAAc,IAAI;AAG3C,KAAI,oCAAkB,OAAO,CAC5B,OAAM,IAAI,MACT;AAIF,QAAO;AACP;;;;;;;;;;;;;;;;;;AAmBD,eAAsB,oBACrBA,MAAc,QAAQ,KAAK,EACH;CACxB,MAAM,EAAE,QAAQ,eAAe,GAAG,MAAM,cAAc,IAAI;AAC1D,QAAO,gBAAgB,gCAAc,QAAQ,cAAc,CAAC;AAC5D;;;;;;;;;;;;;;;;;;;;AAqBD,eAAe,gBAAgBC,QAA6C;CAC3E,MAAM,QAAQ,oCAAkB,OAAO,UAAU;AACjD,KAAI,MAAM,WAAW,EAAG,QAAO;AAE/B,KAAI;EACH,MAAM,WAAW,MAAM,0BAAS;GAC/B,UAAU;GACV,KAAK,OAAO,UAAU;EACtB,EAAC;AAEF,SAAO;GACN,GAAG;GACH;GACA,WAAW;IACV,GAAG,OAAO;IACV,MAAM,YAAY,UAAU,OAAO,UAAU;GAC7C;EACD;CACD,SAAQ,OAAO;EACf,MAAM,UAAU,iBAAiB,QAAQ,MAAM,UAAU,OAAO,MAAM;AACtE,UAAQ,MACN,iEAAiE,QAAQ,EAC1E;AACD,SAAO;CACP;AACD;;;;;;;;;AAmBD,SAAS,oBACRC,cACAC,SACwD;CACxD,MAAM,OAAO,aAAa,UAAU;CAEpC,MAAM,QAAQ,KAAK;AACnB,KAAI,MAAO,QAAO;EAAE,KAAK;EAAS,KAAK;CAAO;CAE9C,MAAM,QAAQ,OAAO,KAAK,KAAK;CAC/B,MAAM,OAAO,MAAM;AACnB,KAAI,aAAa,SAAS,YAAY,MAAM,WAAW,MAAM,KAC5D;CAGD,MAAM,MAAM,KAAK;AAEjB,QAAO,MAAM;EAAE,KAAK;EAAM;CAAK;AAC/B;;;;;;;;;;;;AAaD,eAAsB,cACrBH,MAAc,QAAQ,KAAK,EACA;CAC3B,MAAM,UAAU,kBAAkB,IAAI;AAEtC,MAAK,QACJ,OAAM,IAAI,MACT;CAIF,MAAM,EAAE,QAAQ,eAAe,GAAG,MAAM,cAAc,IAAI;CAC1D,MAAM,eAAe,MAAM,gBAC1B,gCAAc,QAAQ,cAAc,CACpC;CAED,MAAM,WAAW,oBAAoB,cAAc,QAAQ;AAE3D,MAAK,SACJ,OAAM,IAAI,OACR,OAAO,QAAQ,mDAAmD,OAAO,KACzE,aAAa,UAAU,KACvB,CAAC,KAAK,KAAK,CAAC;CAKf,MAAM,EAAE,KAAK,KAAK,GAAG;CAIrB,MAAM,YAAY,kCAAgB,aAAa,WAAW,IAAI;AAE9D,MAAK,UACJ,OAAM,IAAI,OACR,OAAO,QAAQ;AAIlB,QAAO;EACN;EACA;EACA;EACA,WAAW,aAAa;EACxB;EACA,SAAS,oBAAK,eAAe,IAAI,KAAK;CACtC;AACD;;;;;;AAcD,eAAsB,qBACrBA,MAAc,QAAQ,KAAK,EACC;CAC5B,MAAM,UAAU,kBAAkB,IAAI;AAEtC,MAAK,QACJ,OAAM,IAAI,MACT;CAIF,MAAM,EAAE,QAAQ,eAAe,GAAG,MAAM,cAAc,IAAI;CAC1D,MAAM,eAAe,MAAM,gBAC1B,gCAAc,QAAQ,cAAc,CACpC;CAED,MAAM,WAAW,oBAAoB,cAAc,QAAQ;AAE3D,MAAK,UAAU;EACd,MAAM,gBAAgB,OAAO,KAAK,aAAa,UAAU,KAAK,CAAC,KAAK,KAAK;AACzE,QAAM,IAAI,OACR,OAAO,QAAQ,mDAAmD,cAAc;CAGlF;AAED,QAAO;EACN;EACA,KAAK,SAAS;EACd,WAAW,aAAa;EACxB;CACA;AACD"}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { discover } from "./discover-CAlFgYec.mjs";
|
|
2
|
-
import { allConstructGlobs, getAppGkmConfig, isWorkspaceConfig, processConfig } from "./workspace-
|
|
2
|
+
import { allConstructGlobs, getAppGkmConfig, isWorkspaceConfig, processConfig } from "./workspace-zoqRbihZ.mjs";
|
|
3
3
|
import { existsSync, readFileSync } from "node:fs";
|
|
4
4
|
import { dirname, join, parse } from "node:path";
|
|
5
5
|
import { kebabCase } from "@geekmidas/manifest";
|
|
@@ -76,53 +76,15 @@ function markRoot(apps) {
|
|
|
76
76
|
const chosen = sites.find(([name]) => name === "web") ?? (sites.length === 1 ? sites[0] : void 0);
|
|
77
77
|
if (chosen) chosen[1].root = true;
|
|
78
78
|
}
|
|
79
|
-
/**
|
|
80
|
-
* What `app: true` means, and what the object form leaves out.
|
|
81
|
-
*
|
|
82
|
-
* Both fields follow from the construct's own id, so neither was worth making
|
|
83
|
-
* someone write down:
|
|
84
|
-
*
|
|
85
|
-
* - `path` is `apps/<kebab-id>` where that directory exists, and the workspace
|
|
86
|
-
* root otherwise. `Api` means `apps/api` in a monorepo and `.` in a
|
|
87
|
-
* single-app project, and which of the two you are in is answerable by
|
|
88
|
-
* looking.
|
|
89
|
-
* - `code` is the conventional directories under `path`. A glob is worth
|
|
90
|
-
* writing only when the code is somewhere else, which is the case the field
|
|
91
|
-
* still exists for.
|
|
92
|
-
*
|
|
93
|
-
* A site takes no `code`: its build is its framework's, not ours.
|
|
94
|
-
*/
|
|
95
|
-
/**
|
|
96
|
-
* Where an app lives when it did not say.
|
|
97
|
-
*
|
|
98
|
-
* `apps/<kebab-id>`, which is what the id already said — or the project root
|
|
99
|
-
* when there is no such directory, because then there is one app and it is the
|
|
100
|
-
* project.
|
|
101
|
-
*
|
|
102
|
-
* This refused, briefly, when a workspace had an `apps/` directory with nothing
|
|
103
|
-
* under this name — on the grounds that answering `.` makes the app the whole
|
|
104
|
-
* repository. That was the wrong place for the guard twice over. It throws
|
|
105
|
-
* inside discovery, which is caught, so one misplaced app lost the whole
|
|
106
|
-
* manifest and every other app with it. And the hazard it guards — the build
|
|
107
|
-
* filtering turbo onto the root package, whose `build` is `gkm build` — is the
|
|
108
|
-
* build's question, asked where the answer is actionable.
|
|
109
|
-
*/
|
|
110
|
-
function conventionalPath(id, workspaceRoot) {
|
|
111
|
-
const conventional = join("apps", appKey(id));
|
|
112
|
-
return existsSync(join(workspaceRoot, conventional)) ? conventional : ".";
|
|
113
|
-
}
|
|
114
|
-
function resolveAppSpec(id, spec, workspaceRoot) {
|
|
115
|
-
return {
|
|
116
|
-
...spec,
|
|
117
|
-
path: spec.path ?? conventionalPath(id, workspaceRoot)
|
|
118
|
-
};
|
|
119
|
-
}
|
|
120
79
|
function derivedApps(manifest, workspace) {
|
|
121
80
|
const apps = {};
|
|
122
81
|
const defaultTarget = workspace.deploy?.default ?? "dokploy";
|
|
123
82
|
for (const [id, declaration] of Object.entries(manifest)) {
|
|
124
83
|
if (declaration.kind !== "site" && declaration.kind !== "rest-api") continue;
|
|
125
|
-
const spec =
|
|
84
|
+
const spec = declaration.kind === "site" ? declaration.app : {
|
|
85
|
+
path: declaration.path,
|
|
86
|
+
...declaration.telescope ? { telescope: true } : {}
|
|
87
|
+
};
|
|
126
88
|
const name = appKey(id);
|
|
127
89
|
const configured = workspace.apps[name];
|
|
128
90
|
apps[name] = {
|
|
@@ -436,5 +398,5 @@ async function loadWorkspaceAppInfo(cwd = process.cwd()) {
|
|
|
436
398
|
}
|
|
437
399
|
|
|
438
400
|
//#endregion
|
|
439
|
-
export { appKey, defineConfig, derivedApps, getAppNameFromCwd, hostOf, loadAppConfig, loadConfig, loadWorkspaceAppInfo, loadWorkspaceConfig, parseModuleConfig
|
|
440
|
-
//# sourceMappingURL=config-
|
|
401
|
+
export { appKey, defineConfig, derivedApps, getAppNameFromCwd, hostOf, loadAppConfig, loadConfig, loadWorkspaceAppInfo, loadWorkspaceConfig, parseModuleConfig };
|
|
402
|
+
//# sourceMappingURL=config-DVVph9yz.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"config-DVVph9yz.mjs","names":["FRAMEWORKS: Record<string, Framework>","id: string","manifest: ConstructManifest","apps: Record<string, NormalizedAppConfig>","name: string","app: NormalizedAppConfig","workspace: NormalizedWorkspace","defaultTarget: DeployTarget","spec: AppSpec","config: GkmConfig","configString: string","defaultAlias: string","cwd: string","loaded: LoadedConfig","loadedConfig: LoadedConfig","appName: string"],"sources":["../src/workspace/derive.ts","../src/config.ts"],"sourcesContent":["/**\n * The apps a manifest implies.\n *\n * `gkm.config.ts` used to carry an `apps` block naming every app: its type, its\n * path, its port, its framework, which other apps it depended on. Every one of\n * those facts was already declared. A `StaticSite('Web', { path: 'apps/web' })`\n * and an `apps.web = { type: 'web', path: 'apps/web', framework: 'vite',\n * dependencies: ['api'] }` are the same statement written twice, and only one of\n * them was checked against anything — so the copy in config was free to drift,\n * and a surface config had no entry for simply never deployed.\n *\n * So the list comes from the graph. A `site` is an app, and so is every\n * `rest-api` — one surface, one container, at the `path` it declared.\n *\n * What config still supplies is what no graph can answer: the backends a cache\n * and a mailer resolve to, the deploy endpoint, the stage's domain.\n */\n\nimport {\n\ttype AppSpec,\n\ttype ConstructManifest,\n\tkebabCase,\n} from '@geekmidas/manifest';\nimport type {\n\tAppDomainConfig,\n\tDeployTarget,\n\tFramework,\n\tNormalizedAppConfig,\n\tNormalizedWorkspace,\n} from './types.js';\n\n/** Which frontend framework builds each site variant. */\nconst FRAMEWORKS: Record<string, Framework> = {\n\tstatic: 'vite',\n\tnext: 'nextjs',\n\ttanstack: 'tanstack-start',\n};\n\n/**\n * The app key a construct id becomes.\n *\n * The same kebab form every physical name is built from, so `Api` is `api` in\n * the config, `production-kitchen-sink-api` on Dokploy, and `API_URL` in an\n * environment — one id, three renderings, no second list.\n */\nexport function appKey(id: string): string {\n\treturn kebabCase(id);\n}\n\n/**\n * The app serving a surface: its own, always.\n *\n * Every surface gets a container. There is no arrangement in which one runs\n * inside another — two surfaces in a process share a filesystem, an\n * environment, and every credential either was granted, so an auth server\n * beside an API is one bug in the API away from being read by it.\n */\nexport function hostOf(\n\tmanifest: ConstructManifest,\n\tid: string,\n): string | undefined {\n\tconst declaration = manifest[id];\n\tif (!declaration || declaration.kind !== 'rest-api') return undefined;\n\n\treturn id;\n}\n\n/**\n * Local ports, assigned so that adding a site does not renumber the others.\n *\n * Backends first, then the site holding the base domain, then the rest\n * alphabetically. The ordering is arbitrary but it is *fixed*, which is the\n * property that matters: a port that moves when an unrelated app is added is a\n * port nobody can put in a bookmark. A construct that names its own port keeps\n * it, and is skipped here.\n */\nfunction assignPorts(apps: Record<string, NormalizedAppConfig>): void {\n\tconst rank = (\n\t\tname: string,\n\t\tapp: NormalizedAppConfig,\n\t): [number, number, string] => [\n\t\tapp.type === 'backend' ? 0 : app.type === 'web' ? 1 : 2,\n\t\tapp.root ? 0 : 1,\n\t\tname,\n\t];\n\n\tconst ordered = Object.entries(apps).sort(([aName, a], [bName, b]) => {\n\t\tconst [at, ar, an] = rank(aName, a);\n\t\tconst [bt, br, bn] = rank(bName, b);\n\t\treturn at - bt || ar - br || an.localeCompare(bn);\n\t});\n\n\tconst taken = new Set(\n\t\tordered.map(([, app]) => app.port).filter((p): p is number => !!p),\n\t);\n\n\tlet next = 3000;\n\tfor (const [, app] of ordered) {\n\t\tif (app.port) continue;\n\t\twhile (taken.has(next)) next += 1;\n\t\tapp.port = next;\n\t\ttaken.add(next);\n\t}\n}\n\n/**\n * Whichever site holds the base domain.\n *\n * The convention first — a site named `web` — because it is one people already\n * rely on. Then an explicit `root: true`. A project with one site needs neither.\n */\nfunction markRoot(apps: Record<string, NormalizedAppConfig>): void {\n\tconst sites = Object.entries(apps).filter(([, a]) => a.type === 'web');\n\tif (sites.length === 0) return;\n\tif (sites.some(([, a]) => a.root)) return;\n\n\tconst chosen =\n\t\tsites.find(([name]) => name === 'web') ??\n\t\t(sites.length === 1 ? sites[0] : undefined);\n\tif (chosen) chosen[1].root = true;\n}\n\nexport function derivedApps(\n\tmanifest: ConstructManifest,\n\tworkspace: NormalizedWorkspace,\n): Record<string, NormalizedAppConfig> {\n\tconst apps: Record<string, NormalizedAppConfig> = {};\n\tconst defaultTarget: DeployTarget = workspace.deploy?.default ?? 'dokploy';\n\n\tfor (const [id, declaration] of Object.entries(manifest)) {\n\t\t// Two kinds are apps: a site and an HTTP surface. A worker is not one —\n\t\t// it names the process that runs a cron or a subscriber, and that process\n\t\t// is the app's server, which already exists. Everything else is a\n\t\t// resource one of them uses.\n\t\tif (declaration.kind !== 'site' && declaration.kind !== 'rest-api')\n\t\t\tcontinue;\n\n\t\t// No opt-in to be had. A site is an app and so is a surface, and each\n\t\t// says where it lives.\n\t\tconst spec: AppSpec =\n\t\t\tdeclaration.kind === 'site'\n\t\t\t\t? declaration.app\n\t\t\t\t: {\n\t\t\t\t\t\tpath: declaration.path,\n\t\t\t\t\t\t...(declaration.telescope ? { telescope: true } : {}),\n\t\t\t\t\t};\n\n\t\tconst name = appKey(id);\n\t\t// A config entry of the same name still wins, so a workspace can override\n\t\t// one field without restating the app. Nothing in this repo needs to.\n\t\tconst configured = workspace.apps[name];\n\n\t\tapps[name] = {\n\t\t\t...configured,\n\t\t\ttype: declaration.kind === 'site' ? 'web' : 'backend',\n\t\t\tpath: spec.path,\n\t\t\tport: spec.port ?? configured?.port ?? 0,\n\t\t\tdependencies: [],\n\t\t\tresolvedDeployTarget: configured?.resolvedDeployTarget ?? defaultTarget,\n\t\t\t...(declaration.kind === 'site'\n\t\t\t\t? {\n\t\t\t\t\t\tframework: configured?.framework ?? FRAMEWORKS[declaration.variant],\n\t\t\t\t\t\t...(declaration.root ? { root: true } : {}),\n\t\t\t\t\t\t...(spec.config ? { config: spec.config } : {}),\n\t\t\t\t\t}\n\t\t\t\t: {}),\n\t\t\t// One glob fans out to the six the build reads, because each generator\n\t\t\t// already inspects every export and keeps what it recognises. A\n\t\t\t// per-kind field still wins where one was given.\n\t\t\t...(spec.telescope !== undefined ? { telescope: spec.telescope } : {}),\n\t\t\t...(spec.studio !== undefined ? { studio: spec.studio } : {}),\n\t\t\t...(spec.openapi !== undefined ? { openapi: spec.openapi } : {}),\n\t\t\t...(spec.runtime !== undefined ? { runtime: spec.runtime } : {}),\n\t\t\t...(spec.env !== undefined ? { env: spec.env } : {}),\n\t\t\t...(spec.entry !== undefined ? { entry: spec.entry } : {}),\n\t\t\t...(configured?.domain\n\t\t\t\t? { domain: configured.domain as AppDomainConfig }\n\t\t\t\t: {}),\n\t\t} as NormalizedAppConfig;\n\t}\n\n\t// Build order, from the edges rather than from a hand-kept list. A site that\n\t// depends on a surface depends on the app that serves it — which for a\n\t// mounted auth server is its host, not a container that does not exist.\n\tfor (const [id, declaration] of Object.entries(manifest)) {\n\t\t// Two kinds are apps: a site and an HTTP surface. A worker is not one —\n\t\t// it names the process that runs a cron or a subscriber, and that process\n\t\t// is the app's server, which already exists. Everything else is a\n\t\t// resource one of them uses.\n\t\tif (declaration.kind !== 'site' && declaration.kind !== 'rest-api')\n\t\t\tcontinue;\n\n\t\tconst app = apps[appKey(id)];\n\t\tif (!app) continue;\n\n\t\t// A site names its edges `dependencies`; a surface names them `calls`.\n\t\tconst edges =\n\t\t\tdeclaration.kind === 'rest-api'\n\t\t\t\t? (declaration.calls ?? [])\n\t\t\t\t: (declaration.dependencies ?? []);\n\n\t\tconst names = new Set<string>();\n\t\tfor (const edge of edges) {\n\t\t\tif (edge.kind !== 'rest-api') continue;\n\t\t\tconst host = hostOf(manifest, edge.target);\n\t\t\tif (!host) continue;\n\t\t\tconst key = appKey(host);\n\t\t\tif (key !== appKey(id) && apps[key]) names.add(key);\n\t\t}\n\n\t\tapp.dependencies = [...names].sort();\n\t}\n\n\tmarkRoot(apps);\n\tassignPorts(apps);\n\n\t// Apps config declared that the graph knows nothing about — a `type: 'mobile'`\n\t// entry, or a hand-run process. Kept rather than dropped: config is still the\n\t// escape hatch for what nothing declares.\n\tfor (const [name, app] of Object.entries(workspace.apps)) {\n\t\tif (!apps[name]) apps[name] = app;\n\t}\n\n\treturn apps;\n}\n","import { existsSync, readFileSync } from 'node:fs';\nimport { dirname, join, parse } from 'node:path';\nimport { discover } from './reconcile/discover.js';\nimport type { GkmConfig } from './types.js';\nimport { derivedApps } from './workspace/derive.js';\nimport {\n\tallConstructGlobs,\n\tgetAppGkmConfig,\n\tisWorkspaceConfig,\n\ttype LoadedConfig,\n\ttype NormalizedAppConfig,\n\ttype NormalizedWorkspace,\n\tprocessConfig,\n\ttype WorkspaceConfig,\n} from './workspace/index.js';\n\nexport type { GkmConfig } from './types.js';\nexport type { LoadedConfig, WorkspaceConfig } from './workspace/index.js';\nexport { defineWorkspace } from './workspace/index.js';\n/**\n * Define GKM configuration with full TypeScript support.\n * This is an identity function that provides type safety and autocomplete.\n *\n * @example\n * ```ts\n * // gkm.config.ts\n * import { defineConfig } from '@geekmidas/cli/config';\n *\n * export default defineConfig({\n * routes: './src/endpoints/**\\/*.ts',\n * envParser: './src/config/env',\n * logger: './src/config/logger',\n * telescope: true,\n * });\n * ```\n */\nexport function defineConfig(config: GkmConfig): GkmConfig {\n\treturn config;\n}\n\nexport interface ParsedModuleConfig {\n\tpath: string;\n\timportPattern: string;\n}\n\n/**\n * Parse a module config string into path and import pattern.\n *\n * @param configString - Config string in format \"./path/to/module\" or \"./path/to/module#exportName\"\n * @param defaultAlias - The default alias name to use if no export name specified\n * @returns Object with path and import pattern\n *\n * @example\n * parseModuleConfig('./src/config/env', 'envParser')\n * // { path: './src/config/env', importPattern: 'envParser' }\n *\n * parseModuleConfig('./src/config/env#envParser', 'envParser')\n * // { path: './src/config/env', importPattern: '{ envParser }' }\n *\n * parseModuleConfig('./src/config/env#myEnv', 'envParser')\n * // { path: './src/config/env', importPattern: '{ myEnv as envParser }' }\n */\nexport function parseModuleConfig(\n\tconfigString: string,\n\tdefaultAlias: string,\n): ParsedModuleConfig {\n\tconst parts = configString.split('#');\n\tconst path = parts[0] ?? configString;\n\tconst exportName = parts[1];\n\tconst importPattern = !exportName\n\t\t? defaultAlias\n\t\t: exportName === defaultAlias\n\t\t\t? `{ ${defaultAlias} }`\n\t\t\t: `{ ${exportName} as ${defaultAlias} }`;\n\n\treturn { path, importPattern };\n}\n\nexport interface ConfigDiscoveryResult {\n\tconfigPath: string;\n\tworkspaceRoot: string;\n}\n\n/**\n * Find and return the path to the config file.\n *\n * Resolution order:\n * 1. GKM_CONFIG_PATH env var (set by workspace dev command)\n * 2. Walk up directory tree from cwd\n */\nfunction findConfigPath(cwd: string): ConfigDiscoveryResult {\n\tconst files = ['gkm.config.json', 'gkm.config.ts', 'gkm.config.js'];\n\n\t// Check GKM_CONFIG_PATH env var first (set by workspace dev command)\n\tconst envConfigPath = process.env.GKM_CONFIG_PATH;\n\tif (envConfigPath && existsSync(envConfigPath)) {\n\t\treturn {\n\t\t\tconfigPath: envConfigPath,\n\t\t\tworkspaceRoot: dirname(envConfigPath),\n\t\t};\n\t}\n\n\t// Walk up directory tree to find config\n\tlet currentDir = cwd;\n\tconst { root } = parse(currentDir);\n\n\twhile (currentDir !== root) {\n\t\tfor (const file of files) {\n\t\t\tconst configPath = join(currentDir, file);\n\t\t\tif (existsSync(configPath)) {\n\t\t\t\treturn {\n\t\t\t\t\tconfigPath,\n\t\t\t\t\tworkspaceRoot: currentDir,\n\t\t\t\t};\n\t\t\t}\n\t\t}\n\t\tcurrentDir = dirname(currentDir);\n\t}\n\n\tthrow new Error(\n\t\t'Configuration file not found. Please create gkm.config.json, gkm.config.ts, or gkm.config.js in the project root.',\n\t);\n}\n\n/**\n * Get app name from package.json in the given directory.\n * Handles scoped packages by extracting the name after the scope.\n *\n * @example\n * getAppNameFromCwd('/path/to/apps/api')\n * // package.json: { \"name\": \"@myorg/api\" }\n * // Returns: 'api'\n */\nexport function getAppNameFromCwd(cwd: string = process.cwd()): string | null {\n\tconst packageJsonPath = join(cwd, 'package.json');\n\n\tif (!existsSync(packageJsonPath)) {\n\t\treturn null;\n\t}\n\n\ttry {\n\t\tconst packageJson = JSON.parse(readFileSync(packageJsonPath, 'utf-8'));\n\t\tconst name = packageJson.name as string | undefined;\n\n\t\tif (!name) {\n\t\t\treturn null;\n\t\t}\n\n\t\t// Handle scoped packages: @scope/name -> name\n\t\tif (name.startsWith('@') && name.includes('/')) {\n\t\t\treturn name.split('/')[1] ?? null;\n\t\t}\n\n\t\treturn name;\n\t} catch {\n\t\treturn null;\n\t}\n}\n\ninterface RawConfigResult {\n\tconfig: GkmConfig | WorkspaceConfig;\n\tworkspaceRoot: string;\n}\n\n/**\n * Load raw configuration from file.\n */\nasync function loadRawConfig(cwd: string): Promise<RawConfigResult> {\n\tconst { configPath, workspaceRoot } = findConfigPath(cwd);\n\n\ttry {\n\t\tconst config = await import(configPath);\n\t\treturn {\n\t\t\tconfig: config.default,\n\t\t\tworkspaceRoot,\n\t\t};\n\t} catch (error) {\n\t\tthrow new Error(`Failed to load config: ${(error as Error).message}`);\n\t}\n}\n\n/**\n * Load configuration file (single-app format).\n * For backwards compatibility with existing code.\n *\n * @deprecated Use loadWorkspaceConfig for new code\n */\nexport async function loadConfig(\n\tcwd: string = process.cwd(),\n): Promise<GkmConfig> {\n\tconst { config } = await loadRawConfig(cwd);\n\n\t// If it's a workspace config, throw an error\n\tif (isWorkspaceConfig(config)) {\n\t\tthrow new Error(\n\t\t\t'Workspace configuration detected. Use loadWorkspaceConfig() instead.',\n\t\t);\n\t}\n\n\treturn config;\n}\n\n/**\n * Load configuration file and process it as a workspace.\n * Works with both single-app and workspace configurations.\n *\n * Single-app configs are automatically wrapped as a workspace with one app.\n *\n * @example\n * ```ts\n * const { type, workspace } = await loadWorkspaceConfig();\n *\n * if (type === 'workspace') {\n * console.log('Multi-app workspace:', workspace.apps);\n * } else {\n * console.log('Single app wrapped as workspace');\n * }\n * ```\n */\nexport async function loadWorkspaceConfig(\n\tcwd: string = process.cwd(),\n): Promise<LoadedConfig> {\n\tconst { config, workspaceRoot } = await loadRawConfig(cwd);\n\treturn withDerivedApps(processConfig(config, workspaceRoot));\n}\n\n/**\n * The loaded config, with its apps read off the graph.\n *\n * Here rather than at each call site because every command needs the same\n * answer, and the ones that forgot to ask were exactly the bugs: a site that\n * never deployed, a surface that could not be its own process. `gkm dev`,\n * `gkm build`, `gkm docker` and `gkm deploy` now all see the same apps without\n * any of them knowing a derivation happened.\n *\n * Discovery imports the construct modules — declarations only, not endpoints —\n * which is the same import the reconciler and the build already do. A workspace\n * with no `constructs` glob discovers nothing and keeps whatever its config\n * said, so nothing that worked before changes.\n *\n * A construct file that throws is reported and skipped rather than taking the\n * command down: `gkm dev` failing to start because a half-written declaration\n * cannot be imported is a worse failure than starting with the app it does not\n * describe yet.\n */\nasync function withDerivedApps(loaded: LoadedConfig): Promise<LoadedConfig> {\n\tconst globs = allConstructGlobs(loaded.workspace);\n\tif (globs.length === 0) return loaded;\n\n\ttry {\n\t\tconst manifest = await discover({\n\t\t\tpatterns: globs,\n\t\t\tcwd: loaded.workspace.root,\n\t\t});\n\n\t\treturn {\n\t\t\t...loaded,\n\t\t\tmanifest,\n\t\t\tworkspace: {\n\t\t\t\t...loaded.workspace,\n\t\t\t\tapps: derivedApps(manifest, loaded.workspace),\n\t\t\t},\n\t\t};\n\t} catch (error) {\n\t\tconst message = error instanceof Error ? error.message : String(error);\n\t\tconsole.warn(\n\t\t\t`⚠️ Could not read constructs, so apps come from config alone: ${message}`,\n\t\t);\n\t\treturn loaded;\n\t}\n}\n\nexport interface AppConfigResult {\n\tappName: string;\n\tapp: NormalizedAppConfig;\n\tgkmConfig: GkmConfig;\n\tworkspace: NormalizedWorkspace;\n\tworkspaceRoot: string;\n\tappRoot: string;\n}\n\n/**\n * The app a package directory refers to, and what the workspace calls it.\n *\n * A single-app config is wrapped as a one-app workspace keyed `api`, which is\n * almost never the package name. Matching on the key alone means an app like\n * `@geekmidas/example` resolves to nothing and silently loses everything that\n * comes from the workspace — including the URLs its constructs declare.\n */\nfunction resolveWorkspaceApp(\n\tloadedConfig: LoadedConfig,\n\tappName: string,\n): { key: string; app: NormalizedAppConfig } | undefined {\n\tconst apps = loadedConfig.workspace.apps;\n\n\tconst named = apps[appName];\n\tif (named) return { key: appName, app: named };\n\n\tconst names = Object.keys(apps);\n\tconst only = names[0];\n\tif (loadedConfig.type !== 'single' || names.length !== 1 || !only) {\n\t\treturn undefined;\n\t}\n\n\tconst app = apps[only];\n\n\treturn app ? { key: only, app } : undefined;\n}\n\n/**\n * Load app-specific configuration from workspace.\n * Uses the app name from package.json to find the correct app config.\n *\n * @example\n * ```ts\n * // From apps/api directory with package.json: { \"name\": \"@myorg/api\" }\n * const { app, workspace, workspaceRoot } = await loadAppConfig();\n * console.log(app.routes); // './src/endpoints/**\\/*.ts'\n * ```\n */\nexport async function loadAppConfig(\n\tcwd: string = process.cwd(),\n): Promise<AppConfigResult> {\n\tconst appName = getAppNameFromCwd(cwd);\n\n\tif (!appName) {\n\t\tthrow new Error(\n\t\t\t'Could not determine app name. Ensure package.json exists with a \"name\" field.',\n\t\t);\n\t}\n\n\tconst { config, workspaceRoot } = await loadRawConfig(cwd);\n\tconst loadedConfig = await withDerivedApps(\n\t\tprocessConfig(config, workspaceRoot),\n\t);\n\n\tconst resolved = resolveWorkspaceApp(loadedConfig, appName);\n\n\tif (!resolved) {\n\t\tthrow new Error(\n\t\t\t`App \"${appName}\" not found in workspace config. Available apps: ${Object.keys(\n\t\t\t\tloadedConfig.workspace.apps,\n\t\t\t).join(', ')}. ` +\n\t\t\t\t`Ensure the package.json name matches the app key in gkm.config.ts.`,\n\t\t);\n\t}\n\n\tconst { key, app } = resolved;\n\n\t// Keyed by what the workspace calls the app, not by the package name — for a\n\t// single-app config those differ.\n\tconst gkmConfig = getAppGkmConfig(loadedConfig.workspace, key);\n\n\tif (!gkmConfig) {\n\t\tthrow new Error(\n\t\t\t`App \"${appName}\" is not a backend app and cannot be run with gkm dev.`,\n\t\t);\n\t}\n\n\treturn {\n\t\tappName,\n\t\tapp,\n\t\tgkmConfig,\n\t\tworkspace: loadedConfig.workspace,\n\t\tworkspaceRoot,\n\t\tappRoot: join(workspaceRoot, app.path),\n\t};\n}\n\nexport interface WorkspaceAppInfo {\n\tappName: string;\n\tapp: NormalizedAppConfig;\n\tworkspace: NormalizedWorkspace;\n\tworkspaceRoot: string;\n}\n\n/**\n * Load workspace info for any app (frontend or backend).\n * Unlike loadAppConfig, this does NOT require the app to have a gkm config\n * (routes, entry, etc.), making it suitable for gkm exec/test from frontend apps.\n */\nexport async function loadWorkspaceAppInfo(\n\tcwd: string = process.cwd(),\n): Promise<WorkspaceAppInfo> {\n\tconst appName = getAppNameFromCwd(cwd);\n\n\tif (!appName) {\n\t\tthrow new Error(\n\t\t\t'Could not determine app name. Ensure package.json exists with a \"name\" field.',\n\t\t);\n\t}\n\n\tconst { config, workspaceRoot } = await loadRawConfig(cwd);\n\tconst loadedConfig = await withDerivedApps(\n\t\tprocessConfig(config, workspaceRoot),\n\t);\n\n\tconst resolved = resolveWorkspaceApp(loadedConfig, appName);\n\n\tif (!resolved) {\n\t\tconst availableApps = Object.keys(loadedConfig.workspace.apps).join(', ');\n\t\tthrow new Error(\n\t\t\t`App \"${appName}\" not found in workspace config. Available apps: ${availableApps}. ` +\n\t\t\t\t`Ensure the package.json name matches the app key in gkm.config.ts.`,\n\t\t);\n\t}\n\n\treturn {\n\t\tappName,\n\t\tapp: resolved.app,\n\t\tworkspace: loadedConfig.workspace,\n\t\tworkspaceRoot,\n\t};\n}\n"],"mappings":"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"}
|
package/dist/config.cjs
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
require('./discover-DTmFeloL.cjs');
|
|
2
|
-
const require_config = require('./config-
|
|
3
|
-
const require_workspace = require('./workspace-
|
|
2
|
+
const require_config = require('./config-BZ1F0WuA.cjs');
|
|
3
|
+
const require_workspace = require('./workspace-C6VJQVtG.cjs');
|
|
4
4
|
|
|
5
5
|
exports.defineConfig = require_config.defineConfig;
|
|
6
6
|
exports.defineWorkspace = require_workspace.defineWorkspace;
|
package/dist/config.d.cts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { GkmConfig, LoadedConfig, NormalizedAppConfig, NormalizedWorkspace, WorkspaceConfig } from "./types-
|
|
2
|
-
import { defineWorkspace } from "./index-
|
|
1
|
+
import { GkmConfig, LoadedConfig, NormalizedAppConfig, NormalizedWorkspace, WorkspaceConfig } from "./types-Y3fKQSAK.cjs";
|
|
2
|
+
import { defineWorkspace } from "./index-CR5urhWX.cjs";
|
|
3
3
|
|
|
4
4
|
//#region src/config.d.ts
|
|
5
5
|
|
package/dist/config.d.mts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { GkmConfig, LoadedConfig, NormalizedAppConfig, NormalizedWorkspace, WorkspaceConfig } from "./types-
|
|
2
|
-
import { defineWorkspace } from "./index-
|
|
1
|
+
import { GkmConfig, LoadedConfig, NormalizedAppConfig, NormalizedWorkspace, WorkspaceConfig } from "./types-6pkp2RRj.mjs";
|
|
2
|
+
import { defineWorkspace } from "./index-C6e-ULLx.mjs";
|
|
3
3
|
|
|
4
4
|
//#region src/config.d.ts
|
|
5
5
|
|
package/dist/config.mjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import "./discover-CAlFgYec.mjs";
|
|
2
|
-
import { defineConfig, getAppNameFromCwd, loadAppConfig, loadConfig, loadWorkspaceAppInfo, loadWorkspaceConfig, parseModuleConfig } from "./config-
|
|
3
|
-
import { defineWorkspace } from "./workspace-
|
|
2
|
+
import { defineConfig, getAppNameFromCwd, loadAppConfig, loadConfig, loadWorkspaceAppInfo, loadWorkspaceConfig, parseModuleConfig } from "./config-DVVph9yz.mjs";
|
|
3
|
+
import { defineWorkspace } from "./workspace-zoqRbihZ.mjs";
|
|
4
4
|
|
|
5
5
|
export { defineConfig, defineWorkspace, getAppNameFromCwd, loadAppConfig, loadConfig, loadWorkspaceAppInfo, loadWorkspaceConfig, parseModuleConfig };
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
const require_chunk = require('./chunk-CUT6urMc.cjs');
|
|
2
2
|
const require_discover = require('./discover-DTmFeloL.cjs');
|
|
3
|
-
const require_config = require('./config-
|
|
4
|
-
const require_workspace = require('./workspace-
|
|
3
|
+
const require_config = require('./config-BZ1F0WuA.cjs');
|
|
4
|
+
const require_workspace = require('./workspace-C6VJQVtG.cjs');
|
|
5
5
|
const require_generator = require('./generator-CNzBw7g5.cjs');
|
|
6
6
|
const require_storage = require('./storage-BJmzYxt8.cjs');
|
|
7
7
|
const node_fs = require_chunk.__toESM(require("node:fs"));
|
|
@@ -796,8 +796,6 @@ const dockerCli = {
|
|
|
796
796
|
|
|
797
797
|
//#endregion
|
|
798
798
|
//#region src/reconcile/plan.ts
|
|
799
|
-
/** The stage whose resources carry no suffix. */
|
|
800
|
-
const DEFAULT_STAGE = "development";
|
|
801
799
|
/**
|
|
802
800
|
* Which container serves a kind.
|
|
803
801
|
*
|
|
@@ -916,13 +914,13 @@ const SEPARATORS = {
|
|
|
916
914
|
/**
|
|
917
915
|
* The stage-scoped name for a resource.
|
|
918
916
|
*
|
|
919
|
-
* The
|
|
920
|
-
* developer's psql history, saved connections, and muscle memory survive
|
|
921
|
-
* change. Every other stage is suffixed.
|
|
917
|
+
* The local stage carries no suffix, so `gkm dev` keeps using `orders` and a
|
|
918
|
+
* developer's psql history, saved connections, and muscle memory survive a
|
|
919
|
+
* change of stage name. Every other stage is suffixed.
|
|
922
920
|
*/
|
|
923
|
-
function resourceName(id, kind, stage) {
|
|
921
|
+
function resourceName(id, kind, stage, localStage) {
|
|
924
922
|
const base = id.toLowerCase();
|
|
925
|
-
if (stage ===
|
|
923
|
+
if (stage === localStage) return base;
|
|
926
924
|
return `${base}${SEPARATORS[kind] ?? "-"}${stage}`;
|
|
927
925
|
}
|
|
928
926
|
/**
|
|
@@ -966,7 +964,7 @@ function planFor(manifest, stage, order, options = {}) {
|
|
|
966
964
|
id,
|
|
967
965
|
kind: declaration.kind,
|
|
968
966
|
...container ? { container } : {},
|
|
969
|
-
name: resourceName(id, declaration.kind, stage),
|
|
967
|
+
name: resourceName(id, declaration.kind, stage, options.localStage),
|
|
970
968
|
envKey: (0, __geekmidas_manifest.providedKeyFor)(id, declaration.kind, ROLES[declaration.kind] ?? "url"),
|
|
971
969
|
provisions: PROVISIONS[declaration.kind] === true,
|
|
972
970
|
...declaration.kind === "rest-api" ? { callers: (0, __geekmidas_manifest.dependentsOf)(manifest, id) } : {},
|
|
@@ -1841,6 +1839,7 @@ const CADDY_ROOT_IN_CONTAINER = "/data/caddy/pki/authorities/local/root.crt";
|
|
|
1841
1839
|
async function reconcile(options) {
|
|
1842
1840
|
const { root, project, manifest, stage, start = true, provision = true, docker = dockerCli, probe = isPortFree, sql = pgClient, buckets = bucketClient } = options;
|
|
1843
1841
|
const plan = planFor(manifest, stage, (0, __geekmidas_manifest.provisionOrder)(manifest), {
|
|
1842
|
+
localStage: options.localStage,
|
|
1844
1843
|
events: options.events,
|
|
1845
1844
|
cache: options.cache,
|
|
1846
1845
|
extraContainers: options.extraContainers,
|
|
@@ -2000,6 +1999,7 @@ async function derivedContainers(workspace, stage, manifest) {
|
|
|
2000
1999
|
cwd: workspace.root
|
|
2001
2000
|
});
|
|
2002
2001
|
return planFor(found, stage, (0, __geekmidas_manifest.provisionOrder)(found), {
|
|
2002
|
+
localStage: workspace.stages.local,
|
|
2003
2003
|
events: workspace.services.events,
|
|
2004
2004
|
cache: cacheBackendOf(workspace.services.cache, providerOf(workspace))
|
|
2005
2005
|
}).containers;
|
|
@@ -2020,6 +2020,7 @@ async function reconcileWorkspace(workspace, options) {
|
|
|
2020
2020
|
project: workspace.name,
|
|
2021
2021
|
manifest,
|
|
2022
2022
|
stage: options.stage,
|
|
2023
|
+
localStage: workspace.stages.local,
|
|
2023
2024
|
events: workspace.services.events,
|
|
2024
2025
|
cache: cacheBackendOf(workspace.services.cache, providerOf(workspace)),
|
|
2025
2026
|
images: imagePins(workspace),
|
|
@@ -2359,14 +2360,13 @@ async function startWorkspaceServices(workspace, portEnv, secretsEnv) {
|
|
|
2359
2360
|
* For workspace app: maps {APP}_DATABASE_URL → DATABASE_URL.
|
|
2360
2361
|
* @internal Exported for testing
|
|
2361
2362
|
*/
|
|
2362
|
-
async function loadSecretsForApp(secretsRoot,
|
|
2363
|
+
async function loadSecretsForApp(secretsRoot, stage, appName) {
|
|
2363
2364
|
let secrets = {};
|
|
2364
|
-
|
|
2365
|
+
if (require_storage.secretsExist(stage, secretsRoot)) {
|
|
2365
2366
|
const stageSecrets = await require_storage.readStageSecrets(stage, secretsRoot);
|
|
2366
2367
|
if (stageSecrets) {
|
|
2367
2368
|
logger.log(`🔐 Loading secrets from stage: ${stage}`);
|
|
2368
2369
|
secrets = require_storage.toEmbeddableSecrets(stageSecrets);
|
|
2369
|
-
break;
|
|
2370
2370
|
}
|
|
2371
2371
|
}
|
|
2372
2372
|
if (Object.keys(secrets).length === 0) return {};
|
|
@@ -2447,7 +2447,7 @@ await import('${entryPath}');
|
|
|
2447
2447
|
* @param options.resolveDockerPorts - How to resolve Docker ports:
|
|
2448
2448
|
* - `'full'` (default): probe running containers, saved state, then find available ports. Used by dev/test.
|
|
2449
2449
|
* - `'readonly'`: check running containers and saved state only, never probe for new ports. Used by exec.
|
|
2450
|
-
* @param options.
|
|
2450
|
+
* @param options.stage - The stage whose secrets to load. Default: the project's `stages.local`.
|
|
2451
2451
|
* @param options.startDocker - Start Docker Compose services after port resolution. Default: false.
|
|
2452
2452
|
* @param options.secretsFileName - Custom secrets JSON filename. Default: 'dev-secrets-{appName}.json' or 'dev-secrets.json'.
|
|
2453
2453
|
* @internal Exported for testing
|
|
@@ -2469,13 +2469,14 @@ async function prepareEntryCredentials(options) {
|
|
|
2469
2469
|
appName = require_config.getAppNameFromCwd(cwd) ?? void 0;
|
|
2470
2470
|
}
|
|
2471
2471
|
const resolvedPort = options.explicitPort ?? workspaceAppPort ?? 3e3;
|
|
2472
|
-
const
|
|
2472
|
+
const stage = options.stage ?? appInfo?.workspace.stages.local;
|
|
2473
|
+
const credentials = stage ? await loadSecretsForApp(secretsRoot, stage, appName) : {};
|
|
2473
2474
|
credentials.PORT = String(resolvedPort);
|
|
2474
2475
|
const declaredKeys = [];
|
|
2475
2476
|
if (appInfo && usesConstructs(appInfo.workspace)) {
|
|
2476
|
-
const { reconcileWorkspace: reconcileWorkspace$1 } = await Promise.resolve().then(() => require("./workspace-
|
|
2477
|
+
const { reconcileWorkspace: reconcileWorkspace$1 } = await Promise.resolve().then(() => require("./workspace-XXtNhqJl.cjs"));
|
|
2477
2478
|
const reconciled = await reconcileWorkspace$1(appInfo.workspace, {
|
|
2478
|
-
stage: options.reconcileStage ??
|
|
2479
|
+
stage: options.reconcileStage ?? appInfo.workspace.stages.local,
|
|
2479
2480
|
start: options.startDocker ?? false
|
|
2480
2481
|
});
|
|
2481
2482
|
Object.assign(credentials, reconciled.env);
|
|
@@ -2677,4 +2678,4 @@ Object.defineProperty(exports, 'usesConstructs', {
|
|
|
2677
2678
|
return usesConstructs;
|
|
2678
2679
|
}
|
|
2679
2680
|
});
|
|
2680
|
-
//# sourceMappingURL=credentials-
|
|
2681
|
+
//# sourceMappingURL=credentials-Xamqe_NJ.cjs.map
|