@vivero/stoma-cli 0.1.0-rc.1

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.
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/cli.ts","../src/commands/run.ts","../src/gateway/resolve.ts","../src/playground/html.ts","../src/playground/oauth-relay.ts","../src/playground/wrap.ts","../src/server/serve.ts","../src/utils/logger.ts","../src/utils/version.ts","../src/bin.ts"],"sourcesContent":["import { Builtins, Cli } from \"clipanion\";\nimport { RunCommand } from \"./commands/index.js\";\nimport { getVersion } from \"./utils/version.js\";\n\nexport function createCli() {\n const cli = new Cli({\n binaryLabel: \"stoma\",\n binaryName: \"stoma\",\n binaryVersion: getVersion(),\n });\n\n cli.register(Builtins.HelpCommand);\n cli.register(Builtins.VersionCommand);\n cli.register(RunCommand);\n\n return cli;\n}\n\nexport async function runCli(argv: string[]) {\n const cli = createCli();\n await cli.runExit(argv);\n}\n","import path from \"node:path\";\nimport { Command, Option } from \"clipanion\";\nimport { resolveGateway } from \"../gateway/resolve.js\";\nimport { wrapWithPlayground } from \"../playground/wrap.js\";\nimport { startServer } from \"../server/serve.js\";\nimport { createLogger } from \"../utils/logger.js\";\n\nexport class RunCommand extends Command {\n static override paths = [[\"run\"]];\n\n static override usage = Command.Usage({\n description: \"Start a local HTTP server for a Stoma gateway file\",\n details: `\n Loads a compiled gateway JS file (or TypeScript with tsx) and serves it\n on a local Node.js HTTP server.\n\n The file must export a gateway instance, a factory function, or a\n \\`createPlaygroundGateway\\` named export.\n `,\n examples: [\n [\"Run a compiled gateway file\", \"stoma run ./my-gateway.js\"],\n [\"Run on a custom port\", \"stoma run ./my-gateway.js --port 3000\"],\n [\"Run with debug logging\", \"stoma run ./my-gateway.js --debug\"],\n [\n \"Run a remote gateway file\",\n \"stoma run https://example.com/gateway.ts --trust-remote\",\n ],\n ],\n });\n\n file = Option.String({ required: true });\n\n port = Option.String(\"--port,-p\", \"8787\", {\n description: \"Port to listen on (default: 8787)\",\n });\n\n host = Option.String(\"--host,-H\", \"localhost\", {\n description: \"Hostname to bind to (default: localhost)\",\n });\n\n debug = Option.Boolean(\"--debug,-d\", false, {\n description: \"Enable gateway debug logging\",\n });\n\n verbose = Option.Boolean(\"--verbose,-v\", false, {\n description: \"Verbose CLI output\",\n });\n\n playground = Option.Boolean(\"--playground\", false, {\n description: \"Serve interactive playground UI at /__playground\",\n });\n\n trustRemote = Option.Boolean(\"--trust-remote\", false, {\n description:\n \"Allow loading gateway files from remote URLs (downloads and executes code — use only with trusted sources)\",\n });\n\n async execute() {\n const log = createLogger(this.verbose);\n const isRemote =\n this.file.startsWith(\"http://\") || this.file.startsWith(\"https://\");\n const filePath = isRemote ? this.file : path.resolve(this.file);\n const portNum = parseInt(this.port, 10);\n\n if (Number.isNaN(portNum) || portNum < 0 || portNum > 65535) {\n log.error(`Invalid port: ${this.port}`);\n return 1;\n }\n\n log.info(`Loading gateway from ${filePath}`);\n\n let gateway;\n try {\n gateway = await resolveGateway(filePath, {\n debug: this.debug,\n trustRemote: this.trustRemote,\n });\n } catch (err) {\n log.error(\n `Failed to load gateway: ${err instanceof Error ? err.message : String(err)}`\n );\n return 1;\n }\n\n log.info(\n `Gateway \"${gateway.name}\" loaded (${gateway.routeCount} route${gateway.routeCount === 1 ? \"\" : \"s\"})`\n );\n\n if (this.verbose && gateway._registry) {\n for (const route of gateway._registry.routes) {\n const methods = Array.isArray(route.methods)\n ? route.methods.join(\",\")\n : \"ALL\";\n log.verbose(` ${methods.padEnd(8)} ${route.path}`);\n }\n }\n\n try {\n const fetch =\n this.playground && gateway._registry\n ? wrapWithPlayground(gateway.app.fetch, gateway._registry)\n : gateway.app.fetch;\n\n const server = await startServer({\n fetch,\n port: portNum,\n hostname: this.host,\n });\n\n log.info(\n `Gateway \"${gateway.name}\" listening on http://${this.host}:${portNum}`\n );\n\n if (this.playground) {\n log.info(`Playground: http://${this.host}:${portNum}/__playground`);\n }\n\n if (gateway._registry) {\n for (const route of gateway._registry.routes) {\n const methods = Array.isArray(route.methods)\n ? route.methods.join(\",\")\n : \"ALL\";\n log.info(` ${methods.padEnd(8)} ${route.path}`);\n }\n }\n\n await new Promise<void>((resolve) => {\n let shuttingDown = false;\n\n const shutdown = () => {\n if (shuttingDown) {\n log.info(\"\\nForce exit\");\n process.exit(1);\n }\n shuttingDown = true;\n log.info(\"\\nShutting down...\");\n\n // Force exit after 3s if graceful shutdown stalls\n const forceExit = setTimeout(() => {\n log.error(\"Graceful shutdown timed out, forcing exit\");\n process.exit(1);\n }, 3_000);\n forceExit.unref();\n\n server.closeAllConnections();\n server.close(() => {\n clearTimeout(forceExit);\n resolve();\n });\n };\n\n process.on(\"SIGINT\", shutdown);\n process.on(\"SIGTERM\", shutdown);\n });\n\n // Clipanion's runExit() only sets process.exitCode — it doesn't\n // call process.exit(). If @hono/node-server leaves lingering handles\n // the event loop never drains and the process hangs.\n return process.exit(0) as never;\n } catch (err) {\n log.error(\n `Server error: ${err instanceof Error ? err.message : String(err)}`\n );\n return 1;\n }\n }\n}\n","import { existsSync } from \"node:fs\";\nimport { mkdtemp, rm, unlink, writeFile } from \"node:fs/promises\";\nimport { createRequire } from \"node:module\";\nimport { tmpdir } from \"node:os\";\nimport path from \"node:path\";\nimport { pathToFileURL } from \"node:url\";\nimport { build } from \"esbuild\";\nimport type { GatewayInstance } from \"./types.js\";\n\nexport interface ResolveOptions {\n debug?: boolean;\n /** Allow fetching gateway files from remote URLs. */\n trustRemote?: boolean;\n}\n\nconst TS_EXTENSIONS = [\".ts\", \".tsx\", \".mts\"];\n\n/**\n * Dynamic-import a gateway file and resolve its export to a GatewayInstance.\n *\n * TypeScript files are automatically transpiled via esbuild before importing.\n * Remote URLs (http/https) are supported when `trustRemote` is set.\n */\nexport async function resolveGateway(\n filePathOrUrl: string,\n options: ResolveOptions = {}\n): Promise<GatewayInstance> {\n if (isRemoteUrl(filePathOrUrl)) {\n if (!options.trustRemote) {\n throw new Error(\n \"Remote URLs require the --trust-remote flag.\\n\" +\n \"This will download and execute code from the URL. Only use with trusted sources.\"\n );\n }\n return resolveRemoteGateway(filePathOrUrl, options);\n }\n\n if (!existsSync(filePathOrUrl)) {\n throw new Error(`File not found: ${filePathOrUrl}`);\n }\n\n return resolveLocalFile(filePathOrUrl, options);\n}\n\nasync function resolveLocalFile(\n filePath: string,\n options: ResolveOptions\n): Promise<GatewayInstance> {\n const isTypeScript = TS_EXTENSIONS.some((ext) => filePath.endsWith(ext));\n\n let mod: Record<string, unknown>;\n\n if (isTypeScript) {\n mod = await importTypeScript(filePath);\n } else {\n try {\n mod = await import(pathToFileURL(filePath).href);\n } catch (err) {\n throw new Error(\n `Failed to import: ${filePath}\\n${err instanceof Error ? err.message : String(err)}`\n );\n }\n }\n\n return resolveFromModule(mod, options);\n}\n\nfunction isRemoteUrl(input: string): boolean {\n return input.startsWith(\"http://\") || input.startsWith(\"https://\");\n}\n\n/**\n * Fetch a remote gateway file to a temp directory, resolve it, then clean up.\n */\nasync function resolveRemoteGateway(\n url: string,\n options: ResolveOptions\n): Promise<GatewayInstance> {\n const res = await fetch(url);\n if (!res.ok) {\n throw new Error(\n `Failed to fetch remote gateway: ${res.status} ${res.statusText}`\n );\n }\n\n const filename = filenameFromUrl(url, res.headers.get(\"content-type\"));\n const tmpDir = await mkdtemp(path.join(tmpdir(), \"stoma-remote-\"));\n const tmpPath = path.join(tmpDir, filename);\n\n try {\n await writeFile(tmpPath, await res.text(), \"utf-8\");\n return await resolveLocalFile(tmpPath, options);\n } finally {\n await unlink(tmpPath).catch(() => {});\n await rm(tmpDir, { recursive: true, force: true }).catch(() => {});\n }\n}\n\n/**\n * Derive a filename (with extension) from a URL path and optional content-type.\n * Falls back to `.ts` so esbuild can transpile unknown sources.\n */\nfunction filenameFromUrl(url: string, contentType: string | null): string {\n const pathname = new URL(url).pathname;\n const basename = path.basename(pathname);\n\n // If the URL has a recognisable extension, use it\n if (/\\.(ts|tsx|mts|js|mjs|cjs)$/.test(basename)) {\n return basename;\n }\n\n // Infer from content-type\n if (contentType?.includes(\"javascript\")) {\n return \"gateway.mjs\";\n }\n if (contentType?.includes(\"typescript\")) {\n return \"gateway.ts\";\n }\n\n // Default to TypeScript — esbuild can handle both TS and JS\n return basename ? `${basename}.ts` : \"gateway.ts\";\n}\n\n/**\n * Get the node_modules search paths from the CLI's own install location.\n * Uses Node's own resolution algorithm via createRequire, so it works\n * correctly regardless of monorepo hoisting, Yarn PnP, or flat installs.\n */\nfunction getCliNodePaths(): string[] {\n const require = createRequire(import.meta.url);\n return (require.resolve.paths(\"@vivero/stoma\") ?? []).filter(existsSync);\n}\n\n/**\n * Transpile a TypeScript file with esbuild and import the result.\n *\n * Bundles all dependencies into a self-contained JS file so the output\n * runs without any npm install — matching how the docs editor compiles\n * gateway configs. The temp file is written next to the source and\n * cleaned up after import.\n *\n * Uses `nodePaths` from the CLI's own install so that `@vivero/stoma`\n * and `hono` are always available, even when the gateway file lives\n * outside any project (e.g. ~/Downloads/).\n */\nasync function importTypeScript(\n filePath: string\n): Promise<Record<string, unknown>> {\n const tmpFile = filePath.replace(/\\.tsx?$/, `.stoma-tmp-${Date.now()}.mjs`);\n\n try {\n const result = await build({\n entryPoints: [filePath],\n bundle: true,\n format: \"esm\",\n platform: \"node\",\n target: \"node20\",\n nodePaths: getCliNodePaths(),\n write: false,\n logLevel: \"silent\",\n });\n\n if (!result.outputFiles?.length) {\n throw new Error(\"TypeScript transpilation produced no output\");\n }\n\n await writeFile(tmpFile, result.outputFiles[0].text, \"utf-8\");\n return await import(pathToFileURL(tmpFile).href);\n } catch (err) {\n if (\n err instanceof Error &&\n err.message.includes(\"TypeScript transpilation\")\n ) {\n throw err;\n }\n throw new Error(\n `Failed to transpile TypeScript: ${filePath}\\n${err instanceof Error ? err.message : String(err)}`\n );\n } finally {\n await unlink(tmpFile).catch(() => {});\n }\n}\n\n/**\n * Resolve a GatewayInstance from a module's exports.\n *\n * Resolution order:\n * 1. `mod.createPlaygroundGateway` (function) — backward compat with editor snippets\n * 2. `mod.default` (function) — call as async factory\n * 3. `mod.default` (object with `.app` + `._registry`) — GatewayInstance directly\n * 4. `mod.default` (object with `.fetch`) — bare Hono app, wrap in minimal instance\n * 5. Throw descriptive error\n */\nexport async function resolveFromModule(\n mod: Record<string, unknown>,\n _options: ResolveOptions = {}\n): Promise<GatewayInstance> {\n // 1. Named export: createPlaygroundGateway()\n if (typeof mod.createPlaygroundGateway === \"function\") {\n const result = await mod.createPlaygroundGateway();\n return asGatewayInstance(result, \"createPlaygroundGateway()\");\n }\n\n // 2. Default export: factory function\n if (typeof mod.default === \"function\") {\n const result = await mod.default();\n return asGatewayInstance(result, \"default()\");\n }\n\n // 3. Default export: GatewayInstance directly\n if (isGatewayInstance(mod.default)) {\n return mod.default;\n }\n\n // 4. Default export: bare Hono app (has .fetch)\n if (isHonoApp(mod.default)) {\n return {\n app: mod.default,\n name: \"unnamed-gateway\",\n routeCount: 0,\n _registry: { routes: [], policies: [], gatewayName: \"unnamed-gateway\" },\n };\n }\n\n throw new Error(\n \"Could not resolve a gateway from the module exports.\\n\\n\" +\n \"The file must export a gateway in one of these forms:\\n\" +\n \" export default createGateway({ ... })\\n\" +\n \" export default async function() { return createGateway({ ... }) }\\n\" +\n \" export function createPlaygroundGateway() { return createGateway({ ... }) }\\n\" +\n \" export default app // a Hono app with a .fetch method\"\n );\n}\n\nfunction isGatewayInstance(value: unknown): value is GatewayInstance {\n return (\n typeof value === \"object\" &&\n value !== null &&\n \"app\" in value &&\n \"_registry\" in value\n );\n}\n\nfunction isHonoApp(\n value: unknown\n): value is { fetch: (req: Request) => Response | Promise<Response> } {\n return (\n typeof value === \"object\" &&\n value !== null &&\n \"fetch\" in value &&\n typeof (value as Record<string, unknown>).fetch === \"function\"\n );\n}\n\nfunction asGatewayInstance(value: unknown, source: string): GatewayInstance {\n if (isGatewayInstance(value)) {\n return value;\n }\n if (isHonoApp(value)) {\n return {\n app: value,\n name: \"unnamed-gateway\",\n routeCount: 0,\n _registry: { routes: [], policies: [], gatewayName: \"unnamed-gateway\" },\n };\n }\n throw new Error(\n `${source} did not return a valid gateway instance. ` +\n \"Expected an object with .app and ._registry properties, or a Hono app with a .fetch method.\"\n );\n}\n","import type { GatewayRegistry } from \"../gateway/types.js\";\n\nexport function playgroundHtml(registry: GatewayRegistry): string {\n const registryJson = JSON.stringify(registry).replace(/</g, \"\\\\u003c\");\n\n return `<!DOCTYPE html>\n<html lang=\"en\">\n<head>\n<meta charset=\"utf-8\" />\n<meta name=\"viewport\" content=\"width=device-width, initial-scale=1\" />\n<title>${esc(registry.gatewayName)} — Stoma Playground</title>\n<style>\n*,*::before,*::after{box-sizing:border-box;margin:0;padding:0}\n:root{\n --bg:#1e1e1e;--bg2:#252526;--bg3:#2d2d2d;\n --border:#3c3c3c;--text:#d4d4d4;--muted:#969696;--dim:#555;\n --teal:#4ec9b0;--blue:#0e639c;--blue2:#1177bb;\n --yellow:#dcdcaa;--red:#f48771;--blue-text:#569cd6;\n --mono:\"Cascadia Code\",\"Fira Code\",\"JetBrains Mono\",\"SF Mono\",monospace;\n --sans:-apple-system,BlinkMacSystemFont,\"Segoe UI\",Roboto,sans-serif;\n}\nhtml,body{height:100%;background:var(--bg);color:var(--text);font-family:var(--sans)}\nbody{display:flex;flex-direction:column;overflow:hidden}\n\n/* Header */\n.hdr{display:flex;align-items:center;justify-content:space-between;height:3rem;padding:0 1rem;background:var(--bg2);border-bottom:1px solid var(--border);flex-shrink:0}\n.hdr-left{display:flex;align-items:center;gap:.5rem}\n.logo{font-weight:700;font-size:.875rem;color:var(--teal);text-decoration:none}\n.hdr-div{color:var(--dim);font-size:.875rem}\n.hdr-title{font-size:.8125rem;color:var(--muted)}\n\n/* Routes bar */\n.routes-bar{padding:.5rem 1rem;display:flex;flex-wrap:wrap;gap:.25rem;flex-shrink:0;border-bottom:1px solid var(--border);background:var(--bg)}\n.chip{all:unset;display:inline-flex;align-items:center;gap:.375rem;height:1.5rem;padding:0 .5rem;border-radius:.25rem;border:1px solid var(--border);background:var(--bg3);font-family:var(--mono);font-size:.6875rem;cursor:pointer;user-select:none;transition:border-color .15s,background .15s}\n.chip:hover{border-color:var(--blue);background:#333}\n.method{font-size:.5625rem;font-weight:700;letter-spacing:.04em;padding:.0625rem .25rem;border-radius:.125rem}\n.m-get{background:var(--teal);color:var(--bg)}\n.m-post{background:var(--blue-text);color:#fff}\n.m-put,.m-patch{background:var(--yellow);color:var(--bg)}\n.m-delete{background:var(--red);color:var(--bg)}\n\n/* Workspace — two pane split */\n.workspace{flex:1;display:flex;gap:.75rem;padding:.75rem 1rem;overflow:hidden}\n.pane-left{flex:1;display:flex;flex-direction:column;gap:.625rem;overflow-y:auto;min-width:0}\n.pane-right{flex:1;display:flex;flex-direction:column;min-width:0;overflow:hidden}\n\n/* Token Store */\n.token-store{border:1px solid var(--border);border-radius:.25rem;background:var(--bg2);overflow:hidden;flex-shrink:0}\n.token-header{display:flex;align-items:center;justify-content:space-between;padding:.375rem .625rem;background:var(--bg3);border-bottom:1px solid var(--border)}\n.token-title{font-size:.625rem;font-weight:700;text-transform:uppercase;letter-spacing:.08em;color:var(--muted)}\n.token-item{display:flex;align-items:center;gap:.375rem;padding:.25rem .625rem;border-bottom:1px solid rgba(60,60,60,.2)}\n.token-item:last-child{border-bottom:none}\n.token-name{font-size:.6875rem;font-family:var(--mono);color:var(--teal);min-width:6rem;flex-shrink:0}\n.token-val{font-size:.6875rem;font-family:var(--mono);color:var(--dim);flex:1;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}\n.token-actions{display:flex;gap:.25rem;flex-shrink:0}\n.token-btn{all:unset;display:inline-flex;align-items:center;height:1.25rem;padding:0 .375rem;border-radius:.1875rem;font-size:.5625rem;font-weight:600;cursor:pointer;transition:background .15s;user-select:none}\n.token-btn-use{background:var(--blue);color:#fff}\n.token-btn-use:hover{background:var(--blue2)}\n.token-btn-del{background:var(--bg3);color:var(--muted);border:1px solid var(--border)}\n.token-btn-del:hover{background:#3a3a3a}\n\n/* Form */\n.form-row{display:flex;gap:.375rem}\n.sel{width:5.5rem;height:1.75rem;padding:0 .375rem;border-radius:.25rem;border:1px solid var(--border);background:var(--bg3);color:var(--text);font-size:.75rem;font-family:var(--mono);cursor:pointer;outline:none;flex-shrink:0}\n.sel:focus{border-color:var(--blue)}\n.inp{flex:1;height:1.75rem;padding:0 .5rem;border-radius:.25rem;border:1px solid var(--border);background:var(--bg3);color:var(--text);font-size:.75rem;font-family:var(--mono);outline:none;min-width:0}\n.inp:focus{border-color:var(--blue)}\n.btn{all:unset;display:inline-flex;align-items:center;height:1.75rem;padding:0 .75rem;border-radius:.25rem;background:var(--blue);color:#fff;font-size:.75rem;font-weight:600;cursor:pointer;transition:background .15s;user-select:none;white-space:nowrap}\n.btn:hover{background:var(--blue2)}\n.btn:disabled{opacity:.4;cursor:not-allowed}\n\n.form-sections{display:flex;flex-direction:column;gap:.375rem;margin-top:.375rem}\n.field{display:flex;flex-direction:column;gap:.125rem}\n.lbl{font-size:.5625rem;font-weight:700;text-transform:uppercase;letter-spacing:.08em;color:var(--muted)}\n.lbl-row{display:flex;align-items:center;justify-content:space-between}\n.sm-btn{all:unset;font-size:.5625rem;font-weight:600;color:var(--blue-text);cursor:pointer;user-select:none}\n.sm-btn:hover{text-decoration:underline}\n.ta{width:100%;padding:.375rem .5rem;border-radius:.25rem;border:1px solid var(--border);background:var(--bg3);color:var(--text);font-size:.6875rem;font-family:var(--mono);line-height:1.4;resize:vertical;outline:none;flex:1;min-height:3.5rem}\n.ta:focus{border-color:var(--blue)}\n.ta::placeholder{color:var(--dim)}\n\n/* Header rows */\n.hdr-rows{display:flex;flex-direction:column;gap:.25rem}\n.hdr-row{display:flex;gap:.25rem;align-items:center}\n.hdr-input{height:1.5rem;padding:0 .375rem;border-radius:.25rem;border:1px solid var(--border);background:var(--bg3);color:var(--text);font-size:.6875rem;font-family:var(--mono);outline:none}\n.hdr-input:focus{border-color:var(--blue)}\n.hdr-name{width:10rem;flex-shrink:0}\n.hdr-val{flex:1;min-width:0}\n.hdr-del{all:unset;display:inline-flex;align-items:center;justify-content:center;width:1.5rem;height:1.5rem;border-radius:.25rem;font-size:.75rem;color:var(--dim);cursor:pointer;flex-shrink:0}\n.hdr-del:hover{color:var(--red);background:rgba(244,135,113,.1)}\n\n/* Callback Banner */\n.callback-banner{padding:.625rem .75rem;background:#1a2332;border:1px solid var(--blue-text);border-radius:.25rem;font-size:.75rem;color:var(--blue-text);flex-shrink:0}\n.callback-banner strong{color:#fff}\n\n/* Response panel */\n.res{flex:1;display:flex;flex-direction:column;border:1px solid var(--border);border-radius:.25rem;background:var(--bg2);overflow:hidden}\n.res-empty{padding:2rem;text-align:center;color:var(--dim);font-size:.8125rem}\n.res-top{display:flex;align-items:center;gap:.75rem;padding:.5rem .75rem;background:var(--bg2);border-bottom:1px solid var(--border);flex-shrink:0}\n.badge{display:inline-flex;align-items:center;height:1.25rem;padding:0 .375rem;border-radius:.1875rem;font-family:var(--mono);font-size:.6875rem;font-weight:700;line-height:1}\n.b-2xx{background:var(--teal);color:var(--bg)}\n.b-3xx{background:var(--blue-text);color:#fff}\n.b-4xx{background:var(--yellow);color:var(--bg)}\n.b-5xx{background:var(--red);color:var(--bg)}\n.timing{font-size:.75rem;font-family:var(--mono);color:var(--muted)}\n\n/* Response tabs */\n.res-tabs{display:flex;background:var(--bg3);border-bottom:1px solid var(--border);flex-shrink:0}\n.res-tab{all:unset;padding:.375rem .75rem;font-size:.6875rem;font-weight:600;color:var(--muted);cursor:pointer;border-bottom:2px solid transparent;transition:color .15s,border-color .15s;user-select:none}\n.res-tab:hover{color:var(--text)}\n.res-tab.active{color:var(--teal);border-bottom-color:var(--teal)}\n.res-tab-count{font-weight:400;color:var(--dim);margin-left:.25rem}\n\n/* Tab content */\n.res-tab-content{flex:1;overflow:auto;display:none}\n.res-tab-content.active{display:block}\n.res-body{padding:.5rem .75rem}\n.res-body pre{font-family:var(--mono);font-size:.6875rem;line-height:1.5;white-space:pre-wrap;word-break:break-word;margin:0}\n.htable{width:100%;border-collapse:collapse;font-size:.6875rem;font-family:var(--mono)}\n.htable td{padding:.25rem .75rem;border-bottom:1px solid rgba(60,60,60,.2);vertical-align:top}\n.htable td:first-child{color:var(--muted);white-space:nowrap;width:1%;padding-right:.375rem}\n.htable td:last-child{word-break:break-all}\n\n.res-error{padding:1rem;color:var(--red);font-family:var(--mono);font-size:.8125rem;text-align:center;word-break:break-word}\n\n/* Redirect banner (OAuth) */\n.res-redirect{padding:.75rem;background:#1a2332;border-bottom:1px solid var(--border);flex-shrink:0}\n.res-redirect-title{font-size:.75rem;font-weight:700;color:var(--blue-text);margin-bottom:.25rem}\n.res-redirect-url{font-size:.6875rem;font-family:var(--mono);color:var(--muted);word-break:break-all;margin-bottom:.5rem}\n.res-redirect-btn{all:unset;display:inline-flex;align-items:center;height:1.5rem;padding:0 .625rem;border-radius:.25rem;background:var(--blue-text);color:#fff;font-size:.6875rem;font-weight:600;cursor:pointer;transition:background .15s;user-select:none}\n.res-redirect-btn:hover{background:#3d8fd6}\n.res-redirect-notice{font-size:.5625rem;color:var(--dim);margin-top:.375rem}\n\n/* JSON syntax highlighting */\n.j-key{color:#9cdcfe}\n.j-str{color:#ce9178}\n.j-num{color:#b5cea8}\n.j-lit{color:#569cd6}\n</style>\n</head>\n<body>\n\n<header class=\"hdr\">\n <div class=\"hdr-left\">\n <span class=\"logo\">Stoma</span>\n <span class=\"hdr-div\">/</span>\n <span class=\"hdr-title\" id=\"gw-name\"></span>\n </div>\n</header>\n\n<div class=\"routes-bar\" id=\"routes\"></div>\n\n<div class=\"workspace\">\n <!-- Left pane: request -->\n <div class=\"pane-left\">\n <div class=\"token-store\" id=\"token-store\" style=\"display:none\">\n <div class=\"token-header\">\n <span class=\"token-title\">Saved Tokens</span>\n <button class=\"sm-btn\" id=\"clear-tokens\" type=\"button\">Clear All</button>\n </div>\n <div id=\"token-list\"></div>\n </div>\n\n <form id=\"form\" autocomplete=\"off\">\n <div class=\"form-row\">\n <select class=\"sel\" id=\"method\">\n <option>GET</option><option>POST</option><option>PUT</option><option>PATCH</option><option>DELETE</option>\n </select>\n <input class=\"inp\" id=\"path\" placeholder=\"/path\" value=\"/\" />\n <button class=\"btn\" type=\"submit\" id=\"send\">Send</button>\n </div>\n <div class=\"form-sections\">\n <div class=\"field\">\n <div class=\"lbl-row\">\n <label class=\"lbl\">Headers</label>\n <button class=\"sm-btn\" id=\"add-header\" type=\"button\">+ Add</button>\n </div>\n <div class=\"hdr-rows\" id=\"header-rows\"></div>\n </div>\n <div class=\"field\" style=\"flex:1;display:flex;flex-direction:column\">\n <label class=\"lbl\" for=\"body\">Body</label>\n <textarea class=\"ta\" id=\"body\" placeholder='{\"key\":\"value\"}'></textarea>\n </div>\n </div>\n </form>\n\n <div class=\"callback-banner\" id=\"callback-banner\" style=\"display:none\"></div>\n </div>\n\n <!-- Right pane: response -->\n <div class=\"pane-right\">\n <div class=\"res\" id=\"response\">\n <div class=\"res-empty\">Send a request to see the response</div>\n </div>\n </div>\n</div>\n\n<script>\nvar registry = ${registryJson};\ndocument.getElementById(\"gw-name\").textContent = registry.gatewayName || \"Playground\";\n\n// ── Route chips ──\n\nvar routesEl = document.getElementById(\"routes\");\nfor (var ri = 0; ri < registry.routes.length; ri++) {\n var route = registry.routes[ri];\n for (var mi = 0; mi < route.methods.length; mi++) {\n (function(m, path) {\n var chip = document.createElement(\"button\");\n chip.type = \"button\";\n chip.className = \"chip\";\n chip.innerHTML = '<span class=\"method m-' + m.toLowerCase() + '\">' + m + '</span><span>' + esc(path) + '</span>';\n chip.onclick = function() {\n document.getElementById(\"method\").value = m;\n document.getElementById(\"path\").value = path;\n document.getElementById(\"form\").requestSubmit();\n };\n routesEl.appendChild(chip);\n })(route.methods[mi], route.path.replace(/\\\\*$/, \"\"));\n }\n}\n\n// ── Header table management ──\n\nvar headerRowsEl = document.getElementById(\"header-rows\");\n\nfunction addHeaderRow(name, value) {\n var row = document.createElement(\"div\");\n row.className = \"hdr-row\";\n var nameInp = document.createElement(\"input\");\n nameInp.className = \"hdr-input hdr-name\";\n nameInp.placeholder = \"Header name\";\n nameInp.value = name || \"\";\n var valInp = document.createElement(\"input\");\n valInp.className = \"hdr-input hdr-val\";\n valInp.placeholder = \"Value\";\n valInp.value = value || \"\";\n var del = document.createElement(\"button\");\n del.type = \"button\";\n del.className = \"hdr-del\";\n del.textContent = \"\\\\u00d7\";\n del.onclick = function() { row.remove(); };\n row.appendChild(nameInp);\n row.appendChild(valInp);\n row.appendChild(del);\n headerRowsEl.appendChild(row);\n return row;\n}\n\nfunction getHeaders() {\n var headers = {};\n var rows = headerRowsEl.querySelectorAll(\".hdr-row\");\n for (var i = 0; i < rows.length; i++) {\n var inputs = rows[i].querySelectorAll(\"input\");\n var n = inputs[0].value.trim();\n var v = inputs[1].value.trim();\n if (n) headers[n] = v;\n }\n return headers;\n}\n\nfunction setHeader(name, value) {\n var rows = headerRowsEl.querySelectorAll(\".hdr-row\");\n var lowerName = name.toLowerCase();\n for (var i = 0; i < rows.length; i++) {\n var inputs = rows[i].querySelectorAll(\"input\");\n if (inputs[0].value.trim().toLowerCase() === lowerName) {\n inputs[1].value = value;\n return;\n }\n }\n addHeaderRow(name, value);\n}\n\ndocument.getElementById(\"add-header\").onclick = function() { addHeaderRow(); };\n\n// ── Token store (localStorage) ──\n\nvar TOKEN_KEY = \"stoma-playground-tokens\";\n\nfunction loadTokens() {\n try {\n var raw = localStorage.getItem(TOKEN_KEY);\n return raw ? JSON.parse(raw) : [];\n } catch (e) { return []; }\n}\n\nfunction persistTokens(tokens) {\n try { localStorage.setItem(TOKEN_KEY, JSON.stringify(tokens)); } catch (e) {}\n}\n\nfunction saveToken(label, value) {\n var tokens = loadTokens();\n for (var i = 0; i < tokens.length; i++) {\n if (tokens[i].label === label) {\n tokens[i].value = value;\n persistTokens(tokens);\n renderTokenStore();\n return;\n }\n }\n tokens.push({ label: label, value: value });\n persistTokens(tokens);\n renderTokenStore();\n}\n\nfunction removeToken(idx) {\n var tokens = loadTokens();\n tokens.splice(idx, 1);\n persistTokens(tokens);\n renderTokenStore();\n}\n\nfunction clearTokens() {\n persistTokens([]);\n renderTokenStore();\n}\n\nfunction applyToken(idx) {\n var tokens = loadTokens();\n if (tokens[idx]) setHeader(\"Authorization\", \"Bearer \" + tokens[idx].value);\n}\n\nfunction renderTokenStore() {\n var tokens = loadTokens();\n var storeEl = document.getElementById(\"token-store\");\n var listEl = document.getElementById(\"token-list\");\n if (!tokens.length) {\n storeEl.style.display = \"none\";\n listEl.innerHTML = \"\";\n return;\n }\n storeEl.style.display = \"\";\n var html = \"\";\n for (var i = 0; i < tokens.length; i++) {\n var shortVal = tokens[i].value.length > 40 ? tokens[i].value.slice(0, 40) + \"\\\\u2026\" : tokens[i].value;\n html +=\n '<div class=\"token-item\">' +\n '<span class=\"token-name\">' + esc(tokens[i].label) + '</span>' +\n '<span class=\"token-val\">' + esc(shortVal) + '</span>' +\n '<span class=\"token-actions\">' +\n '<button class=\"token-btn token-btn-use\" data-idx=\"' + i + '\">Use</button>' +\n '<button class=\"token-btn token-btn-del\" data-idx=\"' + i + '\">\\\\u00d7</button>' +\n '</span>' +\n '</div>';\n }\n listEl.innerHTML = html;\n listEl.querySelectorAll(\".token-btn-use\").forEach(function(btn) {\n btn.onclick = function() { applyToken(Number(btn.dataset.idx)); };\n });\n listEl.querySelectorAll(\".token-btn-del\").forEach(function(btn) {\n btn.onclick = function() { removeToken(Number(btn.dataset.idx)); };\n });\n}\n\ndocument.getElementById(\"clear-tokens\").onclick = clearTokens;\n\n// Restore tokens on load\nrenderTokenStore();\nvar initTokens = loadTokens();\nif (initTokens.length) applyToken(initTokens.length - 1);\n\n// ── Token auto-detection from responses ──\n\nfunction detectAndSaveTokens(data) {\n if (!data || !data.body) return;\n try {\n var parsed = JSON.parse(data.body);\n var token = parsed.access_token || parsed.token;\n if (typeof token === \"string\" && token.length > 0) {\n var label = parsed.token_type ? parsed.token_type + \"_token\" : \"access_token\";\n saveToken(label, token);\n setHeader(\"Authorization\", \"Bearer \" + token);\n }\n } catch (e) {}\n}\n\n// ── Send request ──\n//\n// Direct fetch to the gateway (same origin). For redirect responses the\n// browser returns an opaque response (redirect:\"manual\"), so we fall back\n// to the server-side proxy which can read the full 3xx details.\n\nfunction sendViaProxy(method, path, headers, bodyText) {\n return fetch(\"/__playground/send\", {\n method: \"POST\",\n headers: { \"content-type\": \"application/json\" },\n body: JSON.stringify({ method: method, path: path, headers: headers, body: bodyText || undefined }),\n }).then(function(proxyRes) {\n if (!proxyRes.ok) {\n return proxyRes.json().catch(function() { return null; }).then(function(err) {\n renderError(err && err.error ? err.error : \"Request failed: \" + proxyRes.status);\n });\n }\n return proxyRes.json().then(function(data) {\n if (data.error) renderError(data.error);\n else { renderResponse(data); detectAndSaveTokens(data); }\n });\n });\n}\n\ndocument.getElementById(\"form\").addEventListener(\"submit\", function(e) {\n e.preventDefault();\n var callbackBanner = document.getElementById(\"callback-banner\");\n callbackBanner.style.display = \"none\";\n\n var method = document.getElementById(\"method\").value;\n var path = document.getElementById(\"path\").value;\n var bodyText = document.getElementById(\"body\").value;\n var sendBtn = document.getElementById(\"send\");\n var headers = getHeaders();\n\n sendBtn.disabled = true;\n sendBtn.textContent = \"Sending\\\\u2026\";\n\n var start = performance.now();\n var init = { method: method, headers: headers, redirect: \"manual\" };\n if (bodyText && method !== \"GET\" && method !== \"HEAD\") init.body = bodyText;\n\n fetch(path, init).then(function(res) {\n // Opaque redirect — fall back to proxy for full 3xx details\n if (res.type === \"opaqueredirect\") return null;\n\n var status = res.status;\n var statusText = res.statusText;\n var resHeaders = {};\n res.headers.forEach(function(v, k) { resHeaders[k] = v; });\n\n return res.text().then(function(bodyStr) {\n return { status: status, statusText: statusText, headers: resHeaders, body: bodyStr, elapsed: performance.now() - start };\n });\n }).catch(function() {\n return null;\n }).then(function(data) {\n if (data) {\n renderResponse(data);\n detectAndSaveTokens(data);\n return;\n }\n return sendViaProxy(method, path, headers, bodyText);\n }).catch(function(err) {\n renderError(err.message || \"Request failed\");\n }).finally(function() {\n sendBtn.disabled = false;\n sendBtn.textContent = \"Send\";\n });\n});\n\n// ── Render helpers ──\n\nfunction escHtml(s) {\n return s.replace(/&/g, \"&amp;\").replace(/</g, \"&lt;\").replace(/>/g, \"&gt;\");\n}\n\nfunction highlightJson(prettyStr) {\n var s = escHtml(prettyStr);\n // Keys\n s = s.replace(/\"([^\"\\\\\\\\]*(?:\\\\\\\\.[^\"\\\\\\\\]*)*)\"s*:/g, '<span class=\"j-key\">\"$1\"</span>:');\n // String values (after : or in arrays after , or [)\n s = s.replace(/:\\\\s*\"([^\"\\\\\\\\]*(?:\\\\\\\\.[^\"\\\\\\\\]*)*)\"/g, ': <span class=\"j-str\">\"$1\"</span>');\n // Numbers\n s = s.replace(/:\\\\s*(-?\\\\d+(?:\\\\.\\\\d+)?(?:[eE][+-]?\\\\d+)?)/g, ': <span class=\"j-num\">$1</span>');\n // Booleans and null\n s = s.replace(/:\\\\s*(true|false|null)\\\\b/g, ': <span class=\"j-lit\">$1</span>');\n return s;\n}\n\nfunction renderResponse(ref) {\n var status = ref.status, statusText = ref.statusText, headers = ref.headers, body = ref.body, elapsed = ref.elapsed;\n var cls = status < 300 ? \"b-2xx\" : status < 400 ? \"b-3xx\" : status < 500 ? \"b-4xx\" : \"b-5xx\";\n\n var rawBody = body || \"\";\n var prettyBody = rawBody;\n var isJson = false;\n try {\n prettyBody = JSON.stringify(JSON.parse(rawBody), null, 2);\n isJson = true;\n } catch (e) {}\n\n var headerCount = 0;\n var headerRows = \"\";\n for (var k in headers) {\n if (headers.hasOwnProperty(k)) {\n headerCount++;\n headerRows += \"<tr><td>\" + esc(k) + \"</td><td>\" + esc(String(headers[k])) + \"</td></tr>\";\n }\n }\n\n // Redirect banner (OAuth flows)\n var redirectBanner = \"\";\n var locationUrl = headers && headers.location;\n if (status >= 300 && status < 400 && locationUrl && !locationUrl.startsWith(\"/\")) {\n lastRedirectUrl = locationUrl;\n redirectBanner =\n '<div class=\"res-redirect\">' +\n '<div class=\"res-redirect-title\">Redirect Detected</div>' +\n '<div class=\"res-redirect-url\">' + esc(locationUrl) + '</div>' +\n '<button class=\"res-redirect-btn\" onclick=\"openAuthPopup(lastRedirectUrl)\">Open Authorization</button>' +\n '<div class=\"res-redirect-notice\">Opens in a popup. After authorizing, the callback parameters will be sent back here.</div>' +\n '</div>';\n }\n\n var prettyContent = isJson\n ? highlightJson(prettyBody)\n : escHtml(prettyBody);\n\n var resEl = document.getElementById(\"response\");\n resEl.innerHTML =\n '<div class=\"res-top\">' +\n '<span class=\"badge ' + cls + '\">' + status + \" \" + esc(statusText || \"\") + '</span>' +\n '<span class=\"timing\">' + (elapsed || 0).toFixed(1) + ' ms</span>' +\n '</div>' +\n redirectBanner +\n '<div class=\"res-tabs\">' +\n '<button class=\"res-tab active\" data-tab=\"pretty\">Pretty</button>' +\n '<button class=\"res-tab\" data-tab=\"raw\">Raw</button>' +\n '<button class=\"res-tab\" data-tab=\"headers\">Headers<span class=\"res-tab-count\">(' + headerCount + ')</span></button>' +\n '</div>' +\n '<div class=\"res-tab-content active\" data-tab=\"pretty\"><div class=\"res-body\"><pre>' + prettyContent + '</pre></div></div>' +\n '<div class=\"res-tab-content\" data-tab=\"raw\"><div class=\"res-body\"><pre>' + esc(rawBody) + '</pre></div></div>' +\n '<div class=\"res-tab-content\" data-tab=\"headers\"><table class=\"htable\">' + headerRows + '</table></div>';\n\n // Tab switching\n resEl.querySelectorAll(\".res-tab\").forEach(function(tab) {\n tab.onclick = function() {\n var target = tab.dataset.tab;\n resEl.querySelectorAll(\".res-tab\").forEach(function(t) { t.classList.toggle(\"active\", t.dataset.tab === target); });\n resEl.querySelectorAll(\".res-tab-content\").forEach(function(c) { c.classList.toggle(\"active\", c.dataset.tab === target); });\n };\n });\n}\n\nfunction renderError(msg) {\n document.getElementById(\"response\").innerHTML = '<div class=\"res-error\">' + esc(msg) + '</div>';\n}\n\nfunction esc(s) {\n var d = document.createElement(\"div\");\n d.textContent = s;\n return d.innerHTML;\n}\n\n// ── OAuth popup ──\n\nvar lastRedirectUrl = \"\";\nvar oauthPopup = null;\nfunction openAuthPopup(url) {\n if (oauthPopup && !oauthPopup.closed) oauthPopup.close();\n oauthPopup = window.open(url, \"stoma-oauth\", \"width=600,height=700\");\n}\n\nwindow.addEventListener(\"message\", function(event) {\n if (event.origin !== window.location.origin) return;\n if (!event.data || event.data.type !== \"stoma-oauth-callback\") return;\n var params = event.data.params;\n if (!params) return;\n\n var callbackPath = \"/auth/callback\";\n for (var i = 0; i < registry.routes.length; i++) {\n if (registry.routes[i].path.toLowerCase().indexOf(\"callback\") !== -1) {\n callbackPath = registry.routes[i].path;\n break;\n }\n }\n\n var qs = new URLSearchParams(params).toString();\n document.getElementById(\"method\").value = \"GET\";\n document.getElementById(\"path\").value = callbackPath + \"?\" + qs;\n\n var banner = document.getElementById(\"callback-banner\");\n banner.innerHTML = 'OAuth parameters received. Click <strong>Send</strong> to complete authorization.';\n banner.style.display = \"\";\n\n document.getElementById(\"response\").innerHTML =\n '<div class=\"res-empty\">OAuth callback parameters prefilled. Click Send to exchange for a token.</div>';\n});\n</script>\n</body>\n</html>`;\n}\n\nfunction esc(s: string): string {\n return s\n .replace(/&/g, \"&amp;\")\n .replace(/</g, \"&lt;\")\n .replace(/>/g, \"&gt;\")\n .replace(/\"/g, \"&quot;\");\n}\n","/**\n * Generates a self-contained HTML page that relays OAuth callback\n * query parameters to the playground opener window via postMessage.\n *\n * Served by `wrapWithPlayground` when a browser navigates to a\n * callback route (e.g., after the OAuth provider redirects the popup).\n */\nexport function oauthRelayHtml(url: URL): string {\n const paramsJson = JSON.stringify(\n Object.fromEntries(url.searchParams.entries())\n ).replace(/</g, \"\\\\u003c\");\n\n return `<!DOCTYPE html>\n<html lang=\"en\">\n<head><meta charset=\"utf-8\" /><title>Authorization Complete</title></head>\n<body style=\"font-family:system-ui;padding:2rem;text-align:center;background:#1e1e1e;color:#d4d4d4\">\n<h3>Authorization Complete</h3>\n<p id=\"msg\">Sending parameters to the playground\\u2026</p>\n<script>\n(function() {\n var params = ${paramsJson};\n if (window.opener) {\n window.opener.postMessage({ type: \"stoma-oauth-callback\", params: params }, location.origin);\n document.getElementById(\"msg\").textContent = \"Parameters sent. You can close this window.\";\n } else {\n document.getElementById(\"msg\").textContent = \"No opener window found. Please copy the URL and paste it into the playground manually.\";\n }\n})();\n</script>\n</body>\n</html>`;\n}\n","import type { GatewayRegistry } from \"../gateway/types.js\";\nimport { playgroundHtml } from \"./html.js\";\nimport { oauthRelayHtml } from \"./oauth-relay.js\";\n\n/**\n * Wrap a gateway's fetch function with playground routes.\n *\n * Intercepts playground paths before delegating to the original gateway fetch.\n *\n * - `/__playground` → serves the playground HTML UI\n * - `/__playground/registry` → returns the gateway registry as JSON\n * - `/__playground/send` → redirect fallback: re-executes a request server-side\n * to capture full 3xx response details that browser\n * fetch cannot access (opaque redirect limitation)\n */\nexport function wrapWithPlayground(\n gatewayFetch: (request: Request) => Response | Promise<Response>,\n registry: GatewayRegistry\n): (request: Request) => Response | Promise<Response> {\n const html = playgroundHtml(registry);\n\n // Pre-compute callback route paths for OAuth relay interception\n const callbackPaths = registry.routes\n .map((r) => r.path)\n .filter((p) => p.toLowerCase().includes(\"callback\"));\n\n return async (request: Request) => {\n const url = new URL(request.url);\n\n if (url.pathname === \"/__playground\") {\n return new Response(html, {\n headers: { \"content-type\": \"text/html; charset=utf-8\" },\n });\n }\n\n if (url.pathname === \"/__playground/registry\") {\n return Response.json(registry);\n }\n\n if (url.pathname === \"/__playground/send\" && request.method === \"POST\") {\n return handlePlaygroundSend(request, gatewayFetch, url.origin);\n }\n\n // OAuth relay: when the OAuth provider redirects the popup to a callback\n // route, serve a relay page that sends the params back to the playground\n // via postMessage instead of letting the gateway handle the browser request.\n const isNavigation = request.headers.get(\"accept\")?.includes(\"text/html\");\n if (isNavigation && url.search) {\n const isCallbackRoute = callbackPaths.some(\n (p) =>\n url.pathname === p || url.pathname.startsWith(p.replace(/\\*$/, \"\"))\n );\n if (isCallbackRoute) {\n return new Response(oauthRelayHtml(url), {\n headers: { \"content-type\": \"text/html; charset=utf-8\" },\n });\n }\n }\n\n return gatewayFetch(request);\n };\n}\n\n/**\n * Execute a request against the gateway in-process and return the full\n * response details as JSON. This avoids browser fetch limitations\n * (redirect following, CORS, opaque responses).\n */\nasync function handlePlaygroundSend(\n request: Request,\n gatewayFetch: (request: Request) => Response | Promise<Response>,\n origin: string\n): Promise<Response> {\n try {\n const payload = (await request.json()) as {\n method: string;\n path: string;\n headers?: Record<string, string>;\n body?: string;\n };\n\n // Build a Request as if the browser had made it directly\n const targetUrl = new URL(payload.path, origin).href;\n const init: RequestInit = {\n method: payload.method,\n headers: payload.headers ?? {},\n };\n if (payload.body && payload.method !== \"GET\" && payload.method !== \"HEAD\") {\n init.body = payload.body;\n }\n\n const gatewayRequest = new Request(targetUrl, init);\n const start = performance.now();\n const res = await gatewayFetch(gatewayRequest);\n const elapsed = performance.now() - start;\n\n const body = await res.text();\n const headers: Record<string, string> = {};\n res.headers.forEach((v, k) => {\n headers[k] = v;\n });\n\n return Response.json({\n status: res.status,\n statusText: res.statusText,\n headers,\n body,\n elapsed,\n });\n } catch (err) {\n return Response.json(\n {\n error: err instanceof Error ? err.message : String(err),\n },\n { status: 500 }\n );\n }\n}\n","import type { Server } from \"node:http\";\nimport { serve } from \"@hono/node-server\";\n\nexport interface StartServerOptions {\n fetch: (request: Request) => Response | Promise<Response>;\n port: number;\n hostname: string;\n}\n\nexport function startServer(options: StartServerOptions): Promise<Server> {\n const { fetch, port, hostname } = options;\n\n return new Promise<Server>((resolve, reject) => {\n const server = serve({ fetch, port, hostname }, () =>\n resolve(server as unknown as Server)\n );\n\n // Handle EADDRINUSE before the callback fires\n (server as unknown as Server).on?.(\n \"error\",\n (err: NodeJS.ErrnoException) => {\n if (err.code === \"EADDRINUSE\") {\n reject(\n new Error(\n `Port ${port} is already in use. Try a different port with --port <number>`\n )\n );\n } else {\n reject(err);\n }\n }\n );\n });\n}\n","export interface Logger {\n info: (message: string) => void;\n verbose: (message: string) => void;\n error: (message: string) => void;\n}\n\nexport function createLogger(verbose: boolean): Logger {\n return {\n info: (message: string) => console.log(message),\n verbose: verbose ? (message: string) => console.log(message) : () => {},\n error: (message: string) => console.error(message),\n };\n}\n","import { existsSync, readFileSync } from \"node:fs\";\nimport { dirname, resolve } from \"node:path\";\nimport { fileURLToPath } from \"node:url\";\n\nlet cachedVersion: string | undefined;\n\nexport function getVersion(): string {\n if (cachedVersion) return cachedVersion;\n\n try {\n const dir = dirname(fileURLToPath(import.meta.url));\n // Try both: bundled (dist/bin.js → ../package.json) and source (src/utils/version.ts → ../../package.json)\n for (const rel of [\"..\", \"../..\"]) {\n const candidate = resolve(dir, rel, \"package.json\");\n if (existsSync(candidate)) {\n const pkg = JSON.parse(readFileSync(candidate, \"utf-8\"));\n if (pkg.name === \"@vivero/stoma-cli\") {\n cachedVersion = (pkg.version as string) ?? \"0.0.0\";\n return cachedVersion!;\n }\n }\n }\n cachedVersion = \"0.0.0\";\n } catch {\n cachedVersion = \"0.0.0\";\n }\n\n return cachedVersion!;\n}\n","import { runCli } from \"./cli.js\";\n\nrunCli(process.argv.slice(2));\n"],"mappings":"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A;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAA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","names":["path","require","fetch","resolve","path","fetch","resolve","existsSync"]}
package/dist/cli.d.ts ADDED
@@ -0,0 +1,7 @@
1
+ import * as clipanion from 'clipanion';
2
+ import { Cli } from 'clipanion';
3
+
4
+ declare function createCli(): Cli<clipanion.BaseContext>;
5
+ declare function runCli(argv: string[]): Promise<void>;
6
+
7
+ export { createCli, runCli };
package/package.json ADDED
@@ -0,0 +1,82 @@
1
+ {
2
+ "name": "@vivero/stoma-cli",
3
+ "version": "0.1.0-rc.1",
4
+ "description": "CLI for running Stoma API gateways locally — load a gateway file and serve it on a Node.js HTTP server.",
5
+ "type": "module",
6
+ "repository": {
7
+ "type": "git",
8
+ "url": "git+https://github.com/vivero-dev/stoma.git",
9
+ "directory": "packages/cli"
10
+ },
11
+ "homepage": "https://stoma.vivero.dev",
12
+ "license": "MIT",
13
+ "author": {
14
+ "name": "Jonathan Bennett"
15
+ },
16
+ "bugs": {
17
+ "url": "https://github.com/vivero-dev/stoma/issues"
18
+ },
19
+ "keywords": [
20
+ "stoma",
21
+ "cli",
22
+ "api-gateway",
23
+ "hono",
24
+ "local-server"
25
+ ],
26
+ "bin": {
27
+ "stoma": "./dist/bin.js"
28
+ },
29
+ "main": "./dist/bin.js",
30
+ "types": "./dist/cli.d.ts",
31
+ "files": [
32
+ "dist",
33
+ "LICENSE",
34
+ "README.md"
35
+ ],
36
+ "exports": {
37
+ ".": {
38
+ "types": "./dist/cli.d.ts",
39
+ "import": "./dist/bin.js",
40
+ "default": "./dist/bin.js"
41
+ }
42
+ },
43
+ "publishConfig": {
44
+ "access": "public",
45
+ "bin": {
46
+ "stoma": "./dist/bin.js"
47
+ },
48
+ "main": "./dist/bin.js",
49
+ "types": "./dist/cli.d.ts",
50
+ "exports": {
51
+ ".": {
52
+ "types": "./dist/cli.d.ts",
53
+ "import": "./dist/bin.js",
54
+ "default": "./dist/bin.js"
55
+ }
56
+ }
57
+ },
58
+ "scripts": {
59
+ "dev": "tsx src/bin.ts",
60
+ "build": "tsup",
61
+ "typecheck": "tsc --noEmit",
62
+ "test": "vitest run",
63
+ "test:watch": "vitest",
64
+ "test:coverage": "vitest run --coverage",
65
+ "lint": "echo 'no linter configured'"
66
+ },
67
+ "dependencies": {
68
+ "@hono/node-server": "^1.14.0",
69
+ "@vivero/stoma": "0.1.0-rc.6",
70
+ "clipanion": "^4.0.0-rc.4",
71
+ "esbuild": "^0.25.0",
72
+ "hono": "^4.7.0"
73
+ },
74
+ "devDependencies": {
75
+ "@cloudflare/workers-types": "^4.20250214.0",
76
+ "@types/node": "^22.0.0",
77
+ "tsup": "^8.5.1",
78
+ "tsx": "^4.19.0",
79
+ "typescript": "^5.9.3",
80
+ "vitest": "3.2.4"
81
+ }
82
+ }