@kubb/studio 0.0.0-canary-20260903193839
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/LICENSE +21 -0
- package/README.md +136 -0
- package/dist/configFile-DjzP1_Ln.cjs +575 -0
- package/dist/configFile-DjzP1_Ln.cjs.map +1 -0
- package/dist/configFile-ZnV5tPon.js +574 -0
- package/dist/configFile-ZnV5tPon.js.map +1 -0
- package/dist/index-BVn89Nw2.d.ts +562 -0
- package/dist/index.cjs +1330 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.ts +274 -0
- package/dist/index.js +1319 -0
- package/dist/index.js.map +1 -0
- package/dist/protocol.cjs +42 -0
- package/dist/protocol.cjs.map +1 -0
- package/dist/protocol.d.ts +2 -0
- package/dist/protocol.js +37 -0
- package/dist/protocol.js.map +1 -0
- package/dist/resolveConfig-B9oGiNMi.js +179 -0
- package/dist/resolveConfig-B9oGiNMi.js.map +1 -0
- package/dist/resolveConfig-Ci-BVhN_.cjs +202 -0
- package/dist/resolveConfig-Ci-BVhN_.cjs.map +1 -0
- package/dist/rolldown-runtime-CRm0XQPb.js +9 -0
- package/dist/rolldown-runtime-qbf5tadS.cjs +38 -0
- package/package.json +86 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","names":["detectTool","process","detectUncachedTool","process","process","kubbVersion","version","delay"],"sources":["../src/constants.ts","../../../internals/utils/src/errors.ts","../../../internals/utils/src/promise.ts","../../../internals/utils/src/tools.ts","../../../internals/utils/src/time.ts","../src/machine.ts","../src/api.ts","../package.json","../src/hooks.ts","../src/generate.ts","../src/ws.ts","../src/connectStudio.ts","../src/client.ts","../src/pair.ts"],"sourcesContent":["/**\n * Hosted Kubb Studio URL. Exported so credential stores can bind tokens to the resolved instance,\n * not whatever default the client would pick on its own.\n */\nexport const defaultStudioUrl = 'https://kubb.studio'\n\n/**\n * Defaults the Studio client uses when a host passes nothing.\n * Config path is left out on purpose: each host discovers that itself.\n */\nexport const agentDefaults = {\n studioUrl: defaultStudioUrl,\n retryIntervalMs: 30_000,\n /**\n * Maximum heartbeat interval. Studio drops agents from the active list after ~90s without a ping,\n * so a slower override would make a healthy agent look dead.\n */\n heartbeatIntervalMs: 30_000,\n poolSize: 1,\n} as const\n","/**\n * Coerces an unknown thrown value to an `Error` instance.\n * Returns the value as-is when it is already an `Error`; otherwise wraps it with `String(value)`.\n *\n * @example\n * ```ts\n * try { ... } catch(err) {\n * throw new Error('Build failed', { cause: toError(err) })\n * }\n * ```\n */\nexport function toError(value: unknown): Error {\n return value instanceof Error ? value : new Error(String(value))\n}\n\n/**\n * Extracts a human-readable message from any thrown value.\n *\n * @example\n * ```ts\n * getErrorMessage(new Error('oops')) // 'oops'\n * getErrorMessage('plain string') // 'plain string'\n * ```\n */\nexport function getErrorMessage(value: unknown): string {\n return value instanceof Error ? value.message : String(value)\n}\n","/** A value that may already be resolved or still pending.\n *\n * @example\n * ```ts\n * function load(id: string): PossiblePromise<string> {\n * return cache.get(id) ?? fetchRemote(id)\n * }\n * ```\n */\nexport type PossiblePromise<T> = Promise<T> | T\n\n/** Returns `true` when `result` is a thenable `Promise`.\n *\n * @example\n * ```ts\n * isPromise(Promise.resolve(1)) // true\n * isPromise(42) // false\n * ```\n */\nexport function isPromise<T>(result: PossiblePromise<T>): result is Promise<T> {\n return result !== null && result !== undefined && typeof (result as Record<string, unknown>)['then'] === 'function'\n}\n\ntype Store<TKey, TValue> = {\n has(key: TKey): boolean\n get(key: TKey): TValue | undefined\n set(key: TKey, value: TValue): unknown\n}\n\n/**\n * Wraps `factory` with a keyed cache backed by the provided store.\n *\n * Pass a `WeakMap` for object keys (results are GC-eligible when the key is\n * collected) or a `Map` for primitive keys. For multi-argument functions,\n * nest two `memoize` calls — the outer keyed by the first argument, the\n * inner (created once per outer miss) keyed by the second.\n *\n * Because the cache is owned by the caller, it can be shared, inspected, or\n * cleared independently of the memoized function.\n *\n * @example Single WeakMap key\n * ```ts\n * const cache = new WeakMap<SchemaNode, Set<string>>()\n * const getRefs = memoize(cache, (node) => collectRefs(node))\n * ```\n *\n * @example Single Map key (primitive)\n * ```ts\n * const cache = new Map<string, Resolver>()\n * const getResolver = memoize(cache, (name) => buildResolver(name))\n * ```\n *\n * @example Two-level (object + primitive)\n * ```ts\n * const outer = new WeakMap<Params[], Map<string, Params[]>>()\n * const fn = memoize(outer, (params) => memoize(new Map(), (key) => transform(params, key)))\n * fn(params)('camelcase')\n * ```\n */\nexport function memoize<TKey, TValue>(store: Store<TKey, TValue>, factory: (key: TKey) => TValue): (key: TKey) => TValue {\n return (key: TKey): TValue => {\n if (store.has(key)) return store.get(key)!\n const value = factory(key)\n store.set(key, value)\n return value\n }\n}\n\ntype ParallelOptions<TItem> = {\n /**\n * Items to work through, handed out in order.\n */\n items: ReadonlyArray<TItem>\n /**\n * How many items may be in flight at once.\n */\n limit: number\n /**\n * Runs once per item, with the item's position so a caller can report progress.\n */\n run(item: TItem, index: number): Promise<void>\n}\n\n/**\n * Runs `run` over every item with at most `limit` in flight. Workers share one iterator, so each\n * takes the next item the moment it frees up instead of waiting for a batch to drain.\n *\n * @example\n * ```ts\n * await inParallel({ items: files, limit: 50, run: (file) => storage.writeItem(file.path, file.source) })\n * ```\n */\nexport async function inParallel<TItem>({ items, limit, run }: ParallelOptions<TItem>): Promise<void> {\n const queue = items.entries()\n\n const worker = async (): Promise<void> => {\n for (const [index, item] of queue) await run(item, index)\n }\n\n await Promise.all(Array.from({ length: Math.min(limit, items.length) }, () => worker()))\n}\n","import { spawn } from 'node:child_process'\n\n/**\n * How one formatter or linter is invoked: the executable, the argv it takes for an output\n * directory, and what to report when it is not installed.\n */\nexport type ToolCommand = {\n command: string\n args: (outputPath: string) => Array<string>\n errorMessage: string\n}\n\n/**\n * CLI command descriptors for each supported code formatter.\n */\nexport const formatters = {\n prettier: {\n command: 'prettier',\n args: (outputPath: string) => ['--ignore-unknown', '--write', outputPath],\n errorMessage: 'Prettier not found',\n },\n biome: {\n command: 'biome',\n args: (outputPath: string) => ['format', '--write', outputPath],\n errorMessage: 'Biome not found',\n },\n oxfmt: {\n command: 'oxfmt',\n args: (outputPath: string) => [outputPath],\n errorMessage: 'Oxfmt not found',\n },\n} as const satisfies Record<string, ToolCommand>\n\n/**\n * CLI command descriptors for each supported linter.\n */\nexport const linters = {\n eslint: {\n command: 'eslint',\n args: (outputPath: string) => [outputPath, '--fix'],\n errorMessage: 'Eslint not found',\n },\n biome: {\n command: 'biome',\n args: (outputPath: string) => ['lint', '--fix', outputPath],\n errorMessage: 'Biome not found',\n },\n oxlint: {\n command: 'oxlint',\n // --no-ignore so oxlint lints the folder even when it's gitignored (generated output dirs usually are).\n args: (outputPath: string) => ['--fix', '--no-ignore', outputPath],\n errorMessage: 'Oxlint not found',\n },\n} as const satisfies Record<string, ToolCommand>\n\n/**\n * Preference order for `format: 'auto'`, most-preferred first. Spelled out rather than taken from\n * the table's key order, which is arbitrary and would silently change what `auto` picks.\n */\nexport const FORMATTER_PREFERENCE = ['oxfmt', 'biome', 'prettier'] as const\n\n/**\n * Preference order for `lint: 'auto'`, most-preferred first.\n */\nexport const LINTER_PREFERENCE = ['oxlint', 'biome', 'eslint'] as const\n\n/**\n * Whether `name` is on PATH and answers `--version` with a zero exit.\n */\nexport function isToolAvailable(name: string): Promise<boolean> {\n return new Promise((resolve) => {\n const child = spawn(name, ['--version'], { stdio: 'ignore' })\n child.on('close', (code) => resolve(code === 0))\n child.on('error', () => resolve(false))\n })\n}\n\n/**\n * Returns the first installed executable from `candidates`, or `null` when none are found.\n *\n * Not memoized: a long-running host that probes repeatedly should cache the result itself, and a\n * `--watch` build should keep noticing a tool installed mid-session.\n */\nexport async function detectTool<TName extends string>(candidates: ReadonlyArray<TName>): Promise<TName | null> {\n for (const candidate of candidates) {\n if (await isToolAvailable(candidate)) {\n return candidate\n }\n }\n\n return null\n}\n\n/**\n * Tokenizes a shell command string, respecting single and double quotes.\n *\n * @example\n * ```ts\n * tokenize('git commit -m \"initial commit\"')\n * // → ['git', 'commit', '-m', 'initial commit']\n * ```\n */\nexport function tokenize(command: string): Array<string> {\n return (command.match(/[^\\s\"']+|\"([^\"]*)\"|'([^']*)'/g) ?? []).map((token) => token.replace(/^[\"']|[\"']$/g, ''))\n}\n","/**\n * Calculates elapsed time in milliseconds from a high-resolution `process.hrtime` start time.\n * Rounds to 2 decimal places for sub-millisecond precision without noise.\n *\n * @example\n * ```ts\n * const start = process.hrtime()\n * doWork()\n * getElapsedMs(start) // 42.35\n * ```\n */\nexport function getElapsedMs(hrStart: [number, number]): number {\n const [seconds, nanoseconds] = process.hrtime(hrStart)\n const ms = seconds * 1000 + nanoseconds / 1e6\n return Math.round(ms * 100) / 100\n}\n\n/**\n * Converts a millisecond duration into a human-readable string (`ms`, `s`, or `m s`).\n *\n * @example\n * ```ts\n * formatMs(250) // '250ms'\n * formatMs(1500) // '1.50s'\n * formatMs(90000) // '1m 30.0s'\n * ```\n */\nexport function formatMs(ms: number): string {\n if (ms >= 60000) {\n const mins = Math.floor(ms / 60000)\n const secs = ((ms % 60000) / 1000).toFixed(1)\n return `${mins}m ${secs}s`\n }\n\n if (ms >= 1000) {\n return `${(ms / 1000).toFixed(2)}s`\n }\n return `${Math.round(ms)}ms`\n}\n","import { hash, randomBytes } from 'node:crypto'\nimport process from 'node:process'\nimport { styleText } from 'node:util'\nimport { createStorage, type Storage } from 'unstorage'\nimport fsDriver from 'unstorage/drivers/fs'\n\n/**\n * Key-value storage the runtime uses for its machine secret and the last Studio config.\n *\n * One storage per process, since one process serves one config file. Hosts install their own\n * driver on startup: Nitro passes its `kubb` mount, the CLI an fs driver under `~/.kubb/cache`.\n * The in-memory default keeps the runtime usable without a host, at the cost of a machine\n * identity that changes on every restart.\n */\nlet storage: Storage = createStorage()\nlet hasInstalledStorage = false\n\n/**\n * Installs the storage driver the runtime persists to. Call once, before connecting.\n */\nexport function setStorage(next: Storage): void {\n storage = next\n hasInstalledStorage = true\n}\n\n/**\n * A storage backed by files under `base`, so the machine secret and the last Studio config\n * survive a restart. Repeated pairings of one machine depend on that secret staying put.\n */\nexport function createFileStorage(base: string): Storage {\n return createStorage({ driver: fsDriver({ base }) })\n}\n\nlet fallbackSecretPromise: Promise<string> | null = null\n\n/**\n * Loads the fallback machine secret from the runtime storage.\n * On first use it generates a secret and persists it, so the machine identity stays\n * stable across restarts. An identity that changes on every boot breaks session\n * creation with Studio whenever the startup registration call fails.\n */\nasync function loadOrCreateFallbackSecret(): Promise<string> {\n // The secret is memoized for the life of the process, so a host that reads the machine token\n // before installing its storage is bound to the throwaway in-memory default. Nothing else\n // surfaces that: the write succeeds, and the identity silently changes on every restart, which\n // Studio rejects with a 403 on the next session create.\n if (!hasInstalledStorage) {\n console.warn(\n styleText('yellow', 'Deriving the machine token before a storage driver was installed'),\n 'call setStorage() first, or set KUBB_AGENT_SECRET, to keep a stable machine identity across restarts',\n )\n }\n\n const stored = await storage.getItem('machine-secret').catch(() => null)\n\n if (typeof stored === 'string' && stored) {\n return stored\n }\n\n const secret = randomBytes(32).toString('hex')\n\n await storage.setItem('machine-secret', secret).catch(() => {\n console.warn(\n styleText('yellow', 'Could not persist the generated machine secret'),\n 'set KUBB_AGENT_SECRET to keep a stable machine identity across restarts',\n )\n })\n\n return secret\n}\n\n/**\n * Returns the machine token derived from the `KUBB_AGENT_SECRET` environment variable.\n * Falls back to a generated secret persisted in the runtime storage if the env var is not set.\n * The token is hashed with SHA-256.\n */\nexport async function getMachineToken(): Promise<string> {\n if (process.env.KUBB_AGENT_SECRET) {\n return hash('sha256', process.env.KUBB_AGENT_SECRET)\n }\n\n fallbackSecretPromise ??= loadOrCreateFallbackSecret()\n\n return hash('sha256', await fallbackSecretPromise)\n}\n","import { styleText } from 'node:util'\nimport { getErrorMessage } from '@internals/utils'\nimport { FetchError, ofetch } from 'ofetch'\nimport type { AgentConnectResponse } from './protocol/index.ts'\nimport { getMachineToken } from './machine.ts'\n\n/**\n * Reads a human-readable message from a Studio JSON error body, when it has one. `FetchError`'s own\n * message stops at the status line, so the detail Studio sends with a failure (an agent limit, a\n * revoked token) would otherwise never reach the user.\n */\nfunction responseMessage(data: unknown): string | undefined {\n if (!data || typeof data !== 'object') {\n return undefined\n }\n\n const body = data as { error_description?: unknown; message?: unknown; error?: unknown }\n for (const value of [body.error_description, body.message, body.error]) {\n if (typeof value === 'string' && value) {\n return value\n }\n }\n\n return undefined\n}\n\n/**\n * Retries after the first registration attempt, each backing off twice as far as the last.\n */\nconst REGISTER_RETRIES = 3\n\n/**\n * Shared in-flight registration so concurrent pool sessions trigger one purge, not N.\n */\nlet registrationInFlight: Promise<boolean> | null = null\n\ntype ConnectProps = {\n studioUrl: string\n token: string\n}\n\n/**\n * Thrown when Studio rejects the agent token itself (401). Retrying cannot help: the token was\n * revoked, or the agent it belonged to was deleted in the Studio UI. Hosts catch this to forget\n * the stored credential and pair again.\n */\nexport class InvalidAgentTokenError extends Error {\n constructor(studioUrl: string, options?: ErrorOptions) {\n super(`Kubb Studio rejected this agent's token. It was revoked or the agent was deleted in ${studioUrl}.`, options)\n this.name = 'InvalidAgentTokenError'\n }\n}\n\n/**\n * Whether a thrown value carries `statusCode`. Not narrowed to `FetchError`: a host wrapper can\n * throw its own error shape with the same field.\n *\n * A 401 means the agent token itself was rejected. A 403 from the session create endpoint means\n * the machine token stored in Studio no longer matches this agent (missing or mismatched).\n */\nfunction rejectedWith(error: unknown, statusCode: number): boolean {\n return (error as { statusCode?: number } | undefined)?.statusCode === statusCode\n}\n\nfunction sessionError(cause: unknown): Error {\n const detail = (cause instanceof FetchError ? responseMessage(cause.data) : undefined) ?? getErrorMessage(cause)\n return new Error(detail ? `Failed to get agent session from Kubb Studio: ${detail}` : 'Failed to get agent session from Kubb Studio', { cause })\n}\n\n/**\n * Performs the raw session create request against Studio.\n */\nasync function requestAgentSession({ token, studioUrl }: ConnectProps): Promise<AgentConnectResponse> {\n const url = `${studioUrl}/api/agent/sessions`\n\n const data = await ofetch<AgentConnectResponse>(url, {\n method: 'POST',\n headers: { Authorization: `Bearer ${token}` },\n body: { machineToken: await getMachineToken() },\n })\n\n if (!data) {\n throw new Error('No data available for agent session')\n }\n\n return data\n}\n\n/**\n * Obtain an agent session token from Kubb Studio via HTTP.\n *\n * When Studio rejects the machine token (403), for example after the agent restarted\n * with a new identity while the startup registration call failed, the agent re-registers\n * and retries once, so a single failed registration can't permanently block session creation.\n */\nexport async function createAgentSession({ token, studioUrl }: ConnectProps): Promise<AgentConnectResponse> {\n try {\n return await requestAgentSession({ token, studioUrl })\n } catch (error: unknown) {\n if (rejectedWith(error, 401)) {\n throw new InvalidAgentTokenError(studioUrl, { cause: error })\n }\n\n if (!rejectedWith(error, 403) || !(await registerAgent({ token, studioUrl }))) {\n throw sessionError(error)\n }\n\n try {\n return await requestAgentSession({ token, studioUrl })\n } catch (retryError: unknown) {\n if (rejectedWith(retryError, 401)) {\n throw new InvalidAgentTokenError(studioUrl, { cause: retryError })\n }\n\n throw sessionError(retryError)\n }\n }\n}\n\ntype RegisterProps = {\n studioUrl: string\n token: string\n poolSize?: number\n}\n\n/**\n * Register this agent with Kubb Studio by sending the machine ID.\n * Called on agent startup before creating a WebSocket session, and again when\n * Studio rejects the machine token during session creation.\n *\n * Retries with backoff because a failed registration leaves Studio with a stale\n * machine token that blocks every subsequent session create call. Registration\n * purges all of the agent's sessions on the Studio side, so concurrent callers\n * (multiple pool sessions hitting a 403 at once) share one in-flight run instead\n * of purging each other's fresh sessions.\n */\nexport function registerAgent(props: RegisterProps): Promise<boolean> {\n registrationInFlight ??= runRegistration(props).finally(() => {\n registrationInFlight = null\n })\n\n return registrationInFlight\n}\n\nasync function runRegistration({ token, studioUrl, poolSize }: RegisterProps): Promise<boolean> {\n const machineToken = await getMachineToken()\n\n try {\n await ofetch(`${studioUrl}/api/agent/connect`, {\n method: 'POST',\n headers: {\n Authorization: `Bearer ${token}`,\n },\n body: { machineToken, poolSize },\n retry: REGISTER_RETRIES,\n // 2s, 4s, then 8s. `retry` counts down, so the first retry is the one with the most left.\n retryDelay: ({ options }) => 2_000 * 2 ** (REGISTER_RETRIES - Number(options.retry)),\n })\n\n return true\n } catch (error) {\n if (rejectedWith(error, 401)) {\n throw new InvalidAgentTokenError(studioUrl, { cause: error })\n }\n\n console.error(styleText('red', `Failed to register agent with Studio after ${REGISTER_RETRIES + 1} attempts`))\n\n return false\n }\n}\n\ntype DisconnectProps = {\n studioUrl: string\n token: string\n sessionId: string\n slug?: string | null\n}\n\n/**\n * Notify Kubb Studio that this agent is disconnecting.\n * Called on process termination or server close. A failed notify is logged and swallowed: the\n * local socket is already gone, and failing teardown must not block shutdown or reconnect.\n */\nexport async function disconnect({ sessionId, token, studioUrl, slug }: DisconnectProps): Promise<void> {\n const url = `${studioUrl}/api/agent/sessions/${sessionId}/disconnect`\n const tag = slug ?? 'agent'\n\n try {\n await ofetch(url, {\n method: 'POST',\n headers: {\n Authorization: `Bearer ${token}`,\n },\n })\n console.log(styleText('green', `[${tag}] Disconnected from Studio`))\n } catch (error) {\n console.warn(styleText('yellow', `[${tag}] Failed to notify Studio of disconnection: ${getErrorMessage(error)}`))\n }\n}\n","","import type { Hookable, KubbHooks } from '@kubb/core'\nimport { x } from 'tinyexec'\n\n/**\n * Events a host emits about its Kubb Studio session, as opposed to a generation. `kubb:` stays\n * reserved for generation lifecycle.\n */\nexport type StudioConnectingContext = {\n /**\n * The Studio instance this session is opening against.\n */\n url: string\n}\n\nexport type StudioConnectedContext = {\n /**\n * The Studio instance this session attached to.\n */\n url: string\n /**\n * Both sides of the connection, so a host can print them and make a mismatch visible.\n */\n versions: {\n /**\n * The Studio instance's own version, when it sent one.\n */\n studio?: string\n /**\n * The version of the runtime that connected.\n */\n kubb: string\n /**\n * The version of the host itself, such as the `kubb` CLI or the agent image.\n */\n agent: string\n }\n}\n\nexport type StudioDisconnectedContext = {\n /**\n * Why Studio ended the session.\n */\n reason: string\n}\n\nexport type StudioCommandStartContext = {\n /**\n * The command Studio sent, without its `studio:` prefix: `generate`, `connect` or `save`.\n */\n command: string\n}\n\nexport type StudioCommandEndContext = {\n /**\n * The command that finished, without its `studio:` prefix.\n */\n command: string\n /**\n * What the command did, when there is something to report: `applied 2/3 edits to kubb.config.ts`.\n */\n info?: string\n}\n\nexport type StudioWarnContext = {\n /**\n * What was refused or ignored, and what would change it.\n */\n message: string\n}\n\nexport type StudioErrorContext = {\n /**\n * The failure, for the host's own output. One Studio needs to hear about goes over the socket\n * through the `kubb:error` generation hook instead.\n */\n error: Error\n}\n\ndeclare global {\n namespace Kubb {\n interface KubbHooksRegistry {\n 'studio:connecting': [ctx: StudioConnectingContext]\n 'studio:connected': [ctx: StudioConnectedContext]\n 'studio:disconnected': [ctx: StudioDisconnectedContext]\n 'studio:command:start': [ctx: StudioCommandStartContext]\n 'studio:command:end': [ctx: StudioCommandEndContext]\n 'studio:warn': [ctx: StudioWarnContext]\n 'studio:error': [ctx: StudioErrorContext]\n }\n }\n}\n\n/**\n * Register a `kubb:hook:start` listener that spawns the requested command via tinyexec,\n * streams each stdout line as a `kubb:hook:line` event, and calls `kubb:hook:end` with the result.\n * Streaming the output lets Kubb Studio render live hook progress over the WebSocket connection.\n */\nexport function setupHookListener(hooks: Hookable<KubbHooks>, root: string): void {\n hooks.hook('kubb:hook:start', async (ctx) => {\n const { id, command, args } = ctx\n // No id means nothing is waiting on the result (benchmarks, tests).\n if (!id) {\n return\n }\n\n const commandWithArgs = args?.length ? `${command} ${args.join(' ')}` : command\n\n try {\n const proc = x(command, [...(args ?? [])], {\n nodeOptions: { cwd: root, detached: true },\n })\n\n for await (const line of proc) {\n await hooks.callHook('kubb:hook:line', { id, line })\n }\n\n const { exitCode } = await proc\n\n if (exitCode !== 0) {\n const error = new Error(`Hook execute failed: ${commandWithArgs}`)\n\n await hooks.callHook('kubb:hook:end', { id, command, args, success: false, error })\n await hooks.callHook('kubb:error', { error })\n\n return\n }\n\n await hooks.callHook('kubb:hook:end', { id, command, args, success: true, error: null })\n } catch (caughtError) {\n const error = new Error(`Hook execute failed: ${commandWithArgs}`)\n error.cause = caughtError\n\n await hooks.callHook('kubb:hook:end', { id, command, args, success: false, error })\n await hooks.callHook('kubb:error', { error })\n }\n })\n}\n\n/**\n * Waits for the `kubb:hook:end` matching `hookId`. Register this before calling `kubb:hook:start`:\n * `callHook` awaits its listeners, and {@link setupHookListener} calls `kubb:hook:end` from inside\n * that same listener, so a handler added afterward would already have missed it.\n */\nexport function waitForHookEnd(hooks: Hookable<KubbHooks>, hookId: string): Promise<void> {\n return new Promise((resolve, reject) => {\n const handleHookEnd = (ctx: { id?: string; success: boolean; error?: Error | null }) => {\n if (ctx.id !== hookId) return\n hooks.removeHook('kubb:hook:end', handleHookEnd)\n\n if (ctx.success) {\n resolve()\n } else {\n reject(ctx.error)\n }\n }\n\n hooks.hook('kubb:hook:end', handleHookEnd)\n })\n}\n","import { hash } from 'node:crypto'\nimport path from 'node:path'\nimport process from 'node:process'\nimport { styleText } from 'node:util'\nimport { type Config, createKubb, type Diagnostic, Diagnostics, type Hookable, type KubbHooks } from '@kubb/core'\nimport {\n FORMATTER_PREFERENCE,\n LINTER_PREFERENCE,\n formatters,\n linters,\n memoize,\n tokenize,\n type ToolCommand,\n detectTool as detectUncachedTool,\n} from '@internals/utils'\nimport { waitForHookEnd } from './hooks.ts'\n\n/**\n * `isToolAvailable` spawns a process, and a long-lived connection generates repeatedly, so each\n * executable is probed once per process. The CLI deliberately does not memoize: a `--watch` build\n * should keep noticing a tool installed mid-session.\n */\nconst detectTool = memoize(new Map<ReadonlyArray<string>, Promise<string | null>>(), detectUncachedTool)\n\n/**\n * The two post-build tool steps. Formatting and linting differ only in which tools they look for,\n * so they run through one loop rather than two near-identical blocks.\n *\n * `noun` and `verbing` are spelled out instead of built from `kind`. Concatenating `` `${kind}ter` ``\n * and `` `${kind}ting` `` works for `format`, but doubles the `t` in `lint`, giving \"lintter\" and\n * \"lintting\" instead of \"linter\" and \"linting\".\n */\nconst TOOL_STEPS: ReadonlyArray<{ kind: 'format' | 'lint'; noun: string; verbing: string; tools: Record<string, ToolCommand>; detect: ReadonlyArray<string> }> =\n [\n // `detect` is the preference order for `auto`, most-preferred first. Spelled out rather than\n // taken from the table's key order, which is arbitrary and silently changes what `auto` picks.\n { kind: 'format', noun: 'formatter', verbing: 'Formatting', tools: formatters, detect: FORMATTER_PREFERENCE },\n { kind: 'lint', noun: 'linter', verbing: 'Linting', tools: linters, detect: LINTER_PREFERENCE },\n ]\n\n/**\n * Absolute path of the directory the formatter and linter are pointed at.\n */\nfunction outputPath(config: Config): string {\n return path.isAbsolute(config.output.path) ? config.output.path : path.resolve(process.cwd(), config.root, config.output.path)\n}\n\ntype RunHookProps = {\n hooks: Hookable<KubbHooks>\n /**\n * Stable identity for the command, hashed so the `kubb:hook:*` events for concurrent commands\n * can be told apart.\n */\n id: string\n command: string\n args: ReadonlyArray<string>\n}\n\n/**\n * Emits `kubb:hook:start` and waits for the matching `kubb:hook:end`. The host spawns the process:\n * this only describes what to run and when it finished.\n *\n * @throws whatever the command failed with, so callers can report it their own way.\n */\nasync function runHook({ hooks, id, command, args }: RunHookProps): Promise<void> {\n const hookId = hash('sha256', id)\n // Registered before the start event, since `callHook` awaits its listeners and the host calls\n // `kubb:hook:end` from inside that same listener.\n const hookEnd = waitForHookEnd(hooks, hookId)\n\n await hooks.callHook('kubb:hook:start', { id: hookId, command, args: [...args] })\n await hookEnd\n}\n\ntype GenerateProps = {\n config: Config\n hooks: Hookable<KubbHooks>\n}\n\nfunction isProblemErrorDiagnostic(diagnostic: Diagnostic): diagnostic is Diagnostic & { plugin?: string; message: string } {\n return (diagnostic.kind ?? 'problem') === 'problem' && diagnostic.severity === 'error'\n}\n\n/**\n * Folds error-severity diagnostics into one thrown error so logs name the failing plugin.\n */\nfunction formatGenerationFailure(diagnostics: ReadonlyArray<Diagnostic>): Error {\n const reasons = diagnostics\n .filter(isProblemErrorDiagnostic)\n .map((diagnostic) => (diagnostic.plugin ? `${diagnostic.plugin}: ${diagnostic.message}` : diagnostic.message))\n\n if (!reasons.length) {\n return new Error('Generation failed')\n }\n\n return new Error(`Generation failed: ${reasons.length} error${reasons.length === 1 ? '' : 's'}: ${reasons.join('; ')}`)\n}\n\n/**\n * Runs a full Kubb code-generation cycle for the given config.\n *\n * Emits lifecycle events on the provided `hooks` emitter so callers (e.g. the WebSocket stream)\n * can forward progress to connected clients. After a successful build, auto-formatting and\n * linting are applied when configured, followed by any user-defined `hooks.done` commands.\n */\nexport async function generate({ config, hooks }: GenerateProps): Promise<void> {\n const hrStart = process.hrtime()\n\n await hooks.callHook('kubb:generation:start', { config })\n\n await hooks.callHook('kubb:info', { message: config.name ? `Setup generation ${config.name}` : 'Setup generation' })\n\n const kubb = createKubb(config, { hooks })\n await kubb.setup()\n\n await hooks.callHook('kubb:info', { message: config.name ? `Build generation ${config.name}` : 'Build generation' })\n\n const { files, diagnostics, storage } = await kubb.safeBuild()\n\n await hooks.callHook('kubb:info', { message: 'Load summary' })\n\n // Core captures build failures as `error`-severity diagnostics instead of throwing, so\n // surface each one as a `kubb:error` event for the client. Warnings and info reported\n // through `ctx.warn`/`ctx.info` reach the client directly via core's events on this shared emitter.\n for (const diagnostic of diagnostics.filter(isProblemErrorDiagnostic)) {\n await hooks.callHook('kubb:error', { error: new Error(diagnostic.plugin ? `${diagnostic.plugin}: ${diagnostic.message}` : diagnostic.message) })\n }\n\n const status = Diagnostics.hasError(diagnostics) ? 'failed' : 'success'\n\n await hooks.callHook('kubb:generation:end', {\n config,\n // Only the files Kubb generated. `fsStorage().readKeys()` lists the working directory, so\n // Studio's tree would show the host's own source and miss output landing outside it.\n storage: { ...storage, readKeys: async () => [...new Set(files.map((file) => file.path))] },\n diagnostics,\n status,\n hrStart,\n filesCreated: files.length,\n })\n\n if (status === 'failed') {\n throw formatGenerationFailure(diagnostics)\n }\n\n await hooks.callHook('kubb:success', { message: 'Generation successfully' })\n\n for (const step of TOOL_STEPS) {\n const setting = config.output[step.kind]\n if (!setting) {\n continue\n }\n\n await hooks.callHook(`kubb:${step.kind}:start`)\n\n // `auto` means \"whatever is installed\", so the tool is detected now rather than at config time.\n const tool = setting === 'auto' ? await detectTool(step.detect) : setting\n\n if (!tool) {\n await hooks.callHook('kubb:warn', { message: `No ${step.noun} found (${step.detect.join(', ')}). Skipping ${step.verbing.toLowerCase()}.` })\n }\n\n if (tool && setting === 'auto') {\n await hooks.callHook('kubb:info', { message: `Auto-detected ${step.noun}: ${styleText('dim', tool)}` })\n }\n\n const command = tool ? step.tools[tool] : undefined\n\n if (command) {\n try {\n await runHook({ hooks, id: [config.name, tool].filter(Boolean).join('-'), command: command.command, args: command.args(outputPath(config)) })\n\n await hooks.callHook('kubb:success', { message: `${step.verbing} with ${tool} successfully` })\n } catch (caughtError) {\n await hooks.callHook('kubb:error', { error: new Error(command.errorMessage, { cause: caughtError }) })\n }\n }\n\n await hooks.callHook(`kubb:${step.kind}:end`)\n }\n\n // `output.postGenerate` commands run in order, each one waiting for the previous to finish.\n if (config.output.postGenerate?.length) {\n await hooks.callHook('kubb:hooks:start')\n\n for (const entry of config.output.postGenerate) {\n const line = typeof entry === 'string' ? entry : entry.command\n const [cmd, ...args] = tokenize(line)\n\n if (!cmd) {\n continue\n }\n\n await runHook({ hooks, id: line, command: cmd, args })\n await hooks.callHook('kubb:success', { message: `${line} successfully executed` })\n }\n\n await hooks.callHook('kubb:hooks:end')\n }\n}\n","import { getElapsedMs, inParallel } from '@internals/utils'\nimport { Diagnostics, type Hookable, type KubbHooks } from '@kubb/core'\nimport WebSocket from 'ws'\nimport type { AgentMessage, DataMessagePayload } from './protocol/index.ts'\n\ntype WebSocketOptions = WebSocket.ClientOptions\n\n/**\n * How many generated files are read from storage at once when building the\n * `kubb:generation:end` payload. A spec producing thousands of files would otherwise fire one\n * `storage.readItem` per file simultaneously.\n */\nconst FILE_READ_CONCURRENCY = 50\n\n/**\n * How long the initial handshake may take before the socket is closed and the reconnect loop\n * takes over.\n */\nconst CONNECT_TIMEOUT_MS = 5_000\n\n/**\n * Per-socket event counter. Every data message carries the next value so Studio can restore the\n * agent's emission order even when the relay delivers frames out of order. Keyed by the socket so\n * the count stays monotonic across every generation run on one connection, and is dropped\n * automatically once the socket is collected.\n */\nconst eventSeqCounters = new WeakMap<WebSocket, number>()\n\nfunction nextEventSeq(ws: WebSocket): number {\n const seq = eventSeqCounters.get(ws) ?? 0\n eventSeqCounters.set(ws, seq + 1)\n\n return seq\n}\n\n/**\n * Opens a Studio WebSocket connection and closes it when the initial handshake exceeds the configured timeout.\n */\nexport function createWebsocket(url: string, options: WebSocketOptions): WebSocket {\n const ws = new WebSocket(url, options)\n\n const timer = setTimeout(() => {\n if (ws.readyState === WebSocket.CONNECTING) {\n ws.close(3008, 'Connection timeout')\n }\n }, CONNECT_TIMEOUT_MS)\n\n // Once the handshake settles the timer has nothing left to check, and leaving it pending holds\n // the socket for the rest of the window.\n ws.once('open', () => clearTimeout(timer))\n ws.once('close', () => clearTimeout(timer))\n\n return ws\n}\n\n/**\n * Sends a serialized agent message when the Studio socket is ready to accept frames.\n */\nexport function sendAgentMessage(ws: WebSocket, message: AgentMessage): void {\n try {\n if (ws.readyState !== WebSocket.OPEN) {\n return\n }\n\n ws.send(JSON.stringify(message))\n } catch (error) {\n throw new Error('Failed to send message to Kubb Studio', { cause: error })\n }\n}\n\n/**\n * Sends a single `kubb:error` payload to Studio, stamped from the same per-socket counter the event stream\n * uses so Studio can still order it against the generation events around it.\n */\nexport function sendErrorMessage(ws: WebSocket, error: Error): void {\n sendAgentMessage(ws, {\n type: 'agent:data',\n payload: { type: 'kubb:error', data: [{ message: error.message, stack: error.stack }], timestamp: Date.now(), seq: nextEventSeq(ws) },\n })\n}\n\n/**\n * Forwards selected Kubb lifecycle events to Studio as data messages for the active session.\n */\nexport function setupEventsStream(ws: WebSocket, hooks: Hookable<KubbHooks>): void {\n function sendDataMessage(payload: Omit<DataMessagePayload, 'seq' | 'timestamp'>) {\n sendAgentMessage(ws, {\n type: 'agent:data',\n payload: { ...payload, timestamp: Date.now(), seq: nextEventSeq(ws) },\n })\n }\n\n hooks.hook('kubb:plugin:start', (ctx) => {\n sendDataMessage({\n type: 'kubb:plugin:start',\n data: [{ plugin: ctx.plugin }],\n })\n })\n\n hooks.hook('kubb:plugin:end', (ctx) => {\n sendDataMessage({\n type: 'kubb:plugin:end',\n data: [{ plugin: ctx.plugin, duration: ctx.duration, success: ctx.success }],\n })\n })\n\n hooks.hook('kubb:build:start', ({ config, adapter }) => {\n sendDataMessage({\n type: 'kubb:build:start',\n data: [{ config: { name: config.name }, adapter: { name: adapter.name } }],\n })\n })\n\n hooks.hook('kubb:build:end', ({ files, outputDir }) => {\n sendDataMessage({\n type: 'kubb:build:end',\n data: [{ files: files.map((file) => ({ path: file.path, name: file.name })), outputDir }],\n })\n })\n\n hooks.hook('kubb:files:processing:start', ({ files }) => {\n sendDataMessage({\n type: 'kubb:files:processing:start',\n data: [{ total: files.length }],\n })\n })\n\n hooks.hook('kubb:files:processing:update', ({ files }) => {\n sendDataMessage({\n type: 'kubb:files:processing:update',\n data: [\n {\n files: files.map(({ file, processed, total, percentage }) => ({\n file: file.path,\n processed,\n total,\n percentage,\n })),\n },\n ],\n })\n })\n\n hooks.hook('kubb:files:processing:end', ({ files }) => {\n sendDataMessage({\n type: 'kubb:files:processing:end',\n data: [{ total: files.length }],\n })\n })\n\n // The three log levels differ only in their event name.\n for (const type of ['kubb:info', 'kubb:success', 'kubb:warn'] as const) {\n hooks.hook(type, ({ message, info }) => {\n sendDataMessage({ type, data: [{ message, info }] })\n })\n }\n\n hooks.hook('kubb:generation:start', ({ config }) => {\n sendDataMessage({\n type: 'kubb:generation:start',\n data: [\n {\n name: config.name,\n plugins: config.plugins.length,\n },\n ],\n })\n })\n\n hooks.hook('kubb:generation:end', async ({ config, storage, diagnostics = [], status, hrStart, filesCreated }) => {\n const paths = await storage.readKeys()\n const files: Record<string, string> = {}\n await inParallel({\n items: paths,\n limit: FILE_READ_CONCURRENCY,\n run: async (path) => {\n const content = await storage.readItem(path)\n if (content !== null) files[path] = content\n },\n })\n\n sendDataMessage({\n type: 'kubb:generation:end',\n data: [{ config, storage: files }],\n })\n\n if (!hrStart) {\n return\n }\n\n const duration = Math.round(getElapsedMs(hrStart))\n\n sendDataMessage({\n type: 'kubb:generation:summary',\n data: [{ duration, fileCount: filesCreated ?? 0, failedPlugins: Diagnostics.failedPlugins(diagnostics).length, status: status ?? 'success' }],\n })\n })\n\n hooks.hook('kubb:error', ({ error }) => {\n sendDataMessage({\n type: 'kubb:error',\n data: [\n {\n message: error.message,\n stack: error.stack,\n },\n ],\n })\n })\n\n // Bracketing events carry no context, so they forward identically.\n for (const type of [\n 'kubb:lifecycle:start',\n 'kubb:lifecycle:end',\n 'kubb:format:start',\n 'kubb:format:end',\n 'kubb:lint:start',\n 'kubb:lint:end',\n 'kubb:hooks:start',\n 'kubb:hooks:end',\n ] as const) {\n hooks.hook(type, () => {\n sendDataMessage({ type, data: [] })\n })\n }\n\n hooks.hook('kubb:hook:start', ({ id, command, args }) => {\n sendDataMessage({\n type: 'kubb:hook:start',\n data: [{ id, command, args: args ? [...args] : undefined }],\n })\n })\n\n hooks.hook('kubb:hook:line', ({ id, line }) => {\n sendDataMessage({\n type: 'kubb:hook:line',\n data: [{ id, line }],\n })\n })\n\n hooks.hook('kubb:hook:end', ({ id, command, args, success, error }) => {\n sendDataMessage({\n type: 'kubb:hook:end',\n data: [\n {\n id,\n command,\n args: args ? [...args] : undefined,\n success,\n error: error ? { message: error.message, stack: error.stack } : undefined,\n },\n ],\n })\n })\n}\n","import { readFile, writeFile } from 'node:fs/promises'\nimport path from 'node:path'\nimport process from 'node:process'\nimport { styleText } from 'node:util'\nimport { getErrorMessage, toError } from '@internals/utils'\nimport { type Config, fsStorage, Hookable, type KubbHooks, memoryStorage } from '@kubb/core'\nimport { version as kubbVersion } from '../package.json'\nimport { setupHookListener } from './hooks.ts'\nimport { type AgentMessage, type ClientInfo, type ConfigFileView, isCommandMessage, isDisconnectMessage, isStudioPingMessage } from './protocol/index.ts'\nimport { createAgentSession, disconnect, InvalidAgentTokenError } from './api.ts'\nimport { generate } from './generate.ts'\nimport { agentDefaults } from './constants.ts'\nimport { mergeAdapter, mergePlugins } from './resolveConfig.ts'\nimport type WebSocket from 'ws'\nimport { createWebsocket, sendAgentMessage, sendErrorMessage, setupEventsStream } from './ws.ts'\n\nexport type ConnectToStudioOptions = {\n token: string\n studioUrl?: string\n configPath: string\n /**\n * Loads the on-disk Kubb config. Injected so each host resolves config its own way: the Docker\n * agent from an explicit `KUBB_AGENT_CONFIG` path, the CLI through the same discovery\n * `kubb generate` uses.\n */\n loadConfig: () => Promise<Config>\n /**\n * The runtime's own version, reported to Studio next to the `kubb` version.\n */\n version: string\n /**\n * Identifies the host to Studio, so the UI can badge a CLI connection and show the real project.\n */\n client?: ClientInfo\n allowWrite?: boolean\n /**\n * Whether Studio may edit the project's `kubb.config.ts`. Granted separately from `allowWrite`,\n * which only covers generated output: this rewrites a file the user wrote by hand.\n */\n allowConfigEdit?: boolean\n allowInput?: boolean\n /**\n * Whether the formatter, the linter, and `output.postGenerate` may run as child processes.\n * Defaults to true, which is what the Docker agent has always done. The CLI runs in the user's\n * own project, so it defaults this off and asks before granting it.\n */\n allowExec?: boolean\n root?: string\n retryInterval?: number\n heartbeatInterval?: number\n /**\n * Number of pool sessions this agent serves. Read by `createClient`, which opens one\n * `connectToStudio` per slot, and reported to Studio at registration.\n */\n poolSize?: number\n /**\n * Aborting this disconnects the session and stops the reconnect loop. Hosts wire it to their own\n * shutdown: Nitro's `close` hook, or `SIGINT`/`SIGTERM` in the CLI.\n */\n signal?: AbortSignal\n /**\n * Installs listeners on an event emitter, once for the session and once per generation. Left out,\n * the runtime prints nothing, which is what a library should default to.\n */\n installLogger?: (hooks: Hookable<KubbHooks>) => void | Promise<void>\n}\n\n/**\n * Schedules another connection attempt.\n *\n * Hoisted out of `connectToStudio` on purpose: a pending retry timer reaches its whole enclosing\n * scope, so keeping it inside would pin the closed socket, the hook emitter, and the session id\n * alive for the length of every retry interval.\n */\nfunction reconnect(options: ConnectToStudioOptions): void {\n const { signal, retryInterval = agentDefaults.retryIntervalMs } = options\n\n if (signal?.aborted) {\n return\n }\n\n console.info(styleText('dim', `Retrying connection in ${retryInterval}ms to Kubb Studio ...`))\n\n const cancel = () => clearTimeout(timer)\n const timer = setTimeout(() => {\n // Removed here rather than left to `{ once: true }`: the signal only aborts at shutdown, so one\n // listener per retry would accumulate for the whole life of a down-Studio retry loop.\n signal?.removeEventListener('abort', cancel)\n\n if (signal?.aborted) {\n return\n }\n\n // The rejection is never awaited, so it has to be caught here or it surfaces as an\n // unhandledRejection that kills the retry loop instead of trying again.\n connectToStudio(options).catch((error: unknown) => {\n console.error(styleText('red', `Reconnect attempt to Kubb Studio failed: ${getErrorMessage(error)}`))\n\n // A rejected token stays rejected, so retrying only spams 401s until the process is killed.\n if (error instanceof InvalidAgentTokenError) {\n return\n }\n\n reconnect(options)\n })\n }, retryInterval)\n\n signal?.addEventListener('abort', cancel, { once: true })\n}\n\nexport async function connectToStudio(options: ConnectToStudioOptions): Promise<void> {\n const {\n token,\n studioUrl = agentDefaults.studioUrl,\n configPath,\n loadConfig,\n version,\n client,\n // Every permission is off unless the host grants it.\n allowWrite = false,\n allowConfigEdit = false,\n allowInput = false,\n allowExec = false,\n root = process.cwd(),\n heartbeatInterval: requestedHeartbeatInterval = agentDefaults.heartbeatIntervalMs,\n signal,\n installLogger,\n } = options\n\n // Studio counts an agent offline once its last ping is older than its liveness window, so a\n // slower cadence would make a healthy agent invisible. Clamped here rather than in a host's env\n // parsing, so every host is held to the contract.\n const heartbeatInterval = Math.min(requestedHeartbeatInterval, agentDefaults.heartbeatIntervalMs)\n\n // Each connection gets its own isolated event emitter so generation events\n // from one session do not bleed into another session's WebSocket stream.\n const hooks = new Hookable<KubbHooks>()\n await installLogger?.(hooks)\n\n try {\n // Before the session exists, so a host can cover the wait: `createAgentSession` is a round\n // trip and the socket after it opens without being awaited.\n await hooks.callHook('studio:connecting', { url: studioUrl })\n\n const { sessionId, slug, wsUrl, isSandbox, version: sessionStudioVersion } = await createAgentSession({ token, studioUrl })\n\n // Known before the agent announces itself, and refreshed by a later `studio:connect`, so both\n // sides can be named from the first connect on.\n let studioVersion = sessionStudioVersion\n const ws = createWebsocket(wsUrl, {\n headers: { Authorization: `Bearer ${token}` },\n })\n\n // Effective permissions: always disabled in sandbox mode\n const canWrite = isSandbox ? false : allowWrite\n // A sandbox has no user project to edit, so a config edit is never granted there.\n const canEditConfig = isSandbox ? false : allowConfigEdit\n // `configPath` is relative to the agent's root unless it is already absolute, which is what\n // `resolve` does on its own.\n const configFilePath = path.resolve(root, configPath)\n // A sandbox agent always generates from the spec Studio supplies; a local agent only when opted in.\n const canUseInput = isSandbox || allowInput\n // Tracks whether the studio server explicitly disconnected us (no reconnect needed)\n let serverDisconnected = false\n // Guards against a second `generate` command starting while one is already running.\n // Without this, two concurrent `generate()` calls share this socket via `setupEventsStream`,\n // and their events interleave with no way for Studio to tell the two runs apart.\n let isGenerating = false\n let heartbeatTimer: ReturnType<typeof setInterval> | undefined\n // Tracks socket liveness: Studio replies to every ping with a pong. When pongs stop\n // arriving the connection is half-open (e.g. dropped during a Studio deploy) and must\n // be terminated so the reconnect loop can establish a fresh session.\n let lastPongAt = Date.now()\n\n const onAbort = () => void teardown({ reason: 'shutdown', retry: false })\n\n function cleanup(reason = 'cleanup') {\n clearInterval(heartbeatTimer)\n heartbeatTimer = undefined\n\n // This connection is over, so its shutdown listener must go with it. Otherwise every\n // reconnect leaves one behind on a signal that only fires at process exit.\n signal?.removeEventListener('abort', onAbort)\n\n hooks.removeAllHooks()\n\n try {\n ws.close(1000, reason)\n } catch {}\n\n ws.removeEventListener('open', onOpen)\n ws.removeEventListener('close', onClose)\n ws.removeEventListener('error', onError)\n ws.removeEventListener('message', onMessage)\n }\n\n /**\n * Reads `kubb.config.ts` and reports which plugin options Studio may edit.\n *\n * Skipped when the host did not grant `allowConfigEdit`. The patcher pulls in `magicast`\n * (~25ms, ~55MB RSS), so read-only agents never import it.\n *\n * Not cached: the user can edit the file between two Studio actions.\n */\n async function readConfigFileView(source?: string): Promise<ConfigFileView | undefined> {\n if (!canEditConfig) {\n return undefined\n }\n\n try {\n const { readConfig } = await import('./configFile.ts')\n\n return readConfig(source ?? (await readFile(configFilePath, 'utf-8')))\n } catch (error) {\n await hooks.callHook('studio:warn', { message: `Could not read ${configFilePath}: ${getErrorMessage(error)}` })\n\n return undefined\n }\n }\n\n async function sendConnectedPayload() {\n const config = await loadConfig()\n\n sendAgentMessage(ws, {\n type: 'agent:connect',\n payload: {\n versions: { kubb: kubbVersion, agent: version },\n root,\n config: {\n path: configPath,\n file: await readConfigFileView(),\n plugins: config.plugins.map((plugin) => ({\n name: `@kubb/${plugin.name}`,\n // Functions and symbols in plugin options are dropped by `JSON.stringify` on the way out.\n options: plugin.options ?? {},\n })),\n },\n permissions: {\n allowWrite: canWrite,\n allowInput: canUseInput,\n allowExec,\n allowConfigEdit: canEditConfig,\n },\n },\n })\n }\n\n async function handleOpen() {\n lastPongAt = Date.now()\n await hooks.callHook('studio:connected', { url: studioUrl, versions: { studio: studioVersion, kubb: kubbVersion, agent: version } })\n\n // Announce readiness without waiting for a `studio:connect` command. The command from the\n // Studio UI is lost when it is sent while the agent is not attached to the session\n // (e.g. reconnecting after a deploy), so the agent introduces itself on every open.\n try {\n await sendConnectedPayload()\n } catch (error) {\n await hooks.callHook('studio:warn', { message: `Failed to send the connect payload: ${getErrorMessage(error)}` })\n }\n }\n\n // `addEventListener` drops the returned promise, so a host whose logger throws would take the\n // process down with an unhandled rejection instead of just losing a line of output. Nothing is\n // left to report it with at that point, which is why this swallows.\n const onOpen = () => void handleOpen().catch(() => {})\n\n /**\n * Drops the socket and tells Studio the session is over. `serverDisconnected` guards against\n * the close event running this a second time, and against a shutdown reconnecting.\n */\n async function teardown({ reason, retry }: { reason?: string; retry: boolean }) {\n if (serverDisconnected) {\n return\n }\n serverDisconnected = true\n\n // Announce the shutdown while the socket is still open, so Studio marks the session offline\n // now instead of waiting out the heartbeat window. `sendAgentMessage` is a no-op on a socket\n // that has already closed, which is every other way we get here.\n if (reason === 'shutdown') {\n sendAgentMessage(ws, { type: 'agent:disconnect', reason: 'shutdown' })\n }\n\n cleanup(reason)\n // Already tearing down, so a failed disconnect changes nothing.\n await disconnect({ sessionId, studioUrl, token, slug }).catch(() => {})\n\n if (retry) {\n reconnect(options)\n }\n }\n\n const onClose = () => teardown({ retry: true })\n\n const onError = () => {\n void hooks.callHook('studio:error', { error: new Error('Failed to connect to Kubb Studio') })\n\n return onClose()\n }\n\n ws.addEventListener('open', onOpen)\n ws.addEventListener('close', onClose)\n ws.addEventListener('error', onError)\n // The socket's own close event fires after this, and `teardown` is idempotent, so the shutdown\n // path cannot be turned into a reconnect by the close that follows it.\n signal?.addEventListener('abort', onAbort, { once: true })\n\n heartbeatTimer = setInterval(() => {\n // Two consecutive missed pongs mean the socket is dead even though no close event\n // arrived. Terminate (not close) so a half-open TCP connection can't linger. The\n // resulting close event triggers cleanup and the reconnect loop.\n if (Date.now() - lastPongAt > heartbeatInterval * 2) {\n void hooks.callHook('studio:warn', { message: 'No reply from Kubb Studio, terminating the stale connection' })\n // Stop the timer here rather than waiting for cleanup(), since the close event can lag,\n // and until it runs this interval would re-terminate and re-log every tick.\n clearInterval(heartbeatTimer)\n heartbeatTimer = undefined\n ws.terminate()\n\n return\n }\n\n sendAgentMessage(ws, { type: 'agent:ping' })\n }, heartbeatInterval)\n\n // Only `kubb:error` is ever fired on the connection emitter. Every generation event goes\n // through its own emitter below, so it gets that one listener rather than the full stream.\n hooks.hook('kubb:error', ({ error }) => sendErrorMessage(ws, error))\n\n const onMessage = async (message: WebSocket.MessageEvent) => {\n try {\n const data = JSON.parse(message.data as string) as AgentMessage\n\n if (isStudioPingMessage(data)) {\n lastPongAt = Date.now()\n\n return\n }\n\n if (isDisconnectMessage(data)) {\n await hooks.callHook('studio:disconnected', { reason: data.reason })\n\n if (data.reason === 'revoked') {\n cleanup(`session_${data.reason}`)\n return\n }\n\n if (data.reason === 'expired') {\n cleanup()\n reconnect(options)\n\n return\n }\n\n return\n }\n\n if (isCommandMessage(data)) {\n // Every command type is `studio:<verb>`, so the verb alone is what a host wants to show.\n const command = data.type.slice('studio:'.length)\n\n await hooks.callHook('studio:command:start', { command })\n\n if (data.type === 'studio:generate') {\n if (isGenerating) {\n await hooks.callHook('studio:warn', { message: 'Ignored generate: a generation is already in progress' })\n\n await Promise.resolve(\n hooks.callHook('kubb:error', { error: new Error('A generation is already in progress, please wait for it to finish') }),\n ).catch(() => {})\n\n return\n }\n\n isGenerating = true\n\n try {\n const config = await loadConfig()\n const patch = data.payload\n const plugins = await mergePlugins(config.plugins, patch?.plugins)\n const adapter = await mergeAdapter(config.adapter, patch?.adapter)\n\n // A sandbox agent always uses the inline spec (empty string included, since it has no disk\n // file); a local agent only when opted in, and an empty or absent spec falls back to disk.\n const inputOverride = isSandbox ? (patch?.input ?? '') : (allowInput && patch?.input) || undefined\n\n if (allowWrite && isSandbox) {\n await hooks.callHook('studio:warn', { message: 'Running in a sandbox, so writing files is disabled' })\n }\n\n if (patch?.input && !canUseInput) {\n // The Docker agent reads `allowInput` from `KUBB_AGENT_ALLOW_INPUT`. The CLI grants it\n // through `--allowInput` or the per-project prompt instead, so each host gets its own remedy.\n const remedy = client?.kind === 'cli' ? '--allowInput, or answer yes when kubb studio asks,' : 'KUBB_AGENT_ALLOW_INPUT=true'\n await hooks.callHook('studio:warn', { message: `Ignored the spec from Studio; set ${remedy} to generate from it` })\n }\n\n const generationHooks = new Hookable<KubbHooks>()\n await installLogger?.(generationHooks)\n setupHookListener(generationHooks, root)\n setupEventsStream(ws, generationHooks)\n\n const resolvedPlugins = plugins ?? config.plugins\n\n await generate({\n config: {\n ...config,\n root,\n input: inputOverride ?? config.input,\n storage: canWrite ? fsStorage() : memoryStorage(),\n output: allowExec ? { ...config.output } : { ...config.output, format: false, lint: false, postGenerate: [] },\n plugins: resolvedPlugins,\n adapter,\n },\n hooks: generationHooks,\n })\n\n await hooks.callHook('studio:command:end', {\n command,\n info: `${resolvedPlugins.length} plugin${resolvedPlugins.length === 1 ? '' : 's'}, ${canWrite ? 'written to disk' : 'in memory'}${inputOverride !== undefined ? ', from a Studio spec' : ''}`,\n })\n } finally {\n isGenerating = false\n }\n\n return\n }\n\n if (data.type === 'studio:connect') {\n studioVersion = data.version ?? studioVersion\n await sendConnectedPayload()\n\n await hooks.callHook('studio:command:end', { command })\n\n return\n }\n\n if (data.type === 'studio:save') {\n // Studio waits on an `agent:save` for every `studio:save`, so every path out of this\n // branch sends one. `edits` is checked before it is walked: the message crosses the same\n // trust boundary as the values inside it.\n if (!Array.isArray(data.edits)) {\n await hooks.callHook('studio:warn', { message: 'Ignored save: the message carried no edits' })\n\n sendAgentMessage(ws, { type: 'agent:save', payload: { outcomes: [], changed: false } })\n\n return\n }\n\n const edits = data.edits\n const refuse = (reason: string) =>\n sendAgentMessage(ws, {\n type: 'agent:save',\n payload: { outcomes: edits.map((edit) => ({ edit, applied: false, reason })), changed: false },\n })\n\n if (!canEditConfig) {\n await hooks.callHook('studio:warn', { message: 'Ignored save: editing kubb.config.ts was not granted' })\n\n refuse('the agent was not granted permission to edit kubb.config.ts')\n\n return\n }\n\n // A generation reloads the config while it runs, so rewriting the file underneath it\n // would leave that run working from half the change.\n if (isGenerating) {\n refuse('a generation is in progress')\n\n return\n }\n\n try {\n // Read straight before the patch rather than reusing what went out on connect. The user\n // may have edited the file since, and since every untouched node keeps its own text,\n // patching what is on disk right now preserves that edit.\n const { applyConfigEdits } = await import('./configFile.ts')\n const current = await readFile(configFilePath, 'utf-8')\n const { source: patched, outcomes, changed } = applyConfigEdits(current, edits)\n\n if (changed) {\n await writeFile(configFilePath, patched, 'utf-8')\n }\n\n sendAgentMessage(ws, {\n type: 'agent:save',\n payload: { outcomes, changed, file: changed ? await readConfigFileView(patched) : undefined },\n })\n\n const applied = outcomes.filter((outcome) => outcome.applied).length\n await hooks.callHook('studio:command:end', { command, info: `applied ${applied}/${outcomes.length} edits to ${configPath}` })\n } catch (error) {\n // An unreadable config, a read-only filesystem. Reported as a refusal of every edit so\n // Studio hears back rather than waiting on a reply that never comes.\n await hooks.callHook('studio:error', { error: toError(error) })\n\n refuse(getErrorMessage(error))\n }\n\n return\n }\n\n return\n }\n\n await hooks.callHook('studio:warn', { message: `Ignored an unknown message from Kubb Studio: ${data.type}` })\n } catch (error) {\n await hooks.callHook('studio:error', { error: toError(error) })\n\n // Errors thrown before `generate()` runs (e.g. config loading, plugin resolution)\n // never reach `generate()`'s own `kubb:error` emission, so without this the Studio\n // UI shows nothing while the agent silently fails. Forward them on the connection-level\n // `hooks` emitter, already wired to this socket via `setupEventsStream`.\n await Promise.resolve(hooks.callHook('kubb:error', { error: toError(error) })).catch(() => {})\n }\n }\n ws.addEventListener('message', onMessage)\n } catch (error) {\n // Reaching here means the session was never created (Studio down, a 502 mid-deploy), so no\n // socket exists and none of the socket-driven reconnect paths can fire. Retry from here or the\n // slot is dropped for the lifetime of the process.\n await hooks.callHook('studio:error', { error: toError(error) })\n\n if (error instanceof InvalidAgentTokenError) {\n throw error\n }\n\n reconnect(options)\n }\n}\n","import type { Storage } from 'unstorage'\nimport { agentDefaults } from './constants.ts'\nimport { registerAgent } from './api.ts'\nimport { type ConnectToStudioOptions, connectToStudio } from './connectStudio.ts'\nimport { setStorage } from './machine.ts'\n\nexport type ClientOptions = Omit<ConnectToStudioOptions, 'signal'> & {\n /**\n * Where the machine secret and the last Studio config are persisted. Defaults to in-memory,\n * which gives up a stable machine identity across restarts.\n */\n storage?: Storage\n}\n\nexport type Client = {\n /**\n * Registers with Studio and opens the session pool. Resolves once the pool is starting: the\n * sessions keep running, and reconnect on their own, until `disconnect` is called.\n */\n connect: () => Promise<void>\n /**\n * Closes every session and stops reconnecting.\n */\n disconnect: () => void\n}\n\n/**\n * Creates the Kubb Studio client: the connection, the command loop, and the generation event\n * stream shared by the `kubb studio` CLI command and the Docker agent.\n *\n * Every permission is off by default. A host that wants more grants it explicitly.\n *\n * @example\n * ```ts\n * const studio = createClient({ token, configPath, version, loadConfig: () => loadMyConfig() })\n * await studio.connect()\n * ```\n */\nexport function createClient({ storage, ...options }: ClientOptions): Client {\n if (storage) {\n setStorage(storage)\n }\n\n const controller = new AbortController()\n const poolSize = options.poolSize ?? agentDefaults.poolSize\n\n return {\n async connect() {\n await registerAgent({ token: options.token, studioUrl: options.studioUrl ?? agentDefaults.studioUrl, poolSize })\n\n // Each slot is its own session, so one Studio user never sees another's generation events.\n // Awaited: `connectToStudio` only ever rejects with `InvalidAgentTokenError` (every other\n // failure is retried internally through its own reconnect loop and resolves normally), so\n // awaiting here surfaces a dead token to the caller without blocking on a down Studio.\n await Promise.all(Array.from({ length: poolSize }, () => connectToStudio({ ...options, signal: controller.signal })))\n },\n disconnect() {\n controller.abort()\n },\n }\n}\n","import { setTimeout as delay } from 'node:timers/promises'\nimport { styleText } from 'node:util'\nimport { getErrorMessage } from '@internals/utils'\nimport { ofetch } from 'ofetch'\nimport { agentDefaults } from './constants.ts'\nimport { getMachineToken } from './machine.ts'\n\n/**\n * RFC 8628 device-authorization response from Studio's `/api/auth/device/code` endpoint.\n * Field names match the RFC; the CLI polls with `device_code` and shows `user_code` to the user.\n */\nexport type PairingSession = {\n device_code: string\n user_code: string\n verification_uri: string\n verification_uri_complete: string\n expires_in: number\n interval: number\n}\n\nexport type PairingResult = {\n /**\n * Bearer token for this machine. Write credentials with mode 0600 and never log the value.\n */\n token: string\n agent: {\n /**\n * Stable agent id in Studio.\n */\n id: string\n /**\n * Short slug used in logs and the UI (for example `brave-otter`).\n */\n slug: string\n /**\n * Display name chosen at pairing time.\n */\n name: string\n }\n}\n\n/**\n * Identifies the CLI to Studio's device authorization endpoint. A label, not a secret: what\n * authorizes a pairing is a signed-in person approving the code in the browser.\n */\nconst CLIENT_ID = 'kubb-cli'\n\ntype StartPairingOptions = {\n studioUrl?: string\n /**\n * Display name for the agent, usually the project or machine name.\n */\n name: string\n hostname: string\n /**\n * Which client is pairing. Defaults to the CLI, where any signed-in member may approve their own\n * machine. The Docker image passes `kubb-agent`, whose codes only an admin can approve.\n */\n clientId?: string\n /**\n * What a `kubb-agent` pairing asks to be registered as. Studio rejects the request without it,\n * and ignores it for the CLI.\n */\n agentKind?: 'user' | 'sandbox'\n}\n\n/**\n * Asks Studio for a pairing code. The machine token travels with the request and is stored against\n * the code, so approval knows which machine it is pairing: the same machine pairing twice rotates\n * one agent's token instead of creating a second agent.\n */\nexport async function startPairing({\n studioUrl = agentDefaults.studioUrl,\n name,\n hostname,\n clientId = CLIENT_ID,\n agentKind,\n}: StartPairingOptions): Promise<PairingSession> {\n return ofetch<PairingSession>(`${studioUrl}/api/auth/device/code`, {\n method: 'POST',\n body: {\n client_id: clientId,\n name,\n hostname,\n machine_token: await getMachineToken(),\n agent_kind: agentKind,\n },\n })\n}\n\ntype PollOptions = {\n studioUrl?: string\n session: PairingSession\n}\n\ntype PollError = 'authorization_pending' | 'slow_down' | 'expired_token' | 'access_denied' | 'invalid_grant'\n\ntype PollResponse =\n | PairingResult\n | {\n error: PollError | string\n /**\n * Why, when Studio has something more useful to say than the RFC code. An approval that hits\n * the organization's agent limit comes back as `access_denied` with the limit spelled out.\n */\n error_description?: string\n }\n\nfunction isPairingResult(response: PollResponse | undefined): response is PairingResult {\n return !!response && typeof response === 'object' && 'token' in response && typeof response.token === 'string'\n}\n\n/**\n * Polls until the user approves or denies, honoring the server's `slow_down` back-off. A poll that\n * cannot reach Studio is warned about and retried, since the code stays valid either way.\n *\n * Studio's own endpoint is used rather than the auth layer's `/device/token`, because an approved\n * Kubb pairing is worth an agent bearer token, not a user session.\n *\n * @throws when the code expires, the user denies it, or Studio returns an unexpected error.\n */\nexport async function pollForPairingToken({ studioUrl = agentDefaults.studioUrl, session }: PollOptions): Promise<PairingResult> {\n // Both fields cross the network, so neither is trusted as-is: a missing or zero `interval` would\n // spin the poll loop, and a missing or zero `expires_in` would expire the code before the first\n // poll. `> 0` is also false for `NaN` and for a missing field, so it doubles as the type guard.\n const deadline = Date.now() + (session.expires_in > 0 ? session.expires_in : 600) * 1000\n let intervalMs = (session.interval > 0 ? session.interval : 5) * 1000\n\n while (Date.now() < deadline) {\n await delay(intervalMs)\n\n let response: PollResponse | undefined\n try {\n response = await ofetch<PollResponse | undefined>(`${studioUrl}/api/agent/token`, {\n method: 'POST',\n body: { device_code: session.device_code },\n // A denial, an expiry, and \"not yet\" all come back as 4xx with a body the caller needs to\n // read, so let every response through and switch on `error` instead of catching.\n ignoreResponseError: true,\n })\n } catch (error) {\n // Studio can go briefly unreachable (a deploy, a dropped connection) during the minutes the\n // user has to approve in the browser. One failed poll should not end a pairing whose code is\n // still valid, so warn and try again on the next tick, the way `registerAgent` retries.\n console.warn(styleText('yellow', `Could not reach Kubb Studio while waiting for approval, retrying: ${getErrorMessage(error)}`))\n continue\n }\n\n if (isPairingResult(response)) {\n return response\n }\n\n if (!response || typeof response !== 'object' || !('error' in response) || typeof response.error !== 'string') {\n throw new Error('Kubb Studio returned an empty pairing response, pair again')\n }\n\n if (response.error === 'authorization_pending') {\n continue\n }\n\n if (response.error === 'slow_down') {\n intervalMs += 5_000\n continue\n }\n\n if (response.error === 'access_denied') {\n throw new Error(response.error_description ?? 'Pairing was denied in the browser')\n }\n\n if (response.error === 'expired_token' || response.error === 'invalid_grant') {\n throw new Error(response.error_description ?? 'The pairing code expired, pair again')\n }\n\n throw new Error(response.error_description ?? `Pairing failed (${response.error})`)\n }\n\n throw new Error('The pairing code expired, pair again')\n}\n"],"mappings":"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"}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
2
|
+
require("./rolldown-runtime-qbf5tadS.cjs");
|
|
3
|
+
//#region src/protocol/index.ts
|
|
4
|
+
/**
|
|
5
|
+
* The command names, for a host that needs the list rather than the union.
|
|
6
|
+
*/
|
|
7
|
+
const commandTypes = [
|
|
8
|
+
"studio:generate",
|
|
9
|
+
"studio:connect",
|
|
10
|
+
"studio:save"
|
|
11
|
+
];
|
|
12
|
+
function isCommandMessage(msg) {
|
|
13
|
+
return commandTypes.includes(msg.type);
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Type guard to narrow a data message to a specific event type.
|
|
17
|
+
*
|
|
18
|
+
* @example
|
|
19
|
+
* ```ts
|
|
20
|
+
* if (isDataMessage(msg, 'kubb:plugin:start')) {
|
|
21
|
+
* // msg.payload.data is now typed as [ctx: { plugin: { name: string } }]
|
|
22
|
+
* const pluginName = msg.payload.data[0].plugin.name
|
|
23
|
+
* }
|
|
24
|
+
* ```
|
|
25
|
+
*/
|
|
26
|
+
function isDataMessage(msg, type) {
|
|
27
|
+
return msg.type === "agent:data" && (type ? msg.payload.type === type : true);
|
|
28
|
+
}
|
|
29
|
+
function isStudioPingMessage(msg) {
|
|
30
|
+
return msg.type === "studio:ping";
|
|
31
|
+
}
|
|
32
|
+
function isDisconnectMessage(msg) {
|
|
33
|
+
return msg.type === "studio:disconnect";
|
|
34
|
+
}
|
|
35
|
+
//#endregion
|
|
36
|
+
exports.commandTypes = commandTypes;
|
|
37
|
+
exports.isCommandMessage = isCommandMessage;
|
|
38
|
+
exports.isDataMessage = isDataMessage;
|
|
39
|
+
exports.isDisconnectMessage = isDisconnectMessage;
|
|
40
|
+
exports.isStudioPingMessage = isStudioPingMessage;
|
|
41
|
+
|
|
42
|
+
//# sourceMappingURL=protocol.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"protocol.cjs","names":[],"sources":["../src/protocol/index.ts"],"sourcesContent":["/**\n * WebSocket message types for the agent ↔ Studio protocol. Every message name carries the side that\n * sent it, so direction reads off the name instead of the verb's tense:\n *\n * - Studio → agent: `studio:generate`, `studio:connect`, `studio:save`, `studio:ping`,\n * `studio:disconnect`, `studio:error`\n * - Agent → Studio: `agent:connect`, `agent:save`, `agent:data`, `agent:ping`\n *\n * `kubb:` stays reserved for generation lifecycle, so the {@link KubbHooks} events relayed inside an\n * `agent:data` payload keep their own names. The envelope says who sent it, the payload says what\n * happened.\n */\n\nimport type { Config } from '@kubb/core'\n\n/**\n * JSON-serializable Kubb config exchanged over the WebSocket. A live `kubb/kit` config holds\n * functions and class instances that cannot survive JSON, so both sides pass this flattened shape\n * and rebuild the real config from it.\n */\nexport type JSONKubbConfig = {\n /**\n * Plugins with their serialized options. `name` is the package name (e.g. `@kubb/plugin-ts`)\n * and `options` is an opaque blob the agent forwards unchanged to the plugin factory. An entry\n * with `disabled: true` is dropped even when the disk config's `plugins` array still lists it.\n */\n plugins?: Array<{\n name: string\n options?: object\n disabled?: boolean\n }>\n /**\n * Raw OpenAPI / Swagger spec content (YAML or JSON string).\n * Always honored for a 'sandbox' agent. For a non-sandbox agent it is honored only when the\n * agent opts in with `KUBB_AGENT_ALLOW_INPUT`; otherwise the spec is read from disk and this is ignored.\n */\n input?: string\n /**\n * Adapter option overrides sent from Studio UI. Merged into the disk config's adapter options\n * and re-applied through the same adapter factory, since an adapter instance's functions\n * (`parse`, `getImports`, ...) can't survive JSON serialization over the WebSocket.\n */\n adapter?: object\n}\n\n/**\n * Which `defineConfig(...)` entry an edit targets, for a config file that exports an array.\n *\n * A number selects by position, a string matches the entry's `name`. Omitted targets the only\n * entry, or the first one when the file exports an array.\n */\nexport type ConfigRef = string | number\n\n/**\n * A value the agent can read out of a plugin option in `kubb.config.ts` and round-trip through JSON.\n */\nexport type OptionValue = string | number | boolean | null | Array<OptionValue> | { [key: string]: OptionValue }\n\n/**\n * One change to a plugin's options in the user's `kubb.config.ts`.\n *\n * `plugin` is the package name (`@kubb/plugin-ts`), the same identity used in {@link JSONKubbConfig}.\n * The agent applies these to the file with an AST patch, so only the targeted values are rewritten.\n *\n * Declared here rather than in `configFile.ts` because this is the wire contract, and the patcher\n * imports it from here. Type-only, so nothing pulls `magicast` into this entry point.\n */\nexport type ConfigEdit =\n /**\n * Write a literal option value. `path` walks nested objects, so `['enum', 'type']` targets\n * `pluginTs({ enum: { type } })`.\n */\n | { operation: 'set'; config?: ConfigRef; plugin: string; path: Array<string>; value: unknown }\n /**\n * Drop an option so the plugin falls back to its default.\n */\n | { operation: 'remove'; config?: ConfigRef; plugin: string; path: Array<string> }\n /**\n * Add a plugin factory call and its import to the `plugins` array.\n */\n | { operation: 'add-plugin'; config?: ConfigRef; plugin: string; importName?: string; options?: Record<string, unknown> }\n /**\n * Comment the plugin call out, keeping its options in the file so enabling it again restores them.\n */\n | { operation: 'disable-plugin'; config?: ConfigRef; plugin: string }\n /**\n * Uncomment a plugin call a previous `disable-plugin` commented out.\n */\n | { operation: 'enable-plugin'; config?: ConfigRef; plugin: string }\n\n/**\n * A plugin factory call the agent found in the `plugins` array of a `defineConfig(...)`.\n */\nexport type PluginView = {\n /**\n * Local identifier of the factory in the file, e.g. `pluginTs`. This is the alias when the plugin\n * was imported under one.\n */\n importName: string\n /**\n * Module the factory is imported from, e.g. `@kubb/plugin-ts`.\n */\n packageName: string\n /**\n * Top-level option keys, each flagged with whether the agent may write it and, when it can, the\n * value found in the file. An option marked `literal: false` holds a function or a reference the\n * agent will not overwrite, so Studio shows the control disabled rather than hiding it, and\n * `value` is absent since there is nothing safe to display as the current value.\n */\n options: Record<string, { literal: boolean; value?: OptionValue }>\n /**\n * Set when the plugin call is commented out in the file. Its options stay on disk but are not\n * readable, so `options` is empty until it is enabled again.\n */\n disabled?: true\n}\n\n/**\n * One `defineConfig(...)` entry. A config file that exports a single object has exactly one.\n */\nexport type ConfigView = {\n /**\n * The entry's `name`, when it sets one. Studio labels the config picker with it.\n */\n name?: string\n /**\n * Each plugin call in the entry, with its top-level option keys.\n */\n plugins: Array<PluginView>\n}\n\n/**\n * What the agent found in the user's config file, so Studio knows which controls it may offer.\n * Absent when the agent could not read the file at all.\n */\nexport type ConfigFileView =\n | {\n managed: true\n /**\n * One entry per config the file exports, in source order. Every {@link ConfigEdit} names\n * which of these it targets through its `config` field.\n */\n configs: Array<ConfigView>\n }\n | {\n managed: false\n /**\n * Why the file is outside what the agent edits, for example a default export that is not a\n * `defineConfig(...)` call. Studio shows this and offers no property-level controls.\n */\n reason: string\n }\n\n/**\n * Outcome of a single {@link ConfigEdit}, returned in a {@link ConfigSavedMessage}.\n */\nexport type ConfigEditOutcome = {\n edit: ConfigEdit\n applied: boolean\n /**\n * Why the edit was refused, absent when it was applied.\n */\n reason?: string\n}\n\n/**\n * Typed events sent by the Kubb agent to Studio over WebSocket.\n * Mirrors the single-context-object tuple style of {@link KubbHooks} in `kubb/kit`,\n * using JSON-serializable shapes (e.g. `sources` as a `Record` instead of `Map`,\n * `error` as `{ message; stack? }` instead of `Error`).\n */\nexport type KubbHooks = {\n 'kubb:plugin:start': [ctx: { plugin: { name: string } }]\n 'kubb:plugin:end': [ctx: { plugin: { name: string }; duration: number; success: boolean }]\n 'kubb:build:start': [ctx: { config: { name?: string }; adapter: { name: string } }]\n 'kubb:build:end': [ctx: { files: Array<{ path: string; name: string }>; outputDir: string }]\n 'kubb:files:processing:start': [ctx: { total: number }]\n 'kubb:files:processing:update': [\n ctx: {\n files: Array<{\n file: string\n processed: number\n total: number\n percentage: number\n }>\n },\n ]\n 'kubb:files:processing:end': [ctx: { total: number }]\n 'kubb:info': [ctx: { message: string; info?: string }]\n 'kubb:success': [ctx: { message: string; info?: string }]\n 'kubb:warn': [ctx: { message: string; info?: string }]\n 'kubb:error': [ctx: { message: string; stack?: string }]\n 'kubb:debug': [ctx: { logs: Array<string>; fileName?: string }]\n 'kubb:generation:start': [ctx: { name?: string; plugins: number }]\n 'kubb:generation:end': [ctx: { config: Config; storage: Record<string, string> }]\n 'kubb:generation:summary': [ctx: { duration: number; fileCount: number; failedPlugins: number; status: 'success' | 'failed' }]\n 'kubb:lifecycle:start': []\n 'kubb:lifecycle:end': []\n 'kubb:format:start': []\n 'kubb:format:end': []\n 'kubb:lint:start': []\n 'kubb:lint:end': []\n 'kubb:hooks:start': []\n 'kubb:hooks:end': []\n 'kubb:hook:start': [ctx: { id?: string; command: string; args?: Array<string> }]\n 'kubb:hook:line': [ctx: { id: string; line: string }]\n 'kubb:hook:end': [\n ctx: {\n id?: string\n command: string\n args?: Array<string>\n success: boolean\n error?: { message: string; stack?: string }\n },\n ]\n}\n\nexport type KubbHook = keyof KubbHooks\n\n/**\n * Run a generation with the given config. `payload` is the merged config Studio wants generated.\n */\nexport type StudioGenerateMessage = {\n type: 'studio:generate'\n payload: JSONKubbConfig\n}\n\n/**\n * Ask the agent to send a fresh `agent:connect` payload. Permissions are fixed when the host starts\n * the agent; this message only triggers another read of disk config and saved Studio state.\n */\nexport type StudioConnectMessage = {\n type: 'studio:connect'\n /**\n * Version of the Studio instance asking, which refreshes what the agent picked up when the\n * session was created. Absent when Studio predates the field.\n */\n version?: string\n}\n\n/**\n * Change plugin options in the user's `kubb.config.ts`. Applied only when the agent was granted\n * `allowConfigEdit`; otherwise every edit comes back refused.\n */\nexport type StudioSaveMessage = {\n type: 'studio:save'\n edits: Array<ConfigEdit>\n}\n\n/**\n * Anything Studio asks the agent to do. Each command is its own `type`, so a handler switches once\n * instead of reading a `type` and then a nested `command` field.\n */\nexport type CommandMessage = StudioGenerateMessage | StudioConnectMessage | StudioSaveMessage\n\n/**\n * The command names, for a host that needs the list rather than the union.\n */\nexport const commandTypes = ['studio:generate', 'studio:connect', 'studio:save'] as const\n\n/**\n * Identifies the host running the Kubb runtime. Local to the runtime rather than part of the wire:\n * it picks which remedy a refused-input warning suggests, since the Docker agent and the CLI grant\n * `allowInput` different ways.\n */\nexport type ClientInfo = {\n /**\n * `cli` for a `kubb studio` connection from a developer's machine, `docker` for the agent image.\n */\n kind: 'cli' | 'docker'\n}\n\n/**\n * Payload of the `agent:connect` handshake, sent when the agent attaches to a session. Carries only\n * what Studio renders, with everything about the config under one key.\n */\nexport type ConnectMessagePayload = {\n /**\n * Always sent, so a mismatch is visible on both sides: Studio badges the connection with these\n * and the host prints them.\n */\n versions: {\n /**\n * The version of the `@kubb/studio` runtime the agent runs.\n */\n kubb: string\n /**\n * The version of the host itself (the `kubb.agent` package or the `kubb` CLI).\n */\n agent: string\n }\n /**\n * The agent's project root (`KUBB_AGENT_ROOT`, or the working directory when unset). This is the\n * workspace that generation runs against.\n */\n root: string\n /**\n * The baseline every generation starts from.\n */\n config: {\n /**\n * The config path as configured (`KUBB_AGENT_CONFIG`), relative to `root` unless absolute.\n */\n path: string\n /**\n * What the agent read out of the config file itself, so Studio can render the plugin editor\n * against the real file. Absent when the agent could not read it, or was not granted\n * `allowConfigEdit`.\n */\n file?: ConfigFileView\n /**\n * Plugins the config registers, with their serialized options.\n */\n plugins?: Array<{\n name: string\n options?: object\n }>\n }\n permissions: {\n /**\n * Whether the agent writes generated files to disk. False for a sandbox agent. For a local\n * agent it mirrors the agent's `KUBB_AGENT_ALLOW_WRITE`.\n */\n allowWrite: boolean\n /**\n * Whether the agent will accept and generate from an OpenAPI spec supplied by Studio.\n * Always true for a sandbox agent; otherwise it mirrors the agent's own opt-in. Studio reads\n * this to decide whether to send `input`.\n */\n allowInput: boolean\n /**\n * Whether the agent runs the formatter, the linter, and `output.postGenerate` as child\n * processes after a generation. Always true for the Docker agent, where the image bounds what\n * can run. The CLI runs in the user's own project and defaults it off.\n */\n allowExec: boolean\n /**\n * Whether the agent may change plugin options in the user's `kubb.config.ts`. Separate from\n * `allowWrite`, which covers generated output: this one edits a hand-authored source file.\n */\n allowConfigEdit: boolean\n }\n}\n\n/**\n * Agent → Studio handshake. Sent when the WebSocket opens and again after a `connect` command.\n * Carries the on-disk config baseline, granted permissions, and paths Studio needs to render the editor.\n */\nexport type AgentConnectMessage = {\n type: 'agent:connect'\n payload: ConnectMessagePayload\n}\n\n/**\n * Reply to a `save` command: what the agent did to the file on disk.\n */\nexport type AgentSaveMessage = {\n type: 'agent:save'\n payload: {\n /**\n * Per-edit result, in the order the edits were sent.\n */\n outcomes: Array<ConfigEditOutcome>\n /**\n * Whether the file on disk changed. False when every edit was refused, and when the applied\n * edits produced the text the file already had.\n */\n changed: boolean\n /**\n * The config file as it now stands, so Studio can re-render without a round trip. Absent when\n * nothing was written. Named to match `config.file` in the connect payload.\n */\n file?: ConfigFileView\n }\n}\n\n/**\n * Failure notice from Studio for something that breaks outside a generation, such as a malformed\n * command. The agent's own failures travel as an `agent:data` message carrying a `kubb:error`\n * payload, which keeps them ordered against the generation events around them.\n */\nexport type StudioErrorMessage = {\n type: 'studio:error'\n message: string\n}\n\n/**\n * Heartbeat sent by the Agent to Studio so the connection is not treated as idle.\n */\nexport type AgentPingMessage = {\n type: 'agent:ping'\n}\n\n/**\n * Studio's reply to an `agent:ping`, confirming the connection is still alive.\n */\nexport type StudioPingMessage = {\n type: 'studio:ping'\n}\n\n/**\n * Disconnect message sent from Studio to Agent when the session is expired or revoked.\n * The agent should close the connection without reconnecting.\n */\nexport type StudioDisconnectMessage = {\n type: 'studio:disconnect'\n reason: 'expired' | 'revoked'\n}\n\n/**\n * The agent going away, so Studio marks the session offline instead of waiting out the heartbeat\n * window. The mirror of {@link StudioDisconnectMessage}.\n *\n * Only sent for a shutdown. An expired or revoked session was Studio's own decision, so echoing it\n * back says nothing new.\n */\nexport type AgentDisconnectMessage = {\n type: 'agent:disconnect'\n reason: 'shutdown'\n}\n\n/**\n * Payload of an `agent:data` message: a single Kubb generation event forwarded to Studio in real time.\n * Generic over the hook name so `data` is typed to that hook's context tuple.\n */\nexport type DataMessagePayload<T extends KubbHook = KubbHook> = {\n /**\n * The Kubb hook this event is for (e.g. `kubb:plugin:start`).\n */\n type: T\n /**\n * The hook's context tuple, matching `KubbHooks[type]`.\n */\n data: KubbHooks[T]\n /**\n * When the agent emitted the event, epoch milliseconds.\n */\n timestamp: number\n /**\n * Monotonic per-connection counter stamped in the order the agent emits events. Studio orders the\n * event log by this, since `timestamp` has millisecond resolution and a full generation fires\n * dozens of events per tick, and the relay can deliver them out of order.\n */\n seq: number\n}\n\n/**\n * Envelope for a single generation event streamed from Agent to Studio. Wraps a\n * {@link DataMessagePayload} so both sides can switch on `type: 'agent:data'`.\n */\nexport type DataMessage<T extends KubbHook = KubbHook> = {\n type: 'agent:data'\n payload: DataMessagePayload<T>\n}\n\n/**\n * Response returned by the Studio `/api/agent/sessions` endpoint.\n */\nexport type AgentConnectResponse = {\n /**\n * WebSocket URL the agent opens to reach the session, with the session token embedded.\n */\n wsUrl: string\n /**\n * When the session expires and the wsUrl stops working (ISO 8601).\n */\n expiresAt: string\n /**\n * When the session was revoked (ISO 8601), or null while it is still valid.\n */\n revokedAt: string | null\n /**\n * Opaque session token, also embedded in `wsUrl`. Store it to revoke the session later.\n */\n sessionId: string\n /**\n * Short readable identifier for this connection, used in logs (e.g. brave-otter).\n */\n slug: string | null\n /**\n * Whether this session belongs to a shared sandbox agent rather than an owned one.\n */\n isSandbox: boolean\n /**\n * The Studio instance's own version. Reported here rather than only on `studio:connect`, so the\n * agent knows it before it announces itself and can name both sides from the first connect.\n * Absent when Studio predates the field.\n */\n version?: string\n}\n\n/**\n * Every message that can cross the agent WebSocket, in either direction. Narrow it with the\n * `is*Message` guards below before reading a variant's fields.\n */\nexport type AgentMessage =\n | CommandMessage\n | DataMessage\n | AgentConnectMessage\n | AgentSaveMessage\n | AgentPingMessage\n | AgentDisconnectMessage\n | StudioErrorMessage\n | StudioPingMessage\n | StudioDisconnectMessage\n\nexport function isCommandMessage(msg: AgentMessage): msg is CommandMessage {\n return (commandTypes as ReadonlyArray<string>).includes(msg.type)\n}\n\n/**\n * Type guard to narrow a data message to a specific event type.\n *\n * @example\n * ```ts\n * if (isDataMessage(msg, 'kubb:plugin:start')) {\n * // msg.payload.data is now typed as [ctx: { plugin: { name: string } }]\n * const pluginName = msg.payload.data[0].plugin.name\n * }\n * ```\n */\nexport function isDataMessage<T extends KubbHook>(msg: AgentMessage, type?: T): msg is DataMessage<T> {\n return msg.type === 'agent:data' && (type ? msg.payload.type === type : true)\n}\n\nexport function isStudioPingMessage(msg: AgentMessage): msg is StudioPingMessage {\n return msg.type === 'studio:ping'\n}\n\nexport function isDisconnectMessage(msg: AgentMessage): msg is StudioDisconnectMessage {\n return msg.type === 'studio:disconnect'\n}\n"],"mappings":";;;;;;AAkQA,MAAa,eAAe;CAAC;CAAmB;CAAkB;AAAa;AAwP/E,SAAgB,iBAAiB,KAA0C;CACzE,OAAQ,aAAuC,SAAS,IAAI,IAAI;AAClE;;;;;;;;;;;;AAaA,SAAgB,cAAkC,KAAmB,MAAiC;CACpG,OAAO,IAAI,SAAS,iBAAiB,OAAO,IAAI,QAAQ,SAAS,OAAO;AAC1E;AAEA,SAAgB,oBAAoB,KAA6C;CAC/E,OAAO,IAAI,SAAS;AACtB;AAEA,SAAgB,oBAAoB,KAAmD;CACrF,OAAO,IAAI,SAAS;AACtB"}
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
import { A as isDataMessage, C as StudioDisconnectMessage, D as StudioSaveMessage, E as StudioPingMessage, M as isStudioPingMessage, O as commandTypes, S as StudioConnectMessage, T as StudioGenerateMessage, _ as JSONKubbConfig, a as AgentPingMessage, b as OptionValue, c as CommandMessage, d as ConfigFileView, f as ConfigRef, g as DataMessagePayload, h as DataMessage, i as AgentMessage, j as isDisconnectMessage, k as isCommandMessage, l as ConfigEdit, m as ConnectMessagePayload, n as AgentConnectResponse, o as AgentSaveMessage, p as ConfigView, r as AgentDisconnectMessage, s as ClientInfo, t as AgentConnectMessage, u as ConfigEditOutcome, v as KubbHook, w as StudioErrorMessage, x as PluginView, y as KubbHooks } from "./index-BVn89Nw2.js";
|
|
2
|
+
export { AgentConnectMessage, AgentConnectResponse, AgentDisconnectMessage, AgentMessage, AgentPingMessage, AgentSaveMessage, ClientInfo, CommandMessage, ConfigEdit, ConfigEditOutcome, ConfigFileView, ConfigRef, ConfigView, ConnectMessagePayload, DataMessage, DataMessagePayload, JSONKubbConfig, KubbHook, KubbHooks, OptionValue, PluginView, StudioConnectMessage, StudioDisconnectMessage, StudioErrorMessage, StudioGenerateMessage, StudioPingMessage, StudioSaveMessage, commandTypes, isCommandMessage, isDataMessage, isDisconnectMessage, isStudioPingMessage };
|
package/dist/protocol.js
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import "./rolldown-runtime-CRm0XQPb.js";
|
|
2
|
+
//#region src/protocol/index.ts
|
|
3
|
+
/**
|
|
4
|
+
* The command names, for a host that needs the list rather than the union.
|
|
5
|
+
*/
|
|
6
|
+
const commandTypes = [
|
|
7
|
+
"studio:generate",
|
|
8
|
+
"studio:connect",
|
|
9
|
+
"studio:save"
|
|
10
|
+
];
|
|
11
|
+
function isCommandMessage(msg) {
|
|
12
|
+
return commandTypes.includes(msg.type);
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Type guard to narrow a data message to a specific event type.
|
|
16
|
+
*
|
|
17
|
+
* @example
|
|
18
|
+
* ```ts
|
|
19
|
+
* if (isDataMessage(msg, 'kubb:plugin:start')) {
|
|
20
|
+
* // msg.payload.data is now typed as [ctx: { plugin: { name: string } }]
|
|
21
|
+
* const pluginName = msg.payload.data[0].plugin.name
|
|
22
|
+
* }
|
|
23
|
+
* ```
|
|
24
|
+
*/
|
|
25
|
+
function isDataMessage(msg, type) {
|
|
26
|
+
return msg.type === "agent:data" && (type ? msg.payload.type === type : true);
|
|
27
|
+
}
|
|
28
|
+
function isStudioPingMessage(msg) {
|
|
29
|
+
return msg.type === "studio:ping";
|
|
30
|
+
}
|
|
31
|
+
function isDisconnectMessage(msg) {
|
|
32
|
+
return msg.type === "studio:disconnect";
|
|
33
|
+
}
|
|
34
|
+
//#endregion
|
|
35
|
+
export { commandTypes, isCommandMessage, isDataMessage, isDisconnectMessage, isStudioPingMessage };
|
|
36
|
+
|
|
37
|
+
//# sourceMappingURL=protocol.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"protocol.js","names":[],"sources":["../src/protocol/index.ts"],"sourcesContent":["/**\n * WebSocket message types for the agent ↔ Studio protocol. Every message name carries the side that\n * sent it, so direction reads off the name instead of the verb's tense:\n *\n * - Studio → agent: `studio:generate`, `studio:connect`, `studio:save`, `studio:ping`,\n * `studio:disconnect`, `studio:error`\n * - Agent → Studio: `agent:connect`, `agent:save`, `agent:data`, `agent:ping`\n *\n * `kubb:` stays reserved for generation lifecycle, so the {@link KubbHooks} events relayed inside an\n * `agent:data` payload keep their own names. The envelope says who sent it, the payload says what\n * happened.\n */\n\nimport type { Config } from '@kubb/core'\n\n/**\n * JSON-serializable Kubb config exchanged over the WebSocket. A live `kubb/kit` config holds\n * functions and class instances that cannot survive JSON, so both sides pass this flattened shape\n * and rebuild the real config from it.\n */\nexport type JSONKubbConfig = {\n /**\n * Plugins with their serialized options. `name` is the package name (e.g. `@kubb/plugin-ts`)\n * and `options` is an opaque blob the agent forwards unchanged to the plugin factory. An entry\n * with `disabled: true` is dropped even when the disk config's `plugins` array still lists it.\n */\n plugins?: Array<{\n name: string\n options?: object\n disabled?: boolean\n }>\n /**\n * Raw OpenAPI / Swagger spec content (YAML or JSON string).\n * Always honored for a 'sandbox' agent. For a non-sandbox agent it is honored only when the\n * agent opts in with `KUBB_AGENT_ALLOW_INPUT`; otherwise the spec is read from disk and this is ignored.\n */\n input?: string\n /**\n * Adapter option overrides sent from Studio UI. Merged into the disk config's adapter options\n * and re-applied through the same adapter factory, since an adapter instance's functions\n * (`parse`, `getImports`, ...) can't survive JSON serialization over the WebSocket.\n */\n adapter?: object\n}\n\n/**\n * Which `defineConfig(...)` entry an edit targets, for a config file that exports an array.\n *\n * A number selects by position, a string matches the entry's `name`. Omitted targets the only\n * entry, or the first one when the file exports an array.\n */\nexport type ConfigRef = string | number\n\n/**\n * A value the agent can read out of a plugin option in `kubb.config.ts` and round-trip through JSON.\n */\nexport type OptionValue = string | number | boolean | null | Array<OptionValue> | { [key: string]: OptionValue }\n\n/**\n * One change to a plugin's options in the user's `kubb.config.ts`.\n *\n * `plugin` is the package name (`@kubb/plugin-ts`), the same identity used in {@link JSONKubbConfig}.\n * The agent applies these to the file with an AST patch, so only the targeted values are rewritten.\n *\n * Declared here rather than in `configFile.ts` because this is the wire contract, and the patcher\n * imports it from here. Type-only, so nothing pulls `magicast` into this entry point.\n */\nexport type ConfigEdit =\n /**\n * Write a literal option value. `path` walks nested objects, so `['enum', 'type']` targets\n * `pluginTs({ enum: { type } })`.\n */\n | { operation: 'set'; config?: ConfigRef; plugin: string; path: Array<string>; value: unknown }\n /**\n * Drop an option so the plugin falls back to its default.\n */\n | { operation: 'remove'; config?: ConfigRef; plugin: string; path: Array<string> }\n /**\n * Add a plugin factory call and its import to the `plugins` array.\n */\n | { operation: 'add-plugin'; config?: ConfigRef; plugin: string; importName?: string; options?: Record<string, unknown> }\n /**\n * Comment the plugin call out, keeping its options in the file so enabling it again restores them.\n */\n | { operation: 'disable-plugin'; config?: ConfigRef; plugin: string }\n /**\n * Uncomment a plugin call a previous `disable-plugin` commented out.\n */\n | { operation: 'enable-plugin'; config?: ConfigRef; plugin: string }\n\n/**\n * A plugin factory call the agent found in the `plugins` array of a `defineConfig(...)`.\n */\nexport type PluginView = {\n /**\n * Local identifier of the factory in the file, e.g. `pluginTs`. This is the alias when the plugin\n * was imported under one.\n */\n importName: string\n /**\n * Module the factory is imported from, e.g. `@kubb/plugin-ts`.\n */\n packageName: string\n /**\n * Top-level option keys, each flagged with whether the agent may write it and, when it can, the\n * value found in the file. An option marked `literal: false` holds a function or a reference the\n * agent will not overwrite, so Studio shows the control disabled rather than hiding it, and\n * `value` is absent since there is nothing safe to display as the current value.\n */\n options: Record<string, { literal: boolean; value?: OptionValue }>\n /**\n * Set when the plugin call is commented out in the file. Its options stay on disk but are not\n * readable, so `options` is empty until it is enabled again.\n */\n disabled?: true\n}\n\n/**\n * One `defineConfig(...)` entry. A config file that exports a single object has exactly one.\n */\nexport type ConfigView = {\n /**\n * The entry's `name`, when it sets one. Studio labels the config picker with it.\n */\n name?: string\n /**\n * Each plugin call in the entry, with its top-level option keys.\n */\n plugins: Array<PluginView>\n}\n\n/**\n * What the agent found in the user's config file, so Studio knows which controls it may offer.\n * Absent when the agent could not read the file at all.\n */\nexport type ConfigFileView =\n | {\n managed: true\n /**\n * One entry per config the file exports, in source order. Every {@link ConfigEdit} names\n * which of these it targets through its `config` field.\n */\n configs: Array<ConfigView>\n }\n | {\n managed: false\n /**\n * Why the file is outside what the agent edits, for example a default export that is not a\n * `defineConfig(...)` call. Studio shows this and offers no property-level controls.\n */\n reason: string\n }\n\n/**\n * Outcome of a single {@link ConfigEdit}, returned in a {@link ConfigSavedMessage}.\n */\nexport type ConfigEditOutcome = {\n edit: ConfigEdit\n applied: boolean\n /**\n * Why the edit was refused, absent when it was applied.\n */\n reason?: string\n}\n\n/**\n * Typed events sent by the Kubb agent to Studio over WebSocket.\n * Mirrors the single-context-object tuple style of {@link KubbHooks} in `kubb/kit`,\n * using JSON-serializable shapes (e.g. `sources` as a `Record` instead of `Map`,\n * `error` as `{ message; stack? }` instead of `Error`).\n */\nexport type KubbHooks = {\n 'kubb:plugin:start': [ctx: { plugin: { name: string } }]\n 'kubb:plugin:end': [ctx: { plugin: { name: string }; duration: number; success: boolean }]\n 'kubb:build:start': [ctx: { config: { name?: string }; adapter: { name: string } }]\n 'kubb:build:end': [ctx: { files: Array<{ path: string; name: string }>; outputDir: string }]\n 'kubb:files:processing:start': [ctx: { total: number }]\n 'kubb:files:processing:update': [\n ctx: {\n files: Array<{\n file: string\n processed: number\n total: number\n percentage: number\n }>\n },\n ]\n 'kubb:files:processing:end': [ctx: { total: number }]\n 'kubb:info': [ctx: { message: string; info?: string }]\n 'kubb:success': [ctx: { message: string; info?: string }]\n 'kubb:warn': [ctx: { message: string; info?: string }]\n 'kubb:error': [ctx: { message: string; stack?: string }]\n 'kubb:debug': [ctx: { logs: Array<string>; fileName?: string }]\n 'kubb:generation:start': [ctx: { name?: string; plugins: number }]\n 'kubb:generation:end': [ctx: { config: Config; storage: Record<string, string> }]\n 'kubb:generation:summary': [ctx: { duration: number; fileCount: number; failedPlugins: number; status: 'success' | 'failed' }]\n 'kubb:lifecycle:start': []\n 'kubb:lifecycle:end': []\n 'kubb:format:start': []\n 'kubb:format:end': []\n 'kubb:lint:start': []\n 'kubb:lint:end': []\n 'kubb:hooks:start': []\n 'kubb:hooks:end': []\n 'kubb:hook:start': [ctx: { id?: string; command: string; args?: Array<string> }]\n 'kubb:hook:line': [ctx: { id: string; line: string }]\n 'kubb:hook:end': [\n ctx: {\n id?: string\n command: string\n args?: Array<string>\n success: boolean\n error?: { message: string; stack?: string }\n },\n ]\n}\n\nexport type KubbHook = keyof KubbHooks\n\n/**\n * Run a generation with the given config. `payload` is the merged config Studio wants generated.\n */\nexport type StudioGenerateMessage = {\n type: 'studio:generate'\n payload: JSONKubbConfig\n}\n\n/**\n * Ask the agent to send a fresh `agent:connect` payload. Permissions are fixed when the host starts\n * the agent; this message only triggers another read of disk config and saved Studio state.\n */\nexport type StudioConnectMessage = {\n type: 'studio:connect'\n /**\n * Version of the Studio instance asking, which refreshes what the agent picked up when the\n * session was created. Absent when Studio predates the field.\n */\n version?: string\n}\n\n/**\n * Change plugin options in the user's `kubb.config.ts`. Applied only when the agent was granted\n * `allowConfigEdit`; otherwise every edit comes back refused.\n */\nexport type StudioSaveMessage = {\n type: 'studio:save'\n edits: Array<ConfigEdit>\n}\n\n/**\n * Anything Studio asks the agent to do. Each command is its own `type`, so a handler switches once\n * instead of reading a `type` and then a nested `command` field.\n */\nexport type CommandMessage = StudioGenerateMessage | StudioConnectMessage | StudioSaveMessage\n\n/**\n * The command names, for a host that needs the list rather than the union.\n */\nexport const commandTypes = ['studio:generate', 'studio:connect', 'studio:save'] as const\n\n/**\n * Identifies the host running the Kubb runtime. Local to the runtime rather than part of the wire:\n * it picks which remedy a refused-input warning suggests, since the Docker agent and the CLI grant\n * `allowInput` different ways.\n */\nexport type ClientInfo = {\n /**\n * `cli` for a `kubb studio` connection from a developer's machine, `docker` for the agent image.\n */\n kind: 'cli' | 'docker'\n}\n\n/**\n * Payload of the `agent:connect` handshake, sent when the agent attaches to a session. Carries only\n * what Studio renders, with everything about the config under one key.\n */\nexport type ConnectMessagePayload = {\n /**\n * Always sent, so a mismatch is visible on both sides: Studio badges the connection with these\n * and the host prints them.\n */\n versions: {\n /**\n * The version of the `@kubb/studio` runtime the agent runs.\n */\n kubb: string\n /**\n * The version of the host itself (the `kubb.agent` package or the `kubb` CLI).\n */\n agent: string\n }\n /**\n * The agent's project root (`KUBB_AGENT_ROOT`, or the working directory when unset). This is the\n * workspace that generation runs against.\n */\n root: string\n /**\n * The baseline every generation starts from.\n */\n config: {\n /**\n * The config path as configured (`KUBB_AGENT_CONFIG`), relative to `root` unless absolute.\n */\n path: string\n /**\n * What the agent read out of the config file itself, so Studio can render the plugin editor\n * against the real file. Absent when the agent could not read it, or was not granted\n * `allowConfigEdit`.\n */\n file?: ConfigFileView\n /**\n * Plugins the config registers, with their serialized options.\n */\n plugins?: Array<{\n name: string\n options?: object\n }>\n }\n permissions: {\n /**\n * Whether the agent writes generated files to disk. False for a sandbox agent. For a local\n * agent it mirrors the agent's `KUBB_AGENT_ALLOW_WRITE`.\n */\n allowWrite: boolean\n /**\n * Whether the agent will accept and generate from an OpenAPI spec supplied by Studio.\n * Always true for a sandbox agent; otherwise it mirrors the agent's own opt-in. Studio reads\n * this to decide whether to send `input`.\n */\n allowInput: boolean\n /**\n * Whether the agent runs the formatter, the linter, and `output.postGenerate` as child\n * processes after a generation. Always true for the Docker agent, where the image bounds what\n * can run. The CLI runs in the user's own project and defaults it off.\n */\n allowExec: boolean\n /**\n * Whether the agent may change plugin options in the user's `kubb.config.ts`. Separate from\n * `allowWrite`, which covers generated output: this one edits a hand-authored source file.\n */\n allowConfigEdit: boolean\n }\n}\n\n/**\n * Agent → Studio handshake. Sent when the WebSocket opens and again after a `connect` command.\n * Carries the on-disk config baseline, granted permissions, and paths Studio needs to render the editor.\n */\nexport type AgentConnectMessage = {\n type: 'agent:connect'\n payload: ConnectMessagePayload\n}\n\n/**\n * Reply to a `save` command: what the agent did to the file on disk.\n */\nexport type AgentSaveMessage = {\n type: 'agent:save'\n payload: {\n /**\n * Per-edit result, in the order the edits were sent.\n */\n outcomes: Array<ConfigEditOutcome>\n /**\n * Whether the file on disk changed. False when every edit was refused, and when the applied\n * edits produced the text the file already had.\n */\n changed: boolean\n /**\n * The config file as it now stands, so Studio can re-render without a round trip. Absent when\n * nothing was written. Named to match `config.file` in the connect payload.\n */\n file?: ConfigFileView\n }\n}\n\n/**\n * Failure notice from Studio for something that breaks outside a generation, such as a malformed\n * command. The agent's own failures travel as an `agent:data` message carrying a `kubb:error`\n * payload, which keeps them ordered against the generation events around them.\n */\nexport type StudioErrorMessage = {\n type: 'studio:error'\n message: string\n}\n\n/**\n * Heartbeat sent by the Agent to Studio so the connection is not treated as idle.\n */\nexport type AgentPingMessage = {\n type: 'agent:ping'\n}\n\n/**\n * Studio's reply to an `agent:ping`, confirming the connection is still alive.\n */\nexport type StudioPingMessage = {\n type: 'studio:ping'\n}\n\n/**\n * Disconnect message sent from Studio to Agent when the session is expired or revoked.\n * The agent should close the connection without reconnecting.\n */\nexport type StudioDisconnectMessage = {\n type: 'studio:disconnect'\n reason: 'expired' | 'revoked'\n}\n\n/**\n * The agent going away, so Studio marks the session offline instead of waiting out the heartbeat\n * window. The mirror of {@link StudioDisconnectMessage}.\n *\n * Only sent for a shutdown. An expired or revoked session was Studio's own decision, so echoing it\n * back says nothing new.\n */\nexport type AgentDisconnectMessage = {\n type: 'agent:disconnect'\n reason: 'shutdown'\n}\n\n/**\n * Payload of an `agent:data` message: a single Kubb generation event forwarded to Studio in real time.\n * Generic over the hook name so `data` is typed to that hook's context tuple.\n */\nexport type DataMessagePayload<T extends KubbHook = KubbHook> = {\n /**\n * The Kubb hook this event is for (e.g. `kubb:plugin:start`).\n */\n type: T\n /**\n * The hook's context tuple, matching `KubbHooks[type]`.\n */\n data: KubbHooks[T]\n /**\n * When the agent emitted the event, epoch milliseconds.\n */\n timestamp: number\n /**\n * Monotonic per-connection counter stamped in the order the agent emits events. Studio orders the\n * event log by this, since `timestamp` has millisecond resolution and a full generation fires\n * dozens of events per tick, and the relay can deliver them out of order.\n */\n seq: number\n}\n\n/**\n * Envelope for a single generation event streamed from Agent to Studio. Wraps a\n * {@link DataMessagePayload} so both sides can switch on `type: 'agent:data'`.\n */\nexport type DataMessage<T extends KubbHook = KubbHook> = {\n type: 'agent:data'\n payload: DataMessagePayload<T>\n}\n\n/**\n * Response returned by the Studio `/api/agent/sessions` endpoint.\n */\nexport type AgentConnectResponse = {\n /**\n * WebSocket URL the agent opens to reach the session, with the session token embedded.\n */\n wsUrl: string\n /**\n * When the session expires and the wsUrl stops working (ISO 8601).\n */\n expiresAt: string\n /**\n * When the session was revoked (ISO 8601), or null while it is still valid.\n */\n revokedAt: string | null\n /**\n * Opaque session token, also embedded in `wsUrl`. Store it to revoke the session later.\n */\n sessionId: string\n /**\n * Short readable identifier for this connection, used in logs (e.g. brave-otter).\n */\n slug: string | null\n /**\n * Whether this session belongs to a shared sandbox agent rather than an owned one.\n */\n isSandbox: boolean\n /**\n * The Studio instance's own version. Reported here rather than only on `studio:connect`, so the\n * agent knows it before it announces itself and can name both sides from the first connect.\n * Absent when Studio predates the field.\n */\n version?: string\n}\n\n/**\n * Every message that can cross the agent WebSocket, in either direction. Narrow it with the\n * `is*Message` guards below before reading a variant's fields.\n */\nexport type AgentMessage =\n | CommandMessage\n | DataMessage\n | AgentConnectMessage\n | AgentSaveMessage\n | AgentPingMessage\n | AgentDisconnectMessage\n | StudioErrorMessage\n | StudioPingMessage\n | StudioDisconnectMessage\n\nexport function isCommandMessage(msg: AgentMessage): msg is CommandMessage {\n return (commandTypes as ReadonlyArray<string>).includes(msg.type)\n}\n\n/**\n * Type guard to narrow a data message to a specific event type.\n *\n * @example\n * ```ts\n * if (isDataMessage(msg, 'kubb:plugin:start')) {\n * // msg.payload.data is now typed as [ctx: { plugin: { name: string } }]\n * const pluginName = msg.payload.data[0].plugin.name\n * }\n * ```\n */\nexport function isDataMessage<T extends KubbHook>(msg: AgentMessage, type?: T): msg is DataMessage<T> {\n return msg.type === 'agent:data' && (type ? msg.payload.type === type : true)\n}\n\nexport function isStudioPingMessage(msg: AgentMessage): msg is StudioPingMessage {\n return msg.type === 'studio:ping'\n}\n\nexport function isDisconnectMessage(msg: AgentMessage): msg is StudioDisconnectMessage {\n return msg.type === 'studio:disconnect'\n}\n"],"mappings":";;;;;AAkQA,MAAa,eAAe;CAAC;CAAmB;CAAkB;AAAa;AAwP/E,SAAgB,iBAAiB,KAA0C;CACzE,OAAQ,aAAuC,SAAS,IAAI,IAAI;AAClE;;;;;;;;;;;;AAaA,SAAgB,cAAkC,KAAmB,MAAiC;CACpG,OAAO,IAAI,SAAS,iBAAiB,OAAO,IAAI,QAAQ,SAAS,OAAO;AAC1E;AAEA,SAAgB,oBAAoB,KAA6C;CAC/E,OAAO,IAAI,SAAS;AACtB;AAEA,SAAgB,oBAAoB,KAAmD;CACrF,OAAO,IAAI,SAAS;AACtB"}
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
import "./rolldown-runtime-CRm0XQPb.js";
|
|
2
|
+
import { createRequire } from "node:module";
|
|
3
|
+
import { existsSync } from "node:fs";
|
|
4
|
+
import { pathToFileURL } from "node:url";
|
|
5
|
+
import { mergeDeep } from "remeda";
|
|
6
|
+
//#region ../../internals/utils/src/casing.ts
|
|
7
|
+
/**
|
|
8
|
+
* Shared implementation for camelCase and PascalCase conversion.
|
|
9
|
+
* Splits on common word boundaries (spaces, hyphens, underscores, dots, slashes, colons)
|
|
10
|
+
* and capitalizes each word according to `pascal`.
|
|
11
|
+
*
|
|
12
|
+
* When `pascal` is `true` the first word is also capitalized (PascalCase), otherwise only subsequent words are.
|
|
13
|
+
*/
|
|
14
|
+
function toCamelOrPascal(text, pascal) {
|
|
15
|
+
return text.trim().replace(/([a-z\d])([A-Z])/g, "$1 $2").replace(/([A-Z]+)([A-Z][a-z])/g, "$1 $2").replace(/(\d)([a-z])/g, "$1 $2").split(/[\s\-_./\\:]+/).filter(Boolean).map((word, i) => {
|
|
16
|
+
if (word.length > 1 && word === word.toUpperCase()) return word;
|
|
17
|
+
return (i === 0 && !pascal ? word.charAt(0).toLowerCase() : word.charAt(0).toUpperCase()) + word.slice(1);
|
|
18
|
+
}).join("").replace(/[^a-zA-Z0-9]/g, "");
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Converts `text` to camelCase.
|
|
22
|
+
*
|
|
23
|
+
* @example Word boundaries
|
|
24
|
+
* `camelCase('hello-world') // 'helloWorld'`
|
|
25
|
+
*
|
|
26
|
+
* @example With a prefix
|
|
27
|
+
* `camelCase('tag', { prefix: 'create' }) // 'createTag'`
|
|
28
|
+
*/
|
|
29
|
+
function camelCase(text, { prefix = "", suffix = "" } = {}) {
|
|
30
|
+
return toCamelOrPascal(`${prefix} ${text} ${suffix}`, false);
|
|
31
|
+
}
|
|
32
|
+
//#endregion
|
|
33
|
+
//#region src/resolveConfig.ts
|
|
34
|
+
/**
|
|
35
|
+
* Imports a package, falling back to how the user's project would resolve it.
|
|
36
|
+
*
|
|
37
|
+
* `import()` resolves from this file, so a linked or globally installed Studio (`pnpm link`,
|
|
38
|
+
* `npm i -g`) only sees its own `node_modules` and misses the plugins installed next to the user's
|
|
39
|
+
* config. The retry resolves from `process.cwd()` instead.
|
|
40
|
+
*/
|
|
41
|
+
async function importFromProject(packageName) {
|
|
42
|
+
try {
|
|
43
|
+
return await import(packageName);
|
|
44
|
+
} catch {
|
|
45
|
+
const resolved = createRequire(pathToFileURL(`${process.cwd()}/`)).resolve(packageName);
|
|
46
|
+
const esm = resolved.replace(/\.cjs$/, ".js");
|
|
47
|
+
return await import(pathToFileURL(esm !== resolved && existsSync(esm) ? esm : resolved).href);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Strips the `@kubb/` scope from a plugin package name, matching the `name` convention Kubb
|
|
52
|
+
* plugin factories use internally.
|
|
53
|
+
*
|
|
54
|
+
* @example
|
|
55
|
+
* ```ts
|
|
56
|
+
* toPluginName('@kubb/plugin-ts') // 'plugin-ts'
|
|
57
|
+
* ```
|
|
58
|
+
*/
|
|
59
|
+
function toPluginName(packageName) {
|
|
60
|
+
return packageName.split("/").pop() ?? packageName;
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Derives the conventional named export for a `@kubb/*` plugin package from its package name.
|
|
64
|
+
*
|
|
65
|
+
* @example
|
|
66
|
+
* ```ts
|
|
67
|
+
* toExportName('@kubb/plugin-react-query') // 'pluginReactQuery'
|
|
68
|
+
* toExportName('@kubb/plugin-ts') // 'pluginTs'
|
|
69
|
+
* ```
|
|
70
|
+
*/
|
|
71
|
+
function toExportName(packageName) {
|
|
72
|
+
return camelCase(toPluginName(packageName));
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* A `@kubb/plugin-*` package specifier. Nothing else may reach `import()`: only Kubb's own
|
|
76
|
+
* plugins are supported, so a payload naming anything else, a third-party package or a path, is
|
|
77
|
+
* refused before it can execute.
|
|
78
|
+
*/
|
|
79
|
+
const KUBB_PLUGIN_SPECIFIER = /^@kubb\/plugin-[\w.-]+$/;
|
|
80
|
+
/**
|
|
81
|
+
* Whether `name` is a `@kubb/plugin-*` specifier. Exported so `configFile.ts` can refuse the same
|
|
82
|
+
* shape before printing a Studio-supplied plugin name into the config file's source text.
|
|
83
|
+
*/
|
|
84
|
+
function isKubbPluginSpecifier(name) {
|
|
85
|
+
return KUBB_PLUGIN_SPECIFIER.test(name);
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* Dynamically imports a `@kubb/plugin-*` package and returns its factory function.
|
|
89
|
+
*
|
|
90
|
+
* Packages must be pre-installed in the Docker image at build time via the `KUBB_PACKAGES`
|
|
91
|
+
* build ARG, no runtime installation is possible in the distroless container.
|
|
92
|
+
*
|
|
93
|
+
* Resolution order: the camelCase named export the package name implies (e.g. `pluginTs`), then
|
|
94
|
+
* the default export.
|
|
95
|
+
*
|
|
96
|
+
* @throws if the package cannot be imported or exports no callable factory.
|
|
97
|
+
*/
|
|
98
|
+
async function loadPluginFactory(packageName) {
|
|
99
|
+
if (!isKubbPluginSpecifier(packageName)) throw new Error(`Plugin "${packageName}" is not a @kubb/plugin-* package. Kubb Studio only supports Kubb's own plugins.`);
|
|
100
|
+
let mod;
|
|
101
|
+
try {
|
|
102
|
+
mod = await importFromProject(packageName);
|
|
103
|
+
} catch (cause) {
|
|
104
|
+
throw new Error(`Plugin "${packageName}" could not be loaded. Make sure it is installed: \`npm install ${packageName}\``, { cause });
|
|
105
|
+
}
|
|
106
|
+
const exportName = toExportName(packageName);
|
|
107
|
+
if (typeof mod[exportName] === "function") return mod[exportName];
|
|
108
|
+
if (typeof mod["default"] === "function") return mod["default"];
|
|
109
|
+
throw new Error(`Plugin "${packageName}" does not export a callable factory. Tried the named export "${exportName}" and "default".`);
|
|
110
|
+
}
|
|
111
|
+
/**
|
|
112
|
+
* Resolves each plugin entry by dynamically importing the `@kubb/plugin-*` package and
|
|
113
|
+
* calling its factory with the provided options.
|
|
114
|
+
*
|
|
115
|
+
* Packages must be pre-installed in the Docker image at build time, use the `KUBB_PACKAGES`
|
|
116
|
+
* build ARG to control which ones are available at runtime.
|
|
117
|
+
*
|
|
118
|
+
* @example
|
|
119
|
+
* ```ts
|
|
120
|
+
* { name: '@kubb/plugin-react-query', options: { output: { path: './hooks' } } }
|
|
121
|
+
* { name: '@kubb/plugin-ts', options: { output: { path: './types' } } }
|
|
122
|
+
* ```
|
|
123
|
+
*/
|
|
124
|
+
async function resolvePlugins(plugins) {
|
|
125
|
+
return Promise.all(plugins.map(async ({ name, options }) => {
|
|
126
|
+
return (await loadPluginFactory(name))(options ?? {});
|
|
127
|
+
}));
|
|
128
|
+
}
|
|
129
|
+
/**
|
|
130
|
+
* Merges studio plugin options with disk config plugins.
|
|
131
|
+
* Studio takes priority: options from studio win over disk, and a plugin Studio explicitly
|
|
132
|
+
* disabled is dropped even when the disk config still lists it. Disk plugins without a studio
|
|
133
|
+
* counterpart are kept as-is. Studio plugins not present on disk are appended.
|
|
134
|
+
*
|
|
135
|
+
* For plugins present in both configs, the plugin is re-instantiated with merged options
|
|
136
|
+
* so that all internal closures correctly reference the merged values.
|
|
137
|
+
*/
|
|
138
|
+
async function mergePlugins(diskPlugins, studioPlugins) {
|
|
139
|
+
const disabledNames = new Set((studioPlugins ?? []).filter((entry) => entry.disabled).map((entry) => toPluginName(entry.name)));
|
|
140
|
+
const activeDiskPlugins = disabledNames.size ? diskPlugins?.filter((plugin) => !disabledNames.has(plugin.name)) : diskPlugins;
|
|
141
|
+
const activeStudioPlugins = studioPlugins?.filter((entry) => !entry.disabled);
|
|
142
|
+
if (!activeDiskPlugins && !activeStudioPlugins?.length) return void 0;
|
|
143
|
+
if (!activeStudioPlugins?.length) return activeDiskPlugins;
|
|
144
|
+
if (!activeDiskPlugins) return resolvePlugins(activeStudioPlugins);
|
|
145
|
+
const studioEntryByName = new Map(activeStudioPlugins.map((entry) => [toPluginName(entry.name), entry]));
|
|
146
|
+
const diskNames = new Set(activeDiskPlugins.map((plugin) => plugin.name));
|
|
147
|
+
const merged = await Promise.all(activeDiskPlugins.map(async (diskPlugin) => {
|
|
148
|
+
const studioEntry = studioEntryByName.get(diskPlugin.name);
|
|
149
|
+
if (!studioEntry) return diskPlugin;
|
|
150
|
+
const options = mergeDeep(diskPlugin.options ?? {}, studioEntry.options ?? {});
|
|
151
|
+
const [resolved] = await resolvePlugins([{
|
|
152
|
+
name: studioEntry.name,
|
|
153
|
+
options
|
|
154
|
+
}]);
|
|
155
|
+
return resolved ?? diskPlugin;
|
|
156
|
+
}));
|
|
157
|
+
const studioOnly = activeStudioPlugins.filter((entry) => !diskNames.has(toPluginName(entry.name)));
|
|
158
|
+
return [...merged, ...await resolvePlugins(studioOnly)];
|
|
159
|
+
}
|
|
160
|
+
/**
|
|
161
|
+
* Merges Studio-provided adapter option overrides into the disk config's adapter.
|
|
162
|
+
*
|
|
163
|
+
* Adapter instances carry live functions (`parse`, `getImports`, ...) that can't survive
|
|
164
|
+
* JSON serialization over the WebSocket, so `studioOptions` is treated as an options patch
|
|
165
|
+
* rather than a replacement adapter. Re-invokes the same `@kubb/adapter-<name>` factory the
|
|
166
|
+
* disk config used, with the merged options, so the resulting instance has fresh closures
|
|
167
|
+
* over the merged values instead of a plain object missing `parse`.
|
|
168
|
+
*/
|
|
169
|
+
async function mergeAdapter(diskAdapter, studioOptions) {
|
|
170
|
+
if (!studioOptions || !diskAdapter) return diskAdapter;
|
|
171
|
+
const packageName = `@kubb/adapter-${diskAdapter.name}`;
|
|
172
|
+
const factory = (await importFromProject(packageName))[toExportName(packageName)];
|
|
173
|
+
if (typeof factory !== "function") return diskAdapter;
|
|
174
|
+
return factory(mergeDeep(diskAdapter.options ?? {}, studioOptions));
|
|
175
|
+
}
|
|
176
|
+
//#endregion
|
|
177
|
+
export { toExportName as i, mergeAdapter as n, mergePlugins as r, isKubbPluginSpecifier as t };
|
|
178
|
+
|
|
179
|
+
//# sourceMappingURL=resolveConfig-B9oGiNMi.js.map
|