@geekmidas/cli 10.0.0-alpha.48 → 10.0.0-alpha.49

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/openapi.cjs CHANGED
@@ -9,18 +9,15 @@ let _geekmidas_manifest = require("@geekmidas/manifest");
9
9
  let node_url = require("node:url");
10
10
  //#region src/openapi.ts
11
11
  /**
12
- * Default output path for generated OpenAPI client (used for single-app configs)
13
- */
14
- const OPENAPI_OUTPUT_PATH = "./.gkm/openapi.ts";
15
- /**
16
- * Where a surface's generated client lands.
12
+ * Where a surface's generated client lands, under the workspace root.
17
13
  *
18
14
  * Named from the surface, so `new RestApi('Webhooks')` writes
19
- * `.gkm/openapi/webhooks.ts` — the same kebab form that gives it its container
20
- * name and its `WEBHOOKS_URL`.
15
+ * `.gkm/client/webhooks.ts` — the same kebab form that gives it its container
16
+ * name and its `WEBHOOKS_URL` — and a site imports it as
17
+ * `@<name>/client/webhooks`, through the alias its tsconfig maps.
21
18
  */
22
19
  function openApiPathFor(surfaceId) {
23
- return `./.gkm/openapi/${(0, _geekmidas_manifest.kebabCase)(surfaceId)}.ts`;
20
+ return `./.gkm/client/${(0, _geekmidas_manifest.kebabCase)(surfaceId)}.ts`;
24
21
  }
25
22
  /**
26
23
  * Resolve OpenAPI config from GkmConfig
@@ -62,6 +59,7 @@ async function generateOpenApiFrom(routes, options = {}) {
62
59
  * @returns What was written, or null if disabled or there were none
63
60
  */
