@devframes/hub
Advanced tools
| import { t as DevframeDocksUserSettings } from "./settings-D7whfcx2.mjs"; | ||
| //#region src/events.d.ts | ||
| /** | ||
| * Centralized registry of every event, broadcast, RPC method, shared-state | ||
| * key, and channel name the hub uses — the single source of truth that keeps | ||
| * these names out of scattered string literals. | ||
| * | ||
| * **Keep this in sync with [`docs/content/8.references/3.events.md`](../../../docs/content/8.references/3.events.md)** | ||
| * (the Hub Events Reference): every name here appears in that page's tables, and | ||
| * every name there resolves to an entry here. Add, rename, or remove a name in | ||
| * both places in the same change, and reference `HUB_EVENTS.*` from call sites | ||
| * instead of re-typing a literal. | ||
| * | ||
| * The `.events` EventEmitter maps in `types/{docks,terminals,messages,commands}.ts` | ||
| * and the RPC augmentation interfaces in `node/context.ts` declare these same | ||
| * names as type-level keys (a literal is unavoidable in a type position); those | ||
| * declarations mirror this map and move with it. | ||
| */ | ||
| declare const HUB_EVENTS: { | ||
| /** | ||
| * Internal node `EventEmitter` events on `ctx.<subsystem>.events`. Emitted | ||
| * and consumed inside the node process (chiefly by `createHubContext`, which | ||
| * fans them out onto the wire); they never cross to the browser. | ||
| */ | ||
| readonly bus: { | ||
| readonly docksEntryUpdated: "docks:entry:updated"; | ||
| readonly docksActivate: "docks:activate"; | ||
| readonly terminalsSessionUpdated: "terminals:session:updated"; | ||
| readonly messagesAdded: "messages:added"; | ||
| readonly messagesUpdated: "messages:updated"; | ||
| readonly messagesRemoved: "messages:removed"; | ||
| readonly messagesCleared: "messages:cleared"; | ||
| readonly commandsRegistered: "commands:registered"; | ||
| readonly commandsUnregistered: "commands:unregistered"; | ||
| }; | ||
| /** Server RPC methods a connected client calls (client → server), `hub:` prefix. */ | ||
| readonly rpc: { | ||
| readonly docksActivate: "hub:docks:activate"; | ||
| readonly commandsExecute: "hub:commands:execute"; | ||
| readonly messagesAdd: "hub:messages:add"; | ||
| readonly messagesUpdate: "hub:messages:update"; | ||
| readonly messagesRemove: "hub:messages:remove"; | ||
| readonly messagesClear: "hub:messages:clear"; | ||
| readonly terminalsWrite: "hub:terminals:write"; | ||
| readonly terminalsResize: "hub:terminals:resize"; | ||
| readonly terminalsTerminate: "hub:terminals:terminate"; | ||
| readonly terminalsRestart: "hub:terminals:restart"; | ||
| readonly terminalsRemove: "hub:terminals:remove"; | ||
| }; | ||
| /** Broadcast notifications the server pushes to clients (server → client), `devframe:` prefix. */ | ||
| readonly broadcast: { | ||
| readonly docksActivate: "devframe:docks:activate"; | ||
| readonly terminalsUpdated: "devframe:terminals:updated"; | ||
| readonly messagesUpdated: "devframe:messages:updated"; | ||
| }; | ||
| /** Shared-state slot keys a hub-aware client reads (server → client), `devframe:` prefix. */ | ||
| readonly sharedState: { | ||
| readonly docks: "devframe:docks"; | ||
| readonly docksActive: "devframe:docks:active"; | ||
| readonly commands: "devframe:commands"; | ||
| readonly userSettings: "devframe:user-settings"; | ||
| readonly dockRenderers: "devframe:dock-renderers"; | ||
| }; | ||
| /** Streaming channel ids (server → client), `devframe:` prefix. */ | ||
| readonly stream: { | ||
| readonly terminals: "devframe:terminals"; | ||
| }; | ||
| /** `postMessage` channels for host ↔ iframe protocols, `devframe:` prefix. */ | ||
| readonly postMessage: { | ||
| readonly frameNav: "devframe:frame-nav"; | ||
| }; | ||
| }; | ||
| //#endregion | ||
| //#region src/constants.d.ts | ||
| /** Default mount base for a hub instance — one namespace, one catch-all. */ | ||
| declare const DEVFRAMES_HUB_BASE = "/__devframes/"; | ||
| /** | ||
| * Normalize a hub mount base to an absolute path with leading and trailing | ||
| * slashes (e.g. `devframes` → `/devframes/`), collapsing any doubled | ||
| * slashes the input introduced. The one implementation every hub-aware | ||
| * host (`@devframes/hub` itself, and the Vite/Nuxt/Next adapters) resolves | ||
| * `options.base` through. | ||
| */ | ||
| declare function normalizeHubBase(base: string): string; | ||
| /** | ||
| * The default ordering weight for each known dock category — lower sorts | ||
| * earlier. Downstream viewers (e.g. `@vitejs/devtools-kit`) import this as the | ||
| * single source of truth so the hub and its viewers agree on category order. | ||
| * `framework` sorts first; `~builtin` (the viewer's own built-in views) last. | ||
| * | ||
| * The buckets read from "closest to your app" → "platform / analysis" → | ||
| * "peripheral". Gaps between the weights are intentional: a kit can interleave | ||
| * its own categories (or override these) without editing this table. | ||
| */ | ||
| declare const DEFAULT_CATEGORIES_ORDER: Record<string, number>; | ||
| /** | ||
| * Shared-state slot carrying the hub's renderer manifest — one | ||
| * {@link import('./client/renderers').DockRendererManifest} entry per dock | ||
| * `type`, published by `initHub({ renderers })` and consumed by every | ||
| * hub-aware client (the headless client host and viewers alike). | ||
| */ | ||
| declare const DOCK_RENDERERS_STATE_KEY: string; | ||
| declare const DEFAULT_STATE_USER_SETTINGS: () => DevframeDocksUserSettings; | ||
| //#endregion | ||
| export { normalizeHubBase as a, DOCK_RENDERERS_STATE_KEY as i, DEFAULT_STATE_USER_SETTINGS as n, HUB_EVENTS as o, DEVFRAMES_HUB_BASE as r, DEFAULT_CATEGORIES_ORDER as t }; |
| import { cleanDoubleSlashes, withLeadingSlash, withTrailingSlash } from "ufo"; | ||
| //#region src/events.ts | ||
| /** | ||
| * Centralized registry of every event, broadcast, RPC method, shared-state | ||
| * key, and channel name the hub uses — the single source of truth that keeps | ||
| * these names out of scattered string literals. | ||
| * | ||
| * **Keep this in sync with [`docs/content/8.references/3.events.md`](../../../docs/content/8.references/3.events.md)** | ||
| * (the Hub Events Reference): every name here appears in that page's tables, and | ||
| * every name there resolves to an entry here. Add, rename, or remove a name in | ||
| * both places in the same change, and reference `HUB_EVENTS.*` from call sites | ||
| * instead of re-typing a literal. | ||
| * | ||
| * The `.events` EventEmitter maps in `types/{docks,terminals,messages,commands}.ts` | ||
| * and the RPC augmentation interfaces in `node/context.ts` declare these same | ||
| * names as type-level keys (a literal is unavoidable in a type position); those | ||
| * declarations mirror this map and move with it. | ||
| */ | ||
| const HUB_EVENTS = { | ||
| /** | ||
| * Internal node `EventEmitter` events on `ctx.<subsystem>.events`. Emitted | ||
| * and consumed inside the node process (chiefly by `createHubContext`, which | ||
| * fans them out onto the wire); they never cross to the browser. | ||
| */ | ||
| bus: { | ||
| docksEntryUpdated: "docks:entry:updated", | ||
| docksActivate: "docks:activate", | ||
| terminalsSessionUpdated: "terminals:session:updated", | ||
| messagesAdded: "messages:added", | ||
| messagesUpdated: "messages:updated", | ||
| messagesRemoved: "messages:removed", | ||
| messagesCleared: "messages:cleared", | ||
| commandsRegistered: "commands:registered", | ||
| commandsUnregistered: "commands:unregistered" | ||
| }, | ||
| /** Server RPC methods a connected client calls (client → server), `hub:` prefix. */ | ||
| rpc: { | ||
| docksActivate: "hub:docks:activate", | ||
| commandsExecute: "hub:commands:execute", | ||
| messagesAdd: "hub:messages:add", | ||
| messagesUpdate: "hub:messages:update", | ||
| messagesRemove: "hub:messages:remove", | ||
| messagesClear: "hub:messages:clear", | ||
| terminalsWrite: "hub:terminals:write", | ||
| terminalsResize: "hub:terminals:resize", | ||
| terminalsTerminate: "hub:terminals:terminate", | ||
| terminalsRestart: "hub:terminals:restart", | ||
| terminalsRemove: "hub:terminals:remove" | ||
| }, | ||
| /** Broadcast notifications the server pushes to clients (server → client), `devframe:` prefix. */ | ||
| broadcast: { | ||
| docksActivate: "devframe:docks:activate", | ||
| terminalsUpdated: "devframe:terminals:updated", | ||
| messagesUpdated: "devframe:messages:updated" | ||
| }, | ||
| /** Shared-state slot keys a hub-aware client reads (server → client), `devframe:` prefix. */ | ||
| sharedState: { | ||
| docks: "devframe:docks", | ||
| docksActive: "devframe:docks:active", | ||
| commands: "devframe:commands", | ||
| userSettings: "devframe:user-settings", | ||
| dockRenderers: "devframe:dock-renderers" | ||
| }, | ||
| /** Streaming channel ids (server → client), `devframe:` prefix. */ | ||
| stream: { terminals: "devframe:terminals" }, | ||
| /** `postMessage` channels for host ↔ iframe protocols, `devframe:` prefix. */ | ||
| postMessage: { frameNav: "devframe:frame-nav" } | ||
| }; | ||
| //#endregion | ||
| //#region src/constants.ts | ||
| /** Default mount base for a hub instance — one namespace, one catch-all. */ | ||
| const DEVFRAMES_HUB_BASE = "/__devframes/"; | ||
| /** | ||
| * Normalize a hub mount base to an absolute path with leading and trailing | ||
| * slashes (e.g. `devframes` → `/devframes/`), collapsing any doubled | ||
| * slashes the input introduced. The one implementation every hub-aware | ||
| * host (`@devframes/hub` itself, and the Vite/Nuxt/Next adapters) resolves | ||
| * `options.base` through. | ||
| */ | ||
| function normalizeHubBase(base) { | ||
| return cleanDoubleSlashes(withTrailingSlash(withLeadingSlash(base))); | ||
| } | ||
| /** | ||
| * The default ordering weight for each known dock category — lower sorts | ||
| * earlier. Downstream viewers (e.g. `@vitejs/devtools-kit`) import this as the | ||
| * single source of truth so the hub and its viewers agree on category order. | ||
| * `framework` sorts first; `~builtin` (the viewer's own built-in views) last. | ||
| * | ||
| * The buckets read from "closest to your app" → "platform / analysis" → | ||
| * "peripheral". Gaps between the weights are intentional: a kit can interleave | ||
| * its own categories (or override these) without editing this table. | ||
| */ | ||
| const DEFAULT_CATEGORIES_ORDER = { | ||
| "framework": -100, | ||
| "default": 0, | ||
| "app": 100, | ||
| "ui": 150, | ||
| "data": 250, | ||
| "web": 300, | ||
| "performance": 350, | ||
| "advanced": 400, | ||
| "docs": 500, | ||
| "~builtin": 1e3 | ||
| }; | ||
| /** | ||
| * Shared-state slot carrying the hub's renderer manifest — one | ||
| * {@link import('./client/renderers').DockRendererManifest} entry per dock | ||
| * `type`, published by `initHub({ renderers })` and consumed by every | ||
| * hub-aware client (the headless client host and viewers alike). | ||
| */ | ||
| const DOCK_RENDERERS_STATE_KEY = HUB_EVENTS.sharedState.dockRenderers; | ||
| const DEFAULT_STATE_USER_SETTINGS = () => ({ | ||
| docksHidden: [], | ||
| docksCategoriesHidden: [], | ||
| docksPinned: [], | ||
| docksCustomOrder: {}, | ||
| commandShortcuts: {} | ||
| }); | ||
| //#endregion | ||
| export { normalizeHubBase as a, DOCK_RENDERERS_STATE_KEY as i, DEFAULT_STATE_USER_SETTINGS as n, HUB_EVENTS as o, DEVFRAMES_HUB_BASE as r, DEFAULT_CATEGORIES_ORDER as t }; |
| import { r as defineHubRpcFunction } from "./define-Ceekw2EO.mjs"; | ||
| import { n as DEFAULT_STATE_USER_SETTINGS, o as HUB_EVENTS } from "./constants-DrF61GFx.mjs"; | ||
| import { i as isBareModuleSpecifier, t as buildRemoteConnectionUrl } from "./remote-url-Bgc7gtsP.mjs"; | ||
| import { createEventEmitter } from "devframe/utils/events"; | ||
| import { createHostContext, createStorage } from "devframe/node"; | ||
| import { getInternalContext, resolveBasePath } from "devframe/node/hub-internals"; | ||
| import { debounce } from "perfect-debounce"; | ||
| import { coerceAgentPositionalArgs } from "devframe/internal"; | ||
| import { defineDiagnostics } from "devframe/utils/nostics"; | ||
| import { join, resolve } from "pathe"; | ||
| import { nanoid } from "devframe/utils/nanoid"; | ||
| import process from "node:process"; | ||
| import { resolveClientAssets } from "devframe"; | ||
| //#region src/node/diagnostics.ts | ||
| const diagnostics = defineDiagnostics({ | ||
| docsBase: "https://devfra.me/errors", | ||
| codes: { | ||
| DF8000: { | ||
| why: (p) => `Devframe id "${p.id}" collides with a reserved hub path — it cannot be mounted directly under the hub base.`, | ||
| fix: "The filenames directly under the hub base (`__connection.json`, `__ws`, `__index.json`, `__client-imports.js`, `__mcp`, `embedded.js`) are reserved for the hub protocol. Rename the devframe id, or override its mount with a non-colliding `basePath`." | ||
| }, | ||
| DF8002: { | ||
| why: "initHub received both `devframes` and `context` — the two assembly modes are mutually exclusive.", | ||
| fix: "Pass `devframes` to let the instance create the hub context and mount each frame itself, or pass a pre-built `context` (your host already mounted the frames) — never both." | ||
| }, | ||
| DF8003: { | ||
| why: "connectionMeta() was called before initHub finished initializing.", | ||
| fix: "Await `instance.ready` (or any request through `instance.handler`) before reading `connectionMeta()` — the WebSocket binding it describes is only known once initialization completes." | ||
| }, | ||
| DF8004: { | ||
| why: (p) => `Devframe id "${p.id}" is not a mountable URL segment — the hub mounts each frame at \`<base><id>/\`.`, | ||
| fix: "Ids become route segments, so they may only contain letters, digits, `_`, `-`, and `.` — `:` and `*` are route-pattern markers to the underlying router, and `/` would escape the segment. Set a route-safe `id` on the definition (e.g. `my_plugin` instead of `my:plugin`)." | ||
| }, | ||
| DF8100: { | ||
| why: (p) => `Dock with id "${p.id}" is already registered`, | ||
| fix: "Use the `force` parameter to overwrite an existing registration." | ||
| }, | ||
| DF8101: { | ||
| why: (p) => `Cannot change the id of dock "${p.id}" to "${p.attempted}". Dock ids are immutable once registered`, | ||
| fix: (p) => `Remove \`id\` from the patch to keep updating "${p.id}", or call register() with the full entry to add "${p.attempted}" as a new dock.` | ||
| }, | ||
| DF8102: { | ||
| why: (p) => `Dock with id "${p.id}" is not registered and cannot be updated`, | ||
| fix: (p) => `Call register() to add "${p.id}" as a new dock, or check the id for typos.` | ||
| }, | ||
| DF8103: { | ||
| why: (p) => `Dock entry "${p.id}" cannot set groupId to its own id`, | ||
| fix: "Point groupId at a different group entry, or omit it." | ||
| }, | ||
| DF8104: { | ||
| why: (p) => `Dock group "${p.id}" cannot itself belong to a group (nested groups are unsupported)`, | ||
| fix: "Remove groupId from the group entry; nest members one level only." | ||
| }, | ||
| DF8105: { | ||
| why: (p) => `Devframe "${p.name}" (id "${p.id}") is already mounted on this hub`, | ||
| fix: "Each devframe is deduplicated by id. Set `duplicationStrategy: \"duplicate\"` on the definition to let instances coexist, `\"silent\"` to drop duplicates quietly, or `\"throw\"` to surface them as errors." | ||
| }, | ||
| DF8106: { | ||
| why: (p) => `The host cannot serve the RPC connection meta for devframe "${p.name}" (id "${p.id}") at "${p.base}" — its \`DevframeHost\` does not implement \`mountConnectionMeta\`.`, | ||
| fix: "Implement `mountConnectionMeta(base)` on your DevframeHost so it serves `__connection.json` at each mounted base. Without it, the devframe SPA connects only when it shares an origin with the hub UI (same-origin window inheritance); cross-origin, sandboxed, or directly-opened iframes stay disconnected. Static-snapshot hosts that bake the meta into the served files can implement it as a no-op to acknowledge this intentionally." | ||
| }, | ||
| DF8107: { | ||
| why: (p) => `Dock activation requested for unknown dock id "${p.id}"`, | ||
| fix: "Pass a `dockId` that matches a registered dock entry. The activation is still broadcast, but no hub UI provider will switch to it. Ids are case-sensitive — check for typos, and ensure the target dock is registered before activating it." | ||
| }, | ||
| DF8108: { | ||
| why: (p) => `A renderer module is already registered for dock type "${p.type}"`, | ||
| fix: "Each dock type resolves to exactly one renderer module in the hub's renderer manifest. Remove the duplicate `renderers` registration, or give the second renderer its own dock type." | ||
| }, | ||
| DF8109: { | ||
| why: (p) => `The renderer module registered for dock type "${p.type}" does not exist at "${p.file}"`, | ||
| fix: "Point the registration's `file` at the prebuilt browser ES module (build the renderer package first, or check the path). Registration helpers like `jsonRenderUiRenderer()` resolve the path for you." | ||
| }, | ||
| DF8110: { | ||
| why: (p) => `Dock type "${p.type}" is not a servable renderer-module name — the hub serves each module at \`<base>__renderers/<type>.mjs\``, | ||
| fix: "Renderer types become URL segments, so they may only contain letters, digits, `_`, `-`, and `.`. Use a route-safe dock type (e.g. `json-render`)." | ||
| }, | ||
| DF8111: { | ||
| why: (p) => `Dock "${p.id}" declares the bare-specifier client script "${p.specifier}", but this host advertises no client-module resolution — the browser cannot resolve a bare npm specifier natively, so the script will fail to load.`, | ||
| fix: "Run under a host framework that declares `initHub({ clientModuleResolution })` (e.g. Vite's `'/@id/{specifier}'`), ship the script as a self-contained bundle served by URL, or resolve it in the hub UI provider via `createDevframeClientRuntime({ resolveClientModule })` (then disregard this warning)." | ||
| }, | ||
| DF8200: { why: (p) => `Terminal session with id "${p.id}" already registered` }, | ||
| DF8201: { why: (p) => `Terminal session with id "${p.id}" not registered` }, | ||
| DF8202: { | ||
| why: (p) => `Terminal session "${p.id}" does not accept input`, | ||
| fix: "Spawn it via ctx.terminals.startPtySession() to get an interactive, writable session." | ||
| }, | ||
| DF8203: { why: (p) => `Failed to spawn PTY session for "${p.command}": ${p.reason}` }, | ||
| DF8204: { | ||
| why: (p) => `Terminal session "${p.id}" cannot be controlled (no lifecycle handle)`, | ||
| fix: "Spawn it via ctx.terminals.startChildProcess() or startPtySession() — sessions added with a bare register() expose no terminate/restart handle." | ||
| }, | ||
| DF8205: { | ||
| why: (p) => `Terminal session "${p.id}" is not restartable`, | ||
| fix: "It was registered with `restartable: false`; restart it through its owner's controls, or spawn it with `restartable: true` (the default) to allow in-place restarts." | ||
| }, | ||
| DF8206: { | ||
| why: (p) => `Terminal session "${p.id}" cannot be restarted — its output stream is already closed`, | ||
| fix: "The session already exited (or was terminated) and its stream is spent. Drop it with `ctx.terminals.remove(session)`, then spawn a replacement via `ctx.terminals.startChildProcess()` or `ctx.terminals.startPtySession()` with a fresh id." | ||
| }, | ||
| DF8400: { why: (p) => `Command "${p.id}" is already registered` }, | ||
| DF8401: { why: "Cannot change the id of a command. Use register() to add new commands" }, | ||
| DF8402: { why: (p) => `Command "${p.id}" is not registered` }, | ||
| DF8403: { | ||
| why: (p) => `Command id "${p.id}" is already used by another command or child command`, | ||
| fix: "Use globally unique command ids for top-level commands and all child commands." | ||
| }, | ||
| DF8404: { | ||
| why: (p) => `Command "${p.id}" declares agent exposure but has no handler`, | ||
| fix: "Agent-exposed commands must be executable server-side. Add a `handler` to the command, or move the `agent` field to an executable child command." | ||
| } | ||
| } | ||
| }); | ||
| //#endregion | ||
| //#region src/node/host-commands.ts | ||
| function findChildCommand(command, id) { | ||
| for (const child of command.children ?? []) { | ||
| if (child.id === id) return child; | ||
| const nested = findChildCommand(child, id); | ||
| if (nested) return nested; | ||
| } | ||
| } | ||
| function collectCommandIds(command, ids = []) { | ||
| ids.push(command.id); | ||
| for (const child of command.children ?? []) collectCommandIds(child, ids); | ||
| return ids; | ||
| } | ||
| function validateCommandIds(commands, command, ignoreTopLevelId) { | ||
| const ids = collectCommandIds(command); | ||
| const seen = /* @__PURE__ */ new Set(); | ||
| for (const id of ids) { | ||
| if (seen.has(id)) throw diagnostics.DF8403({ id }); | ||
| seen.add(id); | ||
| } | ||
| for (const [registeredId, registered] of commands) { | ||
| if (registeredId === ignoreTopLevelId) continue; | ||
| const registeredIds = new Set(collectCommandIds(registered)); | ||
| for (const id of ids) if (registeredIds.has(id)) throw diagnostics.DF8403({ id }); | ||
| } | ||
| } | ||
| var DevframeCommandsHost = class { | ||
| context; | ||
| commands = /* @__PURE__ */ new Map(); | ||
| events = createEventEmitter(); | ||
| /** | ||
| * Lazy agent projection: `ctx.agent` queries this provider at list/invoke | ||
| * time, deriving tools from {@link commands} on demand — the commands map | ||
| * stays the single source of truth, nothing is mirrored or kept in sync. | ||
| */ | ||
| agentProvider; | ||
| constructor(context) { | ||
| this.context = context; | ||
| this.agentProvider = context.agent?.registerToolProvider(() => this.collectAgentTools()); | ||
| } | ||
| register(command) { | ||
| if (this.commands.has(command.id)) throw diagnostics.DF8400({ id: command.id }); | ||
| validateCommandIds(this.commands, command); | ||
| this.validateAgentExposure(command); | ||
| this.commands.set(command.id, command); | ||
| this.events.emit(HUB_EVENTS.bus.commandsRegistered, this.toSerializable(command)); | ||
| this.agentProvider?.notifyChanged(); | ||
| return { | ||
| id: command.id, | ||
| update: (patch) => { | ||
| if ("id" in patch) throw diagnostics.DF8401(); | ||
| const existing = this.commands.get(command.id); | ||
| if (!existing) throw diagnostics.DF8402({ id: command.id }); | ||
| const next = { | ||
| ...existing, | ||
| ...patch, | ||
| id: existing.id | ||
| }; | ||
| validateCommandIds(this.commands, next, existing.id); | ||
| this.validateAgentExposure(next); | ||
| Object.assign(existing, patch); | ||
| this.events.emit(HUB_EVENTS.bus.commandsRegistered, this.toSerializable(existing)); | ||
| this.agentProvider?.notifyChanged(); | ||
| }, | ||
| unregister: () => this.unregister(command.id) | ||
| }; | ||
| } | ||
| unregister(id) { | ||
| const deleted = this.commands.delete(id); | ||
| if (deleted) { | ||
| this.events.emit(HUB_EVENTS.bus.commandsUnregistered, id); | ||
| this.agentProvider?.notifyChanged(); | ||
| } | ||
| return deleted; | ||
| } | ||
| async execute(id, ...args) { | ||
| const found = this.findCommand(id); | ||
| if (!found) throw diagnostics.DF8402({ id }); | ||
| if (!found.handler) throw new Error(`Command "${id}" has no handler (group-only command)`); | ||
| return found.handler(...args); | ||
| } | ||
| list() { | ||
| return Array.from(this.commands.values()).map((cmd) => this.toSerializable(cmd)); | ||
| } | ||
| findCommand(id) { | ||
| const topLevel = this.commands.get(id); | ||
| if (topLevel) return topLevel; | ||
| for (const cmd of this.commands.values()) { | ||
| const child = findChildCommand(cmd, id); | ||
| if (child) return child; | ||
| } | ||
| } | ||
| toSerializable(cmd) { | ||
| const { handler: _, agent: __, children, ...rest } = cmd; | ||
| return { | ||
| ...rest, | ||
| source: "server", | ||
| ...children ? { children: children.map((c) => this.toSerializable(c)) } : {} | ||
| }; | ||
| } | ||
| /** Reject `agent` on handler-less commands anywhere in the tree, up front. */ | ||
| validateAgentExposure(command) { | ||
| if (command.agent && !command.handler) throw diagnostics.DF8404({ id: command.id }); | ||
| for (const child of command.children ?? []) this.validateAgentExposure(child); | ||
| } | ||
| /** | ||
| * Derive the agent-tool projection of the current command trees: every | ||
| * agent-flagged, handler-bearing command (children included) becomes a | ||
| * callable tool. Queried lazily by the provider registered in the | ||
| * constructor. `when` clauses evaluate client-side only and are not | ||
| * enforced here — opting in a `when`-gated command is a deliberate author | ||
| * decision (documented on `DevframeCommandAgentOptions`). | ||
| */ | ||
| collectAgentTools() { | ||
| const tools = []; | ||
| const walk = (command) => { | ||
| const agent = command.agent; | ||
| if (agent && command.handler) tools.push({ | ||
| id: command.id, | ||
| title: agent.title ?? command.title, | ||
| description: agent.description, | ||
| safety: agent.safety ?? "action", | ||
| tags: agent.tags, | ||
| args: agent.args, | ||
| handler: async (args) => this.execute(command.id, ...coerceAgentPositionalArgs(args, agent.args, "drop")) | ||
| }); | ||
| for (const child of command.children ?? []) walk(child); | ||
| }; | ||
| for (const command of this.commands.values()) walk(command); | ||
| return tools; | ||
| } | ||
| }; | ||
| //#endregion | ||
| //#region src/node/host-docks.ts | ||
| function normaliseRemoteOptions(remote) { | ||
| const opts = remote === true ? {} : remote; | ||
| return { | ||
| transport: opts.transport ?? "fragment", | ||
| originLock: opts.originLock ?? true | ||
| }; | ||
| } | ||
| var DevframeDocksHost = class { | ||
| context; | ||
| views = /* @__PURE__ */ new Map(); | ||
| events = createEventEmitter(); | ||
| userSettings = void 0; | ||
| /** Dock-id → allocated remote token + resolved options. */ | ||
| remoteDocks = /* @__PURE__ */ new Map(); | ||
| constructor(context) { | ||
| this.context = context; | ||
| } | ||
| async init() { | ||
| this.userSettings = await this.context.rpc.sharedState.get(HUB_EVENTS.sharedState.userSettings, { sharedState: createStorage({ | ||
| filepath: join(this.context.host.getStorageDir("project"), "settings.json"), | ||
| initialValue: DEFAULT_STATE_USER_SETTINGS() | ||
| }) }); | ||
| } | ||
| values() { | ||
| return Array.from(this.views.values(), (view) => this.projectView(view)); | ||
| } | ||
| projectView(view) { | ||
| if (view.type !== "iframe" || !view.remote) return view; | ||
| const record = this.remoteDocks.get(view.id); | ||
| const endpoint = getInternalContext(this.context).wsEndpoint; | ||
| if (!record || !endpoint) return view; | ||
| const payload = { | ||
| v: 1, | ||
| backend: "websocket", | ||
| websocket: endpoint.url, | ||
| authToken: record.token, | ||
| origin: this.resolveDevServerOrigin() | ||
| }; | ||
| return { | ||
| ...view, | ||
| url: buildRemoteConnectionUrl(view.url, payload, record.options.transport) | ||
| }; | ||
| } | ||
| resolveDevServerOrigin() { | ||
| return this.context.host.resolveOrigin(); | ||
| } | ||
| register(view, force) { | ||
| if (this.views.has(view.id) && !force) throw diagnostics.DF8100({ id: view.id }); | ||
| this.validateGroupMembership(view); | ||
| this.warnUnresolvableClientScript(view); | ||
| this.prepareRemoteRegistration(view); | ||
| this.views.set(view.id, view); | ||
| this.events.emit(HUB_EVENTS.bus.docksEntryUpdated, view); | ||
| return { update: (patch) => { | ||
| if (patch.id && patch.id !== view.id) throw diagnostics.DF8101({ | ||
| id: view.id, | ||
| attempted: patch.id | ||
| }); | ||
| this.update({ | ||
| ...this.views.get(view.id), | ||
| ...patch | ||
| }); | ||
| } }; | ||
| } | ||
| update(view) { | ||
| if (!this.views.has(view.id)) throw diagnostics.DF8102({ id: view.id }); | ||
| this.validateGroupMembership(view); | ||
| this.prepareRemoteRegistration(view); | ||
| this.views.set(view.id, view); | ||
| this.events.emit(HUB_EVENTS.bus.docksEntryUpdated, view); | ||
| } | ||
| activate(dockId, params) { | ||
| if (!this.views.has(dockId)) diagnostics.DF8107({ id: dockId }); | ||
| this.events.emit(HUB_EVENTS.bus.docksActivate, { | ||
| dockId, | ||
| params | ||
| }); | ||
| } | ||
| /** | ||
| * Warn (don't throw — a client-runtime `resolveClientModule` override may still cover | ||
| * it) when a dock declares a **bare-specifier** client script on a host | ||
| * that advertises no `staticConfig.dock.clientModuleResolution`: the | ||
| * browser cannot resolve a bare npm specifier natively, so the script is | ||
| * doomed to fail there. | ||
| */ | ||
| warnUnresolvableClientScript(view) { | ||
| if (this.context.staticConfig?.dock?.clientModuleResolution) return; | ||
| const script = view.clientScript ?? view.action ?? view.renderer; | ||
| if (script?.importFrom && isBareModuleSpecifier(script.importFrom)) diagnostics.DF8111({ | ||
| id: view.id, | ||
| specifier: script.importFrom | ||
| }); | ||
| } | ||
| validateGroupMembership(view) { | ||
| if (view.groupId === void 0) return; | ||
| if (view.groupId === view.id) throw diagnostics.DF8103({ id: view.id }); | ||
| if (view.type === "group") throw diagnostics.DF8104({ id: view.id }); | ||
| } | ||
| prepareRemoteRegistration(view) { | ||
| const internal = getInternalContext(this.context); | ||
| internal.revokeRemoteTokensForDock(view.id); | ||
| this.remoteDocks.delete(view.id); | ||
| if (view.type !== "iframe" || !view.remote) return; | ||
| const options = normaliseRemoteOptions(view.remote); | ||
| let dockOrigin; | ||
| try { | ||
| dockOrigin = new URL(view.url).origin; | ||
| } catch { | ||
| dockOrigin = this.resolveDevServerOrigin(); | ||
| } | ||
| const token = internal.allocateRemoteToken(view.id, dockOrigin, options.originLock); | ||
| this.remoteDocks.set(view.id, { | ||
| token, | ||
| options | ||
| }); | ||
| } | ||
| }; | ||
| //#endregion | ||
| //#region src/node/host-messages.ts | ||
| const MAX_ENTRIES = 1e3; | ||
| const MAX_REMOVALS = 1e3; | ||
| var DevframeMessagesHost = class { | ||
| context; | ||
| entries = /* @__PURE__ */ new Map(); | ||
| events = createEventEmitter(); | ||
| /** Tracks when each entry was last added or updated (monotonic) */ | ||
| lastModified = /* @__PURE__ */ new Map(); | ||
| /** Tracks recently removed entry IDs with their removal time */ | ||
| removals = []; | ||
| _autoDeleteTimers = /* @__PURE__ */ new Map(); | ||
| _clock = 0; | ||
| /** | ||
| * The tick of the newest removal record dropped from the capped | ||
| * `removals` log — cursors older than this can't get a reliable delta | ||
| * and fall back to a full snapshot in {@link listSince}. | ||
| */ | ||
| _removalsTrimmedAt = 0; | ||
| _tick() { | ||
| return ++this._clock; | ||
| } | ||
| _recordRemoval(id, time) { | ||
| this.removals.push({ | ||
| id, | ||
| time | ||
| }); | ||
| if (this.removals.length > MAX_REMOVALS) { | ||
| const dropped = this.removals.splice(0, this.removals.length - MAX_REMOVALS); | ||
| this._removalsTrimmedAt = dropped[dropped.length - 1].time; | ||
| } | ||
| } | ||
| constructor(context) { | ||
| this.context = context; | ||
| } | ||
| async add(input) { | ||
| if (input.id && this.entries.has(input.id)) { | ||
| await this.update(input.id, input); | ||
| return this._createHandle(input.id); | ||
| } | ||
| const entry = { | ||
| ...input, | ||
| id: input.id ?? nanoid(), | ||
| timestamp: input.timestamp ?? Date.now(), | ||
| from: input.from ?? "server" | ||
| }; | ||
| if (this.entries.size >= MAX_ENTRIES) { | ||
| const oldest = this.entries.keys().next().value; | ||
| await this.remove(oldest); | ||
| } | ||
| this.entries.set(entry.id, entry); | ||
| this.lastModified.set(entry.id, this._tick()); | ||
| this.events.emit(HUB_EVENTS.bus.messagesAdded, entry); | ||
| if (entry.autoDelete) this._autoDeleteTimers.set(entry.id, setTimeout(() => { | ||
| this.remove(entry.id); | ||
| }, entry.autoDelete)); | ||
| return this._createHandle(entry.id); | ||
| } | ||
| async update(id, patch) { | ||
| const existing = this.entries.get(id); | ||
| if (!existing) return void 0; | ||
| const updated = { | ||
| ...existing, | ||
| ...patch, | ||
| id: existing.id, | ||
| from: existing.from, | ||
| timestamp: existing.timestamp | ||
| }; | ||
| this.entries.set(id, updated); | ||
| this.lastModified.set(id, this._tick()); | ||
| this.events.emit(HUB_EVENTS.bus.messagesUpdated, updated); | ||
| if (patch.autoDelete !== void 0) { | ||
| const timer = this._autoDeleteTimers.get(id); | ||
| if (timer) { | ||
| clearTimeout(timer); | ||
| this._autoDeleteTimers.delete(id); | ||
| } | ||
| if (patch.autoDelete) this._autoDeleteTimers.set(id, setTimeout(() => { | ||
| this.remove(id); | ||
| }, patch.autoDelete)); | ||
| } | ||
| return updated; | ||
| } | ||
| async remove(id) { | ||
| const timer = this._autoDeleteTimers.get(id); | ||
| if (timer) { | ||
| clearTimeout(timer); | ||
| this._autoDeleteTimers.delete(id); | ||
| } | ||
| this.entries.delete(id); | ||
| this.lastModified.delete(id); | ||
| this._recordRemoval(id, this._tick()); | ||
| this.events.emit(HUB_EVENTS.bus.messagesRemoved, id); | ||
| } | ||
| info(message, extra) { | ||
| return this.add({ | ||
| ...extra, | ||
| message, | ||
| level: "info" | ||
| }); | ||
| } | ||
| warn(message, extra) { | ||
| return this.add({ | ||
| ...extra, | ||
| message, | ||
| level: "warn" | ||
| }); | ||
| } | ||
| error(message, extra) { | ||
| return this.add({ | ||
| ...extra, | ||
| message, | ||
| level: "error" | ||
| }); | ||
| } | ||
| success(message, extra) { | ||
| return this.add({ | ||
| ...extra, | ||
| message, | ||
| level: "success" | ||
| }); | ||
| } | ||
| debug(message, extra) { | ||
| return this.add({ | ||
| ...extra, | ||
| message, | ||
| level: "debug" | ||
| }); | ||
| } | ||
| async clear() { | ||
| for (const timer of this._autoDeleteTimers.values()) clearTimeout(timer); | ||
| this._autoDeleteTimers.clear(); | ||
| const tick = this._tick(); | ||
| for (const id of this.entries.keys()) this._recordRemoval(id, tick); | ||
| this.entries.clear(); | ||
| this.lastModified.clear(); | ||
| this.events.emit(HUB_EVENTS.bus.messagesCleared); | ||
| } | ||
| listSince(since) { | ||
| const version = this._clock; | ||
| if (since == null || since < this._removalsTrimmedAt || since > version) return { | ||
| entries: Array.from(this.entries.values()), | ||
| removedIds: [], | ||
| version, | ||
| full: true | ||
| }; | ||
| const entries = []; | ||
| for (const [id, entry] of this.entries) { | ||
| const mod = this.lastModified.get(id); | ||
| if (mod != null && mod > since) entries.push(entry); | ||
| } | ||
| const removedIds = []; | ||
| for (const removal of this.removals) if (removal.time > since) removedIds.push(removal.id); | ||
| return { | ||
| entries, | ||
| removedIds, | ||
| version, | ||
| full: false | ||
| }; | ||
| } | ||
| _createHandle(id) { | ||
| const host = this; | ||
| return { | ||
| get entry() { | ||
| return host.entries.get(id); | ||
| }, | ||
| get id() { | ||
| return id; | ||
| }, | ||
| update: (patch) => host.update(id, patch), | ||
| dismiss: () => host.remove(id) | ||
| }; | ||
| } | ||
| }; | ||
| //#endregion | ||
| //#region src/node/host-terminals.ts | ||
| /** | ||
| * Channel name used for terminal stream output. Stable, well-known so | ||
| * hub-aware clients can subscribe by name. | ||
| */ | ||
| const TERMINAL_STREAM_CHANNEL = HUB_EVENTS.stream.terminals; | ||
| const TERMINAL_REPLAY_WINDOW = 1e3; | ||
| /** Max chunks retained in the per-session scrollback buffer (bounded like the replay window). */ | ||
| const TERMINAL_BUFFER_LIMIT = 1e3; | ||
| /** TERM handed to spawned PTYs; also used to reject fallback process labels. */ | ||
| const PTY_TERM_NAME = "xterm-256color"; | ||
| var DevframeTerminalsHost = class { | ||
| context; | ||
| sessions = /* @__PURE__ */ new Map(); | ||
| events = createEventEmitter(); | ||
| _boundStreams = /* @__PURE__ */ new Map(); | ||
| _channel; | ||
| constructor(context) { | ||
| this.context = context; | ||
| } | ||
| /** | ||
| * Lazily acquire the streaming channel — `context.rpc` isn't assigned | ||
| * until after every host is constructed, so we can't grab it in the | ||
| * constructor. | ||
| */ | ||
| getStreamingChannel() { | ||
| if (this._channel) return this._channel; | ||
| if (!this.context.rpc?.streaming) return void 0; | ||
| this._channel = this.context.rpc.streaming.create(TERMINAL_STREAM_CHANNEL, { replayWindow: TERMINAL_REPLAY_WINDOW }); | ||
| return this._channel; | ||
| } | ||
| register(session) { | ||
| if (this.sessions.has(session.id)) throw diagnostics.DF8200({ id: session.id }); | ||
| this.sessions.set(session.id, session); | ||
| this.bindStream(session); | ||
| this.events.emit(HUB_EVENTS.bus.terminalsSessionUpdated, session); | ||
| return session; | ||
| } | ||
| update(patch) { | ||
| if (!this.sessions.has(patch.id)) throw diagnostics.DF8201({ id: patch.id }); | ||
| const session = this.sessions.get(patch.id); | ||
| Object.assign(session, patch); | ||
| this.sessions.set(patch.id, session); | ||
| this.bindStream(session); | ||
| this.events.emit(HUB_EVENTS.bus.terminalsSessionUpdated, session); | ||
| } | ||
| remove(session) { | ||
| this._boundStreams.get(session.id)?.dispose(); | ||
| this.sessions.delete(session.id); | ||
| this.events.emit(HUB_EVENTS.bus.terminalsSessionUpdated, session); | ||
| this._boundStreams.delete(session.id); | ||
| } | ||
| bindStream(session) { | ||
| if (this._boundStreams.has(session.id) && this._boundStreams.get(session.id)?.stream === session.stream) return; | ||
| this._boundStreams.get(session.id)?.dispose(); | ||
| this._boundStreams.delete(session.id); | ||
| if (!session.stream) return; | ||
| session.buffer ||= []; | ||
| const sessionBuffer = session.buffer; | ||
| const sink = this.getStreamingChannel()?.start({ id: session.id }); | ||
| const reader = session.stream.getReader(); | ||
| let disposed = false; | ||
| (async () => { | ||
| try { | ||
| while (true) { | ||
| if (disposed) break; | ||
| const result = await reader.read(); | ||
| if (disposed) break; | ||
| if (result.done) break; | ||
| sessionBuffer.push(result.value); | ||
| if (sessionBuffer.length > TERMINAL_BUFFER_LIMIT) sessionBuffer.splice(0, sessionBuffer.length - TERMINAL_BUFFER_LIMIT); | ||
| sink?.write(result.value); | ||
| } | ||
| if (!disposed && sink && !sink.closed) sink.close(); | ||
| } catch (error) { | ||
| if (!disposed && sink && !sink.closed) sink.error(error); | ||
| } finally { | ||
| try { | ||
| reader.releaseLock(); | ||
| } catch {} | ||
| } | ||
| })(); | ||
| this._boundStreams.set(session.id, { | ||
| dispose: () => { | ||
| disposed = true; | ||
| reader.cancel("terminal stream disposed").catch(() => {}); | ||
| if (sink && !sink.closed) sink.close(); | ||
| }, | ||
| stream: session.stream | ||
| }); | ||
| } | ||
| async startChildProcess(executeOptions, terminal) { | ||
| if (this.sessions.has(terminal.id)) throw diagnostics.DF8200({ id: terminal.id }); | ||
| const { exec } = await import("tinyexec"); | ||
| let controller; | ||
| let cp; | ||
| let currentResult; | ||
| let runId = 0; | ||
| let streamClosed = false; | ||
| let session; | ||
| const markStatus = (next) => { | ||
| if (session.status === next) return; | ||
| session.status = next; | ||
| this.events.emit(HUB_EVENTS.bus.terminalsSessionUpdated, session); | ||
| }; | ||
| const closeStream = () => { | ||
| if (streamClosed) return; | ||
| streamClosed = true; | ||
| try { | ||
| controller?.close(); | ||
| } catch {} | ||
| }; | ||
| const errorStream = (error) => { | ||
| if (streamClosed) return; | ||
| streamClosed = true; | ||
| try { | ||
| controller?.error(error); | ||
| } catch {} | ||
| }; | ||
| const stream = new ReadableStream({ | ||
| start(_controller) { | ||
| controller = _controller; | ||
| }, | ||
| cancel() { | ||
| cp?.kill(); | ||
| cp = void 0; | ||
| closeStream(); | ||
| } | ||
| }); | ||
| function createChildProcess() { | ||
| const currentRun = ++runId; | ||
| let runErrored = false; | ||
| const cp = exec(executeOptions.command, executeOptions.args || [], { nodeOptions: { | ||
| env: { | ||
| COLORS: "true", | ||
| FORCE_COLOR: "true", | ||
| ...executeOptions.env || {} | ||
| }, | ||
| cwd: executeOptions.cwd ?? process.cwd(), | ||
| stdio: "pipe" | ||
| } }); | ||
| const stdoutChunks = []; | ||
| const stderrChunks = []; | ||
| let settled = false; | ||
| let resolveOutput; | ||
| const outputPromise = new Promise((resolve) => { | ||
| resolveOutput = resolve; | ||
| }); | ||
| const settle = (exitCode) => { | ||
| if (settled || currentRun !== runId) return; | ||
| settled = true; | ||
| resolveOutput({ | ||
| stdout: stdoutChunks.join(""), | ||
| stderr: stderrChunks.join(""), | ||
| exitCode | ||
| }); | ||
| }; | ||
| cp.process?.stdout?.on("data", (chunk) => { | ||
| if (currentRun !== runId) return; | ||
| const text = chunk.toString(); | ||
| stdoutChunks.push(text); | ||
| if (!streamClosed) controller?.enqueue(text); | ||
| }); | ||
| cp.process?.stderr?.on("data", (chunk) => { | ||
| if (currentRun !== runId) return; | ||
| const text = chunk.toString(); | ||
| stderrChunks.push(text); | ||
| if (!streamClosed) controller?.enqueue(text); | ||
| }); | ||
| cp.process?.once("error", (error) => { | ||
| if (currentRun !== runId) return; | ||
| runErrored = true; | ||
| settle(cp.process?.exitCode ?? void 0); | ||
| errorStream(error); | ||
| markStatus("error"); | ||
| }); | ||
| cp.process?.once("close", (code) => { | ||
| settle(code ?? void 0); | ||
| if (currentRun !== runId) return; | ||
| closeStream(); | ||
| if (!runErrored) markStatus(typeof code === "number" && code !== 0 ? "error" : "stopped"); | ||
| }); | ||
| currentResult = { | ||
| get pid() { | ||
| return cp.process?.pid; | ||
| }, | ||
| get exitCode() { | ||
| return cp.process?.exitCode ?? void 0; | ||
| }, | ||
| get killed() { | ||
| return cp.process?.killed === true; | ||
| }, | ||
| kill: (signal) => cp.kill(signal), | ||
| then: (onfulfilled, onrejected) => outputPromise.then(onfulfilled, onrejected) | ||
| }; | ||
| return cp; | ||
| } | ||
| cp = createChildProcess(); | ||
| const restart = async () => { | ||
| if (streamClosed) throw diagnostics.DF8206({ id: terminal.id }); | ||
| cp?.kill(); | ||
| cp = createChildProcess(); | ||
| markStatus("running"); | ||
| }; | ||
| const terminate = async () => { | ||
| cp?.kill(); | ||
| cp = void 0; | ||
| closeStream(); | ||
| markStatus("stopped"); | ||
| }; | ||
| session = { | ||
| ...terminal, | ||
| status: "running", | ||
| stream, | ||
| type: "child-process", | ||
| executeOptions, | ||
| getChildProcess: () => cp?.process, | ||
| getResult: () => currentResult, | ||
| terminate, | ||
| restart | ||
| }; | ||
| this.register(session); | ||
| return Promise.resolve(session); | ||
| } | ||
| async startPtySession(executeOptions, terminal) { | ||
| if (this.sessions.has(terminal.id)) throw diagnostics.DF8200({ id: terminal.id }); | ||
| const { spawn } = await import("zigpty"); | ||
| const cols = executeOptions.cols ?? 80; | ||
| const rows = executeOptions.rows ?? 24; | ||
| let controller; | ||
| let pty; | ||
| let runId = 0; | ||
| let streamClosed = false; | ||
| let session; | ||
| const markStatus = (next) => { | ||
| if (session.status === next) return; | ||
| session.status = next; | ||
| this.events.emit(HUB_EVENTS.bus.terminalsSessionUpdated, session); | ||
| }; | ||
| const closeStream = () => { | ||
| if (streamClosed) return; | ||
| streamClosed = true; | ||
| try { | ||
| controller?.close(); | ||
| } catch {} | ||
| }; | ||
| const errorStream = (error) => { | ||
| if (streamClosed) return; | ||
| streamClosed = true; | ||
| try { | ||
| controller?.error(error); | ||
| } catch {} | ||
| }; | ||
| const stream = new ReadableStream({ | ||
| start(_controller) { | ||
| controller = _controller; | ||
| }, | ||
| cancel() { | ||
| pty?.kill(); | ||
| pty = void 0; | ||
| closeStream(); | ||
| } | ||
| }); | ||
| const spawnPty = () => { | ||
| const currentRun = ++runId; | ||
| const proc = spawn(executeOptions.command, executeOptions.args ?? [], { | ||
| name: PTY_TERM_NAME, | ||
| cols, | ||
| rows, | ||
| cwd: executeOptions.cwd ?? process.cwd(), | ||
| env: { | ||
| ...process.env, | ||
| TERM: PTY_TERM_NAME, | ||
| COLORTERM: "truecolor", | ||
| FORCE_COLOR: "1", | ||
| ...executeOptions.env ?? {} | ||
| } | ||
| }); | ||
| proc.onData((data) => { | ||
| if (streamClosed || currentRun !== runId) return; | ||
| controller?.enqueue(typeof data === "string" ? data : data.toString("utf8")); | ||
| }); | ||
| proc.onExit(({ exitCode, signal }) => { | ||
| if (currentRun !== runId) return; | ||
| closeStream(); | ||
| markStatus(signal === 0 && exitCode !== 0 ? "error" : "stopped"); | ||
| }); | ||
| return proc; | ||
| }; | ||
| try { | ||
| pty = spawnPty(); | ||
| } catch (error) { | ||
| errorStream(error); | ||
| throw diagnostics.DF8203({ | ||
| command: executeOptions.command, | ||
| reason: error instanceof Error ? error.message : String(error) | ||
| }); | ||
| } | ||
| session = { | ||
| ...terminal, | ||
| status: "running", | ||
| interactive: true, | ||
| stream, | ||
| type: "pty", | ||
| executeOptions, | ||
| write: (data) => { | ||
| try { | ||
| pty?.write(data); | ||
| } catch {} | ||
| }, | ||
| resize: (nextCols, nextRows) => { | ||
| try { | ||
| pty?.resize(Math.max(1, nextCols), Math.max(1, nextRows)); | ||
| } catch {} | ||
| }, | ||
| getProcessName: () => { | ||
| try { | ||
| const name = pty?.process; | ||
| return name && name !== PTY_TERM_NAME ? name : void 0; | ||
| } catch { | ||
| return; | ||
| } | ||
| }, | ||
| terminate: async () => { | ||
| pty?.kill(); | ||
| pty = void 0; | ||
| closeStream(); | ||
| markStatus("stopped"); | ||
| }, | ||
| restart: async () => { | ||
| if (streamClosed) throw diagnostics.DF8206({ id: terminal.id }); | ||
| pty?.kill(); | ||
| pty = spawnPty(); | ||
| markStatus("running"); | ||
| } | ||
| }; | ||
| this.register(session); | ||
| return session; | ||
| } | ||
| }; | ||
| //#endregion | ||
| //#region src/node/install-devframe.ts | ||
| /** | ||
| * Find the next free dock id derived from `baseId`. Returns `baseId` | ||
| * when it is unused, otherwise appends `-2`, `-3`, … until a free slot | ||
| * is found. Used by the `'duplicate'` strategy so co-existing instances | ||
| * never collide in the dock registry. | ||
| */ | ||
| function nextAvailableDockId(views, baseId) { | ||
| if (!views.has(baseId)) return baseId; | ||
| let n = 2; | ||
| while (views.has(`${baseId}-${n}`)) n++; | ||
| return `${baseId}-${n}`; | ||
| } | ||
| /** | ||
| * Framework-neutral primitive backing {@link DevframeHubContext.install} — | ||
| * installs a {@link DevframeDefinition} as a dock inside a hub-aware context: | ||
| * serves the devframe's SPA at the resolved base path, synthesizes an iframe | ||
| * dock entry from the definition's metadata, and runs the definition's | ||
| * `setup(ctx)`. Reach for it through `ctx.install(devframe)` rather than | ||
| * calling it directly. | ||
| * | ||
| * Framework kits wrap `ctx.install` with their own plugin/middleware | ||
| * machinery — e.g. `@vitejs/devtools-kit`'s `createPluginFromDevframe` | ||
| * returns a Vite `Plugin` whose `devtools.setup` ultimately delegates here. | ||
| */ | ||
| /** | ||
| * Phase one of an install: run the duplication guard, serve the SPA + meta, | ||
| * register the iframe dock, and queue the definition's declarative wire | ||
| * services — everything up to (but not including) `setup(ctx)`. Returns a | ||
| * deferred setup thunk, or `null` when the devframe was deduplicated. | ||
| * | ||
| * The hub's initial batch uses this to collect every devframe's services | ||
| * across the whole hub, `ready()` them once, and only then run the setups — | ||
| * so services are ready before any setup, and a plugin can consume a service | ||
| * another plugin declared regardless of mount order. | ||
| */ | ||
| async function prepareDevframe(ctx, d, options = {}) { | ||
| const strategy = d.duplicationStrategy ?? "warn"; | ||
| const isDuplicate = ctx.docks.views.has(d.id); | ||
| if (isDuplicate && strategy !== "duplicate") { | ||
| if (strategy === "throw") throw diagnostics.DF8105({ | ||
| id: d.id, | ||
| name: d.name | ||
| }); | ||
| if (strategy === "warn") diagnostics.DF8105({ | ||
| id: d.id, | ||
| name: d.name | ||
| }); | ||
| return null; | ||
| } | ||
| const id = isDuplicate ? nextAvailableDockId(ctx.docks.views, d.id) : d.id; | ||
| const base = options.base ?? (id === d.id ? resolveBasePath(d, "hosted") : resolveBasePath({ | ||
| ...d, | ||
| id, | ||
| basePath: void 0 | ||
| }, "hosted")); | ||
| const clientAssets = resolveClientAssets(d); | ||
| if (clientAssets) { | ||
| if (ctx.host.mountConnectionMeta) await ctx.host.mountConnectionMeta(base); | ||
| else diagnostics.DF8106({ | ||
| id, | ||
| name: d.name, | ||
| base | ||
| }); | ||
| const distSource = clientAssets; | ||
| ctx.views.hostStatic(base, typeof distSource === "string" ? resolve(distSource) : distSource, d.importMetaUrl); | ||
| } | ||
| ctx.docks.register({ | ||
| id, | ||
| title: d.name, | ||
| icon: d.icon, | ||
| ...d.dock, | ||
| ...options.dock, | ||
| type: "iframe", | ||
| url: base | ||
| }); | ||
| for (const input of d.services ?? []) ctx.services.install(input, { resolveFrom: d.importMetaUrl }); | ||
| return () => Promise.resolve(d.setup(ctx)); | ||
| } | ||
| /** | ||
| * Install a {@link DevframeDefinition} into a hub in one call — serve its SPA, | ||
| * register its dock, ready its services, and run `setup(ctx)`. The imperative | ||
| * counterpart to the hub's declarative `devframes` list (which batches the | ||
| * phases via {@link prepareDevframe}); use it from `configure(ctx)` or | ||
| * wherever you hold the context to plug in an extra devframe after startup. | ||
| */ | ||
| async function installDevframe(ctx, d, options = {}) { | ||
| const run = await prepareDevframe(ctx, d, options); | ||
| if (!run) return; | ||
| await ctx.services.ready(); | ||
| await run(); | ||
| } | ||
| //#endregion | ||
| //#region src/node/rpc-builtins.ts | ||
| /** | ||
| * Resolve an interactive (PTY) terminal session by id, or throw. Sessions | ||
| * spawned via `startChildProcess` are output-only and are rejected here. | ||
| */ | ||
| function resolveInteractiveSession(sessions, id) { | ||
| const session = sessions.get(id); | ||
| if (!session) throw diagnostics.DF8201({ id }); | ||
| if (typeof session.write !== "function") throw diagnostics.DF8202({ id }); | ||
| return session; | ||
| } | ||
| /** | ||
| * Resolve a session that can be terminated/restarted (spawned via | ||
| * `startChildProcess` or `startPtySession`), or throw. Sessions added with a | ||
| * bare `register()` carry no lifecycle handle and are rejected. | ||
| */ | ||
| function resolveControllableSession(sessions, id) { | ||
| const session = sessions.get(id); | ||
| if (!session) throw diagnostics.DF8201({ id }); | ||
| if (typeof session.terminate !== "function") throw diagnostics.DF8204({ id }); | ||
| return session; | ||
| } | ||
| /** | ||
| * `hub:commands:execute` — Invoke a registered server command by id. The | ||
| * arguments after `id` are forwarded to the command's `handler(...)`. | ||
| * Returns whatever the handler returns. | ||
| * | ||
| * Pairs with the `devframe:commands` shared state: clients read the list | ||
| * from the shared state and dispatch by id via this RPC. | ||
| */ | ||
| const hubCommandsExecute = defineHubRpcFunction({ | ||
| name: HUB_EVENTS.rpc.commandsExecute, | ||
| type: "action", | ||
| setup: (context) => ({ async handler(id, ...args) { | ||
| return context.commands.execute(id, ...args); | ||
| } }) | ||
| }); | ||
| /** | ||
| * `hub:messages:add` — Add a message from a browser client into the hub's | ||
| * messages subsystem. Marked `from: 'browser'`. Returns the serializable | ||
| * entry (the mutation handle stays server-side). | ||
| * | ||
| * Pairs with the client-side {@link import('../client').createDevframeClientRuntime} | ||
| * context, whose `messages` client dispatches through these built-ins so a | ||
| * dock client script can report into the same feed the server writes to. | ||
| */ | ||
| const hubMessagesAdd = defineHubRpcFunction({ | ||
| name: HUB_EVENTS.rpc.messagesAdd, | ||
| type: "action", | ||
| jsonSerializable: true, | ||
| setup: (context) => ({ async handler(input) { | ||
| return (await context.messages.add({ | ||
| ...input, | ||
| from: "browser" | ||
| })).entry; | ||
| } }) | ||
| }); | ||
| /** `hub:messages:update` — Patch a message by id; returns the updated entry (or `undefined`). */ | ||
| const hubMessagesUpdate = defineHubRpcFunction({ | ||
| name: HUB_EVENTS.rpc.messagesUpdate, | ||
| type: "action", | ||
| jsonSerializable: true, | ||
| setup: (context) => ({ async handler(id, patch) { | ||
| return context.messages.update(id, patch); | ||
| } }) | ||
| }); | ||
| /** `hub:messages:remove` — Remove a message by id. */ | ||
| const hubMessagesRemove = defineHubRpcFunction({ | ||
| name: HUB_EVENTS.rpc.messagesRemove, | ||
| type: "action", | ||
| setup: (context) => ({ async handler(id) { | ||
| await context.messages.remove(id); | ||
| } }) | ||
| }); | ||
| /** `hub:messages:clear` — Remove every message. */ | ||
| const hubMessagesClear = defineHubRpcFunction({ | ||
| name: HUB_EVENTS.rpc.messagesClear, | ||
| type: "action", | ||
| setup: (context) => ({ async handler() { | ||
| await context.messages.clear(); | ||
| } }) | ||
| }); | ||
| /** | ||
| * `hub:terminals:write` — Send input to an interactive PTY session spawned | ||
| * via `ctx.terminals.startPtySession`. Lets a hub-aware terminal UI (e.g. the | ||
| * terminals plugin) drive a session owned by another plugin. | ||
| */ | ||
| const hubTerminalsWrite = defineHubRpcFunction({ | ||
| name: HUB_EVENTS.rpc.terminalsWrite, | ||
| type: "action", | ||
| setup: (context) => ({ async handler(id, data) { | ||
| resolveInteractiveSession(context.terminals.sessions, id).write(data); | ||
| } }) | ||
| }); | ||
| /** `hub:terminals:resize` — Resize an interactive PTY session by id. */ | ||
| const hubTerminalsResize = defineHubRpcFunction({ | ||
| name: HUB_EVENTS.rpc.terminalsResize, | ||
| type: "action", | ||
| setup: (context) => ({ async handler(id, cols, rows) { | ||
| resolveInteractiveSession(context.terminals.sessions, id).resize(cols, rows); | ||
| } }) | ||
| }); | ||
| /** | ||
| * `hub:terminals:terminate` — Kill a session's process while keeping the | ||
| * session registered (its output/scrollback stays). Works for both read-only | ||
| * child-process and interactive PTY sessions, letting a hub-aware terminal UI | ||
| * force-kill a session owned by another plugin. | ||
| */ | ||
| const hubTerminalsTerminate = defineHubRpcFunction({ | ||
| name: HUB_EVENTS.rpc.terminalsTerminate, | ||
| type: "action", | ||
| setup: (context) => ({ async handler(id) { | ||
| await resolveControllableSession(context.terminals.sessions, id).terminate(); | ||
| } }) | ||
| }); | ||
| /** | ||
| * `hub:terminals:restart` — Re-run a session's command in place. Rejected for | ||
| * sessions registered with `restartable: false`, whose lifecycle is owned | ||
| * elsewhere. | ||
| */ | ||
| const hubTerminalsRestart = defineHubRpcFunction({ | ||
| name: HUB_EVENTS.rpc.terminalsRestart, | ||
| type: "action", | ||
| setup: (context) => ({ async handler(id) { | ||
| const session = resolveControllableSession(context.terminals.sessions, id); | ||
| if (session.restartable === false) throw diagnostics.DF8205({ id }); | ||
| await session.restart(); | ||
| } }) | ||
| }); | ||
| /** | ||
| * `hub:terminals:remove` — Kill a session's process (when it still owns one) | ||
| * and drop it from the registry, disposing its output stream. Lets a hub-aware | ||
| * terminal UI discard a stopped aggregated session. | ||
| */ | ||
| const hubTerminalsRemove = defineHubRpcFunction({ | ||
| name: HUB_EVENTS.rpc.terminalsRemove, | ||
| type: "action", | ||
| setup: (context) => ({ async handler(id) { | ||
| const session = context.terminals.sessions.get(id); | ||
| if (!session) throw diagnostics.DF8201({ id }); | ||
| const controllable = session; | ||
| if (typeof controllable.terminate === "function") await controllable.terminate(); | ||
| context.terminals.remove(session); | ||
| } }) | ||
| }); | ||
| /** | ||
| * `hub:docks:activate` — Ask the active viewer to switch its focused dock to | ||
| * `dockId`, optionally carrying `params` for the target dock to interpret | ||
| * (e.g. `{ sessionId }` for the terminals dock to focus a session). | ||
| * | ||
| * Any connected client may call it, which is the point: a mounted devframe | ||
| * running in its own iframe (on its own RPC client) can steer the host shell's | ||
| * dock selection — client-local state it otherwise can't reach. The hub | ||
| * broadcasts the request live to connected clients (the host shell switches) | ||
| * and mirrors it into the `devframe:docks:active` shared state (a dock that | ||
| * mounts in response still converges on it). | ||
| */ | ||
| const hubDocksActivate = defineHubRpcFunction({ | ||
| name: HUB_EVENTS.rpc.docksActivate, | ||
| type: "action", | ||
| setup: (context) => ({ async handler(input) { | ||
| context.docks.activate(input.dockId, input.params); | ||
| } }) | ||
| }); | ||
| /** | ||
| * Framework-neutral RPC declarations auto-registered by | ||
| * {@link createHubContext}. Provide additional RPCs by passing your own | ||
| * array via `CreateHubContextOptions.builtinRpcDeclarations`; the hub's | ||
| * list is prepended automatically. | ||
| */ | ||
| const builtinHubRpcDeclarations = [ | ||
| hubCommandsExecute, | ||
| hubDocksActivate, | ||
| hubMessagesAdd, | ||
| hubMessagesUpdate, | ||
| hubMessagesRemove, | ||
| hubMessagesClear, | ||
| hubTerminalsWrite, | ||
| hubTerminalsResize, | ||
| hubTerminalsTerminate, | ||
| hubTerminalsRestart, | ||
| hubTerminalsRemove | ||
| ]; | ||
| //#endregion | ||
| //#region src/node/context.ts | ||
| /** | ||
| * Create a hub-level node context: wraps devframe's `createHostContext`, | ||
| * attaches the hub hosts (`docks`, `terminals`, `messages`, `commands`), | ||
| * registers the hub's built-in RPC commands, and wires the shared-state | ||
| * synchronization that powers a hub-aware client UI. | ||
| */ | ||
| async function createHubContext(options) { | ||
| const context = await createHostContext({ | ||
| ...options, | ||
| builtinRpcDeclarations: [...builtinHubRpcDeclarations, ...options.builtinRpcDeclarations ?? []] | ||
| }); | ||
| const docks = new DevframeDocksHost(context); | ||
| const terminals = new DevframeTerminalsHost(context); | ||
| const messages = new DevframeMessagesHost(context); | ||
| const commands = new DevframeCommandsHost(context); | ||
| context.docks = docks; | ||
| context.terminals = terminals; | ||
| context.messages = messages; | ||
| context.commands = commands; | ||
| context.install = (devframe, options) => installDevframe(context, devframe, options); | ||
| await docks.init(); | ||
| const debounceMs = options.mode === "build" ? 0 : 10; | ||
| const docksSharedState = await context.rpc.sharedState.get(HUB_EVENTS.sharedState.docks, { initialValue: [] }); | ||
| const refreshDocks = debounce(() => { | ||
| docksSharedState.mutate(() => docks.values()); | ||
| }, debounceMs); | ||
| docks.events.on(HUB_EVENTS.bus.docksEntryUpdated, refreshDocks); | ||
| getInternalContext(context).onWsEndpointChange(refreshDocks); | ||
| docksSharedState.mutate(() => docks.values()); | ||
| const activeDockSharedState = await context.rpc.sharedState.get(HUB_EVENTS.sharedState.docksActive, { initialValue: { activation: null } }); | ||
| docks.events.on(HUB_EVENTS.bus.docksActivate, (activation) => { | ||
| activeDockSharedState.mutate((state) => { | ||
| state.activation = activation; | ||
| }); | ||
| context.rpc.broadcast({ | ||
| method: HUB_EVENTS.broadcast.docksActivate, | ||
| args: [activation] | ||
| }); | ||
| }); | ||
| const broadcastTerminals = debounce(() => { | ||
| context.rpc.broadcast({ | ||
| method: HUB_EVENTS.broadcast.terminalsUpdated, | ||
| args: [] | ||
| }); | ||
| docksSharedState.mutate(() => docks.values()); | ||
| }, debounceMs); | ||
| terminals.events.on(HUB_EVENTS.bus.terminalsSessionUpdated, broadcastTerminals); | ||
| const broadcastMessages = debounce(() => { | ||
| context.rpc.broadcast({ | ||
| method: HUB_EVENTS.broadcast.messagesUpdated, | ||
| args: [] | ||
| }); | ||
| docksSharedState.mutate(() => docks.values()); | ||
| }, debounceMs); | ||
| messages.events.on(HUB_EVENTS.bus.messagesAdded, broadcastMessages); | ||
| messages.events.on(HUB_EVENTS.bus.messagesUpdated, broadcastMessages); | ||
| messages.events.on(HUB_EVENTS.bus.messagesRemoved, broadcastMessages); | ||
| messages.events.on(HUB_EVENTS.bus.messagesCleared, broadcastMessages); | ||
| const commandsSharedState = await context.rpc.sharedState.get(HUB_EVENTS.sharedState.commands, { initialValue: [] }); | ||
| const syncCommands = debounce(() => { | ||
| commandsSharedState.mutate(() => commands.list()); | ||
| }, debounceMs); | ||
| commands.events.on(HUB_EVENTS.bus.commandsRegistered, syncCommands); | ||
| commands.events.on(HUB_EVENTS.bus.commandsUnregistered, syncCommands); | ||
| commandsSharedState.mutate(() => commands.list()); | ||
| return context; | ||
| } | ||
| //#endregion | ||
| export { DevframeDocksHost as _, hubMessagesAdd as a, hubMessagesUpdate as c, hubTerminalsRestart as d, hubTerminalsTerminate as f, DevframeMessagesHost as g, DevframeTerminalsHost as h, hubDocksActivate as i, hubTerminalsRemove as l, prepareDevframe as m, builtinHubRpcDeclarations as n, hubMessagesClear as o, hubTerminalsWrite as p, hubCommandsExecute as r, hubMessagesRemove as s, createHubContext as t, hubTerminalsResize as u, DevframeCommandsHost as v, diagnostics as y }; |
| import { b as DevframeDockEntryIcon, k as DevframeViewIframe, l as DevframeCommandsHost, m as DevframeDockActivation, w as DevframeDocksHost } from "./settings-D7whfcx2.mjs"; | ||
| import { CreateHostContextOptions } from "devframe/node"; | ||
| import { DevframeDefinition, DevframeHost, DevframeNodeContext, EventEmitter } from "devframe/types"; | ||
| import { ChildProcess } from "node:child_process"; | ||
| //#region src/types/messages.d.ts | ||
| type DevframeMessageLevel = 'info' | 'warn' | 'error' | 'success' | 'debug'; | ||
| type DevframeMessageEntryFrom = 'server' | 'browser'; | ||
| interface DevframeMessageElementPosition { | ||
| /** CSS selector for the element */ | ||
| selector?: string; | ||
| /** Bounding box of the element */ | ||
| boundingBox?: { | ||
| x: number; | ||
| y: number; | ||
| width: number; | ||
| height: number; | ||
| }; | ||
| /** Human-readable description of the element */ | ||
| description?: string; | ||
| } | ||
| interface DevframeMessageFilePosition { | ||
| /** Absolute or relative file path */ | ||
| file: string; | ||
| /** Line number (1-based) */ | ||
| line?: number; | ||
| /** Column number (1-based) */ | ||
| column?: number; | ||
| } | ||
| /** | ||
| * A labeled control a message can carry. Rendered by the messages panel; when | ||
| * clicked it drives the described intent. Discriminated by `kind` so further | ||
| * action kinds can be added without reshaping the field. | ||
| * | ||
| * `'activate'` requests the viewer switch its focused dock to `activate.dockId` | ||
| * (deep-linking via the opaque, serializable `activate.params` bag the target | ||
| * dock interprets), via the hub's `hub:docks:activate` RPC. | ||
| */ | ||
| interface DevframeMessageActivateAction { | ||
| /** Stable id for the action within its entry. */ | ||
| id: string; | ||
| /** Button label shown in the messages panel. */ | ||
| label: string; | ||
| kind: 'activate'; | ||
| /** The dock to focus, plus an optional deep-link params bag. */ | ||
| activate: { | ||
| dockId: string; | ||
| params?: Record<string, unknown>; | ||
| }; | ||
| } | ||
| /** | ||
| * `'command'` invokes a command from the hub's command registry (the same | ||
| * registry backing the command palette) by `command.id`, spreading | ||
| * `command.params` as its positional arguments, via the hub's | ||
| * `hub:commands:execute` RPC. | ||
| */ | ||
| interface DevframeMessageCommandAction { | ||
| /** Stable id for the action within its entry. */ | ||
| id: string; | ||
| /** Button label shown in the messages panel. */ | ||
| label: string; | ||
| kind: 'command'; | ||
| /** The command to invoke, plus an optional list of positional arguments. */ | ||
| command: { | ||
| id: string; | ||
| params?: unknown[]; | ||
| }; | ||
| } | ||
| type DevframeMessageAction = DevframeMessageActivateAction | DevframeMessageCommandAction; | ||
| interface DevframeMessageEntry { | ||
| /** | ||
| * Unique identifier for this message entry (auto-generated if not provided) | ||
| */ | ||
| id: string; | ||
| /** | ||
| * Short title or summary of the message | ||
| */ | ||
| message: string; | ||
| /** | ||
| * Optional detailed description or explanation | ||
| */ | ||
| description?: string; | ||
| /** | ||
| * Severity level, determines color and icon | ||
| */ | ||
| level: DevframeMessageLevel; | ||
| /** | ||
| * Optional stack trace string | ||
| */ | ||
| stacktrace?: string; | ||
| /** | ||
| * Optional DOM element position info (e.g., for a11y issues) | ||
| */ | ||
| elementPosition?: DevframeMessageElementPosition; | ||
| /** | ||
| * Optional source file position info (e.g., for lint errors) | ||
| */ | ||
| filePosition?: DevframeMessageFilePosition; | ||
| /** | ||
| * Whether this message should also appear as a toast notification | ||
| */ | ||
| notify?: boolean; | ||
| /** | ||
| * Origin of the message entry, automatically set by the context | ||
| */ | ||
| from: DevframeMessageEntryFrom; | ||
| /** | ||
| * Grouping category (e.g., 'a11y', 'lint', 'runtime', 'test') | ||
| */ | ||
| category?: string; | ||
| /** | ||
| * Optional tags/labels for filtering | ||
| */ | ||
| labels?: string[]; | ||
| /** | ||
| * Optional labeled actions (e.g. "navigate to a dock") the panel renders as | ||
| * clickable controls in the entry's detail view. | ||
| */ | ||
| actions?: DevframeMessageAction[]; | ||
| /** | ||
| * Time in ms to auto-dismiss the toast notification (client-side) or | ||
| * `false` to keep it indefinitely. | ||
| */ | ||
| autoDismiss?: number | false; | ||
| /** | ||
| * Time in ms to auto-delete this message entry (server-side) | ||
| */ | ||
| autoDelete?: number; | ||
| /** | ||
| * Timestamp when the message was created (auto-generated if not provided) | ||
| */ | ||
| timestamp: number; | ||
| /** | ||
| * Status of the message entry (e.g., 'loading' while an operation is in progress). | ||
| * Defaults to 'idle' when not specified. | ||
| */ | ||
| status?: 'loading' | 'idle'; | ||
| } | ||
| /** | ||
| * Input type for creating a message entry. | ||
| * `id`, `timestamp`, and `from` are auto-filled by the host. | ||
| */ | ||
| type DevframeMessageEntryInput = Omit<DevframeMessageEntry, 'id' | 'timestamp' | 'from'> & { | ||
| id?: string; | ||
| timestamp?: number; | ||
| }; | ||
| interface DevframeMessageHandle { | ||
| /** The underlying message entry data */ | ||
| readonly entry: DevframeMessageEntry; | ||
| /** Shortcut to entry.id */ | ||
| readonly id: string; | ||
| /** Partial update of this message entry */ | ||
| update: (patch: Partial<DevframeMessageEntryInput>) => Promise<DevframeMessageEntry | undefined>; | ||
| /** Remove this message entry */ | ||
| dismiss: () => Promise<void>; | ||
| } | ||
| /** | ||
| * Extra fields accepted by the per-level message shortcuts — | ||
| * everything on {@link DevframeMessageEntryInput} except the | ||
| * `message` and `level` the shortcut itself provides. | ||
| */ | ||
| type DevframeMessageShortcutInput = Omit<DevframeMessageEntryInput, 'message' | 'level'>; | ||
| /** | ||
| * Per-level shortcuts shared by the client and the node host — | ||
| * `messages.info('...')` is `messages.add({ message: '...', level: 'info' })`. | ||
| */ | ||
| interface DevframeMessagesLevelShortcuts { | ||
| /** Shortcut for `add({ message, level: 'info', ...extra })` */ | ||
| info: (message: string, extra?: DevframeMessageShortcutInput) => Promise<DevframeMessageHandle>; | ||
| /** Shortcut for `add({ message, level: 'warn', ...extra })` */ | ||
| warn: (message: string, extra?: DevframeMessageShortcutInput) => Promise<DevframeMessageHandle>; | ||
| /** Shortcut for `add({ message, level: 'error', ...extra })` */ | ||
| error: (message: string, extra?: DevframeMessageShortcutInput) => Promise<DevframeMessageHandle>; | ||
| /** Shortcut for `add({ message, level: 'success', ...extra })` */ | ||
| success: (message: string, extra?: DevframeMessageShortcutInput) => Promise<DevframeMessageHandle>; | ||
| /** Shortcut for `add({ message, level: 'debug', ...extra })` */ | ||
| debug: (message: string, extra?: DevframeMessageShortcutInput) => Promise<DevframeMessageHandle>; | ||
| } | ||
| interface DevframeMessagesClient extends DevframeMessagesLevelShortcuts { | ||
| /** | ||
| * Add a message entry. Returns a Promise resolving to a handle for subsequent updates/dismissal. | ||
| * Can be used without `await` for fire-and-forget usage. | ||
| */ | ||
| add: (input: DevframeMessageEntryInput) => Promise<DevframeMessageHandle>; | ||
| /** Remove a message entry by id */ | ||
| remove: (id: string) => Promise<void>; | ||
| /** Clear all message entries */ | ||
| clear: () => Promise<void>; | ||
| } | ||
| /** | ||
| * A snapshot or delta of the message list, as returned by | ||
| * {@link DevframeMessagesHost.listSince}. Consumers apply `removedIds` | ||
| * first, then upsert `entries`, and pass `version` back as `since` on the | ||
| * next call. | ||
| */ | ||
| interface DevframeMessagesListDelta { | ||
| /** Entries added or updated since the cursor (or all entries when `full`) */ | ||
| entries: DevframeMessageEntry[]; | ||
| /** Ids removed since the cursor (empty when `full`) */ | ||
| removedIds: string[]; | ||
| /** The version cursor — pass back as `since` on the next call */ | ||
| version: number; | ||
| /** | ||
| * When `true`, `entries` is the complete snapshot and any locally cached | ||
| * list must be reset before applying it. | ||
| */ | ||
| full: boolean; | ||
| } | ||
| interface DevframeMessagesHost extends DevframeMessagesLevelShortcuts { | ||
| readonly entries: Map<string, DevframeMessageEntry>; | ||
| readonly events: EventEmitter<{ | ||
| 'messages:added': (entry: DevframeMessageEntry) => void; | ||
| 'messages:updated': (entry: DevframeMessageEntry) => void; | ||
| 'messages:removed': (id: string) => void; | ||
| 'messages:cleared': () => void; | ||
| }>; | ||
| /** | ||
| * Add a new message entry. If an entry with the same `id` already exists, it will be updated instead. | ||
| * Returns a handle for subsequent updates/dismissal. Can be used without `await` for fire-and-forget. | ||
| */ | ||
| add: (entry: DevframeMessageEntryInput) => Promise<DevframeMessageHandle>; | ||
| /** | ||
| * Update an existing message entry by id (partial update) | ||
| */ | ||
| update: (id: string, patch: Partial<DevframeMessageEntryInput>) => Promise<DevframeMessageEntry | undefined>; | ||
| /** | ||
| * Remove a message entry by id | ||
| */ | ||
| remove: (id: string) => Promise<void>; | ||
| /** | ||
| * Clear all message entries | ||
| */ | ||
| clear: () => Promise<void>; | ||
| /** | ||
| * Read the message list incrementally. Pass the `version` from the | ||
| * previous result as `since` to receive only the entries modified and the | ||
| * ids removed after that point; pass `null`/`undefined` for the initial | ||
| * full snapshot. When the host can no longer compute a reliable delta for | ||
| * the given cursor (trimmed removal history, or a cursor from another host | ||
| * incarnation), the result carries `full: true` with the complete list. | ||
| */ | ||
| listSince: (since?: number | null) => DevframeMessagesListDelta; | ||
| } | ||
| //#endregion | ||
| //#region src/types/terminals.d.ts | ||
| interface DevframeTerminalsHost { | ||
| readonly sessions: Map<string, DevframeTerminalSession>; | ||
| readonly events: EventEmitter<{ | ||
| 'terminals:session:updated': (session: DevframeTerminalSession) => void; | ||
| }>; | ||
| register: (session: DevframeTerminalSession) => DevframeTerminalSession; | ||
| update: (session: DevframeTerminalSession) => void; | ||
| /** Drop a session from the registry, disposing its bound output stream. */ | ||
| remove: (session: DevframeTerminalSession) => void; | ||
| /** | ||
| * Spawn a read-only child process (pipe-backed, output only). Use this for | ||
| * long-running logs and dev servers that don't need input. | ||
| */ | ||
| startChildProcess: (executeOptions: DevframeChildProcessExecuteOptions, terminal: Omit<DevframeTerminalSessionBase, 'status'>) => Promise<DevframeChildProcessTerminalSession>; | ||
| /** | ||
| * Spawn a fully interactive pseudo-terminal (PTY) any plugin can drive: | ||
| * keystrokes via {@link DevframePtyTerminalSession.write}, live layout via | ||
| * {@link DevframePtyTerminalSession.resize}, TUI-capable. The session is | ||
| * marked `interactive`, so a hub-aware terminal UI (e.g. the terminals | ||
| * plugin) surfaces it as writable rather than read-only. Powered by | ||
| * `zigpty` — where its native bindings can't load, it degrades to | ||
| * pipe-based terminal emulation. | ||
| */ | ||
| startPtySession: (executeOptions: DevframePtyExecuteOptions, terminal: Omit<DevframeTerminalSessionBase, 'status'>) => Promise<DevframePtyTerminalSession>; | ||
| } | ||
| type DevframeTerminalStatus = 'running' | 'stopped' | 'error'; | ||
| interface DevframeTerminalSessionBase { | ||
| id: string; | ||
| title: string; | ||
| description?: string; | ||
| status: DevframeTerminalStatus; | ||
| icon?: DevframeDockEntryIcon; | ||
| /** | ||
| * Whether the session accepts input (keystrokes + resize). `true` for | ||
| * {@link DevframeTerminalsHost.startPtySession} sessions; absent/`false` | ||
| * for pipe-backed, output-only ones. A hub-aware terminal UI reads this to | ||
| * decide whether to enable stdin and wire resize. | ||
| */ | ||
| interactive?: boolean; | ||
| /** | ||
| * Whether the session may be restarted in place (re-running its command). | ||
| * Defaults to `true`. Set `false` for sessions whose lifecycle is owned | ||
| * elsewhere — e.g. a one-shot build, or a server (like code-server) that | ||
| * should be restarted through its own controls rather than by re-spawning | ||
| * the raw process. A hub-aware terminal UI hides its restart affordance for | ||
| * these, and `hub:terminals:restart` rejects them. | ||
| */ | ||
| restartable?: boolean; | ||
| } | ||
| interface DevframeTerminalSession extends DevframeTerminalSessionBase { | ||
| buffer?: string[]; | ||
| stream?: ReadableStream<string>; | ||
| } | ||
| interface DevframeChildProcessExecuteOptions { | ||
| command: string; | ||
| args: string[]; | ||
| cwd?: string; | ||
| env?: Record<string, string>; | ||
| } | ||
| /** | ||
| * The settled outcome of a {@link DevframeChildProcessTerminalSession} run — | ||
| * stdout/stderr captured separately (unlike the session's merged display | ||
| * `stream`), plus the process's exit code (`undefined` if it was killed by a | ||
| * signal before exiting). | ||
| */ | ||
| interface DevframeChildProcessOutput { | ||
| stdout: string; | ||
| stderr: string; | ||
| exitCode: number | undefined; | ||
| } | ||
| /** | ||
| * A live handle on a child process's outcome — mirrors the ergonomics of | ||
| * `tinyexec`'s `Result` (a promise-like paired with synchronous accessors) so | ||
| * callers migrating from a `tinyexec`/`execa`-based subprocess API (e.g. | ||
| * Nuxt DevTools' `startSubprocess().getResult()`) can adopt | ||
| * {@link DevframeTerminalsHost.startChildProcess} with minimal changes. | ||
| * `await`ing it (or calling `.then()`) resolves once the process exits, with | ||
| * the full captured {@link DevframeChildProcessOutput}. | ||
| */ | ||
| interface DevframeChildProcessResult extends PromiseLike<DevframeChildProcessOutput> { | ||
| readonly pid: number | undefined; | ||
| /** `undefined` while the process is still running. */ | ||
| readonly exitCode: number | undefined; | ||
| readonly killed: boolean; | ||
| kill: (signal?: NodeJS.Signals | number) => boolean; | ||
| } | ||
| interface DevframeChildProcessTerminalSession extends DevframeTerminalSession { | ||
| type: 'child-process'; | ||
| executeOptions: DevframeChildProcessExecuteOptions; | ||
| getChildProcess: () => ChildProcess | undefined; | ||
| /** | ||
| * Get a live handle on the current run's outcome. Reflects the most recent | ||
| * `restart()` — call it again after restarting to track the new run. | ||
| */ | ||
| getResult: () => DevframeChildProcessResult; | ||
| terminate: () => Promise<void>; | ||
| /** Throws `DF8206` once the session's output stream has closed (after a natural exit or `terminate()`) — drop it with `ctx.terminals.remove(session)` and start a fresh session instead. */ | ||
| restart: () => Promise<void>; | ||
| } | ||
| interface DevframePtyExecuteOptions { | ||
| command: string; | ||
| args?: string[]; | ||
| cwd?: string; | ||
| env?: Record<string, string>; | ||
| /** Initial column count. Default: 80. */ | ||
| cols?: number; | ||
| /** Initial row count. Default: 24. */ | ||
| rows?: number; | ||
| } | ||
| interface DevframePtyTerminalSession extends DevframeTerminalSession { | ||
| type: 'pty'; | ||
| interactive: true; | ||
| executeOptions: DevframePtyExecuteOptions; | ||
| /** Send keystrokes / raw input to the PTY. */ | ||
| write: (data: string) => void; | ||
| /** Resize the PTY (emits SIGWINCH so TUIs relayout). */ | ||
| resize: (cols: number, rows: number) => void; | ||
| /** Current foreground process name, when the backend can resolve it. */ | ||
| getProcessName: () => string | undefined; | ||
| terminate: () => Promise<void>; | ||
| /** Throws `DF8206` once the session's output stream has closed (after a natural exit or `terminate()`) — drop it with `ctx.terminals.remove(session)` and start a fresh session instead. */ | ||
| restart: () => Promise<void>; | ||
| } | ||
| //#endregion | ||
| //#region src/node/install-devframe.d.ts | ||
| interface InstallDevframeOptions { | ||
| /** | ||
| * Mount path override. Defaults to `d.basePath` or `/__${d.id}/`. | ||
| */ | ||
| base?: string; | ||
| /** | ||
| * Per-mount overrides for the auto-synthesized iframe dock entry. Use | ||
| * this to customize the entry's `category`, override the icon, hide it | ||
| * via `when` (or only its dock-bar button via `visibility`), etc. Takes | ||
| * precedence over the definition's own {@link DevframeDefinition.dock} | ||
| * defaults. Cannot change `id`, `type`, or `url` — those are derived from | ||
| * the devframe definition. | ||
| */ | ||
| dock?: Partial<Omit<DevframeViewIframe, 'id' | 'type' | 'url'>>; | ||
| } | ||
| //#endregion | ||
| //#region src/node/context.d.ts | ||
| declare module 'devframe/types' { | ||
| interface DevframeRpcClientFunctions { | ||
| /** | ||
| * Server→client request to switch the active dock. Broadcast by the hub | ||
| * context in response to `ctx.docks.activate()` (driven by the | ||
| * `hub:docks:activate` RPC). The client host registers a handler that | ||
| * calls its local `switchEntry(dockId)`; the target dock reads | ||
| * `activation.params` to react (e.g. focus a session). Do not register | ||
| * manually. | ||
| * | ||
| * @internal | ||
| */ | ||
| 'devframe:docks:activate': (activation: DevframeDockActivation) => Promise<void>; | ||
| /** | ||
| * Server→client notification that terminal sessions changed. Broadcast | ||
| * by the hub context; a hub-aware client re-reads terminal state in | ||
| * response. Do not register manually. | ||
| * | ||
| * @internal | ||
| */ | ||
| 'devframe:terminals:updated': () => Promise<void>; | ||
| /** | ||
| * Server→client notification that the message list changed. Broadcast | ||
| * by the hub context; a hub-aware client re-reads message state in | ||
| * response. Do not register manually. | ||
| * | ||
| * @internal | ||
| */ | ||
| 'devframe:messages:updated': () => Promise<void>; | ||
| } | ||
| interface DevframeRpcServerFunctions { | ||
| /** | ||
| * Ask the active viewer to switch its focused dock to `dockId`, optionally | ||
| * carrying `params` for the target dock to interpret (e.g. | ||
| * `{ sessionId }` for the terminals dock). Any connected client may call | ||
| * it — a mounted devframe in its own iframe steers the host shell's dock | ||
| * selection. Handled by {@link import('./rpc-builtins').hubDocksActivate}. | ||
| */ | ||
| 'hub:docks:activate': (input: { | ||
| dockId: string; | ||
| params?: Record<string, unknown>; | ||
| }) => Promise<void>; | ||
| /** | ||
| * Invoke a registered server command by id; trailing args are forwarded to | ||
| * the command's handler. Handled by | ||
| * {@link import('./rpc-builtins').hubCommandsExecute}. | ||
| */ | ||
| 'hub:commands:execute': (id: string, ...args: any[]) => Promise<unknown>; | ||
| /** | ||
| * Add a message from a browser client into the hub's messages feed | ||
| * (marked `from: 'browser'`); returns the serializable entry. Handled by | ||
| * {@link import('./rpc-builtins').hubMessagesAdd}. | ||
| */ | ||
| 'hub:messages:add': (input: DevframeMessageEntryInput) => Promise<DevframeMessageEntry>; | ||
| /** Patch a message by id; resolves the updated entry (or `undefined`). */ | ||
| 'hub:messages:update': (id: string, patch: Partial<DevframeMessageEntryInput>) => Promise<DevframeMessageEntry | undefined>; | ||
| /** Remove a message by id. */ | ||
| 'hub:messages:remove': (id: string) => Promise<void>; | ||
| /** Remove every message. */ | ||
| 'hub:messages:clear': () => Promise<void>; | ||
| /** | ||
| * Send input to an interactive PTY session spawned via | ||
| * `ctx.terminals.startPtySession`. Handled by | ||
| * {@link import('./rpc-builtins').hubTerminalsWrite}. | ||
| */ | ||
| 'hub:terminals:write': (id: string, data: string) => Promise<void>; | ||
| /** Resize an interactive PTY session by id. */ | ||
| 'hub:terminals:resize': (id: string, cols: number, rows: number) => Promise<void>; | ||
| } | ||
| } | ||
| /** | ||
| * Hub-augmented node context — extends devframe's framework-neutral | ||
| * `DevframeNodeContext` with the hub-level subsystems (`docks`, | ||
| * `terminals`, `messages`, `commands`). | ||
| * | ||
| * Framework kits further extend this with their own slots (e.g. | ||
| * `viteConfig`, `viteServer`). Host-specific capabilities (editor open, | ||
| * filesystem reveal, etc.) ship as kit-registered RPC functions rather | ||
| * than as part of this surface. JSON-render is an opt-in integration | ||
| * (`@devframes/json-render`) that augments any devframe context and | ||
| * contributes its own dock type — use `createJsonRenderView` from | ||
| * `@devframes/json-render/node`. | ||
| */ | ||
| interface DevframeHubContext extends DevframeNodeContext { | ||
| readonly host: DevframeHost; | ||
| docks: DevframeDocksHost; | ||
| terminals: DevframeTerminalsHost; | ||
| messages: DevframeMessagesHost; | ||
| commands: DevframeCommandsHost; | ||
| /** | ||
| * Install a {@link DevframeDefinition} into this hub: serve its SPA at the | ||
| * resolved base, synthesize an iframe dock from its metadata, and run its | ||
| * `setup(ctx)`. The imperative counterpart to `initHub`'s declarative | ||
| * `devframes` list — call it from a hub host's `configure(ctx)`, or wherever | ||
| * you hold the context, to plug an extra devframe in. | ||
| */ | ||
| install: (devframe: DevframeDefinition, options?: InstallDevframeOptions) => Promise<void>; | ||
| } | ||
| /** | ||
| * Options for {@link createHubContext} — devframe's | ||
| * {@link CreateHostContextOptions} plus any hub-level additions kits layer on | ||
| * through declaration merging. | ||
| */ | ||
| interface CreateHubContextOptions extends CreateHostContextOptions {} | ||
| /** | ||
| * Create a hub-level node context: wraps devframe's `createHostContext`, | ||
| * attaches the hub hosts (`docks`, `terminals`, `messages`, `commands`), | ||
| * registers the hub's built-in RPC commands, and wires the shared-state | ||
| * synchronization that powers a hub-aware client UI. | ||
| */ | ||
| declare function createHubContext(options: CreateHubContextOptions): Promise<DevframeHubContext>; | ||
| //#endregion | ||
| export { DevframeMessageHandle as C, DevframeMessagesHost as D, DevframeMessagesClient as E, DevframeMessagesLevelShortcuts as O, DevframeMessageFilePosition as S, DevframeMessageShortcutInput as T, DevframeMessageCommandAction as _, DevframeChildProcessExecuteOptions as a, DevframeMessageEntryFrom as b, DevframeChildProcessTerminalSession as c, DevframeTerminalSession as d, DevframeTerminalSessionBase as f, DevframeMessageActivateAction as g, DevframeMessageAction as h, InstallDevframeOptions as i, DevframeMessagesListDelta as k, DevframePtyExecuteOptions as l, DevframeTerminalsHost as m, DevframeHubContext as n, DevframeChildProcessOutput as o, DevframeTerminalStatus as p, createHubContext as r, DevframeChildProcessResult as s, CreateHubContextOptions as t, DevframePtyTerminalSession as u, DevframeMessageElementPosition as v, DevframeMessageLevel as w, DevframeMessageEntryInput as x, DevframeMessageEntry as y }; |
| import "./context-D5JSW_X5.mjs"; | ||
| import "./settings-D7whfcx2.mjs"; | ||
| import { RpcDefinitionsFilter, RpcDefinitionsToFunctions } from "devframe/rpc"; | ||
| import { ConnectionMeta as ConnectionMeta$1, DevframeCapabilities, DevframeDiagnosticsHost, DevframeDiagnosticsLogger, DevframeHost as DevframeHost$1, DevframeNodeRpcSession, DevframeRpcClientFunctions, DevframeRpcServerFunctions, DevframeRpcSharedStates, DevframeViewHost, EventEmitter as EventEmitter$1, EventUnsubscribe, EventsMap, RpcBroadcastOptions, RpcFunctionsHost, RpcSharedStateGetOptions, RpcSharedStateHost, RpcStreamingChannel, RpcStreamingChannelOptions, RpcStreamingHost } from "devframe/types"; | ||
| export { RpcStreamingHost as S, RpcFunctionsHost as _, DevframeHost$1 as a, RpcStreamingChannel as b, DevframeRpcServerFunctions as c, EventEmitter$1 as d, EventUnsubscribe as f, RpcDefinitionsToFunctions as g, RpcDefinitionsFilter as h, DevframeDiagnosticsLogger as i, DevframeRpcSharedStates as l, RpcBroadcastOptions as m, DevframeCapabilities as n, DevframeNodeRpcSession as o, EventsMap as p, DevframeDiagnosticsHost as r, DevframeRpcClientFunctions as s, ConnectionMeta$1 as t, DevframeViewHost as u, RpcSharedStateGetOptions as v, RpcStreamingChannelOptions as x, RpcSharedStateHost as y }; |
+26
-20
@@ -1,4 +0,4 @@ | ||
| import { E as DevframeMessagesClient, x as DevframeMessageEntryInput } from "../context-BVkxwz5k.mjs"; | ||
| import { E as DevframeMessagesClient, x as DevframeMessageEntryInput } from "../context-D5JSW_X5.mjs"; | ||
| import { N as NavTarget, P as RemoteConnectionInfo, S as DevframeDockUserEntry, _ as DevframeDockEntry, a as DevframeCommandEntry, b as DevframeDockEntryIcon, g as DevframeDockEntriesGrouped, k as DevframeViewIframe, n as DevframeClientCommand, p as ClientScriptEntry, s as DevframeCommandKeybinding, t as DevframeDocksUserSettings } from "../settings-D7whfcx2.mjs"; | ||
| import "../index-CyEyZHB3.mjs"; | ||
| import "../index-DhxIxS-Q.mjs"; | ||
| import { DevframeClientRpcHost, DevframeConnection, DevframeConnectionStatus, DevframeRpcClient, DevframeRpcClientOptions, DevframeRpcContext, RpcClientEvents, RpcClientEvents as RpcClientEvents$1 } from "devframe/client"; | ||
@@ -147,3 +147,3 @@ import { EventEmitter } from "devframe/types"; | ||
| * Build the {@link DockRenderersContext} shared by every hub-aware client — | ||
| * `createDevframeClientHost` and viewers that assemble their own context | ||
| * `createDevframeClientRuntime` and hub UI providers that assemble their own context | ||
| * (`@devframes/hub-ui`) both delegate here so local-first resolution, lazy | ||
@@ -166,3 +166,3 @@ * manifest imports, and the typed mount result behave identically everywhere. | ||
| * Per-tab UI state of the dock panel, distinct from the browser-shared geometry | ||
| * in {@link DockPanelStorage}. A viewer persists this to `sessionStorage` so a | ||
| * in {@link DockPanelStorage}. A hub UI provider persists this to `sessionStorage` so a | ||
| * reload (or the RPC auth handshake that follows one) restores the panel to | ||
@@ -213,3 +213,3 @@ * exactly where the developer left it — which dock was open, and the | ||
| /** | ||
| * The live connection status of the underlying devframe client, so a viewer | ||
| * The live connection status of the underlying RPC client, so a hub UI provider | ||
| * can render one central connection indicator for every docked plugin | ||
@@ -250,3 +250,3 @@ * instead of each plugin surfacing its own. | ||
| * selected, and that dock's iframe route. Restored across reloads (and the | ||
| * auth handshake that follows one) by a viewer that persists it to | ||
| * auth handshake that follows one) by a hub UI provider that persists it to | ||
| * `sessionStorage`. | ||
@@ -279,3 +279,3 @@ */ | ||
| * (`ConnectionMeta.configs.dock.categoryOrder`), overridden again by the | ||
| * host page's own `createDevframeClientHost({ categoryOrder })`. Fixed | ||
| * host page's own `createDevframeClientRuntime({ categoryOrder })`. Fixed | ||
| * for the life of the session — resolved once at boot. | ||
@@ -307,3 +307,3 @@ */ | ||
| * shared state, so it stays local to this client instead of syncing to the | ||
| * hub or other viewers — for a view a client host synthesizes itself. | ||
| * hub or other hub UI providers — for a view the client runtime synthesizes itself. | ||
| * | ||
@@ -402,4 +402,4 @@ * Throws when `id` already names a client dock, unless `force` is set. A | ||
| * Publish the global Devframe client context (or clear it with `undefined`). | ||
| * Called by {@link import('./host').createDevframeClientHost}; a dock client | ||
| * script or a viewer reads it back with {@link getDevframeClientContext}. | ||
| * Called by {@link import('./host').createDevframeClientRuntime}; a dock client | ||
| * script or a hub UI provider reads it back with {@link getDevframeClientContext}. | ||
| */ | ||
@@ -493,3 +493,3 @@ declare function setDevframeClientContext(ctx: DevframeClientContext | undefined): void; | ||
| /** | ||
| * Shared-iframe soft navigation — the viewer-side half of a host↔iframe | ||
| * Shared-iframe soft navigation — the hub-UI-provider-side half of a host-page↔iframe | ||
| * `postMessage` protocol. | ||
@@ -614,3 +614,3 @@ * | ||
| //#region src/client/host.d.ts | ||
| interface DevframeClientHostOptions { | ||
| interface DevframeClientRuntimeOptions { | ||
| /** | ||
@@ -637,3 +637,3 @@ * An already-connected RPC client. When omitted, one is created via | ||
| * Resolve a **bare-specifier** client script (`importFrom` naming an npm | ||
| * module) to a URL this page can import — a viewer's own policy, winning | ||
| * module) to a URL this page can import — a hub UI provider's own policy, winning | ||
| * over the host-advertised `ConnectionMeta.configs.dock.clientModuleResolution` | ||
@@ -673,3 +673,3 @@ * template (return `undefined` to fall through to it). URL specifiers never | ||
| * // Surface `data` tools ahead of `app` tools, hub-wide. | ||
| * createDevframeClientHost({ categoryOrder: { data: 50 } }) | ||
| * createDevframeClientRuntime({ categoryOrder: { data: 50 } }) | ||
| * ``` | ||
@@ -679,4 +679,4 @@ */ | ||
| } | ||
| interface DevframeClientHost { | ||
| /** The assembled, globally-registered client host context. */ | ||
| interface DevframeClientRuntime { | ||
| /** The assembled, globally-registered client context. */ | ||
| context: DevframeClientContext; | ||
@@ -687,3 +687,3 @@ /** Tear down listeners and stop tracking newly-registered client scripts. */ | ||
| /** | ||
| * Boot the framework-level client host: connect RPC, assemble the full | ||
| * Boot the client runtime: connect RPC, assemble the full | ||
| * {@link DevframeClientContext} (panel, docks, commands, when) from the hub's | ||
@@ -694,6 +694,12 @@ * shared state, publish it at `__DEVFRAME_HUB_CLIENT_CONTEXT__`, and load every | ||
| * | ||
| * A viewer keeps rendering its own dock UI (reading the same shared state); this | ||
| * A hub UI provider keeps rendering its own dock UI (reading the same shared state); this | ||
| * runtime is what gives plugin client scripts a live host context to run in. | ||
| */ | ||
| declare function createDevframeClientHost(options?: DevframeClientHostOptions): Promise<DevframeClientHost>; | ||
| declare function createDevframeClientRuntime(options?: DevframeClientRuntimeOptions): Promise<DevframeClientRuntime>; | ||
| /** @deprecated Renamed — use {@link DevframeClientRuntimeOptions}. */ | ||
| type DevframeClientHostOptions = DevframeClientRuntimeOptions; | ||
| /** @deprecated Renamed — use {@link DevframeClientRuntime}. */ | ||
| type DevframeClientHost = DevframeClientRuntime; | ||
| /** @deprecated Renamed — use {@link createDevframeClientRuntime}. */ | ||
| declare const createDevframeClientHost: typeof createDevframeClientRuntime; | ||
| //#endregion | ||
@@ -748,2 +754,2 @@ //#region src/client/messages.d.ts | ||
| //#endregion | ||
| export { CLIENT_CONTEXT_KEY, CommandsContext, ConnectRemoteDevframeOptions, CreateDockRenderersContextOptions, DevframeClientContext, DevframeClientHost, DevframeClientHostOptions, type DevframeClientRpcHost, DockClientScriptContext, DockClientType, DockEntryState, DockEntryStateEvents, DockPanelStorage, DockRegistration, DockRenderer, DockRendererInstance, DockRendererManifest, DockRendererMountOptions, DockRendererMountResult, DockRenderersContext, DockSessionStorage, DocksConnectionContext, DocksContext, DocksEntriesContext, DocksPanelContext, FRAME_NAV_CHANNEL, FRAME_NAV_VERSION, FrameLocationHistory, FrameLocationTarget, FrameLocationWindow, FrameNavClient, FrameNavClientOptions, FrameNavEnvelope, FrameNavFrameMessage, FrameNavHostMessage, FrameNavHostPayload, FrameNavListenTarget, FrameTab, MessagesClientOptions, type RpcClientEvents, WatchFrameLocationOptions, WhenClauseContext, attachFrameNavClient, buildRemoteDevframeUrl, clientScriptFailureHint, connectRemoteDevframe, createDevframeClientHost, createDockRenderersContext, createMessagesClient, getDevframeClientContext, parseRemoteConnection, resolveClientModuleSpecifier, resolveDockIcon, resolveDockUrl, setDevframeClientContext, stripRemoteConnectionFromUrl, watchFrameLocation }; | ||
| export { CLIENT_CONTEXT_KEY, CommandsContext, ConnectRemoteDevframeOptions, CreateDockRenderersContextOptions, DevframeClientContext, DevframeClientHost, DevframeClientHostOptions, type DevframeClientRpcHost, DevframeClientRuntime, DevframeClientRuntimeOptions, DockClientScriptContext, DockClientType, DockEntryState, DockEntryStateEvents, DockPanelStorage, DockRegistration, DockRenderer, DockRendererInstance, DockRendererManifest, DockRendererMountOptions, DockRendererMountResult, DockRenderersContext, DockSessionStorage, DocksConnectionContext, DocksContext, DocksEntriesContext, DocksPanelContext, FRAME_NAV_CHANNEL, FRAME_NAV_VERSION, FrameLocationHistory, FrameLocationTarget, FrameLocationWindow, FrameNavClient, FrameNavClientOptions, FrameNavEnvelope, FrameNavFrameMessage, FrameNavHostMessage, FrameNavHostPayload, FrameNavListenTarget, FrameTab, MessagesClientOptions, type RpcClientEvents, WatchFrameLocationOptions, WhenClauseContext, attachFrameNavClient, buildRemoteDevframeUrl, clientScriptFailureHint, connectRemoteDevframe, createDevframeClientHost, createDevframeClientRuntime, createDockRenderersContext, createMessagesClient, getDevframeClientContext, parseRemoteConnection, resolveClientModuleSpecifier, resolveDockIcon, resolveDockUrl, setDevframeClientContext, stripRemoteConnectionFromUrl, watchFrameLocation }; |
+12
-10
@@ -1,2 +0,2 @@ | ||
| import { i as DOCK_RENDERERS_STATE_KEY, n as DEFAULT_STATE_USER_SETTINGS, o as HUB_EVENTS, t as DEFAULT_CATEGORIES_ORDER } from "../constants-fpJMWBtH.mjs"; | ||
| import { i as DOCK_RENDERERS_STATE_KEY, n as DEFAULT_STATE_USER_SETTINGS, o as HUB_EVENTS, t as DEFAULT_CATEGORIES_ORDER } from "../constants-DrF61GFx.mjs"; | ||
| import { a as resolveClientModuleSpecifier, n as stripRemoteConnectionFromUrl, r as clientScriptFailureHint, t as buildRemoteConnectionUrl } from "../remote-url-Bgc7gtsP.mjs"; | ||
@@ -18,4 +18,4 @@ import { REMOTE_CONNECTION_KEY } from "devframe/constants"; | ||
| * Publish the global Devframe client context (or clear it with `undefined`). | ||
| * Called by {@link import('./host').createDevframeClientHost}; a dock client | ||
| * script or a viewer reads it back with {@link getDevframeClientContext}. | ||
| * Called by {@link import('./host').createDevframeClientRuntime}; a dock client | ||
| * script or a hub UI provider reads it back with {@link getDevframeClientContext}. | ||
| */ | ||
@@ -157,3 +157,3 @@ function setDevframeClientContext(ctx) { | ||
| /** | ||
| * Shared-iframe soft navigation — the viewer-side half of a host↔iframe | ||
| * Shared-iframe soft navigation — the hub-UI-provider-side half of a host-page↔iframe | ||
| * `postMessage` protocol. | ||
@@ -412,3 +412,3 @@ * | ||
| * Build the {@link DockRenderersContext} shared by every hub-aware client — | ||
| * `createDevframeClientHost` and viewers that assemble their own context | ||
| * `createDevframeClientRuntime` and hub UI providers that assemble their own context | ||
| * (`@devframes/hub-ui`) both delegate here so local-first resolution, lazy | ||
@@ -507,3 +507,3 @@ * manifest imports, and the typed mount result behave identically everywhere. | ||
| /** | ||
| * Boot the framework-level client host: connect RPC, assemble the full | ||
| * Boot the client runtime: connect RPC, assemble the full | ||
| * {@link DevframeClientContext} (panel, docks, commands, when) from the hub's | ||
@@ -514,6 +514,6 @@ * shared state, publish it at `__DEVFRAME_HUB_CLIENT_CONTEXT__`, and load every | ||
| * | ||
| * A viewer keeps rendering its own dock UI (reading the same shared state); this | ||
| * A hub UI provider keeps rendering its own dock UI (reading the same shared state); this | ||
| * runtime is what gives plugin client scripts a live host context to run in. | ||
| */ | ||
| async function createDevframeClientHost(options = {}) { | ||
| async function createDevframeClientRuntime(options = {}) { | ||
| const clientType = options.clientType ?? "standalone"; | ||
@@ -583,3 +583,3 @@ const rpc = options.rpc ?? await connectDevframe(options.connect); | ||
| }); | ||
| if (getDevframeClientContext()) console.warn("[@devframes/hub] A client host context is already published on this page — replacing it. Boot createDevframeClientHost() once per page (e.g. HTML injection combined with a manual import boots it twice)."); | ||
| if (getDevframeClientContext()) console.warn("[@devframes/hub] A client context is already published on this page — replacing it. Boot createDevframeClientRuntime() once per page (e.g. HTML injection combined with a manual import boots it twice)."); | ||
| setDevframeClientContext(context); | ||
@@ -872,2 +872,4 @@ const loadedScripts = /* @__PURE__ */ new Set(); | ||
| } | ||
| /** @deprecated Renamed — use {@link createDevframeClientRuntime}. */ | ||
| const createDevframeClientHost = createDevframeClientRuntime; | ||
| //#endregion | ||
@@ -981,2 +983,2 @@ //#region src/client/remote.ts | ||
| //#endregion | ||
| export { CLIENT_CONTEXT_KEY, FRAME_NAV_CHANNEL, FRAME_NAV_VERSION, attachFrameNavClient, buildRemoteDevframeUrl, clientScriptFailureHint, connectRemoteDevframe, createDevframeClientHost, createDockRenderersContext, createMessagesClient, getDevframeClientContext, parseRemoteConnection, resolveClientModuleSpecifier, resolveDockIcon, resolveDockUrl, setDevframeClientContext, stripRemoteConnectionFromUrl, watchFrameLocation }; | ||
| export { CLIENT_CONTEXT_KEY, FRAME_NAV_CHANNEL, FRAME_NAV_VERSION, attachFrameNavClient, buildRemoteDevframeUrl, clientScriptFailureHint, connectRemoteDevframe, createDevframeClientHost, createDevframeClientRuntime, createDockRenderersContext, createMessagesClient, getDevframeClientContext, parseRemoteConnection, resolveClientModuleSpecifier, resolveDockIcon, resolveDockUrl, setDevframeClientContext, stripRemoteConnectionFromUrl, watchFrameLocation }; |
@@ -1,3 +0,3 @@ | ||
| import { a as normalizeHubBase, i as DOCK_RENDERERS_STATE_KEY, n as DEFAULT_STATE_USER_SETTINGS, o as HUB_EVENTS, r as DEVFRAMES_HUB_BASE, t as DEFAULT_CATEGORIES_ORDER } from "./constants-BrffEYRZ.mjs"; | ||
| import { a as normalizeHubBase, i as DOCK_RENDERERS_STATE_KEY, n as DEFAULT_STATE_USER_SETTINGS, o as HUB_EVENTS, r as DEVFRAMES_HUB_BASE, t as DEFAULT_CATEGORIES_ORDER } from "./constants-C5t0DeZ0.mjs"; | ||
| export * from "devframe/constants"; | ||
| export { DEFAULT_CATEGORIES_ORDER, DEFAULT_STATE_USER_SETTINGS, DEVFRAMES_HUB_BASE, DOCK_RENDERERS_STATE_KEY, HUB_EVENTS, normalizeHubBase }; |
@@ -1,3 +0,3 @@ | ||
| import { a as normalizeHubBase, i as DOCK_RENDERERS_STATE_KEY, n as DEFAULT_STATE_USER_SETTINGS, o as HUB_EVENTS, r as DEVFRAMES_HUB_BASE, t as DEFAULT_CATEGORIES_ORDER } from "./constants-fpJMWBtH.mjs"; | ||
| import { a as normalizeHubBase, i as DOCK_RENDERERS_STATE_KEY, n as DEFAULT_STATE_USER_SETTINGS, o as HUB_EVENTS, r as DEVFRAMES_HUB_BASE, t as DEFAULT_CATEGORIES_ORDER } from "./constants-DrF61GFx.mjs"; | ||
| export * from "devframe/constants"; | ||
| export { DEFAULT_CATEGORIES_ORDER, DEFAULT_STATE_USER_SETTINGS, DEVFRAMES_HUB_BASE, DOCK_RENDERERS_STATE_KEY, HUB_EVENTS, normalizeHubBase }; |
+2
-2
@@ -1,4 +0,4 @@ | ||
| import { C as DevframeMessageHandle, D as DevframeMessagesHost, E as DevframeMessagesClient, O as DevframeMessagesLevelShortcuts, S as DevframeMessageFilePosition, T as DevframeMessageShortcutInput, _ as DevframeMessageCommandAction, a as DevframeChildProcessExecuteOptions, b as DevframeMessageEntryFrom, c as DevframeChildProcessTerminalSession, d as DevframeTerminalSession, f as DevframeTerminalSessionBase, g as DevframeMessageActivateAction, h as DevframeMessageAction, k as DevframeMessagesListDelta, l as DevframePtyExecuteOptions, m as DevframeTerminalsHost, n as DevframeHubContext, o as DevframeChildProcessOutput, p as DevframeTerminalStatus, s as DevframeChildProcessResult, t as CreateHubContextOptions, u as DevframePtyTerminalSession, v as DevframeMessageElementPosition, w as DevframeMessageLevel, x as DevframeMessageEntryInput, y as DevframeMessageEntry } from "./context-BVkxwz5k.mjs"; | ||
| import { C as DevframeMessageHandle, D as DevframeMessagesHost, E as DevframeMessagesClient, O as DevframeMessagesLevelShortcuts, S as DevframeMessageFilePosition, T as DevframeMessageShortcutInput, _ as DevframeMessageCommandAction, a as DevframeChildProcessExecuteOptions, b as DevframeMessageEntryFrom, c as DevframeChildProcessTerminalSession, d as DevframeTerminalSession, f as DevframeTerminalSessionBase, g as DevframeMessageActivateAction, h as DevframeMessageAction, k as DevframeMessagesListDelta, l as DevframePtyExecuteOptions, m as DevframeTerminalsHost, n as DevframeHubContext, o as DevframeChildProcessOutput, p as DevframeTerminalStatus, s as DevframeChildProcessResult, t as CreateHubContextOptions, u as DevframePtyTerminalSession, v as DevframeMessageElementPosition, w as DevframeMessageLevel, x as DevframeMessageEntryInput, y as DevframeMessageEntry } from "./context-D5JSW_X5.mjs"; | ||
| import { A as DevframeViewLauncher, C as DevframeDocksActiveState, D as DevframeViewCustomRender, E as DevframeViewBuiltin, F as RemoteDockOptions, M as FrameSubTabsConfig, N as NavTarget, O as DevframeViewGroup, P as RemoteConnectionInfo, S as DevframeDockUserEntry, T as DevframeViewAction, _ as DevframeDockEntry, a as DevframeCommandEntry, b as DevframeDockEntryIcon, c as DevframeCommandShortcutOverrides, d as DevframeServerCommandEntry, f as DevframeServerCommandInput, g as DevframeDockEntriesGrouped, h as DevframeDockBadgeVariant, i as DevframeCommandBase, j as DevframeViewLauncherStatus, k as DevframeViewIframe, l as DevframeCommandsHost, m as DevframeDockActivation, n as DevframeClientCommand, o as DevframeCommandHandle, p as ClientScriptEntry, r as DevframeCommandAgentOptions, s as DevframeCommandKeybinding, t as DevframeDocksUserSettings, u as DevframeCommandsHostEvents, v as DevframeDockEntryBase, w as DevframeDocksHost, x as DevframeDockEntryRegistry, y as DevframeDockEntryCategory } from "./settings-D7whfcx2.mjs"; | ||
| import { S as RpcStreamingHost, _ as RpcFunctionsHost, a as DevframeHost, b as RpcStreamingChannel, c as DevframeRpcServerFunctions, d as EventEmitter, f as EventUnsubscribe, g as RpcDefinitionsToFunctions, h as RpcDefinitionsFilter, i as DevframeDiagnosticsLogger, l as DevframeRpcSharedStates, m as RpcBroadcastOptions, n as DevframeCapabilities, o as DevframeNodeRpcSession, p as EventsMap, r as DevframeDiagnosticsHost, s as DevframeRpcClientFunctions, t as ConnectionMeta, u as DevframeViewHost, v as RpcSharedStateGetOptions, x as RpcStreamingChannelOptions, y as RpcSharedStateHost } from "./index-CyEyZHB3.mjs"; | ||
| import { S as RpcStreamingHost, _ as RpcFunctionsHost, a as DevframeHost, b as RpcStreamingChannel, c as DevframeRpcServerFunctions, d as EventEmitter, f as EventUnsubscribe, g as RpcDefinitionsToFunctions, h as RpcDefinitionsFilter, i as DevframeDiagnosticsLogger, l as DevframeRpcSharedStates, m as RpcBroadcastOptions, n as DevframeCapabilities, o as DevframeNodeRpcSession, p as EventsMap, r as DevframeDiagnosticsHost, s as DevframeRpcClientFunctions, t as ConnectionMeta, u as DevframeViewHost, v as RpcSharedStateGetOptions, x as RpcStreamingChannelOptions, y as RpcSharedStateHost } from "./index-DhxIxS-Q.mjs"; | ||
| import { WhenContext, WhenExpression } from "devframe/utils/when"; | ||
@@ -5,0 +5,0 @@ //#region src/define.d.ts |
@@ -1,2 +0,2 @@ | ||
| import { C as DevframeMessageHandle, D as DevframeMessagesHost$1, T as DevframeMessageShortcutInput, a as DevframeChildProcessExecuteOptions, c as DevframeChildProcessTerminalSession, d as DevframeTerminalSession, f as DevframeTerminalSessionBase, i as InstallDevframeOptions, k as DevframeMessagesListDelta, l as DevframePtyExecuteOptions, m as DevframeTerminalsHost$1, n as DevframeHubContext, r as createHubContext, t as CreateHubContextOptions, u as DevframePtyTerminalSession, x as DevframeMessageEntryInput, y as DevframeMessageEntry } from "../context-BVkxwz5k.mjs"; | ||
| import { C as DevframeMessageHandle, D as DevframeMessagesHost$1, T as DevframeMessageShortcutInput, a as DevframeChildProcessExecuteOptions, c as DevframeChildProcessTerminalSession, d as DevframeTerminalSession, f as DevframeTerminalSessionBase, i as InstallDevframeOptions, k as DevframeMessagesListDelta, l as DevframePtyExecuteOptions, m as DevframeTerminalsHost$1, n as DevframeHubContext, r as createHubContext, t as CreateHubContextOptions, u as DevframePtyTerminalSession, x as DevframeMessageEntryInput, y as DevframeMessageEntry } from "../context-D5JSW_X5.mjs"; | ||
| import { S as DevframeDockUserEntry, _ as DevframeDockEntry, d as DevframeServerCommandEntry, f as DevframeServerCommandInput, l as DevframeCommandsHost$1, o as DevframeCommandHandle, p as ClientScriptEntry, t as DevframeDocksUserSettings, w as DevframeDocksHost$1 } from "../settings-D7whfcx2.mjs"; | ||
@@ -55,3 +55,3 @@ import { RpcFunctionDefinitionAny } from "devframe/rpc"; | ||
| /** | ||
| * Warn (don't throw — a viewer-side `resolveClientModule` may still cover | ||
| * Warn (don't throw — a client-runtime `resolveClientModule` override may still cover | ||
| * it) when a dock declares a **bare-specifier** client script on a host | ||
@@ -159,3 +159,3 @@ * that advertises no `staticConfig.dock.clientModuleResolution`: the | ||
| * | ||
| * Pairs with the client-side {@link import('../client').createDevframeClientHost} | ||
| * Pairs with the client-side {@link import('../client').createDevframeClientRuntime} | ||
| * context, whose `messages` client dispatches through these built-ins so a | ||
@@ -162,0 +162,0 @@ * dock client script can report into the same feed the server writes to. |
@@ -1,3 +0,3 @@ | ||
| import { i as InstallDevframeOptions, n as DevframeHubContext, t as CreateHubContextOptions } from "../context-BVkxwz5k.mjs"; | ||
| import { r as DEVFRAMES_HUB_BASE } from "../constants-BrffEYRZ.mjs"; | ||
| import { i as InstallDevframeOptions, n as DevframeHubContext, t as CreateHubContextOptions } from "../context-D5JSW_X5.mjs"; | ||
| import { r as DEVFRAMES_HUB_BASE } from "../constants-C5t0DeZ0.mjs"; | ||
| import { DevframeInstanceRecord } from "devframe/internal"; | ||
@@ -155,3 +155,3 @@ import { ConnectionMeta, DevframeDefinition, DevframeServiceInput, DevframeSseOptions, DevframeStorageScope, DevframeWsOptions, McpRouteOptions } from "devframe/types"; | ||
| * mounts. Renderers registered directly in client code | ||
| * (`createDevframeClientHost({ renderers })`) take precedence. | ||
| * (`createDevframeClientRuntime({ renderers })`) take precedence. | ||
| * | ||
@@ -276,3 +276,3 @@ * ```ts | ||
| connectionMeta: () => ConnectionMeta; | ||
| /** Tear down: WS transport/side-car, MCP sessions. */ | ||
| /** Tear down: WS transport/side-car, MCP handler. */ | ||
| close: () => Promise<void>; | ||
@@ -279,0 +279,0 @@ } |
@@ -1,7 +0,7 @@ | ||
| import { a as normalizeHubBase, i as DOCK_RENDERERS_STATE_KEY, r as DEVFRAMES_HUB_BASE } from "../constants-fpJMWBtH.mjs"; | ||
| import { a as normalizeHubBase, i as DOCK_RENDERERS_STATE_KEY, r as DEVFRAMES_HUB_BASE } from "../constants-DrF61GFx.mjs"; | ||
| import { a as resolveClientModuleSpecifier } from "../remote-url-Bgc7gtsP.mjs"; | ||
| import { m as prepareDevframe, t as createHubContext, y as diagnostics } from "../context-CqHxHlNV.mjs"; | ||
| import { m as prepareDevframe, t as createHubContext, y as diagnostics } from "../context-C3hUUCC1.mjs"; | ||
| import { joinURL, withTrailingSlash, withoutLeadingSlash } from "ufo"; | ||
| import { DEVFRAME_CONNECTION_META_FILENAME, DEVFRAME_DOCK_IMPORTS_FILENAME, DEVFRAME_MCP_ROUTE, DEVFRAME_WS_ROUTE } from "devframe/constants"; | ||
| import { createH3DevframeHost, createInstanceShell, resolveInstanceRegister } from "devframe/internal"; | ||
| import { createH3DevframeHost, createInstanceShell, importRuntimeModule, resolveInstanceRegister } from "devframe/internal"; | ||
| import { resolve } from "pathe"; | ||
@@ -204,3 +204,3 @@ import process from "node:process"; | ||
| const mcpRoute = withoutLeadingSlash(mcpConfig.path ?? DEVFRAME_MCP_ROUTE); | ||
| const { mountMcpHttp } = await import("devframe/adapters/mcp"); | ||
| const { mountMcpHttp } = await importRuntimeModule("devframe/adapters/mcp"); | ||
| const mounted = mountMcpHttp(app, ctx, joinURL(base, mcpRoute), { | ||
@@ -207,0 +207,0 @@ serverName: options.name ?? "devframes-hub", |
@@ -1,4 +0,4 @@ | ||
| import { C as DevframeMessageHandle, D as DevframeMessagesHost, E as DevframeMessagesClient, O as DevframeMessagesLevelShortcuts, S as DevframeMessageFilePosition, T as DevframeMessageShortcutInput, _ as DevframeMessageCommandAction, a as DevframeChildProcessExecuteOptions, b as DevframeMessageEntryFrom, c as DevframeChildProcessTerminalSession, d as DevframeTerminalSession, f as DevframeTerminalSessionBase, g as DevframeMessageActivateAction, h as DevframeMessageAction, k as DevframeMessagesListDelta, l as DevframePtyExecuteOptions, m as DevframeTerminalsHost, n as DevframeHubContext, o as DevframeChildProcessOutput, p as DevframeTerminalStatus, s as DevframeChildProcessResult, t as CreateHubContextOptions, u as DevframePtyTerminalSession, v as DevframeMessageElementPosition, w as DevframeMessageLevel, x as DevframeMessageEntryInput, y as DevframeMessageEntry } from "../context-BVkxwz5k.mjs"; | ||
| import { C as DevframeMessageHandle, D as DevframeMessagesHost, E as DevframeMessagesClient, O as DevframeMessagesLevelShortcuts, S as DevframeMessageFilePosition, T as DevframeMessageShortcutInput, _ as DevframeMessageCommandAction, a as DevframeChildProcessExecuteOptions, b as DevframeMessageEntryFrom, c as DevframeChildProcessTerminalSession, d as DevframeTerminalSession, f as DevframeTerminalSessionBase, g as DevframeMessageActivateAction, h as DevframeMessageAction, k as DevframeMessagesListDelta, l as DevframePtyExecuteOptions, m as DevframeTerminalsHost, n as DevframeHubContext, o as DevframeChildProcessOutput, p as DevframeTerminalStatus, s as DevframeChildProcessResult, t as CreateHubContextOptions, u as DevframePtyTerminalSession, v as DevframeMessageElementPosition, w as DevframeMessageLevel, x as DevframeMessageEntryInput, y as DevframeMessageEntry } from "../context-D5JSW_X5.mjs"; | ||
| import { A as DevframeViewLauncher, C as DevframeDocksActiveState, D as DevframeViewCustomRender, E as DevframeViewBuiltin, F as RemoteDockOptions, M as FrameSubTabsConfig, N as NavTarget, O as DevframeViewGroup, P as RemoteConnectionInfo, S as DevframeDockUserEntry, T as DevframeViewAction, _ as DevframeDockEntry, a as DevframeCommandEntry, b as DevframeDockEntryIcon, c as DevframeCommandShortcutOverrides, d as DevframeServerCommandEntry, f as DevframeServerCommandInput, g as DevframeDockEntriesGrouped, h as DevframeDockBadgeVariant, i as DevframeCommandBase, j as DevframeViewLauncherStatus, k as DevframeViewIframe, l as DevframeCommandsHost, m as DevframeDockActivation, n as DevframeClientCommand, o as DevframeCommandHandle, p as ClientScriptEntry, r as DevframeCommandAgentOptions, s as DevframeCommandKeybinding, t as DevframeDocksUserSettings, u as DevframeCommandsHostEvents, v as DevframeDockEntryBase, w as DevframeDocksHost, x as DevframeDockEntryRegistry, y as DevframeDockEntryCategory } from "../settings-D7whfcx2.mjs"; | ||
| import { S as RpcStreamingHost, _ as RpcFunctionsHost, a as DevframeHost, b as RpcStreamingChannel, c as DevframeRpcServerFunctions, d as EventEmitter, f as EventUnsubscribe, g as RpcDefinitionsToFunctions, h as RpcDefinitionsFilter, i as DevframeDiagnosticsLogger, l as DevframeRpcSharedStates, m as RpcBroadcastOptions, n as DevframeCapabilities, o as DevframeNodeRpcSession, p as EventsMap, r as DevframeDiagnosticsHost, s as DevframeRpcClientFunctions, t as ConnectionMeta, u as DevframeViewHost, v as RpcSharedStateGetOptions, x as RpcStreamingChannelOptions, y as RpcSharedStateHost } from "../index-CyEyZHB3.mjs"; | ||
| import { S as RpcStreamingHost, _ as RpcFunctionsHost, a as DevframeHost, b as RpcStreamingChannel, c as DevframeRpcServerFunctions, d as EventEmitter, f as EventUnsubscribe, g as RpcDefinitionsToFunctions, h as RpcDefinitionsFilter, i as DevframeDiagnosticsLogger, l as DevframeRpcSharedStates, m as RpcBroadcastOptions, n as DevframeCapabilities, o as DevframeNodeRpcSession, p as EventsMap, r as DevframeDiagnosticsHost, s as DevframeRpcClientFunctions, t as ConnectionMeta, u as DevframeViewHost, v as RpcSharedStateGetOptions, x as RpcStreamingChannelOptions, y as RpcSharedStateHost } from "../index-DhxIxS-Q.mjs"; | ||
| export { ClientScriptEntry, type ConnectionMeta, type CreateHubContextOptions, type DevframeCapabilities, DevframeChildProcessExecuteOptions, DevframeChildProcessOutput, DevframeChildProcessResult, DevframeChildProcessTerminalSession, DevframeClientCommand, DevframeCommandAgentOptions, DevframeCommandBase, DevframeCommandEntry, DevframeCommandHandle, DevframeCommandKeybinding, DevframeCommandShortcutOverrides, DevframeCommandsHost, DevframeCommandsHostEvents, type DevframeDiagnosticsHost, type DevframeDiagnosticsLogger, DevframeDockActivation, DevframeDockBadgeVariant, DevframeDockEntriesGrouped, DevframeDockEntry, DevframeDockEntryBase, DevframeDockEntryCategory, DevframeDockEntryIcon, DevframeDockEntryRegistry, DevframeDockUserEntry, DevframeDocksActiveState, DevframeDocksHost, DevframeDocksUserSettings, type DevframeHost, type DevframeHubContext, DevframeMessageAction, DevframeMessageActivateAction, DevframeMessageCommandAction, DevframeMessageElementPosition, DevframeMessageEntry, DevframeMessageEntryFrom, DevframeMessageEntryInput, DevframeMessageFilePosition, DevframeMessageHandle, DevframeMessageLevel, DevframeMessageShortcutInput, DevframeMessagesClient, DevframeMessagesHost, DevframeMessagesLevelShortcuts, DevframeMessagesListDelta, type DevframeNodeRpcSession, DevframePtyExecuteOptions, DevframePtyTerminalSession, type DevframeRpcClientFunctions, type DevframeRpcServerFunctions, type DevframeRpcSharedStates, DevframeServerCommandEntry, DevframeServerCommandInput, DevframeTerminalSession, DevframeTerminalSessionBase, DevframeTerminalStatus, DevframeTerminalsHost, DevframeViewAction, DevframeViewBuiltin, DevframeViewCustomRender, DevframeViewGroup, type DevframeViewHost, DevframeViewIframe, DevframeViewLauncher, DevframeViewLauncherStatus, type EventEmitter, type EventUnsubscribe, type EventsMap, FrameSubTabsConfig, NavTarget, RemoteConnectionInfo, RemoteDockOptions, type RpcBroadcastOptions, type RpcDefinitionsFilter, type RpcDefinitionsToFunctions, type RpcFunctionsHost, type RpcSharedStateGetOptions, type RpcSharedStateHost, type RpcStreamingChannel, type RpcStreamingChannelOptions, type RpcStreamingHost }; |
+3
-3
| { | ||
| "name": "@devframes/hub", | ||
| "type": "module", | ||
| "version": "0.9.5", | ||
| "version": "0.9.6", | ||
| "description": "Hub layer that orchestrates devframe docks, terminals, messages, and commands on any host.", | ||
@@ -37,3 +37,3 @@ "author": "Anthony Fu <anthonyfu117@hotmail.com>", | ||
| "peerDependencies": { | ||
| "devframe": "0.9.5" | ||
| "devframe": "0.9.6" | ||
| }, | ||
@@ -56,3 +56,3 @@ "dependencies": { | ||
| "valibot": "^1.4.2", | ||
| "devframe": "0.9.5" | ||
| "devframe": "0.9.6" | ||
| }, | ||
@@ -59,0 +59,0 @@ "scripts": { |
| import { t as DevframeDocksUserSettings } from "./settings-D7whfcx2.mjs"; | ||
| //#region src/events.d.ts | ||
| /** | ||
| * Centralized registry of every event, broadcast, RPC method, shared-state | ||
| * key, and channel name the hub uses — the single source of truth that keeps | ||
| * these names out of scattered string literals. | ||
| * | ||
| * **Keep this in sync with [`docs/content/1.guide/20.events.md`](../../../docs/content/1.guide/20.events.md)** | ||
| * (the Hub Events Reference): every name here appears in that page's tables, and | ||
| * every name there resolves to an entry here. Add, rename, or remove a name in | ||
| * both places in the same change, and reference `HUB_EVENTS.*` from call sites | ||
| * instead of re-typing a literal. | ||
| * | ||
| * The `.events` EventEmitter maps in `types/{docks,terminals,messages,commands}.ts` | ||
| * and the RPC augmentation interfaces in `node/context.ts` declare these same | ||
| * names as type-level keys (a literal is unavoidable in a type position); those | ||
| * declarations mirror this map and move with it. | ||
| */ | ||
| declare const HUB_EVENTS: { | ||
| /** | ||
| * Internal node `EventEmitter` events on `ctx.<subsystem>.events`. Emitted | ||
| * and consumed inside the node process (chiefly by `createHubContext`, which | ||
| * fans them out onto the wire); they never cross to the browser. | ||
| */ | ||
| readonly bus: { | ||
| readonly docksEntryUpdated: "docks:entry:updated"; | ||
| readonly docksActivate: "docks:activate"; | ||
| readonly terminalsSessionUpdated: "terminals:session:updated"; | ||
| readonly messagesAdded: "messages:added"; | ||
| readonly messagesUpdated: "messages:updated"; | ||
| readonly messagesRemoved: "messages:removed"; | ||
| readonly messagesCleared: "messages:cleared"; | ||
| readonly commandsRegistered: "commands:registered"; | ||
| readonly commandsUnregistered: "commands:unregistered"; | ||
| }; | ||
| /** Server RPC methods a connected client calls (client → server), `hub:` prefix. */ | ||
| readonly rpc: { | ||
| readonly docksActivate: "hub:docks:activate"; | ||
| readonly commandsExecute: "hub:commands:execute"; | ||
| readonly messagesAdd: "hub:messages:add"; | ||
| readonly messagesUpdate: "hub:messages:update"; | ||
| readonly messagesRemove: "hub:messages:remove"; | ||
| readonly messagesClear: "hub:messages:clear"; | ||
| readonly terminalsWrite: "hub:terminals:write"; | ||
| readonly terminalsResize: "hub:terminals:resize"; | ||
| readonly terminalsTerminate: "hub:terminals:terminate"; | ||
| readonly terminalsRestart: "hub:terminals:restart"; | ||
| readonly terminalsRemove: "hub:terminals:remove"; | ||
| }; | ||
| /** Broadcast notifications the server pushes to clients (server → client), `devframe:` prefix. */ | ||
| readonly broadcast: { | ||
| readonly docksActivate: "devframe:docks:activate"; | ||
| readonly terminalsUpdated: "devframe:terminals:updated"; | ||
| readonly messagesUpdated: "devframe:messages:updated"; | ||
| }; | ||
| /** Shared-state slot keys a hub-aware client reads (server → client), `devframe:` prefix. */ | ||
| readonly sharedState: { | ||
| readonly docks: "devframe:docks"; | ||
| readonly docksActive: "devframe:docks:active"; | ||
| readonly commands: "devframe:commands"; | ||
| readonly userSettings: "devframe:user-settings"; | ||
| readonly dockRenderers: "devframe:dock-renderers"; | ||
| }; | ||
| /** Streaming channel ids (server → client), `devframe:` prefix. */ | ||
| readonly stream: { | ||
| readonly terminals: "devframe:terminals"; | ||
| }; | ||
| /** `postMessage` channels for host ↔ iframe protocols, `devframe:` prefix. */ | ||
| readonly postMessage: { | ||
| readonly frameNav: "devframe:frame-nav"; | ||
| }; | ||
| }; | ||
| //#endregion | ||
| //#region src/constants.d.ts | ||
| /** Default mount base for a hub instance — one namespace, one catch-all. */ | ||
| declare const DEVFRAMES_HUB_BASE = "/__devframes/"; | ||
| /** | ||
| * Normalize a hub mount base to an absolute path with leading and trailing | ||
| * slashes (e.g. `devframes` → `/devframes/`), collapsing any doubled | ||
| * slashes the input introduced. The one implementation every hub-aware | ||
| * host (`@devframes/hub` itself, and the Vite/Nuxt/Next adapters) resolves | ||
| * `options.base` through. | ||
| */ | ||
| declare function normalizeHubBase(base: string): string; | ||
| /** | ||
| * The default ordering weight for each known dock category — lower sorts | ||
| * earlier. Downstream viewers (e.g. `@vitejs/devtools-kit`) import this as the | ||
| * single source of truth so the hub and its viewers agree on category order. | ||
| * `framework` sorts first; `~builtin` (the viewer's own built-in views) last. | ||
| * | ||
| * The buckets read from "closest to your app" → "platform / analysis" → | ||
| * "peripheral". Gaps between the weights are intentional: a kit can interleave | ||
| * its own categories (or override these) without editing this table. | ||
| */ | ||
| declare const DEFAULT_CATEGORIES_ORDER: Record<string, number>; | ||
| /** | ||
| * Shared-state slot carrying the hub's renderer manifest — one | ||
| * {@link import('./client/renderers').DockRendererManifest} entry per dock | ||
| * `type`, published by `initHub({ renderers })` and consumed by every | ||
| * hub-aware client (the headless client host and viewers alike). | ||
| */ | ||
| declare const DOCK_RENDERERS_STATE_KEY: string; | ||
| declare const DEFAULT_STATE_USER_SETTINGS: () => DevframeDocksUserSettings; | ||
| //#endregion | ||
| export { normalizeHubBase as a, DOCK_RENDERERS_STATE_KEY as i, DEFAULT_STATE_USER_SETTINGS as n, HUB_EVENTS as o, DEVFRAMES_HUB_BASE as r, DEFAULT_CATEGORIES_ORDER as t }; |
| import { cleanDoubleSlashes, withLeadingSlash, withTrailingSlash } from "ufo"; | ||
| //#region src/events.ts | ||
| /** | ||
| * Centralized registry of every event, broadcast, RPC method, shared-state | ||
| * key, and channel name the hub uses — the single source of truth that keeps | ||
| * these names out of scattered string literals. | ||
| * | ||
| * **Keep this in sync with [`docs/content/1.guide/20.events.md`](../../../docs/content/1.guide/20.events.md)** | ||
| * (the Hub Events Reference): every name here appears in that page's tables, and | ||
| * every name there resolves to an entry here. Add, rename, or remove a name in | ||
| * both places in the same change, and reference `HUB_EVENTS.*` from call sites | ||
| * instead of re-typing a literal. | ||
| * | ||
| * The `.events` EventEmitter maps in `types/{docks,terminals,messages,commands}.ts` | ||
| * and the RPC augmentation interfaces in `node/context.ts` declare these same | ||
| * names as type-level keys (a literal is unavoidable in a type position); those | ||
| * declarations mirror this map and move with it. | ||
| */ | ||
| const HUB_EVENTS = { | ||
| /** | ||
| * Internal node `EventEmitter` events on `ctx.<subsystem>.events`. Emitted | ||
| * and consumed inside the node process (chiefly by `createHubContext`, which | ||
| * fans them out onto the wire); they never cross to the browser. | ||
| */ | ||
| bus: { | ||
| docksEntryUpdated: "docks:entry:updated", | ||
| docksActivate: "docks:activate", | ||
| terminalsSessionUpdated: "terminals:session:updated", | ||
| messagesAdded: "messages:added", | ||
| messagesUpdated: "messages:updated", | ||
| messagesRemoved: "messages:removed", | ||
| messagesCleared: "messages:cleared", | ||
| commandsRegistered: "commands:registered", | ||
| commandsUnregistered: "commands:unregistered" | ||
| }, | ||
| /** Server RPC methods a connected client calls (client → server), `hub:` prefix. */ | ||
| rpc: { | ||
| docksActivate: "hub:docks:activate", | ||
| commandsExecute: "hub:commands:execute", | ||
| messagesAdd: "hub:messages:add", | ||
| messagesUpdate: "hub:messages:update", | ||
| messagesRemove: "hub:messages:remove", | ||
| messagesClear: "hub:messages:clear", | ||
| terminalsWrite: "hub:terminals:write", | ||
| terminalsResize: "hub:terminals:resize", | ||
| terminalsTerminate: "hub:terminals:terminate", | ||
| terminalsRestart: "hub:terminals:restart", | ||
| terminalsRemove: "hub:terminals:remove" | ||
| }, | ||
| /** Broadcast notifications the server pushes to clients (server → client), `devframe:` prefix. */ | ||
| broadcast: { | ||
| docksActivate: "devframe:docks:activate", | ||
| terminalsUpdated: "devframe:terminals:updated", | ||
| messagesUpdated: "devframe:messages:updated" | ||
| }, | ||
| /** Shared-state slot keys a hub-aware client reads (server → client), `devframe:` prefix. */ | ||
| sharedState: { | ||
| docks: "devframe:docks", | ||
| docksActive: "devframe:docks:active", | ||
| commands: "devframe:commands", | ||
| userSettings: "devframe:user-settings", | ||
| dockRenderers: "devframe:dock-renderers" | ||
| }, | ||
| /** Streaming channel ids (server → client), `devframe:` prefix. */ | ||
| stream: { terminals: "devframe:terminals" }, | ||
| /** `postMessage` channels for host ↔ iframe protocols, `devframe:` prefix. */ | ||
| postMessage: { frameNav: "devframe:frame-nav" } | ||
| }; | ||
| //#endregion | ||
| //#region src/constants.ts | ||
| /** Default mount base for a hub instance — one namespace, one catch-all. */ | ||
| const DEVFRAMES_HUB_BASE = "/__devframes/"; | ||
| /** | ||
| * Normalize a hub mount base to an absolute path with leading and trailing | ||
| * slashes (e.g. `devframes` → `/devframes/`), collapsing any doubled | ||
| * slashes the input introduced. The one implementation every hub-aware | ||
| * host (`@devframes/hub` itself, and the Vite/Nuxt/Next adapters) resolves | ||
| * `options.base` through. | ||
| */ | ||
| function normalizeHubBase(base) { | ||
| return cleanDoubleSlashes(withTrailingSlash(withLeadingSlash(base))); | ||
| } | ||
| /** | ||
| * The default ordering weight for each known dock category — lower sorts | ||
| * earlier. Downstream viewers (e.g. `@vitejs/devtools-kit`) import this as the | ||
| * single source of truth so the hub and its viewers agree on category order. | ||
| * `framework` sorts first; `~builtin` (the viewer's own built-in views) last. | ||
| * | ||
| * The buckets read from "closest to your app" → "platform / analysis" → | ||
| * "peripheral". Gaps between the weights are intentional: a kit can interleave | ||
| * its own categories (or override these) without editing this table. | ||
| */ | ||
| const DEFAULT_CATEGORIES_ORDER = { | ||
| "framework": -100, | ||
| "default": 0, | ||
| "app": 100, | ||
| "ui": 150, | ||
| "data": 250, | ||
| "web": 300, | ||
| "performance": 350, | ||
| "advanced": 400, | ||
| "docs": 500, | ||
| "~builtin": 1e3 | ||
| }; | ||
| /** | ||
| * Shared-state slot carrying the hub's renderer manifest — one | ||
| * {@link import('./client/renderers').DockRendererManifest} entry per dock | ||
| * `type`, published by `initHub({ renderers })` and consumed by every | ||
| * hub-aware client (the headless client host and viewers alike). | ||
| */ | ||
| const DOCK_RENDERERS_STATE_KEY = HUB_EVENTS.sharedState.dockRenderers; | ||
| const DEFAULT_STATE_USER_SETTINGS = () => ({ | ||
| docksHidden: [], | ||
| docksCategoriesHidden: [], | ||
| docksPinned: [], | ||
| docksCustomOrder: {}, | ||
| commandShortcuts: {} | ||
| }); | ||
| //#endregion | ||
| export { normalizeHubBase as a, DOCK_RENDERERS_STATE_KEY as i, DEFAULT_STATE_USER_SETTINGS as n, HUB_EVENTS as o, DEVFRAMES_HUB_BASE as r, DEFAULT_CATEGORIES_ORDER as t }; |
| import { b as DevframeDockEntryIcon, k as DevframeViewIframe, l as DevframeCommandsHost, m as DevframeDockActivation, w as DevframeDocksHost } from "./settings-D7whfcx2.mjs"; | ||
| import { CreateHostContextOptions } from "devframe/node"; | ||
| import { DevframeDefinition, DevframeHost, DevframeNodeContext, EventEmitter } from "devframe/types"; | ||
| import { ChildProcess } from "node:child_process"; | ||
| //#region src/types/messages.d.ts | ||
| type DevframeMessageLevel = 'info' | 'warn' | 'error' | 'success' | 'debug'; | ||
| type DevframeMessageEntryFrom = 'server' | 'browser'; | ||
| interface DevframeMessageElementPosition { | ||
| /** CSS selector for the element */ | ||
| selector?: string; | ||
| /** Bounding box of the element */ | ||
| boundingBox?: { | ||
| x: number; | ||
| y: number; | ||
| width: number; | ||
| height: number; | ||
| }; | ||
| /** Human-readable description of the element */ | ||
| description?: string; | ||
| } | ||
| interface DevframeMessageFilePosition { | ||
| /** Absolute or relative file path */ | ||
| file: string; | ||
| /** Line number (1-based) */ | ||
| line?: number; | ||
| /** Column number (1-based) */ | ||
| column?: number; | ||
| } | ||
| /** | ||
| * A labeled control a message can carry. Rendered by the messages panel; when | ||
| * clicked it drives the described intent. Discriminated by `kind` so further | ||
| * action kinds can be added without reshaping the field. | ||
| * | ||
| * `'activate'` requests the viewer switch its focused dock to `activate.dockId` | ||
| * (deep-linking via the opaque, serializable `activate.params` bag the target | ||
| * dock interprets), via the hub's `hub:docks:activate` RPC. | ||
| */ | ||
| interface DevframeMessageActivateAction { | ||
| /** Stable id for the action within its entry. */ | ||
| id: string; | ||
| /** Button label shown in the messages panel. */ | ||
| label: string; | ||
| kind: 'activate'; | ||
| /** The dock to focus, plus an optional deep-link params bag. */ | ||
| activate: { | ||
| dockId: string; | ||
| params?: Record<string, unknown>; | ||
| }; | ||
| } | ||
| /** | ||
| * `'command'` invokes a command from the hub's command registry (the same | ||
| * registry backing the command palette) by `command.id`, spreading | ||
| * `command.params` as its positional arguments, via the hub's | ||
| * `hub:commands:execute` RPC. | ||
| */ | ||
| interface DevframeMessageCommandAction { | ||
| /** Stable id for the action within its entry. */ | ||
| id: string; | ||
| /** Button label shown in the messages panel. */ | ||
| label: string; | ||
| kind: 'command'; | ||
| /** The command to invoke, plus an optional list of positional arguments. */ | ||
| command: { | ||
| id: string; | ||
| params?: unknown[]; | ||
| }; | ||
| } | ||
| type DevframeMessageAction = DevframeMessageActivateAction | DevframeMessageCommandAction; | ||
| interface DevframeMessageEntry { | ||
| /** | ||
| * Unique identifier for this message entry (auto-generated if not provided) | ||
| */ | ||
| id: string; | ||
| /** | ||
| * Short title or summary of the message | ||
| */ | ||
| message: string; | ||
| /** | ||
| * Optional detailed description or explanation | ||
| */ | ||
| description?: string; | ||
| /** | ||
| * Severity level, determines color and icon | ||
| */ | ||
| level: DevframeMessageLevel; | ||
| /** | ||
| * Optional stack trace string | ||
| */ | ||
| stacktrace?: string; | ||
| /** | ||
| * Optional DOM element position info (e.g., for a11y issues) | ||
| */ | ||
| elementPosition?: DevframeMessageElementPosition; | ||
| /** | ||
| * Optional source file position info (e.g., for lint errors) | ||
| */ | ||
| filePosition?: DevframeMessageFilePosition; | ||
| /** | ||
| * Whether this message should also appear as a toast notification | ||
| */ | ||
| notify?: boolean; | ||
| /** | ||
| * Origin of the message entry, automatically set by the context | ||
| */ | ||
| from: DevframeMessageEntryFrom; | ||
| /** | ||
| * Grouping category (e.g., 'a11y', 'lint', 'runtime', 'test') | ||
| */ | ||
| category?: string; | ||
| /** | ||
| * Optional tags/labels for filtering | ||
| */ | ||
| labels?: string[]; | ||
| /** | ||
| * Optional labeled actions (e.g. "navigate to a dock") the panel renders as | ||
| * clickable controls in the entry's detail view. | ||
| */ | ||
| actions?: DevframeMessageAction[]; | ||
| /** | ||
| * Time in ms to auto-dismiss the toast notification (client-side) | ||
| */ | ||
| autoDismiss?: number; | ||
| /** | ||
| * Time in ms to auto-delete this message entry (server-side) | ||
| */ | ||
| autoDelete?: number; | ||
| /** | ||
| * Timestamp when the message was created (auto-generated if not provided) | ||
| */ | ||
| timestamp: number; | ||
| /** | ||
| * Status of the message entry (e.g., 'loading' while an operation is in progress). | ||
| * Defaults to 'idle' when not specified. | ||
| */ | ||
| status?: 'loading' | 'idle'; | ||
| } | ||
| /** | ||
| * Input type for creating a message entry. | ||
| * `id`, `timestamp`, and `from` are auto-filled by the host. | ||
| */ | ||
| type DevframeMessageEntryInput = Omit<DevframeMessageEntry, 'id' | 'timestamp' | 'from'> & { | ||
| id?: string; | ||
| timestamp?: number; | ||
| }; | ||
| interface DevframeMessageHandle { | ||
| /** The underlying message entry data */ | ||
| readonly entry: DevframeMessageEntry; | ||
| /** Shortcut to entry.id */ | ||
| readonly id: string; | ||
| /** Partial update of this message entry */ | ||
| update: (patch: Partial<DevframeMessageEntryInput>) => Promise<DevframeMessageEntry | undefined>; | ||
| /** Remove this message entry */ | ||
| dismiss: () => Promise<void>; | ||
| } | ||
| /** | ||
| * Extra fields accepted by the per-level message shortcuts — | ||
| * everything on {@link DevframeMessageEntryInput} except the | ||
| * `message` and `level` the shortcut itself provides. | ||
| */ | ||
| type DevframeMessageShortcutInput = Omit<DevframeMessageEntryInput, 'message' | 'level'>; | ||
| /** | ||
| * Per-level shortcuts shared by the client and the node host — | ||
| * `messages.info('...')` is `messages.add({ message: '...', level: 'info' })`. | ||
| */ | ||
| interface DevframeMessagesLevelShortcuts { | ||
| /** Shortcut for `add({ message, level: 'info', ...extra })` */ | ||
| info: (message: string, extra?: DevframeMessageShortcutInput) => Promise<DevframeMessageHandle>; | ||
| /** Shortcut for `add({ message, level: 'warn', ...extra })` */ | ||
| warn: (message: string, extra?: DevframeMessageShortcutInput) => Promise<DevframeMessageHandle>; | ||
| /** Shortcut for `add({ message, level: 'error', ...extra })` */ | ||
| error: (message: string, extra?: DevframeMessageShortcutInput) => Promise<DevframeMessageHandle>; | ||
| /** Shortcut for `add({ message, level: 'success', ...extra })` */ | ||
| success: (message: string, extra?: DevframeMessageShortcutInput) => Promise<DevframeMessageHandle>; | ||
| /** Shortcut for `add({ message, level: 'debug', ...extra })` */ | ||
| debug: (message: string, extra?: DevframeMessageShortcutInput) => Promise<DevframeMessageHandle>; | ||
| } | ||
| interface DevframeMessagesClient extends DevframeMessagesLevelShortcuts { | ||
| /** | ||
| * Add a message entry. Returns a Promise resolving to a handle for subsequent updates/dismissal. | ||
| * Can be used without `await` for fire-and-forget usage. | ||
| */ | ||
| add: (input: DevframeMessageEntryInput) => Promise<DevframeMessageHandle>; | ||
| /** Remove a message entry by id */ | ||
| remove: (id: string) => Promise<void>; | ||
| /** Clear all message entries */ | ||
| clear: () => Promise<void>; | ||
| } | ||
| /** | ||
| * A snapshot or delta of the message list, as returned by | ||
| * {@link DevframeMessagesHost.listSince}. Consumers apply `removedIds` | ||
| * first, then upsert `entries`, and pass `version` back as `since` on the | ||
| * next call. | ||
| */ | ||
| interface DevframeMessagesListDelta { | ||
| /** Entries added or updated since the cursor (or all entries when `full`) */ | ||
| entries: DevframeMessageEntry[]; | ||
| /** Ids removed since the cursor (empty when `full`) */ | ||
| removedIds: string[]; | ||
| /** The version cursor — pass back as `since` on the next call */ | ||
| version: number; | ||
| /** | ||
| * When `true`, `entries` is the complete snapshot and any locally cached | ||
| * list must be reset before applying it. | ||
| */ | ||
| full: boolean; | ||
| } | ||
| interface DevframeMessagesHost extends DevframeMessagesLevelShortcuts { | ||
| readonly entries: Map<string, DevframeMessageEntry>; | ||
| readonly events: EventEmitter<{ | ||
| 'messages:added': (entry: DevframeMessageEntry) => void; | ||
| 'messages:updated': (entry: DevframeMessageEntry) => void; | ||
| 'messages:removed': (id: string) => void; | ||
| 'messages:cleared': () => void; | ||
| }>; | ||
| /** | ||
| * Add a new message entry. If an entry with the same `id` already exists, it will be updated instead. | ||
| * Returns a handle for subsequent updates/dismissal. Can be used without `await` for fire-and-forget. | ||
| */ | ||
| add: (entry: DevframeMessageEntryInput) => Promise<DevframeMessageHandle>; | ||
| /** | ||
| * Update an existing message entry by id (partial update) | ||
| */ | ||
| update: (id: string, patch: Partial<DevframeMessageEntryInput>) => Promise<DevframeMessageEntry | undefined>; | ||
| /** | ||
| * Remove a message entry by id | ||
| */ | ||
| remove: (id: string) => Promise<void>; | ||
| /** | ||
| * Clear all message entries | ||
| */ | ||
| clear: () => Promise<void>; | ||
| /** | ||
| * Read the message list incrementally. Pass the `version` from the | ||
| * previous result as `since` to receive only the entries modified and the | ||
| * ids removed after that point; pass `null`/`undefined` for the initial | ||
| * full snapshot. When the host can no longer compute a reliable delta for | ||
| * the given cursor (trimmed removal history, or a cursor from another host | ||
| * incarnation), the result carries `full: true` with the complete list. | ||
| */ | ||
| listSince: (since?: number | null) => DevframeMessagesListDelta; | ||
| } | ||
| //#endregion | ||
| //#region src/types/terminals.d.ts | ||
| interface DevframeTerminalsHost { | ||
| readonly sessions: Map<string, DevframeTerminalSession>; | ||
| readonly events: EventEmitter<{ | ||
| 'terminals:session:updated': (session: DevframeTerminalSession) => void; | ||
| }>; | ||
| register: (session: DevframeTerminalSession) => DevframeTerminalSession; | ||
| update: (session: DevframeTerminalSession) => void; | ||
| /** Drop a session from the registry, disposing its bound output stream. */ | ||
| remove: (session: DevframeTerminalSession) => void; | ||
| /** | ||
| * Spawn a read-only child process (pipe-backed, output only). Use this for | ||
| * long-running logs and dev servers that don't need input. | ||
| */ | ||
| startChildProcess: (executeOptions: DevframeChildProcessExecuteOptions, terminal: Omit<DevframeTerminalSessionBase, 'status'>) => Promise<DevframeChildProcessTerminalSession>; | ||
| /** | ||
| * Spawn a fully interactive pseudo-terminal (PTY) any plugin can drive: | ||
| * keystrokes via {@link DevframePtyTerminalSession.write}, live layout via | ||
| * {@link DevframePtyTerminalSession.resize}, TUI-capable. The session is | ||
| * marked `interactive`, so a hub-aware terminal UI (e.g. the terminals | ||
| * plugin) surfaces it as writable rather than read-only. Powered by | ||
| * `zigpty` — where its native bindings can't load, it degrades to | ||
| * pipe-based terminal emulation. | ||
| */ | ||
| startPtySession: (executeOptions: DevframePtyExecuteOptions, terminal: Omit<DevframeTerminalSessionBase, 'status'>) => Promise<DevframePtyTerminalSession>; | ||
| } | ||
| type DevframeTerminalStatus = 'running' | 'stopped' | 'error'; | ||
| interface DevframeTerminalSessionBase { | ||
| id: string; | ||
| title: string; | ||
| description?: string; | ||
| status: DevframeTerminalStatus; | ||
| icon?: DevframeDockEntryIcon; | ||
| /** | ||
| * Whether the session accepts input (keystrokes + resize). `true` for | ||
| * {@link DevframeTerminalsHost.startPtySession} sessions; absent/`false` | ||
| * for pipe-backed, output-only ones. A hub-aware terminal UI reads this to | ||
| * decide whether to enable stdin and wire resize. | ||
| */ | ||
| interactive?: boolean; | ||
| /** | ||
| * Whether the session may be restarted in place (re-running its command). | ||
| * Defaults to `true`. Set `false` for sessions whose lifecycle is owned | ||
| * elsewhere — e.g. a one-shot build, or a server (like code-server) that | ||
| * should be restarted through its own controls rather than by re-spawning | ||
| * the raw process. A hub-aware terminal UI hides its restart affordance for | ||
| * these, and `hub:terminals:restart` rejects them. | ||
| */ | ||
| restartable?: boolean; | ||
| } | ||
| interface DevframeTerminalSession extends DevframeTerminalSessionBase { | ||
| buffer?: string[]; | ||
| stream?: ReadableStream<string>; | ||
| } | ||
| interface DevframeChildProcessExecuteOptions { | ||
| command: string; | ||
| args: string[]; | ||
| cwd?: string; | ||
| env?: Record<string, string>; | ||
| } | ||
| /** | ||
| * The settled outcome of a {@link DevframeChildProcessTerminalSession} run — | ||
| * stdout/stderr captured separately (unlike the session's merged display | ||
| * `stream`), plus the process's exit code (`undefined` if it was killed by a | ||
| * signal before exiting). | ||
| */ | ||
| interface DevframeChildProcessOutput { | ||
| stdout: string; | ||
| stderr: string; | ||
| exitCode: number | undefined; | ||
| } | ||
| /** | ||
| * A live handle on a child process's outcome — mirrors the ergonomics of | ||
| * `tinyexec`'s `Result` (a promise-like paired with synchronous accessors) so | ||
| * callers migrating from a `tinyexec`/`execa`-based subprocess API (e.g. | ||
| * Nuxt DevTools' `startSubprocess().getResult()`) can adopt | ||
| * {@link DevframeTerminalsHost.startChildProcess} with minimal changes. | ||
| * `await`ing it (or calling `.then()`) resolves once the process exits, with | ||
| * the full captured {@link DevframeChildProcessOutput}. | ||
| */ | ||
| interface DevframeChildProcessResult extends PromiseLike<DevframeChildProcessOutput> { | ||
| readonly pid: number | undefined; | ||
| /** `undefined` while the process is still running. */ | ||
| readonly exitCode: number | undefined; | ||
| readonly killed: boolean; | ||
| kill: (signal?: NodeJS.Signals | number) => boolean; | ||
| } | ||
| interface DevframeChildProcessTerminalSession extends DevframeTerminalSession { | ||
| type: 'child-process'; | ||
| executeOptions: DevframeChildProcessExecuteOptions; | ||
| getChildProcess: () => ChildProcess | undefined; | ||
| /** | ||
| * Get a live handle on the current run's outcome. Reflects the most recent | ||
| * `restart()` — call it again after restarting to track the new run. | ||
| */ | ||
| getResult: () => DevframeChildProcessResult; | ||
| terminate: () => Promise<void>; | ||
| /** Throws `DF8206` once the session's output stream has closed (after a natural exit or `terminate()`) — drop it with `ctx.terminals.remove(session)` and start a fresh session instead. */ | ||
| restart: () => Promise<void>; | ||
| } | ||
| interface DevframePtyExecuteOptions { | ||
| command: string; | ||
| args?: string[]; | ||
| cwd?: string; | ||
| env?: Record<string, string>; | ||
| /** Initial column count. Default: 80. */ | ||
| cols?: number; | ||
| /** Initial row count. Default: 24. */ | ||
| rows?: number; | ||
| } | ||
| interface DevframePtyTerminalSession extends DevframeTerminalSession { | ||
| type: 'pty'; | ||
| interactive: true; | ||
| executeOptions: DevframePtyExecuteOptions; | ||
| /** Send keystrokes / raw input to the PTY. */ | ||
| write: (data: string) => void; | ||
| /** Resize the PTY (emits SIGWINCH so TUIs relayout). */ | ||
| resize: (cols: number, rows: number) => void; | ||
| /** Current foreground process name, when the backend can resolve it. */ | ||
| getProcessName: () => string | undefined; | ||
| terminate: () => Promise<void>; | ||
| /** Throws `DF8206` once the session's output stream has closed (after a natural exit or `terminate()`) — drop it with `ctx.terminals.remove(session)` and start a fresh session instead. */ | ||
| restart: () => Promise<void>; | ||
| } | ||
| //#endregion | ||
| //#region src/node/install-devframe.d.ts | ||
| interface InstallDevframeOptions { | ||
| /** | ||
| * Mount path override. Defaults to `d.basePath` or `/__${d.id}/`. | ||
| */ | ||
| base?: string; | ||
| /** | ||
| * Per-mount overrides for the auto-synthesized iframe dock entry. Use | ||
| * this to customize the entry's `category`, override the icon, hide it | ||
| * via `when` (or only its dock-bar button via `visibility`), etc. Takes | ||
| * precedence over the definition's own {@link DevframeDefinition.dock} | ||
| * defaults. Cannot change `id`, `type`, or `url` — those are derived from | ||
| * the devframe definition. | ||
| */ | ||
| dock?: Partial<Omit<DevframeViewIframe, 'id' | 'type' | 'url'>>; | ||
| } | ||
| //#endregion | ||
| //#region src/node/context.d.ts | ||
| declare module 'devframe/types' { | ||
| interface DevframeRpcClientFunctions { | ||
| /** | ||
| * Server→client request to switch the active dock. Broadcast by the hub | ||
| * context in response to `ctx.docks.activate()` (driven by the | ||
| * `hub:docks:activate` RPC). The client host registers a handler that | ||
| * calls its local `switchEntry(dockId)`; the target dock reads | ||
| * `activation.params` to react (e.g. focus a session). Do not register | ||
| * manually. | ||
| * | ||
| * @internal | ||
| */ | ||
| 'devframe:docks:activate': (activation: DevframeDockActivation) => Promise<void>; | ||
| /** | ||
| * Server→client notification that terminal sessions changed. Broadcast | ||
| * by the hub context; a hub-aware client re-reads terminal state in | ||
| * response. Do not register manually. | ||
| * | ||
| * @internal | ||
| */ | ||
| 'devframe:terminals:updated': () => Promise<void>; | ||
| /** | ||
| * Server→client notification that the message list changed. Broadcast | ||
| * by the hub context; a hub-aware client re-reads message state in | ||
| * response. Do not register manually. | ||
| * | ||
| * @internal | ||
| */ | ||
| 'devframe:messages:updated': () => Promise<void>; | ||
| } | ||
| interface DevframeRpcServerFunctions { | ||
| /** | ||
| * Ask the active viewer to switch its focused dock to `dockId`, optionally | ||
| * carrying `params` for the target dock to interpret (e.g. | ||
| * `{ sessionId }` for the terminals dock). Any connected client may call | ||
| * it — a mounted devframe in its own iframe steers the host shell's dock | ||
| * selection. Handled by {@link import('./rpc-builtins').hubDocksActivate}. | ||
| */ | ||
| 'hub:docks:activate': (input: { | ||
| dockId: string; | ||
| params?: Record<string, unknown>; | ||
| }) => Promise<void>; | ||
| /** | ||
| * Invoke a registered server command by id; trailing args are forwarded to | ||
| * the command's handler. Handled by | ||
| * {@link import('./rpc-builtins').hubCommandsExecute}. | ||
| */ | ||
| 'hub:commands:execute': (id: string, ...args: any[]) => Promise<unknown>; | ||
| /** | ||
| * Add a message from a browser client into the hub's messages feed | ||
| * (marked `from: 'browser'`); returns the serializable entry. Handled by | ||
| * {@link import('./rpc-builtins').hubMessagesAdd}. | ||
| */ | ||
| 'hub:messages:add': (input: DevframeMessageEntryInput) => Promise<DevframeMessageEntry>; | ||
| /** Patch a message by id; resolves the updated entry (or `undefined`). */ | ||
| 'hub:messages:update': (id: string, patch: Partial<DevframeMessageEntryInput>) => Promise<DevframeMessageEntry | undefined>; | ||
| /** Remove a message by id. */ | ||
| 'hub:messages:remove': (id: string) => Promise<void>; | ||
| /** Remove every message. */ | ||
| 'hub:messages:clear': () => Promise<void>; | ||
| /** | ||
| * Send input to an interactive PTY session spawned via | ||
| * `ctx.terminals.startPtySession`. Handled by | ||
| * {@link import('./rpc-builtins').hubTerminalsWrite}. | ||
| */ | ||
| 'hub:terminals:write': (id: string, data: string) => Promise<void>; | ||
| /** Resize an interactive PTY session by id. */ | ||
| 'hub:terminals:resize': (id: string, cols: number, rows: number) => Promise<void>; | ||
| } | ||
| } | ||
| /** | ||
| * Hub-augmented node context — extends devframe's framework-neutral | ||
| * `DevframeNodeContext` with the hub-level subsystems (`docks`, | ||
| * `terminals`, `messages`, `commands`). | ||
| * | ||
| * Framework kits further extend this with their own slots (e.g. | ||
| * `viteConfig`, `viteServer`). Host-specific capabilities (editor open, | ||
| * filesystem reveal, etc.) ship as kit-registered RPC functions rather | ||
| * than as part of this surface. JSON-render is an opt-in integration | ||
| * (`@devframes/json-render`) that augments any devframe context and | ||
| * contributes its own dock type — use `createJsonRenderView` from | ||
| * `@devframes/json-render/node`. | ||
| */ | ||
| interface DevframeHubContext extends DevframeNodeContext { | ||
| readonly host: DevframeHost; | ||
| docks: DevframeDocksHost; | ||
| terminals: DevframeTerminalsHost; | ||
| messages: DevframeMessagesHost; | ||
| commands: DevframeCommandsHost; | ||
| /** | ||
| * Install a {@link DevframeDefinition} into this hub: serve its SPA at the | ||
| * resolved base, synthesize an iframe dock from its metadata, and run its | ||
| * `setup(ctx)`. The imperative counterpart to `initHub`'s declarative | ||
| * `devframes` list — call it from a hub host's `configure(ctx)`, or wherever | ||
| * you hold the context, to plug an extra devframe in. | ||
| */ | ||
| install: (devframe: DevframeDefinition, options?: InstallDevframeOptions) => Promise<void>; | ||
| } | ||
| /** | ||
| * Options for {@link createHubContext} — devframe's | ||
| * {@link CreateHostContextOptions} plus any hub-level additions kits layer on | ||
| * through declaration merging. | ||
| */ | ||
| interface CreateHubContextOptions extends CreateHostContextOptions {} | ||
| /** | ||
| * Create a hub-level node context: wraps devframe's `createHostContext`, | ||
| * attaches the hub hosts (`docks`, `terminals`, `messages`, `commands`), | ||
| * registers the hub's built-in RPC commands, and wires the shared-state | ||
| * synchronization that powers a hub-aware client UI. | ||
| */ | ||
| declare function createHubContext(options: CreateHubContextOptions): Promise<DevframeHubContext>; | ||
| //#endregion | ||
| export { DevframeMessageHandle as C, DevframeMessagesHost as D, DevframeMessagesClient as E, DevframeMessagesLevelShortcuts as O, DevframeMessageFilePosition as S, DevframeMessageShortcutInput as T, DevframeMessageCommandAction as _, DevframeChildProcessExecuteOptions as a, DevframeMessageEntryFrom as b, DevframeChildProcessTerminalSession as c, DevframeTerminalSession as d, DevframeTerminalSessionBase as f, DevframeMessageActivateAction as g, DevframeMessageAction as h, InstallDevframeOptions as i, DevframeMessagesListDelta as k, DevframePtyExecuteOptions as l, DevframeTerminalsHost as m, DevframeHubContext as n, DevframeChildProcessOutput as o, DevframeTerminalStatus as p, createHubContext as r, DevframeChildProcessResult as s, CreateHubContextOptions as t, DevframePtyTerminalSession as u, DevframeMessageElementPosition as v, DevframeMessageLevel as w, DevframeMessageEntryInput as x, DevframeMessageEntry as y }; |
| import { r as defineHubRpcFunction } from "./define-Ceekw2EO.mjs"; | ||
| import { n as DEFAULT_STATE_USER_SETTINGS, o as HUB_EVENTS } from "./constants-fpJMWBtH.mjs"; | ||
| import { i as isBareModuleSpecifier, t as buildRemoteConnectionUrl } from "./remote-url-Bgc7gtsP.mjs"; | ||
| import { createEventEmitter } from "devframe/utils/events"; | ||
| import { createHostContext, createStorage } from "devframe/node"; | ||
| import { getInternalContext, resolveBasePath } from "devframe/node/hub-internals"; | ||
| import { debounce } from "perfect-debounce"; | ||
| import { coerceAgentPositionalArgs } from "devframe/internal"; | ||
| import { defineDiagnostics } from "devframe/utils/nostics"; | ||
| import { join, resolve } from "pathe"; | ||
| import { nanoid } from "devframe/utils/nanoid"; | ||
| import process from "node:process"; | ||
| import { resolveClientAssets } from "devframe"; | ||
| //#region src/node/diagnostics.ts | ||
| const diagnostics = defineDiagnostics({ | ||
| docsBase: "https://devfra.me/errors", | ||
| codes: { | ||
| DF8000: { | ||
| why: (p) => `Devframe id "${p.id}" collides with a reserved hub path — it cannot be mounted directly under the hub base.`, | ||
| fix: "The filenames directly under the hub base (`__connection.json`, `__ws`, `__index.json`, `__client-imports.js`, `__mcp`, `embedded.js`) are reserved for the hub protocol. Rename the devframe id, or override its mount with a non-colliding `basePath`." | ||
| }, | ||
| DF8002: { | ||
| why: "initHub received both `devframes` and `context` — the two assembly modes are mutually exclusive.", | ||
| fix: "Pass `devframes` to let the instance create the hub context and mount each frame itself, or pass a pre-built `context` (your host already mounted the frames) — never both." | ||
| }, | ||
| DF8003: { | ||
| why: "connectionMeta() was called before initHub finished initializing.", | ||
| fix: "Await `instance.ready` (or any request through `instance.handler`) before reading `connectionMeta()` — the WebSocket binding it describes is only known once initialization completes." | ||
| }, | ||
| DF8004: { | ||
| why: (p) => `Devframe id "${p.id}" is not a mountable URL segment — the hub mounts each frame at \`<base><id>/\`.`, | ||
| fix: "Ids become route segments, so they may only contain letters, digits, `_`, `-`, and `.` — `:` and `*` are route-pattern markers to the underlying router, and `/` would escape the segment. Set a route-safe `id` on the definition (e.g. `my_plugin` instead of `my:plugin`)." | ||
| }, | ||
| DF8100: { | ||
| why: (p) => `Dock with id "${p.id}" is already registered`, | ||
| fix: "Use the `force` parameter to overwrite an existing registration." | ||
| }, | ||
| DF8101: { | ||
| why: (p) => `Cannot change the id of dock "${p.id}" to "${p.attempted}". Dock ids are immutable once registered`, | ||
| fix: (p) => `Remove \`id\` from the patch to keep updating "${p.id}", or call register() with the full entry to add "${p.attempted}" as a new dock.` | ||
| }, | ||
| DF8102: { | ||
| why: (p) => `Dock with id "${p.id}" is not registered and cannot be updated`, | ||
| fix: (p) => `Call register() to add "${p.id}" as a new dock, or check the id for typos.` | ||
| }, | ||
| DF8103: { | ||
| why: (p) => `Dock entry "${p.id}" cannot set groupId to its own id`, | ||
| fix: "Point groupId at a different group entry, or omit it." | ||
| }, | ||
| DF8104: { | ||
| why: (p) => `Dock group "${p.id}" cannot itself belong to a group (nested groups are unsupported)`, | ||
| fix: "Remove groupId from the group entry; nest members one level only." | ||
| }, | ||
| DF8105: { | ||
| why: (p) => `Devframe "${p.name}" (id "${p.id}") is already mounted on this hub`, | ||
| fix: "Each devframe is deduplicated by id. Set `duplicationStrategy: \"duplicate\"` on the definition to let instances coexist, `\"silent\"` to drop duplicates quietly, or `\"throw\"` to surface them as errors." | ||
| }, | ||
| DF8106: { | ||
| why: (p) => `The host cannot serve the RPC connection meta for devframe "${p.name}" (id "${p.id}") at "${p.base}" — its \`DevframeHost\` does not implement \`mountConnectionMeta\`.`, | ||
| fix: "Implement `mountConnectionMeta(base)` on your DevframeHost so it serves `__connection.json` at each mounted base. Without it, the devframe SPA connects only when it shares an origin with the hub UI (same-origin window inheritance); cross-origin, sandboxed, or directly-opened iframes stay disconnected. Static-snapshot hosts that bake the meta into the served files can implement it as a no-op to acknowledge this intentionally." | ||
| }, | ||
| DF8107: { | ||
| why: (p) => `Dock activation requested for unknown dock id "${p.id}"`, | ||
| fix: "Pass a `dockId` that matches a registered dock entry. The activation is still broadcast, but no viewer will switch to it. Ids are case-sensitive — check for typos, and ensure the target dock is registered before activating it." | ||
| }, | ||
| DF8108: { | ||
| why: (p) => `A renderer module is already registered for dock type "${p.type}"`, | ||
| fix: "Each dock type resolves to exactly one renderer module in the hub's renderer manifest. Remove the duplicate `renderers` registration, or give the second renderer its own dock type." | ||
| }, | ||
| DF8109: { | ||
| why: (p) => `The renderer module registered for dock type "${p.type}" does not exist at "${p.file}"`, | ||
| fix: "Point the registration's `file` at the prebuilt browser ES module (build the renderer package first, or check the path). Registration helpers like `jsonRenderUiRenderer()` resolve the path for you." | ||
| }, | ||
| DF8110: { | ||
| why: (p) => `Dock type "${p.type}" is not a servable renderer-module name — the hub serves each module at \`<base>__renderers/<type>.mjs\``, | ||
| fix: "Renderer types become URL segments, so they may only contain letters, digits, `_`, `-`, and `.`. Use a route-safe dock type (e.g. `json-render`)." | ||
| }, | ||
| DF8111: { | ||
| why: (p) => `Dock "${p.id}" declares the bare-specifier client script "${p.specifier}", but this host advertises no client-module resolution — the browser cannot resolve a bare npm specifier natively, so the script will fail to load.`, | ||
| fix: "Run under a host that declares `initHub({ clientModuleResolution })` (e.g. Vite's `'/@id/{specifier}'`), ship the script as a self-contained bundle served by URL, or resolve it in the viewer via `createDevframeClientHost({ resolveClientModule })` (then disregard this warning)." | ||
| }, | ||
| DF8200: { why: (p) => `Terminal session with id "${p.id}" already registered` }, | ||
| DF8201: { why: (p) => `Terminal session with id "${p.id}" not registered` }, | ||
| DF8202: { | ||
| why: (p) => `Terminal session "${p.id}" does not accept input`, | ||
| fix: "Spawn it via ctx.terminals.startPtySession() to get an interactive, writable session." | ||
| }, | ||
| DF8203: { why: (p) => `Failed to spawn PTY session for "${p.command}": ${p.reason}` }, | ||
| DF8204: { | ||
| why: (p) => `Terminal session "${p.id}" cannot be controlled (no lifecycle handle)`, | ||
| fix: "Spawn it via ctx.terminals.startChildProcess() or startPtySession() — sessions added with a bare register() expose no terminate/restart handle." | ||
| }, | ||
| DF8205: { | ||
| why: (p) => `Terminal session "${p.id}" is not restartable`, | ||
| fix: "It was registered with `restartable: false`; restart it through its owner's controls, or spawn it with `restartable: true` (the default) to allow in-place restarts." | ||
| }, | ||
| DF8206: { | ||
| why: (p) => `Terminal session "${p.id}" cannot be restarted — its output stream is already closed`, | ||
| fix: "The session already exited (or was terminated) and its stream is spent. Drop it with `ctx.terminals.remove(session)`, then spawn a replacement via `ctx.terminals.startChildProcess()` or `ctx.terminals.startPtySession()` with a fresh id." | ||
| }, | ||
| DF8400: { why: (p) => `Command "${p.id}" is already registered` }, | ||
| DF8401: { why: "Cannot change the id of a command. Use register() to add new commands" }, | ||
| DF8402: { why: (p) => `Command "${p.id}" is not registered` }, | ||
| DF8403: { | ||
| why: (p) => `Command id "${p.id}" is already used by another command or child command`, | ||
| fix: "Use globally unique command ids for top-level commands and all child commands." | ||
| }, | ||
| DF8404: { | ||
| why: (p) => `Command "${p.id}" declares agent exposure but has no handler`, | ||
| fix: "Agent-exposed commands must be executable server-side. Add a `handler` to the command, or move the `agent` field to an executable child command." | ||
| } | ||
| } | ||
| }); | ||
| //#endregion | ||
| //#region src/node/host-commands.ts | ||
| function findChildCommand(command, id) { | ||
| for (const child of command.children ?? []) { | ||
| if (child.id === id) return child; | ||
| const nested = findChildCommand(child, id); | ||
| if (nested) return nested; | ||
| } | ||
| } | ||
| function collectCommandIds(command, ids = []) { | ||
| ids.push(command.id); | ||
| for (const child of command.children ?? []) collectCommandIds(child, ids); | ||
| return ids; | ||
| } | ||
| function validateCommandIds(commands, command, ignoreTopLevelId) { | ||
| const ids = collectCommandIds(command); | ||
| const seen = /* @__PURE__ */ new Set(); | ||
| for (const id of ids) { | ||
| if (seen.has(id)) throw diagnostics.DF8403({ id }); | ||
| seen.add(id); | ||
| } | ||
| for (const [registeredId, registered] of commands) { | ||
| if (registeredId === ignoreTopLevelId) continue; | ||
| const registeredIds = new Set(collectCommandIds(registered)); | ||
| for (const id of ids) if (registeredIds.has(id)) throw diagnostics.DF8403({ id }); | ||
| } | ||
| } | ||
| var DevframeCommandsHost = class { | ||
| context; | ||
| commands = /* @__PURE__ */ new Map(); | ||
| events = createEventEmitter(); | ||
| /** | ||
| * Lazy agent projection: `ctx.agent` queries this provider at list/invoke | ||
| * time, deriving tools from {@link commands} on demand — the commands map | ||
| * stays the single source of truth, nothing is mirrored or kept in sync. | ||
| */ | ||
| agentProvider; | ||
| constructor(context) { | ||
| this.context = context; | ||
| this.agentProvider = context.agent?.registerToolProvider(() => this.collectAgentTools()); | ||
| } | ||
| register(command) { | ||
| if (this.commands.has(command.id)) throw diagnostics.DF8400({ id: command.id }); | ||
| validateCommandIds(this.commands, command); | ||
| this.validateAgentExposure(command); | ||
| this.commands.set(command.id, command); | ||
| this.events.emit(HUB_EVENTS.bus.commandsRegistered, this.toSerializable(command)); | ||
| this.agentProvider?.notifyChanged(); | ||
| return { | ||
| id: command.id, | ||
| update: (patch) => { | ||
| if ("id" in patch) throw diagnostics.DF8401(); | ||
| const existing = this.commands.get(command.id); | ||
| if (!existing) throw diagnostics.DF8402({ id: command.id }); | ||
| const next = { | ||
| ...existing, | ||
| ...patch, | ||
| id: existing.id | ||
| }; | ||
| validateCommandIds(this.commands, next, existing.id); | ||
| this.validateAgentExposure(next); | ||
| Object.assign(existing, patch); | ||
| this.events.emit(HUB_EVENTS.bus.commandsRegistered, this.toSerializable(existing)); | ||
| this.agentProvider?.notifyChanged(); | ||
| }, | ||
| unregister: () => this.unregister(command.id) | ||
| }; | ||
| } | ||
| unregister(id) { | ||
| const deleted = this.commands.delete(id); | ||
| if (deleted) { | ||
| this.events.emit(HUB_EVENTS.bus.commandsUnregistered, id); | ||
| this.agentProvider?.notifyChanged(); | ||
| } | ||
| return deleted; | ||
| } | ||
| async execute(id, ...args) { | ||
| const found = this.findCommand(id); | ||
| if (!found) throw diagnostics.DF8402({ id }); | ||
| if (!found.handler) throw new Error(`Command "${id}" has no handler (group-only command)`); | ||
| return found.handler(...args); | ||
| } | ||
| list() { | ||
| return Array.from(this.commands.values()).map((cmd) => this.toSerializable(cmd)); | ||
| } | ||
| findCommand(id) { | ||
| const topLevel = this.commands.get(id); | ||
| if (topLevel) return topLevel; | ||
| for (const cmd of this.commands.values()) { | ||
| const child = findChildCommand(cmd, id); | ||
| if (child) return child; | ||
| } | ||
| } | ||
| toSerializable(cmd) { | ||
| const { handler: _, agent: __, children, ...rest } = cmd; | ||
| return { | ||
| ...rest, | ||
| source: "server", | ||
| ...children ? { children: children.map((c) => this.toSerializable(c)) } : {} | ||
| }; | ||
| } | ||
| /** Reject `agent` on handler-less commands anywhere in the tree, up front. */ | ||
| validateAgentExposure(command) { | ||
| if (command.agent && !command.handler) throw diagnostics.DF8404({ id: command.id }); | ||
| for (const child of command.children ?? []) this.validateAgentExposure(child); | ||
| } | ||
| /** | ||
| * Derive the agent-tool projection of the current command trees: every | ||
| * agent-flagged, handler-bearing command (children included) becomes a | ||
| * callable tool. Queried lazily by the provider registered in the | ||
| * constructor. `when` clauses evaluate client-side only and are not | ||
| * enforced here — opting in a `when`-gated command is a deliberate author | ||
| * decision (documented on `DevframeCommandAgentOptions`). | ||
| */ | ||
| collectAgentTools() { | ||
| const tools = []; | ||
| const walk = (command) => { | ||
| const agent = command.agent; | ||
| if (agent && command.handler) tools.push({ | ||
| id: command.id, | ||
| title: agent.title ?? command.title, | ||
| description: agent.description, | ||
| safety: agent.safety ?? "action", | ||
| tags: agent.tags, | ||
| args: agent.args, | ||
| handler: async (args) => this.execute(command.id, ...coerceAgentPositionalArgs(args, agent.args, "drop")) | ||
| }); | ||
| for (const child of command.children ?? []) walk(child); | ||
| }; | ||
| for (const command of this.commands.values()) walk(command); | ||
| return tools; | ||
| } | ||
| }; | ||
| //#endregion | ||
| //#region src/node/host-docks.ts | ||
| function normaliseRemoteOptions(remote) { | ||
| const opts = remote === true ? {} : remote; | ||
| return { | ||
| transport: opts.transport ?? "fragment", | ||
| originLock: opts.originLock ?? true | ||
| }; | ||
| } | ||
| var DevframeDocksHost = class { | ||
| context; | ||
| views = /* @__PURE__ */ new Map(); | ||
| events = createEventEmitter(); | ||
| userSettings = void 0; | ||
| /** Dock-id → allocated remote token + resolved options. */ | ||
| remoteDocks = /* @__PURE__ */ new Map(); | ||
| constructor(context) { | ||
| this.context = context; | ||
| } | ||
| async init() { | ||
| this.userSettings = await this.context.rpc.sharedState.get(HUB_EVENTS.sharedState.userSettings, { sharedState: createStorage({ | ||
| filepath: join(this.context.host.getStorageDir("project"), "settings.json"), | ||
| initialValue: DEFAULT_STATE_USER_SETTINGS() | ||
| }) }); | ||
| } | ||
| values() { | ||
| return Array.from(this.views.values(), (view) => this.projectView(view)); | ||
| } | ||
| projectView(view) { | ||
| if (view.type !== "iframe" || !view.remote) return view; | ||
| const record = this.remoteDocks.get(view.id); | ||
| const endpoint = getInternalContext(this.context).wsEndpoint; | ||
| if (!record || !endpoint) return view; | ||
| const payload = { | ||
| v: 1, | ||
| backend: "websocket", | ||
| websocket: endpoint.url, | ||
| authToken: record.token, | ||
| origin: this.resolveDevServerOrigin() | ||
| }; | ||
| return { | ||
| ...view, | ||
| url: buildRemoteConnectionUrl(view.url, payload, record.options.transport) | ||
| }; | ||
| } | ||
| resolveDevServerOrigin() { | ||
| return this.context.host.resolveOrigin(); | ||
| } | ||
| register(view, force) { | ||
| if (this.views.has(view.id) && !force) throw diagnostics.DF8100({ id: view.id }); | ||
| this.validateGroupMembership(view); | ||
| this.warnUnresolvableClientScript(view); | ||
| this.prepareRemoteRegistration(view); | ||
| this.views.set(view.id, view); | ||
| this.events.emit(HUB_EVENTS.bus.docksEntryUpdated, view); | ||
| return { update: (patch) => { | ||
| if (patch.id && patch.id !== view.id) throw diagnostics.DF8101({ | ||
| id: view.id, | ||
| attempted: patch.id | ||
| }); | ||
| this.update({ | ||
| ...this.views.get(view.id), | ||
| ...patch | ||
| }); | ||
| } }; | ||
| } | ||
| update(view) { | ||
| if (!this.views.has(view.id)) throw diagnostics.DF8102({ id: view.id }); | ||
| this.validateGroupMembership(view); | ||
| this.prepareRemoteRegistration(view); | ||
| this.views.set(view.id, view); | ||
| this.events.emit(HUB_EVENTS.bus.docksEntryUpdated, view); | ||
| } | ||
| activate(dockId, params) { | ||
| if (!this.views.has(dockId)) diagnostics.DF8107({ id: dockId }); | ||
| this.events.emit(HUB_EVENTS.bus.docksActivate, { | ||
| dockId, | ||
| params | ||
| }); | ||
| } | ||
| /** | ||
| * Warn (don't throw — a viewer-side `resolveClientModule` may still cover | ||
| * it) when a dock declares a **bare-specifier** client script on a host | ||
| * that advertises no `staticConfig.dock.clientModuleResolution`: the | ||
| * browser cannot resolve a bare npm specifier natively, so the script is | ||
| * doomed to fail there. | ||
| */ | ||
| warnUnresolvableClientScript(view) { | ||
| if (this.context.staticConfig?.dock?.clientModuleResolution) return; | ||
| const script = view.clientScript ?? view.action ?? view.renderer; | ||
| if (script?.importFrom && isBareModuleSpecifier(script.importFrom)) diagnostics.DF8111({ | ||
| id: view.id, | ||
| specifier: script.importFrom | ||
| }); | ||
| } | ||
| validateGroupMembership(view) { | ||
| if (view.groupId === void 0) return; | ||
| if (view.groupId === view.id) throw diagnostics.DF8103({ id: view.id }); | ||
| if (view.type === "group") throw diagnostics.DF8104({ id: view.id }); | ||
| } | ||
| prepareRemoteRegistration(view) { | ||
| const internal = getInternalContext(this.context); | ||
| internal.revokeRemoteTokensForDock(view.id); | ||
| this.remoteDocks.delete(view.id); | ||
| if (view.type !== "iframe" || !view.remote) return; | ||
| const options = normaliseRemoteOptions(view.remote); | ||
| let dockOrigin; | ||
| try { | ||
| dockOrigin = new URL(view.url).origin; | ||
| } catch { | ||
| dockOrigin = this.resolveDevServerOrigin(); | ||
| } | ||
| const token = internal.allocateRemoteToken(view.id, dockOrigin, options.originLock); | ||
| this.remoteDocks.set(view.id, { | ||
| token, | ||
| options | ||
| }); | ||
| } | ||
| }; | ||
| //#endregion | ||
| //#region src/node/host-messages.ts | ||
| const MAX_ENTRIES = 1e3; | ||
| const MAX_REMOVALS = 1e3; | ||
| var DevframeMessagesHost = class { | ||
| context; | ||
| entries = /* @__PURE__ */ new Map(); | ||
| events = createEventEmitter(); | ||
| /** Tracks when each entry was last added or updated (monotonic) */ | ||
| lastModified = /* @__PURE__ */ new Map(); | ||
| /** Tracks recently removed entry IDs with their removal time */ | ||
| removals = []; | ||
| _autoDeleteTimers = /* @__PURE__ */ new Map(); | ||
| _clock = 0; | ||
| /** | ||
| * The tick of the newest removal record dropped from the capped | ||
| * `removals` log — cursors older than this can't get a reliable delta | ||
| * and fall back to a full snapshot in {@link listSince}. | ||
| */ | ||
| _removalsTrimmedAt = 0; | ||
| _tick() { | ||
| return ++this._clock; | ||
| } | ||
| _recordRemoval(id, time) { | ||
| this.removals.push({ | ||
| id, | ||
| time | ||
| }); | ||
| if (this.removals.length > MAX_REMOVALS) { | ||
| const dropped = this.removals.splice(0, this.removals.length - MAX_REMOVALS); | ||
| this._removalsTrimmedAt = dropped[dropped.length - 1].time; | ||
| } | ||
| } | ||
| constructor(context) { | ||
| this.context = context; | ||
| } | ||
| async add(input) { | ||
| if (input.id && this.entries.has(input.id)) { | ||
| await this.update(input.id, input); | ||
| return this._createHandle(input.id); | ||
| } | ||
| const entry = { | ||
| ...input, | ||
| id: input.id ?? nanoid(), | ||
| timestamp: input.timestamp ?? Date.now(), | ||
| from: input.from ?? "server" | ||
| }; | ||
| if (this.entries.size >= MAX_ENTRIES) { | ||
| const oldest = this.entries.keys().next().value; | ||
| await this.remove(oldest); | ||
| } | ||
| this.entries.set(entry.id, entry); | ||
| this.lastModified.set(entry.id, this._tick()); | ||
| this.events.emit(HUB_EVENTS.bus.messagesAdded, entry); | ||
| if (entry.autoDelete) this._autoDeleteTimers.set(entry.id, setTimeout(() => { | ||
| this.remove(entry.id); | ||
| }, entry.autoDelete)); | ||
| return this._createHandle(entry.id); | ||
| } | ||
| async update(id, patch) { | ||
| const existing = this.entries.get(id); | ||
| if (!existing) return void 0; | ||
| const updated = { | ||
| ...existing, | ||
| ...patch, | ||
| id: existing.id, | ||
| from: existing.from, | ||
| timestamp: existing.timestamp | ||
| }; | ||
| this.entries.set(id, updated); | ||
| this.lastModified.set(id, this._tick()); | ||
| this.events.emit(HUB_EVENTS.bus.messagesUpdated, updated); | ||
| if (patch.autoDelete !== void 0) { | ||
| const timer = this._autoDeleteTimers.get(id); | ||
| if (timer) { | ||
| clearTimeout(timer); | ||
| this._autoDeleteTimers.delete(id); | ||
| } | ||
| if (patch.autoDelete) this._autoDeleteTimers.set(id, setTimeout(() => { | ||
| this.remove(id); | ||
| }, patch.autoDelete)); | ||
| } | ||
| return updated; | ||
| } | ||
| async remove(id) { | ||
| const timer = this._autoDeleteTimers.get(id); | ||
| if (timer) { | ||
| clearTimeout(timer); | ||
| this._autoDeleteTimers.delete(id); | ||
| } | ||
| this.entries.delete(id); | ||
| this.lastModified.delete(id); | ||
| this._recordRemoval(id, this._tick()); | ||
| this.events.emit(HUB_EVENTS.bus.messagesRemoved, id); | ||
| } | ||
| info(message, extra) { | ||
| return this.add({ | ||
| ...extra, | ||
| message, | ||
| level: "info" | ||
| }); | ||
| } | ||
| warn(message, extra) { | ||
| return this.add({ | ||
| ...extra, | ||
| message, | ||
| level: "warn" | ||
| }); | ||
| } | ||
| error(message, extra) { | ||
| return this.add({ | ||
| ...extra, | ||
| message, | ||
| level: "error" | ||
| }); | ||
| } | ||
| success(message, extra) { | ||
| return this.add({ | ||
| ...extra, | ||
| message, | ||
| level: "success" | ||
| }); | ||
| } | ||
| debug(message, extra) { | ||
| return this.add({ | ||
| ...extra, | ||
| message, | ||
| level: "debug" | ||
| }); | ||
| } | ||
| async clear() { | ||
| for (const timer of this._autoDeleteTimers.values()) clearTimeout(timer); | ||
| this._autoDeleteTimers.clear(); | ||
| const tick = this._tick(); | ||
| for (const id of this.entries.keys()) this._recordRemoval(id, tick); | ||
| this.entries.clear(); | ||
| this.lastModified.clear(); | ||
| this.events.emit(HUB_EVENTS.bus.messagesCleared); | ||
| } | ||
| listSince(since) { | ||
| const version = this._clock; | ||
| if (since == null || since < this._removalsTrimmedAt || since > version) return { | ||
| entries: Array.from(this.entries.values()), | ||
| removedIds: [], | ||
| version, | ||
| full: true | ||
| }; | ||
| const entries = []; | ||
| for (const [id, entry] of this.entries) { | ||
| const mod = this.lastModified.get(id); | ||
| if (mod != null && mod > since) entries.push(entry); | ||
| } | ||
| const removedIds = []; | ||
| for (const removal of this.removals) if (removal.time > since) removedIds.push(removal.id); | ||
| return { | ||
| entries, | ||
| removedIds, | ||
| version, | ||
| full: false | ||
| }; | ||
| } | ||
| _createHandle(id) { | ||
| const host = this; | ||
| return { | ||
| get entry() { | ||
| return host.entries.get(id); | ||
| }, | ||
| get id() { | ||
| return id; | ||
| }, | ||
| update: (patch) => host.update(id, patch), | ||
| dismiss: () => host.remove(id) | ||
| }; | ||
| } | ||
| }; | ||
| //#endregion | ||
| //#region src/node/host-terminals.ts | ||
| /** | ||
| * Channel name used for terminal stream output. Stable, well-known so | ||
| * hub-aware clients can subscribe by name. | ||
| */ | ||
| const TERMINAL_STREAM_CHANNEL = HUB_EVENTS.stream.terminals; | ||
| const TERMINAL_REPLAY_WINDOW = 1e3; | ||
| /** Max chunks retained in the per-session scrollback buffer (bounded like the replay window). */ | ||
| const TERMINAL_BUFFER_LIMIT = 1e3; | ||
| /** TERM handed to spawned PTYs; also used to reject fallback process labels. */ | ||
| const PTY_TERM_NAME = "xterm-256color"; | ||
| var DevframeTerminalsHost = class { | ||
| context; | ||
| sessions = /* @__PURE__ */ new Map(); | ||
| events = createEventEmitter(); | ||
| _boundStreams = /* @__PURE__ */ new Map(); | ||
| _channel; | ||
| constructor(context) { | ||
| this.context = context; | ||
| } | ||
| /** | ||
| * Lazily acquire the streaming channel — `context.rpc` isn't assigned | ||
| * until after every host is constructed, so we can't grab it in the | ||
| * constructor. | ||
| */ | ||
| getStreamingChannel() { | ||
| if (this._channel) return this._channel; | ||
| if (!this.context.rpc?.streaming) return void 0; | ||
| this._channel = this.context.rpc.streaming.create(TERMINAL_STREAM_CHANNEL, { replayWindow: TERMINAL_REPLAY_WINDOW }); | ||
| return this._channel; | ||
| } | ||
| register(session) { | ||
| if (this.sessions.has(session.id)) throw diagnostics.DF8200({ id: session.id }); | ||
| this.sessions.set(session.id, session); | ||
| this.bindStream(session); | ||
| this.events.emit(HUB_EVENTS.bus.terminalsSessionUpdated, session); | ||
| return session; | ||
| } | ||
| update(patch) { | ||
| if (!this.sessions.has(patch.id)) throw diagnostics.DF8201({ id: patch.id }); | ||
| const session = this.sessions.get(patch.id); | ||
| Object.assign(session, patch); | ||
| this.sessions.set(patch.id, session); | ||
| this.bindStream(session); | ||
| this.events.emit(HUB_EVENTS.bus.terminalsSessionUpdated, session); | ||
| } | ||
| remove(session) { | ||
| this._boundStreams.get(session.id)?.dispose(); | ||
| this.sessions.delete(session.id); | ||
| this.events.emit(HUB_EVENTS.bus.terminalsSessionUpdated, session); | ||
| this._boundStreams.delete(session.id); | ||
| } | ||
| bindStream(session) { | ||
| if (this._boundStreams.has(session.id) && this._boundStreams.get(session.id)?.stream === session.stream) return; | ||
| this._boundStreams.get(session.id)?.dispose(); | ||
| this._boundStreams.delete(session.id); | ||
| if (!session.stream) return; | ||
| session.buffer ||= []; | ||
| const sessionBuffer = session.buffer; | ||
| const sink = this.getStreamingChannel()?.start({ id: session.id }); | ||
| const reader = session.stream.getReader(); | ||
| let disposed = false; | ||
| (async () => { | ||
| try { | ||
| while (true) { | ||
| if (disposed) break; | ||
| const result = await reader.read(); | ||
| if (disposed) break; | ||
| if (result.done) break; | ||
| sessionBuffer.push(result.value); | ||
| if (sessionBuffer.length > TERMINAL_BUFFER_LIMIT) sessionBuffer.splice(0, sessionBuffer.length - TERMINAL_BUFFER_LIMIT); | ||
| sink?.write(result.value); | ||
| } | ||
| if (!disposed && sink && !sink.closed) sink.close(); | ||
| } catch (error) { | ||
| if (!disposed && sink && !sink.closed) sink.error(error); | ||
| } finally { | ||
| try { | ||
| reader.releaseLock(); | ||
| } catch {} | ||
| } | ||
| })(); | ||
| this._boundStreams.set(session.id, { | ||
| dispose: () => { | ||
| disposed = true; | ||
| reader.cancel("terminal stream disposed").catch(() => {}); | ||
| if (sink && !sink.closed) sink.close(); | ||
| }, | ||
| stream: session.stream | ||
| }); | ||
| } | ||
| async startChildProcess(executeOptions, terminal) { | ||
| if (this.sessions.has(terminal.id)) throw diagnostics.DF8200({ id: terminal.id }); | ||
| const { exec } = await import("tinyexec"); | ||
| let controller; | ||
| let cp; | ||
| let currentResult; | ||
| let runId = 0; | ||
| let streamClosed = false; | ||
| let session; | ||
| const markStatus = (next) => { | ||
| if (session.status === next) return; | ||
| session.status = next; | ||
| this.events.emit(HUB_EVENTS.bus.terminalsSessionUpdated, session); | ||
| }; | ||
| const closeStream = () => { | ||
| if (streamClosed) return; | ||
| streamClosed = true; | ||
| try { | ||
| controller?.close(); | ||
| } catch {} | ||
| }; | ||
| const errorStream = (error) => { | ||
| if (streamClosed) return; | ||
| streamClosed = true; | ||
| try { | ||
| controller?.error(error); | ||
| } catch {} | ||
| }; | ||
| const stream = new ReadableStream({ | ||
| start(_controller) { | ||
| controller = _controller; | ||
| }, | ||
| cancel() { | ||
| cp?.kill(); | ||
| cp = void 0; | ||
| closeStream(); | ||
| } | ||
| }); | ||
| function createChildProcess() { | ||
| const currentRun = ++runId; | ||
| let runErrored = false; | ||
| const cp = exec(executeOptions.command, executeOptions.args || [], { nodeOptions: { | ||
| env: { | ||
| COLORS: "true", | ||
| FORCE_COLOR: "true", | ||
| ...executeOptions.env || {} | ||
| }, | ||
| cwd: executeOptions.cwd ?? process.cwd(), | ||
| stdio: "pipe" | ||
| } }); | ||
| const stdoutChunks = []; | ||
| const stderrChunks = []; | ||
| let settled = false; | ||
| let resolveOutput; | ||
| const outputPromise = new Promise((resolve) => { | ||
| resolveOutput = resolve; | ||
| }); | ||
| const settle = (exitCode) => { | ||
| if (settled || currentRun !== runId) return; | ||
| settled = true; | ||
| resolveOutput({ | ||
| stdout: stdoutChunks.join(""), | ||
| stderr: stderrChunks.join(""), | ||
| exitCode | ||
| }); | ||
| }; | ||
| cp.process?.stdout?.on("data", (chunk) => { | ||
| if (currentRun !== runId) return; | ||
| const text = chunk.toString(); | ||
| stdoutChunks.push(text); | ||
| if (!streamClosed) controller?.enqueue(text); | ||
| }); | ||
| cp.process?.stderr?.on("data", (chunk) => { | ||
| if (currentRun !== runId) return; | ||
| const text = chunk.toString(); | ||
| stderrChunks.push(text); | ||
| if (!streamClosed) controller?.enqueue(text); | ||
| }); | ||
| cp.process?.once("error", (error) => { | ||
| if (currentRun !== runId) return; | ||
| runErrored = true; | ||
| settle(cp.process?.exitCode ?? void 0); | ||
| errorStream(error); | ||
| markStatus("error"); | ||
| }); | ||
| cp.process?.once("close", (code) => { | ||
| settle(code ?? void 0); | ||
| if (currentRun !== runId) return; | ||
| closeStream(); | ||
| if (!runErrored) markStatus(typeof code === "number" && code !== 0 ? "error" : "stopped"); | ||
| }); | ||
| currentResult = { | ||
| get pid() { | ||
| return cp.process?.pid; | ||
| }, | ||
| get exitCode() { | ||
| return cp.process?.exitCode ?? void 0; | ||
| }, | ||
| get killed() { | ||
| return cp.process?.killed === true; | ||
| }, | ||
| kill: (signal) => cp.kill(signal), | ||
| then: (onfulfilled, onrejected) => outputPromise.then(onfulfilled, onrejected) | ||
| }; | ||
| return cp; | ||
| } | ||
| cp = createChildProcess(); | ||
| const restart = async () => { | ||
| if (streamClosed) throw diagnostics.DF8206({ id: terminal.id }); | ||
| cp?.kill(); | ||
| cp = createChildProcess(); | ||
| markStatus("running"); | ||
| }; | ||
| const terminate = async () => { | ||
| cp?.kill(); | ||
| cp = void 0; | ||
| closeStream(); | ||
| markStatus("stopped"); | ||
| }; | ||
| session = { | ||
| ...terminal, | ||
| status: "running", | ||
| stream, | ||
| type: "child-process", | ||
| executeOptions, | ||
| getChildProcess: () => cp?.process, | ||
| getResult: () => currentResult, | ||
| terminate, | ||
| restart | ||
| }; | ||
| this.register(session); | ||
| return Promise.resolve(session); | ||
| } | ||
| async startPtySession(executeOptions, terminal) { | ||
| if (this.sessions.has(terminal.id)) throw diagnostics.DF8200({ id: terminal.id }); | ||
| const { spawn } = await import("zigpty"); | ||
| const cols = executeOptions.cols ?? 80; | ||
| const rows = executeOptions.rows ?? 24; | ||
| let controller; | ||
| let pty; | ||
| let runId = 0; | ||
| let streamClosed = false; | ||
| let session; | ||
| const markStatus = (next) => { | ||
| if (session.status === next) return; | ||
| session.status = next; | ||
| this.events.emit(HUB_EVENTS.bus.terminalsSessionUpdated, session); | ||
| }; | ||
| const closeStream = () => { | ||
| if (streamClosed) return; | ||
| streamClosed = true; | ||
| try { | ||
| controller?.close(); | ||
| } catch {} | ||
| }; | ||
| const errorStream = (error) => { | ||
| if (streamClosed) return; | ||
| streamClosed = true; | ||
| try { | ||
| controller?.error(error); | ||
| } catch {} | ||
| }; | ||
| const stream = new ReadableStream({ | ||
| start(_controller) { | ||
| controller = _controller; | ||
| }, | ||
| cancel() { | ||
| pty?.kill(); | ||
| pty = void 0; | ||
| closeStream(); | ||
| } | ||
| }); | ||
| const spawnPty = () => { | ||
| const currentRun = ++runId; | ||
| const proc = spawn(executeOptions.command, executeOptions.args ?? [], { | ||
| name: PTY_TERM_NAME, | ||
| cols, | ||
| rows, | ||
| cwd: executeOptions.cwd ?? process.cwd(), | ||
| env: { | ||
| ...process.env, | ||
| TERM: PTY_TERM_NAME, | ||
| COLORTERM: "truecolor", | ||
| FORCE_COLOR: "1", | ||
| ...executeOptions.env ?? {} | ||
| } | ||
| }); | ||
| proc.onData((data) => { | ||
| if (streamClosed || currentRun !== runId) return; | ||
| controller?.enqueue(typeof data === "string" ? data : data.toString("utf8")); | ||
| }); | ||
| proc.onExit(({ exitCode, signal }) => { | ||
| if (currentRun !== runId) return; | ||
| closeStream(); | ||
| markStatus(signal === 0 && exitCode !== 0 ? "error" : "stopped"); | ||
| }); | ||
| return proc; | ||
| }; | ||
| try { | ||
| pty = spawnPty(); | ||
| } catch (error) { | ||
| errorStream(error); | ||
| throw diagnostics.DF8203({ | ||
| command: executeOptions.command, | ||
| reason: error instanceof Error ? error.message : String(error) | ||
| }); | ||
| } | ||
| session = { | ||
| ...terminal, | ||
| status: "running", | ||
| interactive: true, | ||
| stream, | ||
| type: "pty", | ||
| executeOptions, | ||
| write: (data) => { | ||
| try { | ||
| pty?.write(data); | ||
| } catch {} | ||
| }, | ||
| resize: (nextCols, nextRows) => { | ||
| try { | ||
| pty?.resize(Math.max(1, nextCols), Math.max(1, nextRows)); | ||
| } catch {} | ||
| }, | ||
| getProcessName: () => { | ||
| try { | ||
| const name = pty?.process; | ||
| return name && name !== PTY_TERM_NAME ? name : void 0; | ||
| } catch { | ||
| return; | ||
| } | ||
| }, | ||
| terminate: async () => { | ||
| pty?.kill(); | ||
| pty = void 0; | ||
| closeStream(); | ||
| markStatus("stopped"); | ||
| }, | ||
| restart: async () => { | ||
| if (streamClosed) throw diagnostics.DF8206({ id: terminal.id }); | ||
| pty?.kill(); | ||
| pty = spawnPty(); | ||
| markStatus("running"); | ||
| } | ||
| }; | ||
| this.register(session); | ||
| return session; | ||
| } | ||
| }; | ||
| //#endregion | ||
| //#region src/node/install-devframe.ts | ||
| /** | ||
| * Find the next free dock id derived from `baseId`. Returns `baseId` | ||
| * when it is unused, otherwise appends `-2`, `-3`, … until a free slot | ||
| * is found. Used by the `'duplicate'` strategy so co-existing instances | ||
| * never collide in the dock registry. | ||
| */ | ||
| function nextAvailableDockId(views, baseId) { | ||
| if (!views.has(baseId)) return baseId; | ||
| let n = 2; | ||
| while (views.has(`${baseId}-${n}`)) n++; | ||
| return `${baseId}-${n}`; | ||
| } | ||
| /** | ||
| * Framework-neutral primitive backing {@link DevframeHubContext.install} — | ||
| * installs a {@link DevframeDefinition} as a dock inside a hub-aware context: | ||
| * serves the devframe's SPA at the resolved base path, synthesizes an iframe | ||
| * dock entry from the definition's metadata, and runs the definition's | ||
| * `setup(ctx)`. Reach for it through `ctx.install(devframe)` rather than | ||
| * calling it directly. | ||
| * | ||
| * Framework kits wrap `ctx.install` with their own plugin/middleware | ||
| * machinery — e.g. `@vitejs/devtools-kit`'s `createPluginFromDevframe` | ||
| * returns a Vite `Plugin` whose `devtools.setup` ultimately delegates here. | ||
| */ | ||
| /** | ||
| * Phase one of an install: run the duplication guard, serve the SPA + meta, | ||
| * register the iframe dock, and queue the definition's declarative wire | ||
| * services — everything up to (but not including) `setup(ctx)`. Returns a | ||
| * deferred setup thunk, or `null` when the devframe was deduplicated. | ||
| * | ||
| * The hub's initial batch uses this to collect every devframe's services | ||
| * across the whole hub, `ready()` them once, and only then run the setups — | ||
| * so services are ready before any setup, and a plugin can consume a service | ||
| * another plugin declared regardless of mount order. | ||
| */ | ||
| async function prepareDevframe(ctx, d, options = {}) { | ||
| const strategy = d.duplicationStrategy ?? "warn"; | ||
| const isDuplicate = ctx.docks.views.has(d.id); | ||
| if (isDuplicate && strategy !== "duplicate") { | ||
| if (strategy === "throw") throw diagnostics.DF8105({ | ||
| id: d.id, | ||
| name: d.name | ||
| }); | ||
| if (strategy === "warn") diagnostics.DF8105({ | ||
| id: d.id, | ||
| name: d.name | ||
| }); | ||
| return null; | ||
| } | ||
| const id = isDuplicate ? nextAvailableDockId(ctx.docks.views, d.id) : d.id; | ||
| const base = options.base ?? (id === d.id ? resolveBasePath(d, "hosted") : resolveBasePath({ | ||
| ...d, | ||
| id, | ||
| basePath: void 0 | ||
| }, "hosted")); | ||
| const clientAssets = resolveClientAssets(d); | ||
| if (clientAssets) { | ||
| if (ctx.host.mountConnectionMeta) await ctx.host.mountConnectionMeta(base); | ||
| else diagnostics.DF8106({ | ||
| id, | ||
| name: d.name, | ||
| base | ||
| }); | ||
| const distSource = clientAssets; | ||
| ctx.views.hostStatic(base, typeof distSource === "string" ? resolve(distSource) : distSource, d.importMetaUrl); | ||
| } | ||
| ctx.docks.register({ | ||
| id, | ||
| title: d.name, | ||
| icon: d.icon, | ||
| ...d.dock, | ||
| ...options.dock, | ||
| type: "iframe", | ||
| url: base | ||
| }); | ||
| for (const input of d.services ?? []) ctx.services.install(input, { resolveFrom: d.importMetaUrl }); | ||
| return () => Promise.resolve(d.setup(ctx)); | ||
| } | ||
| /** | ||
| * Install a {@link DevframeDefinition} into a hub in one call — serve its SPA, | ||
| * register its dock, ready its services, and run `setup(ctx)`. The imperative | ||
| * counterpart to the hub's declarative `devframes` list (which batches the | ||
| * phases via {@link prepareDevframe}); use it from `configure(ctx)` or | ||
| * wherever you hold the context to plug in an extra devframe after startup. | ||
| */ | ||
| async function installDevframe(ctx, d, options = {}) { | ||
| const run = await prepareDevframe(ctx, d, options); | ||
| if (!run) return; | ||
| await ctx.services.ready(); | ||
| await run(); | ||
| } | ||
| //#endregion | ||
| //#region src/node/rpc-builtins.ts | ||
| /** | ||
| * Resolve an interactive (PTY) terminal session by id, or throw. Sessions | ||
| * spawned via `startChildProcess` are output-only and are rejected here. | ||
| */ | ||
| function resolveInteractiveSession(sessions, id) { | ||
| const session = sessions.get(id); | ||
| if (!session) throw diagnostics.DF8201({ id }); | ||
| if (typeof session.write !== "function") throw diagnostics.DF8202({ id }); | ||
| return session; | ||
| } | ||
| /** | ||
| * Resolve a session that can be terminated/restarted (spawned via | ||
| * `startChildProcess` or `startPtySession`), or throw. Sessions added with a | ||
| * bare `register()` carry no lifecycle handle and are rejected. | ||
| */ | ||
| function resolveControllableSession(sessions, id) { | ||
| const session = sessions.get(id); | ||
| if (!session) throw diagnostics.DF8201({ id }); | ||
| if (typeof session.terminate !== "function") throw diagnostics.DF8204({ id }); | ||
| return session; | ||
| } | ||
| /** | ||
| * `hub:commands:execute` — Invoke a registered server command by id. The | ||
| * arguments after `id` are forwarded to the command's `handler(...)`. | ||
| * Returns whatever the handler returns. | ||
| * | ||
| * Pairs with the `devframe:commands` shared state: clients read the list | ||
| * from the shared state and dispatch by id via this RPC. | ||
| */ | ||
| const hubCommandsExecute = defineHubRpcFunction({ | ||
| name: HUB_EVENTS.rpc.commandsExecute, | ||
| type: "action", | ||
| setup: (context) => ({ async handler(id, ...args) { | ||
| return context.commands.execute(id, ...args); | ||
| } }) | ||
| }); | ||
| /** | ||
| * `hub:messages:add` — Add a message from a browser client into the hub's | ||
| * messages subsystem. Marked `from: 'browser'`. Returns the serializable | ||
| * entry (the mutation handle stays server-side). | ||
| * | ||
| * Pairs with the client-side {@link import('../client').createDevframeClientHost} | ||
| * context, whose `messages` client dispatches through these built-ins so a | ||
| * dock client script can report into the same feed the server writes to. | ||
| */ | ||
| const hubMessagesAdd = defineHubRpcFunction({ | ||
| name: HUB_EVENTS.rpc.messagesAdd, | ||
| type: "action", | ||
| jsonSerializable: true, | ||
| setup: (context) => ({ async handler(input) { | ||
| return (await context.messages.add({ | ||
| ...input, | ||
| from: "browser" | ||
| })).entry; | ||
| } }) | ||
| }); | ||
| /** `hub:messages:update` — Patch a message by id; returns the updated entry (or `undefined`). */ | ||
| const hubMessagesUpdate = defineHubRpcFunction({ | ||
| name: HUB_EVENTS.rpc.messagesUpdate, | ||
| type: "action", | ||
| jsonSerializable: true, | ||
| setup: (context) => ({ async handler(id, patch) { | ||
| return context.messages.update(id, patch); | ||
| } }) | ||
| }); | ||
| /** `hub:messages:remove` — Remove a message by id. */ | ||
| const hubMessagesRemove = defineHubRpcFunction({ | ||
| name: HUB_EVENTS.rpc.messagesRemove, | ||
| type: "action", | ||
| setup: (context) => ({ async handler(id) { | ||
| await context.messages.remove(id); | ||
| } }) | ||
| }); | ||
| /** `hub:messages:clear` — Remove every message. */ | ||
| const hubMessagesClear = defineHubRpcFunction({ | ||
| name: HUB_EVENTS.rpc.messagesClear, | ||
| type: "action", | ||
| setup: (context) => ({ async handler() { | ||
| await context.messages.clear(); | ||
| } }) | ||
| }); | ||
| /** | ||
| * `hub:terminals:write` — Send input to an interactive PTY session spawned | ||
| * via `ctx.terminals.startPtySession`. Lets a hub-aware terminal UI (e.g. the | ||
| * terminals plugin) drive a session owned by another plugin. | ||
| */ | ||
| const hubTerminalsWrite = defineHubRpcFunction({ | ||
| name: HUB_EVENTS.rpc.terminalsWrite, | ||
| type: "action", | ||
| setup: (context) => ({ async handler(id, data) { | ||
| resolveInteractiveSession(context.terminals.sessions, id).write(data); | ||
| } }) | ||
| }); | ||
| /** `hub:terminals:resize` — Resize an interactive PTY session by id. */ | ||
| const hubTerminalsResize = defineHubRpcFunction({ | ||
| name: HUB_EVENTS.rpc.terminalsResize, | ||
| type: "action", | ||
| setup: (context) => ({ async handler(id, cols, rows) { | ||
| resolveInteractiveSession(context.terminals.sessions, id).resize(cols, rows); | ||
| } }) | ||
| }); | ||
| /** | ||
| * `hub:terminals:terminate` — Kill a session's process while keeping the | ||
| * session registered (its output/scrollback stays). Works for both read-only | ||
| * child-process and interactive PTY sessions, letting a hub-aware terminal UI | ||
| * force-kill a session owned by another plugin. | ||
| */ | ||
| const hubTerminalsTerminate = defineHubRpcFunction({ | ||
| name: HUB_EVENTS.rpc.terminalsTerminate, | ||
| type: "action", | ||
| setup: (context) => ({ async handler(id) { | ||
| await resolveControllableSession(context.terminals.sessions, id).terminate(); | ||
| } }) | ||
| }); | ||
| /** | ||
| * `hub:terminals:restart` — Re-run a session's command in place. Rejected for | ||
| * sessions registered with `restartable: false`, whose lifecycle is owned | ||
| * elsewhere. | ||
| */ | ||
| const hubTerminalsRestart = defineHubRpcFunction({ | ||
| name: HUB_EVENTS.rpc.terminalsRestart, | ||
| type: "action", | ||
| setup: (context) => ({ async handler(id) { | ||
| const session = resolveControllableSession(context.terminals.sessions, id); | ||
| if (session.restartable === false) throw diagnostics.DF8205({ id }); | ||
| await session.restart(); | ||
| } }) | ||
| }); | ||
| /** | ||
| * `hub:terminals:remove` — Kill a session's process (when it still owns one) | ||
| * and drop it from the registry, disposing its output stream. Lets a hub-aware | ||
| * terminal UI discard a stopped aggregated session. | ||
| */ | ||
| const hubTerminalsRemove = defineHubRpcFunction({ | ||
| name: HUB_EVENTS.rpc.terminalsRemove, | ||
| type: "action", | ||
| setup: (context) => ({ async handler(id) { | ||
| const session = context.terminals.sessions.get(id); | ||
| if (!session) throw diagnostics.DF8201({ id }); | ||
| const controllable = session; | ||
| if (typeof controllable.terminate === "function") await controllable.terminate(); | ||
| context.terminals.remove(session); | ||
| } }) | ||
| }); | ||
| /** | ||
| * `hub:docks:activate` — Ask the active viewer to switch its focused dock to | ||
| * `dockId`, optionally carrying `params` for the target dock to interpret | ||
| * (e.g. `{ sessionId }` for the terminals dock to focus a session). | ||
| * | ||
| * Any connected client may call it, which is the point: a mounted devframe | ||
| * running in its own iframe (on its own RPC client) can steer the host shell's | ||
| * dock selection — client-local state it otherwise can't reach. The hub | ||
| * broadcasts the request live to connected clients (the host shell switches) | ||
| * and mirrors it into the `devframe:docks:active` shared state (a dock that | ||
| * mounts in response still converges on it). | ||
| */ | ||
| const hubDocksActivate = defineHubRpcFunction({ | ||
| name: HUB_EVENTS.rpc.docksActivate, | ||
| type: "action", | ||
| setup: (context) => ({ async handler(input) { | ||
| context.docks.activate(input.dockId, input.params); | ||
| } }) | ||
| }); | ||
| /** | ||
| * Framework-neutral RPC declarations auto-registered by | ||
| * {@link createHubContext}. Provide additional RPCs by passing your own | ||
| * array via `CreateHubContextOptions.builtinRpcDeclarations`; the hub's | ||
| * list is prepended automatically. | ||
| */ | ||
| const builtinHubRpcDeclarations = [ | ||
| hubCommandsExecute, | ||
| hubDocksActivate, | ||
| hubMessagesAdd, | ||
| hubMessagesUpdate, | ||
| hubMessagesRemove, | ||
| hubMessagesClear, | ||
| hubTerminalsWrite, | ||
| hubTerminalsResize, | ||
| hubTerminalsTerminate, | ||
| hubTerminalsRestart, | ||
| hubTerminalsRemove | ||
| ]; | ||
| //#endregion | ||
| //#region src/node/context.ts | ||
| /** | ||
| * Create a hub-level node context: wraps devframe's `createHostContext`, | ||
| * attaches the hub hosts (`docks`, `terminals`, `messages`, `commands`), | ||
| * registers the hub's built-in RPC commands, and wires the shared-state | ||
| * synchronization that powers a hub-aware client UI. | ||
| */ | ||
| async function createHubContext(options) { | ||
| const context = await createHostContext({ | ||
| ...options, | ||
| builtinRpcDeclarations: [...builtinHubRpcDeclarations, ...options.builtinRpcDeclarations ?? []] | ||
| }); | ||
| const docks = new DevframeDocksHost(context); | ||
| const terminals = new DevframeTerminalsHost(context); | ||
| const messages = new DevframeMessagesHost(context); | ||
| const commands = new DevframeCommandsHost(context); | ||
| context.docks = docks; | ||
| context.terminals = terminals; | ||
| context.messages = messages; | ||
| context.commands = commands; | ||
| context.install = (devframe, options) => installDevframe(context, devframe, options); | ||
| await docks.init(); | ||
| const debounceMs = options.mode === "build" ? 0 : 10; | ||
| const docksSharedState = await context.rpc.sharedState.get(HUB_EVENTS.sharedState.docks, { initialValue: [] }); | ||
| const refreshDocks = debounce(() => { | ||
| docksSharedState.mutate(() => docks.values()); | ||
| }, debounceMs); | ||
| docks.events.on(HUB_EVENTS.bus.docksEntryUpdated, refreshDocks); | ||
| getInternalContext(context).onWsEndpointChange(refreshDocks); | ||
| docksSharedState.mutate(() => docks.values()); | ||
| const activeDockSharedState = await context.rpc.sharedState.get(HUB_EVENTS.sharedState.docksActive, { initialValue: { activation: null } }); | ||
| docks.events.on(HUB_EVENTS.bus.docksActivate, (activation) => { | ||
| activeDockSharedState.mutate((state) => { | ||
| state.activation = activation; | ||
| }); | ||
| context.rpc.broadcast({ | ||
| method: HUB_EVENTS.broadcast.docksActivate, | ||
| args: [activation] | ||
| }); | ||
| }); | ||
| const broadcastTerminals = debounce(() => { | ||
| context.rpc.broadcast({ | ||
| method: HUB_EVENTS.broadcast.terminalsUpdated, | ||
| args: [] | ||
| }); | ||
| docksSharedState.mutate(() => docks.values()); | ||
| }, debounceMs); | ||
| terminals.events.on(HUB_EVENTS.bus.terminalsSessionUpdated, broadcastTerminals); | ||
| const broadcastMessages = debounce(() => { | ||
| context.rpc.broadcast({ | ||
| method: HUB_EVENTS.broadcast.messagesUpdated, | ||
| args: [] | ||
| }); | ||
| docksSharedState.mutate(() => docks.values()); | ||
| }, debounceMs); | ||
| messages.events.on(HUB_EVENTS.bus.messagesAdded, broadcastMessages); | ||
| messages.events.on(HUB_EVENTS.bus.messagesUpdated, broadcastMessages); | ||
| messages.events.on(HUB_EVENTS.bus.messagesRemoved, broadcastMessages); | ||
| messages.events.on(HUB_EVENTS.bus.messagesCleared, broadcastMessages); | ||
| const commandsSharedState = await context.rpc.sharedState.get(HUB_EVENTS.sharedState.commands, { initialValue: [] }); | ||
| const syncCommands = debounce(() => { | ||
| commandsSharedState.mutate(() => commands.list()); | ||
| }, debounceMs); | ||
| commands.events.on(HUB_EVENTS.bus.commandsRegistered, syncCommands); | ||
| commands.events.on(HUB_EVENTS.bus.commandsUnregistered, syncCommands); | ||
| commandsSharedState.mutate(() => commands.list()); | ||
| return context; | ||
| } | ||
| //#endregion | ||
| export { DevframeDocksHost as _, hubMessagesAdd as a, hubMessagesUpdate as c, hubTerminalsRestart as d, hubTerminalsTerminate as f, DevframeMessagesHost as g, DevframeTerminalsHost as h, hubDocksActivate as i, hubTerminalsRemove as l, prepareDevframe as m, builtinHubRpcDeclarations as n, hubMessagesClear as o, hubTerminalsWrite as p, hubCommandsExecute as r, hubMessagesRemove as s, createHubContext as t, hubTerminalsResize as u, DevframeCommandsHost as v, diagnostics as y }; |
| import "./context-BVkxwz5k.mjs"; | ||
| import "./settings-D7whfcx2.mjs"; | ||
| import { RpcDefinitionsFilter, RpcDefinitionsToFunctions } from "devframe/rpc"; | ||
| import { ConnectionMeta as ConnectionMeta$1, DevframeCapabilities, DevframeDiagnosticsHost, DevframeDiagnosticsLogger, DevframeHost as DevframeHost$1, DevframeNodeRpcSession, DevframeRpcClientFunctions, DevframeRpcServerFunctions, DevframeRpcSharedStates, DevframeViewHost, EventEmitter as EventEmitter$1, EventUnsubscribe, EventsMap, RpcBroadcastOptions, RpcFunctionsHost, RpcSharedStateGetOptions, RpcSharedStateHost, RpcStreamingChannel, RpcStreamingChannelOptions, RpcStreamingHost } from "devframe/types"; | ||
| export { RpcStreamingHost as S, RpcFunctionsHost as _, DevframeHost$1 as a, RpcStreamingChannel as b, DevframeRpcServerFunctions as c, EventEmitter$1 as d, EventUnsubscribe as f, RpcDefinitionsToFunctions as g, RpcDefinitionsFilter as h, DevframeDiagnosticsLogger as i, DevframeRpcSharedStates as l, RpcBroadcastOptions as m, DevframeCapabilities as n, DevframeNodeRpcSession as o, EventsMap as p, DevframeDiagnosticsHost as r, DevframeRpcClientFunctions as s, ConnectionMeta$1 as t, DevframeViewHost as u, RpcSharedStateGetOptions as v, RpcStreamingChannelOptions as x, RpcSharedStateHost as y }; |
Sorry, the diff of this file is too big to display
AI-detected potential code anomaly
Supply chain riskAI has identified unusual behaviors that may pose a security risk.
URL strings
Supply chain riskPackage contains fragments of external URLs or IP addresses, which the package may be accessing at runtime.
AI-detected potential security risk
Supply chain riskAI has determined that this package may contain potential security issues or vulnerabilities.
URL strings
Supply chain riskPackage contains fragments of external URLs or IP addresses, which the package may be accessing at runtime.
433791
0.22%8512
0.04%3
-25%15
7.14%