@forinda/kickjs-devtools 7.11.0 → 7.12.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1 +1 @@
1
- {"version":3,"file":"index.mjs","names":["\"DevToolsAdapter\""],"sources":["../src/cron.ts","../src/topology.ts","../src/devtools-tabs.ts","../src/request-log.ts","../src/jobs.ts","../src/source-locator.ts","../src/bus/server.ts","../src/adapter.ts"],"sourcesContent":["/**\n * `@Cron` jobs for the Jobs tab: every job the container can resolve, its\n * next run, and what happened when it last ran — whatever runs it (KickJS's\n * `KickCronAdapter`, an app's own croner adapter, a serverless trigger).\n *\n * Run stats come from wrapping each job's method on its class once: the\n * runner resolves the instance and calls the method, so the wrapper sees every\n * run without the runner knowing DevTools exists.\n */\nimport { createRequire } from 'node:module'\nimport { join } from 'node:path'\nimport { isCronJobEnabled, listCronJobs, runCronJob, type CronJob } from '@forinda/kickjs'\n\nexport interface CronJobStats {\n runs: number\n failures: number\n /** Runs in progress now. */\n running: number\n lastStartedAt?: number\n lastDurationMs?: number\n lastOutcome?: 'ok' | 'failed'\n lastError?: string\n}\n\nexport interface CronJobEntry {\n name: string\n className: string\n handler: string\n expression: string\n timezone?: string\n description?: string\n enabled: boolean\n overlap: boolean\n runOnInit: boolean\n /** Epoch ms of the next tick, when `croner` is installed to work it out. */\n nextRunAt: number | null\n stats: CronJobStats\n}\n\ntype Container = Parameters<typeof listCronJobs>[0] & { resolve(token: unknown): unknown }\n\nconst TRACKED = Symbol.for('kickjs.devtools.cronTracked')\nconst stats = new Map<string, CronJobStats>()\n\n/**\n * By class *name*, like KickJS's own cron registry and `runCronJob`'s running\n * set: `listCronJobs` holds one class per name so an HMR reload replaces the\n * old class, and keying stats the same way keeps a job's counts across\n * reloads (the class is new; the job isn't).\n */\nconst keyOf = (job: Pick<CronJob, 'target' | 'handlerName'>): string =>\n `${job.target.name}.${job.handlerName}`\n\nfunction statsFor(key: string): CronJobStats {\n let s = stats.get(key)\n if (!s) stats.set(key, (s = { runs: 0, failures: 0, running: 0 }))\n return s\n}\n\n/**\n * Wrap each job's method once so its runs are counted. A sync handler stays\n * sync; a promise is passed through after its outcome is recorded.\n */\nexport function trackCronJobs(jobs: readonly CronJob[]): void {\n for (const job of jobs) {\n const proto = job.target.prototype as Record<string, unknown>\n const original = proto[job.handlerName]\n if (typeof original !== 'function' || (original as { [TRACKED]?: true })[TRACKED]) continue\n const s = statsFor(keyOf(job))\n const settle = (startedAt: number, error?: unknown): void => {\n s.running--\n s.runs++\n s.lastDurationMs = performance.now() - startedAt\n s.lastOutcome = error === undefined ? 'ok' : 'failed'\n if (error !== undefined) {\n s.failures++\n s.lastError = error instanceof Error ? error.message : String(error)\n }\n }\n const wrapped = function (this: unknown, ...args: unknown[]): unknown {\n s.running++\n s.lastStartedAt = Date.now()\n const startedAt = performance.now()\n let result: unknown\n try {\n result = (original as (...a: unknown[]) => unknown).apply(this, args)\n } catch (err) {\n settle(startedAt, err ?? new Error('threw'))\n throw err\n }\n if (result && typeof (result as Promise<unknown>).then === 'function') {\n return (result as Promise<unknown>).then(\n (value) => {\n settle(startedAt)\n return value\n },\n (err: unknown) => {\n settle(startedAt, err ?? new Error('rejected'))\n throw err\n },\n )\n }\n settle(startedAt)\n return result\n }\n Object.defineProperty(wrapped, TRACKED, { value: true })\n Object.defineProperty(wrapped, 'name', { value: (original as { name: string }).name })\n proto[job.handlerName] = wrapped\n }\n}\n\n/** Track every job the container can resolve — call at startup so early runs count. */\nexport function trackContainerCronJobs(container: Container): void {\n trackCronJobs(listCronJobs(container))\n}\n\ntype NextRun = (expression: string, timezone?: string) => number | null\n\n/** `croner` from the app's own dependencies, if it has it; otherwise no next-run times. */\nfunction loadNextRun(): NextRun {\n try {\n const { Cron } = createRequire(join(process.cwd(), 'noop.js'))('croner') as {\n Cron: new (\n expr: string,\n opts: { paused: boolean; timezone?: string },\n ) => { nextRun(): Date | null; stop(): void }\n }\n return (expression, timezone) => {\n try {\n const c = new Cron(expression, { paused: true, ...(timezone ? { timezone } : {}) })\n const next = c.nextRun()\n c.stop()\n return next ? next.getTime() : null\n } catch {\n return null\n }\n }\n } catch {\n return () => null\n }\n}\nlet nextRun: NextRun | undefined\n\n/** Every resolvable `@Cron` job, tracked, with its next run and stats. */\nexport function cronSnapshot(container: Container): CronJobEntry[] {\n const jobs = listCronJobs(container)\n trackCronJobs(jobs)\n nextRun ??= loadNextRun()\n return jobs.map((job) => ({\n name: job.name,\n className: job.target.name,\n handler: job.handlerName,\n expression: job.expression,\n ...(job.timezone ? { timezone: job.timezone } : {}),\n ...(job.description ? { description: job.description } : {}),\n enabled: isCronJobEnabled(job),\n overlap: !!job.overlap,\n runOnInit: !!job.runOnInit,\n nextRunAt: nextRun!(job.expression, job.timezone),\n stats: { ...statsFor(keyOf(job)) },\n }))\n}\n\n/**\n * Start one job now (trigger `manual`) without waiting for it: a sweep can\n * take minutes, and its stats show when it settles. Skips a disabled job and,\n * unless it allows overlap, one that's already running.\n */\nexport function runCronJobNow(\n container: Container,\n name: string,\n): { status: number; body: Record<string, unknown> } {\n const job = listCronJobs(container).find((j) => j.name === name)\n if (!job) return { status: 404, body: { error: `No cron job named \"${name}\"` } }\n if (!isCronJobEnabled(job)) return { status: 409, body: { error: `${name} is disabled` } }\n trackCronJobs([job])\n if (!job.overlap && statsFor(keyOf(job)).running > 0) {\n return { status: 409, body: { error: `${name} is already running` } }\n }\n // Failures are logged and reported by runCronJob, and land in the stats.\n void runCronJob(job, container, 'manual').catch(() => {})\n return { status: 202, body: { started: true } }\n}\n","/**\n * Topology aggregator — walks the running KickJS application + DI\n * container and produces a JSON-serialisable {@link TopologySnapshot}.\n *\n * Calls `introspect()` on every plugin and adapter that implements it\n * (architecture.md §23.3); falls back to a minimal `{ protocolVersion,\n * name, kind }` stub for primitives that don't. Per-primitive\n * introspection failures are caught + collected into the snapshot's\n * `errors` field rather than failing the entire endpoint.\n *\n * @module @forinda/kickjs-devtools/topology\n */\n\nimport type { AppAdapter, Container, KickPlugin } from '@forinda/kickjs'\nimport {\n PROTOCOL_VERSION,\n type IntrospectionSnapshot,\n type LifecycleHook,\n type TopologyContributorEntry,\n type TopologyError,\n type TopologySnapshot,\n type TopologyTokenEntry,\n} from '@forinda/kickjs-devtools-kit'\n\n/** Minimal Application surface the aggregator needs — keeps coupling thin. */\nexport interface TopologyApplicationLike {\n getAdapters(): AppAdapter[]\n getPlugins(): readonly KickPlugin[]\n /** Adapters with a `when` condition and whether each is on — absent on older Applications. */\n getConditionReport?(): ReadonlyArray<{ adapter: string; enabled: boolean; reason?: string }>\n /** Where the last startup's time went — absent on older Applications. */\n getStartupTimeline?(): { totalMs: number; steps: ReadonlyArray<{ name: string; ms: number }> }\n /** Mounted routes with their contributor chains — absent on older Applications. */\n getRoutes?(): ReadonlyArray<{\n method: string\n pattern: string\n controller: string\n handler: string\n contributors: ReadonlyArray<{\n key: string\n source: string\n label?: string\n dependsOn: readonly string[]\n beforeValidation: boolean\n overrides: ReadonlyArray<{ source: string; label?: string }>\n }>\n }>\n /**\n * Optional in older Application implementations — when present, returns\n * the union of contributors across adapters/plugins/global with proper\n * source labels. The topology aggregator prefers this over walking\n * `adapter.contributors()` + `plugin.contributors()` directly because\n * the application-level walk preserves source provenance.\n */\n getContributors?(): ReadonlyArray<{\n key: string\n source: 'method' | 'class' | 'adapter' | 'plugin' | 'module' | 'global'\n label: string\n dependsOn: readonly string[]\n }>\n}\n\n/** Inputs to {@link collectTopologySnapshot}. */\nexport interface CollectTopologyOptions {\n /** The running KickJS application (or anything that exposes its surface). */\n app: TopologyApplicationLike\n /** The DI container — used to enumerate registered tokens. */\n container: Container\n /**\n * Optional per-call introspect timeout in milliseconds. An adapter\n * whose `introspect()` doesn't settle within the budget gets a stub\n * snapshot + an entry in `errors`. Default: 100ms — keeps the\n * topology endpoint snappy even if one adapter misbehaves.\n */\n introspectTimeoutMs?: number\n}\n\nconst DEFAULT_INTROSPECT_TIMEOUT_MS = 100\n\n/** Lifecycle hooks of `AppAdapter` and `KickPlugin`, in the order they run. */\nconst HOOKS: readonly LifecycleHook[] = [\n 'register',\n 'modules',\n 'adapters',\n 'setup',\n 'middleware',\n 'contributors',\n 'beforeMount',\n 'onRouteMount',\n 'beforeStart',\n 'afterStart',\n 'onReady',\n 'onResponse',\n 'onError',\n 'onHealthCheck',\n 'shutdown',\n]\n\n/**\n * Collect a {@link TopologySnapshot} for the given application.\n *\n * Async because plugin/adapter `introspect()` may be async; the work\n * is parallelised across all primitives so wall-clock time scales\n * with the slowest introspector, not their sum.\n */\nexport async function collectTopologySnapshot(\n opts: CollectTopologyOptions,\n): Promise<TopologySnapshot> {\n const { app, container, introspectTimeoutMs = DEFAULT_INTROSPECT_TIMEOUT_MS } = opts\n const errors: TopologyError[] = []\n\n const plugins = await Promise.all(\n app.getPlugins().map((p) => snapshotFor(p, 'plugin', introspectTimeoutMs, errors)),\n )\n const adapters = await Promise.all(\n app.getAdapters().map((a) => snapshotFor(a, 'adapter', introspectTimeoutMs, errors)),\n )\n\n return {\n protocolVersion: PROTOCOL_VERSION,\n timestamp: Date.now(),\n plugins,\n adapters,\n contributors: collectContributors(app),\n diTokens: collectTokens(container),\n errors,\n }\n}\n\n/**\n * Coerce one primitive into an {@link IntrospectionSnapshot}. Tries\n * the primitive's own `introspect()` first; falls back to a stub on\n * any of: missing implementation, throw, timeout, or non-object return.\n */\nasync function snapshotFor(\n primitive: AppAdapter | KickPlugin,\n kind: 'plugin' | 'adapter',\n timeoutMs: number,\n errors: TopologyError[],\n): Promise<IntrospectionSnapshot> {\n const name = primitive.name ?? '(unnamed)'\n const hooks = HOOKS.filter((h) => typeof (primitive as Record<string, unknown>)[h] === 'function')\n const dependsOn = Array.isArray(primitive.dependsOn) ? [...primitive.dependsOn] : []\n const stub: IntrospectionSnapshot = {\n protocolVersion: PROTOCOL_VERSION,\n name,\n kind,\n hooks,\n dependsOn,\n }\n if (typeof primitive.introspect !== 'function') return stub\n\n try {\n const result = await Promise.race([\n Promise.resolve(primitive.introspect()),\n new Promise<never>((_, reject) =>\n setTimeout(\n () => reject(new Error(`introspect() timed out after ${timeoutMs}ms`)),\n timeoutMs,\n ),\n ),\n ])\n if (result && typeof result === 'object') {\n // Accept whatever the primitive returned; the kit's\n // IntrospectionSnapshot is opt-in typed via the kit, not enforced\n // at this layer (see plugin.ts / adapter.ts comment about avoiding\n // a runtime dep on the kit).\n const snap = result as Partial<IntrospectionSnapshot>\n return {\n protocolVersion: PROTOCOL_VERSION,\n name: snap.name ?? name,\n kind: snap.kind ?? kind,\n version: snap.version,\n state: snap.state,\n tokens: snap.tokens,\n metrics: snap.metrics,\n memoryBytes: snap.memoryBytes,\n hooks,\n dependsOn,\n }\n }\n return stub\n } catch (err) {\n errors.push({\n name,\n kind,\n message: err instanceof Error ? err.message : String(err),\n })\n return stub\n }\n}\n\n/** Read DI registrations into the topology shape — drops noisy HMR tokens. */\nfunction collectTokens(container: Container): TopologyTokenEntry[] {\n return container\n .getRegistrations()\n .filter((r) => !r.token.startsWith('__hmr__'))\n .map((r) => ({\n token: r.token,\n scope: r.scope,\n kind: r.kind,\n instantiated: r.instantiated,\n }))\n}\n\n/**\n * Contributor enumeration. Prefers `app.getContributors()` (the\n * Application-level walk that preserves adapter / plugin / global\n * source labels) when present; falls back to walking\n * `adapter.contributors()` + `plugin.contributors()` directly for\n * older Application implementations that don't expose the helper.\n *\n * Every source is covered, decorators included — the Application reads\n * class / method contributors back from the routes it mounted. Each route's\n * full chain, in execution order, is on `/_debug/routes`.\n */\nfunction collectContributors(app: TopologyApplicationLike): TopologyContributorEntry[] {\n const seen = new Map<string, TopologyContributorEntry>()\n\n if (typeof app.getContributors === 'function') {\n for (const c of app.getContributors()) {\n // One entry per key AND source: a method decorator overriding a module\n // contributor of the same key is a different registration, not a repeat.\n const id = `${c.source}:${c.key}`\n if (seen.has(id)) continue\n // Pass `source` through unchanged — the kit's\n // `TopologyContributorSource` union accepts every value the\n // framework's `getContributors()` returns. Earlier versions\n // collapsed `'plugin' | 'global'` → `'adapter'` because the kit\n // type was narrower; that's no longer needed.\n seen.set(id, {\n key: c.key,\n source: c.source as TopologyContributorEntry['source'],\n dependsOn: c.dependsOn as string[],\n })\n }\n return [...seen.values()]\n }\n\n // Legacy path — direct walk.\n const ingest = (\n list: ReadonlyArray<unknown> | null | undefined,\n source: TopologyContributorEntry['source'],\n ): void => {\n if (!list) return\n for (const reg of list) {\n const r = reg as { key?: string; dependsOn?: readonly string[] }\n const key = typeof r.key === 'string' ? r.key : null\n if (!key || seen.has(key)) continue\n seen.set(key, {\n key,\n source,\n dependsOn: Array.isArray(r.dependsOn) ? r.dependsOn : [],\n })\n }\n }\n\n for (const plugin of app.getPlugins()) {\n ingest(plugin.contributors?.(), 'adapter')\n }\n for (const adapter of app.getAdapters()) {\n ingest(adapter.contributors?.(), 'adapter')\n }\n\n return [...seen.values()]\n}\n","/**\n * Custom-tab aggregator — collects every plugin/adapter's\n * `devtoolsTabs()` contribution into a single deduped list for the\n * panel's `/_debug/tabs` endpoint.\n *\n * Validation is permissive: anything that doesn't look like a\n * {@link DevtoolsTabDescriptor} is dropped with a console warning\n * rather than throwing — a misconfigured adapter shouldn't break the\n * whole DevTools panel.\n *\n * @module @forinda/kickjs-devtools/devtools-tabs\n */\n\nimport type { DevtoolsTabDescriptor } from '@forinda/kickjs-devtools-kit'\nimport type { TopologyApplicationLike } from './topology'\n\n/** Result of {@link collectDevtoolsTabs}. */\nexport interface DevtoolsTabsResult {\n /** Deduped, validated tabs in registration order. */\n tabs: DevtoolsTabDescriptor[]\n /** Per-source collection errors (drop reason, source name). */\n errors: ReadonlyArray<{ source: string; reason: string }>\n}\n\nconst VALID_VIEW_TYPES = new Set(['iframe', 'launch', 'html', 'module'])\n\n/**\n * Walk every plugin + adapter, call `devtoolsTabs?()`, validate each\n * entry, and dedupe by `id`. Last-source-wins on collision is too\n * surprising — first-source-wins + a warning entry instead.\n */\nexport function collectDevtoolsTabs(app: TopologyApplicationLike): DevtoolsTabsResult {\n const tabs: DevtoolsTabDescriptor[] = []\n const seen = new Map<string, string>() // id → source name (for collision warnings)\n const errors: Array<{ source: string; reason: string }> = []\n\n const ingest = (sourceName: string, raw: unknown): void => {\n if (!Array.isArray(raw)) {\n errors.push({ source: sourceName, reason: 'devtoolsTabs() did not return an array' })\n return\n }\n for (const entry of raw) {\n const validation = validateTab(entry)\n if (!validation.ok) {\n errors.push({ source: sourceName, reason: validation.reason })\n continue\n }\n const tab = validation.tab\n const owner = seen.get(tab.id)\n if (owner) {\n errors.push({\n source: sourceName,\n reason: `tab id '${tab.id}' already registered by ${owner} — first-source wins`,\n })\n continue\n }\n seen.set(tab.id, sourceName)\n tabs.push(tab)\n }\n }\n\n for (const plugin of app.getPlugins()) {\n if (typeof plugin.devtoolsTabs !== 'function') continue\n try {\n ingest(plugin.name ?? '(unnamed plugin)', plugin.devtoolsTabs())\n } catch (err) {\n errors.push({\n source: plugin.name ?? '(unnamed plugin)',\n reason: err instanceof Error ? err.message : String(err),\n })\n }\n }\n for (const adapter of app.getAdapters()) {\n if (typeof adapter.devtoolsTabs !== 'function') continue\n try {\n ingest(adapter.name ?? '(unnamed adapter)', adapter.devtoolsTabs())\n } catch (err) {\n errors.push({\n source: adapter.name ?? '(unnamed adapter)',\n reason: err instanceof Error ? err.message : String(err),\n })\n }\n }\n\n return { tabs, errors }\n}\n\ninterface ValidationOk {\n ok: true\n tab: DevtoolsTabDescriptor\n}\n\ninterface ValidationFail {\n ok: false\n reason: string\n}\n\n/**\n * Coerce + validate a tab descriptor. Returns the typed object on\n * success; a structured reason on failure. Uses a permissive checker\n * so a partially-correct entry from a third-party adapter doesn't\n * break the whole panel.\n */\nfunction validateTab(raw: unknown): ValidationOk | ValidationFail {\n if (!raw || typeof raw !== 'object') {\n return { ok: false, reason: 'tab entry is not an object' }\n }\n const obj = raw as Record<string, unknown>\n if (typeof obj.id !== 'string' || obj.id.length === 0) {\n return { ok: false, reason: 'tab.id must be a non-empty string' }\n }\n if (typeof obj.title !== 'string' || obj.title.length === 0) {\n return { ok: false, reason: `tab '${obj.id}': title must be a non-empty string` }\n }\n const view = obj.view as Record<string, unknown> | undefined\n if (!view || typeof view !== 'object') {\n return { ok: false, reason: `tab '${obj.id}': view object is required` }\n }\n if (typeof view.type !== 'string' || !VALID_VIEW_TYPES.has(view.type)) {\n return {\n ok: false,\n reason: `tab '${obj.id}': view.type must be one of iframe / launch / html / module`,\n }\n }\n switch (view.type) {\n case 'iframe':\n if (typeof view.src !== 'string' || view.src.length === 0) {\n return { ok: false, reason: `tab '${obj.id}': iframe view requires a non-empty src` }\n }\n break\n case 'html':\n if (typeof view.html !== 'string') {\n return { ok: false, reason: `tab '${obj.id}': html view requires html string` }\n }\n break\n case 'module':\n if (typeof view.src !== 'string' || view.src.length === 0) {\n return { ok: false, reason: `tab '${obj.id}': module view requires a non-empty src` }\n }\n break\n case 'launch':\n if (!Array.isArray(view.actions)) {\n return { ok: false, reason: `tab '${obj.id}': launch view requires actions array` }\n }\n break\n }\n return { ok: true, tab: obj as unknown as DevtoolsTabDescriptor }\n}\n\n/**\n * Run a `launch` tab's button. The tabs are collected again rather than\n * cached, so an HMR-reloaded adapter's new `run()` is the one called.\n */\nexport async function runTabAction(\n app: TopologyApplicationLike,\n tabId: string,\n actionId: string,\n): Promise<{ status: number; body: { result?: unknown; error?: string } }> {\n const tab = collectDevtoolsTabs(app).tabs.find((t) => t.id === tabId)\n const action =\n tab?.view.type === 'launch' ? tab.view.actions.find((a) => a.id === actionId) : undefined\n if (!action) return { status: 404, body: { error: `no action '${actionId}' on tab '${tabId}'` } }\n if (typeof action.run !== 'function') {\n return { status: 404, body: { error: `action '${actionId}' has no run()` } }\n }\n try {\n return { status: 200, body: { result: (await action.run()) ?? null } }\n } catch (err) {\n return { status: 500, body: { error: err instanceof Error ? err.message : String(err) } }\n }\n}\n","/**\n * The last N requests the app answered, for the dashboard's Requests tab.\n *\n * Fed by the adapter's `onResponse` / `onError` hooks. An error is matched\n * to its request by `requestId`, whichever of the two hooks fires first.\n *\n * @module @forinda/kickjs-devtools/request-log\n */\n\nexport interface RequestLogEntry {\n /** Increasing per entry — the dashboard asks for entries after the last one it has. */\n seq: number\n /** When the response finished, ms since epoch. */\n at: number\n method: string\n path: string\n /** Matched route pattern; absent for unmatched requests. */\n route?: string\n status: number\n durationMs: number\n requestId?: string\n /** The error the request failed with, when one was reported. */\n error?: { name: string; message: string }\n}\n\ntype Response = Pick<\n RequestLogEntry,\n 'method' | 'path' | 'route' | 'status' | 'durationMs' | 'requestId'\n>\n\n/** Errors waiting for their response, keyed by request id — bounded like the log. */\nconst MAX_PENDING_ERRORS = 100\n\nexport class RequestLog {\n private entries: RequestLogEntry[] = []\n private pendingErrors = new Map<string, RequestLogEntry['error']>()\n private seq = 0\n\n constructor(private readonly capacity: number) {}\n\n record(info: Response, now = Date.now()): void {\n if (this.capacity <= 0) return\n const entry: RequestLogEntry = { seq: ++this.seq, at: now, ...info }\n if (info.requestId && this.pendingErrors.has(info.requestId)) {\n entry.error = this.pendingErrors.get(info.requestId)\n this.pendingErrors.delete(info.requestId)\n }\n this.entries.push(entry)\n if (this.entries.length > this.capacity) this.entries.shift()\n }\n\n recordError(requestId: string | undefined, err: unknown): void {\n if (this.capacity <= 0 || !requestId) return\n const error =\n err instanceof Error\n ? { name: err.name, message: err.message }\n : { name: 'Error', message: String(err) }\n // Response already logged — attach to it; otherwise hold until it is.\n for (let i = this.entries.length - 1; i >= 0; i--) {\n if (this.entries[i]!.requestId === requestId) {\n this.entries[i]!.error = error\n return\n }\n }\n this.pendingErrors.set(requestId, error)\n if (this.pendingErrors.size > MAX_PENDING_ERRORS) {\n this.pendingErrors.delete(this.pendingErrors.keys().next().value!)\n }\n }\n\n /** The newest entry's `seq` — lower than a client's last one means the app restarted. */\n latest(): number {\n return this.seq\n }\n\n /** Entries after `since`, oldest first. */\n after(since = 0): RequestLogEntry[] {\n return this.entries.filter((e) => e.seq > since)\n }\n}\n","/**\n * Job management for the Queues tab — over any {@link JobInspector} an\n * adapter or plugin exposes from `jobInspector()`. Each inspector is a\n * `source`, named after its owner, so two job tools can sit side by side.\n *\n * @module @forinda/kickjs-devtools/jobs\n */\n\nimport type { JobInspector, JobQueueInfo, JobState } from '@forinda/kickjs-devtools-kit'\nimport type { TopologyApplicationLike } from './topology'\n\nconst STATES: ReadonlySet<string> = new Set([\n 'waiting',\n 'active',\n 'delayed',\n 'completed',\n 'failed',\n 'paused',\n])\nconst ACTIONS = ['retry', 'remove', 'retryAll', 'clean', 'pause', 'resume'] as const\ntype Action = (typeof ACTIONS)[number]\n\n/** Most jobs one list request returns. */\nconst MAX_PAGE = 100\n\ntype Reply = { status: number; body: unknown }\nconst bad = (error: string, status = 400): Reply => ({ status, body: { error } })\n\n/** Every inspector the app's adapters and plugins expose, keyed by owner name. */\nexport function findJobInspectors(app: TopologyApplicationLike): Map<string, JobInspector> {\n const found = new Map<string, JobInspector>()\n for (const owner of [...app.getAdapters(), ...app.getPlugins()]) {\n const get = (owner as { jobInspector?: () => JobInspector | undefined }).jobInspector\n if (typeof get !== 'function') continue\n const inspector = get.call(owner)\n if (inspector) found.set(owner.name ?? 'jobs', inspector)\n }\n return found\n}\n\n/** `GET /jobs` — each source's queues and the actions it supports. */\nexport async function listJobSources(inspectors: Map<string, JobInspector>): Promise<Reply> {\n const sources = await Promise.all(\n [...inspectors].map(async ([source, inspector]) => {\n const actions = ACTIONS.filter((a) => typeof inspector[a] === 'function')\n try {\n return { source, actions, queues: await inspector.queues() }\n } catch (err) {\n return { source, actions, queues: [] as JobQueueInfo[], error: message(err) }\n }\n }),\n )\n return { status: 200, body: { sources } }\n}\n\n/** `GET /jobs/list?source&queue&state&start&end` */\nexport async function listJobs(\n inspectors: Map<string, JobInspector>,\n q: Record<string, unknown>,\n): Promise<Reply> {\n const inspector = inspectors.get(String(q.source ?? ''))\n if (!inspector) return bad('unknown source', 404)\n const state = String(q.state ?? 'failed')\n if (!STATES.has(state)) return bad(`unknown state '${state}'`)\n const start = Math.max(0, Number(q.start ?? 0) || 0)\n const end = Math.min(start + MAX_PAGE - 1, Math.max(start, Number(q.end ?? start + 49) || 0))\n try {\n const jobs = await inspector.jobs(String(q.queue ?? ''), state as JobState, { start, end })\n return { status: 200, body: { jobs } }\n } catch (err) {\n return bad(message(err), 500)\n }\n}\n\n/** `GET /jobs/job?source&queue&id` */\nexport async function getJob(\n inspectors: Map<string, JobInspector>,\n q: Record<string, unknown>,\n): Promise<Reply> {\n const inspector = inspectors.get(String(q.source ?? ''))\n if (!inspector) return bad('unknown source', 404)\n try {\n const job = await inspector.job(String(q.queue ?? ''), String(q.id ?? ''))\n return job ? { status: 200, body: job } : bad('job not found', 404)\n } catch (err) {\n return bad(message(err), 500)\n }\n}\n\n/** `POST /jobs/action?source&queue&action&id&state` */\nexport async function runJobAction(\n inspectors: Map<string, JobInspector>,\n q: Record<string, unknown>,\n): Promise<Reply> {\n const inspector = inspectors.get(String(q.source ?? ''))\n if (!inspector) return bad('unknown source', 404)\n const action = String(q.action ?? '') as Action\n if (!ACTIONS.includes(action)) return bad(`unknown action '${action}'`)\n const queue = String(q.queue ?? '')\n const id = String(q.id ?? '')\n const state = String(q.state ?? '')\n try {\n switch (action) {\n case 'retry':\n case 'remove':\n if (!id) return bad('id is required')\n if (!inspector[action]) return bad(`${action} is not supported`, 501)\n await inspector[action]!(queue, id)\n return { status: 200, body: { ok: true } }\n case 'clean':\n if (!STATES.has(state)) return bad('state is required')\n if (!inspector.clean) return bad('clean is not supported', 501)\n return {\n status: 200,\n body: { ok: true, count: await inspector.clean(queue, state as JobState) },\n }\n default:\n if (!inspector[action]) return bad(`${action} is not supported`, 501)\n return { status: 200, body: { ok: true, count: (await inspector[action]!(queue)) ?? null } }\n }\n } catch (err) {\n return bad(message(err), 500)\n }\n}\n\nconst message = (err: unknown): string => (err instanceof Error ? err.message : String(err))\n","/**\n * Find a controller method in the project's source — what \"open handler in\n * editor\" needs. The running app knows the class and method names but not the\n * file (the dev server may have transformed it), so this reads `src/` for the\n * class declaration, then the method inside it.\n *\n * Read on each lookup rather than cached: it runs on a click, in development,\n * and files move under HMR.\n */\nimport { existsSync, readdirSync, readFileSync } from 'node:fs'\nimport { join, relative, sep } from 'node:path'\n\nexport interface HandlerSource {\n /** Absolute path. */\n file: string\n /** Path relative to the project root, `/`-separated — for mapping into another checkout. */\n relative: string\n /** 1-based line of the method (or of the class, when the method isn't found). */\n line: number\n}\n\nconst escape = (name: string) => name.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\$&')\n\n/** 1-based line of the handler inside `controller`, or `undefined` when the class isn't declared here. */\nexport function findHandlerLine(\n source: string,\n controller: string,\n handler: string,\n): number | undefined {\n const cls = new RegExp(`\\\\bclass\\\\s+${escape(controller)}\\\\b`).exec(source)\n if (!cls) return undefined\n const lineAt = (offset: number) => source.slice(0, offset).split('\\n').length\n // Only this class's body: up to the next top-level class declaration, so a\n // same-named method of a later class is never picked.\n const start = cls.index + cls[0].length\n const rest = source.slice(start)\n const next = rest.search(/^(?:export\\s+)?(?:default\\s+)?(?:abstract\\s+)?class\\s/m)\n const body = next === -1 ? rest : rest.slice(0, next)\n const method = new RegExp(\n `^[ \\\\t]*(?:(?:public|private|protected|static|async|override)\\\\s+)*${escape(handler)}\\\\s*[(<]`,\n 'm',\n ).exec(body)\n return lineAt(method ? start + method.index + method[0].search(/\\S/) : cls.index)\n}\n\n/** Locate `controller.handler` under `<root>/src` (or `<root>` when there is no `src/`). */\nexport function locateHandler(\n root: string,\n controller: string,\n handler: string,\n): HandlerSource | undefined {\n const dir = existsSync(join(root, 'src')) ? join(root, 'src') : root\n const files = readdirSync(dir, { recursive: true, encoding: 'utf-8' }).filter(\n (f) =>\n /\\.[mc]?tsx?$/.test(f) &&\n !/\\.d\\.[mc]?ts$/.test(f) &&\n !f.split(sep).includes('node_modules') &&\n !f.split(sep).includes('dist'),\n )\n for (const f of files) {\n const file = join(dir, f)\n let source: string\n try {\n source = readFileSync(file, 'utf-8')\n } catch {\n continue\n }\n if (!source.includes(controller)) continue\n const line = findHandlerLine(source, controller, handler)\n if (line !== undefined) {\n return { file, relative: relative(root, file).split(sep).join('/'), line }\n }\n }\n return undefined\n}\n","// Server-side KickEventBus implementation.\n//\n// Wraps the bus core from `@forinda/kickjs-devtools-kit/bus` with a\n// `ws` server bound to a path on the framework's existing\n// `http.Server`. Architecture:\n//\n// server.on('upgrade') ─┐\n// ├──► WebSocketServer.handleUpgrade()\n// │ └─► ws clients: receive emits as JSON\n// bus.emit(type, payload) ─┴──► serialize → fan out to clients +\n// dispatch locally to in-process subs\n//\n// Why piggyback on the framework's http.Server instead of starting a\n// dedicated WS server: the framework already owns the listener, the\n// upgrade event is the canonical hook, and reusing it means devtools\n// inherits whatever TLS / proxy / port config the application provides.\n//\n// Lifecycle:\n// - createServerBus() builds the WebSocketServer in `noServer: true`\n// mode (no listener of its own) and returns the bus + attach/close\n// handles.\n// - attachUpgrade(httpServer) wires server.on('upgrade') so upgrades\n// matching `wsPath` route into the WebSocketServer. Other upgrade\n// paths (kickjs-ws, etc.) ignore us by virtue of path matching.\n// - close() drops every client and detaches from the http server so\n// repeated dev-mode restarts don't leak listener handles.\n//\n// Authentication:\n// - The optional `secret` matches the devtools panel's existing\n// `x-devtools-token` / `?token=` access guard. When set, upgrades\n// without the right token get rejected at HTTP-handshake time —\n// never reaching the bus.\n//\n// Loop avoidance:\n// - Server emits fan out to every connected client. Client-side\n// emits sent UP the socket dispatch into the local core (so\n// server tabs can react) but are NOT re-broadcast to other\n// clients. This keeps the same \"no echo\" contract the browser\n// bus enforces; without it a tab's emit would round-trip through\n// the server and back to itself.\n\nimport type { IncomingMessage } from 'node:http'\nimport type { KickServer } from '@forinda/kickjs'\nimport type { Socket } from 'node:net'\n\nimport { createBusCore } from '@forinda/kickjs-devtools-kit/bus'\nimport type { KickDevtoolsEvent, KickEventBus, Unsubscribe } from '@forinda/kickjs-devtools-kit/bus'\nimport { WebSocketServer, type WebSocket } from 'ws'\n\nexport interface ServerBusOptions {\n /** Path the WebSocket upgrade listens on. Default `/_debug/_bus`. */\n wsPath?: string\n /**\n * Optional shared secret. When set, clients must include either\n * `?token=<secret>` on the upgrade URL or an\n * `x-devtools-token: <secret>` header. Unauthorized upgrades return\n * 401 and never reach the bus. Pass `false` (or omit) to disable.\n */\n secret?: string | false\n}\n\ninterface TaggedEnvelope extends KickDevtoolsEvent {\n __kick_origin?: 'local' | 'broadcast' | 'ws'\n}\n\nexport interface ServerBus extends KickEventBus {\n /**\n * Hook into the app server's `upgrade` event — `http`, `https`, or HTTP/2\n * with HTTP/1.1 fallback all emit it for WebSocket handshakes.\n */\n attachUpgrade(httpServer: KickServer): void\n /** Drop all clients + detach. Idempotent. */\n close(): void\n /** Currently connected client count. Useful for the devtools health card. */\n clientCount(): number\n}\n\nexport function createServerBus(opts: ServerBusOptions = {}): ServerBus {\n const wsPath = opts.wsPath ?? '/_debug/_bus'\n const secret = opts.secret ?? false\n const core = createBusCore()\n const wss = new WebSocketServer({ noServer: true })\n const clients = new Set<WebSocket>()\n let attachedServer: KickServer | null = null\n let upgradeHandler: ((req: IncomingMessage, socket: Socket, head: Buffer) => void) | null = null\n\n wss.on('connection', (ws: WebSocket) => {\n clients.add(ws)\n ws.on('message', (data: Buffer | string) => {\n try {\n const text = typeof data === 'string' ? data : data.toString('utf-8')\n const parsed = JSON.parse(text) as TaggedEnvelope\n if (!parsed || typeof parsed.type !== 'string' || typeof parsed.ts !== 'number') return\n // Dispatch locally only — never echo back to other clients.\n // (Tabs that need cross-tab fan-out use BroadcastChannel; the\n // server is point-to-point per client.)\n core.dispatch({ ...parsed, __kick_origin: 'ws' } as KickDevtoolsEvent)\n } catch {\n // Malformed — drop silently. A misbehaving tab shouldn't\n // crash the server bus.\n }\n })\n ws.on('close', () => {\n clients.delete(ws)\n })\n ws.on('error', () => {\n clients.delete(ws)\n })\n })\n\n const fanOut = (envelope: TaggedEnvelope): void => {\n if (clients.size === 0) return\n // Emitters call this synchronously — kick/db does from inside a query —\n // so it must never throw. A bigint (a bigint column's value) goes out as\n // a string; an event JSON can't hold at all (a circular payload) is\n // dropped for the dashboard, not the caller.\n let text: string\n try {\n text = JSON.stringify(envelope, (_key, value: unknown) =>\n typeof value === 'bigint' ? value.toString() : value,\n )\n } catch {\n return\n }\n for (const client of clients) {\n // OPEN === 1 in the ws spec; reusing the import for the type\n // guard keeps this readable without pulling in WebSocket consts.\n if (client.readyState === client.OPEN) {\n try {\n client.send(text)\n } catch {\n // Network error mid-fan-out — clean removal happens via the\n // 'close' / 'error' handlers; nothing to do here.\n }\n }\n }\n }\n\n const checkAuth = (req: IncomingMessage): boolean => {\n if (secret === false) return true\n const header = req.headers['x-devtools-token']\n if (typeof header === 'string' && header === secret) return true\n // Parse query — `req.url` is path + ?query for upgrade requests.\n const url = req.url ?? ''\n const queryIdx = url.indexOf('?')\n if (queryIdx === -1) return false\n const params = new URLSearchParams(url.slice(queryIdx + 1))\n return params.get('token') === secret\n }\n\n const attachUpgrade = (httpServer: KickServer): void => {\n if (attachedServer) return // idempotent\n attachedServer = httpServer\n upgradeHandler = (req, socket, head) => {\n // Path match — ignore upgrades destined for other adapters\n // (kickjs-ws, custom adapters, etc.). They register their own\n // upgrade handlers; ours just no-ops on non-matching URLs.\n const url = req.url ?? ''\n const pathOnly = url.split('?')[0]\n if (pathOnly !== wsPath) return\n\n if (!checkAuth(req)) {\n socket.write('HTTP/1.1 401 Unauthorized\\r\\n\\r\\n')\n socket.destroy()\n return\n }\n\n wss.handleUpgrade(req, socket, head, (ws: WebSocket) => {\n wss.emit('connection', ws, req)\n })\n }\n httpServer.on('upgrade', upgradeHandler)\n }\n\n const close = (): void => {\n if (attachedServer && upgradeHandler) {\n attachedServer.off('upgrade', upgradeHandler)\n }\n attachedServer = null\n upgradeHandler = null\n for (const client of clients) {\n try {\n client.close()\n } catch {\n // ignore\n }\n }\n clients.clear()\n wss.close()\n }\n\n return {\n on(type: string, handler: (payload: unknown) => void): Unsubscribe {\n return core.on(type, handler)\n },\n onAny(handler: (event: KickDevtoolsEvent) => void): Unsubscribe {\n return core.onAny(handler)\n },\n emit(type: string, payload: unknown): void {\n const envelope: TaggedEnvelope = {\n type,\n payload,\n ts: Date.now(),\n __kick_origin: 'local',\n }\n core.dispatch(envelope)\n fanOut(envelope)\n },\n attachUpgrade,\n close,\n clientCount: () => clients.size,\n } as ServerBus\n}\n","import type { IncomingMessage, ServerResponse } from 'node:http'\nimport { dirname, join } from 'node:path'\nimport { tmpdir } from 'node:os'\nimport { fileURLToPath } from 'node:url'\nimport { createReadStream, existsSync, readFileSync, statSync, unlink } from 'node:fs'\nimport { Readable } from 'node:stream'\nimport { randomBytes } from 'node:crypto'\nimport { writeHeapSnapshot } from 'node:v8'\nimport {\n type AdapterMiddleware,\n type Container,\n METADATA,\n defineAdapter,\n ref,\n computed,\n reactive,\n watch,\n createLogger,\n type Ref,\n type ComputedRef,\n getClassMeta,\n getMethodMeta,\n getRouteFlags,\n type MatchedRoute,\n type RequestContext,\n} from '@forinda/kickjs'\nimport * as kick from '@forinda/kickjs'\nimport {\n MemoryAnalyzer,\n PROTOCOL_VERSION,\n RuntimeSampler,\n type IntrospectionSnapshot,\n} from '@forinda/kickjs-devtools-kit'\nimport { cronSnapshot, runCronJobNow, trackContainerCronJobs } from './cron'\nimport {\n DEVTOOLS_BUS,\n getActiveDevtoolsBus,\n setActiveDevtoolsBus,\n} from '@forinda/kickjs-devtools-kit/bus/token'\nimport { collectTopologySnapshot, type TopologyApplicationLike } from './topology'\nimport { collectDevtoolsTabs, runTabAction } from './devtools-tabs'\nimport { RequestLog } from './request-log'\nimport { findJobInspectors, getJob, listJobs, listJobSources, runJobAction } from './jobs'\nimport { locateHandler } from './source-locator'\nimport { createServerBus, type ServerBus } from './bus/server'\nimport { detectSchema } from '@forinda/kickjs-schema'\n\nconst log = createLogger('DevTools')\n\n/** The serialisable part of a handler's `@FileUpload` config, if it has one. */\nfunction uploadInfo(controllerClass: any, handlerName: string): Pick<RouteInfo, 'upload'> {\n const config = getMethodMeta<\n { mode: 'single' | 'array' | 'none'; fieldName?: string; maxCount?: number } | undefined\n >(METADATA.FILE_UPLOAD, controllerClass, handlerName, undefined)\n if (!config) return {}\n const { mode, fieldName, maxCount } = config\n return {\n upload: { mode, ...(fieldName ? { fieldName } : {}), ...(maxCount ? { maxCount } : {}) },\n }\n}\n\n/** Route metadata collected during mount */\ninterface RouteInfo {\n method: string\n path: string\n controller: string\n handler: string\n middleware: string[]\n /**\n * Route flags in force, resolved method-over-class. Present so the dashboard\n * can answer \"why is this endpoint not requiring auth\" from the route list\n * itself, rather than by reading the controller.\n */\n flags: Record<string, unknown>\n /**\n * `@FileUpload` config, when the handler declares one — the dashboard's API\n * runner starts such routes in multipart mode with this field. Only the\n * plain fields: `allowedTypes` may be a function and isn't serialisable.\n */\n upload?: { mode: 'single' | 'array' | 'none'; fieldName?: string; maxCount?: number }\n /**\n * The route's request schemas as JSON Schema (OpenAPI 3.0 form, what the\n * request accepts) — the API runner prefills from them, with or without\n * the Swagger adapter.\n */\n schemas?: { body?: JsonSchema; query?: JsonSchema; params?: JsonSchema }\n}\n\ntype JsonSchema = Record<string, unknown>\n\n/** A route's body, query and params schemas, each one that converts. */\nfunction requestSchemas(validation: RouteValidation | undefined): Pick<RouteInfo, 'schemas'> {\n if (!validation) return {}\n const schemas: NonNullable<RouteInfo['schemas']> = {}\n for (const part of ['body', 'query', 'params'] as const) {\n if (!validation[part]) continue\n try {\n schemas[part] = detectSchema(validation[part]).toJsonSchema({\n target: 'openapi-3.0',\n io: 'input',\n })\n } catch {\n // Not a schema kickjs-schema knows, or one it can't convert: no prefill for it.\n }\n }\n return Object.keys(schemas).length > 0 ? { schemas } : {}\n}\n\ntype RouteValidation = { body?: unknown; query?: unknown; params?: unknown }\n\n/** Per-route latency stats with percentile tracking */\ninterface RouteStats {\n count: number\n totalMs: number\n minMs: number\n maxMs: number\n /** Responses with status >= 500. */\n serverErrors: number\n /** Responses with status 400–499. */\n clientErrors: number\n /** Ring buffer of last N samples for percentile computation */\n samples: number[]\n}\n\n/** Upper bounds (ms) of the latency histogram's buckets; the last bucket is everything above. */\nconst LATENCY_BUCKETS_MS = [1, 2, 5, 10, 25, 50, 100, 250, 500, 1000] as const\n\n/** How many recent samples fall in each {@link LATENCY_BUCKETS_MS} bucket (plus one overflow bucket). */\nfunction latencyHistogram(samples: readonly number[]): number[] {\n const counts = Array.from({ length: LATENCY_BUCKETS_MS.length + 1 }, () => 0)\n for (const ms of samples) {\n const i = LATENCY_BUCKETS_MS.findIndex((bound) => ms <= bound)\n counts[i === -1 ? LATENCY_BUCKETS_MS.length : i]!++\n }\n return counts\n}\n\nconst MAX_SAMPLES = 1000\nconst METHODS = new Set(['GET', 'HEAD', 'POST', 'PUT', 'DELETE', 'PATCH', 'OPTIONS', 'QUERY'])\n\n/**\n * Where every runtime publishes the matched route on the raw request — the\n * same `Symbol.for` slot `ctx.route` reads in `@forinda/kickjs`. Registry\n * symbol, so it is shared across module copies.\n */\nconst MATCHED_ROUTE_SLOT = Symbol.for('kick.route')\n\n/**\n * Whether the installed `@forinda/kickjs` reports every response to adapter\n * `onResponse` hooks. When it does, the dashboard's counters are fed from\n * there; on older releases (still supported peers) the adapter falls back to\n * its own middleware. Looked up, not imported by name, so older releases load.\n */\nconst FRAMEWORK_REPORTS_RESPONSES =\n typeof (kick as { reportResponse?: unknown }).reportResponse === 'function'\n\n/** Compute a percentile from a sorted array of numbers */\nfunction percentile(sorted: number[], p: number): number {\n if (sorted.length === 0) return 0\n const idx = Math.ceil(p * sorted.length) - 1\n return sorted[Math.max(0, idx)]\n}\n\n/** Compute p50, p95, p99 from a RouteStats samples buffer */\nfunction computePercentiles(stats: RouteStats): { p50: number; p95: number; p99: number } {\n const sorted = [...stats.samples].toSorted((a, b) => a - b)\n return {\n p50: percentile(sorted, 0.5),\n p95: percentile(sorted, 0.95),\n p99: percentile(sorted, 0.99),\n }\n}\n\n/**\n * Race a promise against a timeout. Clears the timer when the inner\n * promise wins so we don't leak handles every time a peer responds\n * quickly. Used to bound peer `onHealthCheck()` calls inside the\n * `/_debug/health` handler — one slow peer must not be able to stall\n * the dashboard request.\n */\nfunction raceWithTimeout<T>(promise: Promise<T>, ms: number, label: string): Promise<T> {\n let timer: ReturnType<typeof setTimeout> | undefined\n const timeout = new Promise<never>((_, reject) => {\n timer = setTimeout(() => reject(new Error(`${label} timed out after ${ms}ms`)), ms)\n })\n return Promise.race([promise, timeout]).finally(() => {\n if (timer) clearTimeout(timer)\n })\n}\n\n/**\n * Per-peer budget for `onHealthCheck()` inside `/_debug/health`.\n * Long enough for a typical DB ping or remote check, short enough that\n * one misbehaving adapter can't stall the dashboard endpoint.\n */\n/** How often `/stream` re-sends the counters even when nothing changed. */\nconst METRICS_TICK_MS = 5000\n\nconst PEER_HEALTHCHECK_TIMEOUT_MS = 1500\n\n/**\n * Module-scoped guard for `POST /_debug/memory/snapshot`. `writeHeapSnapshot()`\n * blocks the event loop for the duration of the capture (multi-second\n * for large heaps); two concurrent captures would compound the pause.\n * The endpoint returns 503 + a hint message when this flag is set so\n * the SPA can surface a \"snapshot already running\" warning rather than\n * silently queueing.\n *\n * Module-scoped (not per-adapter-instance) because writeHeapSnapshot\n * acts on the V8 process — multiple DevToolsAdapter instances would\n * still race at the v8 layer.\n */\nlet snapshotInProgress = false\n\nexport interface DevToolsOptions {\n /** Base path for debug endpoints (default: '/_debug') */\n basePath?: string\n /** Only enable when this is true (default: process.env.NODE_ENV !== 'production') */\n enabled?: boolean\n /**\n * Include environment variables (sanitized) at /_debug/config, with where\n * each declared variable's value comes from (default: false)\n */\n exposeConfig?: boolean\n /** Env var prefixes to expose (default: ['APP_', 'NODE_ENV']). Others are redacted. */\n configPrefixes?: string[]\n /** Callback when error rate exceeds threshold */\n onErrorRateExceeded?: (rate: number) => void\n /** Error rate threshold (default: 0.5 = 50%) */\n errorRateThreshold?: number\n /** Other adapters to discover stats from (e.g., WsAdapter) */\n adapters?: any[]\n\n /**\n * Secret token to guard DevTools access. When set, all requests must\n * include this token as `x-devtools-token` header or `?token=` query param.\n *\n * Auto-generated on startup if not provided. The token is logged to the console.\n * Set to `false` to disable the guard entirely (not recommended).\n *\n * @example\n * ```ts\n * DevToolsAdapter({ secret: getEnv('DEVTOOLS_SECRET') })\n * ```\n */\n secret?: string | false\n\n /**\n * Tuning for the runtime sampler that powers `/_debug/runtime` and\n * the upcoming Memory tab (architecture.md §23). Defaults to\n * 1-second polling with a 60-sample (~1 minute) ring buffer.\n *\n * Set `runtime.enabled = false` to skip starting the sampler — the\n * `/_debug/runtime` endpoint then returns 404 instead of an empty\n * snapshot, signalling deliberate opt-out rather than a startup race.\n */\n runtime?: {\n /** Whether to start the runtime sampler at all. Default: true. */\n enabled?: boolean\n /** Polling interval in milliseconds. Default: 1000. */\n intervalMs?: number\n /** Ring-buffer size — number of past samples retained. Default: 60. */\n bufferSize?: number\n }\n /**\n * How many recent requests the Requests tab keeps (method, path, status,\n * duration, error). Default: 200. `0` keeps none.\n */\n requestLog?: number\n}\n\n/**\n * Public reactive surface exposed by a DevToolsAdapter instance —\n * counters, computed metrics, and the route latency map. Tests and\n * peer adapters consume these directly to drive their own behavior.\n */\nexport interface DevToolsAdapterExtensions {\n /** Total requests received. */\n readonly requestCount: Ref<number>\n /** Total responses with status >= 500. */\n readonly errorCount: Ref<number>\n /** Total responses with status >= 400 and < 500. */\n readonly clientErrorCount: Ref<number>\n /** Server start time. */\n readonly startedAt: Ref<number>\n /** Computed error rate (server errors / total requests). */\n readonly errorRate: ComputedRef<number>\n /** Computed uptime in seconds. */\n readonly uptimeSeconds: ComputedRef<number>\n /** Per-route latency tracking. */\n readonly routeLatency: Record<string, RouteStats>\n /**\n * Tier-1 runtime sampler from `@forinda/kickjs-devtools-kit`. `null`\n * when `runtime.enabled` is `false`. Tests can `latest()` and\n * `history()` directly without going through the HTTP endpoint.\n */\n readonly runtimeSampler: RuntimeSampler | null\n /**\n * Memory analyzer fed by the runtime sampler. `null` when the\n * sampler is disabled. Use `analyzer.health(sampler.history())` to\n * compose a composite memory-health snapshot.\n */\n readonly memoryAnalyzer: MemoryAnalyzer | null\n}\n\n/**\n * DevToolsAdapter — Vue-style reactive introspection for KickJS applications.\n *\n * Exposes debug endpoints powered by reactive state (ref, computed, watch):\n * - `GET /_debug/routes` — all registered routes with middleware\n * - `GET /_debug/container` — DI registry with scopes and instantiation status\n * - `GET /_debug/metrics` — live request/error counts, error rate, uptime\n * - `GET /_debug/health` — deep health check with adapter status\n * - `GET /_debug/config` — sanitized environment variables (opt-in)\n * - `GET /_debug/state` — full reactive state snapshot\n *\n * @example\n * ```ts\n * import { DevToolsAdapter } from '@forinda/kickjs-devtools'\n *\n * bootstrap({\n * modules: [UserModule],\n * adapters: [\n * DevToolsAdapter({\n * enabled: process.env.NODE_ENV !== 'production',\n * exposeConfig: true,\n * configPrefixes: ['APP_', 'DATABASE_'],\n * }),\n * ],\n * })\n * ```\n */\nexport const DevToolsAdapter = defineAdapter<DevToolsOptions, DevToolsAdapterExtensions>({\n name: 'DevToolsAdapter',\n defaults: {\n basePath: '/_debug',\n errorRateThreshold: 0.5,\n exposeConfig: false,\n configPrefixes: ['APP_', 'NODE_ENV'],\n },\n build: (options) => {\n const basePath = options.basePath!\n const enabled = options.enabled ?? process.env.NODE_ENV !== 'production'\n const exposeConfig = options.exposeConfig!\n const configPrefixes = options.configPrefixes!\n const errorRateThreshold = options.errorRateThreshold!\n const peerAdapters = options.adapters ?? []\n const requestLog = new RequestLog(options.requestLog ?? 200)\n // The dashboard's own polling would otherwise push the app's requests out.\n const logRequest = (info: Parameters<RequestLog['record']>[0]): void => {\n if (info.path !== basePath && !info.path.startsWith(`${basePath}/`)) requestLog.record(info)\n }\n\n // Secret token guard\n let secret: string | false\n if (options.secret === false) {\n secret = false\n } else if (options.secret) {\n secret = options.secret\n } else {\n secret = randomBytes(16).toString('hex')\n }\n\n // ── Runtime sampler + memory analyzer (kit) ────────────────────\n // Lazily instantiated so an adopter that disables runtime monitoring\n // doesn't pay the perf_hooks histogram cost. Lifecycle: start in\n // `beforeMount`, stop in `shutdown`.\n const runtimeEnabled = options.runtime?.enabled ?? true\n const runtimeSampler = runtimeEnabled\n ? new RuntimeSampler({\n intervalMs: options.runtime?.intervalMs ?? 1000,\n bufferSize: options.runtime?.bufferSize ?? 60,\n })\n : null\n const memoryAnalyzer = runtimeEnabled ? new MemoryAnalyzer() : null\n\n // ── Server-side event bus ──────────────────────────────────────\n // Lazily resolved in `beforeMount` — when the devtools adapter is\n // rebuilt during HMR, the same `Container` may already hold a bus\n // from a previous mount with live WebSocket clients + in-flight\n // subscriptions. Creating a fresh bus on every rebuild would\n // silently swap the registered instance, leaving plugins that\n // already captured the old reference publishing into a dead bus.\n // Using `container.has(DEVTOOLS_BUS)` to reuse instead keeps event\n // delivery stable across rebuilds.\n //\n // When the devtools adapter is disabled (`enabled === false`) we\n // register a no-op stub instead so adopters who Inject(DEVTOOLS_BUS)\n // under @Optional() get a callable instance rather than undefined.\n let bus: ServerBus | null = null\n\n const ensureBus = (container: Container): ServerBus => {\n if (container.has(DEVTOOLS_BUS)) {\n // The token is typed `KickEventBus` but the registered instance\n // is the wider `ServerBus` (KickEventBus + attachUpgrade /\n // close / clientCount). Explicit generic on resolve avoids the\n // structural-cast — Container.resolve's last overload accepts\n // an explicit T and returns it.\n bus = container.resolve<ServerBus>(DEVTOOLS_BUS)\n return bus\n }\n bus = enabled\n ? createServerBus({\n wsPath: `${basePath}/_bus`,\n secret: secret === false ? false : secret,\n })\n : ({\n on: () => () => {},\n onAny: () => () => {},\n emit: () => {},\n attachUpgrade: () => {},\n close: () => {},\n clientCount: () => 0,\n } as ServerBus)\n container.registerInstance(DEVTOOLS_BUS, bus)\n return bus\n }\n\n // ── Reactive state ─────────────────────────────────────────────\n const requestCount = ref(0)\n const errorCount = ref(0)\n const clientErrorCount = ref(0)\n // App mount timestamp — kept for reference, but uptime is derived from the\n // node process (see below) so it survives HMR rebuilds.\n const startedAt = ref(Date.now())\n const routeLatency = reactive<Record<string, RouteStats>>({})\n\n const errorRate = computed(() =>\n requestCount.value > 0 ? errorCount.value / requestCount.value : 0,\n )\n\n // Uptime is the NODE PROCESS uptime, not \"time since beforeMount\". In dev,\n // `beforeMount` re-runs on every HMR rebuild / SSR re-bootstrap, which used\n // to reset `startedAt` and pin uptime near 0s. `process.uptime()` is\n // monotonic from process start, so it reports the real server uptime across\n // reloads. Not a `computed()`: the process clock is not reactive, so a\n // computed would cache its first read and report a frozen uptime forever.\n const uptimeSeconds: ComputedRef<number> = {\n get value() {\n return Math.floor(process.uptime())\n },\n toJSON() {\n return this.value\n },\n dispose() {},\n }\n\n // ── Internal mutable state ─────────────────────────────────────\n let routes: RouteInfo[] = []\n let container: Container | null = null\n let appRef: any = null\n\n /**\n * One finished response into the reactive state. Latency is keyed by the\n * full route pattern (`GET /api/v1/users/:id`), so two modules' `/:id`\n * routes no longer share a bucket; unmatched requests (404 probing)\n * collapse into one `<unmatched>` bucket per method instead of one entry\n * per raw URL, which grew the map without bound.\n */\n const recordResponse = (\n method: string,\n route: string | undefined,\n status: number,\n elapsedMs: number,\n ): void => {\n requestCount.value++\n if (status >= 500) errorCount.value++\n else if (status >= 400) clientErrorCount.value++\n\n // Known methods only: a fetch-based host (Bun, Deno) passes any token.\n const verb = METHODS.has(method.toUpperCase()) ? method.toUpperCase() : 'OTHER'\n const routeKey = `${verb} ${route ?? '<unmatched>'}`\n if (!routeLatency[routeKey]) {\n routeLatency[routeKey] = {\n count: 0,\n totalMs: 0,\n minMs: Infinity,\n maxMs: 0,\n serverErrors: 0,\n clientErrors: 0,\n samples: [],\n }\n }\n const stats = routeLatency[routeKey]\n stats.count++\n if (status >= 500) stats.serverErrors++\n else if (status >= 400) stats.clientErrors++\n stats.totalMs += elapsedMs\n stats.minMs = Math.min(stats.minMs, elapsedMs)\n stats.maxMs = Math.max(stats.maxMs, elapsedMs)\n stats.samples.push(elapsedMs)\n if (stats.samples.length > MAX_SAMPLES) stats.samples.shift()\n }\n const adapterStatuses: Record<string, string> = {}\n let stopErrorWatch: (() => void) | null = null\n\n // Watch error rate — log warnings when elevated\n if (options.onErrorRateExceeded) {\n const callback = options.onErrorRateExceeded\n stopErrorWatch = watch(errorRate, (rate) => {\n if (rate > errorRateThreshold) {\n callback(rate)\n }\n })\n } else {\n stopErrorWatch = watch(errorRate, (rate) => {\n if (rate > errorRateThreshold) {\n log.warn(`Error rate elevated: ${(rate * 100).toFixed(1)}%`)\n }\n })\n }\n\n /**\n * Find the dashboard's public directory. Prefers the new Solid SPA\n * (public/spa/, shipped from PR 4 of §23); falls back to the legacy\n * Vue+Tailwind dashboard (public/devtools/) for environments that\n * haven't run `pnpm --filter @forinda/kickjs-devtools build:spa`.\n */\n const resolvePublicDir = (): string | null => {\n const thisDir = dirname(fileURLToPath(import.meta.url))\n const candidates = [\n join(thisDir, '..', 'public', 'spa'), // dist/ -> public/spa\n join(thisDir, '..', '..', 'public', 'spa'), // src/ -> public/spa\n join(thisDir, '..', 'public', 'devtools'), // legacy Vue dashboard\n join(thisDir, '..', '..', 'public', 'devtools'), // legacy from src/\n ]\n for (const dir of candidates) {\n if (existsSync(join(dir, 'index.html'))) return dir\n }\n return null\n }\n\n /**\n * Resolve peer adapters at request time. Prefers live adapters from the\n * Application registry (survives HMR rebuild) and falls back to the\n * constructor-provided refs.\n */\n const getPeerAdapters = (): any[] => {\n const kickApp = appRef?.__kickApp\n if (kickApp && typeof kickApp.getAdapters === 'function') {\n return kickApp.getAdapters()\n }\n return peerAdapters\n }\n\n /**\n * Active HTTP runtime identity + capabilities (`express` | `fastify` | `h3`),\n * read from the Application via `getActiveRuntime()`. Returns `null` when the\n * framework is older than the getter (graceful for mixed versions). Lets the\n * dashboard show which engine the app is running on.\n */\n const readActiveRuntime = (): {\n name: string\n capabilities: Record<string, boolean>\n } | null => {\n const kickApp = appRef?.__kickApp\n if (kickApp && typeof kickApp.getActiveRuntime === 'function') {\n try {\n return kickApp.getActiveRuntime()\n } catch {\n return null\n }\n }\n return null\n }\n\n return {\n // ── Extensions (TExtra) ───────────────────────────────────────\n requestCount,\n errorCount,\n clientErrorCount,\n startedAt,\n errorRate,\n uptimeSeconds,\n routeLatency,\n runtimeSampler,\n memoryAnalyzer,\n\n // ── DevTools introspection (architecture.md §23) ─────────────\n // Cheap snapshot — counters + flags only. Anything that requires\n // a container walk or DB hit belongs in the per-endpoint RPCs\n // (`/_debug/container`, `/_debug/graph`), not here.\n introspect(): IntrospectionSnapshot {\n return {\n protocolVersion: PROTOCOL_VERSION,\n name: 'DevToolsAdapter',\n kind: 'adapter',\n state: {\n basePath,\n enabled,\n runtimeEnabled,\n secret: secret === false ? false : 'present',\n },\n metrics: {\n requestCount: requestCount.value,\n serverErrors: errorCount.value,\n clientErrors: clientErrorCount.value,\n uptimeSeconds: uptimeSeconds.value,\n routesTracked: routes.length,\n },\n }\n },\n\n // ── Lifecycle ─────────────────────────────────────────────────\n\n beforeMount({ app, http, container: containerArg }) {\n if (!enabled) return\n\n appRef = app\n container = containerArg\n // Lazy resolve — reuses the existing bus when the same\n // container is reused across HMR rebuilds, otherwise creates\n // and registers a fresh instance. See `ensureBus` for why.\n // Also published process-wide, so a kick/db client created at import\n // time — before any container exists — reports its queries here.\n setActiveDevtoolsBus(ensureBus(containerArg))\n startedAt.value = Date.now()\n // Clear routes on rebuild/restart to prevent HMR duplication\n routes = []\n adapterStatuses['DevToolsAdapter'] = 'running'\n\n // Start the runtime sampler + memory analyzer here (rather than\n // at construction) so test harnesses that build the adapter but\n // never mount it don't leak interval timers.\n runtimeSampler?.start()\n memoryAnalyzer?.start()\n\n // Every dashboard route registers through the engine-agnostic HTTP\n // facade (`ctx.http`) and answers through `RequestContext`, so the\n // dashboard runs under Express, Fastify, and h3 alike. The dashboard\n // root (`'/'`) maps to `basePath` itself.\n //\n // The token guard runs inside each route rather than as a path-scoped\n // connect middleware: connect middleware sees the engine's raw\n // request, whose `path` / `query` differ per engine (and Express\n // strips the mount prefix while the others don't).\n const joinBase = (p: string): string => (p === '/' ? basePath : `${basePath}${p}`)\n const authorized = (ctx: RequestContext): boolean => {\n if (secret === false) return true\n const provided = ctx.headers['x-devtools-token'] ?? ctx.query?.token\n if (provided === secret) return true\n ctx.json({ error: 'Forbidden — invalid or missing devtools token' }, 403)\n return false\n }\n const guarded =\n (handler: (ctx: RequestContext) => unknown) =>\n (ctx: RequestContext): unknown =>\n authorized(ctx) ? handler(ctx) : undefined\n const router = {\n get: (path: string, handler: (ctx: RequestContext) => unknown): void =>\n http.route('GET', joinBase(path), guarded(handler)),\n post: (path: string, handler: (ctx: RequestContext) => unknown): void =>\n http.route('POST', joinBase(path), guarded(handler)),\n }\n\n router.get('/routes', (ctx: RequestContext) => {\n // Each route's context-contributor chain, from what the app actually\n // mounted — decorators, module, adapter and global, in execution order.\n const kickApp = appRef?.__kickApp as TopologyApplicationLike | undefined\n const mounted = kickApp?.getRoutes?.()\n if (!mounted) {\n ctx.json({ routes })\n return\n }\n const chains = new Map(\n mounted.map((r) => [\n `${r.method.toUpperCase()} ${r.controller}.${r.handler}`,\n r.contributors,\n ]),\n )\n ctx.json({\n routes: routes.map((r) => ({\n ...r,\n contributors: chains.get(`${r.method} ${r.controller}.${r.handler}`) ?? [],\n })),\n })\n })\n\n // Where a route's handler lives — \"open in editor\" in the dashboard\n // and the VS Code extension.\n router.get('/source', (ctx: RequestContext) => {\n const { controller, handler } = ctx.query as Record<string, string | undefined>\n const known = routes.some((r) => r.controller === controller && r.handler === handler)\n const found =\n known && controller && handler\n ? locateHandler(process.cwd(), controller, handler)\n : undefined\n if (!found) {\n ctx.json({ error: 'Handler source not found' }, 404)\n return\n }\n ctx.json(found)\n })\n\n router.get('/container', (ctx: RequestContext) => {\n // `__hmr__` entries are the dev server's shadow registrations — the\n // topology and graph endpoints leave them out too.\n const registrations = (container?.getRegistrations() ?? []).filter(\n (r) => !r.token.startsWith('__hmr__'),\n )\n ctx.json({ registrations, count: registrations.length })\n })\n\n // Recent requests, oldest first. `?since=<seq>` returns only newer ones,\n // so the Requests tab polls cheaply.\n router.get('/requests', (ctx: RequestContext) => {\n const since = Number((ctx.query as Record<string, unknown> | undefined)?.since ?? 0)\n ctx.json({\n requests: requestLog.after(Number.isFinite(since) ? since : 0),\n // A client holding a higher `seq` than this knows the app restarted.\n latest: requestLog.latest(),\n })\n })\n\n router.get('/metrics', (ctx: RequestContext) => {\n // Build latency with percentiles, omitting raw samples from response\n const latency: Record<string, any> = {}\n for (const [key, stats] of Object.entries(routeLatency)) {\n const { samples, ...rest } = stats\n latency[key] = {\n ...rest,\n ...computePercentiles(stats),\n histogram: latencyHistogram(samples),\n }\n }\n ctx.json({\n requests: requestCount.value,\n serverErrors: errorCount.value,\n clientErrors: clientErrorCount.value,\n errorRate: errorRate.value,\n uptimeSeconds: uptimeSeconds.value,\n startedAt: new Date(startedAt.value).toISOString(),\n routeLatency: latency,\n latencyBucketsMs: LATENCY_BUCKETS_MS,\n })\n })\n\n router.get('/health', async (ctx: RequestContext) => {\n const healthy = errorRate.value < errorRateThreshold\n const status = healthy ? 'healthy' : 'degraded'\n\n // Refresh peer statuses from `onHealthCheck()` when present.\n // The static `adapterStatuses` dict (seeded in `afterStart`)\n // proves a peer was mounted; calling `onHealthCheck()` here\n // upgrades that to a live `up`/`down`/`degraded` reading so\n // the Overview > Health card reflects actual adapter state,\n // not just \"registered at boot\".\n //\n // Each peer call is bounded by `PEER_HEALTHCHECK_TIMEOUT_MS` —\n // an adapter whose check hangs (network DB ping, remote\n // service, etc.) must not be able to stall the entire\n // dashboard request. Timed-out peers fall through to the\n // catch arm and are reported as `down`.\n const live: Record<string, string> = { ...adapterStatuses }\n await Promise.all(\n getPeerAdapters().map(async (peer) => {\n if (typeof peer?.onHealthCheck !== 'function') return\n const name: unknown = peer?.name\n if (typeof name !== 'string' || name === 'DevToolsAdapter') return\n try {\n const result = await raceWithTimeout(\n Promise.resolve(peer.onHealthCheck()),\n PEER_HEALTHCHECK_TIMEOUT_MS,\n `onHealthCheck(${name})`,\n )\n if (result && typeof result.status === 'string') {\n live[name] = result.status\n }\n } catch {\n live[name] = 'down'\n }\n }),\n )\n\n ctx.json(\n {\n status,\n errorRate: errorRate.value,\n uptime: uptimeSeconds.value,\n runtime: readActiveRuntime(),\n adapters: live,\n },\n healthy ? 200 : 503,\n )\n })\n\n router.get('/state', (ctx: RequestContext) => {\n const wsAdapter = getPeerAdapters().find(\n (a) => a.name === 'WsAdapter' && typeof a.getStats === 'function',\n )\n ctx.json({\n reactive: {\n requestCount: requestCount.value,\n errorCount: errorCount.value,\n clientErrorCount: clientErrorCount.value,\n errorRate: errorRate.value,\n uptimeSeconds: uptimeSeconds.value,\n startedAt: new Date(startedAt.value).toISOString(),\n },\n routes: routes.length,\n container: container?.getRegistrations().length ?? 0,\n routeLatency,\n ...(wsAdapter ? { ws: wsAdapter.getStats() } : {}),\n })\n })\n\n // ── Tier-1 runtime monitoring (architecture.md §23) ─────────\n // Returns the latest RuntimeSnapshot + a composite MemoryHealth\n // computed from the sampler's ring buffer. Returns 404 when the\n // sampler is disabled so adopters can distinguish \"off\" from\n // \"starting up\".\n router.get('/runtime', (ctx: RequestContext) => {\n if (!runtimeSampler || !memoryAnalyzer) {\n ctx.json({ error: 'runtime sampler disabled — set runtime.enabled = true' }, 404)\n return\n }\n const latest = runtimeSampler.latest()\n const history = runtimeSampler.history()\n const health = memoryAnalyzer.health(history)\n ctx.json({\n protocolVersion: PROTOCOL_VERSION,\n // Identity of the process these stats describe — every memory / CPU /\n // event-loop number below is for THIS Node process (the one running\n // your KickJS app), not the OS or any child. Lets the panel label it\n // unambiguously (\"Node v22 · pid 12345 · linux/x64 · engine: fastify\").\n process: {\n nodeVersion: process.version,\n pid: process.pid,\n platform: process.platform,\n arch: process.arch,\n runtime: readActiveRuntime(),\n },\n latest,\n history,\n health,\n })\n })\n\n // ── SSE: live runtime snapshots (architecture.md §23) ───────\n // Pushes the latest RuntimeSnapshot every `intervalMs`. SSE\n // (not WebSocket) avoids the http.Server `upgrade`-event\n // coordination cost — see devtools-flows.md §3 for rationale.\n router.get('/runtime/stream', (ctx: RequestContext) => {\n if (!runtimeSampler) {\n ctx.json({ error: 'runtime sampler disabled' }, 404)\n return\n }\n const sse = ctx.sse()\n const intervalMs = options.runtime?.intervalMs ?? 1000\n const tick = (): void => {\n const snap = runtimeSampler.latest()\n if (snap) sse.send(snap)\n }\n tick()\n const interval = setInterval(tick, intervalMs)\n const heartbeat = setInterval(() => sse.comment('heartbeat'), 30_000)\n sse.onClose(() => {\n clearInterval(interval)\n clearInterval(heartbeat)\n })\n })\n\n // ── SSE: memory health composite (architecture.md §23) ──────\n router.get('/memory/stream', (ctx: RequestContext) => {\n if (!runtimeSampler || !memoryAnalyzer) {\n ctx.json({ error: 'runtime sampler disabled' }, 404)\n return\n }\n const sse = ctx.sse()\n const intervalMs = options.runtime?.intervalMs ?? 1000\n const tick = (): void => {\n const snap = runtimeSampler.latest()\n if (!snap) return\n const health = memoryAnalyzer.health(runtimeSampler.history())\n sse.send({ snapshot: snap, health })\n }\n tick()\n const interval = setInterval(tick, intervalMs)\n const heartbeat = setInterval(() => sse.comment('heartbeat'), 30_000)\n sse.onClose(() => {\n clearInterval(interval)\n clearInterval(heartbeat)\n })\n })\n\n // ── Heap snapshot — Tier 3 monitoring (architecture.md §23) ─\n // POST /_debug/memory/snapshot — captures a V8 heap snapshot\n // and streams the resulting .heapsnapshot file back as\n // application/json with Content-Disposition. Snapshot capture\n // blocks the event loop while writing (multi-second pause for\n // large heaps), so the endpoint enforces single-flight via a\n // module-scoped flag — concurrent calls return 503.\n //\n // The file streams through `ctx.sendResponse` (engine-neutral, with\n // backpressure — never buffered whole) and the temp file is deleted\n // once the response completes, fails, or the client disconnects.\n // Worst case if the process dies mid-snapshot is a stranded file in\n // `os.tmpdir()`, which the OS cleans up on next boot.\n router.post('/memory/snapshot', async (ctx: RequestContext) => {\n if (snapshotInProgress) {\n ctx.json({ error: 'heap snapshot already in progress — wait for it to complete' }, 503)\n return\n }\n snapshotInProgress = true\n const startedAt = Date.now()\n const filename = `kickjs-heap-${new Date().toISOString().replace(/[:.]/g, '-')}.heapsnapshot`\n const targetPath = join(tmpdir(), filename)\n\n let written: string\n try {\n // writeHeapSnapshot is synchronous — the event loop is\n // blocked for the duration. There's no async variant in\n // the v8 module today; if one ships, swap here.\n written = writeHeapSnapshot(targetPath)\n } catch (err) {\n snapshotInProgress = false\n log.error({ err }, 'heap snapshot capture failed')\n ctx.json({ error: 'snapshot capture failed', message: (err as Error).message }, 500)\n return\n }\n\n let size = 0\n try {\n size = statSync(written).size\n } catch {\n /* size header is best-effort */\n }\n const elapsedMs = Date.now() - startedAt\n log.info(\n `Heap snapshot captured: ${written} (${(size / 1024 / 1024).toFixed(1)} MiB in ${elapsedMs}ms)`,\n )\n\n const headers: Record<string, string> = {\n 'Content-Type': 'application/json',\n 'Content-Disposition': `attachment; filename=\"${filename}\"`,\n 'X-Snapshot-Capture-Ms': String(elapsedMs),\n }\n if (size > 0) headers['Content-Length'] = String(size)\n\n const stream = createReadStream(written)\n try {\n await ctx.sendResponse(\n new Response(Readable.toWeb(stream) as ReadableStream, { headers }),\n )\n } catch (err) {\n log.error({ err }, 'heap snapshot stream failed')\n } finally {\n // Also covers a client that disconnected mid-download:\n // sendResponse stops reading, so close the file here.\n stream.destroy()\n unlink(written, (err) => {\n if (err) log.warn(`Failed to delete heap snapshot ${written}: ${err.message}`)\n snapshotInProgress = false\n })\n }\n })\n\n // ── Force GC — Tier 3 monitoring (architecture.md §23) ──────\n // POST /_debug/memory/gc — calls `global.gc()` if Node was\n // started with --expose-gc, returning before/after heap size\n // so the panel can show how much was reclaimed. Useful for\n // confirming \"is this growth real or just delayed GC?\".\n //\n // Returns 412 Precondition Failed (with hint) when the flag\n // wasn't set, so the SPA can show \"Run with --expose-gc to\n // enable this button\" instead of a generic 5xx.\n router.post('/memory/gc', (ctx: RequestContext) => {\n const gc = (globalThis as { gc?: () => void }).gc\n if (typeof gc !== 'function') {\n ctx.json(\n {\n error:\n 'global.gc unavailable — start Node with --expose-gc to enable this endpoint',\n },\n 412,\n )\n return\n }\n const before = process.memoryUsage().heapUsed\n const startedAt = Date.now()\n try {\n gc()\n } catch (err) {\n ctx.json(\n {\n error: 'forced GC failed',\n message: err instanceof Error ? err.message : String(err),\n },\n 500,\n )\n return\n }\n const after = process.memoryUsage().heapUsed\n const reclaimedBytes = Math.max(0, before - after)\n const elapsedMs = Date.now() - startedAt\n log.info(\n `Forced GC reclaimed ${(reclaimedBytes / 1024 / 1024).toFixed(2)} MiB in ${elapsedMs}ms`,\n )\n ctx.json({ before, after, reclaimedBytes, elapsedMs })\n })\n\n // ── Custom-tab discovery (architecture.md §23) ──────────────\n // Walks every plugin/adapter `devtoolsTabs?()` contribution\n // and serves the deduped + validated list. The SPA fetches\n // this once at boot to render dynamic tabs after the four\n // built-ins.\n // Adapter conditions — which adapters a `when` turned off, and why.\n router.get('/conditions', (ctx: RequestContext) => {\n const kickApp = appRef?.__kickApp as TopologyApplicationLike | undefined\n const report = kickApp?.getConditionReport?.()\n if (!report) {\n ctx.json({ error: 'condition report unavailable' }, 503)\n return\n }\n ctx.json({ adapters: report })\n })\n\n // Startup timeline — each phase and adapter hook of the last boot.\n router.get('/startup', (ctx: RequestContext) => {\n const kickApp = appRef?.__kickApp as TopologyApplicationLike | undefined\n const timeline = kickApp?.getStartupTimeline?.()\n if (!timeline) {\n ctx.json({ error: 'startup timeline unavailable' }, 503)\n return\n }\n ctx.json(timeline)\n })\n\n router.get('/tabs', (ctx: RequestContext) => {\n const kickApp = appRef?.__kickApp as TopologyApplicationLike | undefined\n if (!kickApp) {\n ctx.json({ error: 'tabs unavailable — application surface not exposed' }, 503)\n return\n }\n ctx.json(collectDevtoolsTabs(kickApp))\n })\n\n // A `launch` tab's button — runs the action's `run()` on the server.\n router.post('/tabs/run', async (ctx: RequestContext) => {\n const kickApp = appRef?.__kickApp as TopologyApplicationLike | undefined\n if (!kickApp) {\n ctx.json({ error: 'tabs unavailable — application surface not exposed' }, 503)\n return\n }\n const { tab, action } = (ctx.query ?? {}) as { tab?: string; action?: string }\n const { status, body } = await runTabAction(\n kickApp,\n String(tab ?? ''),\n String(action ?? ''),\n )\n ctx.json(body, status)\n })\n\n // ── Jobs: browse and manage via any adapter's / plugin's jobInspector() ──\n const inspectors = () => {\n const kickApp = appRef?.__kickApp as TopologyApplicationLike | undefined\n return kickApp ? findJobInspectors(kickApp) : new Map()\n }\n const query = (ctx: RequestContext) => (ctx.query ?? {}) as Record<string, unknown>\n router.get('/jobs', async (ctx: RequestContext) => {\n const r = await listJobSources(inspectors())\n ctx.json(r.body, r.status)\n })\n router.get('/jobs/list', async (ctx: RequestContext) => {\n const r = await listJobs(inspectors(), query(ctx))\n ctx.json(r.body, r.status)\n })\n router.get('/jobs/job', async (ctx: RequestContext) => {\n const r = await getJob(inspectors(), query(ctx))\n ctx.json(r.body, r.status)\n })\n router.post('/jobs/action', async (ctx: RequestContext) => {\n const r = await runJobAction(inspectors(), query(ctx))\n ctx.json(r.body, r.status)\n })\n\n // ── @Cron jobs — whatever schedules them ────────────────────\n router.get('/cron', (ctx: RequestContext) => {\n ctx.json({ jobs: container ? cronSnapshot(container) : [] })\n })\n router.post('/cron/run', (ctx: RequestContext) => {\n const name = query(ctx).name\n if (!container || typeof name !== 'string') {\n ctx.json({ error: 'name is required' }, 400)\n return\n }\n const r = runCronJobNow(container, name)\n ctx.json(r.body, r.status)\n })\n\n // ── Topology RPC (architecture.md §23) ──────────────────────\n // Aggregates plugins + adapters + contributors + DI tokens\n // into one snapshot; calls each primitive's introspect() in\n // parallel with a per-call timeout so a misbehaving adapter\n // can't block the endpoint. Errors are collected, not thrown.\n router.get('/topology', async (ctx: RequestContext) => {\n if (!container) {\n ctx.json({ error: 'topology unavailable — container not bound yet' }, 503)\n return\n }\n const kickApp = appRef?.__kickApp as TopologyApplicationLike | undefined\n if (!kickApp) {\n ctx.json({ error: 'topology unavailable — application surface not exposed' }, 503)\n return\n }\n try {\n const snapshot = await collectTopologySnapshot({ app: kickApp, container })\n ctx.json(snapshot)\n } catch (err) {\n const message = err instanceof Error ? err.message : String(err)\n ctx.json({ error: 'topology collection failed', message }, 500)\n }\n })\n\n router.get('/ws', (ctx: RequestContext) => {\n const wsAdapter = getPeerAdapters().find(\n (a) => a.name === 'WsAdapter' && typeof a.getStats === 'function',\n )\n if (!wsAdapter) {\n ctx.json({ enabled: false, message: 'WsAdapter not found' })\n return\n }\n ctx.json({ enabled: true, ...wsAdapter.getStats() })\n })\n\n router.get('/queues', async (ctx: RequestContext) => {\n const queueAdapter = getPeerAdapters().find(\n (a) => a.name === 'QueueAdapter' && typeof a.getQueueNames === 'function',\n )\n if (!queueAdapter) {\n ctx.json({ enabled: false, message: 'QueueAdapter not found' })\n return\n }\n try {\n const names: string[] = queueAdapter.getQueueNames?.() ?? []\n const queues: any[] = []\n for (const name of names) {\n const stats = await queueAdapter.getQueueStats?.(name)\n queues.push({ name, ...stats })\n }\n ctx.json({ enabled: true, queues })\n } catch {\n ctx.json({ enabled: true, queues: [], error: 'Failed to fetch queue stats' })\n }\n })\n\n // ── Dependency graph ────────────────────────────────────────\n router.get('/graph', (ctx: RequestContext) => {\n const registrations = container?.getRegistrations() ?? []\n const nodes = registrations\n .filter((r) => !r.token.startsWith('__hmr__'))\n .map((r) => ({\n id: r.token,\n kind: r.kind,\n scope: r.scope,\n resolveCount: r.resolveCount,\n }))\n const nodeIds = new Set(nodes.map((n) => n.id))\n const edges: Array<{ from: string; to: string }> = []\n for (const r of registrations) {\n if (r.token.startsWith('__hmr__')) continue\n for (const dep of r.dependencies) {\n if (nodeIds.has(dep)) {\n edges.push({ from: r.token, to: dep })\n }\n }\n }\n ctx.json({ nodes, edges })\n })\n\n // ── SSE stream for real-time updates ────────────────────────\n router.get('/stream', (ctx: RequestContext) => {\n const sse = ctx.sse()\n\n const sendMetrics = () => {\n sse.send({\n type: 'metrics',\n requestCount: requestCount.value,\n errorCount: errorCount.value,\n clientErrorCount: clientErrorCount.value,\n errorRate: errorRate.value,\n uptimeSeconds: uptimeSeconds.value,\n })\n }\n sendMetrics()\n\n const unsubContainer = container?.onChange?.((changes) => {\n sse.send({ type: 'container', changes, timestamp: Date.now() })\n sendMetrics()\n })\n\n const stopRequestWatch = watch(requestCount, () => sendMetrics())\n const stopErrWatch = watch(errorCount, () => sendMetrics())\n\n const heartbeat = setInterval(() => sse.comment('heartbeat'), 30000)\n // An idle app changes no counter, so without this tick the\n // dashboard's uptime and \"Updated\" time would freeze.\n const tick = setInterval(sendMetrics, METRICS_TICK_MS)\n\n sse.onClose(() => {\n unsubContainer?.()\n stopRequestWatch()\n stopErrWatch()\n clearInterval(heartbeat)\n clearInterval(tick)\n })\n })\n\n // Where each declared variable's value comes from: env, a `_FILE`, the\n // schema default, `provide()`, or unset. Names only, never values, so\n // it is served without `exposeConfig`.\n router.get('/config/origins', (ctx: RequestContext) => {\n const app = appRef?.__kickApp as { configOrigins?: () => unknown } | undefined\n ctx.json(app?.configOrigins?.() ?? { env: [], sections: [] })\n })\n\n if (exposeConfig) {\n router.get('/config', (ctx: RequestContext) => {\n const config: Record<string, string> = {}\n for (const [key, value] of Object.entries(process.env)) {\n if (value === undefined) continue\n const allowed = configPrefixes.some((prefix) => key.startsWith(prefix))\n config[key] = allowed ? value : '[REDACTED]'\n }\n // Where each declared variable's value comes from — names only.\n const origins = (\n appRef?.__kickApp as { configOrigins?: () => unknown } | undefined\n )?.configOrigins?.()\n ctx.json({ config, origins })\n })\n }\n\n // Dashboard UI — Vue + Tailwind from public/devtools directory\n const publicDir = resolvePublicDir()\n // The page itself is not token-guarded — it loads first and sends the\n // token on its API calls — unless a `?token=` is given, which must match.\n const dashboard =\n (html: string) =>\n (ctx: RequestContext): unknown =>\n ctx.query?.token === undefined || authorized(ctx) ? ctx.html(html) : undefined\n if (publicDir) {\n // Serve only the SPA's `assets/` statically, so the static server can\n // never answer the dashboard root with the raw `index.html` — the\n // route below owns it and injects `data-base`. The legacy Vue\n // dashboard keeps its files at the top level.\n const assetsDir = join(publicDir, 'assets')\n if (existsSync(assetsDir)) http.serveStatic(`${basePath}/assets`, assetsDir)\n else http.serveStatic(basePath, publicDir)\n\n const indexHtml = readFileSync(join(publicDir, 'index.html'), 'utf-8')\n http.route(\n 'GET',\n basePath,\n dashboard(indexHtml.replace('<body', `<body data-base=\"${basePath}\"`)),\n )\n } else {\n http.route('GET', basePath, dashboard('<h1>DevTools: public directory not found</h1>'))\n }\n\n if (secret) {\n log.info(`DevTools mounted at ${basePath} [token: ${secret}]`)\n log.info(`Access: ${basePath}?token=${secret}`)\n } else {\n log.info(`DevTools mounted at ${basePath} [no guard]`)\n }\n },\n\n /**\n * Feeds the reactive counters (`requestCount`, `errorCount`,\n * `routeLatency`, …) from the framework's response funnel — one\n * listener for every observer instead of one middleware per adapter.\n */\n onResponse(info) {\n if (!enabled) return\n recordResponse(info.method, info.route, info.status, info.durationMs)\n logRequest(info)\n },\n\n /** Attaches a failed request's error to its Requests-tab entry. */\n onError(err, info) {\n if (!enabled || info.source !== 'request') return\n requestLog.recordError(info.requestId, err)\n },\n\n middleware(): AdapterMiddleware[] {\n // The framework already reports every response to `onResponse`.\n if (!enabled || FRAMEWORK_REPORTS_RESPONSES) return []\n\n return [\n {\n // Older kickjs: time the response ourselves. Connect-style, so it\n // sees the engine's raw Node request/response on every runtime.\n handler: (req: IncomingMessage, res: ServerResponse, next: () => void) => {\n const start = Date.now()\n res.on('finish', () => {\n const matched = (req as unknown as Record<symbol, MatchedRoute | undefined>)[\n MATCHED_ROUTE_SLOT\n ]\n const route = matched?.pattern ?? matched?.path\n const durationMs = Date.now() - start\n recordResponse(req.method ?? 'GET', route, res.statusCode, durationMs)\n logRequest({\n method: req.method ?? 'GET',\n path: (req.url ?? '/').split('?')[0]!,\n route,\n status: res.statusCode,\n durationMs,\n })\n })\n next()\n },\n phase: 'beforeGlobal',\n },\n ]\n },\n\n onRouteMount(controllerClass, mountPath) {\n if (!enabled) return\n\n const collectedRoutes = getClassMeta<\n Array<{ method: string; path: string; handlerName: string; validation?: RouteValidation }>\n >(METADATA.ROUTES, controllerClass, [])\n\n const classMiddleware = getClassMeta<any[]>(METADATA.CLASS_MIDDLEWARES, controllerClass, [])\n\n for (const route of collectedRoutes) {\n const methodMiddleware = getMethodMeta<any[]>(\n METADATA.METHOD_MIDDLEWARES,\n controllerClass.prototype,\n route.handlerName,\n [],\n )\n\n routes.push({\n method: route.method.toUpperCase(),\n path: `${mountPath}${route.path === '/' ? '' : route.path}`,\n controller: controllerClass.name,\n handler: route.handlerName,\n middleware: [\n ...classMiddleware.map((m: any) => m.name || 'anonymous'),\n ...methodMiddleware.map((m: any) => m.name || 'anonymous'),\n ],\n flags: Object.fromEntries(getRouteFlags(controllerClass, route.handlerName)),\n ...uploadInfo(controllerClass, route.handlerName),\n ...requestSchemas(route.validation),\n })\n }\n },\n\n afterStart({ server }) {\n if (!enabled) return\n // Wire the bus's WS upgrade to the framework's http.Server.\n // Path-based routing inside attachUpgrade means kickjs-ws or\n // any other adapter sharing the same listener stays\n // unaffected — only `${basePath}/_bus` is claimed.\n if (server) bus?.attachUpgrade(server)\n\n // Seed `/_debug/health.adapters` with every peer the framework\n // mounted alongside us. Before this, only `DevToolsAdapter`\n // appeared in the Overview > Health card because the dict was\n // only ever written in `beforeMount` (self) and `shutdown`\n // (self). Peers with `onHealthCheck` get their live status;\n // anything else is reported as `running` since reaching\n // `afterStart` means the framework already mounted them.\n for (const peer of getPeerAdapters()) {\n const name = peer?.name\n if (typeof name !== 'string' || name === 'DevToolsAdapter') continue\n adapterStatuses[name] = 'running'\n }\n // Count @Cron runs from the first tick, before anyone opens the Jobs tab.\n if (container) trackContainerCronJobs(container)\n\n log.info(\n `DevTools ready — ${routes.length} routes tracked, ` +\n `${container?.getRegistrations().length ?? 0} DI bindings, ` +\n `event bus on ${basePath}/_bus`,\n )\n },\n\n shutdown() {\n stopErrorWatch?.()\n runtimeSampler?.stop()\n memoryAnalyzer?.stop()\n if (bus && getActiveDevtoolsBus() === bus) setActiveDevtoolsBus(undefined)\n bus?.close()\n adapterStatuses['DevToolsAdapter'] = 'stopped'\n },\n }\n },\n})\n"],"mappings":"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"}
1
+ {"version":3,"file":"index.mjs","names":["\"DevToolsAdapter\""],"sources":["../src/cron.ts","../src/topology.ts","../src/devtools-tabs.ts","../src/request-log.ts","../src/jobs.ts","../src/source-locator.ts","../src/bus/server.ts","../src/adapter.ts"],"sourcesContent":["/**\n * `@Cron` jobs for the Jobs tab: every job the container can resolve, its\n * next run, and what happened when it last ran — whatever runs it (KickJS's\n * `KickCronAdapter`, an app's own croner adapter, a serverless trigger).\n *\n * Run stats come from wrapping each job's method on its class once: the\n * runner resolves the instance and calls the method, so the wrapper sees every\n * run without the runner knowing DevTools exists.\n */\nimport { createRequire } from 'node:module'\nimport { join } from 'node:path'\nimport { isCronJobEnabled, listCronJobs, runCronJob, type CronJob } from '@forinda/kickjs'\n\nexport interface CronJobStats {\n runs: number\n failures: number\n /** Runs in progress now. */\n running: number\n lastStartedAt?: number\n lastDurationMs?: number\n lastOutcome?: 'ok' | 'failed'\n lastError?: string\n}\n\nexport interface CronJobEntry {\n name: string\n className: string\n handler: string\n expression: string\n timezone?: string\n description?: string\n enabled: boolean\n overlap: boolean\n runOnInit: boolean\n /** Epoch ms of the next tick, when `croner` is installed to work it out. */\n nextRunAt: number | null\n stats: CronJobStats\n}\n\ntype Container = Parameters<typeof listCronJobs>[0] & { resolve(token: unknown): unknown }\n\nconst TRACKED = Symbol.for('kickjs.devtools.cronTracked')\nconst stats = new Map<string, CronJobStats>()\n\n/**\n * By class *name*, like KickJS's own cron registry and `runCronJob`'s running\n * set: `listCronJobs` holds one class per name so an HMR reload replaces the\n * old class, and keying stats the same way keeps a job's counts across\n * reloads (the class is new; the job isn't).\n */\nconst keyOf = (job: Pick<CronJob, 'target' | 'handlerName'>): string =>\n `${job.target.name}.${job.handlerName}`\n\nfunction statsFor(key: string): CronJobStats {\n let s = stats.get(key)\n if (!s) stats.set(key, (s = { runs: 0, failures: 0, running: 0 }))\n return s\n}\n\n/**\n * Wrap each job's method once so its runs are counted. A sync handler stays\n * sync; a promise is passed through after its outcome is recorded.\n */\nexport function trackCronJobs(jobs: readonly CronJob[]): void {\n for (const job of jobs) {\n const proto = job.target.prototype as Record<string, unknown>\n const original = proto[job.handlerName]\n if (typeof original !== 'function' || (original as { [TRACKED]?: true })[TRACKED]) continue\n const s = statsFor(keyOf(job))\n const settle = (startedAt: number, error?: unknown): void => {\n s.running--\n s.runs++\n s.lastDurationMs = performance.now() - startedAt\n s.lastOutcome = error === undefined ? 'ok' : 'failed'\n if (error !== undefined) {\n s.failures++\n s.lastError = error instanceof Error ? error.message : String(error)\n }\n }\n const wrapped = function (this: unknown, ...args: unknown[]): unknown {\n s.running++\n s.lastStartedAt = Date.now()\n const startedAt = performance.now()\n let result: unknown\n try {\n result = (original as (...a: unknown[]) => unknown).apply(this, args)\n } catch (err) {\n settle(startedAt, err ?? new Error('threw'))\n throw err\n }\n if (result && typeof (result as Promise<unknown>).then === 'function') {\n return (result as Promise<unknown>).then(\n (value) => {\n settle(startedAt)\n return value\n },\n (err: unknown) => {\n settle(startedAt, err ?? new Error('rejected'))\n throw err\n },\n )\n }\n settle(startedAt)\n return result\n }\n Object.defineProperty(wrapped, TRACKED, { value: true })\n Object.defineProperty(wrapped, 'name', { value: (original as { name: string }).name })\n proto[job.handlerName] = wrapped\n }\n}\n\n/** Track every job the container can resolve — call at startup so early runs count. */\nexport function trackContainerCronJobs(container: Container): void {\n trackCronJobs(listCronJobs(container))\n}\n\ntype NextRun = (expression: string, timezone?: string) => number | null\n\n/** `croner` from the app's own dependencies, if it has it; otherwise no next-run times. */\nfunction loadNextRun(): NextRun {\n try {\n const { Cron } = createRequire(join(process.cwd(), 'noop.js'))('croner') as {\n Cron: new (\n expr: string,\n opts: { paused: boolean; timezone?: string },\n ) => { nextRun(): Date | null; stop(): void }\n }\n return (expression, timezone) => {\n try {\n const c = new Cron(expression, { paused: true, ...(timezone ? { timezone } : {}) })\n const next = c.nextRun()\n c.stop()\n return next ? next.getTime() : null\n } catch {\n return null\n }\n }\n } catch {\n return () => null\n }\n}\nlet nextRun: NextRun | undefined\n\n/** Every resolvable `@Cron` job, tracked, with its next run and stats. */\nexport function cronSnapshot(container: Container): CronJobEntry[] {\n const jobs = listCronJobs(container)\n trackCronJobs(jobs)\n nextRun ??= loadNextRun()\n return jobs.map((job) => ({\n name: job.name,\n className: job.target.name,\n handler: job.handlerName,\n expression: job.expression,\n ...(job.timezone ? { timezone: job.timezone } : {}),\n ...(job.description ? { description: job.description } : {}),\n enabled: isCronJobEnabled(job),\n overlap: !!job.overlap,\n runOnInit: !!job.runOnInit,\n nextRunAt: nextRun!(job.expression, job.timezone),\n stats: { ...statsFor(keyOf(job)) },\n }))\n}\n\n/**\n * Start one job now (trigger `manual`) without waiting for it: a sweep can\n * take minutes, and its stats show when it settles. Skips a disabled job and,\n * unless it allows overlap, one that's already running.\n */\nexport function runCronJobNow(\n container: Container,\n name: string,\n): { status: number; body: Record<string, unknown> } {\n const job = listCronJobs(container).find((j) => j.name === name)\n if (!job) return { status: 404, body: { error: `No cron job named \"${name}\"` } }\n if (!isCronJobEnabled(job)) return { status: 409, body: { error: `${name} is disabled` } }\n trackCronJobs([job])\n if (!job.overlap && statsFor(keyOf(job)).running > 0) {\n return { status: 409, body: { error: `${name} is already running` } }\n }\n // Failures are logged and reported by runCronJob, and land in the stats.\n void runCronJob(job, container, 'manual').catch(() => {})\n return { status: 202, body: { started: true } }\n}\n","/**\n * Topology aggregator — walks the running KickJS application + DI\n * container and produces a JSON-serialisable {@link TopologySnapshot}.\n *\n * Calls `introspect()` on every plugin and adapter that implements it\n * (architecture.md §23.3); falls back to a minimal `{ protocolVersion,\n * name, kind }` stub for primitives that don't. Per-primitive\n * introspection failures are caught + collected into the snapshot's\n * `errors` field rather than failing the entire endpoint.\n *\n * @module @forinda/kickjs-devtools/topology\n */\n\nimport type { AppAdapter, Container, KickPlugin } from '@forinda/kickjs'\nimport {\n PROTOCOL_VERSION,\n type IntrospectionSnapshot,\n type LifecycleHook,\n type TopologyContributorEntry,\n type TopologyError,\n type TopologySnapshot,\n type TopologyTokenEntry,\n} from '@forinda/kickjs-devtools-kit'\n\n/** Minimal Application surface the aggregator needs — keeps coupling thin. */\nexport interface TopologyApplicationLike {\n getAdapters(): AppAdapter[]\n getPlugins(): readonly KickPlugin[]\n /** Adapters with a `when` condition and whether each is on — absent on older Applications. */\n getConditionReport?(): ReadonlyArray<{ adapter: string; enabled: boolean; reason?: string }>\n /** Where the last startup's time went — absent on older Applications. */\n getStartupTimeline?(): { totalMs: number; steps: ReadonlyArray<{ name: string; ms: number }> }\n /** Mounted routes with their contributor chains — absent on older Applications. */\n getRoutes?(): ReadonlyArray<{\n method: string\n pattern: string\n controller: string\n handler: string\n contributors: ReadonlyArray<{\n key: string\n source: string\n label?: string\n dependsOn: readonly string[]\n beforeValidation: boolean\n overrides: ReadonlyArray<{ source: string; label?: string }>\n }>\n }>\n /**\n * Optional in older Application implementations — when present, returns\n * the union of contributors across adapters/plugins/global with proper\n * source labels. The topology aggregator prefers this over walking\n * `adapter.contributors()` + `plugin.contributors()` directly because\n * the application-level walk preserves source provenance.\n */\n getContributors?(): ReadonlyArray<{\n key: string\n source: 'method' | 'class' | 'adapter' | 'plugin' | 'module' | 'global'\n label: string\n dependsOn: readonly string[]\n }>\n}\n\n/** Inputs to {@link collectTopologySnapshot}. */\nexport interface CollectTopologyOptions {\n /** The running KickJS application (or anything that exposes its surface). */\n app: TopologyApplicationLike\n /** The DI container — used to enumerate registered tokens. */\n container: Container\n /**\n * Optional per-call introspect timeout in milliseconds. An adapter\n * whose `introspect()` doesn't settle within the budget gets a stub\n * snapshot + an entry in `errors`. Default: 100ms — keeps the\n * topology endpoint snappy even if one adapter misbehaves.\n */\n introspectTimeoutMs?: number\n}\n\nconst DEFAULT_INTROSPECT_TIMEOUT_MS = 100\n\n/** Lifecycle hooks of `AppAdapter` and `KickPlugin`, in the order they run. */\nconst HOOKS: readonly LifecycleHook[] = [\n 'register',\n 'modules',\n 'adapters',\n 'setup',\n 'middleware',\n 'contributors',\n 'beforeMount',\n 'onRouteMount',\n 'beforeStart',\n 'afterStart',\n 'onReady',\n 'onResponse',\n 'onError',\n 'onHealthCheck',\n 'shutdown',\n]\n\n/**\n * Collect a {@link TopologySnapshot} for the given application.\n *\n * Async because plugin/adapter `introspect()` may be async; the work\n * is parallelised across all primitives so wall-clock time scales\n * with the slowest introspector, not their sum.\n */\nexport async function collectTopologySnapshot(\n opts: CollectTopologyOptions,\n): Promise<TopologySnapshot> {\n const { app, container, introspectTimeoutMs = DEFAULT_INTROSPECT_TIMEOUT_MS } = opts\n const errors: TopologyError[] = []\n\n const plugins = await Promise.all(\n app.getPlugins().map((p) => snapshotFor(p, 'plugin', introspectTimeoutMs, errors)),\n )\n const adapters = await Promise.all(\n app.getAdapters().map((a) => snapshotFor(a, 'adapter', introspectTimeoutMs, errors)),\n )\n\n return {\n protocolVersion: PROTOCOL_VERSION,\n timestamp: Date.now(),\n plugins,\n adapters,\n contributors: collectContributors(app),\n diTokens: collectTokens(container),\n errors,\n }\n}\n\n/**\n * Coerce one primitive into an {@link IntrospectionSnapshot}. Tries\n * the primitive's own `introspect()` first; falls back to a stub on\n * any of: missing implementation, throw, timeout, or non-object return.\n */\nasync function snapshotFor(\n primitive: AppAdapter | KickPlugin,\n kind: 'plugin' | 'adapter',\n timeoutMs: number,\n errors: TopologyError[],\n): Promise<IntrospectionSnapshot> {\n const name = primitive.name ?? '(unnamed)'\n const hooks = HOOKS.filter((h) => typeof (primitive as Record<string, unknown>)[h] === 'function')\n const dependsOn = Array.isArray(primitive.dependsOn) ? [...primitive.dependsOn] : []\n const stub: IntrospectionSnapshot = {\n protocolVersion: PROTOCOL_VERSION,\n name,\n kind,\n hooks,\n dependsOn,\n }\n if (typeof primitive.introspect !== 'function') return stub\n\n try {\n const result = await Promise.race([\n Promise.resolve(primitive.introspect()),\n new Promise<never>((_, reject) =>\n setTimeout(\n () => reject(new Error(`introspect() timed out after ${timeoutMs}ms`)),\n timeoutMs,\n ),\n ),\n ])\n if (result && typeof result === 'object') {\n // Accept whatever the primitive returned; the kit's\n // IntrospectionSnapshot is opt-in typed via the kit, not enforced\n // at this layer (see plugin.ts / adapter.ts comment about avoiding\n // a runtime dep on the kit).\n const snap = result as Partial<IntrospectionSnapshot>\n return {\n protocolVersion: PROTOCOL_VERSION,\n name: snap.name ?? name,\n kind: snap.kind ?? kind,\n version: snap.version,\n state: snap.state,\n tokens: snap.tokens,\n metrics: snap.metrics,\n memoryBytes: snap.memoryBytes,\n hooks,\n dependsOn,\n }\n }\n return stub\n } catch (err) {\n errors.push({\n name,\n kind,\n message: err instanceof Error ? err.message : String(err),\n })\n return stub\n }\n}\n\n/** Read DI registrations into the topology shape — drops noisy HMR tokens. */\nfunction collectTokens(container: Container): TopologyTokenEntry[] {\n return container\n .getRegistrations()\n .filter((r) => !r.token.startsWith('__hmr__'))\n .map((r) => ({\n token: r.token,\n scope: r.scope,\n kind: r.kind,\n instantiated: r.instantiated,\n }))\n}\n\n/**\n * Contributor enumeration. Prefers `app.getContributors()` (the\n * Application-level walk that preserves adapter / plugin / global\n * source labels) when present; falls back to walking\n * `adapter.contributors()` + `plugin.contributors()` directly for\n * older Application implementations that don't expose the helper.\n *\n * Every source is covered, decorators included — the Application reads\n * class / method contributors back from the routes it mounted. Each route's\n * full chain, in execution order, is on `/_debug/routes`.\n */\nfunction collectContributors(app: TopologyApplicationLike): TopologyContributorEntry[] {\n const seen = new Map<string, TopologyContributorEntry>()\n\n if (typeof app.getContributors === 'function') {\n for (const c of app.getContributors()) {\n // One entry per key AND source: a method decorator overriding a module\n // contributor of the same key is a different registration, not a repeat.\n const id = `${c.source}:${c.key}`\n if (seen.has(id)) continue\n // Pass `source` through unchanged — the kit's\n // `TopologyContributorSource` union accepts every value the\n // framework's `getContributors()` returns. Earlier versions\n // collapsed `'plugin' | 'global'` → `'adapter'` because the kit\n // type was narrower; that's no longer needed.\n seen.set(id, {\n key: c.key,\n source: c.source as TopologyContributorEntry['source'],\n dependsOn: c.dependsOn as string[],\n })\n }\n return [...seen.values()]\n }\n\n // Legacy path — direct walk.\n const ingest = (\n list: ReadonlyArray<unknown> | null | undefined,\n source: TopologyContributorEntry['source'],\n ): void => {\n if (!list) return\n for (const reg of list) {\n const r = reg as { key?: string; dependsOn?: readonly string[] }\n const key = typeof r.key === 'string' ? r.key : null\n if (!key || seen.has(key)) continue\n seen.set(key, {\n key,\n source,\n dependsOn: Array.isArray(r.dependsOn) ? r.dependsOn : [],\n })\n }\n }\n\n for (const plugin of app.getPlugins()) {\n ingest(plugin.contributors?.(), 'adapter')\n }\n for (const adapter of app.getAdapters()) {\n ingest(adapter.contributors?.(), 'adapter')\n }\n\n return [...seen.values()]\n}\n","/**\n * Custom-tab aggregator — collects every plugin/adapter's\n * `devtoolsTabs()` contribution into a single deduped list for the\n * panel's `/_debug/tabs` endpoint.\n *\n * Validation is permissive: anything that doesn't look like a\n * {@link DevtoolsTabDescriptor} is dropped with a console warning\n * rather than throwing — a misconfigured adapter shouldn't break the\n * whole DevTools panel.\n *\n * @module @forinda/kickjs-devtools/devtools-tabs\n */\n\nimport type { DevtoolsTabDescriptor } from '@forinda/kickjs-devtools-kit'\nimport type { TopologyApplicationLike } from './topology'\n\n/** Result of {@link collectDevtoolsTabs}. */\nexport interface DevtoolsTabsResult {\n /** Deduped, validated tabs in registration order. */\n tabs: DevtoolsTabDescriptor[]\n /** Per-source collection errors (drop reason, source name). */\n errors: ReadonlyArray<{ source: string; reason: string }>\n}\n\nconst VALID_VIEW_TYPES = new Set(['iframe', 'launch', 'html', 'module'])\n\n/**\n * Walk every plugin + adapter, call `devtoolsTabs?()`, validate each\n * entry, and dedupe by `id`. Last-source-wins on collision is too\n * surprising — first-source-wins + a warning entry instead.\n */\nexport function collectDevtoolsTabs(app: TopologyApplicationLike): DevtoolsTabsResult {\n const tabs: DevtoolsTabDescriptor[] = []\n const seen = new Map<string, string>() // id → source name (for collision warnings)\n const errors: Array<{ source: string; reason: string }> = []\n\n const ingest = (sourceName: string, raw: unknown): void => {\n if (!Array.isArray(raw)) {\n errors.push({ source: sourceName, reason: 'devtoolsTabs() did not return an array' })\n return\n }\n for (const entry of raw) {\n const validation = validateTab(entry)\n if (!validation.ok) {\n errors.push({ source: sourceName, reason: validation.reason })\n continue\n }\n const tab = validation.tab\n const owner = seen.get(tab.id)\n if (owner) {\n errors.push({\n source: sourceName,\n reason: `tab id '${tab.id}' already registered by ${owner} — first-source wins`,\n })\n continue\n }\n seen.set(tab.id, sourceName)\n tabs.push(tab)\n }\n }\n\n for (const plugin of app.getPlugins()) {\n if (typeof plugin.devtoolsTabs !== 'function') continue\n try {\n ingest(plugin.name ?? '(unnamed plugin)', plugin.devtoolsTabs())\n } catch (err) {\n errors.push({\n source: plugin.name ?? '(unnamed plugin)',\n reason: err instanceof Error ? err.message : String(err),\n })\n }\n }\n for (const adapter of app.getAdapters()) {\n if (typeof adapter.devtoolsTabs !== 'function') continue\n try {\n ingest(adapter.name ?? '(unnamed adapter)', adapter.devtoolsTabs())\n } catch (err) {\n errors.push({\n source: adapter.name ?? '(unnamed adapter)',\n reason: err instanceof Error ? err.message : String(err),\n })\n }\n }\n\n return { tabs, errors }\n}\n\ninterface ValidationOk {\n ok: true\n tab: DevtoolsTabDescriptor\n}\n\ninterface ValidationFail {\n ok: false\n reason: string\n}\n\n/**\n * Coerce + validate a tab descriptor. Returns the typed object on\n * success; a structured reason on failure. Uses a permissive checker\n * so a partially-correct entry from a third-party adapter doesn't\n * break the whole panel.\n */\nfunction validateTab(raw: unknown): ValidationOk | ValidationFail {\n if (!raw || typeof raw !== 'object') {\n return { ok: false, reason: 'tab entry is not an object' }\n }\n const obj = raw as Record<string, unknown>\n if (typeof obj.id !== 'string' || obj.id.length === 0) {\n return { ok: false, reason: 'tab.id must be a non-empty string' }\n }\n if (typeof obj.title !== 'string' || obj.title.length === 0) {\n return { ok: false, reason: `tab '${obj.id}': title must be a non-empty string` }\n }\n const view = obj.view as Record<string, unknown> | undefined\n if (!view || typeof view !== 'object') {\n return { ok: false, reason: `tab '${obj.id}': view object is required` }\n }\n if (typeof view.type !== 'string' || !VALID_VIEW_TYPES.has(view.type)) {\n return {\n ok: false,\n reason: `tab '${obj.id}': view.type must be one of iframe / launch / html / module`,\n }\n }\n switch (view.type) {\n case 'iframe':\n if (typeof view.src !== 'string' || view.src.length === 0) {\n return { ok: false, reason: `tab '${obj.id}': iframe view requires a non-empty src` }\n }\n break\n case 'html':\n if (typeof view.html !== 'string') {\n return { ok: false, reason: `tab '${obj.id}': html view requires html string` }\n }\n break\n case 'module':\n if (typeof view.src !== 'string' || view.src.length === 0) {\n return { ok: false, reason: `tab '${obj.id}': module view requires a non-empty src` }\n }\n break\n case 'launch':\n if (!Array.isArray(view.actions)) {\n return { ok: false, reason: `tab '${obj.id}': launch view requires actions array` }\n }\n break\n }\n return { ok: true, tab: obj as unknown as DevtoolsTabDescriptor }\n}\n\n/**\n * Run a `launch` tab's button. The tabs are collected again rather than\n * cached, so an HMR-reloaded adapter's new `run()` is the one called.\n */\nexport async function runTabAction(\n app: TopologyApplicationLike,\n tabId: string,\n actionId: string,\n): Promise<{ status: number; body: { result?: unknown; error?: string } }> {\n const tab = collectDevtoolsTabs(app).tabs.find((t) => t.id === tabId)\n const action =\n tab?.view.type === 'launch' ? tab.view.actions.find((a) => a.id === actionId) : undefined\n if (!action) return { status: 404, body: { error: `no action '${actionId}' on tab '${tabId}'` } }\n if (typeof action.run !== 'function') {\n return { status: 404, body: { error: `action '${actionId}' has no run()` } }\n }\n try {\n return { status: 200, body: { result: (await action.run()) ?? null } }\n } catch (err) {\n return { status: 500, body: { error: err instanceof Error ? err.message : String(err) } }\n }\n}\n","/**\n * The last N requests the app answered, for the dashboard's Requests tab.\n *\n * Fed by the adapter's `onResponse` / `onError` hooks. An error is matched\n * to its request by `requestId`, whichever of the two hooks fires first.\n *\n * @module @forinda/kickjs-devtools/request-log\n */\n\nexport interface RequestLogEntry {\n /** Increasing per entry — the dashboard asks for entries after the last one it has. */\n seq: number\n /** When the response finished, ms since epoch. */\n at: number\n method: string\n path: string\n /** Matched route pattern; absent for unmatched requests. */\n route?: string\n status: number\n durationMs: number\n requestId?: string\n /** The error the request failed with, when one was reported. */\n error?: { name: string; message: string }\n}\n\ntype Response = Pick<\n RequestLogEntry,\n 'method' | 'path' | 'route' | 'status' | 'durationMs' | 'requestId'\n>\n\n/** Errors waiting for their response, keyed by request id — bounded like the log. */\nconst MAX_PENDING_ERRORS = 100\n\nexport class RequestLog {\n private entries: RequestLogEntry[] = []\n private pendingErrors = new Map<string, RequestLogEntry['error']>()\n private seq = 0\n\n constructor(private readonly capacity: number) {}\n\n record(info: Response, now = Date.now()): void {\n if (this.capacity <= 0) return\n const entry: RequestLogEntry = { seq: ++this.seq, at: now, ...info }\n if (info.requestId && this.pendingErrors.has(info.requestId)) {\n entry.error = this.pendingErrors.get(info.requestId)\n this.pendingErrors.delete(info.requestId)\n }\n this.entries.push(entry)\n if (this.entries.length > this.capacity) this.entries.shift()\n }\n\n recordError(requestId: string | undefined, err: unknown): void {\n if (this.capacity <= 0 || !requestId) return\n const error =\n err instanceof Error\n ? { name: err.name, message: err.message }\n : { name: 'Error', message: String(err) }\n // Response already logged — attach to it; otherwise hold until it is.\n for (let i = this.entries.length - 1; i >= 0; i--) {\n if (this.entries[i]!.requestId === requestId) {\n this.entries[i]!.error = error\n return\n }\n }\n this.pendingErrors.set(requestId, error)\n if (this.pendingErrors.size > MAX_PENDING_ERRORS) {\n this.pendingErrors.delete(this.pendingErrors.keys().next().value!)\n }\n }\n\n /** The newest entry's `seq` — lower than a client's last one means the app restarted. */\n latest(): number {\n return this.seq\n }\n\n /** Entries after `since`, oldest first. */\n after(since = 0): RequestLogEntry[] {\n return this.entries.filter((e) => e.seq > since)\n }\n}\n","/**\n * Job management for the Queues tab — over any {@link JobInspector} an\n * adapter or plugin exposes from `jobInspector()`. Each inspector is a\n * `source`, named after its owner, so two job tools can sit side by side.\n *\n * @module @forinda/kickjs-devtools/jobs\n */\n\nimport type { JobInspector, JobQueueInfo, JobState } from '@forinda/kickjs-devtools-kit'\nimport type { TopologyApplicationLike } from './topology'\n\nconst STATES: ReadonlySet<string> = new Set([\n 'waiting',\n 'active',\n 'delayed',\n 'completed',\n 'failed',\n 'paused',\n])\nconst ACTIONS = ['retry', 'remove', 'retryAll', 'clean', 'pause', 'resume'] as const\ntype Action = (typeof ACTIONS)[number]\n\n/** Most jobs one list request returns. */\nconst MAX_PAGE = 100\n\ntype Reply = { status: number; body: unknown }\nconst bad = (error: string, status = 400): Reply => ({ status, body: { error } })\n\n/** Every inspector the app's adapters and plugins expose, keyed by owner name. */\nexport function findJobInspectors(app: TopologyApplicationLike): Map<string, JobInspector> {\n const found = new Map<string, JobInspector>()\n for (const owner of [...app.getAdapters(), ...app.getPlugins()]) {\n const get = (owner as { jobInspector?: () => JobInspector | undefined }).jobInspector\n if (typeof get !== 'function') continue\n const inspector = get.call(owner)\n if (inspector) found.set(owner.name ?? 'jobs', inspector)\n }\n return found\n}\n\n/** `GET /jobs` — each source's queues and the actions it supports. */\nexport async function listJobSources(inspectors: Map<string, JobInspector>): Promise<Reply> {\n const sources = await Promise.all(\n [...inspectors].map(async ([source, inspector]) => {\n const actions = ACTIONS.filter((a) => typeof inspector[a] === 'function')\n try {\n return { source, actions, queues: await inspector.queues() }\n } catch (err) {\n return { source, actions, queues: [] as JobQueueInfo[], error: message(err) }\n }\n }),\n )\n return { status: 200, body: { sources } }\n}\n\n/** `GET /jobs/list?source&queue&state&start&end` */\nexport async function listJobs(\n inspectors: Map<string, JobInspector>,\n q: Record<string, unknown>,\n): Promise<Reply> {\n const inspector = inspectors.get(String(q.source ?? ''))\n if (!inspector) return bad('unknown source', 404)\n const state = String(q.state ?? 'failed')\n if (!STATES.has(state)) return bad(`unknown state '${state}'`)\n const start = Math.max(0, Number(q.start ?? 0) || 0)\n const end = Math.min(start + MAX_PAGE - 1, Math.max(start, Number(q.end ?? start + 49) || 0))\n try {\n const jobs = await inspector.jobs(String(q.queue ?? ''), state as JobState, { start, end })\n return { status: 200, body: { jobs } }\n } catch (err) {\n return bad(message(err), 500)\n }\n}\n\n/** `GET /jobs/job?source&queue&id` */\nexport async function getJob(\n inspectors: Map<string, JobInspector>,\n q: Record<string, unknown>,\n): Promise<Reply> {\n const inspector = inspectors.get(String(q.source ?? ''))\n if (!inspector) return bad('unknown source', 404)\n try {\n const job = await inspector.job(String(q.queue ?? ''), String(q.id ?? ''))\n return job ? { status: 200, body: job } : bad('job not found', 404)\n } catch (err) {\n return bad(message(err), 500)\n }\n}\n\n/** `POST /jobs/action?source&queue&action&id&state` */\nexport async function runJobAction(\n inspectors: Map<string, JobInspector>,\n q: Record<string, unknown>,\n): Promise<Reply> {\n const inspector = inspectors.get(String(q.source ?? ''))\n if (!inspector) return bad('unknown source', 404)\n const action = String(q.action ?? '') as Action\n if (!ACTIONS.includes(action)) return bad(`unknown action '${action}'`)\n const queue = String(q.queue ?? '')\n const id = String(q.id ?? '')\n const state = String(q.state ?? '')\n try {\n switch (action) {\n case 'retry':\n case 'remove':\n if (!id) return bad('id is required')\n if (!inspector[action]) return bad(`${action} is not supported`, 501)\n await inspector[action]!(queue, id)\n return { status: 200, body: { ok: true } }\n case 'clean':\n if (!STATES.has(state)) return bad('state is required')\n if (!inspector.clean) return bad('clean is not supported', 501)\n return {\n status: 200,\n body: { ok: true, count: await inspector.clean(queue, state as JobState) },\n }\n default:\n if (!inspector[action]) return bad(`${action} is not supported`, 501)\n return { status: 200, body: { ok: true, count: (await inspector[action]!(queue)) ?? null } }\n }\n } catch (err) {\n return bad(message(err), 500)\n }\n}\n\nconst message = (err: unknown): string => (err instanceof Error ? err.message : String(err))\n","/**\n * Find a controller method in the project's source — what \"open handler in\n * editor\" needs. The running app knows the class and method names but not the\n * file (the dev server may have transformed it), so this reads `src/` for the\n * class declaration, then the method inside it.\n *\n * Read on each lookup rather than cached: it runs on a click, in development,\n * and files move under HMR.\n */\nimport { existsSync, readdirSync, readFileSync } from 'node:fs'\nimport { join, relative, sep } from 'node:path'\n\nexport interface HandlerSource {\n /** Absolute path. */\n file: string\n /** Path relative to the project root, `/`-separated — for mapping into another checkout. */\n relative: string\n /** 1-based line of the method (or of the class, when the method isn't found). */\n line: number\n}\n\nconst escape = (name: string) => name.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\$&')\n\n/** 1-based line of the handler inside `controller`, or `undefined` when the class isn't declared here. */\nexport function findHandlerLine(\n source: string,\n controller: string,\n handler: string,\n): number | undefined {\n const cls = new RegExp(`\\\\bclass\\\\s+${escape(controller)}\\\\b`).exec(source)\n if (!cls) return undefined\n const lineAt = (offset: number) => source.slice(0, offset).split('\\n').length\n // Only this class's body: up to the next top-level class declaration, so a\n // same-named method of a later class is never picked.\n const start = cls.index + cls[0].length\n const rest = source.slice(start)\n const next = rest.search(/^(?:export\\s+)?(?:default\\s+)?(?:abstract\\s+)?class\\s/m)\n const body = next === -1 ? rest : rest.slice(0, next)\n const method = new RegExp(\n `^[ \\\\t]*(?:(?:public|private|protected|static|async|override)\\\\s+)*${escape(handler)}\\\\s*[(<]`,\n 'm',\n ).exec(body)\n return lineAt(method ? start + method.index + method[0].search(/\\S/) : cls.index)\n}\n\n/** Locate `controller.handler` under `<root>/src` (or `<root>` when there is no `src/`). */\nexport function locateHandler(\n root: string,\n controller: string,\n handler: string,\n): HandlerSource | undefined {\n const dir = existsSync(join(root, 'src')) ? join(root, 'src') : root\n const files = readdirSync(dir, { recursive: true, encoding: 'utf-8' }).filter(\n (f) =>\n /\\.[mc]?tsx?$/.test(f) &&\n !/\\.d\\.[mc]?ts$/.test(f) &&\n !f.split(sep).includes('node_modules') &&\n !f.split(sep).includes('dist'),\n )\n for (const f of files) {\n const file = join(dir, f)\n let source: string\n try {\n source = readFileSync(file, 'utf-8')\n } catch {\n continue\n }\n if (!source.includes(controller)) continue\n const line = findHandlerLine(source, controller, handler)\n if (line !== undefined) {\n return { file, relative: relative(root, file).split(sep).join('/'), line }\n }\n }\n return undefined\n}\n","// Server-side KickEventBus implementation.\n//\n// Wraps the bus core from `@forinda/kickjs-devtools-kit/bus` with a\n// `ws` server bound to a path on the framework's existing\n// `http.Server`. Architecture:\n//\n// server.on('upgrade') ─┐\n// ├──► WebSocketServer.handleUpgrade()\n// │ └─► ws clients: receive emits as JSON\n// bus.emit(type, payload) ─┴──► serialize → fan out to clients +\n// dispatch locally to in-process subs\n//\n// Why piggyback on the framework's http.Server instead of starting a\n// dedicated WS server: the framework already owns the listener, the\n// upgrade event is the canonical hook, and reusing it means devtools\n// inherits whatever TLS / proxy / port config the application provides.\n//\n// Lifecycle:\n// - createServerBus() builds the WebSocketServer in `noServer: true`\n// mode (no listener of its own) and returns the bus + attach/close\n// handles.\n// - attachUpgrade(httpServer) wires server.on('upgrade') so upgrades\n// matching `wsPath` route into the WebSocketServer. Other upgrade\n// paths (kickjs-ws, etc.) ignore us by virtue of path matching.\n// - close() drops every client and detaches from the http server so\n// repeated dev-mode restarts don't leak listener handles.\n//\n// Authentication:\n// - The optional `secret` matches the devtools panel's existing\n// `x-devtools-token` / `?token=` access guard. When set, upgrades\n// without the right token get rejected at HTTP-handshake time —\n// never reaching the bus.\n//\n// Loop avoidance:\n// - Server emits fan out to every connected client. Client-side\n// emits sent UP the socket dispatch into the local core (so\n// server tabs can react) but are NOT re-broadcast to other\n// clients. This keeps the same \"no echo\" contract the browser\n// bus enforces; without it a tab's emit would round-trip through\n// the server and back to itself.\n\nimport type { IncomingMessage } from 'node:http'\nimport type { KickServer } from '@forinda/kickjs'\nimport type { Socket } from 'node:net'\n\nimport { createBusCore } from '@forinda/kickjs-devtools-kit/bus'\nimport type { KickDevtoolsEvent, KickEventBus, Unsubscribe } from '@forinda/kickjs-devtools-kit/bus'\nimport { WebSocketServer, type WebSocket } from 'ws'\n\nexport interface ServerBusOptions {\n /** Path the WebSocket upgrade listens on. Default `/_debug/_bus`. */\n wsPath?: string\n /**\n * Optional shared secret. When set, clients must include either\n * `?token=<secret>` on the upgrade URL or an\n * `x-devtools-token: <secret>` header. Unauthorized upgrades return\n * 401 and never reach the bus. Pass `false` (or omit) to disable.\n */\n secret?: string | false\n}\n\ninterface TaggedEnvelope extends KickDevtoolsEvent {\n __kick_origin?: 'local' | 'broadcast' | 'ws'\n}\n\nexport interface ServerBus extends KickEventBus {\n /**\n * Hook into the app server's `upgrade` event — `http`, `https`, or HTTP/2\n * with HTTP/1.1 fallback all emit it for WebSocket handshakes.\n */\n attachUpgrade(httpServer: KickServer): void\n /** Drop all clients + detach. Idempotent. */\n close(): void\n /** Currently connected client count. Useful for the devtools health card. */\n clientCount(): number\n}\n\nexport function createServerBus(opts: ServerBusOptions = {}): ServerBus {\n const wsPath = opts.wsPath ?? '/_debug/_bus'\n const secret = opts.secret ?? false\n const core = createBusCore()\n const wss = new WebSocketServer({ noServer: true })\n const clients = new Set<WebSocket>()\n let attachedServer: KickServer | null = null\n let upgradeHandler: ((req: IncomingMessage, socket: Socket, head: Buffer) => void) | null = null\n\n wss.on('connection', (ws: WebSocket) => {\n clients.add(ws)\n ws.on('message', (data: Buffer | string) => {\n try {\n const text = typeof data === 'string' ? data : data.toString('utf-8')\n const parsed = JSON.parse(text) as TaggedEnvelope\n if (!parsed || typeof parsed.type !== 'string' || typeof parsed.ts !== 'number') return\n // Dispatch locally only — never echo back to other clients.\n // (Tabs that need cross-tab fan-out use BroadcastChannel; the\n // server is point-to-point per client.)\n core.dispatch({ ...parsed, __kick_origin: 'ws' } as KickDevtoolsEvent)\n } catch {\n // Malformed — drop silently. A misbehaving tab shouldn't\n // crash the server bus.\n }\n })\n ws.on('close', () => {\n clients.delete(ws)\n })\n ws.on('error', () => {\n clients.delete(ws)\n })\n })\n\n const fanOut = (envelope: TaggedEnvelope): void => {\n if (clients.size === 0) return\n // Emitters call this synchronously — kick/db does from inside a query —\n // so it must never throw. A bigint (a bigint column's value) goes out as\n // a string; an event JSON can't hold at all (a circular payload) is\n // dropped for the dashboard, not the caller.\n let text: string\n try {\n text = JSON.stringify(envelope, (_key, value: unknown) =>\n typeof value === 'bigint' ? value.toString() : value,\n )\n } catch {\n return\n }\n for (const client of clients) {\n // OPEN === 1 in the ws spec; reusing the import for the type\n // guard keeps this readable without pulling in WebSocket consts.\n if (client.readyState === client.OPEN) {\n try {\n client.send(text)\n } catch {\n // Network error mid-fan-out — clean removal happens via the\n // 'close' / 'error' handlers; nothing to do here.\n }\n }\n }\n }\n\n const checkAuth = (req: IncomingMessage): boolean => {\n if (secret === false) return true\n const header = req.headers['x-devtools-token']\n if (typeof header === 'string' && header === secret) return true\n // Parse query — `req.url` is path + ?query for upgrade requests.\n const url = req.url ?? ''\n const queryIdx = url.indexOf('?')\n if (queryIdx === -1) return false\n const params = new URLSearchParams(url.slice(queryIdx + 1))\n return params.get('token') === secret\n }\n\n const attachUpgrade = (httpServer: KickServer): void => {\n if (attachedServer) return // idempotent\n attachedServer = httpServer\n upgradeHandler = (req, socket, head) => {\n // Path match — ignore upgrades destined for other adapters\n // (kickjs-ws, custom adapters, etc.). They register their own\n // upgrade handlers; ours just no-ops on non-matching URLs.\n const url = req.url ?? ''\n const pathOnly = url.split('?')[0]\n if (pathOnly !== wsPath) return\n\n if (!checkAuth(req)) {\n socket.write('HTTP/1.1 401 Unauthorized\\r\\n\\r\\n')\n socket.destroy()\n return\n }\n\n wss.handleUpgrade(req, socket, head, (ws: WebSocket) => {\n wss.emit('connection', ws, req)\n })\n }\n httpServer.on('upgrade', upgradeHandler)\n }\n\n const close = (): void => {\n if (attachedServer && upgradeHandler) {\n attachedServer.off('upgrade', upgradeHandler)\n }\n attachedServer = null\n upgradeHandler = null\n for (const client of clients) {\n try {\n client.close()\n } catch {\n // ignore\n }\n }\n clients.clear()\n wss.close()\n }\n\n return {\n on(type: string, handler: (payload: unknown) => void): Unsubscribe {\n return core.on(type, handler)\n },\n onAny(handler: (event: KickDevtoolsEvent) => void): Unsubscribe {\n return core.onAny(handler)\n },\n emit(type: string, payload: unknown): void {\n const envelope: TaggedEnvelope = {\n type,\n payload,\n ts: Date.now(),\n __kick_origin: 'local',\n }\n core.dispatch(envelope)\n fanOut(envelope)\n },\n attachUpgrade,\n close,\n clientCount: () => clients.size,\n } as ServerBus\n}\n","import type { IncomingMessage, ServerResponse } from 'node:http'\nimport { dirname, join } from 'node:path'\nimport { tmpdir } from 'node:os'\nimport { fileURLToPath } from 'node:url'\nimport { createReadStream, existsSync, readFileSync, statSync, unlink } from 'node:fs'\nimport { Readable } from 'node:stream'\nimport { randomBytes } from 'node:crypto'\nimport { writeHeapSnapshot } from 'node:v8'\nimport {\n type AdapterMiddleware,\n type Container,\n METADATA,\n defineAdapter,\n ref,\n computed,\n reactive,\n watch,\n createLogger,\n type Ref,\n type ComputedRef,\n getClassMeta,\n getMethodMeta,\n getRouteFlags,\n type MatchedRoute,\n type RequestContext,\n type ConfigOrigins,\n} from '@forinda/kickjs'\nimport * as kick from '@forinda/kickjs'\nimport {\n MemoryAnalyzer,\n PROTOCOL_VERSION,\n RuntimeSampler,\n type IntrospectionSnapshot,\n} from '@forinda/kickjs-devtools-kit'\nimport { cronSnapshot, runCronJobNow, trackContainerCronJobs } from './cron'\nimport {\n DEVTOOLS_BUS,\n getActiveDevtoolsBus,\n setActiveDevtoolsBus,\n} from '@forinda/kickjs-devtools-kit/bus/token'\nimport { collectTopologySnapshot, type TopologyApplicationLike } from './topology'\nimport { collectDevtoolsTabs, runTabAction } from './devtools-tabs'\nimport { RequestLog } from './request-log'\nimport { findJobInspectors, getJob, listJobs, listJobSources, runJobAction } from './jobs'\nimport { locateHandler } from './source-locator'\nimport { createServerBus, type ServerBus } from './bus/server'\nimport { detectSchema } from '@forinda/kickjs-schema'\n\nconst log = createLogger('DevTools')\n\n/** The serialisable part of a handler's `@FileUpload` config, if it has one. */\nfunction uploadInfo(controllerClass: any, handlerName: string): Pick<RouteInfo, 'upload'> {\n const config = getMethodMeta<\n { mode: 'single' | 'array' | 'none'; fieldName?: string; maxCount?: number } | undefined\n >(METADATA.FILE_UPLOAD, controllerClass, handlerName, undefined)\n if (!config) return {}\n const { mode, fieldName, maxCount } = config\n return {\n upload: { mode, ...(fieldName ? { fieldName } : {}), ...(maxCount ? { maxCount } : {}) },\n }\n}\n\n/** Route metadata collected during mount */\ninterface RouteInfo {\n method: string\n path: string\n controller: string\n handler: string\n middleware: string[]\n /**\n * Route flags in force, resolved method-over-class. Present so the dashboard\n * can answer \"why is this endpoint not requiring auth\" from the route list\n * itself, rather than by reading the controller.\n */\n flags: Record<string, unknown>\n /**\n * `@FileUpload` config, when the handler declares one — the dashboard's API\n * runner starts such routes in multipart mode with this field. Only the\n * plain fields: `allowedTypes` may be a function and isn't serialisable.\n */\n upload?: { mode: 'single' | 'array' | 'none'; fieldName?: string; maxCount?: number }\n /**\n * The route's request schemas as JSON Schema (OpenAPI 3.0 form, what the\n * request accepts) — the API runner prefills from them, with or without\n * the Swagger adapter.\n */\n schemas?: { body?: JsonSchema; query?: JsonSchema; params?: JsonSchema }\n}\n\ntype JsonSchema = Record<string, unknown>\n\n/** A route's body, query and params schemas, each one that converts. */\nfunction requestSchemas(validation: RouteValidation | undefined): Pick<RouteInfo, 'schemas'> {\n if (!validation) return {}\n const schemas: NonNullable<RouteInfo['schemas']> = {}\n for (const part of ['body', 'query', 'params'] as const) {\n if (!validation[part]) continue\n try {\n schemas[part] = detectSchema(validation[part]).toJsonSchema({\n target: 'openapi-3.0',\n io: 'input',\n })\n } catch {\n // Not a schema kickjs-schema knows, or one it can't convert: no prefill for it.\n }\n }\n return Object.keys(schemas).length > 0 ? { schemas } : {}\n}\n\ntype RouteValidation = { body?: unknown; query?: unknown; params?: unknown }\n\n/** Per-route latency stats with percentile tracking */\ninterface RouteStats {\n count: number\n totalMs: number\n minMs: number\n maxMs: number\n /** Responses with status >= 500. */\n serverErrors: number\n /** Responses with status 400–499. */\n clientErrors: number\n /** Ring buffer of last N samples for percentile computation */\n samples: number[]\n}\n\n/** Upper bounds (ms) of the latency histogram's buckets; the last bucket is everything above. */\nconst LATENCY_BUCKETS_MS = [1, 2, 5, 10, 25, 50, 100, 250, 500, 1000] as const\n\n/** How many recent samples fall in each {@link LATENCY_BUCKETS_MS} bucket (plus one overflow bucket). */\nfunction latencyHistogram(samples: readonly number[]): number[] {\n const counts = Array.from({ length: LATENCY_BUCKETS_MS.length + 1 }, () => 0)\n for (const ms of samples) {\n const i = LATENCY_BUCKETS_MS.findIndex((bound) => ms <= bound)\n counts[i === -1 ? LATENCY_BUCKETS_MS.length : i]!++\n }\n return counts\n}\n\nconst MAX_SAMPLES = 1000\nconst METHODS = new Set(['GET', 'HEAD', 'POST', 'PUT', 'DELETE', 'PATCH', 'OPTIONS', 'QUERY'])\n\n/**\n * Where every runtime publishes the matched route on the raw request — the\n * same `Symbol.for` slot `ctx.route` reads in `@forinda/kickjs`. Registry\n * symbol, so it is shared across module copies.\n */\nconst MATCHED_ROUTE_SLOT = Symbol.for('kick.route')\n\n/**\n * Whether the installed `@forinda/kickjs` reports every response to adapter\n * `onResponse` hooks. When it does, the dashboard's counters are fed from\n * there; on older releases (still supported peers) the adapter falls back to\n * its own middleware. Looked up, not imported by name, so older releases load.\n */\nconst FRAMEWORK_REPORTS_RESPONSES =\n typeof (kick as { reportResponse?: unknown }).reportResponse === 'function'\n\n/** Compute a percentile from a sorted array of numbers */\nfunction percentile(sorted: number[], p: number): number {\n if (sorted.length === 0) return 0\n const idx = Math.ceil(p * sorted.length) - 1\n return sorted[Math.max(0, idx)]\n}\n\n/** Compute p50, p95, p99 from a RouteStats samples buffer */\nfunction computePercentiles(stats: RouteStats): { p50: number; p95: number; p99: number } {\n const sorted = [...stats.samples].toSorted((a, b) => a - b)\n return {\n p50: percentile(sorted, 0.5),\n p95: percentile(sorted, 0.95),\n p99: percentile(sorted, 0.99),\n }\n}\n\n/**\n * Race a promise against a timeout. Clears the timer when the inner\n * promise wins so we don't leak handles every time a peer responds\n * quickly. Used to bound peer `onHealthCheck()` calls inside the\n * `/_debug/health` handler — one slow peer must not be able to stall\n * the dashboard request.\n */\nfunction raceWithTimeout<T>(promise: Promise<T>, ms: number, label: string): Promise<T> {\n let timer: ReturnType<typeof setTimeout> | undefined\n const timeout = new Promise<never>((_, reject) => {\n timer = setTimeout(() => reject(new Error(`${label} timed out after ${ms}ms`)), ms)\n })\n return Promise.race([promise, timeout]).finally(() => {\n if (timer) clearTimeout(timer)\n })\n}\n\n/**\n * Per-peer budget for `onHealthCheck()` inside `/_debug/health`.\n * Long enough for a typical DB ping or remote check, short enough that\n * one misbehaving adapter can't stall the dashboard endpoint.\n */\n/** How often `/stream` re-sends the counters even when nothing changed. */\nconst METRICS_TICK_MS = 5000\n\nconst PEER_HEALTHCHECK_TIMEOUT_MS = 1500\n\n/**\n * Module-scoped guard for `POST /_debug/memory/snapshot`. `writeHeapSnapshot()`\n * blocks the event loop for the duration of the capture (multi-second\n * for large heaps); two concurrent captures would compound the pause.\n * The endpoint returns 503 + a hint message when this flag is set so\n * the SPA can surface a \"snapshot already running\" warning rather than\n * silently queueing.\n *\n * Module-scoped (not per-adapter-instance) because writeHeapSnapshot\n * acts on the V8 process — multiple DevToolsAdapter instances would\n * still race at the v8 layer.\n */\nlet snapshotInProgress = false\n\nexport interface DevToolsOptions {\n /** Base path for debug endpoints (default: '/_debug') */\n basePath?: string\n /**\n * Mount the dashboard. Default: only when `NODE_ENV` is `'development'` or\n * `'test'` — an unset or unrecognised `NODE_ENV` keeps `/_debug` off.\n */\n enabled?: boolean\n /**\n * Include environment variables (sanitized) at /_debug/config, with where\n * each declared variable's value comes from (default: false)\n */\n exposeConfig?: boolean\n /**\n * Env var prefixes whose values `/config` shows (default: ['APP_', 'NODE_ENV']).\n * The app's other declared variables (env schema, settings sections) are\n * listed as `[REDACTED]`; the rest of the environment is left out.\n */\n configPrefixes?: string[]\n /** Callback when error rate exceeds threshold */\n onErrorRateExceeded?: (rate: number) => void\n /** Error rate threshold (default: 0.5 = 50%) */\n errorRateThreshold?: number\n /** Other adapters to discover stats from (e.g., WsAdapter) */\n adapters?: any[]\n\n /**\n * Secret token to guard DevTools access. When set, all requests must\n * include this token as `x-devtools-token` header or `?token=` query param.\n *\n * Auto-generated on startup if not provided. The token is logged to the console.\n * Set to `false` to disable the guard entirely (not recommended).\n *\n * @example\n * ```ts\n * DevToolsAdapter({ secret: getEnv('DEVTOOLS_SECRET') })\n * ```\n */\n secret?: string | false\n\n /**\n * Tuning for the runtime sampler that powers `/_debug/runtime` and\n * the upcoming Memory tab (architecture.md §23). Defaults to\n * 1-second polling with a 60-sample (~1 minute) ring buffer.\n *\n * Set `runtime.enabled = false` to skip starting the sampler — the\n * `/_debug/runtime` endpoint then returns 404 instead of an empty\n * snapshot, signalling deliberate opt-out rather than a startup race.\n */\n runtime?: {\n /** Whether to start the runtime sampler at all. Default: true. */\n enabled?: boolean\n /** Polling interval in milliseconds. Default: 1000. */\n intervalMs?: number\n /** Ring-buffer size — number of past samples retained. Default: 60. */\n bufferSize?: number\n }\n /**\n * How many recent requests the Requests tab keeps (method, path, status,\n * duration, error). Default: 200. `0` keeps none.\n */\n requestLog?: number\n}\n\n/**\n * Public reactive surface exposed by a DevToolsAdapter instance —\n * counters, computed metrics, and the route latency map. Tests and\n * peer adapters consume these directly to drive their own behavior.\n */\nexport interface DevToolsAdapterExtensions {\n /** Total requests received. */\n readonly requestCount: Ref<number>\n /** Total responses with status >= 500. */\n readonly errorCount: Ref<number>\n /** Total responses with status >= 400 and < 500. */\n readonly clientErrorCount: Ref<number>\n /** Server start time. */\n readonly startedAt: Ref<number>\n /** Computed error rate (server errors / total requests). */\n readonly errorRate: ComputedRef<number>\n /** Computed uptime in seconds. */\n readonly uptimeSeconds: ComputedRef<number>\n /** Per-route latency tracking. */\n readonly routeLatency: Record<string, RouteStats>\n /**\n * Tier-1 runtime sampler from `@forinda/kickjs-devtools-kit`. `null`\n * when `runtime.enabled` is `false`. Tests can `latest()` and\n * `history()` directly without going through the HTTP endpoint.\n */\n readonly runtimeSampler: RuntimeSampler | null\n /**\n * Memory analyzer fed by the runtime sampler. `null` when the\n * sampler is disabled. Use `analyzer.health(sampler.history())` to\n * compose a composite memory-health snapshot.\n */\n readonly memoryAnalyzer: MemoryAnalyzer | null\n}\n\n/**\n * DevToolsAdapter — Vue-style reactive introspection for KickJS applications.\n *\n * Exposes debug endpoints powered by reactive state (ref, computed, watch):\n * - `GET /_debug/routes` — all registered routes with middleware\n * - `GET /_debug/container` — DI registry with scopes and instantiation status\n * - `GET /_debug/metrics` — live request/error counts, error rate, uptime\n * - `GET /_debug/health` — deep health check with adapter status\n * - `GET /_debug/config` — sanitized environment variables (opt-in)\n * - `GET /_debug/state` — full reactive state snapshot\n *\n * @example\n * ```ts\n * import { DevToolsAdapter } from '@forinda/kickjs-devtools'\n *\n * bootstrap({\n * modules: [UserModule],\n * adapters: [\n * DevToolsAdapter({\n * // Default: on only under NODE_ENV=development or test.\n * exposeConfig: true,\n * configPrefixes: ['APP_', 'DATABASE_'],\n * }),\n * ],\n * })\n * ```\n */\nexport const DevToolsAdapter = defineAdapter<DevToolsOptions, DevToolsAdapterExtensions>({\n name: 'DevToolsAdapter',\n defaults: {\n basePath: '/_debug',\n errorRateThreshold: 0.5,\n exposeConfig: false,\n configPrefixes: ['APP_', 'NODE_ENV'],\n },\n build: (options) => {\n const basePath = options.basePath!\n // Fail closed: an unset NODE_ENV is how many production deploys run.\n const enabled =\n options.enabled ?? (process.env.NODE_ENV === 'development' || process.env.NODE_ENV === 'test')\n const exposeConfig = options.exposeConfig!\n const configPrefixes = options.configPrefixes!\n const errorRateThreshold = options.errorRateThreshold!\n const peerAdapters = options.adapters ?? []\n const requestLog = new RequestLog(options.requestLog ?? 200)\n // The dashboard's own polling would otherwise push the app's requests out.\n const logRequest = (info: Parameters<RequestLog['record']>[0]): void => {\n if (info.path !== basePath && !info.path.startsWith(`${basePath}/`)) requestLog.record(info)\n }\n\n // Secret token guard\n let secret: string | false\n if (options.secret === false) {\n secret = false\n } else if (options.secret) {\n secret = options.secret\n } else {\n secret = randomBytes(16).toString('hex')\n }\n\n // ── Runtime sampler + memory analyzer (kit) ────────────────────\n // Lazily instantiated so an adopter that disables runtime monitoring\n // doesn't pay the perf_hooks histogram cost. Lifecycle: start in\n // `beforeMount`, stop in `shutdown`.\n const runtimeEnabled = options.runtime?.enabled ?? true\n const runtimeSampler = runtimeEnabled\n ? new RuntimeSampler({\n intervalMs: options.runtime?.intervalMs ?? 1000,\n bufferSize: options.runtime?.bufferSize ?? 60,\n })\n : null\n const memoryAnalyzer = runtimeEnabled ? new MemoryAnalyzer() : null\n\n // ── Server-side event bus ──────────────────────────────────────\n // Lazily resolved in `beforeMount` — when the devtools adapter is\n // rebuilt during HMR, the same `Container` may already hold a bus\n // from a previous mount with live WebSocket clients + in-flight\n // subscriptions. Creating a fresh bus on every rebuild would\n // silently swap the registered instance, leaving plugins that\n // already captured the old reference publishing into a dead bus.\n // Using `container.has(DEVTOOLS_BUS)` to reuse instead keeps event\n // delivery stable across rebuilds.\n //\n // When the devtools adapter is disabled (`enabled === false`) we\n // register a no-op stub instead so adopters who Inject(DEVTOOLS_BUS)\n // under @Optional() get a callable instance rather than undefined.\n let bus: ServerBus | null = null\n\n const ensureBus = (container: Container): ServerBus => {\n if (container.has(DEVTOOLS_BUS)) {\n // The token is typed `KickEventBus` but the registered instance\n // is the wider `ServerBus` (KickEventBus + attachUpgrade /\n // close / clientCount). Explicit generic on resolve avoids the\n // structural-cast — Container.resolve's last overload accepts\n // an explicit T and returns it.\n bus = container.resolve<ServerBus>(DEVTOOLS_BUS)\n return bus\n }\n bus = enabled\n ? createServerBus({\n wsPath: `${basePath}/_bus`,\n secret: secret === false ? false : secret,\n })\n : ({\n on: () => () => {},\n onAny: () => () => {},\n emit: () => {},\n attachUpgrade: () => {},\n close: () => {},\n clientCount: () => 0,\n } as ServerBus)\n container.registerInstance(DEVTOOLS_BUS, bus)\n return bus\n }\n\n // ── Reactive state ─────────────────────────────────────────────\n const requestCount = ref(0)\n const errorCount = ref(0)\n const clientErrorCount = ref(0)\n // App mount timestamp — kept for reference, but uptime is derived from the\n // node process (see below) so it survives HMR rebuilds.\n const startedAt = ref(Date.now())\n const routeLatency = reactive<Record<string, RouteStats>>({})\n\n const errorRate = computed(() =>\n requestCount.value > 0 ? errorCount.value / requestCount.value : 0,\n )\n\n // Uptime is the NODE PROCESS uptime, not \"time since beforeMount\". In dev,\n // `beforeMount` re-runs on every HMR rebuild / SSR re-bootstrap, which used\n // to reset `startedAt` and pin uptime near 0s. `process.uptime()` is\n // monotonic from process start, so it reports the real server uptime across\n // reloads. Not a `computed()`: the process clock is not reactive, so a\n // computed would cache its first read and report a frozen uptime forever.\n const uptimeSeconds: ComputedRef<number> = {\n get value() {\n return Math.floor(process.uptime())\n },\n toJSON() {\n return this.value\n },\n dispose() {},\n }\n\n // ── Internal mutable state ─────────────────────────────────────\n let routes: RouteInfo[] = []\n let container: Container | null = null\n let appRef: any = null\n\n /**\n * One finished response into the reactive state. Latency is keyed by the\n * full route pattern (`GET /api/v1/users/:id`), so two modules' `/:id`\n * routes no longer share a bucket; unmatched requests (404 probing)\n * collapse into one `<unmatched>` bucket per method instead of one entry\n * per raw URL, which grew the map without bound.\n */\n const recordResponse = (\n method: string,\n route: string | undefined,\n status: number,\n elapsedMs: number,\n ): void => {\n requestCount.value++\n if (status >= 500) errorCount.value++\n else if (status >= 400) clientErrorCount.value++\n\n // Known methods only: a fetch-based host (Bun, Deno) passes any token.\n const verb = METHODS.has(method.toUpperCase()) ? method.toUpperCase() : 'OTHER'\n const routeKey = `${verb} ${route ?? '<unmatched>'}`\n if (!routeLatency[routeKey]) {\n routeLatency[routeKey] = {\n count: 0,\n totalMs: 0,\n minMs: Infinity,\n maxMs: 0,\n serverErrors: 0,\n clientErrors: 0,\n samples: [],\n }\n }\n const stats = routeLatency[routeKey]\n stats.count++\n if (status >= 500) stats.serverErrors++\n else if (status >= 400) stats.clientErrors++\n stats.totalMs += elapsedMs\n stats.minMs = Math.min(stats.minMs, elapsedMs)\n stats.maxMs = Math.max(stats.maxMs, elapsedMs)\n stats.samples.push(elapsedMs)\n if (stats.samples.length > MAX_SAMPLES) stats.samples.shift()\n }\n const adapterStatuses: Record<string, string> = {}\n let stopErrorWatch: (() => void) | null = null\n\n // Watch error rate — log warnings when elevated\n if (options.onErrorRateExceeded) {\n const callback = options.onErrorRateExceeded\n stopErrorWatch = watch(errorRate, (rate) => {\n if (rate > errorRateThreshold) {\n callback(rate)\n }\n })\n } else {\n stopErrorWatch = watch(errorRate, (rate) => {\n if (rate > errorRateThreshold) {\n log.warn(`Error rate elevated: ${(rate * 100).toFixed(1)}%`)\n }\n })\n }\n\n /**\n * Find the dashboard's public directory. Prefers the new Solid SPA\n * (public/spa/, shipped from PR 4 of §23); falls back to the legacy\n * Vue+Tailwind dashboard (public/devtools/) for environments that\n * haven't run `pnpm --filter @forinda/kickjs-devtools build:spa`.\n */\n const resolvePublicDir = (): string | null => {\n const thisDir = dirname(fileURLToPath(import.meta.url))\n const candidates = [\n join(thisDir, '..', 'public', 'spa'), // dist/ -> public/spa\n join(thisDir, '..', '..', 'public', 'spa'), // src/ -> public/spa\n join(thisDir, '..', 'public', 'devtools'), // legacy Vue dashboard\n join(thisDir, '..', '..', 'public', 'devtools'), // legacy from src/\n ]\n for (const dir of candidates) {\n if (existsSync(join(dir, 'index.html'))) return dir\n }\n return null\n }\n\n /**\n * Resolve peer adapters at request time. Prefers live adapters from the\n * Application registry (survives HMR rebuild) and falls back to the\n * constructor-provided refs.\n */\n const getPeerAdapters = (): any[] => {\n const kickApp = appRef?.__kickApp\n if (kickApp && typeof kickApp.getAdapters === 'function') {\n return kickApp.getAdapters()\n }\n return peerAdapters\n }\n\n /**\n * Active HTTP runtime identity + capabilities (`express` | `fastify` | `h3`),\n * read from the Application via `getActiveRuntime()`. Returns `null` when the\n * framework is older than the getter (graceful for mixed versions). Lets the\n * dashboard show which engine the app is running on.\n */\n const readActiveRuntime = (): {\n name: string\n capabilities: Record<string, boolean>\n } | null => {\n const kickApp = appRef?.__kickApp\n if (kickApp && typeof kickApp.getActiveRuntime === 'function') {\n try {\n return kickApp.getActiveRuntime()\n } catch {\n return null\n }\n }\n return null\n }\n\n return {\n // ── Extensions (TExtra) ───────────────────────────────────────\n requestCount,\n errorCount,\n clientErrorCount,\n startedAt,\n errorRate,\n uptimeSeconds,\n routeLatency,\n runtimeSampler,\n memoryAnalyzer,\n\n // ── DevTools introspection (architecture.md §23) ─────────────\n // Cheap snapshot — counters + flags only. Anything that requires\n // a container walk or DB hit belongs in the per-endpoint RPCs\n // (`/_debug/container`, `/_debug/graph`), not here.\n introspect(): IntrospectionSnapshot {\n return {\n protocolVersion: PROTOCOL_VERSION,\n name: 'DevToolsAdapter',\n kind: 'adapter',\n state: {\n basePath,\n enabled,\n runtimeEnabled,\n secret: secret === false ? false : 'present',\n },\n metrics: {\n requestCount: requestCount.value,\n serverErrors: errorCount.value,\n clientErrors: clientErrorCount.value,\n uptimeSeconds: uptimeSeconds.value,\n routesTracked: routes.length,\n },\n }\n },\n\n // ── Lifecycle ─────────────────────────────────────────────────\n\n beforeMount({ app, http, container: containerArg }) {\n if (!enabled) return\n\n appRef = app\n container = containerArg\n // Lazy resolve — reuses the existing bus when the same\n // container is reused across HMR rebuilds, otherwise creates\n // and registers a fresh instance. See `ensureBus` for why.\n // Also published process-wide, so a kick/db client created at import\n // time — before any container exists — reports its queries here.\n setActiveDevtoolsBus(ensureBus(containerArg))\n startedAt.value = Date.now()\n // Clear routes on rebuild/restart to prevent HMR duplication\n routes = []\n adapterStatuses['DevToolsAdapter'] = 'running'\n\n // Start the runtime sampler + memory analyzer here (rather than\n // at construction) so test harnesses that build the adapter but\n // never mount it don't leak interval timers.\n runtimeSampler?.start()\n memoryAnalyzer?.start()\n\n // Every dashboard route registers through the engine-agnostic HTTP\n // facade (`ctx.http`) and answers through `RequestContext`, so the\n // dashboard runs under Express, Fastify, and h3 alike. The dashboard\n // root (`'/'`) maps to `basePath` itself.\n //\n // The token guard runs inside each route rather than as a path-scoped\n // connect middleware: connect middleware sees the engine's raw\n // request, whose `path` / `query` differ per engine (and Express\n // strips the mount prefix while the others don't).\n const joinBase = (p: string): string => (p === '/' ? basePath : `${basePath}${p}`)\n const authorized = (ctx: RequestContext): boolean => {\n if (secret === false) return true\n const provided = ctx.headers['x-devtools-token'] ?? ctx.query?.token\n if (provided === secret) return true\n ctx.json({ error: 'Forbidden — invalid or missing devtools token' }, 403)\n return false\n }\n const guarded =\n (handler: (ctx: RequestContext) => unknown) =>\n (ctx: RequestContext): unknown =>\n authorized(ctx) ? handler(ctx) : undefined\n const router = {\n get: (path: string, handler: (ctx: RequestContext) => unknown): void =>\n http.route('GET', joinBase(path), guarded(handler)),\n post: (path: string, handler: (ctx: RequestContext) => unknown): void =>\n http.route('POST', joinBase(path), guarded(handler)),\n }\n\n router.get('/routes', (ctx: RequestContext) => {\n // Each route's context-contributor chain, from what the app actually\n // mounted — decorators, module, adapter and global, in execution order.\n const kickApp = appRef?.__kickApp as TopologyApplicationLike | undefined\n const mounted = kickApp?.getRoutes?.()\n if (!mounted) {\n ctx.json({ routes })\n return\n }\n const chains = new Map(\n mounted.map((r) => [\n `${r.method.toUpperCase()} ${r.controller}.${r.handler}`,\n r.contributors,\n ]),\n )\n ctx.json({\n routes: routes.map((r) => ({\n ...r,\n contributors: chains.get(`${r.method} ${r.controller}.${r.handler}`) ?? [],\n })),\n })\n })\n\n // Where a route's handler lives — \"open in editor\" in the dashboard\n // and the VS Code extension.\n router.get('/source', (ctx: RequestContext) => {\n const { controller, handler } = ctx.query as Record<string, string | undefined>\n const known = routes.some((r) => r.controller === controller && r.handler === handler)\n const found =\n known && controller && handler\n ? locateHandler(process.cwd(), controller, handler)\n : undefined\n if (!found) {\n ctx.json({ error: 'Handler source not found' }, 404)\n return\n }\n ctx.json(found)\n })\n\n router.get('/container', (ctx: RequestContext) => {\n // `__hmr__` entries are the dev server's shadow registrations — the\n // topology and graph endpoints leave them out too.\n const registrations = (container?.getRegistrations() ?? []).filter(\n (r) => !r.token.startsWith('__hmr__'),\n )\n ctx.json({ registrations, count: registrations.length })\n })\n\n // Recent requests, oldest first. `?since=<seq>` returns only newer ones,\n // so the Requests tab polls cheaply.\n router.get('/requests', (ctx: RequestContext) => {\n const since = Number((ctx.query as Record<string, unknown> | undefined)?.since ?? 0)\n ctx.json({\n requests: requestLog.after(Number.isFinite(since) ? since : 0),\n // A client holding a higher `seq` than this knows the app restarted.\n latest: requestLog.latest(),\n })\n })\n\n router.get('/metrics', (ctx: RequestContext) => {\n // Build latency with percentiles, omitting raw samples from response\n const latency: Record<string, any> = {}\n for (const [key, stats] of Object.entries(routeLatency)) {\n const { samples, ...rest } = stats\n latency[key] = {\n ...rest,\n ...computePercentiles(stats),\n histogram: latencyHistogram(samples),\n }\n }\n ctx.json({\n requests: requestCount.value,\n serverErrors: errorCount.value,\n clientErrors: clientErrorCount.value,\n errorRate: errorRate.value,\n uptimeSeconds: uptimeSeconds.value,\n startedAt: new Date(startedAt.value).toISOString(),\n routeLatency: latency,\n latencyBucketsMs: LATENCY_BUCKETS_MS,\n })\n })\n\n router.get('/health', async (ctx: RequestContext) => {\n const healthy = errorRate.value < errorRateThreshold\n const status = healthy ? 'healthy' : 'degraded'\n\n // Refresh peer statuses from `onHealthCheck()` when present.\n // The static `adapterStatuses` dict (seeded in `afterStart`)\n // proves a peer was mounted; calling `onHealthCheck()` here\n // upgrades that to a live `up`/`down`/`degraded` reading so\n // the Overview > Health card reflects actual adapter state,\n // not just \"registered at boot\".\n //\n // Each peer call is bounded by `PEER_HEALTHCHECK_TIMEOUT_MS` —\n // an adapter whose check hangs (network DB ping, remote\n // service, etc.) must not be able to stall the entire\n // dashboard request. Timed-out peers fall through to the\n // catch arm and are reported as `down`.\n const live: Record<string, string> = { ...adapterStatuses }\n await Promise.all(\n getPeerAdapters().map(async (peer) => {\n if (typeof peer?.onHealthCheck !== 'function') return\n const name: unknown = peer?.name\n if (typeof name !== 'string' || name === 'DevToolsAdapter') return\n try {\n const result = await raceWithTimeout(\n Promise.resolve(peer.onHealthCheck()),\n PEER_HEALTHCHECK_TIMEOUT_MS,\n `onHealthCheck(${name})`,\n )\n if (result && typeof result.status === 'string') {\n live[name] = result.status\n }\n } catch {\n live[name] = 'down'\n }\n }),\n )\n\n ctx.json(\n {\n status,\n errorRate: errorRate.value,\n uptime: uptimeSeconds.value,\n runtime: readActiveRuntime(),\n adapters: live,\n },\n healthy ? 200 : 503,\n )\n })\n\n router.get('/state', (ctx: RequestContext) => {\n const wsAdapter = getPeerAdapters().find(\n (a) => a.name === 'WsAdapter' && typeof a.getStats === 'function',\n )\n ctx.json({\n reactive: {\n requestCount: requestCount.value,\n errorCount: errorCount.value,\n clientErrorCount: clientErrorCount.value,\n errorRate: errorRate.value,\n uptimeSeconds: uptimeSeconds.value,\n startedAt: new Date(startedAt.value).toISOString(),\n },\n routes: routes.length,\n container: container?.getRegistrations().length ?? 0,\n routeLatency,\n ...(wsAdapter ? { ws: wsAdapter.getStats() } : {}),\n })\n })\n\n // ── Tier-1 runtime monitoring (architecture.md §23) ─────────\n // Returns the latest RuntimeSnapshot + a composite MemoryHealth\n // computed from the sampler's ring buffer. Returns 404 when the\n // sampler is disabled so adopters can distinguish \"off\" from\n // \"starting up\".\n router.get('/runtime', (ctx: RequestContext) => {\n if (!runtimeSampler || !memoryAnalyzer) {\n ctx.json({ error: 'runtime sampler disabled — set runtime.enabled = true' }, 404)\n return\n }\n const latest = runtimeSampler.latest()\n const history = runtimeSampler.history()\n const health = memoryAnalyzer.health(history)\n ctx.json({\n protocolVersion: PROTOCOL_VERSION,\n // Identity of the process these stats describe — every memory / CPU /\n // event-loop number below is for THIS Node process (the one running\n // your KickJS app), not the OS or any child. Lets the panel label it\n // unambiguously (\"Node v22 · pid 12345 · linux/x64 · engine: fastify\").\n process: {\n nodeVersion: process.version,\n pid: process.pid,\n platform: process.platform,\n arch: process.arch,\n runtime: readActiveRuntime(),\n },\n latest,\n history,\n health,\n })\n })\n\n // ── SSE: live runtime snapshots (architecture.md §23) ───────\n // Pushes the latest RuntimeSnapshot every `intervalMs`. SSE\n // (not WebSocket) avoids the http.Server `upgrade`-event\n // coordination cost — see devtools-flows.md §3 for rationale.\n router.get('/runtime/stream', (ctx: RequestContext) => {\n if (!runtimeSampler) {\n ctx.json({ error: 'runtime sampler disabled' }, 404)\n return\n }\n const sse = ctx.sse()\n const intervalMs = options.runtime?.intervalMs ?? 1000\n const tick = (): void => {\n const snap = runtimeSampler.latest()\n if (snap) sse.send(snap)\n }\n tick()\n const interval = setInterval(tick, intervalMs)\n const heartbeat = setInterval(() => sse.comment('heartbeat'), 30_000)\n sse.onClose(() => {\n clearInterval(interval)\n clearInterval(heartbeat)\n })\n })\n\n // ── SSE: memory health composite (architecture.md §23) ──────\n router.get('/memory/stream', (ctx: RequestContext) => {\n if (!runtimeSampler || !memoryAnalyzer) {\n ctx.json({ error: 'runtime sampler disabled' }, 404)\n return\n }\n const sse = ctx.sse()\n const intervalMs = options.runtime?.intervalMs ?? 1000\n const tick = (): void => {\n const snap = runtimeSampler.latest()\n if (!snap) return\n const health = memoryAnalyzer.health(runtimeSampler.history())\n sse.send({ snapshot: snap, health })\n }\n tick()\n const interval = setInterval(tick, intervalMs)\n const heartbeat = setInterval(() => sse.comment('heartbeat'), 30_000)\n sse.onClose(() => {\n clearInterval(interval)\n clearInterval(heartbeat)\n })\n })\n\n // ── Heap snapshot — Tier 3 monitoring (architecture.md §23) ─\n // POST /_debug/memory/snapshot — captures a V8 heap snapshot\n // and streams the resulting .heapsnapshot file back as\n // application/json with Content-Disposition. Snapshot capture\n // blocks the event loop while writing (multi-second pause for\n // large heaps), so the endpoint enforces single-flight via a\n // module-scoped flag — concurrent calls return 503.\n //\n // The file streams through `ctx.sendResponse` (engine-neutral, with\n // backpressure — never buffered whole) and the temp file is deleted\n // once the response completes, fails, or the client disconnects.\n // Worst case if the process dies mid-snapshot is a stranded file in\n // `os.tmpdir()`, which the OS cleans up on next boot.\n router.post('/memory/snapshot', async (ctx: RequestContext) => {\n if (snapshotInProgress) {\n ctx.json({ error: 'heap snapshot already in progress — wait for it to complete' }, 503)\n return\n }\n snapshotInProgress = true\n const startedAt = Date.now()\n const filename = `kickjs-heap-${new Date().toISOString().replace(/[:.]/g, '-')}.heapsnapshot`\n const targetPath = join(tmpdir(), filename)\n\n let written: string\n try {\n // writeHeapSnapshot is synchronous — the event loop is\n // blocked for the duration. There's no async variant in\n // the v8 module today; if one ships, swap here.\n written = writeHeapSnapshot(targetPath)\n } catch (err) {\n snapshotInProgress = false\n log.error({ err }, 'heap snapshot capture failed')\n ctx.json({ error: 'snapshot capture failed', message: (err as Error).message }, 500)\n return\n }\n\n let size = 0\n try {\n size = statSync(written).size\n } catch {\n /* size header is best-effort */\n }\n const elapsedMs = Date.now() - startedAt\n log.info(\n `Heap snapshot captured: ${written} (${(size / 1024 / 1024).toFixed(1)} MiB in ${elapsedMs}ms)`,\n )\n\n const headers: Record<string, string> = {\n 'Content-Type': 'application/json',\n 'Content-Disposition': `attachment; filename=\"${filename}\"`,\n 'X-Snapshot-Capture-Ms': String(elapsedMs),\n }\n if (size > 0) headers['Content-Length'] = String(size)\n\n const stream = createReadStream(written)\n try {\n await ctx.sendResponse(\n new Response(Readable.toWeb(stream) as ReadableStream, { headers }),\n )\n } catch (err) {\n log.error({ err }, 'heap snapshot stream failed')\n } finally {\n // Also covers a client that disconnected mid-download:\n // sendResponse stops reading, so close the file here.\n stream.destroy()\n unlink(written, (err) => {\n if (err) log.warn(`Failed to delete heap snapshot ${written}: ${err.message}`)\n snapshotInProgress = false\n })\n }\n })\n\n // ── Force GC — Tier 3 monitoring (architecture.md §23) ──────\n // POST /_debug/memory/gc — calls `global.gc()` if Node was\n // started with --expose-gc, returning before/after heap size\n // so the panel can show how much was reclaimed. Useful for\n // confirming \"is this growth real or just delayed GC?\".\n //\n // Returns 412 Precondition Failed (with hint) when the flag\n // wasn't set, so the SPA can show \"Run with --expose-gc to\n // enable this button\" instead of a generic 5xx.\n router.post('/memory/gc', (ctx: RequestContext) => {\n const gc = (globalThis as { gc?: () => void }).gc\n if (typeof gc !== 'function') {\n ctx.json(\n {\n error:\n 'global.gc unavailable — start Node with --expose-gc to enable this endpoint',\n },\n 412,\n )\n return\n }\n const before = process.memoryUsage().heapUsed\n const startedAt = Date.now()\n try {\n gc()\n } catch (err) {\n ctx.json(\n {\n error: 'forced GC failed',\n message: err instanceof Error ? err.message : String(err),\n },\n 500,\n )\n return\n }\n const after = process.memoryUsage().heapUsed\n const reclaimedBytes = Math.max(0, before - after)\n const elapsedMs = Date.now() - startedAt\n log.info(\n `Forced GC reclaimed ${(reclaimedBytes / 1024 / 1024).toFixed(2)} MiB in ${elapsedMs}ms`,\n )\n ctx.json({ before, after, reclaimedBytes, elapsedMs })\n })\n\n // ── Custom-tab discovery (architecture.md §23) ──────────────\n // Walks every plugin/adapter `devtoolsTabs?()` contribution\n // and serves the deduped + validated list. The SPA fetches\n // this once at boot to render dynamic tabs after the four\n // built-ins.\n // Adapter conditions — which adapters a `when` turned off, and why.\n router.get('/conditions', (ctx: RequestContext) => {\n const kickApp = appRef?.__kickApp as TopologyApplicationLike | undefined\n const report = kickApp?.getConditionReport?.()\n if (!report) {\n ctx.json({ error: 'condition report unavailable' }, 503)\n return\n }\n ctx.json({ adapters: report })\n })\n\n // Startup timeline — each phase and adapter hook of the last boot.\n router.get('/startup', (ctx: RequestContext) => {\n const kickApp = appRef?.__kickApp as TopologyApplicationLike | undefined\n const timeline = kickApp?.getStartupTimeline?.()\n if (!timeline) {\n ctx.json({ error: 'startup timeline unavailable' }, 503)\n return\n }\n ctx.json(timeline)\n })\n\n router.get('/tabs', (ctx: RequestContext) => {\n const kickApp = appRef?.__kickApp as TopologyApplicationLike | undefined\n if (!kickApp) {\n ctx.json({ error: 'tabs unavailable — application surface not exposed' }, 503)\n return\n }\n ctx.json(collectDevtoolsTabs(kickApp))\n })\n\n // A `launch` tab's button — runs the action's `run()` on the server.\n router.post('/tabs/run', async (ctx: RequestContext) => {\n const kickApp = appRef?.__kickApp as TopologyApplicationLike | undefined\n if (!kickApp) {\n ctx.json({ error: 'tabs unavailable — application surface not exposed' }, 503)\n return\n }\n const { tab, action } = (ctx.query ?? {}) as { tab?: string; action?: string }\n const { status, body } = await runTabAction(\n kickApp,\n String(tab ?? ''),\n String(action ?? ''),\n )\n ctx.json(body, status)\n })\n\n // ── Jobs: browse and manage via any adapter's / plugin's jobInspector() ──\n const inspectors = () => {\n const kickApp = appRef?.__kickApp as TopologyApplicationLike | undefined\n return kickApp ? findJobInspectors(kickApp) : new Map()\n }\n const query = (ctx: RequestContext) => (ctx.query ?? {}) as Record<string, unknown>\n router.get('/jobs', async (ctx: RequestContext) => {\n const r = await listJobSources(inspectors())\n ctx.json(r.body, r.status)\n })\n router.get('/jobs/list', async (ctx: RequestContext) => {\n const r = await listJobs(inspectors(), query(ctx))\n ctx.json(r.body, r.status)\n })\n router.get('/jobs/job', async (ctx: RequestContext) => {\n const r = await getJob(inspectors(), query(ctx))\n ctx.json(r.body, r.status)\n })\n router.post('/jobs/action', async (ctx: RequestContext) => {\n const r = await runJobAction(inspectors(), query(ctx))\n ctx.json(r.body, r.status)\n })\n\n // ── @Cron jobs — whatever schedules them ────────────────────\n router.get('/cron', (ctx: RequestContext) => {\n ctx.json({ jobs: container ? cronSnapshot(container) : [] })\n })\n router.post('/cron/run', (ctx: RequestContext) => {\n const name = query(ctx).name\n if (!container || typeof name !== 'string') {\n ctx.json({ error: 'name is required' }, 400)\n return\n }\n const r = runCronJobNow(container, name)\n ctx.json(r.body, r.status)\n })\n\n // ── Topology RPC (architecture.md §23) ──────────────────────\n // Aggregates plugins + adapters + contributors + DI tokens\n // into one snapshot; calls each primitive's introspect() in\n // parallel with a per-call timeout so a misbehaving adapter\n // can't block the endpoint. Errors are collected, not thrown.\n router.get('/topology', async (ctx: RequestContext) => {\n if (!container) {\n ctx.json({ error: 'topology unavailable — container not bound yet' }, 503)\n return\n }\n const kickApp = appRef?.__kickApp as TopologyApplicationLike | undefined\n if (!kickApp) {\n ctx.json({ error: 'topology unavailable — application surface not exposed' }, 503)\n return\n }\n try {\n const snapshot = await collectTopologySnapshot({ app: kickApp, container })\n ctx.json(snapshot)\n } catch (err) {\n const message = err instanceof Error ? err.message : String(err)\n ctx.json({ error: 'topology collection failed', message }, 500)\n }\n })\n\n router.get('/ws', (ctx: RequestContext) => {\n const wsAdapter = getPeerAdapters().find(\n (a) => a.name === 'WsAdapter' && typeof a.getStats === 'function',\n )\n if (!wsAdapter) {\n ctx.json({ enabled: false, message: 'WsAdapter not found' })\n return\n }\n ctx.json({ enabled: true, ...wsAdapter.getStats() })\n })\n\n router.get('/queues', async (ctx: RequestContext) => {\n const queueAdapter = getPeerAdapters().find(\n (a) => a.name === 'QueueAdapter' && typeof a.getQueueNames === 'function',\n )\n if (!queueAdapter) {\n ctx.json({ enabled: false, message: 'QueueAdapter not found' })\n return\n }\n try {\n const names: string[] = queueAdapter.getQueueNames?.() ?? []\n const queues: any[] = []\n for (const name of names) {\n const stats = await queueAdapter.getQueueStats?.(name)\n queues.push({ name, ...stats })\n }\n ctx.json({ enabled: true, queues })\n } catch {\n ctx.json({ enabled: true, queues: [], error: 'Failed to fetch queue stats' })\n }\n })\n\n // ── Dependency graph ────────────────────────────────────────\n router.get('/graph', (ctx: RequestContext) => {\n const registrations = container?.getRegistrations() ?? []\n const nodes = registrations\n .filter((r) => !r.token.startsWith('__hmr__'))\n .map((r) => ({\n id: r.token,\n kind: r.kind,\n scope: r.scope,\n resolveCount: r.resolveCount,\n }))\n const nodeIds = new Set(nodes.map((n) => n.id))\n const edges: Array<{ from: string; to: string }> = []\n for (const r of registrations) {\n if (r.token.startsWith('__hmr__')) continue\n for (const dep of r.dependencies) {\n if (nodeIds.has(dep)) {\n edges.push({ from: r.token, to: dep })\n }\n }\n }\n ctx.json({ nodes, edges })\n })\n\n // ── SSE stream for real-time updates ────────────────────────\n router.get('/stream', (ctx: RequestContext) => {\n const sse = ctx.sse()\n\n const sendMetrics = () => {\n sse.send({\n type: 'metrics',\n requestCount: requestCount.value,\n errorCount: errorCount.value,\n clientErrorCount: clientErrorCount.value,\n errorRate: errorRate.value,\n uptimeSeconds: uptimeSeconds.value,\n })\n }\n sendMetrics()\n\n const unsubContainer = container?.onChange?.((changes) => {\n sse.send({ type: 'container', changes, timestamp: Date.now() })\n sendMetrics()\n })\n\n const stopRequestWatch = watch(requestCount, () => sendMetrics())\n const stopErrWatch = watch(errorCount, () => sendMetrics())\n\n const heartbeat = setInterval(() => sse.comment('heartbeat'), 30000)\n // An idle app changes no counter, so without this tick the\n // dashboard's uptime and \"Updated\" time would freeze.\n const tick = setInterval(sendMetrics, METRICS_TICK_MS)\n\n sse.onClose(() => {\n unsubContainer?.()\n stopRequestWatch()\n stopErrWatch()\n clearInterval(heartbeat)\n clearInterval(tick)\n })\n })\n\n // Where each declared variable's value comes from: env, a `_FILE`, the\n // schema default, `provide()`, or unset. Names only, never values, so\n // it is served without `exposeConfig`.\n router.get('/config/origins', (ctx: RequestContext) => {\n const app = appRef?.__kickApp as { configOrigins?: () => unknown } | undefined\n ctx.json(app?.configOrigins?.() ?? { env: [], sections: [] })\n })\n\n if (exposeConfig) {\n router.get('/config', (ctx: RequestContext) => {\n // Where each declared variable's value comes from — names only.\n const origins = (\n appRef?.__kickApp as { configOrigins?: () => ConfigOrigins } | undefined\n )?.configOrigins?.()\n // The app's own variables — its env schema and settings sections.\n // Anything else in the environment (the shell's, the platform's)\n // isn't the app's config, and even its name stays out.\n const declared = new Set(\n [...(origins?.env ?? []), ...(origins?.sections ?? []).flatMap((s) => s.vars)].map(\n (v) => v.name,\n ),\n )\n const config: Record<string, string> = {}\n for (const [key, value] of Object.entries(process.env)) {\n if (value === undefined) continue\n if (configPrefixes.some((prefix) => key.startsWith(prefix))) config[key] = value\n else if (declared.has(key)) config[key] = '[REDACTED]'\n }\n ctx.json({ config, origins })\n })\n }\n\n // Dashboard UI — Vue + Tailwind from public/devtools directory\n const publicDir = resolvePublicDir()\n // The page itself is not token-guarded — it loads first and sends the\n // token on its API calls — unless a `?token=` is given, which must match.\n const dashboard =\n (html: string) =>\n (ctx: RequestContext): unknown =>\n ctx.query?.token === undefined || authorized(ctx) ? ctx.html(html) : undefined\n if (publicDir) {\n // Serve only the SPA's `assets/` statically, so the static server can\n // never answer the dashboard root with the raw `index.html` — the\n // route below owns it and injects `data-base`. The legacy Vue\n // dashboard keeps its files at the top level.\n const assetsDir = join(publicDir, 'assets')\n if (existsSync(assetsDir)) http.serveStatic(`${basePath}/assets`, assetsDir)\n else http.serveStatic(basePath, publicDir)\n\n const indexHtml = readFileSync(join(publicDir, 'index.html'), 'utf-8')\n http.route(\n 'GET',\n basePath,\n dashboard(indexHtml.replace('<body', `<body data-base=\"${basePath}\"`)),\n )\n } else {\n http.route('GET', basePath, dashboard('<h1>DevTools: public directory not found</h1>'))\n }\n\n if (secret) {\n log.info(`DevTools mounted at ${basePath} [token: ${secret}]`)\n log.info(`Access: ${basePath}?token=${secret}`)\n } else {\n log.info(`DevTools mounted at ${basePath} [no guard]`)\n }\n },\n\n /**\n * Feeds the reactive counters (`requestCount`, `errorCount`,\n * `routeLatency`, …) from the framework's response funnel — one\n * listener for every observer instead of one middleware per adapter.\n */\n onResponse(info) {\n if (!enabled) return\n recordResponse(info.method, info.route, info.status, info.durationMs)\n logRequest(info)\n },\n\n /** Attaches a failed request's error to its Requests-tab entry. */\n onError(err, info) {\n if (!enabled || info.source !== 'request') return\n requestLog.recordError(info.requestId, err)\n },\n\n middleware(): AdapterMiddleware[] {\n // The framework already reports every response to `onResponse`.\n if (!enabled || FRAMEWORK_REPORTS_RESPONSES) return []\n\n return [\n {\n // Older kickjs: time the response ourselves. Connect-style, so it\n // sees the engine's raw Node request/response on every runtime.\n handler: (req: IncomingMessage, res: ServerResponse, next: () => void) => {\n const start = Date.now()\n res.on('finish', () => {\n const matched = (req as unknown as Record<symbol, MatchedRoute | undefined>)[\n MATCHED_ROUTE_SLOT\n ]\n const route = matched?.pattern ?? matched?.path\n const durationMs = Date.now() - start\n recordResponse(req.method ?? 'GET', route, res.statusCode, durationMs)\n logRequest({\n method: req.method ?? 'GET',\n path: (req.url ?? '/').split('?')[0]!,\n route,\n status: res.statusCode,\n durationMs,\n })\n })\n next()\n },\n phase: 'beforeGlobal',\n },\n ]\n },\n\n onRouteMount(controllerClass, mountPath) {\n if (!enabled) return\n\n const collectedRoutes = getClassMeta<\n Array<{ method: string; path: string; handlerName: string; validation?: RouteValidation }>\n >(METADATA.ROUTES, controllerClass, [])\n\n const classMiddleware = getClassMeta<any[]>(METADATA.CLASS_MIDDLEWARES, controllerClass, [])\n\n for (const route of collectedRoutes) {\n const methodMiddleware = getMethodMeta<any[]>(\n METADATA.METHOD_MIDDLEWARES,\n controllerClass.prototype,\n route.handlerName,\n [],\n )\n\n routes.push({\n method: route.method.toUpperCase(),\n path: `${mountPath}${route.path === '/' ? '' : route.path}`,\n controller: controllerClass.name,\n handler: route.handlerName,\n middleware: [\n ...classMiddleware.map((m: any) => m.name || 'anonymous'),\n ...methodMiddleware.map((m: any) => m.name || 'anonymous'),\n ],\n flags: Object.fromEntries(getRouteFlags(controllerClass, route.handlerName)),\n ...uploadInfo(controllerClass, route.handlerName),\n ...requestSchemas(route.validation),\n })\n }\n },\n\n afterStart({ server }) {\n if (!enabled) return\n // Wire the bus's WS upgrade to the framework's http.Server.\n // Path-based routing inside attachUpgrade means kickjs-ws or\n // any other adapter sharing the same listener stays\n // unaffected — only `${basePath}/_bus` is claimed.\n if (server) bus?.attachUpgrade(server)\n\n // Seed `/_debug/health.adapters` with every peer the framework\n // mounted alongside us. Before this, only `DevToolsAdapter`\n // appeared in the Overview > Health card because the dict was\n // only ever written in `beforeMount` (self) and `shutdown`\n // (self). Peers with `onHealthCheck` get their live status;\n // anything else is reported as `running` since reaching\n // `afterStart` means the framework already mounted them.\n for (const peer of getPeerAdapters()) {\n const name = peer?.name\n if (typeof name !== 'string' || name === 'DevToolsAdapter') continue\n adapterStatuses[name] = 'running'\n }\n // Count @Cron runs from the first tick, before anyone opens the Jobs tab.\n if (container) trackContainerCronJobs(container)\n\n log.info(\n `DevTools ready — ${routes.length} routes tracked, ` +\n `${container?.getRegistrations().length ?? 0} DI bindings, ` +\n `event bus on ${basePath}/_bus`,\n )\n },\n\n shutdown() {\n stopErrorWatch?.()\n runtimeSampler?.stop()\n memoryAnalyzer?.stop()\n if (bus && getActiveDevtoolsBus() === bus) setActiveDevtoolsBus(undefined)\n bus?.close()\n adapterStatuses['DevToolsAdapter'] = 'stopped'\n },\n }\n },\n})\n"],"mappings":";;;;;;;;;;85BAyCA,MAAM,QAAU,OAAO,IAAI,6BAA6B,EAClD,sBAAQ,IAAI,IAQZ,MAAS,KACb,GAAG,IAAI,OAAO,KAAK,GAAG,IAAI,cAE5B,SAAS,SAAS,IAA2B,CAC3C,IAAI,EAAI,MAAM,IAAI,GAAG,EAErB,OADK,GAAG,MAAM,IAAI,IAAM,EAAI,CAAE,KAAM,EAAG,SAAU,EAAG,QAAS,CAAE,CAAE,EAC1D,CACT,CAMA,SAAgB,cAAc,KAAgC,CAC5D,IAAK,IAAM,OAAO,KAAM,CACtB,IAAM,MAAQ,IAAI,OAAO,UACnB,SAAW,MAAM,IAAI,aAC3B,GAAI,OAAO,UAAa,YAAe,SAAkC,SAAU,SACnF,IAAM,EAAI,SAAS,MAAM,GAAG,CAAC,EACvB,QAAU,UAAmB,QAA0B,CAC3D,EAAE,UACF,EAAE,OACF,EAAE,eAAiB,YAAY,IAAI,EAAI,UACvC,EAAE,YAAc,QAAU,IAAA,GAAY,KAAO,SACzC,QAAU,IAAA,KACZ,EAAE,WACF,EAAE,UAAY,iBAAiB,MAAQ,MAAM,QAAU,OAAO,KAAK,EAEvE,EACM,QAAU,SAAyB,GAAG,KAA0B,CACpE,EAAE,UACF,EAAE,cAAgB,KAAK,IAAI,EAC3B,IAAM,UAAY,YAAY,IAAI,EAC9B,OACJ,GAAI,CACF,OAAU,SAA0C,MAAM,KAAM,IAAI,CACtE,OAAS,IAAK,CAEZ,MADA,OAAO,UAAW,KAAO,gBAAI,MAAM,OAAO,CAAC,EACrC,GACR,CAcA,OAbI,QAAU,OAAQ,OAA4B,MAAS,WACjD,OAA4B,KACjC,QACC,OAAO,SAAS,EACT,OAER,KAAiB,CAEhB,MADA,OAAO,UAAW,KAAO,gBAAI,MAAM,UAAU,CAAC,EACxC,GACR,CACF,GAEF,OAAO,SAAS,EACT,OACT,EACA,OAAO,eAAe,QAAS,QAAS,CAAE,MAAO,EAAK,CAAC,EACvD,OAAO,eAAe,QAAS,OAAQ,CAAE,MAAQ,SAA8B,IAAK,CAAC,EACrF,MAAM,IAAI,aAAe,OAC3B,CACF,CAGA,SAAgB,uBAAuB,UAA4B,CACjE,cAAc,aAAa,SAAS,CAAC,CACvC,CAKA,SAAS,aAAuB,CAC9B,GAAI,CACF,GAAM,CAAE,MAAS,cAAc,KAAK,QAAQ,IAAI,EAAG,SAAS,CAAC,CAAC,CAAC,QAAQ,EAMvE,OAAQ,WAAY,WAAa,CAC/B,GAAI,CACF,IAAM,EAAI,IAAI,KAAK,WAAY,CAAE,OAAQ,GAAM,GAAI,SAAW,CAAE,QAAS,EAAI,CAAC,CAAG,CAAC,EAC5E,KAAO,EAAE,QAAQ,EAEvB,OADA,EAAE,KAAK,EACA,KAAO,KAAK,QAAQ,EAAI,IACjC,MAAQ,CACN,OAAO,IACT,CACF,CACF,MAAQ,CACN,UAAa,IACf,CACF,CACA,IAAI,QAGJ,SAAgB,aAAa,UAAsC,CACjE,IAAM,KAAO,aAAa,SAAS,EAGnC,OAFA,cAAc,IAAI,EAClB,UAAY,YAAY,EACjB,KAAK,IAAK,MAAS,CACxB,KAAM,IAAI,KACV,UAAW,IAAI,OAAO,KACtB,QAAS,IAAI,YACb,WAAY,IAAI,WAChB,GAAI,IAAI,SAAW,CAAE,SAAU,IAAI,QAAS,EAAI,CAAC,EACjD,GAAI,IAAI,YAAc,CAAE,YAAa,IAAI,WAAY,EAAI,CAAC,EAC1D,QAAS,iBAAiB,GAAG,EAC7B,QAAS,CAAC,CAAC,IAAI,QACf,UAAW,CAAC,CAAC,IAAI,UACjB,UAAW,QAAS,IAAI,WAAY,IAAI,QAAQ,EAChD,MAAO,CAAE,GAAG,SAAS,MAAM,GAAG,CAAC,CAAE,CACnC,EAAE,CACJ,CAOA,SAAgB,cACd,UACA,KACmD,CACnD,IAAM,IAAM,aAAa,SAAS,CAAC,CAAC,KAAM,GAAM,EAAE,OAAS,IAAI,EAS/D,OARK,IACA,iBAAiB,GAAG,GACzB,cAAc,CAAC,GAAG,CAAC,EACf,CAAC,IAAI,SAAW,SAAS,MAAM,GAAG,CAAC,CAAC,CAAC,QAAU,EAC1C,CAAE,OAAQ,IAAK,KAAM,CAAE,MAAO,GAAG,KAAK,oBAAqB,CAAE,GAGtE,WAAgB,IAAK,UAAW,QAAQ,CAAC,CAAC,UAAY,CAAC,CAAC,EACjD,CAAE,OAAQ,IAAK,KAAM,CAAE,QAAS,EAAK,CAAE,IAPX,CAAE,OAAQ,IAAK,KAAM,CAAE,MAAO,GAAG,KAAK,aAAc,CAAE,EADxE,CAAE,OAAQ,IAAK,KAAM,CAAE,MAAO,sBAAsB,KAAK,EAAG,CAAE,CASjF,CCzGA,MAGM,MAAkC,CACtC,WACA,UACA,WACA,QACA,aACA,eACA,cACA,eACA,cACA,aACA,UACA,aACA,UACA,gBACA,UACF,EASA,eAAsB,wBACpB,KAC2B,CAC3B,GAAM,CAAE,IAAK,UAAW,oBAAsB,KAAkC,KAC1E,OAA0B,CAAC,EAE3B,QAAU,MAAM,QAAQ,IAC5B,IAAI,WAAW,CAAC,CAAC,IAAK,GAAM,YAAY,EAAG,SAAU,oBAAqB,MAAM,CAAC,CACnF,EACM,SAAW,MAAM,QAAQ,IAC7B,IAAI,YAAY,CAAC,CAAC,IAAK,GAAM,YAAY,EAAG,UAAW,oBAAqB,MAAM,CAAC,CACrF,EAEA,MAAO,CACL,gBAAiB,iBACjB,UAAW,KAAK,IAAI,EACpB,QACA,SACA,aAAc,oBAAoB,GAAG,EACrC,SAAU,cAAc,SAAS,EACjC,MACF,CACF,CAOA,eAAe,YACb,UACA,KACA,UACA,OACgC,CAChC,IAAM,KAAO,UAAU,MAAQ,YACzB,MAAQ,MAAM,OAAQ,GAAM,OAAQ,UAAsC,IAAO,UAAU,EAC3F,UAAY,MAAM,QAAQ,UAAU,SAAS,EAAI,CAAC,GAAG,UAAU,SAAS,EAAI,CAAC,EAC7E,KAA8B,CAClC,gBAAiB,iBACjB,KACA,KACA,MACA,SACF,EACA,GAAI,OAAO,UAAU,YAAe,WAAY,OAAO,KAEvD,GAAI,CACF,IAAM,OAAS,MAAM,QAAQ,KAAK,CAChC,QAAQ,QAAQ,UAAU,WAAW,CAAC,EACtC,IAAI,SAAgB,EAAG,SACrB,eACQ,OAAO,gBAAI,MAAM,gCAAgC,UAAU,GAAG,CAAC,EACrE,SACF,CACF,CACF,CAAC,EACD,GAAI,QAAU,OAAO,QAAW,SAAU,CAKxC,IAAM,KAAO,OACb,MAAO,CACL,gBAAiB,iBACjB,KAAM,KAAK,MAAQ,KACnB,KAAM,KAAK,MAAQ,KACnB,QAAS,KAAK,QACd,MAAO,KAAK,MACZ,OAAQ,KAAK,OACb,QAAS,KAAK,QACd,YAAa,KAAK,YAClB,MACA,SACF,CACF,CACA,OAAO,IACT,OAAS,IAAK,CAMZ,OALA,OAAO,KAAK,CACV,KACA,KACA,QAAS,eAAe,MAAQ,IAAI,QAAU,OAAO,GAAG,CAC1D,CAAC,EACM,IACT,CACF,CAGA,SAAS,cAAc,UAA4C,CACjE,OAAO,UACJ,iBAAiB,CAAC,CAClB,OAAQ,GAAM,CAAC,EAAE,MAAM,WAAW,SAAS,CAAC,CAAC,CAC7C,IAAK,IAAO,CACX,MAAO,EAAE,MACT,MAAO,EAAE,MACT,KAAM,EAAE,KACR,aAAc,EAAE,YAClB,EAAE,CACN,CAaA,SAAS,oBAAoB,IAA0D,CACrF,IAAM,qBAAO,IAAI,IAEjB,GAAI,OAAO,IAAI,iBAAoB,WAAY,CAC7C,IAAK,IAAM,KAAK,IAAI,gBAAgB,EAAG,CAGrC,IAAM,GAAK,GAAG,EAAE,OAAO,GAAG,EAAE,MACxB,KAAK,IAAI,EAAE,GAMf,KAAK,IAAI,GAAI,CACX,IAAK,EAAE,IACP,OAAQ,EAAE,OACV,UAAW,EAAE,SACf,CAAC,CACH,CACA,MAAO,CAAC,GAAG,KAAK,OAAO,CAAC,CAC1B,CAGA,IAAM,QACJ,KACA,SACS,CACT,GAAK,KACL,IAAK,IAAM,OAAO,KAAM,CACtB,IAAM,EAAI,IACJ,IAAM,OAAO,EAAE,KAAQ,SAAW,EAAE,IAAM,KAC3C,KAAO,MAAK,IAAI,GAAG,GACxB,KAAK,IAAI,IAAK,CACZ,IACA,OACA,UAAW,MAAM,QAAQ,EAAE,SAAS,EAAI,EAAE,UAAY,CAAC,CACzD,CAAC,CACH,CACF,EAEA,IAAK,IAAM,UAAU,IAAI,WAAW,EAClC,OAAO,OAAO,eAAe,EAAG,SAAS,EAE3C,IAAK,IAAM,WAAW,IAAI,YAAY,EACpC,OAAO,QAAQ,eAAe,EAAG,SAAS,EAG5C,MAAO,CAAC,GAAG,KAAK,OAAO,CAAC,CAC1B,CCjPA,MAAM,iCAAmB,IAAI,IAAI,CAAC,SAAU,SAAU,OAAQ,QAAQ,CAAC,EAOvE,SAAgB,oBAAoB,IAAkD,CACpF,IAAM,KAAgC,CAAC,EACjC,qBAAO,IAAI,IACX,OAAoD,CAAC,EAErD,QAAU,WAAoB,MAAuB,CACzD,GAAI,CAAC,MAAM,QAAQ,GAAG,EACpB,OAAO,KAAK,CAAE,OAAQ,WAAY,OAAQ,wCAAyC,CAAC,OAGtF,IAAK,IAAM,SAAS,IAAK,CACvB,IAAM,WAAa,YAAY,KAAK,EACpC,GAAI,CAAC,WAAW,GAAI,CAClB,OAAO,KAAK,CAAE,OAAQ,WAAY,OAAQ,WAAW,MAAO,CAAC,EAC7D,QACF,CACA,IAAM,IAAM,WAAW,IACjB,MAAQ,KAAK,IAAI,IAAI,EAAE,EACzB,MACF,OAAO,KAAK,CACV,OAAQ,WACR,OAAQ,WAAW,IAAI,GAAG,0BAA0B,MAAM,qBAC5D,CAAC,GAGH,KAAK,IAAI,IAAI,GAAI,UAAU,EAC3B,KAAK,KAAK,GAAG,EACf,CACF,EAEA,IAAK,IAAM,UAAU,IAAI,WAAW,EAClC,GAAI,OAAO,OAAO,cAAiB,WACnC,GAAI,CACF,OAAO,OAAO,MAAQ,mBAAoB,OAAO,aAAa,CAAC,CACjE,OAAS,IAAK,CACZ,OAAO,KAAK,CACV,OAAQ,OAAO,MAAQ,mBACvB,OAAQ,eAAe,MAAQ,IAAI,QAAU,OAAO,GAAG,CACzD,CAAC,CACH,CAEF,IAAK,IAAM,WAAW,IAAI,YAAY,EACpC,GAAI,OAAO,QAAQ,cAAiB,WACpC,GAAI,CACF,OAAO,QAAQ,MAAQ,oBAAqB,QAAQ,aAAa,CAAC,CACpE,OAAS,IAAK,CACZ,OAAO,KAAK,CACV,OAAQ,QAAQ,MAAQ,oBACxB,OAAQ,eAAe,MAAQ,IAAI,QAAU,OAAO,GAAG,CACzD,CAAC,CACH,CAGF,MAAO,CAAE,KAAM,MAAO,CACxB,CAkBA,SAAS,YAAY,IAA6C,CAChE,GAAI,CAAC,KAAO,OAAO,KAAQ,SACzB,MAAO,CAAE,GAAI,GAAO,OAAQ,4BAA6B,EAE3D,IAAM,IAAM,IACZ,GAAI,OAAO,IAAI,IAAO,UAAY,IAAI,GAAG,SAAW,EAClD,MAAO,CAAE,GAAI,GAAO,OAAQ,mCAAoC,EAElE,GAAI,OAAO,IAAI,OAAU,UAAY,IAAI,MAAM,SAAW,EACxD,MAAO,CAAE,GAAI,GAAO,OAAQ,QAAQ,IAAI,GAAG,oCAAqC,EAElF,IAAM,KAAO,IAAI,KACjB,GAAI,CAAC,MAAQ,OAAO,MAAS,SAC3B,MAAO,CAAE,GAAI,GAAO,OAAQ,QAAQ,IAAI,GAAG,2BAA4B,EAEzE,GAAI,OAAO,KAAK,MAAS,UAAY,CAAC,iBAAiB,IAAI,KAAK,IAAI,EAClE,MAAO,CACL,GAAI,GACJ,OAAQ,QAAQ,IAAI,GAAG,4DACzB,EAEF,OAAQ,KAAK,KAAb,CACE,IAAK,SACH,GAAI,OAAO,KAAK,KAAQ,UAAY,KAAK,IAAI,SAAW,EACtD,MAAO,CAAE,GAAI,GAAO,OAAQ,QAAQ,IAAI,GAAG,wCAAyC,EAEtF,MACF,IAAK,OACH,GAAI,OAAO,KAAK,MAAS,SACvB,MAAO,CAAE,GAAI,GAAO,OAAQ,QAAQ,IAAI,GAAG,kCAAmC,EAEhF,MACF,IAAK,SACH,GAAI,OAAO,KAAK,KAAQ,UAAY,KAAK,IAAI,SAAW,EACtD,MAAO,CAAE,GAAI,GAAO,OAAQ,QAAQ,IAAI,GAAG,wCAAyC,EAEtF,MACF,IAAK,SACH,GAAI,CAAC,MAAM,QAAQ,KAAK,OAAO,EAC7B,MAAO,CAAE,GAAI,GAAO,OAAQ,QAAQ,IAAI,GAAG,sCAAuC,CAGxF,CACA,MAAO,CAAE,GAAI,GAAM,IAAK,GAAwC,CAClE,CAMA,eAAsB,aACpB,IACA,MACA,SACyE,CACzE,IAAM,IAAM,oBAAoB,GAAG,CAAC,CAAC,KAAK,KAAM,GAAM,EAAE,KAAO,KAAK,EAC9D,OACJ,KAAK,KAAK,OAAS,SAAW,IAAI,KAAK,QAAQ,KAAM,GAAM,EAAE,KAAO,QAAQ,EAAI,IAAA,GAClF,GAAI,CAAC,OAAQ,MAAO,CAAE,OAAQ,IAAK,KAAM,CAAE,MAAO,cAAc,SAAS,YAAY,MAAM,EAAG,CAAE,EAChG,GAAI,OAAO,OAAO,KAAQ,WACxB,MAAO,CAAE,OAAQ,IAAK,KAAM,CAAE,MAAO,WAAW,SAAS,eAAgB,CAAE,EAE7E,GAAI,CACF,MAAO,CAAE,OAAQ,IAAK,KAAM,CAAE,OAAS,MAAM,OAAO,IAAI,GAAM,IAAK,CAAE,CACvE,OAAS,IAAK,CACZ,MAAO,CAAE,OAAQ,IAAK,KAAM,CAAE,MAAO,eAAe,MAAQ,IAAI,QAAU,OAAO,GAAG,CAAE,CAAE,CAC1F,CACF,CCzIA,IAAa,WAAb,KAAwB,CAKO,SAJ7B,QAAqC,CAAC,EACtC,8BAAwB,IAAI,IAC5B,IAAc,EAEd,YAAY,SAAmC,CAAlB,KAAA,SAAA,QAAmB,CAEhD,OAAO,KAAgB,IAAM,KAAK,IAAI,EAAS,CAC7C,GAAI,KAAK,UAAY,EAAG,OACxB,IAAM,MAAyB,CAAE,IAAK,EAAE,KAAK,IAAK,GAAI,IAAK,GAAG,IAAK,EAC/D,KAAK,WAAa,KAAK,cAAc,IAAI,KAAK,SAAS,IACzD,MAAM,MAAQ,KAAK,cAAc,IAAI,KAAK,SAAS,EACnD,KAAK,cAAc,OAAO,KAAK,SAAS,GAE1C,KAAK,QAAQ,KAAK,KAAK,EACnB,KAAK,QAAQ,OAAS,KAAK,UAAU,KAAK,QAAQ,MAAM,CAC9D,CAEA,YAAY,UAA+B,IAAoB,CAC7D,GAAI,KAAK,UAAY,GAAK,CAAC,UAAW,OACtC,IAAM,MACJ,eAAe,MACX,CAAE,KAAM,IAAI,KAAM,QAAS,IAAI,OAAQ,EACvC,CAAE,KAAM,QAAS,QAAS,OAAO,GAAG,CAAE,EAE5C,IAAK,IAAI,EAAI,KAAK,QAAQ,OAAS,EAAG,GAAK,EAAG,IAC5C,GAAI,KAAK,QAAQ,EAAE,CAAE,YAAc,UAAW,CAC5C,KAAK,QAAQ,EAAE,CAAE,MAAQ,MACzB,MACF,CAEF,KAAK,cAAc,IAAI,UAAW,KAAK,EACnC,KAAK,cAAc,KAAO,KAC5B,KAAK,cAAc,OAAO,KAAK,cAAc,KAAK,CAAC,CAAC,KAAK,CAAC,CAAC,KAAM,CAErE,CAGA,QAAiB,CACf,OAAO,KAAK,GACd,CAGA,MAAM,MAAQ,EAAsB,CAClC,OAAO,KAAK,QAAQ,OAAQ,GAAM,EAAE,IAAM,KAAK,CACjD,CACF,ECpEA,MAAM,uBAA8B,IAAI,IAAI,CAC1C,UACA,SACA,UACA,YACA,SACA,QACF,CAAC,EACK,QAAU,CAAC,QAAS,SAAU,WAAY,QAAS,QAAS,QAAQ,EAOpE,KAAO,MAAe,OAAS,OAAgB,CAAE,OAAQ,KAAM,CAAE,KAAM,CAAE,GAG/E,SAAgB,kBAAkB,IAAyD,CACzF,IAAM,sBAAQ,IAAI,IAClB,IAAK,IAAM,QAAS,CAAC,GAAG,IAAI,YAAY,EAAG,GAAG,IAAI,WAAW,CAAC,EAAG,CAC/D,IAAM,IAAO,MAA4D,aACzE,GAAI,OAAO,KAAQ,WAAY,SAC/B,IAAM,UAAY,IAAI,KAAK,KAAK,EAC5B,WAAW,MAAM,IAAI,MAAM,MAAQ,OAAQ,SAAS,CAC1D,CACA,OAAO,KACT,CAGA,eAAsB,eAAe,WAAuD,CAW1F,MAAO,CAAE,OAAQ,IAAK,KAAM,CAAE,QAAA,MAVR,QAAQ,IAC5B,CAAC,GAAG,UAAU,CAAC,CAAC,IAAI,MAAO,CAAC,OAAQ,aAAe,CACjD,IAAM,QAAU,QAAQ,OAAQ,GAAM,OAAO,UAAU,IAAO,UAAU,EACxE,GAAI,CACF,MAAO,CAAE,OAAQ,QAAS,OAAQ,MAAM,UAAU,OAAO,CAAE,CAC7D,OAAS,IAAK,CACZ,MAAO,CAAE,OAAQ,QAAS,OAAQ,CAAC,EAAqB,MAAO,QAAQ,GAAG,CAAE,CAC9E,CACF,CAAC,CACH,CACsC,CAAE,CAC1C,CAGA,eAAsB,SACpB,WACA,EACgB,CAChB,IAAM,UAAY,WAAW,IAAI,OAAO,EAAE,QAAU,EAAE,CAAC,EACvD,GAAI,CAAC,UAAW,OAAO,IAAI,iBAAkB,GAAG,EAChD,IAAM,MAAQ,OAAO,EAAE,OAAS,QAAQ,EACxC,GAAI,CAAC,OAAO,IAAI,KAAK,EAAG,OAAO,IAAI,kBAAkB,MAAM,EAAE,EAC7D,IAAM,MAAQ,KAAK,IAAI,EAAG,OAAO,EAAE,OAAS,CAAC,GAAK,CAAC,EAC7C,IAAM,KAAK,IAAI,MAAQ,IAAW,EAAG,KAAK,IAAI,MAAO,OAAO,EAAE,KAAO,MAAQ,EAAE,GAAK,CAAC,CAAC,EAC5F,GAAI,CAEF,MAAO,CAAE,OAAQ,IAAK,KAAM,CAAE,KAAA,MADX,UAAU,KAAK,OAAO,EAAE,OAAS,EAAE,EAAG,MAAmB,CAAE,MAAO,GAAI,CAAC,CACvD,CAAE,CACvC,OAAS,IAAK,CACZ,OAAO,IAAI,QAAQ,GAAG,EAAG,GAAG,CAC9B,CACF,CAGA,eAAsB,OACpB,WACA,EACgB,CAChB,IAAM,UAAY,WAAW,IAAI,OAAO,EAAE,QAAU,EAAE,CAAC,EACvD,GAAI,CAAC,UAAW,OAAO,IAAI,iBAAkB,GAAG,EAChD,GAAI,CACF,IAAM,IAAM,MAAM,UAAU,IAAI,OAAO,EAAE,OAAS,EAAE,EAAG,OAAO,EAAE,IAAM,EAAE,CAAC,EACzE,OAAO,IAAM,CAAE,OAAQ,IAAK,KAAM,GAAI,EAAI,IAAI,gBAAiB,GAAG,CACpE,OAAS,IAAK,CACZ,OAAO,IAAI,QAAQ,GAAG,EAAG,GAAG,CAC9B,CACF,CAGA,eAAsB,aACpB,WACA,EACgB,CAChB,IAAM,UAAY,WAAW,IAAI,OAAO,EAAE,QAAU,EAAE,CAAC,EACvD,GAAI,CAAC,UAAW,OAAO,IAAI,iBAAkB,GAAG,EAChD,IAAM,OAAS,OAAO,EAAE,QAAU,EAAE,EACpC,GAAI,CAAC,QAAQ,SAAS,MAAM,EAAG,OAAO,IAAI,mBAAmB,OAAO,EAAE,EACtE,IAAM,MAAQ,OAAO,EAAE,OAAS,EAAE,EAC5B,GAAK,OAAO,EAAE,IAAM,EAAE,EACtB,MAAQ,OAAO,EAAE,OAAS,EAAE,EAClC,GAAI,CACF,OAAQ,OAAR,CACE,IAAK,QACL,IAAK,SAIH,OAHK,GACA,UAAU,SACf,MAAM,UAAU,OAAO,CAAE,MAAO,EAAE,EAC3B,CAAE,OAAQ,IAAK,KAAM,CAAE,GAAI,EAAK,CAAE,GAFV,IAAI,GAAG,OAAO,mBAAoB,GAAG,EADpD,IAAI,gBAAgB,EAItC,IAAK,QAGH,OAFK,OAAO,IAAI,KAAK,EAChB,UAAU,MACR,CACL,OAAQ,IACR,KAAM,CAAE,GAAI,GAAM,MAAO,MAAM,UAAU,MAAM,MAAO,KAAiB,CAAE,CAC3E,EAJ6B,IAAI,yBAA0B,GAAG,EAD/B,IAAI,mBAAmB,EAMxD,QAEE,OADK,UAAU,QACR,CAAE,OAAQ,IAAK,KAAM,CAAE,GAAI,GAAM,MAAQ,MAAM,UAAU,OAAO,CAAE,KAAK,GAAM,IAAK,CAAE,EAD5D,IAAI,GAAG,OAAO,mBAAoB,GAAG,CAExE,CACF,OAAS,IAAK,CACZ,OAAO,IAAI,QAAQ,GAAG,EAAG,GAAG,CAC9B,CACF,CAEA,MAAM,QAAW,KAA0B,eAAe,MAAQ,IAAI,QAAU,OAAO,GAAG,ECxGpF,OAAU,MAAiB,KAAK,QAAQ,sBAAuB,MAAM,EAG3E,SAAgB,gBACd,OACA,WACA,QACoB,CACpB,IAAM,IAAU,OAAO,eAAe,OAAO,UAAU,EAAE,IAAI,CAAC,CAAC,KAAK,MAAM,EAC1E,GAAI,CAAC,IAAK,OACV,IAAM,OAAU,QAAmB,OAAO,MAAM,EAAG,MAAM,CAAC,CAAC,MAAM;CAAI,CAAC,CAAC,OAGjE,MAAQ,IAAI,MAAQ,IAAI,EAAE,CAAC,OAC3B,KAAO,OAAO,MAAM,KAAK,EACzB,KAAO,KAAK,OAAO,wDAAwD,EAC3E,KAAO,OAAS,GAAK,KAAO,KAAK,MAAM,EAAG,IAAI,EAC9C,OAAa,OACjB,sEAAsE,OAAO,OAAO,EAAE,UACtF,GACF,CAAC,CAAC,KAAK,IAAI,EACX,OAAO,OAAO,OAAS,MAAQ,OAAO,MAAQ,OAAO,EAAE,CAAC,OAAO,IAAI,EAAI,IAAI,KAAK,CAClF,CAGA,SAAgB,cACd,KACA,WACA,QAC2B,CAC3B,IAAM,IAAM,WAAW,KAAK,KAAM,KAAK,CAAC,EAAI,KAAK,KAAM,KAAK,EAAI,KAC1D,MAAQ,YAAY,IAAK,CAAE,UAAW,GAAM,SAAU,OAAQ,CAAC,CAAC,CAAC,OACpE,GACC,eAAe,KAAK,CAAC,GACrB,CAAC,gBAAgB,KAAK,CAAC,GACvB,CAAC,EAAE,MAAM,GAAG,CAAC,CAAC,SAAS,cAAc,GACrC,CAAC,EAAE,MAAM,GAAG,CAAC,CAAC,SAAS,MAAM,CACjC,EACA,IAAK,IAAM,KAAK,MAAO,CACrB,IAAM,KAAO,KAAK,IAAK,CAAC,EACpB,OACJ,GAAI,CACF,OAAS,aAAa,KAAM,OAAO,CACrC,MAAQ,CACN,QACF,CACA,GAAI,CAAC,OAAO,SAAS,UAAU,EAAG,SAClC,IAAM,KAAO,gBAAgB,OAAQ,WAAY,OAAO,EACxD,GAAI,OAAS,IAAA,GACX,MAAO,CAAE,KAAM,SAAU,SAAS,KAAM,IAAI,CAAC,CAAC,MAAM,GAAG,CAAC,CAAC,KAAK,GAAG,EAAG,IAAK,CAE7E,CAEF,CCGA,SAAgB,gBAAgB,KAAyB,CAAC,EAAc,CACtE,IAAM,OAAS,KAAK,QAAU,eACxB,OAAS,KAAK,QAAU,GACxB,KAAO,cAAc,EACrB,IAAM,IAAI,gBAAgB,CAAE,SAAU,EAAK,CAAC,EAC5C,wBAAU,IAAI,IAChB,eAAoC,KACpC,eAAwF,KAE5F,IAAI,GAAG,aAAe,IAAkB,CACtC,QAAQ,IAAI,EAAE,EACd,GAAG,GAAG,UAAY,MAA0B,CAC1C,GAAI,CACF,IAAM,KAAO,OAAO,MAAS,SAAW,KAAO,KAAK,SAAS,OAAO,EAC9D,OAAS,KAAK,MAAM,IAAI,EAC9B,GAAI,CAAC,QAAU,OAAO,OAAO,MAAS,UAAY,OAAO,OAAO,IAAO,SAAU,OAIjF,KAAK,SAAS,CAAE,GAAG,OAAQ,cAAe,IAAK,CAAsB,CACvE,MAAQ,CAGR,CACF,CAAC,EACD,GAAG,GAAG,YAAe,CACnB,QAAQ,OAAO,EAAE,CACnB,CAAC,EACD,GAAG,GAAG,YAAe,CACnB,QAAQ,OAAO,EAAE,CACnB,CAAC,CACH,CAAC,EAED,IAAM,OAAU,UAAmC,CACjD,GAAI,QAAQ,OAAS,EAAG,OAKxB,IAAI,KACJ,GAAI,CACF,KAAO,KAAK,UAAU,UAAW,KAAM,QACrC,OAAO,OAAU,SAAW,MAAM,SAAS,EAAI,KACjD,CACF,MAAQ,CACN,MACF,CACA,IAAK,IAAM,UAAU,QAGnB,GAAI,OAAO,aAAe,OAAO,KAC/B,GAAI,CACF,OAAO,KAAK,IAAI,CAClB,MAAQ,CAGR,CAGN,EAEM,UAAa,KAAkC,CACnD,GAAI,SAAW,GAAO,MAAO,GAC7B,IAAM,OAAS,IAAI,QAAQ,oBAC3B,GAAI,OAAO,QAAW,UAAY,SAAW,OAAQ,MAAO,GAE5D,IAAM,IAAM,IAAI,KAAO,GACjB,SAAW,IAAI,QAAQ,GAAG,EAGhC,OAFI,WAAa,IAEV,IADY,gBAAgB,IAAI,MAAM,SAAW,CAAC,CAC7C,CAAC,CAAC,IAAI,OAAO,IAAM,MACjC,EA2CA,MAAO,CACL,GAAG,KAAc,QAAkD,CACjE,OAAO,KAAK,GAAG,KAAM,OAAO,CAC9B,EACA,MAAM,QAA0D,CAC9D,OAAO,KAAK,MAAM,OAAO,CAC3B,EACA,KAAK,KAAc,QAAwB,CACzC,IAAM,SAA2B,CAC/B,KACA,QACA,GAAI,KAAK,IAAI,EACb,cAAe,OACjB,EACA,KAAK,SAAS,QAAQ,EACtB,OAAO,QAAQ,CACjB,EACA,cA1DqB,YAAiC,CAClD,iBACJ,eAAiB,WACjB,gBAAkB,IAAK,OAAQ,OAAS,EAI1B,IAAI,KAAO,GAAA,CACF,MAAM,GAAG,CAAC,CAAC,KACf,SAEZ,UAAU,GAAG,EAMlB,IAAI,cAAc,IAAK,OAAQ,KAAO,IAAkB,CACtD,IAAI,KAAK,aAAc,GAAI,GAAG,CAChC,CAAC,GAPC,OAAO,MAAM;;CAAmC,EAChD,OAAO,QAAQ,GAOnB,EACA,WAAW,GAAG,UAAW,cAAc,EACzC,EAqCE,UAnCwB,CACpB,gBAAkB,gBACpB,eAAe,IAAI,UAAW,cAAc,EAE9C,eAAiB,KACjB,eAAiB,KACjB,IAAK,IAAM,UAAU,QACnB,GAAI,CACF,OAAO,MAAM,CACf,MAAQ,CAER,CAEF,QAAQ,MAAM,EACd,IAAI,MAAM,CACZ,EAqBE,gBAAmB,QAAQ,IAC7B,CACF,CCpKA,MAAM,IAAM,aAAa,UAAU,EAGnC,SAAS,WAAW,gBAAsB,YAAgD,CACxF,IAAM,OAAS,cAEb,SAAS,YAAa,gBAAiB,YAAa,IAAA,EAAS,EAC/D,GAAI,CAAC,OAAQ,MAAO,CAAC,EACrB,GAAM,CAAE,KAAM,UAAW,UAAa,OACtC,MAAO,CACL,OAAQ,CAAE,KAAM,GAAI,UAAY,CAAE,SAAU,EAAI,CAAC,EAAI,GAAI,SAAW,CAAE,QAAS,EAAI,CAAC,CAAG,CACzF,CACF,CAgCA,SAAS,eAAe,WAAqE,CAC3F,GAAI,CAAC,WAAY,MAAO,CAAC,EACzB,IAAM,QAA6C,CAAC,EACpD,IAAK,IAAM,OAAQ,CAAC,OAAQ,QAAS,QAAQ,EAC3C,GAAK,WAAW,MAChB,GAAI,CACF,QAAQ,MAAQ,aAAa,WAAW,KAAK,CAAC,CAAC,aAAa,CAC1D,OAAQ,cACR,GAAI,OACN,CAAC,CACH,MAAQ,CAER,CAEF,OAAO,OAAO,KAAK,OAAO,CAAC,CAAC,OAAS,EAAI,CAAE,OAAQ,EAAI,CAAC,CAC1D,CAmBA,MAAM,mBAAqB,CAAC,EAAG,EAAG,EAAG,GAAI,GAAI,GAAI,IAAK,IAAK,IAAK,GAAI,EAGpE,SAAS,iBAAiB,QAAsC,CAC9D,IAAM,OAAS,MAAM,KAAK,CAAE,OAAQ,mBAAmB,OAAS,CAAE,MAAS,CAAC,EAC5E,IAAK,IAAM,MAAM,QAAS,CACxB,IAAM,EAAI,mBAAmB,UAAW,OAAU,IAAM,KAAK,EAC7D,OAAO,IAAM,GAAK,mBAAmB,OAAS,EAAE,EAClD,CACA,OAAO,MACT,CAEA,MACM,wBAAU,IAAI,IAAI,CAAC,MAAO,OAAQ,OAAQ,MAAO,SAAU,QAAS,UAAW,OAAO,CAAC,EAOvF,mBAAqB,OAAO,IAAI,YAAY,EAQ5C,4BACJ,OAAQ,KAAsC,gBAAmB,WAGnE,SAAS,WAAW,OAAkB,EAAmB,CACvD,GAAI,OAAO,SAAW,EAAG,MAAO,GAChC,IAAM,IAAM,KAAK,KAAK,EAAI,OAAO,MAAM,EAAI,EAC3C,OAAO,OAAO,KAAK,IAAI,EAAG,GAAG,EAC/B,CAGA,SAAS,mBAAmB,MAA8D,CACxF,IAAM,OAAS,CAAC,GAAG,MAAM,OAAO,CAAC,CAAC,UAAU,EAAG,IAAM,EAAI,CAAC,EAC1D,MAAO,CACL,IAAK,WAAW,OAAQ,EAAG,EAC3B,IAAK,WAAW,OAAQ,GAAI,EAC5B,IAAK,WAAW,OAAQ,GAAI,CAC9B,CACF,CASA,SAAS,gBAAmB,QAAqB,GAAY,MAA2B,CACtF,IAAI,MACE,QAAU,IAAI,SAAgB,EAAG,SAAW,CAChD,MAAQ,eAAiB,OAAO,gBAAI,MAAM,GAAG,MAAM,mBAAmB,GAAG,GAAG,CAAC,EAAG,EAAE,CACpF,CAAC,EACD,OAAO,QAAQ,KAAK,CAAC,QAAS,OAAO,CAAC,CAAC,CAAC,YAAc,CAChD,OAAO,aAAa,KAAK,CAC/B,CAAC,CACH,CAwBA,IAAI,mBAAqB,GA+HzB,MAAa,gBAAkB,cAA0D,CACvF,KAAM,kBACN,SAAU,CACR,SAAU,UACV,mBAAoB,GACpB,aAAc,GACd,eAAgB,CAAC,OAAQ,UAAU,CACrC,EACA,MAAQ,SAAY,CAClB,IAAM,SAAW,QAAQ,SAEnB,QACJ,QAAQ,UAAY,QAAQ,IAAI,WAAa,eAAiB,QAAQ,IAAI,WAAa,QACnF,aAAe,QAAQ,aACvB,eAAiB,QAAQ,eACzB,mBAAqB,QAAQ,mBAC7B,aAAe,QAAQ,UAAY,CAAC,EACpC,WAAa,IAAI,WAAW,QAAQ,YAAc,GAAG,EAErD,WAAc,MAAoD,CAClE,KAAK,OAAS,UAAY,CAAC,KAAK,KAAK,WAAW,GAAG,SAAS,EAAE,GAAG,WAAW,OAAO,IAAI,CAC7F,EAGI,OACJ,AAKE,OALE,QAAQ,SAAW,GACZ,GACA,QAAQ,OACR,QAAQ,OAER,YAAY,EAAE,CAAC,CAAC,SAAS,KAAK,EAOzC,IAAM,eAAiB,QAAQ,SAAS,SAAW,GAC7C,eAAiB,eACnB,IAAI,eAAe,CACjB,WAAY,QAAQ,SAAS,YAAc,IAC3C,WAAY,QAAQ,SAAS,YAAc,EAC7C,CAAC,EACD,KACE,eAAiB,eAAiB,IAAI,eAAmB,KAe3D,IAAwB,KAEtB,UAAa,WACb,UAAU,IAAI,YAAY,GAM5B,IAAM,UAAU,QAAmB,YAAY,EACxC,MAET,IAAM,QACF,gBAAgB,CACd,OAAQ,GAAG,SAAS,OACpB,OAAQ,SAAW,IAAgB,MACrC,CAAC,EACA,CACC,WAAgB,CAAC,EACjB,cAAmB,CAAC,EACpB,SAAY,CAAC,EACb,kBAAqB,CAAC,EACtB,UAAa,CAAC,EACd,gBAAmB,CACrB,EACJ,UAAU,iBAAiB,aAAc,GAAG,EACrC,KAIH,aAAe,IAAI,CAAC,EACpB,WAAa,IAAI,CAAC,EAClB,iBAAmB,IAAI,CAAC,EAGxB,UAAY,IAAI,KAAK,IAAI,CAAC,EAC1B,aAAe,SAAqC,CAAC,CAAC,EAEtD,UAAY,aAChB,aAAa,MAAQ,EAAI,WAAW,MAAQ,aAAa,MAAQ,CACnE,EAQM,cAAqC,CACzC,IAAI,OAAQ,CACV,OAAO,KAAK,MAAM,QAAQ,OAAO,CAAC,CACpC,EACA,QAAS,CACP,OAAO,KAAK,KACd,EACA,SAAU,CAAC,CACb,EAGI,OAAsB,CAAC,EACvB,UAA8B,KAC9B,OAAc,KASZ,gBACJ,OACA,MACA,OACA,YACS,CACT,aAAa,QACT,QAAU,IAAK,WAAW,QACrB,QAAU,KAAK,iBAAiB,QAIzC,IAAM,SAAW,GADJ,QAAQ,IAAI,OAAO,YAAY,CAAC,EAAI,OAAO,YAAY,EAAI,QAC/C,GAAG,OAAS,gBAChC,aAAa,YAChB,aAAa,UAAY,CACvB,MAAO,EACP,QAAS,EACT,MAAO,IACP,MAAO,EACP,aAAc,EACd,aAAc,EACd,QAAS,CAAC,CACZ,GAEF,IAAM,MAAQ,aAAa,UAC3B,MAAM,QACF,QAAU,IAAK,MAAM,eAChB,QAAU,KAAK,MAAM,eAC9B,MAAM,SAAW,UACjB,MAAM,MAAQ,KAAK,IAAI,MAAM,MAAO,SAAS,EAC7C,MAAM,MAAQ,KAAK,IAAI,MAAM,MAAO,SAAS,EAC7C,MAAM,QAAQ,KAAK,SAAS,EACxB,MAAM,QAAQ,OAAS,KAAa,MAAM,QAAQ,MAAM,CAC9D,EACM,gBAA0C,CAAC,EAC7C,eAAsC,KAG1C,GAAI,QAAQ,oBAAqB,CAC/B,IAAM,SAAW,QAAQ,oBACzB,eAAiB,MAAM,UAAY,MAAS,CACtC,KAAO,oBACT,SAAS,IAAI,CAEjB,CAAC,CACH,KACE,gBAAiB,MAAM,UAAY,MAAS,CACtC,KAAO,oBACT,IAAI,KAAK,yBAAyB,KAAO,IAAA,CAAK,QAAQ,CAAC,EAAE,EAAE,CAE/D,CAAC,EASH,IAAM,qBAAwC,CAC5C,IAAM,QAAU,QAAQ,cAAc,YAAY,GAAG,CAAC,EAChD,WAAa,CACjB,KAAK,QAAS,KAAM,SAAU,KAAK,EACnC,KAAK,QAAS,KAAM,KAAM,SAAU,KAAK,EACzC,KAAK,QAAS,KAAM,SAAU,UAAU,EACxC,KAAK,QAAS,KAAM,KAAM,SAAU,UAAU,CAChD,EACA,IAAK,IAAM,OAAO,WAChB,GAAI,WAAW,KAAK,IAAK,YAAY,CAAC,EAAG,OAAO,IAElD,OAAO,IACT,EAOM,oBAA+B,CACnC,IAAM,QAAU,QAAQ,UAIxB,OAHI,SAAW,OAAO,QAAQ,aAAgB,WACrC,QAAQ,YAAY,EAEtB,YACT,EAQM,sBAGM,CACV,IAAM,QAAU,QAAQ,UACxB,GAAI,SAAW,OAAO,QAAQ,kBAAqB,WACjD,GAAI,CACF,OAAO,QAAQ,iBAAiB,CAClC,MAAQ,CACN,OAAO,IACT,CAEF,OAAO,IACT,EAEA,MAAO,CAEL,aACA,WACA,iBACA,UACA,UACA,cACA,aACA,eACA,eAMA,YAAoC,CAClC,MAAO,CACL,gBAAiB,iBACjB,KAAM,kBACN,KAAM,UACN,MAAO,CACL,SACA,QACA,eACA,OAAQ,SAAW,IAAgB,SACrC,EACA,QAAS,CACP,aAAc,aAAa,MAC3B,aAAc,WAAW,MACzB,aAAc,iBAAiB,MAC/B,cAAe,cAAc,MAC7B,cAAe,OAAO,MACxB,CACF,CACF,EAIA,YAAY,CAAE,IAAK,KAAM,UAAW,cAAgB,CAClD,GAAI,CAAC,QAAS,OAEd,OAAS,IACT,UAAY,aAMZ,qBAAqB,UAAU,YAAY,CAAC,EAC5C,UAAU,MAAQ,KAAK,IAAI,EAE3B,OAAS,CAAC,EACV,gBAAgBA,gBAAqB,UAKrC,gBAAgB,MAAM,EACtB,gBAAgB,MAAM,EAWtB,IAAM,SAAY,GAAuB,IAAM,IAAM,SAAW,GAAG,WAAW,IACxE,WAAc,KACd,SAAW,KACE,IAAI,QAAQ,qBAAuB,IAAI,OAAO,SAC9C,SACjB,IAAI,KAAK,CAAE,MAAO,+CAAgD,EAAG,GAAG,EACjE,IAEH,QACH,SACA,KACC,WAAW,GAAG,EAAI,QAAQ,GAAG,EAAI,IAAA,GAC/B,OAAS,CACb,KAAM,KAAc,UAClB,KAAK,MAAM,MAAO,SAAS,IAAI,EAAG,QAAQ,OAAO,CAAC,EACpD,MAAO,KAAc,UACnB,KAAK,MAAM,OAAQ,SAAS,IAAI,EAAG,QAAQ,OAAO,CAAC,CACvD,EAEA,OAAO,IAAI,UAAY,KAAwB,CAI7C,IAAM,SADU,QAAQ,UAAA,EACC,YAAY,EACrC,GAAI,CAAC,QAAS,CACZ,IAAI,KAAK,CAAE,MAAO,CAAC,EACnB,MACF,CACA,IAAM,OAAS,IAAI,IACjB,QAAQ,IAAK,GAAM,CACjB,GAAG,EAAE,OAAO,YAAY,EAAE,GAAG,EAAE,WAAW,GAAG,EAAE,UAC/C,EAAE,YACJ,CAAC,CACH,EACA,IAAI,KAAK,CACP,OAAQ,OAAO,IAAK,IAAO,CACzB,GAAG,EACH,aAAc,OAAO,IAAI,GAAG,EAAE,OAAO,GAAG,EAAE,WAAW,GAAG,EAAE,SAAS,GAAK,CAAC,CAC3E,EAAE,CACJ,CAAC,CACH,CAAC,EAID,OAAO,IAAI,UAAY,KAAwB,CAC7C,GAAM,CAAE,WAAY,SAAY,IAAI,MAE9B,MADQ,OAAO,KAAM,GAAM,EAAE,aAAe,YAAc,EAAE,UAAY,OAExE,GAAK,YAAc,QACnB,cAAc,QAAQ,IAAI,EAAG,WAAY,OAAO,EAChD,IAAA,GACD,MAIL,IAAI,KAAK,KAAK,EAHZ,IAAI,KAAK,CAAE,MAAO,0BAA2B,EAAG,GAAG,CAIvD,CAAC,EAED,OAAO,IAAI,aAAe,KAAwB,CAGhD,IAAM,eAAiB,WAAW,iBAAiB,GAAK,CAAC,EAAA,CAAG,OACzD,GAAM,CAAC,EAAE,MAAM,WAAW,SAAS,CACtC,EACA,IAAI,KAAK,CAAE,cAAe,MAAO,cAAc,MAAO,CAAC,CACzD,CAAC,EAID,OAAO,IAAI,YAAc,KAAwB,CAC/C,IAAM,MAAQ,OAAQ,IAAI,OAA+C,OAAS,CAAC,EACnF,IAAI,KAAK,CACP,SAAU,WAAW,MAAM,OAAO,SAAS,KAAK,EAAI,MAAQ,CAAC,EAE7D,OAAQ,WAAW,OAAO,CAC5B,CAAC,CACH,CAAC,EAED,OAAO,IAAI,WAAa,KAAwB,CAE9C,IAAM,QAA+B,CAAC,EACtC,IAAK,GAAM,CAAC,IAAK,SAAU,OAAO,QAAQ,YAAY,EAAG,CACvD,GAAM,CAAE,QAAS,GAAG,MAAS,MAC7B,QAAQ,KAAO,CACb,GAAG,KACH,GAAG,mBAAmB,KAAK,EAC3B,UAAW,iBAAiB,OAAO,CACrC,CACF,CACA,IAAI,KAAK,CACP,SAAU,aAAa,MACvB,aAAc,WAAW,MACzB,aAAc,iBAAiB,MAC/B,UAAW,UAAU,MACrB,cAAe,cAAc,MAC7B,UAAW,IAAI,KAAK,UAAU,KAAK,CAAC,CAAC,YAAY,EACjD,aAAc,QACd,iBAAkB,kBACpB,CAAC,CACH,CAAC,EAED,OAAO,IAAI,UAAW,KAAO,MAAwB,CACnD,IAAM,QAAU,UAAU,MAAQ,mBAC5B,OAAS,QAAU,UAAY,WAc/B,KAA+B,CAAE,GAAG,eAAgB,EAC1D,MAAM,QAAQ,IACZ,gBAAgB,CAAC,CAAC,IAAI,KAAO,OAAS,CACpC,GAAI,OAAO,MAAM,eAAkB,WAAY,OAC/C,IAAM,KAAgB,MAAM,KAC5B,GAAI,OAAO,MAAS,UAAY,OAAS,kBACzC,GAAI,CACF,IAAM,OAAS,MAAM,gBACnB,QAAQ,QAAQ,KAAK,cAAc,CAAC,EACpC,KACA,iBAAiB,KAAK,EACxB,EACI,QAAU,OAAO,OAAO,QAAW,WACrC,KAAK,MAAQ,OAAO,OAExB,MAAQ,CACN,KAAK,MAAQ,MACf,CACF,CAAC,CACH,EAEA,IAAI,KACF,CACE,OACA,UAAW,UAAU,MACrB,OAAQ,cAAc,MACtB,QAAS,kBAAkB,EAC3B,SAAU,IACZ,EACA,QAAU,IAAM,GAClB,CACF,CAAC,EAED,OAAO,IAAI,SAAW,KAAwB,CAC5C,IAAM,UAAY,gBAAgB,CAAC,CAAC,KACjC,GAAM,EAAE,OAAS,aAAe,OAAO,EAAE,UAAa,UACzD,EACA,IAAI,KAAK,CACP,SAAU,CACR,aAAc,aAAa,MAC3B,WAAY,WAAW,MACvB,iBAAkB,iBAAiB,MACnC,UAAW,UAAU,MACrB,cAAe,cAAc,MAC7B,UAAW,IAAI,KAAK,UAAU,KAAK,CAAC,CAAC,YAAY,CACnD,EACA,OAAQ,OAAO,OACf,UAAW,WAAW,iBAAiB,CAAC,CAAC,QAAU,EACnD,aACA,GAAI,UAAY,CAAE,GAAI,UAAU,SAAS,CAAE,EAAI,CAAC,CAClD,CAAC,CACH,CAAC,EAOD,OAAO,IAAI,WAAa,KAAwB,CAC9C,GAAI,CAAC,gBAAkB,CAAC,eAAgB,CACtC,IAAI,KAAK,CAAE,MAAO,uDAAwD,EAAG,GAAG,EAChF,MACF,CACA,IAAM,OAAS,eAAe,OAAO,EAC/B,QAAU,eAAe,QAAQ,EACjC,OAAS,eAAe,OAAO,OAAO,EAC5C,IAAI,KAAK,CACP,gBAAiB,iBAKjB,QAAS,CACP,YAAa,QAAQ,QACrB,IAAK,QAAQ,IACb,SAAU,QAAQ,SAClB,KAAM,QAAQ,KACd,QAAS,kBAAkB,CAC7B,EACA,OACA,QACA,MACF,CAAC,CACH,CAAC,EAMD,OAAO,IAAI,kBAAoB,KAAwB,CACrD,GAAI,CAAC,eAAgB,CACnB,IAAI,KAAK,CAAE,MAAO,0BAA2B,EAAG,GAAG,EACnD,MACF,CACA,IAAM,IAAM,IAAI,IAAI,EACd,WAAa,QAAQ,SAAS,YAAc,IAC5C,SAAmB,CACvB,IAAM,KAAO,eAAe,OAAO,EAC/B,MAAM,IAAI,KAAK,IAAI,CACzB,EACA,KAAK,EACL,IAAM,SAAW,YAAY,KAAM,UAAU,EACvC,UAAY,gBAAkB,IAAI,QAAQ,WAAW,EAAG,GAAM,EACpE,IAAI,YAAc,CAChB,cAAc,QAAQ,EACtB,cAAc,SAAS,CACzB,CAAC,CACH,CAAC,EAGD,OAAO,IAAI,iBAAmB,KAAwB,CACpD,GAAI,CAAC,gBAAkB,CAAC,eAAgB,CACtC,IAAI,KAAK,CAAE,MAAO,0BAA2B,EAAG,GAAG,EACnD,MACF,CACA,IAAM,IAAM,IAAI,IAAI,EACd,WAAa,QAAQ,SAAS,YAAc,IAC5C,SAAmB,CACvB,IAAM,KAAO,eAAe,OAAO,EACnC,GAAI,CAAC,KAAM,OACX,IAAM,OAAS,eAAe,OAAO,eAAe,QAAQ,CAAC,EAC7D,IAAI,KAAK,CAAE,SAAU,KAAM,MAAO,CAAC,CACrC,EACA,KAAK,EACL,IAAM,SAAW,YAAY,KAAM,UAAU,EACvC,UAAY,gBAAkB,IAAI,QAAQ,WAAW,EAAG,GAAM,EACpE,IAAI,YAAc,CAChB,cAAc,QAAQ,EACtB,cAAc,SAAS,CACzB,CAAC,CACH,CAAC,EAeD,OAAO,KAAK,mBAAoB,KAAO,MAAwB,CAC7D,GAAI,mBAAoB,CACtB,IAAI,KAAK,CAAE,MAAO,6DAA8D,EAAG,GAAG,EACtF,MACF,CACA,mBAAqB,GACrB,IAAM,UAAY,KAAK,IAAI,EACrB,SAAW,gCAAe,IAAI,KAAK,EAAA,CAAE,YAAY,CAAC,CAAC,QAAQ,QAAS,GAAG,EAAE,eACzE,WAAa,KAAK,OAAO,EAAG,QAAQ,EAEtC,QACJ,GAAI,CAIF,QAAU,kBAAkB,UAAU,CACxC,OAAS,IAAK,CACZ,mBAAqB,GACrB,IAAI,MAAM,CAAE,GAAI,EAAG,8BAA8B,EACjD,IAAI,KAAK,CAAE,MAAO,0BAA2B,QAAU,IAAc,OAAQ,EAAG,GAAG,EACnF,MACF,CAEA,IAAI,KAAO,EACX,GAAI,CACF,KAAO,SAAS,OAAO,CAAC,CAAC,IAC3B,MAAQ,CAER,CACA,IAAM,UAAY,KAAK,IAAI,EAAI,UAC/B,IAAI,KACF,2BAA2B,QAAQ,KAAK,KAAO,KAAO,KAAA,CAAM,QAAQ,CAAC,EAAE,UAAU,UAAU,IAC7F,EAEA,IAAM,QAAkC,CACtC,eAAgB,mBAChB,sBAAuB,yBAAyB,SAAS,GACzD,wBAAyB,OAAO,SAAS,CAC3C,EACI,KAAO,IAAG,QAAQ,kBAAoB,OAAO,IAAI,GAErD,IAAM,OAAS,iBAAiB,OAAO,EACvC,GAAI,CACF,MAAM,IAAI,aACR,IAAI,SAAS,SAAS,MAAM,MAAM,EAAqB,CAAE,OAAQ,CAAC,CACpE,CACF,OAAS,IAAK,CACZ,IAAI,MAAM,CAAE,GAAI,EAAG,6BAA6B,CAClD,QAAU,CAGR,OAAO,QAAQ,EACf,OAAO,QAAU,KAAQ,CACnB,KAAK,IAAI,KAAK,kCAAkC,QAAQ,IAAI,IAAI,SAAS,EAC7E,mBAAqB,EACvB,CAAC,CACH,CACF,CAAC,EAWD,OAAO,KAAK,aAAe,KAAwB,CACjD,IAAM,GAAM,WAAmC,GAC/C,GAAI,OAAO,IAAO,WAAY,CAC5B,IAAI,KACF,CACE,MACE,6EACJ,EACA,GACF,EACA,MACF,CACA,IAAM,OAAS,QAAQ,YAAY,CAAC,CAAC,SAC/B,UAAY,KAAK,IAAI,EAC3B,GAAI,CACF,GAAG,CACL,OAAS,IAAK,CACZ,IAAI,KACF,CACE,MAAO,mBACP,QAAS,eAAe,MAAQ,IAAI,QAAU,OAAO,GAAG,CAC1D,EACA,GACF,EACA,MACF,CACA,IAAM,MAAQ,QAAQ,YAAY,CAAC,CAAC,SAC9B,eAAiB,KAAK,IAAI,EAAG,OAAS,KAAK,EAC3C,UAAY,KAAK,IAAI,EAAI,UAC/B,IAAI,KACF,wBAAwB,eAAiB,KAAO,KAAA,CAAM,QAAQ,CAAC,EAAE,UAAU,UAAU,GACvF,EACA,IAAI,KAAK,CAAE,OAAQ,MAAO,eAAgB,SAAU,CAAC,CACvD,CAAC,EAQD,OAAO,IAAI,cAAgB,KAAwB,CAEjD,IAAM,QADU,QAAQ,UAAA,EACA,qBAAqB,EACxC,OAIL,IAAI,KAAK,CAAE,SAAU,MAAO,CAAC,EAH3B,IAAI,KAAK,CAAE,MAAO,8BAA+B,EAAG,GAAG,CAI3D,CAAC,EAGD,OAAO,IAAI,WAAa,KAAwB,CAE9C,IAAM,UADU,QAAQ,UAAA,EACE,qBAAqB,EAC1C,SAIL,IAAI,KAAK,QAAQ,EAHf,IAAI,KAAK,CAAE,MAAO,8BAA+B,EAAG,GAAG,CAI3D,CAAC,EAED,OAAO,IAAI,QAAU,KAAwB,CAC3C,IAAM,QAAU,QAAQ,UACnB,QAIL,IAAI,KAAK,oBAAoB,OAAO,CAAC,EAHnC,IAAI,KAAK,CAAE,MAAO,oDAAqD,EAAG,GAAG,CAIjF,CAAC,EAGD,OAAO,KAAK,YAAa,KAAO,MAAwB,CACtD,IAAM,QAAU,QAAQ,UACxB,GAAI,CAAC,QAAS,CACZ,IAAI,KAAK,CAAE,MAAO,oDAAqD,EAAG,GAAG,EAC7E,MACF,CACA,GAAM,CAAE,IAAK,QAAY,IAAI,OAAS,CAAC,EACjC,CAAE,OAAQ,MAAS,MAAM,aAC7B,QACA,OAAO,KAAO,EAAE,EAChB,OAAO,QAAU,EAAE,CACrB,EACA,IAAI,KAAK,KAAM,MAAM,CACvB,CAAC,EAGD,IAAM,eAAmB,CACvB,IAAM,QAAU,QAAQ,UACxB,OAAO,QAAU,kBAAkB,OAAO,kBAAI,IAAI,GACpD,EACM,MAAS,KAAyB,IAAI,OAAS,CAAC,EACtD,OAAO,IAAI,QAAS,KAAO,MAAwB,CACjD,IAAM,EAAI,MAAM,eAAe,WAAW,CAAC,EAC3C,IAAI,KAAK,EAAE,KAAM,EAAE,MAAM,CAC3B,CAAC,EACD,OAAO,IAAI,aAAc,KAAO,MAAwB,CACtD,IAAM,EAAI,MAAM,SAAS,WAAW,EAAG,MAAM,GAAG,CAAC,EACjD,IAAI,KAAK,EAAE,KAAM,EAAE,MAAM,CAC3B,CAAC,EACD,OAAO,IAAI,YAAa,KAAO,MAAwB,CACrD,IAAM,EAAI,MAAM,OAAO,WAAW,EAAG,MAAM,GAAG,CAAC,EAC/C,IAAI,KAAK,EAAE,KAAM,EAAE,MAAM,CAC3B,CAAC,EACD,OAAO,KAAK,eAAgB,KAAO,MAAwB,CACzD,IAAM,EAAI,MAAM,aAAa,WAAW,EAAG,MAAM,GAAG,CAAC,EACrD,IAAI,KAAK,EAAE,KAAM,EAAE,MAAM,CAC3B,CAAC,EAGD,OAAO,IAAI,QAAU,KAAwB,CAC3C,IAAI,KAAK,CAAE,KAAM,UAAY,aAAa,SAAS,EAAI,CAAC,CAAE,CAAC,CAC7D,CAAC,EACD,OAAO,KAAK,YAAc,KAAwB,CAChD,IAAM,KAAO,MAAM,GAAG,CAAC,CAAC,KACxB,GAAI,CAAC,WAAa,OAAO,MAAS,SAAU,CAC1C,IAAI,KAAK,CAAE,MAAO,kBAAmB,EAAG,GAAG,EAC3C,MACF,CACA,IAAM,EAAI,cAAc,UAAW,IAAI,EACvC,IAAI,KAAK,EAAE,KAAM,EAAE,MAAM,CAC3B,CAAC,EAOD,OAAO,IAAI,YAAa,KAAO,MAAwB,CACrD,GAAI,CAAC,UAAW,CACd,IAAI,KAAK,CAAE,MAAO,gDAAiD,EAAG,GAAG,EACzE,MACF,CACA,IAAM,QAAU,QAAQ,UACxB,GAAI,CAAC,QACH,IAAI,KAAK,CAAE,MAAO,wDAAyD,EAAG,GAAG,OAGnF,GAAI,CACF,IAAM,SAAW,MAAM,wBAAwB,CAAE,IAAK,QAAS,SAAU,CAAC,EAC1E,IAAI,KAAK,QAAQ,CACnB,OAAS,IAAK,CACZ,IAAM,QAAU,eAAe,MAAQ,IAAI,QAAU,OAAO,GAAG,EAC/D,IAAI,KAAK,CAAE,MAAO,6BAA8B,OAAQ,EAAG,GAAG,CAChE,CACF,CAAC,EAED,OAAO,IAAI,MAAQ,KAAwB,CACzC,IAAM,UAAY,gBAAgB,CAAC,CAAC,KACjC,GAAM,EAAE,OAAS,aAAe,OAAO,EAAE,UAAa,UACzD,EACK,UAIL,IAAI,KAAK,CAAE,QAAS,GAAM,GAAG,UAAU,SAAS,CAAE,CAAC,EAHjD,IAAI,KAAK,CAAE,QAAS,GAAO,QAAS,qBAAsB,CAAC,CAI/D,CAAC,EAED,OAAO,IAAI,UAAW,KAAO,MAAwB,CACnD,IAAM,aAAe,gBAAgB,CAAC,CAAC,KACpC,GAAM,EAAE,OAAS,gBAAkB,OAAO,EAAE,eAAkB,UACjE,EACA,GAAI,CAAC,aACH,IAAI,KAAK,CAAE,QAAS,GAAO,QAAS,wBAAyB,CAAC,OAGhE,GAAI,CACF,IAAM,MAAkB,aAAa,gBAAgB,GAAK,CAAC,EACrD,OAAgB,CAAC,EACvB,IAAK,IAAM,QAAQ,MAAO,CACxB,IAAM,MAAQ,MAAM,aAAa,gBAAgB,IAAI,EACrD,OAAO,KAAK,CAAE,KAAM,GAAG,KAAM,CAAC,CAChC,CACA,IAAI,KAAK,CAAE,QAAS,GAAM,MAAO,CAAC,CACpC,MAAQ,CACN,IAAI,KAAK,CAAE,QAAS,GAAM,OAAQ,CAAC,EAAG,MAAO,6BAA8B,CAAC,CAC9E,CACF,CAAC,EAGD,OAAO,IAAI,SAAW,KAAwB,CAC5C,IAAM,cAAgB,WAAW,iBAAiB,GAAK,CAAC,EAClD,MAAQ,cACX,OAAQ,GAAM,CAAC,EAAE,MAAM,WAAW,SAAS,CAAC,CAAC,CAC7C,IAAK,IAAO,CACX,GAAI,EAAE,MACN,KAAM,EAAE,KACR,MAAO,EAAE,MACT,aAAc,EAAE,YAClB,EAAE,EACE,QAAU,IAAI,IAAI,MAAM,IAAK,GAAM,EAAE,EAAE,CAAC,EACxC,MAA6C,CAAC,EACpD,IAAK,IAAM,KAAK,cACd,GAAI,GAAE,MAAM,WAAW,SAAS,EAChC,IAAK,IAAM,OAAO,EAAE,aACd,QAAQ,IAAI,GAAG,GACjB,MAAM,KAAK,CAAE,KAAM,EAAE,MAAO,GAAI,GAAI,CAAC,EAI3C,IAAI,KAAK,CAAE,MAAO,KAAM,CAAC,CAC3B,CAAC,EAGD,OAAO,IAAI,UAAY,KAAwB,CAC7C,IAAM,IAAM,IAAI,IAAI,EAEd,gBAAoB,CACxB,IAAI,KAAK,CACP,KAAM,UACN,aAAc,aAAa,MAC3B,WAAY,WAAW,MACvB,iBAAkB,iBAAiB,MACnC,UAAW,UAAU,MACrB,cAAe,cAAc,KAC/B,CAAC,CACH,EACA,YAAY,EAEZ,IAAM,eAAiB,WAAW,WAAY,SAAY,CACxD,IAAI,KAAK,CAAE,KAAM,YAAa,QAAS,UAAW,KAAK,IAAI,CAAE,CAAC,EAC9D,YAAY,CACd,CAAC,EAEK,iBAAmB,MAAM,iBAAoB,YAAY,CAAC,EAC1D,aAAe,MAAM,eAAkB,YAAY,CAAC,EAEpD,UAAY,gBAAkB,IAAI,QAAQ,WAAW,EAAG,GAAK,EAG7D,KAAO,YAAY,YAAa,GAAe,EAErD,IAAI,YAAc,CAChB,iBAAiB,EACjB,iBAAiB,EACjB,aAAa,EACb,cAAc,SAAS,EACvB,cAAc,IAAI,CACpB,CAAC,CACH,CAAC,EAKD,OAAO,IAAI,kBAAoB,KAAwB,CACrD,IAAM,IAAM,QAAQ,UACpB,IAAI,KAAK,KAAK,gBAAgB,GAAK,CAAE,IAAK,CAAC,EAAG,SAAU,CAAC,CAAE,CAAC,CAC9D,CAAC,EAEG,cACF,OAAO,IAAI,UAAY,KAAwB,CAE7C,IAAM,SACJ,QAAQ,UAAA,EACP,gBAAgB,EAIb,SAAW,IAAI,IACnB,CAAC,GAAI,SAAS,KAAO,CAAC,EAAI,IAAI,SAAS,UAAY,CAAC,EAAA,CAAG,QAAS,GAAM,EAAE,IAAI,CAAC,CAAC,CAAC,IAC5E,GAAM,EAAE,IACX,CACF,EACM,OAAiC,CAAC,EACxC,IAAK,GAAM,CAAC,IAAK,SAAU,OAAO,QAAQ,QAAQ,GAAG,EAC/C,QAAU,IAAA,KACV,eAAe,KAAM,QAAW,IAAI,WAAW,MAAM,CAAC,EAAG,OAAO,KAAO,MAClE,SAAS,IAAI,GAAG,IAAG,OAAO,KAAO,eAE5C,IAAI,KAAK,CAAE,OAAQ,OAAQ,CAAC,CAC9B,CAAC,EAIH,IAAM,UAAY,iBAAiB,EAG7B,UACH,MACA,KACC,IAAI,OAAO,QAAU,IAAA,IAAa,WAAW,GAAG,EAAI,IAAI,KAAK,IAAI,EAAI,IAAA,GACzE,GAAI,UAAW,CAKb,IAAM,UAAY,KAAK,UAAW,QAAQ,EACtC,WAAW,SAAS,EAAG,KAAK,YAAY,GAAG,SAAS,SAAU,SAAS,EACtE,KAAK,YAAY,SAAU,SAAS,EAEzC,IAAM,UAAY,aAAa,KAAK,UAAW,YAAY,EAAG,OAAO,EACrE,KAAK,MACH,MACA,SACA,UAAU,UAAU,QAAQ,QAAS,oBAAoB,SAAS,EAAE,CAAC,CACvE,CACF,MACE,KAAK,MAAM,MAAO,SAAU,UAAU,+CAA+C,CAAC,EAGpF,QACF,IAAI,KAAK,uBAAuB,SAAS,WAAW,OAAO,EAAE,EAC7D,IAAI,KAAK,WAAW,SAAS,SAAS,QAAQ,GAE9C,IAAI,KAAK,uBAAuB,SAAS,YAAY,CAEzD,EAOA,WAAW,KAAM,CACV,UACL,eAAe,KAAK,OAAQ,KAAK,MAAO,KAAK,OAAQ,KAAK,UAAU,EACpE,WAAW,IAAI,EACjB,EAGA,QAAQ,IAAK,KAAM,CACZ,SAAW,KAAK,SAAW,WAChC,WAAW,YAAY,KAAK,UAAW,GAAG,CAC5C,EAEA,YAAkC,CAIhC,MAFI,CAAC,SAAW,4BAAoC,CAAC,EAE9C,CACL,CAGE,SAAU,IAAsB,IAAqB,OAAqB,CACxE,IAAM,MAAQ,KAAK,IAAI,EACvB,IAAI,GAAG,aAAgB,CACrB,IAAM,QAAW,IACf,oBAEI,MAAQ,SAAS,SAAW,SAAS,KACrC,WAAa,KAAK,IAAI,EAAI,MAChC,eAAe,IAAI,QAAU,MAAO,MAAO,IAAI,WAAY,UAAU,EACrE,WAAW,CACT,OAAQ,IAAI,QAAU,MACtB,MAAO,IAAI,KAAO,IAAA,CAAK,MAAM,GAAG,CAAC,CAAC,GAClC,MACA,OAAQ,IAAI,WACZ,UACF,CAAC,CACH,CAAC,EACD,KAAK,CACP,EACA,MAAO,cACT,CACF,CACF,EAEA,aAAa,gBAAiB,UAAW,CACvC,GAAI,CAAC,QAAS,OAEd,IAAM,gBAAkB,aAEtB,SAAS,OAAQ,gBAAiB,CAAC,CAAC,EAEhC,gBAAkB,aAAoB,SAAS,kBAAmB,gBAAiB,CAAC,CAAC,EAE3F,IAAK,IAAM,SAAS,gBAAiB,CACnC,IAAM,iBAAmB,cACvB,SAAS,mBACT,gBAAgB,UAChB,MAAM,YACN,CAAC,CACH,EAEA,OAAO,KAAK,CACV,OAAQ,MAAM,OAAO,YAAY,EACjC,KAAM,GAAG,YAAY,MAAM,OAAS,IAAM,GAAK,MAAM,OACrD,WAAY,gBAAgB,KAC5B,QAAS,MAAM,YACf,WAAY,CACV,GAAG,gBAAgB,IAAK,GAAW,EAAE,MAAQ,WAAW,EACxD,GAAG,iBAAiB,IAAK,GAAW,EAAE,MAAQ,WAAW,CAC3D,EACA,MAAO,OAAO,YAAY,cAAc,gBAAiB,MAAM,WAAW,CAAC,EAC3E,GAAG,WAAW,gBAAiB,MAAM,WAAW,EAChD,GAAG,eAAe,MAAM,UAAU,CACpC,CAAC,CACH,CACF,EAEA,WAAW,CAAE,QAAU,CACrB,GAAK,QAKL,CAAI,QAAQ,KAAK,cAAc,MAAM,EASrC,IAAK,IAAM,QAAQ,gBAAgB,EAAG,CACpC,IAAM,KAAO,MAAM,KACf,OAAO,MAAS,UAAY,OAAS,oBACzC,gBAAgB,MAAQ,UAC1B,CAEI,WAAW,uBAAuB,SAAS,EAE/C,IAAI,KACF,oBAAoB,OAAO,OAAO,mBAC7B,WAAW,iBAAiB,CAAC,CAAC,QAAU,EAAE,6BAC7B,SAAS,MAC7B,CArBqC,CAsBvC,EAEA,UAAW,CACT,iBAAiB,EACjB,gBAAgB,KAAK,EACrB,gBAAgB,KAAK,EACjB,KAAO,qBAAqB,IAAM,KAAK,qBAAqB,IAAA,EAAS,EACzE,KAAK,MAAM,EACX,gBAAgBA,gBAAqB,SACvC,CACF,CACF,CACF,CAAC"}