64
61
  async function generateOpenApi(endpoints, options = {}) {
62
+ const root = options.root ?? process.cwd();
65
63
  const logger = options.silent ? { log: () => {} } : console;
66
64
  const openApiConfig = resolveOpenApiConfig({ openapi: options.openapi });
67
65
  if (!openApiConfig.enabled) return null;
@@ -85,7 +83,7 @@ async function generateOpenApi(endpoints, options = {}) {
85
83
  const written = [];
86
84
  for (const [surfaceId, surfaceEndpoints] of bySurface) {
87
85
  const relative = openApiPathFor(surfaceId);
88
- const outputPath = (0, node_path.join)(process.cwd(), relative);
86
+ const outputPath = (0, node_path.join)(root, relative);
89
87
  await (0, node_fs_promises.mkdir)((0, node_path.dirname)(outputPath), { recursive: true });
90
88
  const tsContent = await tsGenerator.generate(surfaceEndpoints, {
91
89
  title: openApiConfig.title ?? surfaceId,
@@ -94,11 +92,11 @@ async function generateOpenApi(endpoints, options = {}) {
94
92
  });
95
93
  await (0, node_fs_promises.writeFile)(outputPath, tsContent);
96
94
  written.push(relative);
97
- logger.log(`📄 OpenAPI client generated: ${relative} (${surfaceEndpoints.length} endpoints)`);
95
+ logger.log(`📄 OpenAPI client generated: ${(0, node_path.relative)(process.cwd(), outputPath)} (${surfaceEndpoints.length} endpoints)`);
98
96
  }
99
97
  return {
100
- outputPath: (0, node_path.join)(process.cwd(), written[0]),
101
- outputPaths: written.map((r) => (0, node_path.join)(process.cwd(), r)),
98
+ outputPath: (0, node_path.join)(root, written[0]),
99
+ outputPaths: written.map((r) => (0, node_path.join)(root, r)),
102
100
  endpointCount: endpoints.length
103
101
  };
104
102
  }
@@ -109,7 +107,10 @@ async function openapiCommand(options = {}) {
109
107
  if (loadedConfig.type === "single") {
110
108
  const config = loadedConfig.raw;
111
109
  if (!config.openapi) config.openapi = { enabled: true };
112
- const result = await generateOpenApiFrom(config.constructs, { openapi: config.openapi });
110
+ const result = await generateOpenApiFrom(config.constructs, {
111
+ openapi: config.openapi,
112
+ root: loadedConfig.workspace.root
113
+ });
113
114
  if (result) logger.log(`Found ${result.endpointCount} endpoints`);
114
115
  } else {
115
116
  const { workspace } = loadedConfig;
@@ -151,7 +152,8 @@ async function generateOpenApiForApp(workspaceRoot, _appName, app, silent = true
151
152
  const appPath = (0, node_path.join)(workspaceRoot, app.path);
152
153
  return generateOpenApiFrom([(0, node_path.join)(appPath, "**/*.ts")], {
153
154
  openapi: app.openapi ?? { enabled: true },
154
- silent
155
+ silent,
156
+ root: workspaceRoot
155
157
  });
156
158
  }
157
159
  /**
@@ -189,7 +191,6 @@ async function runOpenApiInSubprocess(appCwd, appName) {
189
191
  });
190
192
  }
191
193
  //#endregion
192
- exports.OPENAPI_OUTPUT_PATH = OPENAPI_OUTPUT_PATH;
193
194
  exports.generateOpenApi = generateOpenApi;
194
195
  exports.generateOpenApiFrom = generateOpenApiFrom;
195
196
  exports.openApiPathFor = openApiPathFor;
@@ -1 +1 @@
1
- {"version":3,"file":"openapi.cjs","names":["kebabCase","EndpointGenerator","OpenApiTsGenerator","join","mkdir","dirname","writeFile","loadWorkspaceConfig","resolve","fileURLToPath","spawn"],"sources":["../src/openapi.ts"],"sourcesContent":["#!/usr/bin/env -S npx tsx\n\nimport { spawn } from 'node:child_process';\nimport { mkdir, writeFile } from 'node:fs/promises';\nimport { dirname, join, resolve } from 'node:path';\nimport { fileURLToPath } from 'node:url';\nimport type { Endpoint } from '@geekmidas/constructs/endpoints';\nimport { kebabCase } from '@geekmidas/manifest';\nimport { loadWorkspaceConfig } from './config.js';\nimport { EndpointGenerator } from './generators/EndpointGenerator.js';\nimport { OpenApiTsGenerator } from './generators/OpenApiTsGenerator.js';\nimport type { GkmConfig, OpenApiConfig, Routes } from './types.js';\nimport { normalizeRoutes } from './workspace/client-generator.js';\nimport type { NormalizedAppConfig } from './workspace/types.js';\n\ninterface OpenAPIOptions {\n\tcwd?: string;\n\t/**\n\t * Workspace mode: generate for a single named backend app. When set,\n\t * generation runs in-process against the current CWD. When unset,\n\t * workspace mode spawns one subprocess per backend app so each gets a\n\t * fresh tsx that loads the app's own tsconfig (path aliases included).\n\t */\n\tapp?: string;\n}\n\n/**\n * Default output path for generated OpenAPI client (used for single-app configs)\n */\nexport const OPENAPI_OUTPUT_PATH = './.gkm/openapi.ts';\n\n/**\n * Where a surface's generated client lands.\n *\n * Named from the surface, so `new RestApi('Webhooks')` writes\n * `.gkm/openapi/webhooks.ts` — the same kebab form that gives it its container\n * name and its `WEBHOOKS_URL`.\n */\nexport function openApiPathFor(surfaceId: string): string {\n\treturn `./.gkm/openapi/${kebabCase(surfaceId)}.ts`;\n}\n\n/**\n * Resolve OpenAPI config from GkmConfig\n */\nexport function resolveOpenApiConfig(config: {\n\topenapi?: boolean | OpenApiConfig;\n}): OpenApiConfig & { enabled: boolean } {\n\tif (config.openapi === false) {\n\t\treturn { enabled: false };\n\t}\n\n\tif (config.openapi === true || config.openapi === undefined) {\n\t\t// Enable by default when not explicitly set (undefined) or explicitly true\n\t\treturn {\n\t\t\tenabled: true,\n\t\t\ttitle: 'API Documentation',\n\t\t\tversion: '1.0.0',\n\t\t\tdescription: 'Auto-generated API documentation from endpoints',\n\t\t};\n\t}\n\n\treturn {\n\t\tenabled: config.openapi.enabled !== false,\n\t\ttitle: config.openapi.title || 'API Documentation',\n\t\tversion: config.openapi.version || '1.0.0',\n\t\tdescription:\n\t\t\tconfig.openapi.description ||\n\t\t\t'Auto-generated API documentation from endpoints',\n\t};\n}\n\n/** What a generation run wrote. */\nexport interface OpenApiResult {\n\t/** The first spec written — kept so existing callers still compile. */\n\toutputPath: string;\n\t/** Every spec written, one per surface. */\n\toutputPaths: string[];\n\t/** Endpoints across all of them. */\n\tendpointCount: number;\n}\n\n/**\n * Load endpoints from a glob, then generate.\n *\n * For the callers that have no endpoints in hand — `gkm openapi`, and `gkm dev`\n * regenerating on reload. The glob is the app's derived one — the conventional\n * directories under it — not a `routes` field anybody wrote.\n */\nexport async function generateOpenApiFrom(\n\troutes: Routes,\n\toptions: {\n\t\topenapi?: boolean | OpenApiConfig;\n\t\tsilent?: boolean;\n\t\tbustCache?: boolean;\n\t} = {},\n): Promise<OpenApiResult | null> {\n\tconst loaded = await new EndpointGenerator().load(\n\t\troutes,\n\t\tundefined,\n\t\toptions.bustCache,\n\t);\n\n\treturn generateOpenApi(\n\t\tloaded.map(({ construct }) => construct),\n\t\toptions,\n\t);\n}\n\n/**\n * Generate an OpenAPI spec for each surface these endpoints name.\n *\n * Takes the endpoints, not a glob to find them with. The build has already\n * loaded every one of them to generate handlers, so globbing again was a second\n * discovery pass over the same files — and the reason this function needed a\n * `routes` to be told where to look, which is how a retired config field stayed\n * load-bearing.\n *\n * @returns What was written, or null if disabled or there were none\n */\nexport async function generateOpenApi(\n\tendpoints: readonly Endpoint<any, any, any, any, any>[],\n\toptions: {\n\t\topenapi?: boolean | OpenApiConfig;\n\t\tsilent?: boolean;\n\t} = {},\n): Promise<OpenApiResult | null> {\n\tconst logger = options.silent ? { log: () => {} } : console;\n\tconst openApiConfig = resolveOpenApiConfig({ openapi: options.openapi });\n\n\tif (!openApiConfig.enabled) {\n\t\treturn null;\n\t}\n\n\tif (endpoints.length === 0) {\n\t\tlogger.log('No valid endpoints found for OpenAPI generation');\n\t\treturn null;\n\t}\n\n\t// One spec per surface, not one per glob.\n\t//\n\t// The glob is a directory, and a directory is not a surface: two `RestApi`s\n\t// whose endpoints live side by side produced a single spec describing both,\n\t// under a filename that named neither. Every endpoint carries the surface\n\t// that serves it, so that is what the spec is cut along — `Api` becomes\n\t// `api.ts`, `Webhooks` becomes `webhooks.ts`, by the same kebab rule that\n\t// gives the surface its container name and its environment keys.\n\tconst bySurface = new Map<string, Endpoint<any, any, any, any, any>[]>();\n\tfor (const endpoint of endpoints) {\n\t\tconst owner = endpoint.owner ?? endpoint.surface?.id;\n\t\tif (!owner) continue;\n\t\tconst existing = bySurface.get(owner);\n\t\tif (existing) existing.push(endpoint);\n\t\telse bySurface.set(owner, [endpoint]);\n\t}\n\n\tif (bySurface.size === 0) {\n\t\tlogger.log('No endpoints named a surface, so no OpenAPI was generated');\n\t\treturn null;\n\t}\n\n\tconst tsGenerator = new OpenApiTsGenerator();\n\tconst written: string[] = [];\n\n\tfor (const [surfaceId, surfaceEndpoints] of bySurface) {\n\t\tconst relative = openApiPathFor(surfaceId);\n\t\tconst outputPath = join(process.cwd(), relative);\n\n\t\tawait mkdir(dirname(outputPath), { recursive: true });\n\n\t\tconst tsContent = await tsGenerator.generate(surfaceEndpoints, {\n\t\t\t// The surface's own name, so a spec says which API it describes\n\t\t\t// rather than repeating one title across all of them.\n\t\t\ttitle: openApiConfig.title ?? surfaceId,\n\t\t\tversion: openApiConfig.version!,\n\t\t\tdescription: openApiConfig.description!,\n\t\t});\n\n\t\tawait writeFile(outputPath, tsContent);\n\t\twritten.push(relative);\n\t\tlogger.log(\n\t\t\t`📄 OpenAPI client generated: ${relative} (${surfaceEndpoints.length} endpoints)`,\n\t\t);\n\t}\n\n\treturn {\n\t\t// The first, for a caller that wants one path. `outputPaths` has them all.\n\t\toutputPath: join(process.cwd(), written[0]!),\n\t\toutputPaths: written.map((r) => join(process.cwd(), r)),\n\t\tendpointCount: endpoints.length,\n\t};\n}\n\nexport async function openapiCommand(\n\toptions: OpenAPIOptions = {},\n): Promise<void> {\n\tconst logger = console;\n\n\ttry {\n\t\tconst loadedConfig = await loadWorkspaceConfig(options.cwd);\n\n\t\tif (loadedConfig.type === 'single') {\n\t\t\t// Single-app config - use existing behavior\n\t\t\tconst config = loadedConfig.raw as GkmConfig;\n\n\t\t\t// Enable openapi if not configured\n\t\t\tif (!config.openapi) {\n\t\t\t\tconfig.openapi = { enabled: true };\n\t\t\t}\n\n\t\t\tconst result = await generateOpenApiFrom(config.constructs, {\n\t\t\t\topenapi: config.openapi,\n\t\t\t});\n\n\t\t\tif (result) {\n\t\t\t\tlogger.log(`Found ${result.endpointCount} endpoints`);\n\t\t\t}\n\t\t} else {\n\t\t\t// Workspace config - generate for each backend app\n\t\t\tconst { workspace } = loadedConfig;\n\t\t\t// Always derive the workspace root from the loaded config rather than\n\t\t\t// CWD. Subprocesses run with CWD set to an app directory, and falling\n\t\t\t// back to CWD there causes `join(workspaceRoot, app.path)` to point at\n\t\t\t// a non-existent nested path (e.g. `apps/api/apps/api`).\n\t\t\tconst workspaceRoot = workspace.root;\n\n\t\t\t// Backend apps with openapi enabled — which is all of them that have\n\t\t\t// routes, unless one says otherwise.\n\t\t\t//\n\t\t\t// This used to require `openapi: true` written out, while the app's\n\t\t\t// own build generated a spec regardless. Two paths disagreeing about\n\t\t\t// the same flag: the app emitted `.gkm/openapi.ts` and this pass,\n\t\t\t// finding nothing \"enabled\", skipped copying it to the frontends that\n\t\t\t// consume it. A surface whose routes are declared rather than\n\t\t\t// discovered has no spec to generate, so it is not a candidate.\n\t\t\tconst backendApps = Object.entries(workspace.apps).filter(\n\t\t\t\t([_, app]) =>\n\t\t\t\t\tapp.type === 'backend' &&\n\t\t\t\t\tapp.openapi !== false &&\n\t\t\t\t\t(typeof app.openapi !== 'object' || app.openapi.enabled !== false),\n\t\t\t);\n\n\t\t\tif (backendApps.length === 0) {\n\t\t\t\tlogger.log('No backend apps with OpenAPI enabled found');\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\t// Single-app mode: generate in-process. The caller is expected to\n\t\t\t// have CWD set to the app's directory (e.g., when invoked as a\n\t\t\t// subprocess), so tsx picks up the app's tsconfig path aliases.\n\t\t\tif (options.app) {\n\t\t\t\tconst entry = backendApps.find(([name]) => name === options.app);\n\t\t\t\tif (!entry) {\n\t\t\t\t\tthrow new Error(\n\t\t\t\t\t\t`App \"${options.app}\" not found or has OpenAPI disabled`,\n\t\t\t\t\t);\n\t\t\t\t}\n\t\t\t\tconst [appName, app] = entry;\n\t\t\t\tconst result = await generateOpenApiForApp(workspaceRoot, appName, app);\n\t\t\t\tif (result) {\n\t\t\t\t\tlogger.log(\n\t\t\t\t\t\t`📄 [${appName}] Generated OpenAPI (${result.endpointCount} endpoints)`,\n\t\t\t\t\t);\n\t\t\t\t}\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\t// Multi-app mode: spawn a subprocess per backend app. Each\n\t\t\t// subprocess starts with CWD at the app's directory so that\n\t\t\t// tsx's tsconfig discovery picks up the app's `paths` aliases\n\t\t\t// (e.g., `~/*`) instead of the workspace root's tsconfig.\n\t\t\tfor (const [appName, app] of backendApps) {\n\t\t\t\t// No `app.routes` check any more: the per-kind globs are gone, and\n\t\t\t\t// an app's code is found by one glob rather than by a field naming\n\t\t\t\t// endpoints. Guarding on `routes` here meant every app was skipped\n\t\t\t\t// the moment that field stopped existing — silently, because a loop\n\t\t\t\t// that runs zero times looks exactly like one with nothing to do.\n\t\t\t\tconst appPath = join(workspaceRoot, app.path);\n\n\t\t\t\t// A subprocess exists to put CWD inside the app, so tsx picks up\n\t\t\t\t// that app's tsconfig aliases instead of the workspace root's. When\n\t\t\t\t// the app *is* the root there is nothing to change: spawning would\n\t\t\t\t// re-enter the same directory, having paid for a process and a\n\t\t\t\t// module graph, and needing `tsx` resolvable from it.\n\t\t\t\tif (resolve(appPath) === resolve(workspaceRoot)) {\n\t\t\t\t\t// Not silent: silence is for a subprocess whose output the\n\t\t\t\t\t// parent relays, and there is no parent here to relay it. The\n\t\t\t\t\t// per-surface lines come from the generator; the count is this\n\t\t\t\t\t// command's own summary, as in the single-app path.\n\t\t\t\t\tconst result = await generateOpenApiForApp(\n\t\t\t\t\t\tworkspaceRoot,\n\t\t\t\t\t\tappName,\n\t\t\t\t\t\tapp,\n\t\t\t\t\t\tfalse,\n\t\t\t\t\t);\n\t\t\t\t\tif (result) {\n\t\t\t\t\t\tlogger.log(`Found ${result.endpointCount} endpoints`);\n\t\t\t\t\t}\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\n\t\t\t\tawait runOpenApiInSubprocess(appPath, appName);\n\t\t\t}\n\t\t}\n\t} catch (error) {\n\t\tthrow new Error(`OpenAPI generation failed: ${(error as Error).message}`);\n\t}\n}\n\n/**\n * Generate OpenAPI for a single named app within a workspace.\n * Runs in-process. The caller is responsible for ensuring `process.cwd()`\n * is the app's directory so tsx loads the app's tsconfig path aliases.\n */\nasync function generateOpenApiForApp(\n\tworkspaceRoot: string,\n\t_appName: string,\n\tapp: NormalizedAppConfig,\n\t/**\n\t * Quiet by default, because the usual caller is a subprocess whose output\n\t * the parent relays. Generating in-process there is no parent to relay to,\n\t * and silence would swallow the only report of what happened.\n\t */\n\tsilent = true,\n): Promise<{ outputPath: string; endpointCount: number } | null> {\n\t// A backend app, and everything under it.\n\t//\n\t// It used to gate on `app.routes` and glob that; derive no longer sets it,\n\t// because an app has one glob and which kind a module exports is decided by\n\t// the value. A surface is an app, so an app's own directory contains exactly\n\t// its own surface's endpoints — and the split inside is by surface anyway.\n\tif (app.type !== 'backend') {\n\t\treturn null;\n\t}\n\n\tconst appPath = join(workspaceRoot, app.path);\n\tconst globs = [join(appPath, '**/*.ts')];\n\n\treturn generateOpenApiFrom(globs, {\n\t\t// Absent means enabled, which is what the caller's filter already decided\n\t\t// when it kept this app: it skips one that says `openapi: false` and keeps\n\t\t// every other. Passing `undefined` through let `resolveOpenApiConfig`\n\t\t// default it to *disabled*, so an app selected for generation generated\n\t\t// nothing and said nothing — two places disagreeing about one flag, which\n\t\t// is the failure the filter above was written to describe.\n\t\topenapi: app.openapi ?? { enabled: true },\n\t\tsilent,\n\t});\n}\n\n/**\n * Resolve the gkm bin path. Tests can override via `GKM_BIN_PATH` env var\n * to avoid depending on the built dist.\n */\nfunction resolveGkmBinPath(): string {\n\tif (process.env.GKM_BIN_PATH) {\n\t\treturn process.env.GKM_BIN_PATH;\n\t}\n\treturn fileURLToPath(new URL('../bin/gkm.mjs', import.meta.url));\n}\n\n/**\n * Spawn a subprocess that runs `gkm openapi --app <name>` with `cwd` set to\n * the app's directory. Inherits `NODE_OPTIONS` (which already contains\n * `--import tsx`), so the child gets a fresh tsx instance whose tsconfig\n * discovery picks up the app's tsconfig path aliases.\n */\nasync function runOpenApiInSubprocess(\n\tappCwd: string,\n\tappName: string,\n): Promise<void> {\n\tconst binPath = resolveGkmBinPath();\n\n\tawait new Promise<void>((resolve, reject) => {\n\t\tconst child = spawn(\n\t\t\tprocess.execPath,\n\t\t\t[binPath, 'openapi', '--app', appName],\n\t\t\t{\n\t\t\t\tcwd: appCwd,\n\t\t\t\tstdio: 'inherit',\n\t\t\t\tenv: process.env,\n\t\t\t},\n\t\t);\n\n\t\tchild.on('error', reject);\n\t\tchild.on('close', (code) => {\n\t\t\tif (code === 0) {\n\t\t\t\tresolve();\n\t\t\t} else {\n\t\t\t\treject(\n\t\t\t\t\tnew Error(\n\t\t\t\t\t\t`OpenAPI generation for app \"${appName}\" exited with code ${code}`,\n\t\t\t\t\t),\n\t\t\t\t);\n\t\t\t}\n\t\t});\n\t});\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"openapi.cjs","names":["kebabCase","EndpointGenerator","OpenApiTsGenerator","join","mkdir","dirname","writeFile","relativePath","loadWorkspaceConfig","resolve","fileURLToPath","spawn"],"sources":["../src/openapi.ts"],"sourcesContent":["#!/usr/bin/env -S npx tsx\n\nimport { spawn } from 'node:child_process';\nimport { mkdir, writeFile } from 'node:fs/promises';\nimport { dirname, join, relative as relativePath, resolve } from 'node:path';\nimport { fileURLToPath } from 'node:url';\nimport type { Endpoint } from '@geekmidas/constructs/endpoints';\nimport { kebabCase } from '@geekmidas/manifest';\nimport { loadWorkspaceConfig } from './config.js';\nimport { EndpointGenerator } from './generators/EndpointGenerator.js';\nimport { OpenApiTsGenerator } from './generators/OpenApiTsGenerator.js';\nimport type { GkmConfig, OpenApiConfig, Routes } from './types.js';\nimport { normalizeRoutes } from './workspace/client-generator.js';\nimport type { NormalizedAppConfig } from './workspace/types.js';\n\ninterface OpenAPIOptions {\n\tcwd?: string;\n\t/**\n\t * Workspace mode: generate for a single named backend app. When set,\n\t * generation runs in-process against the current CWD. When unset,\n\t * workspace mode spawns one subprocess per backend app so each gets a\n\t * fresh tsx that loads the app's own tsconfig (path aliases included).\n\t */\n\tapp?: string;\n}\n\n/**\n * Where a surface's generated client lands, under the workspace root.\n *\n * Named from the surface, so `new RestApi('Webhooks')` writes\n * `.gkm/client/webhooks.ts` — the same kebab form that gives it its container\n * name and its `WEBHOOKS_URL` — and a site imports it as\n * `@<name>/client/webhooks`, through the alias its tsconfig maps.\n */\nexport function openApiPathFor(surfaceId: string): string {\n\treturn `./.gkm/client/${kebabCase(surfaceId)}.ts`;\n}\n\n/**\n * Resolve OpenAPI config from GkmConfig\n */\nexport function resolveOpenApiConfig(config: {\n\topenapi?: boolean | OpenApiConfig;\n}): OpenApiConfig & { enabled: boolean } {\n\tif (config.openapi === false) {\n\t\treturn { enabled: false };\n\t}\n\n\tif (config.openapi === true || config.openapi === undefined) {\n\t\t// Enable by default when not explicitly set (undefined) or explicitly true\n\t\treturn {\n\t\t\tenabled: true,\n\t\t\ttitle: 'API Documentation',\n\t\t\tversion: '1.0.0',\n\t\t\tdescription: 'Auto-generated API documentation from endpoints',\n\t\t};\n\t}\n\n\treturn {\n\t\tenabled: config.openapi.enabled !== false,\n\t\ttitle: config.openapi.title || 'API Documentation',\n\t\tversion: config.openapi.version || '1.0.0',\n\t\tdescription:\n\t\t\tconfig.openapi.description ||\n\t\t\t'Auto-generated API documentation from endpoints',\n\t};\n}\n\n/** What a generation run wrote. */\nexport interface OpenApiResult {\n\t/** The first spec written — kept so existing callers still compile. */\n\toutputPath: string;\n\t/** Every spec written, one per surface. */\n\toutputPaths: string[];\n\t/** Endpoints across all of them. */\n\tendpointCount: number;\n}\n\n/**\n * Load endpoints from a glob, then generate.\n *\n * For the callers that have no endpoints in hand — `gkm openapi`, and `gkm dev`\n * regenerating on reload. The glob is the app's derived one — the conventional\n * directories under it — not a `routes` field anybody wrote.\n */\nexport async function generateOpenApiFrom(\n\troutes: Routes,\n\toptions: {\n\t\topenapi?: boolean | OpenApiConfig;\n\t\tsilent?: boolean;\n\t\tbustCache?: boolean;\n\t\troot?: string;\n\t} = {},\n): Promise<OpenApiResult | null> {\n\tconst loaded = await new EndpointGenerator().load(\n\t\troutes,\n\t\tundefined,\n\t\toptions.bustCache,\n\t);\n\n\treturn generateOpenApi(\n\t\tloaded.map(({ construct }) => construct),\n\t\toptions,\n\t);\n}\n\n/**\n * Generate an OpenAPI spec for each surface these endpoints name.\n *\n * Takes the endpoints, not a glob to find them with. The build has already\n * loaded every one of them to generate handlers, so globbing again was a second\n * discovery pass over the same files — and the reason this function needed a\n * `routes` to be told where to look, which is how a retired config field stayed\n * load-bearing.\n *\n * @returns What was written, or null if disabled or there were none\n */\nexport async function generateOpenApi(\n\tendpoints: readonly Endpoint<any, any, any, any, any>[],\n\toptions: {\n\t\topenapi?: boolean | OpenApiConfig;\n\t\tsilent?: boolean;\n\t\t/**\n\t\t * The workspace root, whose `.gkm/client/` holds every surface's client.\n\t\t * The client is the application's, like its manifest — a site reaches it\n\t\t * as `@<name>/client/<surface>` — so it is written there whichever\n\t\t * directory the build happened to run in.\n\t\t */\n\t\troot?: string;\n\t} = {},\n): Promise<OpenApiResult | null> {\n\tconst root = options.root ?? process.cwd();\n\tconst logger = options.silent ? { log: () => {} } : console;\n\tconst openApiConfig = resolveOpenApiConfig({ openapi: options.openapi });\n\n\tif (!openApiConfig.enabled) {\n\t\treturn null;\n\t}\n\n\tif (endpoints.length === 0) {\n\t\tlogger.log('No valid endpoints found for OpenAPI generation');\n\t\treturn null;\n\t}\n\n\t// One spec per surface, not one per glob.\n\t//\n\t// The glob is a directory, and a directory is not a surface: two `RestApi`s\n\t// whose endpoints live side by side produced a single spec describing both,\n\t// under a filename that named neither. Every endpoint carries the surface\n\t// that serves it, so that is what the spec is cut along — `Api` becomes\n\t// `api.ts`, `Webhooks` becomes `webhooks.ts`, by the same kebab rule that\n\t// gives the surface its container name and its environment keys.\n\tconst bySurface = new Map<string, Endpoint<any, any, any, any, any>[]>();\n\tfor (const endpoint of endpoints) {\n\t\tconst owner = endpoint.owner ?? endpoint.surface?.id;\n\t\tif (!owner) continue;\n\t\tconst existing = bySurface.get(owner);\n\t\tif (existing) existing.push(endpoint);\n\t\telse bySurface.set(owner, [endpoint]);\n\t}\n\n\tif (bySurface.size === 0) {\n\t\tlogger.log('No endpoints named a surface, so no OpenAPI was generated');\n\t\treturn null;\n\t}\n\n\tconst tsGenerator = new OpenApiTsGenerator();\n\tconst written: string[] = [];\n\n\tfor (const [surfaceId, surfaceEndpoints] of bySurface) {\n\t\tconst relative = openApiPathFor(surfaceId);\n\t\tconst outputPath = join(root, relative);\n\n\t\tawait mkdir(dirname(outputPath), { recursive: true });\n\n\t\tconst tsContent = await tsGenerator.generate(surfaceEndpoints, {\n\t\t\t// The surface's own name, so a spec says which API it describes\n\t\t\t// rather than repeating one title across all of them.\n\t\t\ttitle: openApiConfig.title ?? surfaceId,\n\t\t\tversion: openApiConfig.version!,\n\t\t\tdescription: openApiConfig.description!,\n\t\t});\n\n\t\tawait writeFile(outputPath, tsContent);\n\t\twritten.push(relative);\n\t\tlogger.log(\n\t\t\t`📄 OpenAPI client generated: ${relativePath(process.cwd(), outputPath)} (${surfaceEndpoints.length} endpoints)`,\n\t\t);\n\t}\n\n\treturn {\n\t\t// The first, for a caller that wants one path. `outputPaths` has them all.\n\t\toutputPath: join(root, written[0]!),\n\t\toutputPaths: written.map((r) => join(root, r)),\n\t\tendpointCount: endpoints.length,\n\t};\n}\n\nexport async function openapiCommand(\n\toptions: OpenAPIOptions = {},\n): Promise<void> {\n\tconst logger = console;\n\n\ttry {\n\t\tconst loadedConfig = await loadWorkspaceConfig(options.cwd);\n\n\t\tif (loadedConfig.type === 'single') {\n\t\t\t// Single-app config - use existing behavior\n\t\t\tconst config = loadedConfig.raw as GkmConfig;\n\n\t\t\t// Enable openapi if not configured\n\t\t\tif (!config.openapi) {\n\t\t\t\tconfig.openapi = { enabled: true };\n\t\t\t}\n\n\t\t\tconst result = await generateOpenApiFrom(config.constructs, {\n\t\t\t\topenapi: config.openapi,\n\t\t\t\troot: loadedConfig.workspace.root,\n\t\t\t});\n\n\t\t\tif (result) {\n\t\t\t\tlogger.log(`Found ${result.endpointCount} endpoints`);\n\t\t\t}\n\t\t} else {\n\t\t\t// Workspace config - generate for each backend app\n\t\t\tconst { workspace } = loadedConfig;\n\t\t\t// Always derive the workspace root from the loaded config rather than\n\t\t\t// CWD. Subprocesses run with CWD set to an app directory, and falling\n\t\t\t// back to CWD there causes `join(workspaceRoot, app.path)` to point at\n\t\t\t// a non-existent nested path (e.g. `apps/api/apps/api`).\n\t\t\tconst workspaceRoot = workspace.root;\n\n\t\t\t// Backend apps with openapi enabled — which is all of them that have\n\t\t\t// routes, unless one says otherwise.\n\t\t\t//\n\t\t\t// This used to require `openapi: true` written out, while the app's\n\t\t\t// own build generated a spec regardless. Two paths disagreeing about\n\t\t\t// the same flag: the app emitted `.gkm/openapi.ts` and this pass,\n\t\t\t// finding nothing \"enabled\", skipped copying it to the frontends that\n\t\t\t// consume it. A surface whose routes are declared rather than\n\t\t\t// discovered has no spec to generate, so it is not a candidate.\n\t\t\tconst backendApps = Object.entries(workspace.apps).filter(\n\t\t\t\t([_, app]) =>\n\t\t\t\t\tapp.type === 'backend' &&\n\t\t\t\t\tapp.openapi !== false &&\n\t\t\t\t\t(typeof app.openapi !== 'object' || app.openapi.enabled !== false),\n\t\t\t);\n\n\t\t\tif (backendApps.length === 0) {\n\t\t\t\tlogger.log('No backend apps with OpenAPI enabled found');\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\t// Single-app mode: generate in-process. The caller is expected to\n\t\t\t// have CWD set to the app's directory (e.g., when invoked as a\n\t\t\t// subprocess), so tsx picks up the app's tsconfig path aliases.\n\t\t\tif (options.app) {\n\t\t\t\tconst entry = backendApps.find(([name]) => name === options.app);\n\t\t\t\tif (!entry) {\n\t\t\t\t\tthrow new Error(\n\t\t\t\t\t\t`App \"${options.app}\" not found or has OpenAPI disabled`,\n\t\t\t\t\t);\n\t\t\t\t}\n\t\t\t\tconst [appName, app] = entry;\n\t\t\t\tconst result = await generateOpenApiForApp(workspaceRoot, appName, app);\n\t\t\t\tif (result) {\n\t\t\t\t\tlogger.log(\n\t\t\t\t\t\t`📄 [${appName}] Generated OpenAPI (${result.endpointCount} endpoints)`,\n\t\t\t\t\t);\n\t\t\t\t}\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\t// Multi-app mode: spawn a subprocess per backend app. Each\n\t\t\t// subprocess starts with CWD at the app's directory so that\n\t\t\t// tsx's tsconfig discovery picks up the app's `paths` aliases\n\t\t\t// (e.g., `~/*`) instead of the workspace root's tsconfig.\n\t\t\tfor (const [appName, app] of backendApps) {\n\t\t\t\t// No `app.routes` check any more: the per-kind globs are gone, and\n\t\t\t\t// an app's code is found by one glob rather than by a field naming\n\t\t\t\t// endpoints. Guarding on `routes` here meant every app was skipped\n\t\t\t\t// the moment that field stopped existing — silently, because a loop\n\t\t\t\t// that runs zero times looks exactly like one with nothing to do.\n\t\t\t\tconst appPath = join(workspaceRoot, app.path);\n\n\t\t\t\t// A subprocess exists to put CWD inside the app, so tsx picks up\n\t\t\t\t// that app's tsconfig aliases instead of the workspace root's. When\n\t\t\t\t// the app *is* the root there is nothing to change: spawning would\n\t\t\t\t// re-enter the same directory, having paid for a process and a\n\t\t\t\t// module graph, and needing `tsx` resolvable from it.\n\t\t\t\tif (resolve(appPath) === resolve(workspaceRoot)) {\n\t\t\t\t\t// Not silent: silence is for a subprocess whose output the\n\t\t\t\t\t// parent relays, and there is no parent here to relay it. The\n\t\t\t\t\t// per-surface lines come from the generator; the count is this\n\t\t\t\t\t// command's own summary, as in the single-app path.\n\t\t\t\t\tconst result = await generateOpenApiForApp(\n\t\t\t\t\t\tworkspaceRoot,\n\t\t\t\t\t\tappName,\n\t\t\t\t\t\tapp,\n\t\t\t\t\t\tfalse,\n\t\t\t\t\t);\n\t\t\t\t\tif (result) {\n\t\t\t\t\t\tlogger.log(`Found ${result.endpointCount} endpoints`);\n\t\t\t\t\t}\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\n\t\t\t\tawait runOpenApiInSubprocess(appPath, appName);\n\t\t\t}\n\t\t}\n\t} catch (error) {\n\t\tthrow new Error(`OpenAPI generation failed: ${(error as Error).message}`);\n\t}\n}\n\n/**\n * Generate OpenAPI for a single named app within a workspace.\n * Runs in-process. The caller is responsible for ensuring `process.cwd()`\n * is the app's directory so tsx loads the app's tsconfig path aliases.\n */\nasync function generateOpenApiForApp(\n\tworkspaceRoot: string,\n\t_appName: string,\n\tapp: NormalizedAppConfig,\n\t/**\n\t * Quiet by default, because the usual caller is a subprocess whose output\n\t * the parent relays. Generating in-process there is no parent to relay to,\n\t * and silence would swallow the only report of what happened.\n\t */\n\tsilent = true,\n): Promise<{ outputPath: string; endpointCount: number } | null> {\n\t// A backend app, and everything under it.\n\t//\n\t// It used to gate on `app.routes` and glob that; derive no longer sets it,\n\t// because an app has one glob and which kind a module exports is decided by\n\t// the value. A surface is an app, so an app's own directory contains exactly\n\t// its own surface's endpoints — and the split inside is by surface anyway.\n\tif (app.type !== 'backend') {\n\t\treturn null;\n\t}\n\n\tconst appPath = join(workspaceRoot, app.path);\n\tconst globs = [join(appPath, '**/*.ts')];\n\n\treturn generateOpenApiFrom(globs, {\n\t\t// Absent means enabled, which is what the caller's filter already decided\n\t\t// when it kept this app: it skips one that says `openapi: false` and keeps\n\t\t// every other. Passing `undefined` through let `resolveOpenApiConfig`\n\t\t// default it to *disabled*, so an app selected for generation generated\n\t\t// nothing and said nothing — two places disagreeing about one flag, which\n\t\t// is the failure the filter above was written to describe.\n\t\topenapi: app.openapi ?? { enabled: true },\n\t\tsilent,\n\t\troot: workspaceRoot,\n\t});\n}\n\n/**\n * Resolve the gkm bin path. Tests can override via `GKM_BIN_PATH` env var\n * to avoid depending on the built dist.\n */\nfunction resolveGkmBinPath(): string {\n\tif (process.env.GKM_BIN_PATH) {\n\t\treturn process.env.GKM_BIN_PATH;\n\t}\n\treturn fileURLToPath(new URL('../bin/gkm.mjs', import.meta.url));\n}\n\n/**\n * Spawn a subprocess that runs `gkm openapi --app <name>` with `cwd` set to\n * the app's directory. Inherits `NODE_OPTIONS` (which already contains\n * `--import tsx`), so the child gets a fresh tsx instance whose tsconfig\n * discovery picks up the app's tsconfig path aliases.\n */\nasync function runOpenApiInSubprocess(\n\tappCwd: string,\n\tappName: string,\n): Promise<void> {\n\tconst binPath = resolveGkmBinPath();\n\n\tawait new Promise<void>((resolve, reject) => {\n\t\tconst child = spawn(\n\t\t\tprocess.execPath,\n\t\t\t[binPath, 'openapi', '--app', appName],\n\t\t\t{\n\t\t\t\tcwd: appCwd,\n\t\t\t\tstdio: 'inherit',\n\t\t\t\tenv: process.env,\n\t\t\t},\n\t\t);\n\n\t\tchild.on('error', reject);\n\t\tchild.on('close', (code) => {\n\t\t\tif (code === 0) {\n\t\t\t\tresolve();\n\t\t\t} else {\n\t\t\t\treject(\n\t\t\t\t\tnew Error(\n\t\t\t\t\t\t`OpenAPI generation for app \"${appName}\" exited with code ${code}`,\n\t\t\t\t\t),\n\t\t\t\t);\n\t\t\t}\n\t\t});\n\t});\n}\n"],"mappings":"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"}
@@ -12,15 +12,12 @@ interface OpenAPIOptions {
12
12
  app?: string;
13
13
  }
14
14
  /**
15
- * Default output path for generated OpenAPI client (used for single-app configs)
16
- */
17
- export declare const OPENAPI_OUTPUT_PATH = "./.gkm/openapi.ts";
18
- /**
19
- * Where a surface's generated client lands.
15
+ * Where a surface's generated client lands, under the workspace root.
20
16
  *
21
17
  * Named from the surface, so `new RestApi('Webhooks')` writes
22
- * `.gkm/openapi/webhooks.ts` — the same kebab form that gives it its container
23
- * name and its `WEBHOOKS_URL`.
18
+ * `.gkm/client/webhooks.ts` — the same kebab form that gives it its container
19
+ * name and its `WEBHOOKS_URL` — and a site imports it as
20
+ * `@<name>/client/webhooks`, through the alias its tsconfig maps.
24
21
  */
25
22
  export declare function openApiPathFor(surfaceId: string): string;
26
23
  /**
@@ -51,6 +48,7 @@ export declare function generateOpenApiFrom(routes: Routes, options?: {
51
48
  openapi?: boolean | OpenApiConfig;
52
49
  silent?: boolean;
53
50
  bustCache?: boolean;
51
+ root?: string;
54
52
  }): Promise<OpenApiResult | null>;
55
53
  /**
56
54
  * Generate an OpenAPI spec for each surface these endpoints name.
@@ -66,6 +64,13 @@ export declare function generateOpenApiFrom(routes: Routes, options?: {
66
64
  export declare function generateOpenApi(endpoints: readonly Endpoint<any, any, any, any, any>[], options?: {
67
65
  openapi?: boolean | OpenApiConfig;
68
66
  silent?: boolean;
67
+ /**
68
+ * The workspace root, whose `.gkm/client/` holds every surface's client.
69
+ * The client is the application's, like its manifest — a site reaches it
70
+ * as `@<name>/client/<surface>` — so it is written there whichever
71
+ * directory the build happened to run in.
72
+ */
73
+ root?: string;
69
74
  }): Promise<OpenApiResult | null>;
70
75
  export declare function openapiCommand(options?: OpenAPIOptions): Promise<void>;
71
76
  //#endregion
@@ -1 +1 @@
1
- {"version":3,"file":"openapi.d.cts","names":[],"sources":["../src/openapi.ts"],"mappings":";;;UAeU;EACT;;;;;;;EAOA;;;;;qBAMY;;;;;;;;wBASG,eAAe;;;;wBAOf,qBAAqB;EACpC,oBAAoB;IACjB;EAAkB;;;iBA0BL;;EAEhB;;EAEA;;EAEA;;;;;;;;;wBAUqB,oBACrB,QAAQ,QACR;EACC,oBAAoB;EACpB;EACA;IAEC,QAAQ;;;;;;;;;;;;wBAwBW,gBACrB,oBAAoB,qCACpB;EACC,oBAAoB;EACpB;IAEC,QAAQ;wBAmEW,eACrB,UAAS,iBACP"}
1
+ {"version":3,"file":"openapi.d.cts","names":[],"sources":["../src/openapi.ts"],"mappings":";;;UAeU;EACT;;;;;;;EAOA;;;;;;;;;;wBAWe,eAAe;;;;wBAOf,qBAAqB;EACpC,oBAAoB;IACjB;EAAkB;;;iBA0BL;;EAEhB;;EAEA;;EAEA;;;;;;;;;wBAUqB,oBACrB,QAAQ,QACR;EACC,oBAAoB;EACpB;EACA;EACA;IAEC,QAAQ;;;;;;;;;;;;wBAwBW,gBACrB,oBAAoB,qCACpB;EACC,oBAAoB;EACpB;;;;;;;EAOA;IAEC,QAAQ;wBAoEW,eACrB,UAAS,iBACP"}
@@ -12,15 +12,12 @@ interface OpenAPIOptions {
12
12
  app?: string;
13
13
  }
14
14
  /**
15
- * Default output path for generated OpenAPI client (used for single-app configs)
16
- */
17
- export declare const OPENAPI_OUTPUT_PATH = "./.gkm/openapi.ts";
18
- /**
19
- * Where a surface's generated client lands.
15
+ * Where a surface's generated client lands, under the workspace root.
20
16
  *
21
17
  * Named from the surface, so `new RestApi('Webhooks')` writes
22
- * `.gkm/openapi/webhooks.ts` — the same kebab form that gives it its container
23
- * name and its `WEBHOOKS_URL`.
18
+ * `.gkm/client/webhooks.ts` — the same kebab form that gives it its container
19
+ * name and its `WEBHOOKS_URL` — and a site imports it as
20
+ * `@<name>/client/webhooks`, through the alias its tsconfig maps.
24
21
  */
25
22
  export declare function openApiPathFor(surfaceId: string): string;
26
23
  /**
@@ -51,6 +48,7 @@ export declare function generateOpenApiFrom(routes: Routes, options?: {
51
48
  openapi?: boolean | OpenApiConfig;
52
49
  silent?: boolean;
53
50
  bustCache?: boolean;
51
+ root?: string;
54
52
  }): Promise<OpenApiResult | null>;
55
53
  /**
56
54
  * Generate an OpenAPI spec for each surface these endpoints name.
@@ -66,6 +64,13 @@ export declare function generateOpenApiFrom(routes: Routes, options?: {
66
64
  export declare function generateOpenApi(endpoints: readonly Endpoint<any, any, any, any, any>[], options?: {
67
65
  openapi?: boolean | OpenApiConfig;
68
66
  silent?: boolean;
67
+ /**
68
+ * The workspace root, whose `.gkm/client/` holds every surface's client.
69
+ * The client is the application's, like its manifest — a site reaches it
70
+ * as `@<name>/client/<surface>` — so it is written there whichever
71
+ * directory the build happened to run in.
72
+ */
73
+ root?: string;
69
74
  }): Promise<OpenApiResult | null>;
70
75
  export declare function openapiCommand(options?: OpenAPIOptions): Promise<void>;
71
76
  //#endregion
@@ -1 +1 @@
1
- {"version":3,"file":"openapi.d.mts","names":[],"sources":["../src/openapi.ts"],"mappings":";;;UAeU;EACT;;;;;;;EAOA;;;;;qBAMY;;;;;;;;wBASG,eAAe;;;;wBAOf,qBAAqB;EACpC,oBAAoB;IACjB;EAAkB;;;iBA0BL;;EAEhB;;EAEA;;EAEA;;;;;;;;;wBAUqB,oBACrB,QAAQ,QACR;EACC,oBAAoB;EACpB;EACA;IAEC,QAAQ;;;;;;;;;;;;wBAwBW,gBACrB,oBAAoB,qCACpB;EACC,oBAAoB;EACpB;IAEC,QAAQ;wBAmEW,eACrB,UAAS,iBACP"}
1
+ {"version":3,"file":"openapi.d.mts","names":[],"sources":["../src/openapi.ts"],"mappings":";;;UAeU;EACT;;;;;;;EAOA;;;;;;;;;;wBAWe,eAAe;;;;wBAOf,qBAAqB;EACpC,oBAAoB;IACjB;EAAkB;;;iBA0BL;;EAEhB;;EAEA;;EAEA;;;;;;;;;wBAUqB,oBACrB,QAAQ,QACR;EACC,oBAAoB;EACpB;EACA;EACA;IAEC,QAAQ;;;;;;;;;;;;wBAwBW,gBACrB,oBAAoB,qCACpB;EACC,oBAAoB;EACpB;;;;;;;EAOA;IAEC,QAAQ;wBAoEW,eACrB,UAAS,iBACP"}
package/dist/openapi.mjs CHANGED
@@ -2,24 +2,21 @@
2
2
  import { l as loadWorkspaceConfig } from "./config-D8J5rrTb.mjs";
3
3
  import { n as EndpointGenerator, t as OpenApiTsGenerator } from "./OpenApiTsGenerator-BzIGy8KE.mjs";
4
4
  import { mkdir, writeFile } from "node:fs/promises";
5
- import { dirname, join, resolve } from "node:path";
5
+ import { dirname, join, relative, resolve } from "node:path";
6
6
  import { spawn } from "node:child_process";
7
7
  import { kebabCase } from "@geekmidas/manifest";
8
8
  import { fileURLToPath } from "node:url";
9
9
  //#region src/openapi.ts
10
10
  /**
11
- * Default output path for generated OpenAPI client (used for single-app configs)
12
- */
13
- const OPENAPI_OUTPUT_PATH = "./.gkm/openapi.ts";
14
- /**
15
- * Where a surface's generated client lands.
11
+ * Where a surface's generated client lands, under the workspace root.
16
12
  *
17
13
  * Named from the surface, so `new RestApi('Webhooks')` writes
18
- * `.gkm/openapi/webhooks.ts` — the same kebab form that gives it its container
19
- * name and its `WEBHOOKS_URL`.
14
+ * `.gkm/client/webhooks.ts` — the same kebab form that gives it its container
15
+ * name and its `WEBHOOKS_URL` — and a site imports it as
16
+ * `@<name>/client/webhooks`, through the alias its tsconfig maps.
20
17
  */
21
18
  function openApiPathFor(surfaceId) {
22
- return `./.gkm/openapi/${kebabCase(surfaceId)}.ts`;
19
+ return `./.gkm/client/${kebabCase(surfaceId)}.ts`;
23
20
  }
24
21
  /**
25
22
  * Resolve OpenAPI config from GkmConfig
@@ -61,6 +58,7 @@ async function generateOpenApiFrom(routes, options = {}) {
61
58
  * @returns What was written, or null if disabled or there were none
62
59
  */
63
60
  async function generateOpenApi(endpoints, options = {}) {
61
+ const root = options.root ?? process.cwd();
64
62
  const logger = options.silent ? { log: () => {} } : console;
65
63
  const openApiConfig = resolveOpenApiConfig({ openapi: options.openapi });
66
64
  if (!openApiConfig.enabled) return null;
@@ -83,8 +81,8 @@ async function generateOpenApi(endpoints, options = {}) {
83
81
  const tsGenerator = new OpenApiTsGenerator();
84
82
  const written = [];
85
83
  for (const [surfaceId, surfaceEndpoints] of bySurface) {
86
- const relative = openApiPathFor(surfaceId);
87
- const outputPath = join(process.cwd(), relative);
84
+ const relative$1 = openApiPathFor(surfaceId);
85
+ const outputPath = join(root, relative$1);
88
86
  await mkdir(dirname(outputPath), { recursive: true });
89
87
  const tsContent = await tsGenerator.generate(surfaceEndpoints, {
90
88
  title: openApiConfig.title ?? surfaceId,
@@ -92,12 +90,12 @@ async function generateOpenApi(endpoints, options = {}) {
92
90
  description: openApiConfig.description
93
91
  });
94
92
  await writeFile(outputPath, tsContent);
95
- written.push(relative);
96
- logger.log(`📄 OpenAPI client generated: ${relative} (${surfaceEndpoints.length} endpoints)`);
93
+ written.push(relative$1);
94
+ logger.log(`📄 OpenAPI client generated: ${relative(process.cwd(), outputPath)} (${surfaceEndpoints.length} endpoints)`);
97
95
  }
98
96
  return {
99
- outputPath: join(process.cwd(), written[0]),
100
- outputPaths: written.map((r) => join(process.cwd(), r)),
97
+ outputPath: join(root, written[0]),
98
+ outputPaths: written.map((r) => join(root, r)),
101
99
  endpointCount: endpoints.length
102
100
  };
103
101
  }
@@ -108,7 +106,10 @@ async function openapiCommand(options = {}) {
108
106
  if (loadedConfig.type === "single") {
109
107
  const config = loadedConfig.raw;
110
108
  if (!config.openapi) config.openapi = { enabled: true };
111
- const result = await generateOpenApiFrom(config.constructs, { openapi: config.openapi });
109
+ const result = await generateOpenApiFrom(config.constructs, {
110
+ openapi: config.openapi,
111
+ root: loadedConfig.workspace.root
112
+ });
112
113
  if (result) logger.log(`Found ${result.endpointCount} endpoints`);
113
114
  } else {
114
115
  const { workspace } = loadedConfig;
@@ -150,7 +151,8 @@ async function generateOpenApiForApp(workspaceRoot, _appName, app, silent = true
150
151
  const appPath = join(workspaceRoot, app.path);
151
152
  return generateOpenApiFrom([join(appPath, "**/*.ts")], {
152
153
  openapi: app.openapi ?? { enabled: true },
153
- silent
154
+ silent,
155
+ root: workspaceRoot
154
156
  });
155
157
  }
156
158
  /**
@@ -188,6 +190,6 @@ async function runOpenApiInSubprocess(appCwd, appName) {
188
190
  });
189
191
  }
190
192
  //#endregion
191
- export { OPENAPI_OUTPUT_PATH, generateOpenApi, generateOpenApiFrom, openApiPathFor, openapiCommand, resolveOpenApiConfig };
193
+ export { generateOpenApi, generateOpenApiFrom, openApiPathFor, openapiCommand, resolveOpenApiConfig };
192
194
 
193
195
  //# sourceMappingURL=openapi.mjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"openapi.mjs","names":[],"sources":["../src/openapi.ts"],"sourcesContent":["#!/usr/bin/env -S npx tsx\n\nimport { spawn } from 'node:child_process';\nimport { mkdir, writeFile } from 'node:fs/promises';\nimport { dirname, join, resolve } from 'node:path';\nimport { fileURLToPath } from 'node:url';\nimport type { Endpoint } from '@geekmidas/constructs/endpoints';\nimport { kebabCase } from '@geekmidas/manifest';\nimport { loadWorkspaceConfig } from './config.js';\nimport { EndpointGenerator } from './generators/EndpointGenerator.js';\nimport { OpenApiTsGenerator } from './generators/OpenApiTsGenerator.js';\nimport type { GkmConfig, OpenApiConfig, Routes } from './types.js';\nimport { normalizeRoutes } from './workspace/client-generator.js';\nimport type { NormalizedAppConfig } from './workspace/types.js';\n\ninterface OpenAPIOptions {\n\tcwd?: string;\n\t/**\n\t * Workspace mode: generate for a single named backend app. When set,\n\t * generation runs in-process against the current CWD. When unset,\n\t * workspace mode spawns one subprocess per backend app so each gets a\n\t * fresh tsx that loads the app's own tsconfig (path aliases included).\n\t */\n\tapp?: string;\n}\n\n/**\n * Default output path for generated OpenAPI client (used for single-app configs)\n */\nexport const OPENAPI_OUTPUT_PATH = './.gkm/openapi.ts';\n\n/**\n * Where a surface's generated client lands.\n *\n * Named from the surface, so `new RestApi('Webhooks')` writes\n * `.gkm/openapi/webhooks.ts` — the same kebab form that gives it its container\n * name and its `WEBHOOKS_URL`.\n */\nexport function openApiPathFor(surfaceId: string): string {\n\treturn `./.gkm/openapi/${kebabCase(surfaceId)}.ts`;\n}\n\n/**\n * Resolve OpenAPI config from GkmConfig\n */\nexport function resolveOpenApiConfig(config: {\n\topenapi?: boolean | OpenApiConfig;\n}): OpenApiConfig & { enabled: boolean } {\n\tif (config.openapi === false) {\n\t\treturn { enabled: false };\n\t}\n\n\tif (config.openapi === true || config.openapi === undefined) {\n\t\t// Enable by default when not explicitly set (undefined) or explicitly true\n\t\treturn {\n\t\t\tenabled: true,\n\t\t\ttitle: 'API Documentation',\n\t\t\tversion: '1.0.0',\n\t\t\tdescription: 'Auto-generated API documentation from endpoints',\n\t\t};\n\t}\n\n\treturn {\n\t\tenabled: config.openapi.enabled !== false,\n\t\ttitle: config.openapi.title || 'API Documentation',\n\t\tversion: config.openapi.version || '1.0.0',\n\t\tdescription:\n\t\t\tconfig.openapi.description ||\n\t\t\t'Auto-generated API documentation from endpoints',\n\t};\n}\n\n/** What a generation run wrote. */\nexport interface OpenApiResult {\n\t/** The first spec written — kept so existing callers still compile. */\n\toutputPath: string;\n\t/** Every spec written, one per surface. */\n\toutputPaths: string[];\n\t/** Endpoints across all of them. */\n\tendpointCount: number;\n}\n\n/**\n * Load endpoints from a glob, then generate.\n *\n * For the callers that have no endpoints in hand — `gkm openapi`, and `gkm dev`\n * regenerating on reload. The glob is the app's derived one — the conventional\n * directories under it — not a `routes` field anybody wrote.\n */\nexport async function generateOpenApiFrom(\n\troutes: Routes,\n\toptions: {\n\t\topenapi?: boolean | OpenApiConfig;\n\t\tsilent?: boolean;\n\t\tbustCache?: boolean;\n\t} = {},\n): Promise<OpenApiResult | null> {\n\tconst loaded = await new EndpointGenerator().load(\n\t\troutes,\n\t\tundefined,\n\t\toptions.bustCache,\n\t);\n\n\treturn generateOpenApi(\n\t\tloaded.map(({ construct }) => construct),\n\t\toptions,\n\t);\n}\n\n/**\n * Generate an OpenAPI spec for each surface these endpoints name.\n *\n * Takes the endpoints, not a glob to find them with. The build has already\n * loaded every one of them to generate handlers, so globbing again was a second\n * discovery pass over the same files — and the reason this function needed a\n * `routes` to be told where to look, which is how a retired config field stayed\n * load-bearing.\n *\n * @returns What was written, or null if disabled or there were none\n */\nexport async function generateOpenApi(\n\tendpoints: readonly Endpoint<any, any, any, any, any>[],\n\toptions: {\n\t\topenapi?: boolean | OpenApiConfig;\n\t\tsilent?: boolean;\n\t} = {},\n): Promise<OpenApiResult | null> {\n\tconst logger = options.silent ? { log: () => {} } : console;\n\tconst openApiConfig = resolveOpenApiConfig({ openapi: options.openapi });\n\n\tif (!openApiConfig.enabled) {\n\t\treturn null;\n\t}\n\n\tif (endpoints.length === 0) {\n\t\tlogger.log('No valid endpoints found for OpenAPI generation');\n\t\treturn null;\n\t}\n\n\t// One spec per surface, not one per glob.\n\t//\n\t// The glob is a directory, and a directory is not a surface: two `RestApi`s\n\t// whose endpoints live side by side produced a single spec describing both,\n\t// under a filename that named neither. Every endpoint carries the surface\n\t// that serves it, so that is what the spec is cut along — `Api` becomes\n\t// `api.ts`, `Webhooks` becomes `webhooks.ts`, by the same kebab rule that\n\t// gives the surface its container name and its environment keys.\n\tconst bySurface = new Map<string, Endpoint<any, any, any, any, any>[]>();\n\tfor (const endpoint of endpoints) {\n\t\tconst owner = endpoint.owner ?? endpoint.surface?.id;\n\t\tif (!owner) continue;\n\t\tconst existing = bySurface.get(owner);\n\t\tif (existing) existing.push(endpoint);\n\t\telse bySurface.set(owner, [endpoint]);\n\t}\n\n\tif (bySurface.size === 0) {\n\t\tlogger.log('No endpoints named a surface, so no OpenAPI was generated');\n\t\treturn null;\n\t}\n\n\tconst tsGenerator = new OpenApiTsGenerator();\n\tconst written: string[] = [];\n\n\tfor (const [surfaceId, surfaceEndpoints] of bySurface) {\n\t\tconst relative = openApiPathFor(surfaceId);\n\t\tconst outputPath = join(process.cwd(), relative);\n\n\t\tawait mkdir(dirname(outputPath), { recursive: true });\n\n\t\tconst tsContent = await tsGenerator.generate(surfaceEndpoints, {\n\t\t\t// The surface's own name, so a spec says which API it describes\n\t\t\t// rather than repeating one title across all of them.\n\t\t\ttitle: openApiConfig.title ?? surfaceId,\n\t\t\tversion: openApiConfig.version!,\n\t\t\tdescription: openApiConfig.description!,\n\t\t});\n\n\t\tawait writeFile(outputPath, tsContent);\n\t\twritten.push(relative);\n\t\tlogger.log(\n\t\t\t`📄 OpenAPI client generated: ${relative} (${surfaceEndpoints.length} endpoints)`,\n\t\t);\n\t}\n\n\treturn {\n\t\t// The first, for a caller that wants one path. `outputPaths` has them all.\n\t\toutputPath: join(process.cwd(), written[0]!),\n\t\toutputPaths: written.map((r) => join(process.cwd(), r)),\n\t\tendpointCount: endpoints.length,\n\t};\n}\n\nexport async function openapiCommand(\n\toptions: OpenAPIOptions = {},\n): Promise<void> {\n\tconst logger = console;\n\n\ttry {\n\t\tconst loadedConfig = await loadWorkspaceConfig(options.cwd);\n\n\t\tif (loadedConfig.type === 'single') {\n\t\t\t// Single-app config - use existing behavior\n\t\t\tconst config = loadedConfig.raw as GkmConfig;\n\n\t\t\t// Enable openapi if not configured\n\t\t\tif (!config.openapi) {\n\t\t\t\tconfig.openapi = { enabled: true };\n\t\t\t}\n\n\t\t\tconst result = await generateOpenApiFrom(config.constructs, {\n\t\t\t\topenapi: config.openapi,\n\t\t\t});\n\n\t\t\tif (result) {\n\t\t\t\tlogger.log(`Found ${result.endpointCount} endpoints`);\n\t\t\t}\n\t\t} else {\n\t\t\t// Workspace config - generate for each backend app\n\t\t\tconst { workspace } = loadedConfig;\n\t\t\t// Always derive the workspace root from the loaded config rather than\n\t\t\t// CWD. Subprocesses run with CWD set to an app directory, and falling\n\t\t\t// back to CWD there causes `join(workspaceRoot, app.path)` to point at\n\t\t\t// a non-existent nested path (e.g. `apps/api/apps/api`).\n\t\t\tconst workspaceRoot = workspace.root;\n\n\t\t\t// Backend apps with openapi enabled — which is all of them that have\n\t\t\t// routes, unless one says otherwise.\n\t\t\t//\n\t\t\t// This used to require `openapi: true` written out, while the app's\n\t\t\t// own build generated a spec regardless. Two paths disagreeing about\n\t\t\t// the same flag: the app emitted `.gkm/openapi.ts` and this pass,\n\t\t\t// finding nothing \"enabled\", skipped copying it to the frontends that\n\t\t\t// consume it. A surface whose routes are declared rather than\n\t\t\t// discovered has no spec to generate, so it is not a candidate.\n\t\t\tconst backendApps = Object.entries(workspace.apps).filter(\n\t\t\t\t([_, app]) =>\n\t\t\t\t\tapp.type === 'backend' &&\n\t\t\t\t\tapp.openapi !== false &&\n\t\t\t\t\t(typeof app.openapi !== 'object' || app.openapi.enabled !== false),\n\t\t\t);\n\n\t\t\tif (backendApps.length === 0) {\n\t\t\t\tlogger.log('No backend apps with OpenAPI enabled found');\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\t// Single-app mode: generate in-process. The caller is expected to\n\t\t\t// have CWD set to the app's directory (e.g., when invoked as a\n\t\t\t// subprocess), so tsx picks up the app's tsconfig path aliases.\n\t\t\tif (options.app) {\n\t\t\t\tconst entry = backendApps.find(([name]) => name === options.app);\n\t\t\t\tif (!entry) {\n\t\t\t\t\tthrow new Error(\n\t\t\t\t\t\t`App \"${options.app}\" not found or has OpenAPI disabled`,\n\t\t\t\t\t);\n\t\t\t\t}\n\t\t\t\tconst [appName, app] = entry;\n\t\t\t\tconst result = await generateOpenApiForApp(workspaceRoot, appName, app);\n\t\t\t\tif (result) {\n\t\t\t\t\tlogger.log(\n\t\t\t\t\t\t`📄 [${appName}] Generated OpenAPI (${result.endpointCount} endpoints)`,\n\t\t\t\t\t);\n\t\t\t\t}\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\t// Multi-app mode: spawn a subprocess per backend app. Each\n\t\t\t// subprocess starts with CWD at the app's directory so that\n\t\t\t// tsx's tsconfig discovery picks up the app's `paths` aliases\n\t\t\t// (e.g., `~/*`) instead of the workspace root's tsconfig.\n\t\t\tfor (const [appName, app] of backendApps) {\n\t\t\t\t// No `app.routes` check any more: the per-kind globs are gone, and\n\t\t\t\t// an app's code is found by one glob rather than by a field naming\n\t\t\t\t// endpoints. Guarding on `routes` here meant every app was skipped\n\t\t\t\t// the moment that field stopped existing — silently, because a loop\n\t\t\t\t// that runs zero times looks exactly like one with nothing to do.\n\t\t\t\tconst appPath = join(workspaceRoot, app.path);\n\n\t\t\t\t// A subprocess exists to put CWD inside the app, so tsx picks up\n\t\t\t\t// that app's tsconfig aliases instead of the workspace root's. When\n\t\t\t\t// the app *is* the root there is nothing to change: spawning would\n\t\t\t\t// re-enter the same directory, having paid for a process and a\n\t\t\t\t// module graph, and needing `tsx` resolvable from it.\n\t\t\t\tif (resolve(appPath) === resolve(workspaceRoot)) {\n\t\t\t\t\t// Not silent: silence is for a subprocess whose output the\n\t\t\t\t\t// parent relays, and there is no parent here to relay it. The\n\t\t\t\t\t// per-surface lines come from the generator; the count is this\n\t\t\t\t\t// command's own summary, as in the single-app path.\n\t\t\t\t\tconst result = await generateOpenApiForApp(\n\t\t\t\t\t\tworkspaceRoot,\n\t\t\t\t\t\tappName,\n\t\t\t\t\t\tapp,\n\t\t\t\t\t\tfalse,\n\t\t\t\t\t);\n\t\t\t\t\tif (result) {\n\t\t\t\t\t\tlogger.log(`Found ${result.endpointCount} endpoints`);\n\t\t\t\t\t}\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\n\t\t\t\tawait runOpenApiInSubprocess(appPath, appName);\n\t\t\t}\n\t\t}\n\t} catch (error) {\n\t\tthrow new Error(`OpenAPI generation failed: ${(error as Error).message}`);\n\t}\n}\n\n/**\n * Generate OpenAPI for a single named app within a workspace.\n * Runs in-process. The caller is responsible for ensuring `process.cwd()`\n * is the app's directory so tsx loads the app's tsconfig path aliases.\n */\nasync function generateOpenApiForApp(\n\tworkspaceRoot: string,\n\t_appName: string,\n\tapp: NormalizedAppConfig,\n\t/**\n\t * Quiet by default, because the usual caller is a subprocess whose output\n\t * the parent relays. Generating in-process there is no parent to relay to,\n\t * and silence would swallow the only report of what happened.\n\t */\n\tsilent = true,\n): Promise<{ outputPath: string; endpointCount: number } | null> {\n\t// A backend app, and everything under it.\n\t//\n\t// It used to gate on `app.routes` and glob that; derive no longer sets it,\n\t// because an app has one glob and which kind a module exports is decided by\n\t// the value. A surface is an app, so an app's own directory contains exactly\n\t// its own surface's endpoints — and the split inside is by surface anyway.\n\tif (app.type !== 'backend') {\n\t\treturn null;\n\t}\n\n\tconst appPath = join(workspaceRoot, app.path);\n\tconst globs = [join(appPath, '**/*.ts')];\n\n\treturn generateOpenApiFrom(globs, {\n\t\t// Absent means enabled, which is what the caller's filter already decided\n\t\t// when it kept this app: it skips one that says `openapi: false` and keeps\n\t\t// every other. Passing `undefined` through let `resolveOpenApiConfig`\n\t\t// default it to *disabled*, so an app selected for generation generated\n\t\t// nothing and said nothing — two places disagreeing about one flag, which\n\t\t// is the failure the filter above was written to describe.\n\t\topenapi: app.openapi ?? { enabled: true },\n\t\tsilent,\n\t});\n}\n\n/**\n * Resolve the gkm bin path. Tests can override via `GKM_BIN_PATH` env var\n * to avoid depending on the built dist.\n */\nfunction resolveGkmBinPath(): string {\n\tif (process.env.GKM_BIN_PATH) {\n\t\treturn process.env.GKM_BIN_PATH;\n\t}\n\treturn fileURLToPath(new URL('../bin/gkm.mjs', import.meta.url));\n}\n\n/**\n * Spawn a subprocess that runs `gkm openapi --app <name>` with `cwd` set to\n * the app's directory. Inherits `NODE_OPTIONS` (which already contains\n * `--import tsx`), so the child gets a fresh tsx instance whose tsconfig\n * discovery picks up the app's tsconfig path aliases.\n */\nasync function runOpenApiInSubprocess(\n\tappCwd: string,\n\tappName: string,\n): Promise<void> {\n\tconst binPath = resolveGkmBinPath();\n\n\tawait new Promise<void>((resolve, reject) => {\n\t\tconst child = spawn(\n\t\t\tprocess.execPath,\n\t\t\t[binPath, 'openapi', '--app', appName],\n\t\t\t{\n\t\t\t\tcwd: appCwd,\n\t\t\t\tstdio: 'inherit',\n\t\t\t\tenv: process.env,\n\t\t\t},\n\t\t);\n\n\t\tchild.on('error', reject);\n\t\tchild.on('close', (code) => {\n\t\t\tif (code === 0) {\n\t\t\t\tresolve();\n\t\t\t} else {\n\t\t\t\treject(\n\t\t\t\t\tnew Error(\n\t\t\t\t\t\t`OpenAPI generation for app \"${appName}\" exited with code ${code}`,\n\t\t\t\t\t),\n\t\t\t\t);\n\t\t\t}\n\t\t});\n\t});\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"openapi.mjs","names":["relative","relativePath"],"sources":["../src/openapi.ts"],"sourcesContent":["#!/usr/bin/env -S npx tsx\n\nimport { spawn } from 'node:child_process';\nimport { mkdir, writeFile } from 'node:fs/promises';\nimport { dirname, join, relative as relativePath, resolve } from 'node:path';\nimport { fileURLToPath } from 'node:url';\nimport type { Endpoint } from '@geekmidas/constructs/endpoints';\nimport { kebabCase } from '@geekmidas/manifest';\nimport { loadWorkspaceConfig } from './config.js';\nimport { EndpointGenerator } from './generators/EndpointGenerator.js';\nimport { OpenApiTsGenerator } from './generators/OpenApiTsGenerator.js';\nimport type { GkmConfig, OpenApiConfig, Routes } from './types.js';\nimport { normalizeRoutes } from './workspace/client-generator.js';\nimport type { NormalizedAppConfig } from './workspace/types.js';\n\ninterface OpenAPIOptions {\n\tcwd?: string;\n\t/**\n\t * Workspace mode: generate for a single named backend app. When set,\n\t * generation runs in-process against the current CWD. When unset,\n\t * workspace mode spawns one subprocess per backend app so each gets a\n\t * fresh tsx that loads the app's own tsconfig (path aliases included).\n\t */\n\tapp?: string;\n}\n\n/**\n * Where a surface's generated client lands, under the workspace root.\n *\n * Named from the surface, so `new RestApi('Webhooks')` writes\n * `.gkm/client/webhooks.ts` — the same kebab form that gives it its container\n * name and its `WEBHOOKS_URL` — and a site imports it as\n * `@<name>/client/webhooks`, through the alias its tsconfig maps.\n */\nexport function openApiPathFor(surfaceId: string): string {\n\treturn `./.gkm/client/${kebabCase(surfaceId)}.ts`;\n}\n\n/**\n * Resolve OpenAPI config from GkmConfig\n */\nexport function resolveOpenApiConfig(config: {\n\topenapi?: boolean | OpenApiConfig;\n}): OpenApiConfig & { enabled: boolean } {\n\tif (config.openapi === false) {\n\t\treturn { enabled: false };\n\t}\n\n\tif (config.openapi === true || config.openapi === undefined) {\n\t\t// Enable by default when not explicitly set (undefined) or explicitly true\n\t\treturn {\n\t\t\tenabled: true,\n\t\t\ttitle: 'API Documentation',\n\t\t\tversion: '1.0.0',\n\t\t\tdescription: 'Auto-generated API documentation from endpoints',\n\t\t};\n\t}\n\n\treturn {\n\t\tenabled: config.openapi.enabled !== false,\n\t\ttitle: config.openapi.title || 'API Documentation',\n\t\tversion: config.openapi.version || '1.0.0',\n\t\tdescription:\n\t\t\tconfig.openapi.description ||\n\t\t\t'Auto-generated API documentation from endpoints',\n\t};\n}\n\n/** What a generation run wrote. */\nexport interface OpenApiResult {\n\t/** The first spec written — kept so existing callers still compile. */\n\toutputPath: string;\n\t/** Every spec written, one per surface. */\n\toutputPaths: string[];\n\t/** Endpoints across all of them. */\n\tendpointCount: number;\n}\n\n/**\n * Load endpoints from a glob, then generate.\n *\n * For the callers that have no endpoints in hand — `gkm openapi`, and `gkm dev`\n * regenerating on reload. The glob is the app's derived one — the conventional\n * directories under it — not a `routes` field anybody wrote.\n */\nexport async function generateOpenApiFrom(\n\troutes: Routes,\n\toptions: {\n\t\topenapi?: boolean | OpenApiConfig;\n\t\tsilent?: boolean;\n\t\tbustCache?: boolean;\n\t\troot?: string;\n\t} = {},\n): Promise<OpenApiResult | null> {\n\tconst loaded = await new EndpointGenerator().load(\n\t\troutes,\n\t\tundefined,\n\t\toptions.bustCache,\n\t);\n\n\treturn generateOpenApi(\n\t\tloaded.map(({ construct }) => construct),\n\t\toptions,\n\t);\n}\n\n/**\n * Generate an OpenAPI spec for each surface these endpoints name.\n *\n * Takes the endpoints, not a glob to find them with. The build has already\n * loaded every one of them to generate handlers, so globbing again was a second\n * discovery pass over the same files — and the reason this function needed a\n * `routes` to be told where to look, which is how a retired config field stayed\n * load-bearing.\n *\n * @returns What was written, or null if disabled or there were none\n */\nexport async function generateOpenApi(\n\tendpoints: readonly Endpoint<any, any, any, any, any>[],\n\toptions: {\n\t\topenapi?: boolean | OpenApiConfig;\n\t\tsilent?: boolean;\n\t\t/**\n\t\t * The workspace root, whose `.gkm/client/` holds every surface's client.\n\t\t * The client is the application's, like its manifest — a site reaches it\n\t\t * as `@<name>/client/<surface>` — so it is written there whichever\n\t\t * directory the build happened to run in.\n\t\t */\n\t\troot?: string;\n\t} = {},\n): Promise<OpenApiResult | null> {\n\tconst root = options.root ?? process.cwd();\n\tconst logger = options.silent ? { log: () => {} } : console;\n\tconst openApiConfig = resolveOpenApiConfig({ openapi: options.openapi });\n\n\tif (!openApiConfig.enabled) {\n\t\treturn null;\n\t}\n\n\tif (endpoints.length === 0) {\n\t\tlogger.log('No valid endpoints found for OpenAPI generation');\n\t\treturn null;\n\t}\n\n\t// One spec per surface, not one per glob.\n\t//\n\t// The glob is a directory, and a directory is not a surface: two `RestApi`s\n\t// whose endpoints live side by side produced a single spec describing both,\n\t// under a filename that named neither. Every endpoint carries the surface\n\t// that serves it, so that is what the spec is cut along — `Api` becomes\n\t// `api.ts`, `Webhooks` becomes `webhooks.ts`, by the same kebab rule that\n\t// gives the surface its container name and its environment keys.\n\tconst bySurface = new Map<string, Endpoint<any, any, any, any, any>[]>();\n\tfor (const endpoint of endpoints) {\n\t\tconst owner = endpoint.owner ?? endpoint.surface?.id;\n\t\tif (!owner) continue;\n\t\tconst existing = bySurface.get(owner);\n\t\tif (existing) existing.push(endpoint);\n\t\telse bySurface.set(owner, [endpoint]);\n\t}\n\n\tif (bySurface.size === 0) {\n\t\tlogger.log('No endpoints named a surface, so no OpenAPI was generated');\n\t\treturn null;\n\t}\n\n\tconst tsGenerator = new OpenApiTsGenerator();\n\tconst written: string[] = [];\n\n\tfor (const [surfaceId, surfaceEndpoints] of bySurface) {\n\t\tconst relative = openApiPathFor(surfaceId);\n\t\tconst outputPath = join(root, relative);\n\n\t\tawait mkdir(dirname(outputPath), { recursive: true });\n\n\t\tconst tsContent = await tsGenerator.generate(surfaceEndpoints, {\n\t\t\t// The surface's own name, so a spec says which API it describes\n\t\t\t// rather than repeating one title across all of them.\n\t\t\ttitle: openApiConfig.title ?? surfaceId,\n\t\t\tversion: openApiConfig.version!,\n\t\t\tdescription: openApiConfig.description!,\n\t\t});\n\n\t\tawait writeFile(outputPath, tsContent);\n\t\twritten.push(relative);\n\t\tlogger.log(\n\t\t\t`📄 OpenAPI client generated: ${relativePath(process.cwd(), outputPath)} (${surfaceEndpoints.length} endpoints)`,\n\t\t);\n\t}\n\n\treturn {\n\t\t// The first, for a caller that wants one path. `outputPaths` has them all.\n\t\toutputPath: join(root, written[0]!),\n\t\toutputPaths: written.map((r) => join(root, r)),\n\t\tendpointCount: endpoints.length,\n\t};\n}\n\nexport async function openapiCommand(\n\toptions: OpenAPIOptions = {},\n): Promise<void> {\n\tconst logger = console;\n\n\ttry {\n\t\tconst loadedConfig = await loadWorkspaceConfig(options.cwd);\n\n\t\tif (loadedConfig.type === 'single') {\n\t\t\t// Single-app config - use existing behavior\n\t\t\tconst config = loadedConfig.raw as GkmConfig;\n\n\t\t\t// Enable openapi if not configured\n\t\t\tif (!config.openapi) {\n\t\t\t\tconfig.openapi = { enabled: true };\n\t\t\t}\n\n\t\t\tconst result = await generateOpenApiFrom(config.constructs, {\n\t\t\t\topenapi: config.openapi,\n\t\t\t\troot: loadedConfig.workspace.root,\n\t\t\t});\n\n\t\t\tif (result) {\n\t\t\t\tlogger.log(`Found ${result.endpointCount} endpoints`);\n\t\t\t}\n\t\t} else {\n\t\t\t// Workspace config - generate for each backend app\n\t\t\tconst { workspace } = loadedConfig;\n\t\t\t// Always derive the workspace root from the loaded config rather than\n\t\t\t// CWD. Subprocesses run with CWD set to an app directory, and falling\n\t\t\t// back to CWD there causes `join(workspaceRoot, app.path)` to point at\n\t\t\t// a non-existent nested path (e.g. `apps/api/apps/api`).\n\t\t\tconst workspaceRoot = workspace.root;\n\n\t\t\t// Backend apps with openapi enabled — which is all of them that have\n\t\t\t// routes, unless one says otherwise.\n\t\t\t//\n\t\t\t// This used to require `openapi: true` written out, while the app's\n\t\t\t// own build generated a spec regardless. Two paths disagreeing about\n\t\t\t// the same flag: the app emitted `.gkm/openapi.ts` and this pass,\n\t\t\t// finding nothing \"enabled\", skipped copying it to the frontends that\n\t\t\t// consume it. A surface whose routes are declared rather than\n\t\t\t// discovered has no spec to generate, so it is not a candidate.\n\t\t\tconst backendApps = Object.entries(workspace.apps).filter(\n\t\t\t\t([_, app]) =>\n\t\t\t\t\tapp.type === 'backend' &&\n\t\t\t\t\tapp.openapi !== false &&\n\t\t\t\t\t(typeof app.openapi !== 'object' || app.openapi.enabled !== false),\n\t\t\t);\n\n\t\t\tif (backendApps.length === 0) {\n\t\t\t\tlogger.log('No backend apps with OpenAPI enabled found');\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\t// Single-app mode: generate in-process. The caller is expected to\n\t\t\t// have CWD set to the app's directory (e.g., when invoked as a\n\t\t\t// subprocess), so tsx picks up the app's tsconfig path aliases.\n\t\t\tif (options.app) {\n\t\t\t\tconst entry = backendApps.find(([name]) => name === options.app);\n\t\t\t\tif (!entry) {\n\t\t\t\t\tthrow new Error(\n\t\t\t\t\t\t`App \"${options.app}\" not found or has OpenAPI disabled`,\n\t\t\t\t\t);\n\t\t\t\t}\n\t\t\t\tconst [appName, app] = entry;\n\t\t\t\tconst result = await generateOpenApiForApp(workspaceRoot, appName, app);\n\t\t\t\tif (result) {\n\t\t\t\t\tlogger.log(\n\t\t\t\t\t\t`📄 [${appName}] Generated OpenAPI (${result.endpointCount} endpoints)`,\n\t\t\t\t\t);\n\t\t\t\t}\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\t// Multi-app mode: spawn a subprocess per backend app. Each\n\t\t\t// subprocess starts with CWD at the app's directory so that\n\t\t\t// tsx's tsconfig discovery picks up the app's `paths` aliases\n\t\t\t// (e.g., `~/*`) instead of the workspace root's tsconfig.\n\t\t\tfor (const [appName, app] of backendApps) {\n\t\t\t\t// No `app.routes` check any more: the per-kind globs are gone, and\n\t\t\t\t// an app's code is found by one glob rather than by a field naming\n\t\t\t\t// endpoints. Guarding on `routes` here meant every app was skipped\n\t\t\t\t// the moment that field stopped existing — silently, because a loop\n\t\t\t\t// that runs zero times looks exactly like one with nothing to do.\n\t\t\t\tconst appPath = join(workspaceRoot, app.path);\n\n\t\t\t\t// A subprocess exists to put CWD inside the app, so tsx picks up\n\t\t\t\t// that app's tsconfig aliases instead of the workspace root's. When\n\t\t\t\t// the app *is* the root there is nothing to change: spawning would\n\t\t\t\t// re-enter the same directory, having paid for a process and a\n\t\t\t\t// module graph, and needing `tsx` resolvable from it.\n\t\t\t\tif (resolve(appPath) === resolve(workspaceRoot)) {\n\t\t\t\t\t// Not silent: silence is for a subprocess whose output the\n\t\t\t\t\t// parent relays, and there is no parent here to relay it. The\n\t\t\t\t\t// per-surface lines come from the generator; the count is this\n\t\t\t\t\t// command's own summary, as in the single-app path.\n\t\t\t\t\tconst result = await generateOpenApiForApp(\n\t\t\t\t\t\tworkspaceRoot,\n\t\t\t\t\t\tappName,\n\t\t\t\t\t\tapp,\n\t\t\t\t\t\tfalse,\n\t\t\t\t\t);\n\t\t\t\t\tif (result) {\n\t\t\t\t\t\tlogger.log(`Found ${result.endpointCount} endpoints`);\n\t\t\t\t\t}\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\n\t\t\t\tawait runOpenApiInSubprocess(appPath, appName);\n\t\t\t}\n\t\t}\n\t} catch (error) {\n\t\tthrow new Error(`OpenAPI generation failed: ${(error as Error).message}`);\n\t}\n}\n\n/**\n * Generate OpenAPI for a single named app within a workspace.\n * Runs in-process. The caller is responsible for ensuring `process.cwd()`\n * is the app's directory so tsx loads the app's tsconfig path aliases.\n */\nasync function generateOpenApiForApp(\n\tworkspaceRoot: string,\n\t_appName: string,\n\tapp: NormalizedAppConfig,\n\t/**\n\t * Quiet by default, because the usual caller is a subprocess whose output\n\t * the parent relays. Generating in-process there is no parent to relay to,\n\t * and silence would swallow the only report of what happened.\n\t */\n\tsilent = true,\n): Promise<{ outputPath: string; endpointCount: number } | null> {\n\t// A backend app, and everything under it.\n\t//\n\t// It used to gate on `app.routes` and glob that; derive no longer sets it,\n\t// because an app has one glob and which kind a module exports is decided by\n\t// the value. A surface is an app, so an app's own directory contains exactly\n\t// its own surface's endpoints — and the split inside is by surface anyway.\n\tif (app.type !== 'backend') {\n\t\treturn null;\n\t}\n\n\tconst appPath = join(workspaceRoot, app.path);\n\tconst globs = [join(appPath, '**/*.ts')];\n\n\treturn generateOpenApiFrom(globs, {\n\t\t// Absent means enabled, which is what the caller's filter already decided\n\t\t// when it kept this app: it skips one that says `openapi: false` and keeps\n\t\t// every other. Passing `undefined` through let `resolveOpenApiConfig`\n\t\t// default it to *disabled*, so an app selected for generation generated\n\t\t// nothing and said nothing — two places disagreeing about one flag, which\n\t\t// is the failure the filter above was written to describe.\n\t\topenapi: app.openapi ?? { enabled: true },\n\t\tsilent,\n\t\troot: workspaceRoot,\n\t});\n}\n\n/**\n * Resolve the gkm bin path. Tests can override via `GKM_BIN_PATH` env var\n * to avoid depending on the built dist.\n */\nfunction resolveGkmBinPath(): string {\n\tif (process.env.GKM_BIN_PATH) {\n\t\treturn process.env.GKM_BIN_PATH;\n\t}\n\treturn fileURLToPath(new URL('../bin/gkm.mjs', import.meta.url));\n}\n\n/**\n * Spawn a subprocess that runs `gkm openapi --app <name>` with `cwd` set to\n * the app's directory. Inherits `NODE_OPTIONS` (which already contains\n * `--import tsx`), so the child gets a fresh tsx instance whose tsconfig\n * discovery picks up the app's tsconfig path aliases.\n */\nasync function runOpenApiInSubprocess(\n\tappCwd: string,\n\tappName: string,\n): Promise<void> {\n\tconst binPath = resolveGkmBinPath();\n\n\tawait new Promise<void>((resolve, reject) => {\n\t\tconst child = spawn(\n\t\t\tprocess.execPath,\n\t\t\t[binPath, 'openapi', '--app', appName],\n\t\t\t{\n\t\t\t\tcwd: appCwd,\n\t\t\t\tstdio: 'inherit',\n\t\t\t\tenv: process.env,\n\t\t\t},\n\t\t);\n\n\t\tchild.on('error', reject);\n\t\tchild.on('close', (code) => {\n\t\t\tif (code === 0) {\n\t\t\t\tresolve();\n\t\t\t} else {\n\t\t\t\treject(\n\t\t\t\t\tnew Error(\n\t\t\t\t\t\t`OpenAPI generation for app \"${appName}\" exited with code ${code}`,\n\t\t\t\t\t),\n\t\t\t\t);\n\t\t\t}\n\t\t});\n\t});\n}\n"],"mappings":";;;;;;;;;;;;;;;;;AAkCA,SAAgB,eAAe,WAA2B;CACzD,OAAO,iBAAiB,UAAU,SAAS,EAAE;AAC9C;;;;AAKA,SAAgB,qBAAqB,QAEI;CACxC,IAAI,OAAO,YAAY,OACtB,OAAO,EAAE,SAAS,MAAM;CAGzB,IAAI,OAAO,YAAY,QAAQ,OAAO,YAAY,KAAA,GAEjD,OAAO;EACN,SAAS;EACT,OAAO;EACP,SAAS;EACT,aAAa;CACd;CAGD,OAAO;EACN,SAAS,OAAO,QAAQ,YAAY;EACpC,OAAO,OAAO,QAAQ,SAAS;EAC/B,SAAS,OAAO,QAAQ,WAAW;EACnC,aACC,OAAO,QAAQ,eACf;CACF;AACD;;;;;;;;AAmBA,eAAsB,oBACrB,QACA,UAKI,CAAC,GAC2B;CAOhC,OAAO,iBACN,MAPoB,IAAI,kBAAkB,CAAC,CAAC,KAC5C,QACA,KAAA,GACA,QAAQ,SACT,EAAA,CAGQ,KAAK,EAAE,gBAAgB,SAAS,GACvC,OACD;AACD;;;;;;;;;;;;AAaA,eAAsB,gBACrB,WACA,UAUI,CAAC,GAC2B;CAChC,MAAM,OAAO,QAAQ,QAAQ,QAAQ,IAAI;CACzC,MAAM,SAAS,QAAQ,SAAS,EAAE,WAAW,CAAC,EAAE,IAAI;CACpD,MAAM,gBAAgB,qBAAqB,EAAE,SAAS,QAAQ,QAAQ,CAAC;CAEvE,IAAI,CAAC,cAAc,SAClB,OAAO;CAGR,IAAI,UAAU,WAAW,GAAG;EAC3B,OAAO,IAAI,iDAAiD;EAC5D,OAAO;CACR;CAUA,MAAM,4BAAY,IAAI,IAAiD;CACvE,KAAK,MAAM,YAAY,WAAW;EACjC,MAAM,QAAQ,SAAS,SAAS,SAAS,SAAS;EAClD,IAAI,CAAC,OAAO;EACZ,MAAM,WAAW,UAAU,IAAI,KAAK;EACpC,IAAI,UAAU,SAAS,KAAK,QAAQ;OAC/B,UAAU,IAAI,OAAO,CAAC,QAAQ,CAAC;CACrC;CAEA,IAAI,UAAU,SAAS,GAAG;EACzB,OAAO,IAAI,2DAA2D;EACtE,OAAO;CACR;CAEA,MAAM,cAAc,IAAI,mBAAmB;CAC3C,MAAM,UAAoB,CAAC;CAE3B,KAAK,MAAM,CAAC,WAAW,qBAAqB,WAAW;EACtD,MAAMA,aAAW,eAAe,SAAS;EACzC,MAAM,aAAa,KAAK,MAAMA,UAAQ;EAEtC,MAAM,MAAM,QAAQ,UAAU,GAAG,EAAE,WAAW,KAAK,CAAC;EAEpD,MAAM,YAAY,MAAM,YAAY,SAAS,kBAAkB;GAG9D,OAAO,cAAc,SAAS;GAC9B,SAAS,cAAc;GACvB,aAAa,cAAc;EAC5B,CAAC;EAED,MAAM,UAAU,YAAY,SAAS;EACrC,QAAQ,KAAKA,UAAQ;EACrB,OAAO,IACN,gCAAgCC,SAAa,QAAQ,IAAI,GAAG,UAAU,EAAE,IAAI,iBAAiB,OAAO,YACrG;CACD;CAEA,OAAO;EAEN,YAAY,KAAK,MAAM,QAAQ,EAAG;EAClC,aAAa,QAAQ,KAAK,MAAM,KAAK,MAAM,CAAC,CAAC;EAC7C,eAAe,UAAU;CAC1B;AACD;AAEA,eAAsB,eACrB,UAA0B,CAAC,GACX;CAChB,MAAM,SAAS;CAEf,IAAI;EACH,MAAM,eAAe,MAAM,oBAAoB,QAAQ,GAAG;EAE1D,IAAI,aAAa,SAAS,UAAU;GAEnC,MAAM,SAAS,aAAa;GAG5B,IAAI,CAAC,OAAO,SACX,OAAO,UAAU,EAAE,SAAS,KAAK;GAGlC,MAAM,SAAS,MAAM,oBAAoB,OAAO,YAAY;IAC3D,SAAS,OAAO;IAChB,MAAM,aAAa,UAAU;GAC9B,CAAC;GAED,IAAI,QACH,OAAO,IAAI,SAAS,OAAO,cAAc,WAAW;EAEtD,OAAO;GAEN,MAAM,EAAE,cAAc;GAKtB,MAAM,gBAAgB,UAAU;GAWhC,MAAM,cAAc,OAAO,QAAQ,UAAU,IAAI,CAAC,CAAC,QACjD,CAAC,GAAG,SACJ,IAAI,SAAS,aACb,IAAI,YAAY,UACf,OAAO,IAAI,YAAY,YAAY,IAAI,QAAQ,YAAY,MAC9D;GAEA,IAAI,YAAY,WAAW,GAAG;IAC7B,OAAO,IAAI,4CAA4C;IACvD;GACD;GAKA,IAAI,QAAQ,KAAK;IAChB,MAAM,QAAQ,YAAY,MAAM,CAAC,UAAU,SAAS,QAAQ,GAAG;IAC/D,IAAI,CAAC,OACJ,MAAM,IAAI,MACT,QAAQ,QAAQ,IAAI,oCACrB;IAED,MAAM,CAAC,SAAS,OAAO;IACvB,MAAM,SAAS,MAAM,sBAAsB,eAAe,SAAS,GAAG;IACtE,IAAI,QACH,OAAO,IACN,OAAO,QAAQ,uBAAuB,OAAO,cAAc,YAC5D;IAED;GACD;GAMA,KAAK,MAAM,CAAC,SAAS,QAAQ,aAAa;IAMzC,MAAM,UAAU,KAAK,eAAe,IAAI,IAAI;IAO5C,IAAI,QAAQ,OAAO,MAAM,QAAQ,aAAa,GAAG;KAKhD,MAAM,SAAS,MAAM,sBACpB,eACA,SACA,KACA,KACD;KACA,IAAI,QACH,OAAO,IAAI,SAAS,OAAO,cAAc,WAAW;KAErD;IACD;IAEA,MAAM,uBAAuB,SAAS,OAAO;GAC9C;EACD;CACD,SAAS,OAAO;EACf,MAAM,IAAI,MAAM,8BAA+B,MAAgB,SAAS;CACzE;AACD;;;;;;AAOA,eAAe,sBACd,eACA,UACA,KAMA,SAAS,MACuD;CAOhE,IAAI,IAAI,SAAS,WAChB,OAAO;CAGR,MAAM,UAAU,KAAK,eAAe,IAAI,IAAI;CAG5C,OAAO,oBAAoB,CAFZ,KAAK,SAAS,SAAS,CAEP,GAAG;EAOjC,SAAS,IAAI,WAAW,EAAE,SAAS,KAAK;EACxC;EACA,MAAM;CACP,CAAC;AACF;;;;;AAMA,SAAS,oBAA4B;CACpC,IAAI,QAAQ,IAAI,cACf,OAAO,QAAQ,IAAI;CAEpB,OAAO,cAAc,IAAI,IAAI,kBAAkB,YAAY,GAAG,CAAC;AAChE;;;;;;;AAQA,eAAe,uBACd,QACA,SACgB;CAChB,MAAM,UAAU,kBAAkB;CAElC,MAAM,IAAI,SAAe,SAAS,WAAW;EAC5C,MAAM,QAAQ,MACb,QAAQ,UACR;GAAC;GAAS;GAAW;GAAS;EAAO,GACrC;GACC,KAAK;GACL,OAAO;GACP,KAAK,QAAQ;EACd,CACD;EAEA,MAAM,GAAG,SAAS,MAAM;EACxB,MAAM,GAAG,UAAU,SAAS;GAC3B,IAAI,SAAS,GACZ,QAAQ;QAER,uBACC,IAAI,MACH,+BAA+B,QAAQ,qBAAqB,MAC7D,CACD;EAEF,CAAC;CACF,CAAC;AACF"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@geekmidas/cli",
3
- "version": "10.0.0-alpha.48",
3
+ "version": "10.0.0-alpha.49",
4
4
  "description": "CLI tools for building Lambda handlers, server applications, and generating OpenAPI specs",
5
5
  "private": false,
6
6
  "type": "module",
@@ -98,15 +98,15 @@
98
98
  "prompts": "~2.4.2",
99
99
  "tsx": "~4.23.15",
100
100
  "yaml": "~2.9.1",
101
- "@geekmidas/cache": "10.0.0-alpha.48",
102
- "@geekmidas/constructs": "~10.0.0-alpha.48",
103
- "@geekmidas/db": "10.0.0-alpha.48",
104
- "@geekmidas/envkit": "~10.0.0-alpha.48",
105
- "@geekmidas/errors": "~10.0.0-alpha.48",
106
- "@geekmidas/logger": "~10.0.0-alpha.48",
107
- "@geekmidas/manifest": "10.0.0-alpha.48",
108
- "@geekmidas/schema": "~10.0.0-alpha.48",
109
- "@geekmidas/services": "~10.0.0-alpha.48"
101
+ "@geekmidas/cache": "10.0.0-alpha.49",
102
+ "@geekmidas/constructs": "~10.0.0-alpha.49",
103
+ "@geekmidas/db": "10.0.0-alpha.49",
104
+ "@geekmidas/envkit": "~10.0.0-alpha.49",
105
+ "@geekmidas/errors": "~10.0.0-alpha.49",
106
+ "@geekmidas/logger": "~10.0.0-alpha.49",
107
+ "@geekmidas/manifest": "10.0.0-alpha.49",
108
+ "@geekmidas/schema": "~10.0.0-alpha.49",
109
+ "@geekmidas/services": "~10.0.0-alpha.49"
110
110
  },
111
111
  "devDependencies": {
112
112
  "@hono/node-server": "~1.19.5",
@@ -120,12 +120,12 @@
120
120
  "typescript": "~7.0.2",
121
121
  "vitest": "~5.0.2",
122
122
  "zod": "~4.6.5",
123
- "@geekmidas/events": "~10.0.0-alpha.48",
124
- "@geekmidas/studio": "~10.0.0-alpha.48",
125
- "@geekmidas/testkit": "10.0.0-alpha.48"
123
+ "@geekmidas/events": "~10.0.0-alpha.49",
124
+ "@geekmidas/studio": "~10.0.0-alpha.49",
125
+ "@geekmidas/testkit": "10.0.0-alpha.49"
126
126
  },
127
127
  "peerDependencies": {
128
- "@geekmidas/telescope": "~10.0.0-alpha.48"
128
+ "@geekmidas/telescope": "~10.0.0-alpha.49"
129
129
  },
130
130
  "peerDependenciesMeta": {
131
131
  "@geekmidas/telescope": {