@forinda/kickjs-devtools 3.2.0 → 4.0.0

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":["serveStatic"],"sources":["../src/adapter.ts"],"sourcesContent":["import type { Request, Response, NextFunction } from 'express'\nimport { Router, static as serveStatic } from 'express'\nimport { dirname, join } from 'node:path'\nimport { fileURLToPath } from 'node:url'\nimport { existsSync, readFileSync } from 'node:fs'\nimport { randomBytes } from 'node:crypto'\nimport {\n type AppAdapter,\n type AdapterContext,\n type AdapterMiddleware,\n type Container,\n METADATA,\n ref,\n computed,\n reactive,\n watch,\n createLogger,\n type Ref,\n type ComputedRef,\n getClassMeta,\n getMethodMeta,\n} from '@forinda/kickjs'\n\nconst log = createLogger('DevTools')\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\n/** Per-route latency stats with percentile tracking */\ninterface RouteStats {\n count: number\n totalMs: number\n minMs: number\n maxMs: number\n /** Ring buffer of last N samples for percentile computation */\n samples: number[]\n}\n\nconst MAX_SAMPLES = 1000\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].sort((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\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 /** Include environment variables (sanitized) at /_debug/config (default: false) */\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 * new DevToolsAdapter({ secret: process.env.DEVTOOLS_SECRET })\n * ```\n */\n secret?: string | false\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 * new DevToolsAdapter({\n * enabled: process.env.NODE_ENV !== 'production',\n * exposeConfig: true,\n * configPrefixes: ['APP_', 'DATABASE_'],\n * }),\n * ],\n * })\n * ```\n */\nexport class DevToolsAdapter implements AppAdapter {\n readonly name = 'DevToolsAdapter'\n\n private basePath: string\n private enabled: boolean\n private exposeConfig: boolean\n private configPrefixes: string[]\n private errorRateThreshold: number\n private secret: string | false\n\n // ── Reactive State ───────────────────────────────────────────────────\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 // ── Internal State ───────────────────────────────────────────────────\n private routes: RouteInfo[] = []\n private container: Container | null = null\n private appRef: any = null\n private adapterStatuses: Record<string, string> = {}\n private stopErrorWatch: (() => void) | null = null\n private peerAdapters: any[] = []\n\n constructor(options: DevToolsOptions = {}) {\n this.basePath = options.basePath ?? '/_debug'\n this.enabled = options.enabled ?? process.env.NODE_ENV !== 'production'\n this.exposeConfig = options.exposeConfig ?? false\n this.configPrefixes = options.configPrefixes ?? ['APP_', 'NODE_ENV']\n this.errorRateThreshold = options.errorRateThreshold ?? 0.5\n this.peerAdapters = options.adapters ?? []\n\n // Secret token guard\n if (options.secret === false) {\n this.secret = false\n } else if (options.secret) {\n this.secret = options.secret\n } else {\n // Auto-generate a random token\n this.secret = randomBytes(16).toString('hex')\n }\n\n // Initialize reactive state\n this.requestCount = ref(0)\n this.errorCount = ref(0)\n this.clientErrorCount = ref(0)\n this.startedAt = ref(Date.now())\n this.routeLatency = reactive({})\n\n this.errorRate = computed(() =>\n this.requestCount.value > 0 ? this.errorCount.value / this.requestCount.value : 0,\n )\n\n this.uptimeSeconds = computed(() => Math.floor((Date.now() - this.startedAt.value) / 1000))\n\n // Watch error rate — log warnings when elevated\n if (options.onErrorRateExceeded) {\n const callback = options.onErrorRateExceeded\n const threshold = this.errorRateThreshold\n this.stopErrorWatch = watch(this.errorRate, (rate) => {\n if (rate > threshold) {\n callback(rate)\n }\n })\n } else {\n this.stopErrorWatch = watch(this.errorRate, (rate) => {\n if (rate > this.errorRateThreshold) {\n log.warn(`Error rate elevated: ${(rate * 100).toFixed(1)}%`)\n }\n })\n }\n }\n\n // ── Adapter Lifecycle ────────────────────────────────────────────────\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 private getPeerAdapters(): any[] {\n const kickApp = this.appRef?.__kickApp\n if (kickApp && typeof kickApp.getAdapters === 'function') {\n return kickApp.getAdapters()\n }\n return this.peerAdapters\n }\n\n beforeMount({ app, container }: AdapterContext): void {\n if (!this.enabled) return\n\n this.appRef = app\n this.container = container\n this.startedAt.value = Date.now()\n // Clear routes on rebuild/restart to prevent HMR duplication\n this.routes = []\n this.adapterStatuses[this.name] = 'running'\n\n const router = Router()\n\n // ── Access guard — require secret token ──────────────────────────\n if (this.secret !== false) {\n const token = this.secret\n router.use((req: Request, res: Response, next: NextFunction) => {\n const provided = req.headers['x-devtools-token'] ?? req.query?.token\n if (provided === token) return next()\n // Allow the dashboard HTML itself (it will include the token in API calls)\n if (req.path === '/' && req.method === 'GET' && !req.query?.token) {\n return next() // serve dashboard, it handles auth via token\n }\n // Serve static assets for the dashboard (js files)\n if (req.path.endsWith('.js') || req.path.endsWith('.css')) {\n return next()\n }\n res.status(403).json({ error: 'Forbidden — invalid or missing devtools token' })\n })\n }\n\n router.get('/routes', (_req: Request, res: Response) => {\n res.json({ routes: this.routes })\n })\n\n router.get('/container', (_req: Request, res: Response) => {\n const registrations = this.container?.getRegistrations() ?? []\n res.json({ registrations, count: registrations.length })\n })\n\n router.get('/metrics', (_req: Request, res: Response) => {\n // Build latency with percentiles, omitting raw samples from response\n const latency: Record<string, any> = {}\n for (const [key, stats] of Object.entries(this.routeLatency)) {\n const { samples: _, ...rest } = stats\n latency[key] = { ...rest, ...computePercentiles(stats) }\n }\n res.json({\n requests: this.requestCount.value,\n serverErrors: this.errorCount.value,\n clientErrors: this.clientErrorCount.value,\n errorRate: this.errorRate.value,\n uptimeSeconds: this.uptimeSeconds.value,\n startedAt: new Date(this.startedAt.value).toISOString(),\n routeLatency: latency,\n })\n })\n\n router.get('/health', (_req: Request, res: Response) => {\n const healthy = this.errorRate.value < this.errorRateThreshold\n const status = healthy ? 'healthy' : 'degraded'\n\n res.status(healthy ? 200 : 503).json({\n status,\n errorRate: this.errorRate.value,\n uptime: this.uptimeSeconds.value,\n adapters: this.adapterStatuses,\n })\n })\n\n router.get('/state', (_req: Request, res: Response) => {\n const wsAdapter = this.getPeerAdapters().find(\n (a) => a.name === 'WsAdapter' && typeof a.getStats === 'function',\n )\n res.json({\n reactive: {\n requestCount: this.requestCount.value,\n errorCount: this.errorCount.value,\n clientErrorCount: this.clientErrorCount.value,\n errorRate: this.errorRate.value,\n uptimeSeconds: this.uptimeSeconds.value,\n startedAt: new Date(this.startedAt.value).toISOString(),\n },\n routes: this.routes.length,\n container: this.container?.getRegistrations().length ?? 0,\n routeLatency: this.routeLatency,\n ...(wsAdapter ? { ws: wsAdapter.getStats() } : {}),\n })\n })\n\n router.get('/ws', (_req: Request, res: Response) => {\n const wsAdapter = this.getPeerAdapters().find(\n (a) => a.name === 'WsAdapter' && typeof a.getStats === 'function',\n )\n if (!wsAdapter) {\n res.json({ enabled: false, message: 'WsAdapter not found' })\n return\n }\n res.json({ enabled: true, ...wsAdapter.getStats() })\n })\n\n router.get('/queues', async (_req: Request, res: Response) => {\n const queueAdapter = this.getPeerAdapters().find(\n (a) => a.name === 'QueueAdapter' && typeof a.getQueueNames === 'function',\n )\n if (!queueAdapter) {\n res.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 res.json({ enabled: true, queues })\n } catch {\n res.json({ enabled: true, queues: [], error: 'Failed to fetch queue stats' })\n }\n })\n\n // ── Dependency graph ────────────────────────────────────────────\n router.get('/graph', (_req: Request, res: Response) => {\n const registrations = this.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 res.json({ nodes, edges })\n })\n\n // ── SSE stream for real-time updates ───────────────────────\n // Uses container.onChange() to push events only when something\n // actually changes — no polling. Also sends periodic heartbeats\n // to keep the connection alive through proxies.\n router.get('/stream', (req: Request, res: Response) => {\n res.writeHead(200, {\n 'Content-Type': 'text/event-stream',\n 'Cache-Control': 'no-cache',\n Connection: 'keep-alive',\n })\n\n // Send initial state\n const sendMetrics = () => {\n const data = {\n type: 'metrics',\n requestCount: this.requestCount.value,\n errorCount: this.errorCount.value,\n clientErrorCount: this.clientErrorCount.value,\n errorRate: this.errorRate.value,\n uptimeSeconds: this.uptimeSeconds.value,\n }\n res.write(`data: ${JSON.stringify(data)}\\n\\n`)\n }\n sendMetrics()\n\n // Subscribe to container changes (batched, ~50ms debounce)\n const unsubContainer = this.container?.onChange?.((changes) => {\n res.write(\n `data: ${JSON.stringify({ type: 'container', changes, timestamp: Date.now() })}\\n\\n`,\n )\n // Also send updated metrics after container changes\n sendMetrics()\n })\n\n // Watch reactive metrics — push on change (request count, errors)\n const stopRequestWatch = watch(this.requestCount, () => sendMetrics())\n const stopErrorWatch = watch(this.errorCount, () => sendMetrics())\n\n // Heartbeat every 30s to keep connection alive through proxies/LBs\n const heartbeat = setInterval(() => {\n res.write(`: heartbeat\\n\\n`)\n }, 30000)\n\n req.on('close', () => {\n unsubContainer?.()\n stopRequestWatch()\n stopErrorWatch()\n clearInterval(heartbeat)\n })\n })\n\n if (this.exposeConfig) {\n router.get('/config', (_req: Request, res: Response) => {\n const config: Record<string, string> = {}\n for (const [key, value] of Object.entries(process.env)) {\n if (value === undefined) continue\n const allowed = this.configPrefixes.some((prefix) => key.startsWith(prefix))\n config[key] = allowed ? value : '[REDACTED]'\n }\n res.json({ config })\n })\n }\n\n // Dashboard UI — Vue + Tailwind from public/devtools directory\n const publicDir = this.resolvePublicDir()\n if (publicDir) {\n // Serve static assets (vue.global.min.js, tailwind-cdn.js)\n router.use(serveStatic(publicDir))\n\n // Serve index.html with base path injected\n const indexHtml = readFileSync(join(publicDir, 'index.html'), 'utf-8')\n router.get('/', (_req: Request, res: Response) => {\n // Inject basePath as data attribute for the Vue app\n const html = indexHtml.replace('<body', `<body data-base=\"${this.basePath}\"`)\n res.type('html').send(html)\n })\n } else {\n router.get('/', (_req: Request, res: Response) => {\n res.type('html').send('<h1>DevTools: public directory not found</h1>')\n })\n }\n\n app.use(this.basePath, router)\n\n if (this.secret) {\n log.info(`DevTools mounted at ${this.basePath} [token: ${this.secret}]`)\n log.info(`Access: ${this.basePath}?token=${this.secret}`)\n } else {\n log.info(`DevTools mounted at ${this.basePath} [no guard]`)\n }\n }\n\n middleware(): AdapterMiddleware[] {\n if (!this.enabled) return []\n\n return [\n {\n handler: (req: Request, res: Response, next: NextFunction) => {\n const start = Date.now()\n this.requestCount.value++\n\n res.on('finish', () => {\n if (res.statusCode >= 500) this.errorCount.value++\n else if (res.statusCode >= 400) this.clientErrorCount.value++\n\n // Track per-route latency\n const routeKey = `${req.method} ${req.route?.path ?? req.path}`\n const elapsed = Date.now() - start\n\n if (!this.routeLatency[routeKey]) {\n this.routeLatency[routeKey] = {\n count: 0,\n totalMs: 0,\n minMs: Infinity,\n maxMs: 0,\n samples: [],\n }\n }\n const stats = this.routeLatency[routeKey]\n stats.count++\n stats.totalMs += elapsed\n stats.minMs = Math.min(stats.minMs, elapsed)\n stats.maxMs = Math.max(stats.maxMs, elapsed)\n stats.samples.push(elapsed)\n if (stats.samples.length > MAX_SAMPLES) stats.samples.shift()\n })\n\n next()\n },\n phase: 'beforeGlobal',\n },\n ]\n }\n\n onRouteMount(controllerClass: any, mountPath: string): void {\n if (!this.enabled) return\n\n const routes = getClassMeta<Array<{ method: string; path: string; handlerName: string }>>(\n METADATA.ROUTES,\n controllerClass,\n [],\n )\n\n const classMiddleware = getClassMeta<any[]>(METADATA.CLASS_MIDDLEWARES, controllerClass, [])\n\n for (const route of routes) {\n const methodMiddleware = getMethodMeta<any[]>(\n METADATA.METHOD_MIDDLEWARES,\n controllerClass.prototype,\n route.handlerName,\n [],\n )\n\n this.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 })\n }\n }\n\n afterStart({}: AdapterContext): void {\n if (!this.enabled) return\n log.info(\n `DevTools ready — ${this.routes.length} routes tracked, ` +\n `${this.container?.getRegistrations().length ?? 0} DI bindings`,\n )\n }\n\n shutdown(): void {\n this.stopErrorWatch?.()\n this.adapterStatuses[this.name] = 'stopped'\n }\n\n /** Find the public/devtools directory relative to the built dist or source */\n private resolvePublicDir(): string | null {\n // Try relative to this file's location (works in dist/)\n const thisDir = dirname(fileURLToPath(import.meta.url))\n const candidates = [\n join(thisDir, '..', 'public', 'devtools'), // dist/ -> public/devtools\n join(thisDir, '..', '..', 'public', 'devtools'), // src/ -> public/devtools\n ]\n for (const dir of candidates) {\n if (existsSync(join(dir, 'index.html'))) return dir\n }\n return null\n }\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"index.mjs","names":["serveStatic"],"sources":["../src/topology.ts","../src/devtools-tabs.ts","../src/adapter.ts"],"sourcesContent":["/**\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 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}\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/**\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 stub: IntrospectionSnapshot = {\n protocolVersion: PROTOCOL_VERSION,\n name,\n kind,\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 }\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 * Best-effort contributor enumeration. Plugins + adapters can both\n * return contributor registrations via `contributors()` — we walk both\n * lists, normalise the entries, and dedupe by `key`. Module-level\n * contributors (the most common case) are not enumerable from this\n * surface; the panel will surface them per-route in a later iteration.\n */\nfunction collectContributors(app: TopologyApplicationLike): TopologyContributorEntry[] {\n const seen = new Map<string, TopologyContributorEntry>()\n\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'])\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`,\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 '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","import type { Request, Response, NextFunction } from 'express'\nimport { Router, static as serveStatic } from 'express'\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 { 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} from '@forinda/kickjs'\nimport {\n MemoryAnalyzer,\n PROTOCOL_VERSION,\n RuntimeSampler,\n type IntrospectionSnapshot,\n} from '@forinda/kickjs-devtools-kit'\nimport { collectTopologySnapshot, type TopologyApplicationLike } from './topology'\nimport { collectDevtoolsTabs } from './devtools-tabs'\n\nconst log = createLogger('DevTools')\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\n/** Per-route latency stats with percentile tracking */\ninterface RouteStats {\n count: number\n totalMs: number\n minMs: number\n maxMs: number\n /** Ring buffer of last N samples for percentile computation */\n samples: number[]\n}\n\nconst MAX_SAMPLES = 1000\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].sort((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 * Open an SSE response — sets the headers, flushes them, and disables\n * any compression middleware that may have been registered upstream\n * (compression buffers SSE chunks indefinitely; we need flush-on-write).\n */\nfunction openSseStream(req: Request, res: Response): void {\n res.writeHead(200, {\n 'Content-Type': 'text/event-stream',\n 'Cache-Control': 'no-cache, no-transform',\n Connection: 'keep-alive',\n 'X-Accel-Buffering': 'no',\n })\n res.flushHeaders?.()\n // Defensive: the compression middleware checks this header. Some\n // setups also wrap res.write — calling res.flush after each write\n // is safe even when no compression is in play.\n void req\n}\n\n/** Write a single SSE `data:` event with a JSON payload + double newline. */\nfunction writeSseEvent(res: Response, payload: unknown): void {\n res.write(`data: ${JSON.stringify(payload)}\\n\\n`)\n}\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 /** Include environment variables (sanitized) at /_debug/config (default: false) */\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\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\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 // ── Reactive state ─────────────────────────────────────────────\n const requestCount = ref(0)\n const errorCount = ref(0)\n const clientErrorCount = ref(0)\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 const uptimeSeconds = computed(() => Math.floor((Date.now() - startedAt.value) / 1000))\n\n // ── Internal mutable state ─────────────────────────────────────\n let routes: RouteInfo[] = []\n let container: Container | null = null\n let appRef: any = null\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 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, container: containerArg }) {\n if (!enabled) return\n\n appRef = app\n container = 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 const router = Router()\n\n // ── Access guard — require secret token ──────────────────\n if (secret !== false) {\n const token = secret\n router.use((req: Request, res: Response, next: NextFunction) => {\n const provided = req.headers['x-devtools-token'] ?? req.query?.token\n if (provided === token) return next()\n // Allow the dashboard HTML itself (it will include the token in API calls)\n if (req.path === '/' && req.method === 'GET' && !req.query?.token) {\n return next() // serve dashboard, it handles auth via token\n }\n // Serve static assets for the dashboard (js files)\n if (req.path.endsWith('.js') || req.path.endsWith('.css')) {\n return next()\n }\n res.status(403).json({ error: 'Forbidden — invalid or missing devtools token' })\n })\n }\n\n router.get('/routes', (_req: Request, res: Response) => {\n res.json({ routes })\n })\n\n router.get('/container', (_req: Request, res: Response) => {\n const registrations = container?.getRegistrations() ?? []\n res.json({ registrations, count: registrations.length })\n })\n\n router.get('/metrics', (_req: Request, res: Response) => {\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] = { ...rest, ...computePercentiles(stats) }\n }\n res.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 })\n })\n\n router.get('/health', (_req: Request, res: Response) => {\n const healthy = errorRate.value < errorRateThreshold\n const status = healthy ? 'healthy' : 'degraded'\n\n res.status(healthy ? 200 : 503).json({\n status,\n errorRate: errorRate.value,\n uptime: uptimeSeconds.value,\n adapters: adapterStatuses,\n })\n })\n\n router.get('/state', (_req: Request, res: Response) => {\n const wsAdapter = getPeerAdapters().find(\n (a) => a.name === 'WsAdapter' && typeof a.getStats === 'function',\n )\n res.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', (_req: Request, res: Response) => {\n if (!runtimeSampler || !memoryAnalyzer) {\n res.status(404).json({ error: 'runtime sampler disabled — set runtime.enabled = true' })\n return\n }\n const latest = runtimeSampler.latest()\n const history = runtimeSampler.history()\n const health = memoryAnalyzer.health(history)\n res.json({\n protocolVersion: PROTOCOL_VERSION,\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', (req: Request, res: Response) => {\n if (!runtimeSampler) {\n res.status(404).json({ error: 'runtime sampler disabled' })\n return\n }\n openSseStream(req, res)\n const intervalMs = options.runtime?.intervalMs ?? 1000\n const tick = (): void => {\n const snap = runtimeSampler.latest()\n if (snap) writeSseEvent(res, snap)\n }\n tick()\n const interval = setInterval(tick, intervalMs)\n const heartbeat = setInterval(() => res.write(': heartbeat\\n\\n'), 30_000)\n req.on('close', () => {\n clearInterval(interval)\n clearInterval(heartbeat)\n })\n })\n\n // ── SSE: memory health composite (architecture.md §23) ──────\n router.get('/memory/stream', (req: Request, res: Response) => {\n if (!runtimeSampler || !memoryAnalyzer) {\n res.status(404).json({ error: 'runtime sampler disabled' })\n return\n }\n openSseStream(req, res)\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 writeSseEvent(res, { snapshot: snap, health })\n }\n tick()\n const interval = setInterval(tick, intervalMs)\n const heartbeat = setInterval(() => res.write(': heartbeat\\n\\n'), 30_000)\n req.on('close', () => {\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 temp file is deleted as soon as the response stream\n // completes (success or error). Worst case if the process\n // dies mid-snapshot is a stranded file in `os.tmpdir()`,\n // which the OS cleans up on next boot.\n router.post('/memory/snapshot', (_req: Request, res: Response) => {\n if (snapshotInProgress) {\n res\n .status(503)\n .json({ error: 'heap snapshot already in progress — wait for it to complete' })\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 res\n .status(500)\n .json({ error: 'snapshot capture failed', message: (err as Error).message })\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 res.setHeader('Content-Type', 'application/json')\n res.setHeader('Content-Disposition', `attachment; filename=\"${filename}\"`)\n if (size > 0) res.setHeader('Content-Length', String(size))\n res.setHeader('X-Snapshot-Capture-Ms', String(elapsedMs))\n\n const cleanup = (): void => {\n unlink(written, (err) => {\n if (err) log.warn(`Failed to delete heap snapshot ${written}: ${err.message}`)\n snapshotInProgress = false\n })\n }\n const stream = createReadStream(written)\n stream.on('end', cleanup)\n stream.on('error', (err) => {\n log.error({ err }, 'heap snapshot stream failed')\n if (!res.headersSent) res.status(500).end()\n cleanup()\n })\n // Defensive: if the client disconnects mid-download, abort\n // the stream + run cleanup so the temp file doesn't linger.\n res.on('close', () => {\n stream.destroy()\n // If the stream already fired 'end', cleanup already ran\n // — the unlink is idempotent enough that running twice\n // just yields ENOENT which we ignore in cleanup.\n })\n stream.pipe(res)\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', (_req: Request, res: Response) => {\n const gc = (globalThis as { gc?: () => void }).gc\n if (typeof gc !== 'function') {\n res.status(412).json({\n error: 'global.gc unavailable — start Node with --expose-gc to enable this endpoint',\n })\n return\n }\n const before = process.memoryUsage().heapUsed\n const startedAt = Date.now()\n try {\n gc()\n } catch (err) {\n res.status(500).json({\n error: 'forced GC failed',\n message: err instanceof Error ? err.message : String(err),\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 res.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 router.get('/tabs', (_req: Request, res: Response) => {\n const kickApp = appRef?.__kickApp as TopologyApplicationLike | undefined\n if (!kickApp) {\n res.status(503).json({ error: 'tabs unavailable — application surface not exposed' })\n return\n }\n res.json(collectDevtoolsTabs(kickApp))\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 (_req: Request, res: Response) => {\n if (!container) {\n res.status(503).json({ error: 'topology unavailable — container not bound yet' })\n return\n }\n const kickApp = appRef?.__kickApp as TopologyApplicationLike | undefined\n if (!kickApp) {\n res\n .status(503)\n .json({ error: 'topology unavailable — application surface not exposed' })\n return\n }\n try {\n const snapshot = await collectTopologySnapshot({ app: kickApp, container })\n res.json(snapshot)\n } catch (err) {\n const message = err instanceof Error ? err.message : String(err)\n res.status(500).json({ error: 'topology collection failed', message })\n }\n })\n\n router.get('/ws', (_req: Request, res: Response) => {\n const wsAdapter = getPeerAdapters().find(\n (a) => a.name === 'WsAdapter' && typeof a.getStats === 'function',\n )\n if (!wsAdapter) {\n res.json({ enabled: false, message: 'WsAdapter not found' })\n return\n }\n res.json({ enabled: true, ...wsAdapter.getStats() })\n })\n\n router.get('/queues', async (_req: Request, res: Response) => {\n const queueAdapter = getPeerAdapters().find(\n (a) => a.name === 'QueueAdapter' && typeof a.getQueueNames === 'function',\n )\n if (!queueAdapter) {\n res.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 res.json({ enabled: true, queues })\n } catch {\n res.json({ enabled: true, queues: [], error: 'Failed to fetch queue stats' })\n }\n })\n\n // ── Dependency graph ────────────────────────────────────────\n router.get('/graph', (_req: Request, res: Response) => {\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 res.json({ nodes, edges })\n })\n\n // ── SSE stream for real-time updates ────────────────────────\n router.get('/stream', (req: Request, res: Response) => {\n res.writeHead(200, {\n 'Content-Type': 'text/event-stream',\n 'Cache-Control': 'no-cache',\n Connection: 'keep-alive',\n })\n\n const sendMetrics = () => {\n const data = {\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 res.write(`data: ${JSON.stringify(data)}\\n\\n`)\n }\n sendMetrics()\n\n const unsubContainer = container?.onChange?.((changes) => {\n res.write(\n `data: ${JSON.stringify({ type: 'container', changes, timestamp: Date.now() })}\\n\\n`,\n )\n sendMetrics()\n })\n\n const stopRequestWatch = watch(requestCount, () => sendMetrics())\n const stopErrWatch = watch(errorCount, () => sendMetrics())\n\n const heartbeat = setInterval(() => {\n res.write(`: heartbeat\\n\\n`)\n }, 30000)\n\n req.on('close', () => {\n unsubContainer?.()\n stopRequestWatch()\n stopErrWatch()\n clearInterval(heartbeat)\n })\n })\n\n if (exposeConfig) {\n router.get('/config', (_req: Request, res: Response) => {\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 res.json({ config })\n })\n }\n\n // Dashboard UI — Vue + Tailwind from public/devtools directory\n const publicDir = resolvePublicDir()\n if (publicDir) {\n router.use(serveStatic(publicDir))\n\n const indexHtml = readFileSync(join(publicDir, 'index.html'), 'utf-8')\n router.get('/', (_req: Request, res: Response) => {\n const html = indexHtml.replace('<body', `<body data-base=\"${basePath}\"`)\n res.type('html').send(html)\n })\n } else {\n router.get('/', (_req: Request, res: Response) => {\n res.type('html').send('<h1>DevTools: public directory not found</h1>')\n })\n }\n\n app.use(basePath, router)\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 middleware(): AdapterMiddleware[] {\n if (!enabled) return []\n\n return [\n {\n handler: (req: Request, res: Response, next: NextFunction) => {\n const start = Date.now()\n requestCount.value++\n\n res.on('finish', () => {\n if (res.statusCode >= 500) errorCount.value++\n else if (res.statusCode >= 400) clientErrorCount.value++\n\n const routeKey = `${req.method} ${req.route?.path ?? req.path}`\n const elapsed = Date.now() - start\n\n if (!routeLatency[routeKey]) {\n routeLatency[routeKey] = {\n count: 0,\n totalMs: 0,\n minMs: Infinity,\n maxMs: 0,\n samples: [],\n }\n }\n const stats = routeLatency[routeKey]\n stats.count++\n stats.totalMs += elapsed\n stats.minMs = Math.min(stats.minMs, elapsed)\n stats.maxMs = Math.max(stats.maxMs, elapsed)\n stats.samples.push(elapsed)\n if (stats.samples.length > MAX_SAMPLES) stats.samples.shift()\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 }>\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 })\n }\n },\n\n afterStart() {\n if (!enabled) return\n log.info(\n `DevTools ready — ${routes.length} routes tracked, ` +\n `${container?.getRegistrations().length ?? 0} DI bindings`,\n )\n },\n\n shutdown() {\n stopErrorWatch?.()\n runtimeSampler?.stop()\n memoryAnalyzer?.stop()\n adapterStatuses['DevToolsAdapter'] = 'stopped'\n },\n }\n },\n})\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;AA4CA,MAAM,gCAAgC;;;;;;;;AAStC,eAAsB,wBACpB,MAC2B;CAC3B,MAAM,EAAE,KAAK,WAAW,sBAAsB,kCAAkC;CAChF,MAAM,SAA0B,EAAE;CAElC,MAAM,UAAU,MAAM,QAAQ,IAC5B,IAAI,YAAY,CAAC,KAAK,MAAM,YAAY,GAAG,UAAU,qBAAqB,OAAO,CAAC,CACnF;CACD,MAAM,WAAW,MAAM,QAAQ,IAC7B,IAAI,aAAa,CAAC,KAAK,MAAM,YAAY,GAAG,WAAW,qBAAqB,OAAO,CAAC,CACrF;AAED,QAAO;EACL,iBAAiB;EACjB,WAAW,KAAK,KAAK;EACrB;EACA;EACA,cAAc,oBAAoB,IAAI;EACtC,UAAU,cAAc,UAAU;EAClC;EACD;;;;;;;AAQH,eAAe,YACb,WACA,MACA,WACA,QACgC;CAChC,MAAM,OAAO,UAAU,QAAQ;CAC/B,MAAM,OAA8B;EAClC,iBAAiB;EACjB;EACA;EACD;AACD,KAAI,OAAO,UAAU,eAAe,WAAY,QAAO;AAEvD,KAAI;EACF,MAAM,SAAS,MAAM,QAAQ,KAAK,CAChC,QAAQ,QAAQ,UAAU,YAAY,CAAC,EACvC,IAAI,SAAgB,GAAG,WACrB,iBACQ,uBAAO,IAAI,MAAM,gCAAgC,UAAU,IAAI,CAAC,EACtE,UACD,CACF,CACF,CAAC;AACF,MAAI,UAAU,OAAO,WAAW,UAAU;GAKxC,MAAM,OAAO;AACb,UAAO;IACL,iBAAiB;IACjB,MAAM,KAAK,QAAQ;IACnB,MAAM,KAAK,QAAQ;IACnB,SAAS,KAAK;IACd,OAAO,KAAK;IACZ,QAAQ,KAAK;IACb,SAAS,KAAK;IACd,aAAa,KAAK;IACnB;;AAEH,SAAO;UACA,KAAK;AACZ,SAAO,KAAK;GACV;GACA;GACA,SAAS,eAAe,QAAQ,IAAI,UAAU,OAAO,IAAI;GAC1D,CAAC;AACF,SAAO;;;;AAKX,SAAS,cAAc,WAA4C;AACjE,QAAO,UACJ,kBAAkB,CAClB,QAAQ,MAAM,CAAC,EAAE,MAAM,WAAW,UAAU,CAAC,CAC7C,KAAK,OAAO;EACX,OAAO,EAAE;EACT,OAAO,EAAE;EACT,MAAM,EAAE;EACR,cAAc,EAAE;EACjB,EAAE;;;;;;;;;AAUP,SAAS,oBAAoB,KAA0D;CACrF,MAAM,uBAAO,IAAI,KAAuC;CAExD,MAAM,UACJ,MACA,WACS;AACT,MAAI,CAAC,KAAM;AACX,OAAK,MAAM,OAAO,MAAM;GACtB,MAAM,IAAI;GACV,MAAM,MAAM,OAAO,EAAE,QAAQ,WAAW,EAAE,MAAM;AAChD,OAAI,CAAC,OAAO,KAAK,IAAI,IAAI,CAAE;AAC3B,QAAK,IAAI,KAAK;IACZ;IACA;IACA,WAAW,MAAM,QAAQ,EAAE,UAAU,GAAG,EAAE,YAAY,EAAE;IACzD,CAAC;;;AAIN,MAAK,MAAM,UAAU,IAAI,YAAY,CACnC,QAAO,OAAO,gBAAgB,EAAE,UAAU;AAE5C,MAAK,MAAM,WAAW,IAAI,aAAa,CACrC,QAAO,QAAQ,gBAAgB,EAAE,UAAU;AAG7C,QAAO,CAAC,GAAG,KAAK,QAAQ,CAAC;;;;AC7J3B,MAAM,mBAAmB,IAAI,IAAI;CAAC;CAAU;CAAU;CAAO,CAAC;;;;;;AAO9D,SAAgB,oBAAoB,KAAkD;CACpF,MAAM,OAAgC,EAAE;CACxC,MAAM,uBAAO,IAAI,KAAqB;CACtC,MAAM,SAAoD,EAAE;CAE5D,MAAM,UAAU,YAAoB,QAAuB;AACzD,MAAI,CAAC,MAAM,QAAQ,IAAI,EAAE;AACvB,UAAO,KAAK;IAAE,QAAQ;IAAY,QAAQ;IAA0C,CAAC;AACrF;;AAEF,OAAK,MAAM,SAAS,KAAK;GACvB,MAAM,aAAa,YAAY,MAAM;AACrC,OAAI,CAAC,WAAW,IAAI;AAClB,WAAO,KAAK;KAAE,QAAQ;KAAY,QAAQ,WAAW;KAAQ,CAAC;AAC9D;;GAEF,MAAM,MAAM,WAAW;GACvB,MAAM,QAAQ,KAAK,IAAI,IAAI,GAAG;AAC9B,OAAI,OAAO;AACT,WAAO,KAAK;KACV,QAAQ;KACR,QAAQ,WAAW,IAAI,GAAG,0BAA0B,MAAM;KAC3D,CAAC;AACF;;AAEF,QAAK,IAAI,IAAI,IAAI,WAAW;AAC5B,QAAK,KAAK,IAAI;;;AAIlB,MAAK,MAAM,UAAU,IAAI,YAAY,EAAE;AACrC,MAAI,OAAO,OAAO,iBAAiB,WAAY;AAC/C,MAAI;AACF,UAAO,OAAO,QAAQ,oBAAoB,OAAO,cAAc,CAAC;WACzD,KAAK;AACZ,UAAO,KAAK;IACV,QAAQ,OAAO,QAAQ;IACvB,QAAQ,eAAe,QAAQ,IAAI,UAAU,OAAO,IAAI;IACzD,CAAC;;;AAGN,MAAK,MAAM,WAAW,IAAI,aAAa,EAAE;AACvC,MAAI,OAAO,QAAQ,iBAAiB,WAAY;AAChD,MAAI;AACF,UAAO,QAAQ,QAAQ,qBAAqB,QAAQ,cAAc,CAAC;WAC5D,KAAK;AACZ,UAAO,KAAK;IACV,QAAQ,QAAQ,QAAQ;IACxB,QAAQ,eAAe,QAAQ,IAAI,UAAU,OAAO,IAAI;IACzD,CAAC;;;AAIN,QAAO;EAAE;EAAM;EAAQ;;;;;;;;AAmBzB,SAAS,YAAY,KAA6C;AAChE,KAAI,CAAC,OAAO,OAAO,QAAQ,SACzB,QAAO;EAAE,IAAI;EAAO,QAAQ;EAA8B;CAE5D,MAAM,MAAM;AACZ,KAAI,OAAO,IAAI,OAAO,YAAY,IAAI,GAAG,WAAW,EAClD,QAAO;EAAE,IAAI;EAAO,QAAQ;EAAqC;AAEnE,KAAI,OAAO,IAAI,UAAU,YAAY,IAAI,MAAM,WAAW,EACxD,QAAO;EAAE,IAAI;EAAO,QAAQ,QAAQ,IAAI,GAAG;EAAsC;CAEnF,MAAM,OAAO,IAAI;AACjB,KAAI,CAAC,QAAQ,OAAO,SAAS,SAC3B,QAAO;EAAE,IAAI;EAAO,QAAQ,QAAQ,IAAI,GAAG;EAA6B;AAE1E,KAAI,OAAO,KAAK,SAAS,YAAY,CAAC,iBAAiB,IAAI,KAAK,KAAK,CACnE,QAAO;EACL,IAAI;EACJ,QAAQ,QAAQ,IAAI,GAAG;EACxB;AAEH,SAAQ,KAAK,MAAb;EACE,KAAK;AACH,OAAI,OAAO,KAAK,QAAQ,YAAY,KAAK,IAAI,WAAW,EACtD,QAAO;IAAE,IAAI;IAAO,QAAQ,QAAQ,IAAI,GAAG;IAA0C;AAEvF;EACF,KAAK;AACH,OAAI,OAAO,KAAK,SAAS,SACvB,QAAO;IAAE,IAAI;IAAO,QAAQ,QAAQ,IAAI,GAAG;IAAoC;AAEjF;EACF,KAAK;AACH,OAAI,CAAC,MAAM,QAAQ,KAAK,QAAQ,CAC9B,QAAO;IAAE,IAAI;IAAO,QAAQ,QAAQ,IAAI,GAAG;IAAwC;AAErF;;AAEJ,QAAO;EAAE,IAAI;EAAM,KAAK;EAAyC;;;;AC7GnE,MAAM,MAAM,aAAa,WAAW;AAqBpC,MAAM,cAAc;;AAGpB,SAAS,WAAW,QAAkB,GAAmB;AACvD,KAAI,OAAO,WAAW,EAAG,QAAO;CAChC,MAAM,MAAM,KAAK,KAAK,IAAI,OAAO,OAAO,GAAG;AAC3C,QAAO,OAAO,KAAK,IAAI,GAAG,IAAI;;;AAIhC,SAAS,mBAAmB,OAA8D;CACxF,MAAM,SAAS,CAAC,GAAG,MAAM,QAAQ,CAAC,MAAM,GAAG,MAAM,IAAI,EAAE;AACvD,QAAO;EACL,KAAK,WAAW,QAAQ,GAAI;EAC5B,KAAK,WAAW,QAAQ,IAAK;EAC7B,KAAK,WAAW,QAAQ,IAAK;EAC9B;;;;;;;AAQH,SAAS,cAAc,KAAc,KAAqB;AACxD,KAAI,UAAU,KAAK;EACjB,gBAAgB;EAChB,iBAAiB;EACjB,YAAY;EACZ,qBAAqB;EACtB,CAAC;AACF,KAAI,gBAAgB;;;AAQtB,SAAS,cAAc,KAAe,SAAwB;AAC5D,KAAI,MAAM,SAAS,KAAK,UAAU,QAAQ,CAAC,MAAM;;;;;;;;;;;;;;AAenD,IAAI,qBAAqB;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAgHzB,MAAa,kBAAkB,cAA0D;CACvF,MAAM;CACN,UAAU;EACR,UAAU;EACV,oBAAoB;EACpB,cAAc;EACd,gBAAgB,CAAC,QAAQ,WAAW;EACrC;CACD,QAAQ,YAAY;EAClB,MAAM,WAAW,QAAQ;EACzB,MAAM,UAAU,QAAQ,WAAW,QAAQ,IAAI,aAAa;EAC5D,MAAM,eAAe,QAAQ;EAC7B,MAAM,iBAAiB,QAAQ;EAC/B,MAAM,qBAAqB,QAAQ;EACnC,MAAM,eAAe,QAAQ,YAAY,EAAE;EAG3C,IAAI;AACJ,MAAI,QAAQ,WAAW,MACrB,UAAS;WACA,QAAQ,OACjB,UAAS,QAAQ;MAEjB,UAAS,YAAY,GAAG,CAAC,SAAS,MAAM;EAO1C,MAAM,iBAAiB,QAAQ,SAAS,WAAW;EACnD,MAAM,iBAAiB,iBACnB,IAAI,eAAe;GACjB,YAAY,QAAQ,SAAS,cAAc;GAC3C,YAAY,QAAQ,SAAS,cAAc;GAC5C,CAAC,GACF;EACJ,MAAM,iBAAiB,iBAAiB,IAAI,gBAAgB,GAAG;EAG/D,MAAM,eAAe,IAAI,EAAE;EAC3B,MAAM,aAAa,IAAI,EAAE;EACzB,MAAM,mBAAmB,IAAI,EAAE;EAC/B,MAAM,YAAY,IAAI,KAAK,KAAK,CAAC;EACjC,MAAM,eAAe,SAAqC,EAAE,CAAC;EAE7D,MAAM,YAAY,eAChB,aAAa,QAAQ,IAAI,WAAW,QAAQ,aAAa,QAAQ,EAClE;EAED,MAAM,gBAAgB,eAAe,KAAK,OAAO,KAAK,KAAK,GAAG,UAAU,SAAS,IAAK,CAAC;EAGvF,IAAI,SAAsB,EAAE;EAC5B,IAAI,YAA8B;EAClC,IAAI,SAAc;EAClB,MAAM,kBAA0C,EAAE;EAClD,IAAI,iBAAsC;AAG1C,MAAI,QAAQ,qBAAqB;GAC/B,MAAM,WAAW,QAAQ;AACzB,oBAAiB,MAAM,YAAY,SAAS;AAC1C,QAAI,OAAO,mBACT,UAAS,KAAK;KAEhB;QAEF,kBAAiB,MAAM,YAAY,SAAS;AAC1C,OAAI,OAAO,mBACT,KAAI,KAAK,yBAAyB,OAAO,KAAK,QAAQ,EAAE,CAAC,GAAG;IAE9D;;;;;;;EASJ,MAAM,yBAAwC;GAC5C,MAAM,UAAU,QAAQ,cAAc,OAAO,KAAK,IAAI,CAAC;GACvD,MAAM,aAAa;IACjB,KAAK,SAAS,MAAM,UAAU,MAAM;IACpC,KAAK,SAAS,MAAM,MAAM,UAAU,MAAM;IAC1C,KAAK,SAAS,MAAM,UAAU,WAAW;IACzC,KAAK,SAAS,MAAM,MAAM,UAAU,WAAW;IAChD;AACD,QAAK,MAAM,OAAO,WAChB,KAAI,WAAW,KAAK,KAAK,aAAa,CAAC,CAAE,QAAO;AAElD,UAAO;;;;;;;EAQT,MAAM,wBAA+B;GACnC,MAAM,UAAU,QAAQ;AACxB,OAAI,WAAW,OAAO,QAAQ,gBAAgB,WAC5C,QAAO,QAAQ,aAAa;AAE9B,UAAO;;AAGT,SAAO;GAEL;GACA;GACA;GACA;GACA;GACA;GACA;GACA;GACA;GAMA,aAAoC;AAClC,WAAO;KACL,iBAAiB;KACjB,MAAM;KACN,MAAM;KACN,OAAO;MACL;MACA;MACA;MACA,QAAQ,WAAW,QAAQ,QAAQ;MACpC;KACD,SAAS;MACP,cAAc,aAAa;MAC3B,cAAc,WAAW;MACzB,cAAc,iBAAiB;MAC/B,eAAe,cAAc;MAC7B,eAAe,OAAO;MACvB;KACF;;GAKH,YAAY,EAAE,KAAK,WAAW,gBAAgB;AAC5C,QAAI,CAAC,QAAS;AAEd,aAAS;AACT,gBAAY;AACZ,cAAU,QAAQ,KAAK,KAAK;AAE5B,aAAS,EAAE;AACX,oBAAgB,qBAAqB;AAKrC,oBAAgB,OAAO;AACvB,oBAAgB,OAAO;IAEvB,MAAM,SAAS,QAAQ;AAGvB,QAAI,WAAW,OAAO;KACpB,MAAM,QAAQ;AACd,YAAO,KAAK,KAAc,KAAe,SAAuB;AAE9D,WADiB,IAAI,QAAQ,uBAAuB,IAAI,OAAO,WAC9C,MAAO,QAAO,MAAM;AAErC,UAAI,IAAI,SAAS,OAAO,IAAI,WAAW,SAAS,CAAC,IAAI,OAAO,MAC1D,QAAO,MAAM;AAGf,UAAI,IAAI,KAAK,SAAS,MAAM,IAAI,IAAI,KAAK,SAAS,OAAO,CACvD,QAAO,MAAM;AAEf,UAAI,OAAO,IAAI,CAAC,KAAK,EAAE,OAAO,iDAAiD,CAAC;OAChF;;AAGJ,WAAO,IAAI,YAAY,MAAe,QAAkB;AACtD,SAAI,KAAK,EAAE,QAAQ,CAAC;MACpB;AAEF,WAAO,IAAI,eAAe,MAAe,QAAkB;KACzD,MAAM,gBAAgB,WAAW,kBAAkB,IAAI,EAAE;AACzD,SAAI,KAAK;MAAE;MAAe,OAAO,cAAc;MAAQ,CAAC;MACxD;AAEF,WAAO,IAAI,aAAa,MAAe,QAAkB;KAEvD,MAAM,UAA+B,EAAE;AACvC,UAAK,MAAM,CAAC,KAAK,UAAU,OAAO,QAAQ,aAAa,EAAE;MACvD,MAAM,EAAE,SAAS,GAAG,GAAG,SAAS;AAChC,cAAQ,OAAO;OAAE,GAAG;OAAM,GAAG,mBAAmB,MAAM;OAAE;;AAE1D,SAAI,KAAK;MACP,UAAU,aAAa;MACvB,cAAc,WAAW;MACzB,cAAc,iBAAiB;MAC/B,WAAW,UAAU;MACrB,eAAe,cAAc;MAC7B,WAAW,IAAI,KAAK,UAAU,MAAM,CAAC,aAAa;MAClD,cAAc;MACf,CAAC;MACF;AAEF,WAAO,IAAI,YAAY,MAAe,QAAkB;KACtD,MAAM,UAAU,UAAU,QAAQ;KAClC,MAAM,SAAS,UAAU,YAAY;AAErC,SAAI,OAAO,UAAU,MAAM,IAAI,CAAC,KAAK;MACnC;MACA,WAAW,UAAU;MACrB,QAAQ,cAAc;MACtB,UAAU;MACX,CAAC;MACF;AAEF,WAAO,IAAI,WAAW,MAAe,QAAkB;KACrD,MAAM,YAAY,iBAAiB,CAAC,MACjC,MAAM,EAAE,SAAS,eAAe,OAAO,EAAE,aAAa,WACxD;AACD,SAAI,KAAK;MACP,UAAU;OACR,cAAc,aAAa;OAC3B,YAAY,WAAW;OACvB,kBAAkB,iBAAiB;OACnC,WAAW,UAAU;OACrB,eAAe,cAAc;OAC7B,WAAW,IAAI,KAAK,UAAU,MAAM,CAAC,aAAa;OACnD;MACD,QAAQ,OAAO;MACf,WAAW,WAAW,kBAAkB,CAAC,UAAU;MACnD;MACA,GAAI,YAAY,EAAE,IAAI,UAAU,UAAU,EAAE,GAAG,EAAE;MAClD,CAAC;MACF;AAOF,WAAO,IAAI,aAAa,MAAe,QAAkB;AACvD,SAAI,CAAC,kBAAkB,CAAC,gBAAgB;AACtC,UAAI,OAAO,IAAI,CAAC,KAAK,EAAE,OAAO,yDAAyD,CAAC;AACxF;;KAEF,MAAM,SAAS,eAAe,QAAQ;KACtC,MAAM,UAAU,eAAe,SAAS;KACxC,MAAM,SAAS,eAAe,OAAO,QAAQ;AAC7C,SAAI,KAAK;MACP,iBAAiB;MACjB;MACA;MACA;MACD,CAAC;MACF;AAMF,WAAO,IAAI,oBAAoB,KAAc,QAAkB;AAC7D,SAAI,CAAC,gBAAgB;AACnB,UAAI,OAAO,IAAI,CAAC,KAAK,EAAE,OAAO,4BAA4B,CAAC;AAC3D;;AAEF,mBAAc,KAAK,IAAI;KACvB,MAAM,aAAa,QAAQ,SAAS,cAAc;KAClD,MAAM,aAAmB;MACvB,MAAM,OAAO,eAAe,QAAQ;AACpC,UAAI,KAAM,eAAc,KAAK,KAAK;;AAEpC,WAAM;KACN,MAAM,WAAW,YAAY,MAAM,WAAW;KAC9C,MAAM,YAAY,kBAAkB,IAAI,MAAM,kBAAkB,EAAE,IAAO;AACzE,SAAI,GAAG,eAAe;AACpB,oBAAc,SAAS;AACvB,oBAAc,UAAU;OACxB;MACF;AAGF,WAAO,IAAI,mBAAmB,KAAc,QAAkB;AAC5D,SAAI,CAAC,kBAAkB,CAAC,gBAAgB;AACtC,UAAI,OAAO,IAAI,CAAC,KAAK,EAAE,OAAO,4BAA4B,CAAC;AAC3D;;AAEF,mBAAc,KAAK,IAAI;KACvB,MAAM,aAAa,QAAQ,SAAS,cAAc;KAClD,MAAM,aAAmB;MACvB,MAAM,OAAO,eAAe,QAAQ;AACpC,UAAI,CAAC,KAAM;AAEX,oBAAc,KAAK;OAAE,UAAU;OAAM,QADtB,eAAe,OAAO,eAAe,SAAS,CAAC;OACjB,CAAC;;AAEhD,WAAM;KACN,MAAM,WAAW,YAAY,MAAM,WAAW;KAC9C,MAAM,YAAY,kBAAkB,IAAI,MAAM,kBAAkB,EAAE,IAAO;AACzE,SAAI,GAAG,eAAe;AACpB,oBAAc,SAAS;AACvB,oBAAc,UAAU;OACxB;MACF;AAcF,WAAO,KAAK,qBAAqB,MAAe,QAAkB;AAChE,SAAI,oBAAoB;AACtB,UACG,OAAO,IAAI,CACX,KAAK,EAAE,OAAO,+DAA+D,CAAC;AACjF;;AAEF,0BAAqB;KACrB,MAAM,YAAY,KAAK,KAAK;KAC5B,MAAM,WAAW,gCAAe,IAAI,MAAM,EAAC,aAAa,CAAC,QAAQ,SAAS,IAAI,CAAC;KAC/E,MAAM,aAAa,KAAK,QAAQ,EAAE,SAAS;KAE3C,IAAI;AACJ,SAAI;AAIF,gBAAU,kBAAkB,WAAW;cAChC,KAAK;AACZ,2BAAqB;AACrB,UAAI,MAAM,EAAE,KAAK,EAAE,+BAA+B;AAClD,UACG,OAAO,IAAI,CACX,KAAK;OAAE,OAAO;OAA2B,SAAU,IAAc;OAAS,CAAC;AAC9E;;KAGF,IAAI,OAAO;AACX,SAAI;AACF,aAAO,SAAS,QAAQ,CAAC;aACnB;KAGR,MAAM,YAAY,KAAK,KAAK,GAAG;AAC/B,SAAI,KACF,2BAA2B,QAAQ,KAAK,OAAO,OAAO,MAAM,QAAQ,EAAE,CAAC,UAAU,UAAU,KAC5F;AAED,SAAI,UAAU,gBAAgB,mBAAmB;AACjD,SAAI,UAAU,uBAAuB,yBAAyB,SAAS,GAAG;AAC1E,SAAI,OAAO,EAAG,KAAI,UAAU,kBAAkB,OAAO,KAAK,CAAC;AAC3D,SAAI,UAAU,yBAAyB,OAAO,UAAU,CAAC;KAEzD,MAAM,gBAAsB;AAC1B,aAAO,UAAU,QAAQ;AACvB,WAAI,IAAK,KAAI,KAAK,kCAAkC,QAAQ,IAAI,IAAI,UAAU;AAC9E,4BAAqB;QACrB;;KAEJ,MAAM,SAAS,iBAAiB,QAAQ;AACxC,YAAO,GAAG,OAAO,QAAQ;AACzB,YAAO,GAAG,UAAU,QAAQ;AAC1B,UAAI,MAAM,EAAE,KAAK,EAAE,8BAA8B;AACjD,UAAI,CAAC,IAAI,YAAa,KAAI,OAAO,IAAI,CAAC,KAAK;AAC3C,eAAS;OACT;AAGF,SAAI,GAAG,eAAe;AACpB,aAAO,SAAS;OAIhB;AACF,YAAO,KAAK,IAAI;MAChB;AAWF,WAAO,KAAK,eAAe,MAAe,QAAkB;KAC1D,MAAM,KAAM,WAAmC;AAC/C,SAAI,OAAO,OAAO,YAAY;AAC5B,UAAI,OAAO,IAAI,CAAC,KAAK,EACnB,OAAO,+EACR,CAAC;AACF;;KAEF,MAAM,SAAS,QAAQ,aAAa,CAAC;KACrC,MAAM,YAAY,KAAK,KAAK;AAC5B,SAAI;AACF,UAAI;cACG,KAAK;AACZ,UAAI,OAAO,IAAI,CAAC,KAAK;OACnB,OAAO;OACP,SAAS,eAAe,QAAQ,IAAI,UAAU,OAAO,IAAI;OAC1D,CAAC;AACF;;KAEF,MAAM,QAAQ,QAAQ,aAAa,CAAC;KACpC,MAAM,iBAAiB,KAAK,IAAI,GAAG,SAAS,MAAM;KAClD,MAAM,YAAY,KAAK,KAAK,GAAG;AAC/B,SAAI,KACF,wBAAwB,iBAAiB,OAAO,MAAM,QAAQ,EAAE,CAAC,UAAU,UAAU,IACtF;AACD,SAAI,KAAK;MAAE;MAAQ;MAAO;MAAgB;MAAW,CAAC;MACtD;AAOF,WAAO,IAAI,UAAU,MAAe,QAAkB;KACpD,MAAM,UAAU,QAAQ;AACxB,SAAI,CAAC,SAAS;AACZ,UAAI,OAAO,IAAI,CAAC,KAAK,EAAE,OAAO,sDAAsD,CAAC;AACrF;;AAEF,SAAI,KAAK,oBAAoB,QAAQ,CAAC;MACtC;AAOF,WAAO,IAAI,aAAa,OAAO,MAAe,QAAkB;AAC9D,SAAI,CAAC,WAAW;AACd,UAAI,OAAO,IAAI,CAAC,KAAK,EAAE,OAAO,kDAAkD,CAAC;AACjF;;KAEF,MAAM,UAAU,QAAQ;AACxB,SAAI,CAAC,SAAS;AACZ,UACG,OAAO,IAAI,CACX,KAAK,EAAE,OAAO,0DAA0D,CAAC;AAC5E;;AAEF,SAAI;MACF,MAAM,WAAW,MAAM,wBAAwB;OAAE,KAAK;OAAS;OAAW,CAAC;AAC3E,UAAI,KAAK,SAAS;cACX,KAAK;MACZ,MAAM,UAAU,eAAe,QAAQ,IAAI,UAAU,OAAO,IAAI;AAChE,UAAI,OAAO,IAAI,CAAC,KAAK;OAAE,OAAO;OAA8B;OAAS,CAAC;;MAExE;AAEF,WAAO,IAAI,QAAQ,MAAe,QAAkB;KAClD,MAAM,YAAY,iBAAiB,CAAC,MACjC,MAAM,EAAE,SAAS,eAAe,OAAO,EAAE,aAAa,WACxD;AACD,SAAI,CAAC,WAAW;AACd,UAAI,KAAK;OAAE,SAAS;OAAO,SAAS;OAAuB,CAAC;AAC5D;;AAEF,SAAI,KAAK;MAAE,SAAS;MAAM,GAAG,UAAU,UAAU;MAAE,CAAC;MACpD;AAEF,WAAO,IAAI,WAAW,OAAO,MAAe,QAAkB;KAC5D,MAAM,eAAe,iBAAiB,CAAC,MACpC,MAAM,EAAE,SAAS,kBAAkB,OAAO,EAAE,kBAAkB,WAChE;AACD,SAAI,CAAC,cAAc;AACjB,UAAI,KAAK;OAAE,SAAS;OAAO,SAAS;OAA0B,CAAC;AAC/D;;AAEF,SAAI;MACF,MAAM,QAAkB,aAAa,iBAAiB,IAAI,EAAE;MAC5D,MAAM,SAAgB,EAAE;AACxB,WAAK,MAAM,QAAQ,OAAO;OACxB,MAAM,QAAQ,MAAM,aAAa,gBAAgB,KAAK;AACtD,cAAO,KAAK;QAAE;QAAM,GAAG;QAAO,CAAC;;AAEjC,UAAI,KAAK;OAAE,SAAS;OAAM;OAAQ,CAAC;aAC7B;AACN,UAAI,KAAK;OAAE,SAAS;OAAM,QAAQ,EAAE;OAAE,OAAO;OAA+B,CAAC;;MAE/E;AAGF,WAAO,IAAI,WAAW,MAAe,QAAkB;KACrD,MAAM,gBAAgB,WAAW,kBAAkB,IAAI,EAAE;KACzD,MAAM,QAAQ,cACX,QAAQ,MAAM,CAAC,EAAE,MAAM,WAAW,UAAU,CAAC,CAC7C,KAAK,OAAO;MACX,IAAI,EAAE;MACN,MAAM,EAAE;MACR,OAAO,EAAE;MACT,cAAc,EAAE;MACjB,EAAE;KACL,MAAM,UAAU,IAAI,IAAI,MAAM,KAAK,MAAM,EAAE,GAAG,CAAC;KAC/C,MAAM,QAA6C,EAAE;AACrD,UAAK,MAAM,KAAK,eAAe;AAC7B,UAAI,EAAE,MAAM,WAAW,UAAU,CAAE;AACnC,WAAK,MAAM,OAAO,EAAE,aAClB,KAAI,QAAQ,IAAI,IAAI,CAClB,OAAM,KAAK;OAAE,MAAM,EAAE;OAAO,IAAI;OAAK,CAAC;;AAI5C,SAAI,KAAK;MAAE;MAAO;MAAO,CAAC;MAC1B;AAGF,WAAO,IAAI,YAAY,KAAc,QAAkB;AACrD,SAAI,UAAU,KAAK;MACjB,gBAAgB;MAChB,iBAAiB;MACjB,YAAY;MACb,CAAC;KAEF,MAAM,oBAAoB;MACxB,MAAM,OAAO;OACX,MAAM;OACN,cAAc,aAAa;OAC3B,YAAY,WAAW;OACvB,kBAAkB,iBAAiB;OACnC,WAAW,UAAU;OACrB,eAAe,cAAc;OAC9B;AACD,UAAI,MAAM,SAAS,KAAK,UAAU,KAAK,CAAC,MAAM;;AAEhD,kBAAa;KAEb,MAAM,iBAAiB,WAAW,YAAY,YAAY;AACxD,UAAI,MACF,SAAS,KAAK,UAAU;OAAE,MAAM;OAAa;OAAS,WAAW,KAAK,KAAK;OAAE,CAAC,CAAC,MAChF;AACD,mBAAa;OACb;KAEF,MAAM,mBAAmB,MAAM,oBAAoB,aAAa,CAAC;KACjE,MAAM,eAAe,MAAM,kBAAkB,aAAa,CAAC;KAE3D,MAAM,YAAY,kBAAkB;AAClC,UAAI,MAAM,kBAAkB;QAC3B,IAAM;AAET,SAAI,GAAG,eAAe;AACpB,wBAAkB;AAClB,wBAAkB;AAClB,oBAAc;AACd,oBAAc,UAAU;OACxB;MACF;AAEF,QAAI,aACF,QAAO,IAAI,YAAY,MAAe,QAAkB;KACtD,MAAM,SAAiC,EAAE;AACzC,UAAK,MAAM,CAAC,KAAK,UAAU,OAAO,QAAQ,QAAQ,IAAI,EAAE;AACtD,UAAI,UAAU,KAAA,EAAW;AAEzB,aAAO,OADS,eAAe,MAAM,WAAW,IAAI,WAAW,OAAO,CAAC,GAC/C,QAAQ;;AAElC,SAAI,KAAK,EAAE,QAAQ,CAAC;MACpB;IAIJ,MAAM,YAAY,kBAAkB;AACpC,QAAI,WAAW;AACb,YAAO,IAAIA,SAAY,UAAU,CAAC;KAElC,MAAM,YAAY,aAAa,KAAK,WAAW,aAAa,EAAE,QAAQ;AACtE,YAAO,IAAI,MAAM,MAAe,QAAkB;MAChD,MAAM,OAAO,UAAU,QAAQ,SAAS,oBAAoB,SAAS,GAAG;AACxE,UAAI,KAAK,OAAO,CAAC,KAAK,KAAK;OAC3B;UAEF,QAAO,IAAI,MAAM,MAAe,QAAkB;AAChD,SAAI,KAAK,OAAO,CAAC,KAAK,gDAAgD;MACtE;AAGJ,QAAI,IAAI,UAAU,OAAO;AAEzB,QAAI,QAAQ;AACV,SAAI,KAAK,uBAAuB,SAAS,WAAW,OAAO,GAAG;AAC9D,SAAI,KAAK,WAAW,SAAS,SAAS,SAAS;UAE/C,KAAI,KAAK,uBAAuB,SAAS,aAAa;;GAI1D,aAAkC;AAChC,QAAI,CAAC,QAAS,QAAO,EAAE;AAEvB,WAAO,CACL;KACE,UAAU,KAAc,KAAe,SAAuB;MAC5D,MAAM,QAAQ,KAAK,KAAK;AACxB,mBAAa;AAEb,UAAI,GAAG,gBAAgB;AACrB,WAAI,IAAI,cAAc,IAAK,YAAW;gBAC7B,IAAI,cAAc,IAAK,kBAAiB;OAEjD,MAAM,WAAW,GAAG,IAAI,OAAO,GAAG,IAAI,OAAO,QAAQ,IAAI;OACzD,MAAM,UAAU,KAAK,KAAK,GAAG;AAE7B,WAAI,CAAC,aAAa,UAChB,cAAa,YAAY;QACvB,OAAO;QACP,SAAS;QACT,OAAO;QACP,OAAO;QACP,SAAS,EAAE;QACZ;OAEH,MAAM,QAAQ,aAAa;AAC3B,aAAM;AACN,aAAM,WAAW;AACjB,aAAM,QAAQ,KAAK,IAAI,MAAM,OAAO,QAAQ;AAC5C,aAAM,QAAQ,KAAK,IAAI,MAAM,OAAO,QAAQ;AAC5C,aAAM,QAAQ,KAAK,QAAQ;AAC3B,WAAI,MAAM,QAAQ,SAAS,YAAa,OAAM,QAAQ,OAAO;QAC7D;AAEF,YAAM;;KAER,OAAO;KACR,CACF;;GAGH,aAAa,iBAAiB,WAAW;AACvC,QAAI,CAAC,QAAS;IAEd,MAAM,kBAAkB,aAEtB,SAAS,QAAQ,iBAAiB,EAAE,CAAC;IAEvC,MAAM,kBAAkB,aAAoB,SAAS,mBAAmB,iBAAiB,EAAE,CAAC;AAE5F,SAAK,MAAM,SAAS,iBAAiB;KACnC,MAAM,mBAAmB,cACvB,SAAS,oBACT,gBAAgB,WAChB,MAAM,aACN,EAAE,CACH;AAED,YAAO,KAAK;MACV,QAAQ,MAAM,OAAO,aAAa;MAClC,MAAM,GAAG,YAAY,MAAM,SAAS,MAAM,KAAK,MAAM;MACrD,YAAY,gBAAgB;MAC5B,SAAS,MAAM;MACf,YAAY,CACV,GAAG,gBAAgB,KAAK,MAAW,EAAE,QAAQ,YAAY,EACzD,GAAG,iBAAiB,KAAK,MAAW,EAAE,QAAQ,YAAY,CAC3D;MACF,CAAC;;;GAIN,aAAa;AACX,QAAI,CAAC,QAAS;AACd,QAAI,KACF,oBAAoB,OAAO,OAAO,mBAC7B,WAAW,kBAAkB,CAAC,UAAU,EAAE,cAChD;;GAGH,WAAW;AACT,sBAAkB;AAClB,oBAAgB,MAAM;AACtB,oBAAgB,MAAM;AACtB,oBAAgB,qBAAqB;;GAExC;;CAEJ,CAAC"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@forinda/kickjs-devtools",
3
- "version": "3.2.0",
3
+ "version": "4.0.0",
4
4
  "description": "Development introspection dashboard for KickJS — routes, DI container, metrics, health",
5
5
  "keywords": [
6
6
  "kickjs",
@@ -23,13 +23,10 @@
23
23
  "@forinda/kickjs",
24
24
  "@forinda/kickjs-auth",
25
25
  "@forinda/kickjs-cli",
26
- "@forinda/kickjs-config",
27
- "@forinda/kickjs-core",
28
26
  "@forinda/kickjs-cron",
29
27
  "@forinda/kickjs-devtools",
30
28
  "@forinda/kickjs-drizzle",
31
29
  "@forinda/kickjs-graphql",
32
- "@forinda/kickjs-http",
33
30
  "@forinda/kickjs-mailer",
34
31
  "@forinda/kickjs-multi-tenant",
35
32
  "@forinda/kickjs-notifications",
@@ -66,14 +63,12 @@
66
63
  "output": [
67
64
  "dist/**"
68
65
  ],
69
- "dependencies": [
70
- "../core:build",
71
- "../http:build"
72
- ]
66
+ "dependencies": []
73
67
  }
74
68
  },
75
69
  "dependencies": {
76
- "reflect-metadata": "^0.2.2"
70
+ "reflect-metadata": "^0.2.2",
71
+ "@forinda/kickjs-devtools-kit": "4.0.0"
77
72
  },
78
73
  "peerDependencies": {
79
74
  "express": "^5.1.0",
@@ -81,11 +76,14 @@
81
76
  },
82
77
  "devDependencies": {
83
78
  "@types/express": "^5.0.6",
84
- "@types/node": "^25.0.0",
79
+ "@types/node": "^25.6.0",
85
80
  "express": "^5.1.0",
86
- "typescript": "^5.9.2",
87
- "vitest": "^4.1.2",
88
- "@forinda/kickjs": "3.2.0"
81
+ "solid-js": "^1.9.12",
82
+ "typescript": "^6.0.3",
83
+ "vite": "^8.0.9",
84
+ "vite-plugin-solid": "^2.11.12",
85
+ "vitest": "^4.1.5",
86
+ "@forinda/kickjs": "4.0.0"
89
87
  },
90
88
  "publishConfig": {
91
89
  "access": "public"
@@ -107,7 +105,9 @@
107
105
  "peerDependenciesMeta": {},
108
106
  "scripts": {
109
107
  "build": "wireit",
108
+ "build:spa": "vite build --config spa/vite.config.ts spa",
110
109
  "dev": "tsdown --watch",
110
+ "dev:spa": "vite spa",
111
111
  "test": "vitest run --passWithNoTests",
112
112
  "typecheck": "tsc --noEmit",
113
113
  "clean": "rm -rf dist .wireit"
@@ -0,0 +1 @@
1
+ (function(){const t=document.createElement("link").relList;if(t&&t.supports&&t.supports("modulepreload"))return;for(const i of document.querySelectorAll('link[rel="modulepreload"]'))n(i);new MutationObserver(i=>{for(const s of i)if(s.type==="childList")for(const d of s.addedNodes)d.tagName==="LINK"&&d.rel==="modulepreload"&&n(d)}).observe(document,{childList:!0,subtree:!0});function r(i){const s={};return i.integrity&&(s.integrity=i.integrity),i.referrerPolicy&&(s.referrerPolicy=i.referrerPolicy),i.crossOrigin==="use-credentials"?s.credentials="include":i.crossOrigin==="anonymous"?s.credentials="omit":s.credentials="same-origin",s}function n(i){if(i.ep)return;i.ep=!0;const s=r(i);fetch(i.href,s)}})();var S={context:void 0,registry:void 0,effects:void 0,done:!1,getContextId(){return ye(this.context.count)},getNextContextId(){return ye(this.context.count++)}};function ye(e){const t=String(e),r=t.length-1;return S.context.id+(r?String.fromCharCode(96+r):"")+t}function he(e){S.context=e}function qe(){return{...S.context,id:S.getNextContextId(),count:0}}var Ke=(e,t)=>e===t,We=Symbol("solid-track"),se={equals:Ke},xe=null,Oe=Ie,M=1,Y=2,Me={owned:null,cleanups:null,context:null,owner:null},y=null,v=null,X=null,G=null,C=null,T=null,A=null,ce=0;function le(e,t){const r=C,n=y,i=e.length===0,s=t===void 0?n:t,d=i?Me:{owned:null,cleanups:null,context:s?s.context:null,owner:s},o=i?e:()=>e(()=>I(()=>F(d)));y=d,C=null;try{return U(o,!0)}finally{C=r,y=n}}function _(e,t){t=t?Object.assign({},se,t):se;const r={value:e,observers:null,observerSlots:null,comparator:t.equals||void 0},n=i=>(typeof i=="function"&&(v&&v.running&&v.sources.has(r)?i=i(r.tValue):i=i(r.value)),Pe(r,i));return[Ne.bind(r),n]}function R(e,t,r){const n=me(e,t,!1,M);X&&v&&v.running?T.push(n):ee(n)}function Ye(e,t,r){Oe=et;const n=me(e,t,!1,M),i=ge&&Je(ge);i&&(n.suspense=i),(!r||!r.render)&&(n.user=!0),A?A.push(n):ee(n)}function j(e,t,r){r=r?Object.assign({},se,r):se;const n=me(e,t,!0,0);return n.observers=null,n.observerSlots=null,n.comparator=r.equals||void 0,X&&v&&v.running?(n.tState=M,T.push(n)):ee(n),Ne.bind(n)}function I(e){if(!G&&C===null)return e();const t=C;C=null;try{return G?G.untrack(e):e()}finally{C=t}}function Z(e){Ye(()=>I(e))}function ue(e){return y===null||(y.cleanups===null?y.cleanups=[e]:y.cleanups.push(e)),e}function Qe(e){if(v&&v.running)return e(),v.done;const t=C,r=y;return Promise.resolve().then(()=>{C=t,y=r;let n;return(X||ge)&&(n=v||(v={sources:new Set,effects:[],promises:new Set,disposed:new Set,queue:new Set,running:!0}),n.done||(n.done=new Promise(i=>n.resolve=i)),n.running=!0),U(e,!1),C=y=null,n?n.done:void 0})}var[Rr,_e]=_(!1);function Je(e){let t;return y&&y.context&&(t=y.context[e.id])!==void 0?t:e.defaultValue}var ge;function Ne(){const e=v&&v.running;if(this.sources&&(e?this.tState:this.state))if((e?this.tState:this.state)===M)ee(this);else{const t=T;T=null,U(()=>oe(this),!1),T=t}if(C){const t=this.observers?this.observers.length:0;C.sources?(C.sources.push(this),C.sourceSlots.push(t)):(C.sources=[this],C.sourceSlots=[t]),this.observers?(this.observers.push(C),this.observerSlots.push(C.sources.length-1)):(this.observers=[C],this.observerSlots=[C.sources.length-1])}return e&&v.sources.has(this)?this.tValue:this.value}function Pe(e,t,r){let n=v&&v.running&&v.sources.has(e)?e.tValue:e.value;if(!e.comparator||!e.comparator(n,t)){if(v){const i=v.running;(i||!r&&v.sources.has(e))&&(v.sources.add(e),e.tValue=t),i||(e.value=t)}else e.value=t;e.observers&&e.observers.length&&U(()=>{for(let i=0;i<e.observers.length;i+=1){const s=e.observers[i],d=v&&v.running;d&&v.disposed.has(s)||((d?!s.tState:!s.state)&&(s.pure?T.push(s):A.push(s),s.observers&&Ue(s)),d?s.tState=M:s.state=M)}if(T.length>1e6)throw T=[],new Error},!1)}return t}function ee(e){if(!e.fn)return;F(e);const t=ce;we(e,v&&v.running&&v.sources.has(e)?e.tValue:e.value,t),v&&!v.running&&v.sources.has(e)&&queueMicrotask(()=>{U(()=>{v&&(v.running=!0),C=y=e,we(e,e.tValue,t),C=y=null},!1)})}function we(e,t,r){let n;const i=y,s=C;C=y=e;try{n=e.fn(t)}catch(d){return e.pure&&(v&&v.running?(e.tState=M,e.tOwned&&e.tOwned.forEach(F),e.tOwned=void 0):(e.state=M,e.owned&&e.owned.forEach(F),e.owned=null)),e.updatedAt=r+1,$e(d)}finally{C=s,y=i}(!e.updatedAt||e.updatedAt<=r)&&(e.updatedAt!=null&&"observers"in e?Pe(e,n,!0):v&&v.running&&e.pure?(v.sources.has(e)||(e.value=n),v.sources.add(e),e.tValue=n):e.value=n,e.updatedAt=r)}function me(e,t,r,n=M,i){const s={fn:e,state:n,updatedAt:null,owned:null,sources:null,sourceSlots:null,cleanups:null,value:t,owner:y,context:y?y.context:null,pure:r};if(v&&v.running&&(s.state=0,s.tState=n),y===null||y!==Me&&(v&&v.running&&y.pure?y.tOwned?y.tOwned.push(s):y.tOwned=[s]:y.owned?y.owned.push(s):y.owned=[s]),G&&s.fn){const d=s.fn,[o,l]=_(void 0,{equals:!1}),u=G.factory(d,l);ue(()=>u.dispose());let f;const a=()=>Qe(l).then(()=>{f&&(f.dispose(),f=void 0)});s.fn=h=>(o(),v&&v.running?(f||(f=G.factory(d,a)),f.track(h)):u.track(h))}return s}function Q(e){const t=v&&v.running;if((t?e.tState:e.state)===0)return;if((t?e.tState:e.state)===Y)return oe(e);if(e.suspense&&I(e.suspense.inFallback))return e.suspense.effects.push(e);const r=[e];for(;(e=e.owner)&&(!e.updatedAt||e.updatedAt<ce);){if(t&&v.disposed.has(e))return;(t?e.tState:e.state)&&r.push(e)}for(let n=r.length-1;n>=0;n--){if(e=r[n],t){let i=e,s=r[n+1];for(;(i=i.owner)&&i!==s;)if(v.disposed.has(i))return}if((t?e.tState:e.state)===M)ee(e);else if((t?e.tState:e.state)===Y){const i=T;T=null,U(()=>oe(e,r[0]),!1),T=i}}}function U(e,t){if(T)return e();let r=!1;t||(T=[]),A?r=!0:A=[],ce++;try{const n=e();return Xe(r),n}catch(n){r||(A=null),T=null,$e(n)}}function Xe(e){if(T&&(X&&v&&v.running?Ze(T):Ie(T),T=null),e)return;let t;if(v){if(!v.promises.size&&!v.queue.size){const n=v.sources,i=v.disposed;A.push.apply(A,v.effects),t=v.resolve;for(const s of A)"tState"in s&&(s.state=s.tState),delete s.tState;v=null,U(()=>{for(const s of i)F(s);for(const s of n){if(s.value=s.tValue,s.owned)for(let d=0,o=s.owned.length;d<o;d++)F(s.owned[d]);s.tOwned&&(s.owned=s.tOwned),delete s.tValue,delete s.tOwned,s.tState=0}_e(!1)},!1)}else if(v.running){v.running=!1,v.effects.push.apply(v.effects,A),A=null,_e(!0);return}}const r=A;A=null,r.length&&U(()=>Oe(r),!1),t&&t()}function Ie(e){for(let t=0;t<e.length;t++)Q(e[t])}function Ze(e){for(let t=0;t<e.length;t++){const r=e[t],n=v.queue;n.has(r)||(n.add(r),X(()=>{n.delete(r),U(()=>{v.running=!0,Q(r)},!1),v&&(v.running=!1)}))}}function et(e){let t,r=0;for(t=0;t<e.length;t++){const n=e[t];n.user?e[r++]=n:Q(n)}if(S.context){if(S.count){S.effects||(S.effects=[]),S.effects.push(...e.slice(0,r));return}he()}for(S.effects&&(S.done||!S.count)&&(e=[...S.effects,...e],r+=S.effects.length,delete S.effects),t=0;t<r;t++)Q(e[t])}function oe(e,t){const r=v&&v.running;r?e.tState=0:e.state=0;for(let n=0;n<e.sources.length;n+=1){const i=e.sources[n];if(i.sources){const s=r?i.tState:i.state;s===M?i!==t&&(!i.updatedAt||i.updatedAt<ce)&&Q(i):s===Y&&oe(i,t)}}}function Ue(e){const t=v&&v.running;for(let r=0;r<e.observers.length;r+=1){const n=e.observers[r];(t?!n.tState:!n.state)&&(t?n.tState=Y:n.state=Y,n.pure?T.push(n):A.push(n),n.observers&&Ue(n))}}function F(e){let t;if(e.sources)for(;e.sources.length;){const r=e.sources.pop(),n=e.sourceSlots.pop(),i=r.observers;if(i&&i.length){const s=i.pop(),d=r.observerSlots.pop();n<i.length&&(s.sourceSlots[d]=n,i[n]=s,r.observerSlots[n]=d)}}if(e.tOwned){for(t=e.tOwned.length-1;t>=0;t--)F(e.tOwned[t]);delete e.tOwned}if(v&&v.running&&e.pure)Be(e,!0);else if(e.owned){for(t=e.owned.length-1;t>=0;t--)F(e.owned[t]);e.owned=null}if(e.cleanups){for(t=e.cleanups.length-1;t>=0;t--)e.cleanups[t]();e.cleanups=null}v&&v.running?e.tState=0:e.state=0}function Be(e,t){if(t||(e.tState=0,v.disposed.add(e)),e.owned)for(let r=0;r<e.owned.length;r++)Be(e.owned[r])}function tt(e){return e instanceof Error?e:new Error(typeof e=="string"?e:"Unknown error",{cause:e})}function Se(e,t,r){try{for(const n of t)n(e)}catch(n){$e(n,r&&r.owner||null)}}function $e(e,t=y){const r=xe&&t&&t.context&&t.context[xe],n=tt(e);if(!r)throw n;A?A.push({fn(){Se(n,r,t)},state:M}):Se(n,r,t)}var rt=Symbol("fallback");function Ce(e){for(let t=0;t<e.length;t++)e[t]()}function nt(e,t,r={}){let n=[],i=[],s=[],d=0,o=t.length>1?[]:null;return ue(()=>Ce(s)),()=>{let l=e()||[],u=l.length,f,a;return l[We],I(()=>{let p,$,b,w,k,E,O,P,z;if(u===0)d!==0&&(Ce(s),s=[],n=[],i=[],d=0,o&&(o=[])),r.fallback&&(n=[rt],i[0]=le(Ge=>(s[0]=Ge,r.fallback())),d=1);else if(d===0){for(i=new Array(u),a=0;a<u;a++)n[a]=l[a],i[a]=le(h);d=u}else{for(b=new Array(u),w=new Array(u),o&&(k=new Array(u)),E=0,O=Math.min(d,u);E<O&&n[E]===l[E];E++);for(O=d-1,P=u-1;O>=E&&P>=E&&n[O]===l[P];O--,P--)b[P]=i[O],w[P]=s[O],o&&(k[P]=o[O]);for(p=new Map,$=new Array(P+1),a=P;a>=E;a--)z=l[a],f=p.get(z),$[a]=f===void 0?-1:f,p.set(z,a);for(f=E;f<=O;f++)z=n[f],a=p.get(z),a!==void 0&&a!==-1?(b[a]=i[f],w[a]=s[f],o&&(k[a]=o[f]),a=$[a],p.set(z,a)):s[f]();for(a=E;a<u;a++)a in b?(i[a]=b[a],s[a]=w[a],o&&(o[a]=k[a],o[a](a))):i[a]=le(h);i=i.slice(0,d=u),n=l.slice(0)}return i});function h(p){if(s[a]=p,o){const[$,b]=_(a);return o[a]=b,t(l[a],$)}return t(l[a])}}}var lt=!1;function g(e,t){if(lt&&S.context){const r=S.context;he(qe());const n=I(()=>e(t||{}));return he(r),n}return I(()=>e(t||{}))}var it=e=>`Stale read from <${e}>.`;function N(e){const t="fallback"in e&&{fallback:()=>e.fallback};return j(nt(()=>e.each,e.children,t||void 0))}function x(e){const t=e.keyed,r=j(()=>e.when,void 0,void 0),n=t?r:j(r,void 0,{equals:(i,s)=>!i==!s});return j(()=>{const i=n();if(i){const s=e.children;return typeof s=="function"&&s.length>0?I(()=>s(t?i:()=>{if(!I(n))throw it("Show");return r()})):s}return e.fallback},void 0,void 0)}var st=e=>j(()=>e());function ot(e,t,r){let n=r.length,i=t.length,s=n,d=0,o=0,l=t[i-1].nextSibling,u=null;for(;d<i||o<s;){if(t[d]===r[o]){d++,o++;continue}for(;t[i-1]===r[s-1];)i--,s--;if(i===d){const f=s<n?o?r[o-1].nextSibling:r[s-o]:l;for(;o<s;)e.insertBefore(r[o++],f)}else if(s===o)for(;d<i;)(!u||!u.has(t[d]))&&t[d].remove(),d++;else if(t[d]===r[s-1]&&r[o]===t[i-1]){const f=t[--i].nextSibling;e.insertBefore(r[o++],t[d++].nextSibling),e.insertBefore(r[--s],f),t[i]=r[s]}else{if(!u){u=new Map;let a=o;for(;a<s;)u.set(r[a],a++)}const f=u.get(t[d]);if(f!=null)if(o<f&&f<s){let a=d,h=1,p;for(;++a<i&&a<s&&!((p=u.get(t[a]))==null||p!==f+h);)h++;if(h>f-o){const $=t[d];for(;o<f;)e.insertBefore(r[o++],$)}else e.replaceChild(r[o++],t[d++])}else d++;else t[d++].remove()}}}var ke="_$DX_DELEGATE";function at(e,t,r,n={}){let i;return le(s=>{i=s,t===document?e():c(t,e(),t.firstChild?null:void 0,r)},n.owner),()=>{i(),t.textContent=""}}function m(e,t,r,n){let i;const s=()=>{const o=n?document.createElementNS("http://www.w3.org/1998/Math/MathML","template"):document.createElement("template");return o.innerHTML=e,r?o.content.firstChild.firstChild:n?o.firstChild:o.content.firstChild},d=t?()=>I(()=>document.importNode(i||(i=s()),!0)):()=>(i||(i=s())).cloneNode(!0);return d.cloneNode=d,d}function de(e,t=window.document){const r=t[ke]||(t[ke]=new Set);for(let n=0,i=e.length;n<i;n++){const s=e[n];r.has(s)||(r.add(s),t.addEventListener(s,ct))}}function V(e,t,r){be(e)||(r==null?e.removeAttribute(t):e.setAttribute(t,r))}function J(e,t){be(e)||(t==null?e.removeAttribute("class"):e.className=t)}function c(e,t,r,n){if(r!==void 0&&!n&&(n=[]),typeof t!="function")return ae(e,t,n,r);R(i=>ae(e,t(),i,r),n)}function be(e){return!!S.context&&!S.done&&(!e||e.isConnected)}function ct(e){if(S.registry&&S.events&&S.events.find(([l,u])=>u===e))return;let t=e.target;const r=`$$${e.type}`,n=e.target,i=e.currentTarget,s=l=>Object.defineProperty(e,"target",{configurable:!0,value:l}),d=()=>{const l=t[r];if(l&&!t.disabled){const u=t[`${r}Data`];if(u!==void 0?l.call(t,u,e):l.call(t,e),e.cancelBubble)return}return t.host&&typeof t.host!="string"&&!t.host._$host&&t.contains(e.target)&&s(t.host),!0},o=()=>{for(;d()&&(t=t._$host||t.parentNode||t.host););};if(Object.defineProperty(e,"currentTarget",{configurable:!0,get(){return t||document}}),S.registry&&!S.done&&(S.done=_$HY.done=!0),e.composedPath){const l=e.composedPath();s(l[0]);for(let u=0;u<l.length-2&&(t=l[u],!!d());u++){if(t._$host){t=t._$host,o();break}if(t.parentNode===i)break}}else o();s(n)}function ae(e,t,r,n,i){const s=be(e);if(s){!r&&(r=[...e.childNodes]);let l=[];for(let u=0;u<r.length;u++){const f=r[u];f.nodeType===8&&f.data.slice(0,2)==="!$"?f.remove():l.push(f)}r=l}for(;typeof r=="function";)r=r();if(t===r)return r;const d=typeof t,o=n!==void 0;if(e=o&&r[0]&&r[0].parentNode||e,d==="string"||d==="number"){if(s||d==="number"&&(t=t.toString(),t===r))return r;if(o){let l=r[0];l&&l.nodeType===3?l.data!==t&&(l.data=t):l=document.createTextNode(t),r=D(e,r,n,l)}else r!==""&&typeof r=="string"?r=e.firstChild.data=t:r=e.textContent=t}else if(t==null||d==="boolean"){if(s)return r;r=D(e,r,n)}else{if(d==="function")return R(()=>{let l=t();for(;typeof l=="function";)l=l();r=ae(e,l,r,n)}),()=>r;if(Array.isArray(t)){const l=[],u=r&&Array.isArray(r);if(ve(l,t,r,i))return R(()=>r=ae(e,l,r,n,!0)),()=>r;if(s){if(!l.length)return r;if(n===void 0)return r=[...e.childNodes];let f=l[0];if(f.parentNode!==e)return r;const a=[f];for(;(f=f.nextSibling)!==n;)a.push(f);return r=a}if(l.length===0){if(r=D(e,r,n),o)return r}else u?r.length===0?Te(e,l,n):ot(e,r,l):(r&&D(e),Te(e,l));r=l}else if(t.nodeType){if(s&&t.parentNode)return r=o?[t]:t;if(Array.isArray(r)){if(o)return r=D(e,r,n,t);D(e,r,null,t)}else r==null||r===""||!e.firstChild?e.appendChild(t):e.replaceChild(t,e.firstChild);r=t}}return r}function ve(e,t,r,n){let i=!1;for(let s=0,d=t.length;s<d;s++){let o=t[s],l=r&&r[e.length],u;if(!(o==null||o===!0||o===!1))if((u=typeof o)=="object"&&o.nodeType)e.push(o);else if(Array.isArray(o))i=ve(e,o,l)||i;else if(u==="function")if(n){for(;typeof o=="function";)o=o();i=ve(e,Array.isArray(o)?o:[o],Array.isArray(l)?l:[l])||i}else e.push(o),i=!0;else{const f=String(o);l&&l.nodeType===3&&l.data===f?e.push(l):e.push(document.createTextNode(f))}}return i}function Te(e,t,r=null){for(let n=0,i=t.length;n<i;n++)e.insertBefore(t[n],r)}function D(e,t,r,n){if(r===void 0)return e.textContent="";const i=n||document.createTextNode("");if(t.length){let s=!1;for(let d=t.length-1;d>=0;d--){const o=t[d];if(i!==o){const l=o.parentNode===e;!s&&!d?l?e.replaceChild(i,o):e.insertBefore(i,r):l&&o.remove()}else s=!0}}else e.insertBefore(i,r);return[i]}var ut=m("<svg class=sparkline preserveAspectRatio=none aria-hidden=true><path>"),ie=e=>{const t=()=>e.width??200,n=j(()=>{const i=e.values;if(i.length<2)return"";const s=Math.min(...i),d=Math.max(...i)-s||1,o=t()/(i.length-1),l=f=>40-(f-s)/d*36-2;let u=`M0 ${l(i[0]).toFixed(2)}`;for(let f=1;f<i.length;f++)u+=` L${(f*o).toFixed(2)} ${l(i[f]).toFixed(2)}`;return u});return(()=>{var i=ut(),s=i.firstChild;return R(d=>{var o=`0 0 ${t()} 40`,l=n(),u=e.stroke;return o!==d.e&&V(i,"viewBox",d.e=o),l!==d.t&&V(s,"d",d.t=l),u!==d.a&&V(s,"stroke",d.a=u),d},{e:void 0,t:void 0,a:void 0}),i})()};function te(){return document.body.dataset.base??"/_debug"}function q(){return new URLSearchParams(location.search).get("token")}function je(e){const t=q();return t?`${e}${e.includes("?")?"&":"?"}token=${encodeURIComponent(t)}`:e}async function K(e){const t=je(`${te()}${e}`),r=await fetch(t,{headers:dt()});if(!r.ok)throw new Error(`GET ${e} failed: ${r.status} ${r.statusText}`);return await r.json()}function dt(){const e=q();return e?{"x-devtools-token":e}:{}}var re={runtime:()=>K("/runtime"),topology:()=>K("/topology"),routes:()=>K("/metrics"),health:()=>K("/health"),tabs:()=>K("/tabs")};function Fe(e,t,r){const n=je(`${te()}${e}`),i=new EventSource(n);return i.onmessage=s=>{try{t(JSON.parse(s.data))}catch(d){console.warn("SSE parse failed",d)}},r&&(i.onerror=r),()=>i.close()}function L(e){return e<1024?`${e} B`:e<1024*1024?`${(e/1024).toFixed(1)} KiB`:e<1024*1024*1024?`${(e/1024/1024).toFixed(1)} MiB`:`${(e/1024/1024/1024).toFixed(2)} GiB`}function ft(e){if(e===0)return"0";const t=e*60;return Math.abs(t)<1024*1024?`${(t/1024).toFixed(1)} KiB/min`:`${(t/1024/1024).toFixed(1)} MiB/min`}function H(e){return e<1?`${e.toFixed(2)} ms`:e<100?`${e.toFixed(1)} ms`:`${Math.round(e)} ms`}function He(e){const t=Math.floor(e);return t<60?`${t}s`:t<3600?`${Math.floor(t/60)}m ${t%60}s`:`${Math.floor(t/3600)}h ${Math.floor(t%3600/60)}m ${t%60}s`}function pe(e){return`${Math.round(e*100)}%`}var ht=m("<div class=status><span><span></span></span><span> samples"),gt=m("<div class=empty>Loading runtime metrics…"),vt=m("<div class=empty style=padding:0;text-align:left;font-size:11px>GC: <!> runs, <!> total"),Ee=m("<div class=grid>"),pt=m("<div class=card><div class=card-header><div class=card-title></div><div class=card-value>"),fe=60,mt=()=>{const[e,t]=_(null),[r,n]=_([]),[i,s]=_([]),[d,o]=_([]),[l,u]=_(!1),f=a=>{t(a),n(h=>[...h,a.memory.heapUsed].slice(-fe)),s(h=>[...h,a.memory.rss].slice(-fe)),o(h=>[...h,a.eventLoop.p99].slice(-fe))};return Z(()=>{re.runtime().then(a=>{for(const h of a.history)f(h);u(!0)}).catch(()=>u(!1)),ue(Fe("/runtime/stream",a=>{u(!0),f(a)},()=>u(!1)))}),[g(x,{get when(){return e()},get fallback(){return gt()},children:a=>[(()=>{var h=Ee();return c(h,g(B,{title:"Heap used",get value(){return L(a().memory.heapUsed)},get children(){return g(ie,{get values(){return r()}})}}),null),c(h,g(B,{title:"RSS",get value(){return L(a().memory.rss)},get children(){return g(ie,{get values(){return i()}})}}),null),c(h,g(B,{title:"Event loop p99",get value(){return H(a().eventLoop.p99)},get children(){return g(ie,{get values(){return d()}})}}),null),c(h,g(B,{title:"Uptime",get value(){return He(a().uptimeSec)},get children(){var p=vt(),$=p.firstChild.nextSibling,b=$.nextSibling.nextSibling;return b.nextSibling,c(p,()=>a().gc.count,$),c(p,()=>H(a().gc.totalPauseMs),b),p}}),null),h})(),(()=>{var h=Ee();return c(h,g(B,{title:"External",get value(){return L(a().memory.external)}}),null),c(h,g(B,{title:"Array buffers",get value(){return L(a().memory.arrayBuffers)}}),null),c(h,g(B,{title:"Heap total",get value(){return L(a().memory.heapTotal)}}),null),c(h,g(B,{title:"CPU (last sample)",get value(){return`${(a().cpu.userMicros/1e3).toFixed(1)} ms u / ${(a().cpu.systemMicros/1e3).toFixed(1)} ms s`}}),null),h})()]}),(()=>{var a=ht(),h=a.firstChild,p=h.firstChild,$=h.nextSibling,b=$.firstChild;return c(h,()=>l()?"streaming":"reconnecting…",null),c($,()=>r().length,b),R(()=>J(p,`status-dot ${l()?"connected":"disconnected"}`)),a})()]},B=e=>(()=>{var t=pt(),r=t.firstChild.firstChild,n=r.nextSibling;return c(r,()=>e.title),c(n,()=>e.value),c(t,()=>e.children,null),t})(),$t=m("<div class=status><span><span></span></span><span> samples"),bt=m("<div class=empty>Loading memory metrics…"),yt=m("<div class=card><div class=card-header><div class=card-title>Leak risk</div><span></span></div><div style=display:flex;justify-content:space-between;align-items:baseline><div class=card-value> growth</div><div class=card-value style=font-size:13px;color:var(--text-dim)>GC reclaim <!> • heap util "),xt=m("<div class=grid>"),_t=m("<table><thead><tr><th>Type</th><th style=text-align:right>Count</th></tr></thead><tbody>"),wt=m("<div class=card><div class=card-header><div class=card-title>Active handles</div><div class=card-value>"),St=m("<div class=empty>No handle inventory available on this runtime."),Ct=m("<tr><td></td><td style=text-align:right>"),kt=m("<div class=card><div class=card-header><div class=card-title></div><div class=card-value>"),Tt=m('<div class=card><div class=card-header><div class=card-title>Heap snapshot</div><div class=card-value style=font-size:13px;color:var(--text-dim)>~<!> estimate</div></div><p style="margin:0 0 12px;color:var(--text-dim);font-size:12px">Captures a V8 heap snapshot for retained-size analysis in Chrome DevTools (Memory tab, Load profile). Capture blocks the event loop for several seconds — avoid in production.</p><div style=display:flex;align-items:center;gap:12px><button type=button class=tab style="border:1px solid var(--accent);border-radius:4px;padding:6px 14px;border-bottom-color:var(--accent);color:var(--accent)">'),Et=m("<span style=color:var(--text-dim);font-size:12px;font-family:var(--font-mono)>Last: <!> (<!>)"),Ve=m('<p style="margin:8px 0 0;color:var(--critical);font-size:12px;font-family:var(--font-mono)">'),At=m('<div class=card><div class=card-header><div class=card-title>Force GC</div></div><p style="margin:0 0 12px;color:var(--text-dim);font-size:12px">Triggers a synchronous V8 GC pass to confirm whether heap growth is real retention or just uncollected garbage. Requires <code>node --expose-gc</code>.</p><div style=display:flex;align-items:center;gap:12px;flex-wrap:wrap><button type=button class=tab style="border:1px solid var(--accent);border-radius:4px;padding:6px 14px;border-bottom-color:var(--accent);color:var(--accent)">'),Rt=m("<span style=color:var(--text-dim);font-size:12px;font-family:var(--font-mono)>Reclaimed <!> (<!> → <!>) in <!>ms"),Ae=60,Lt=()=>{const[e,t]=_(null),[r,n]=_([]),[i,s]=_(!1),d=l=>{t(l),n(u=>[...u,l.snapshot.memory.heapUsed].slice(-Ae))};Z(()=>{re.runtime().then(l=>{l.latest&&(d({snapshot:l.latest,health:l.health}),n(l.history.map(u=>u.memory.heapUsed).slice(-Ae))),s(!0)}).catch(()=>s(!1)),ue(Fe("/memory/stream",l=>{s(!0),d(l)},()=>s(!1)))});const o=j(()=>{const l=e();return l?Object.entries(l.health.handlesByType).sort((u,f)=>f[1]-u[1]):[]});return[g(x,{get when(){return e()},get fallback(){return bt()},children:l=>[(()=>{var u=yt(),f=u.firstChild,a=f.firstChild.nextSibling,h=f.nextSibling.firstChild,p=h.firstChild,$=h.nextSibling,b=$.firstChild.nextSibling;return b.nextSibling,c(a,()=>l().health.heapGrowthSeverity),c(h,()=>ft(l().health.heapGrowthBytesPerSec),p),c($,()=>pe(l().health.gcReclaimRatio),b),c($,()=>pe(l().health.heapUtilization),null),c(u,g(ie,{get values(){return r()}}),null),R(()=>J(a,`badge badge-${l().health.heapGrowthSeverity}`)),u})(),(()=>{var u=xt();return c(u,g(ne,{title:"Heap used",get value(){return L(l().snapshot.memory.heapUsed)}}),null),c(u,g(ne,{title:"Heap total",get value(){return L(l().snapshot.memory.heapTotal)}}),null),c(u,g(ne,{title:"RSS",get value(){return L(l().snapshot.memory.rss)}}),null),c(u,g(ne,{title:"External",get value(){return L(l().snapshot.memory.external)}}),null),u})(),(()=>{var u=wt(),f=u.firstChild.firstChild.nextSibling;return c(f,()=>l().health.activeHandles),c(u,g(x,{get when(){return o().length>0},get fallback(){return St()},get children(){var a=_t(),h=a.firstChild.nextSibling;return c(h,g(N,{get each(){return o()},children:([p,$])=>(()=>{var b=Ct(),w=b.firstChild,k=w.nextSibling;return c(w,p),c(k,$),b})()})),a}}),null),u})(),g(Ot,{get heapTotal(){return l().snapshot.memory.heapTotal}}),g(Pt,{})]}),(()=>{var l=$t(),u=l.firstChild,f=u.firstChild,a=u.nextSibling,h=a.firstChild;return c(u,()=>i()?"streaming":"reconnecting…",null),c(a,()=>r().length,h),R(()=>J(f,`status-dot ${i()?"connected":"disconnected"}`)),l})()]},ne=e=>(()=>{var t=kt(),r=t.firstChild.firstChild,n=r.nextSibling;return c(r,()=>e.title),c(n,()=>e.value),t})(),Ot=e=>{const[t,r]=_(!1),[n,i]=_(null),[s,d]=_(null),o=async()=>{r(!0),i(null);try{const l=q(),u=`${te()}/memory/snapshot${l?`?token=${encodeURIComponent(l)}`:""}`,f=await fetch(u,{method:"POST",headers:l?{"x-devtools-token":l}:void 0});if(!f.ok){let p=`${f.status} ${f.statusText}`;try{const $=await f.json();$.error&&(p=$.error)}catch{}throw new Error(p)}const a=await f.blob(),h=Mt(f.headers.get("content-disposition"))??`kickjs-heap-${Date.now()}.heapsnapshot`;Nt(a,h),d({name:h,sizeBytes:a.size})}catch(l){i(l instanceof Error?l.message:String(l))}finally{r(!1)}};return(()=>{var l=Tt(),u=l.firstChild,f=u.firstChild.nextSibling,a=f.firstChild.nextSibling;a.nextSibling;var h=u.nextSibling.nextSibling,p=h.firstChild;return c(f,()=>L(e.heapTotal),a),p.$$click=()=>{o()},c(p,()=>t()?"Capturing…":"Take snapshot"),c(h,g(x,{get when(){return s()},children:$=>(()=>{var b=Et(),w=b.firstChild.nextSibling,k=w.nextSibling.nextSibling;return k.nextSibling,c(b,()=>$().name,w),c(b,()=>L($().sizeBytes),k),b})()}),null),c(l,g(x,{get when(){return n()},children:$=>(()=>{var b=Ve();return c(b,$),b})()}),null),R(()=>p.disabled=t()),l})()};function Mt(e){if(!e)return null;const t=/filename="?([^";]+)"?/.exec(e);return t?t[1]:null}function Nt(e,t){const r=URL.createObjectURL(e),n=document.createElement("a");n.href=r,n.download=t,document.body.appendChild(n),n.click(),document.body.removeChild(n),URL.revokeObjectURL(r)}var Pt=()=>{const[e,t]=_(!1),[r,n]=_(null),[i,s]=_(null),d=async()=>{t(!0),s(null);try{const o=q(),l=`${te()}/memory/gc${o?`?token=${encodeURIComponent(o)}`:""}`,u=await fetch(l,{method:"POST",headers:o?{"x-devtools-token":o}:void 0});if(!u.ok){let f=`${u.status} ${u.statusText}`;try{const a=await u.json();a.error&&(f=a.error)}catch{}throw new Error(f)}n(await u.json())}catch(o){s(o instanceof Error?o.message:String(o))}finally{t(!1)}};return(()=>{var o=At(),l=o.firstChild.nextSibling.nextSibling,u=l.firstChild;return u.$$click=()=>{d()},c(u,()=>e()?"Collecting…":"Run GC"),c(l,g(x,{get when(){return r()},children:f=>(()=>{var a=Rt(),h=a.firstChild.nextSibling,p=h.nextSibling.nextSibling,$=p.nextSibling.nextSibling,b=$.nextSibling.nextSibling;return b.nextSibling,c(a,()=>L(f().reclaimedBytes),h),c(a,()=>L(f().before),p),c(a,()=>L(f().after),$),c(a,()=>f().elapsedMs,b),a})()}),null),c(o,g(x,{get when(){return i()},children:f=>(()=>{var a=Ve();return c(a,f),a})()}),null),R(()=>u.disabled=e()),o})()};de(["click"]);var It=m('<div style=display:flex;justify-content:space-between;align-items:center;margin-bottom:12px><h2 style=margin:0;font-size:16px>Topology</h2><button type=button class=tab style="border:1px solid var(--border);border-radius:4px;padding:4px 12px;border-bottom-color:var(--border)">'),Ut=m("<div class=card style=border-color:var(--critical)><div class=card-title style=color:var(--critical)>Topology error</div><code>"),Bt=m("<div class=empty>Loading topology…"),jt=m('<div class=card style=border-color:var(--warn)><div class=card-title style=color:var(--warn)> introspect failure(s)</div><ul style="margin:8px 0 0;padding-left:20px;font-family:var(--font-mono);font-size:12px">'),Ft=m("<table><thead><tr><th>Key</th><th>Source</th><th>Depends on</th></tr></thead><tbody>"),Ht=m("<div class=card><div class=card-header><div class=card-title>Contributors (<!>)"),Vt=m("<table><thead><tr><th>Token</th><th>Scope</th><th>Kind</th><th>Resolved</th></tr></thead><tbody>"),zt=m("<div class=card><div class=card-header><div class=card-title>DI tokens (<!>)"),Dt=m("<li>/<!>: "),Gt=m("<div class=empty>No contributors registered"),qt=m("<tr><td><span class=tree-key></span></td><td></td><td>"),Kt=m("<div class=empty>No DI tokens registered"),Wt=m("<tr><td><span class=tree-key></span></td><td></td><td></td><td>"),Yt=m("<div class=card><div class=card-header><div class=card-title>"),Qt=m("<div class=empty>"),Jt=m("<span class=tree-meta>v"),Xt=m("<div class=tree-meta>provides: "),Zt=m("<div class=tree-node><span class=tree-key>"),er=m("<div class=tree-meta>"),tr=m("<span style=margin-right:12px>: <strong style=color:var(--text)>"),rr=()=>{const[e,t]=_(null),[r,n]=_(null),[i,s]=_(!1),d=async()=>{s(!0),n(null);try{t(await re.topology())}catch(o){n(o instanceof Error?o.message:String(o))}finally{s(!1)}};return Z(()=>{d()}),[(()=>{var o=It(),l=o.firstChild.nextSibling;return l.$$click=()=>{d()},c(l,()=>i()?"Refreshing…":"Refresh"),R(()=>l.disabled=i()),o})(),g(x,{get when(){return r()},children:o=>(()=>{var l=Ut(),u=l.firstChild.nextSibling;return c(u,o),l})()}),g(x,{get when(){return e()},get fallback(){return Bt()},children:o=>[g(x,{get when(){return o().errors.length>0},get children(){var l=jt(),u=l.firstChild,f=u.firstChild,a=u.nextSibling;return c(u,()=>o().errors.length,f),c(a,g(N,{get each(){return o().errors},children:h=>(()=>{var p=Dt(),$=p.firstChild,b=$.nextSibling;return b.nextSibling,c(p,()=>h.kind,$),c(p,()=>h.name,b),c(p,()=>h.message,null),p})()})),l}}),g(Re,{get title(){return`Plugins (${o().plugins.length})`},get items(){return o().plugins},empty:"No plugins registered"}),g(Re,{get title(){return`Adapters (${o().adapters.length})`},get items(){return o().adapters},empty:"No adapters registered"}),(()=>{var l=Ht(),u=l.firstChild.firstChild,f=u.firstChild.nextSibling;return f.nextSibling,c(u,()=>o().contributors.length,f),c(l,g(x,{get when(){return o().contributors.length>0},get fallback(){return Gt()},get children(){var a=Ft(),h=a.firstChild.nextSibling;return c(h,g(N,{get each(){return o().contributors},children:p=>(()=>{var $=qt(),b=$.firstChild,w=b.firstChild,k=b.nextSibling,E=k.nextSibling;return c(w,()=>p.key),c(k,()=>p.source),c(E,()=>p.dependsOn.join(", ")||"—"),$})()})),a}}),null),l})(),(()=>{var l=zt(),u=l.firstChild.firstChild,f=u.firstChild.nextSibling;return f.nextSibling,c(u,()=>o().diTokens.length,f),c(l,g(x,{get when(){return o().diTokens.length>0},get fallback(){return Kt()},get children(){var a=Vt(),h=a.firstChild.nextSibling;return c(h,g(N,{get each(){return o().diTokens},children:p=>(()=>{var $=Wt(),b=$.firstChild,w=b.firstChild,k=b.nextSibling,E=k.nextSibling,O=E.nextSibling;return c(w,()=>p.token),c(k,()=>p.scope),c(E,()=>p.kind),c(O,()=>p.instantiated?"yes":"no"),$})()})),a}}),null),l})()]})]},Re=e=>(()=>{var t=Yt(),r=t.firstChild.firstChild;return c(r,()=>e.title),c(t,g(x,{get when(){return e.items.length>0},get fallback(){return(()=>{var n=Qt();return c(n,()=>e.empty),n})()},get children(){return g(N,{get each(){return e.items},children:n=>g(nr,{item:n})})}}),null),t})(),nr=e=>(()=>{var t=Zt(),r=t.firstChild;return c(r,()=>e.item.name),c(t,g(x,{get when(){return e.item.version},get children(){var n=Jt();return n.firstChild,c(n,()=>e.item.version,null),n}}),null),c(t,g(x,{get when(){return e.item.tokens?.provides.length},get children(){var n=Xt();return n.firstChild,c(n,()=>e.item.tokens?.provides.join(", "),null),n}}),null),c(t,g(x,{get when(){return e.item.metrics},children:n=>(()=>{var i=er();return c(i,g(N,{get each(){return Object.entries(n())},children:([s,d])=>(()=>{var o=tr(),l=o.firstChild,u=l.nextSibling;return c(o,s,l),c(u,d),o})()})),i})()}),null),t})();de(["click"]);var lr=m("<div class=card style=border-color:var(--critical)><div class=card-title style=color:var(--critical)>Failed to load metrics</div><code>"),ir=m("<div class=empty>Loading route metrics…"),sr=m("<div class=grid>"),or=m("<table><thead><tr><th>Route</th><th style=text-align:right>Calls</th><th style=text-align:right>Avg</th><th style=text-align:right>p50</th><th style=text-align:right>p95</th><th style=text-align:right>p99</th><th style=text-align:right>Max</th></tr></thead><tbody>"),ar=m("<div class=card><div class=card-header><div class=card-title>Per-route latency"),cr=m("<div class=empty>No requests recorded yet"),ur=m("<tr><td></td><td style=text-align:right></td><td style=text-align:right></td><td style=text-align:right></td><td style=text-align:right></td><td style=text-align:right></td><td style=text-align:right>"),dr=m("<div class=card><div class=card-header><div class=card-title></div><div class=card-value>"),fr=()=>{const[e,t]=_(null),[r,n]=_(null),i=async()=>{try{t(await re.routes()),n(null)}catch(d){n(d instanceof Error?d.message:String(d))}};Z(()=>{i();const d=setInterval(()=>{i()},2e3);return()=>clearInterval(d)});const s=j(()=>{const d=e();return d?Object.entries(d.routeLatency).map(([o,l])=>({key:o,...l,avg:l.count>0?l.totalMs/l.count:0})).sort((o,l)=>l.count-o.count):[]});return[g(x,{get when(){return r()},children:d=>(()=>{var o=lr(),l=o.firstChild.nextSibling;return c(l,d),o})()}),g(x,{get when(){return e()},get fallback(){return ir()},children:d=>[(()=>{var o=sr();return c(o,g(W,{title:"Requests",get value(){return d().requests.toString()}}),null),c(o,g(W,{title:"Server errors",get value(){return d().serverErrors.toString()}}),null),c(o,g(W,{title:"Client errors",get value(){return d().clientErrors.toString()}}),null),c(o,g(W,{title:"Error rate",get value(){return pe(d().errorRate)}}),null),c(o,g(W,{title:"Uptime",get value(){return He(d().uptimeSeconds)}}),null),o})(),(()=>{var o=ar();return o.firstChild,c(o,g(x,{get when(){return s().length>0},get fallback(){return cr()},get children(){var l=or(),u=l.firstChild.nextSibling;return c(u,g(N,{get each(){return s()},children:f=>(()=>{var a=ur(),h=a.firstChild,p=h.nextSibling,$=p.nextSibling,b=$.nextSibling,w=b.nextSibling,k=w.nextSibling,E=k.nextSibling;return c(h,()=>f.key),c(p,()=>f.count),c($,()=>H(f.avg)),c(b,()=>H(f.p50)),c(w,()=>H(f.p95)),c(k,()=>H(f.p99)),c(E,()=>H(f.maxMs)),a})()})),l}}),null),o})()]})]},W=e=>(()=>{var t=dr(),r=t.firstChild.firstChild,n=r.nextSibling;return c(r,()=>e.title),c(n,()=>e.value),t})(),hr=m("<div style=display:flex;flex-direction:column;height:100%>"),gr=m('<iframe title="custom devtools tab"sandbox="allow-scripts allow-same-origin allow-forms"style=flex:1;width:100%;border:none;background:var(--bg)>'),vr=m("<div style=padding:16px><div class=card>"),pr=m("<div style=color:var(--text-dim);font-size:12px>"),mr=m('<div style="display:flex;align-items:center;justify-content:space-between;padding:8px 0;border-bottom:1px solid var(--border)"><div><div style=font-weight:600></div></div><button type=button class=tab style="border:1px solid var(--border);border-radius:4px;padding:6px 14px;border-bottom-color:var(--border)">'),$r=m("<div class=card><div class=card-title>Last response</div><code>"),br=m("<div style=padding:16px;flex:1;overflow:auto>"),yr=e=>(()=>{var t=hr();return c(t,g(x,{get when(){return e.tab.view.type==="iframe"},get children(){return g(xr,{get view(){return e.tab.view}})}}),null),c(t,g(x,{get when(){return e.tab.view.type==="launch"},get children(){return g(_r,{get view(){return e.tab.view},get tabId(){return e.tab.id}})}}),null),c(t,g(x,{get when(){return e.tab.view.type==="html"},get children(){return g(wr,{get view(){return e.tab.view}})}}),null),t})(),xr=e=>{const t=()=>{const r=e.view.src,n=(r.startsWith("http")||r.startsWith("//"),r),i=q();return i?`${n}${n.includes("?")?"&":"?"}token=${encodeURIComponent(i)}`:n};return(()=>{var r=gr();return R(()=>V(r,"src",t())),r})()},_r=e=>{const[t,r]=_(null),[n,i]=_(null),s=async d=>{r(d),i(null);try{const o=`${te()}/tabs/${encodeURIComponent(e.tabId)}/actions/${encodeURIComponent(d)}`,l=q(),u=await fetch(l?`${o}?token=${encodeURIComponent(l)}`:o,{method:"POST",headers:l?{"x-devtools-token":l}:void 0});i(`${u.status} ${u.statusText}`)}catch(o){i(o instanceof Error?o.message:String(o))}finally{r(null)}};return(()=>{var d=vr(),o=d.firstChild;return c(o,g(N,{get each(){return e.view.actions},children:l=>(()=>{var u=mr(),f=u.firstChild,a=f.firstChild,h=f.nextSibling;return c(a,()=>l.label),c(f,g(x,{get when(){return l.description},get children(){var p=pr();return c(p,()=>l.description),p}}),null),h.$$click=()=>{s(l.id)},c(h,()=>t()===l.id?"Running…":"Run"),R(()=>h.disabled=t()===l.id),u})()})),c(d,g(x,{get when(){return n()},children:l=>(()=>{var u=$r(),f=u.firstChild.nextSibling;return c(f,l),u})()}),null),d})()},wr=e=>(()=>{var t=br();return R(()=>t.innerHTML=e.view.html),t})();de(["click"]);var Sr=m('<span aria-hidden=true style="width:1px;background:var(--border);margin:6px 8px">'),Cr=m('<div class=card style=border-color:var(--warn);margin-top:16px><div class=card-title style=color:var(--warn)> custom-tab issue(s)</div><ul style="margin:8px 0 0;padding-left:20px;font-family:var(--font-mono);font-size:12px">'),kr=m("<div class=app><nav class=tabs role=tablist></nav><main class=tab-body>"),Le=m("<button type=button role=tab>"),Tr=m("<li>: "),ze=[{id:"runtime",label:"Runtime"},{id:"memory",label:"Memory"},{id:"topology",label:"Topology"},{id:"routes",label:"Routes"}],Er=new Set(ze.map(e=>e.id)),Ar=()=>{const[e,t]=_((()=>{try{const l=localStorage.getItem("kickjs-devtools-tab");if(l)return l}catch{}return"runtime"})()),[r,n]=_([]),[i,s]=_([]);Z(()=>{re.tabs().then(({tabs:l,errors:u})=>{n(l.filter(f=>!Er.has(f.id))),s(u)}).catch(()=>{})});const d=l=>{t(l);try{localStorage.setItem("kickjs-devtools-tab",l)}catch{}},o=()=>r().find(l=>l.id===e());return(()=>{var l=kr(),u=l.firstChild,f=u.nextSibling;return c(u,g(N,{each:ze,children:a=>(()=>{var h=Le();return h.$$click=()=>d(a.id),c(h,()=>a.label),R(p=>{var $=`tab ${e()===a.id?"active":""}`,b=e()===a.id;return $!==p.e&&J(h,p.e=$),b!==p.t&&V(h,"aria-selected",p.t=b),p},{e:void 0,t:void 0}),h})()}),null),c(u,g(x,{get when(){return r().length>0},get children(){return Sr()}}),null),c(u,g(N,{get each(){return r()},children:a=>(()=>{var h=Le();return h.$$click=()=>d(a.id),c(h,()=>a.title),R(p=>{var $=`tab ${e()===a.id?"active":""}`,b=e()===a.id,w=a.title;return $!==p.e&&J(h,p.e=$),b!==p.t&&V(h,"aria-selected",p.t=b),w!==p.a&&V(h,"title",p.a=w),p},{e:void 0,t:void 0,a:void 0}),h})()}),null),c(f,g(x,{get when(){return e()==="runtime"},get children(){return g(mt,{})}}),null),c(f,g(x,{get when(){return e()==="memory"},get children(){return g(Lt,{})}}),null),c(f,g(x,{get when(){return e()==="topology"},get children(){return g(rr,{})}}),null),c(f,g(x,{get when(){return e()==="routes"},get children(){return g(fr,{})}}),null),c(f,g(x,{get when(){return o()},children:a=>g(yr,{get tab(){return a()}})}),null),c(f,g(x,{get when(){return st(()=>i().length>0)()&&e()==="runtime"},get children(){var a=Cr(),h=a.firstChild,p=h.firstChild,$=h.nextSibling;return c(h,()=>i().length,p),c($,g(N,{get each(){return i()},children:b=>(()=>{var w=Tr(),k=w.firstChild;return c(w,()=>b.source,k),c(w,()=>b.reason,null),w})()})),a}}),null),l})()};de(["click"]);var De=document.getElementById("root");if(!De)throw new Error("#root not found");at(()=>g(Ar,{}),De);
@@ -0,0 +1,2 @@
1
+ :root{--bg:#0f1115;--bg-elev:#161922;--bg-hover:#1c2030;--border:#2a2f3d;--text:#d8dde6;--text-dim:#7e8696;--text-faint:#5b6070;--accent:#5b9dff;--ok:#4ade80;--warn:#facc15;--critical:#f87171;--font-mono:ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;--font-sans:-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen, Ubuntu, Cantarell, sans-serif}@media (prefers-color-scheme:light){:root{--bg:#fff;--bg-elev:#f7f8fa;--bg-hover:#ecf0f7;--border:#e2e6ee;--text:#1c2030;--text-dim:#5b6070;--text-faint:#9aa1b0;--accent:#2563eb}}*{box-sizing:border-box}html,body{background:var(--bg);height:100%;color:var(--text);font-family:var(--font-sans);margin:0;padding:0;font-size:14px;line-height:1.5}#root,.app{flex-direction:column;height:100%;display:flex}.tabs{background:var(--bg-elev);border-bottom:1px solid var(--border);gap:2px;padding:0 12px;display:flex}.tab{color:var(--text-dim);cursor:pointer;font:inherit;background:0 0;border:none;border-bottom:2px solid #0000;padding:10px 16px}.tab:hover{color:var(--text);background:var(--bg-hover)}.tab.active{color:var(--text);border-bottom-color:var(--accent)}.tab-body{flex:1;padding:16px;overflow:auto}.card{background:var(--bg-elev);border:1px solid var(--border);border-radius:6px;margin-bottom:12px;padding:12px 16px}.card-header{justify-content:space-between;align-items:baseline;margin-bottom:8px;display:flex}.card-title{text-transform:uppercase;letter-spacing:.05em;color:var(--text-dim);font-size:13px;font-weight:600}.card-value{font-family:var(--font-mono);color:var(--text);font-size:18px}.grid{grid-template-columns:repeat(auto-fill,minmax(220px,1fr));gap:12px;margin-bottom:12px;display:grid}.badge{text-transform:uppercase;border-radius:12px;padding:2px 8px;font-size:11px;font-weight:600;display:inline-block}.badge-ok{background:color-mix(in srgb, var(--ok) 20%, transparent);color:var(--ok)}.badge-warn{background:color-mix(in srgb, var(--warn) 20%, transparent);color:var(--warn)}.badge-critical{background:color-mix(in srgb, var(--critical) 25%, transparent);color:var(--critical)}table{border-collapse:collapse;width:100%;font-family:var(--font-mono);font-size:13px}th,td{text-align:left;border-bottom:1px solid var(--border);padding:6px 8px}th{color:var(--text-dim);text-transform:uppercase;letter-spacing:.05em;font-size:11px;font-weight:600}tr:hover td{background:var(--bg-hover)}.tree{font-family:var(--font-mono);font-size:13px}.tree-node{border-left:1px solid var(--border);margin-left:8px;padding:4px 0 4px 16px}.tree-key{color:var(--accent)}.tree-meta{color:var(--text-faint);margin-left:8px}.sparkline{width:100%;height:40px;display:block}.sparkline path{fill:none;stroke:var(--accent);stroke-width:1.5px}.status{background:var(--bg-elev);border-top:1px solid var(--border);font-family:var(--font-mono);color:var(--text-dim);justify-content:space-between;padding:6px 12px;font-size:11px;display:flex}.status-dot{border-radius:50%;width:8px;height:8px;margin-right:4px;display:inline-block}.status-dot.connected{background:var(--ok)}.status-dot.disconnected{background:var(--critical)}.empty{color:var(--text-dim);text-align:center;padding:32px;font-style:italic}
2
+ /*$vite$:1*/
@@ -0,0 +1,14 @@
1
+ <!doctype html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
+ <meta name="color-scheme" content="dark light" />
7
+ <title>KickJS DevTools</title>
8
+ <script type="module" crossorigin src="/_debug/assets/index.js"></script>
9
+ <link rel="stylesheet" crossorigin href="/_debug/assets/style.css">
10
+ </head>
11
+ <body>
12
+ <div id="root"></div>
13
+ </body>
14
+ </html>