@devframes/hub
Advanced tools
| import { ConnectionMeta, EventEmitter } from "devframe/types"; | ||
| import { StandardSchemaV1 } from "@standard-schema/spec"; | ||
| //#region src/types/docks.d.ts | ||
| interface DevframeDocksHost { | ||
| readonly views: Map<string, DevframeDockUserEntry>; | ||
| readonly events: EventEmitter<{ | ||
| 'dock:entry:updated': (entry: DevframeDockUserEntry) => void; | ||
| 'dock:activate': (activation: DevframeDockActivation) => void; | ||
| }>; | ||
| register: <T extends DevframeDockUserEntry>(entry: T, force?: boolean) => { | ||
| update: (patch: Partial<T>) => void; | ||
| }; | ||
| update: (entry: DevframeDockUserEntry) => void; | ||
| values: () => DevframeDockEntry[]; | ||
| /** | ||
| * Request the active viewer switch its focused dock to `dockId`, optionally | ||
| * carrying `params` for the target dock to interpret (e.g. a terminals | ||
| * session id). | ||
| * | ||
| * Any connected client may drive this via the `hub:docks:activate` RPC — a | ||
| * mounted devframe running in its own iframe can steer the host shell's dock | ||
| * selection, which is otherwise client-local. The request is delivered live | ||
| * to connected clients (broadcast) and mirrored into the | ||
| * `devframe:docks:active` shared state so a dock that mounts in response | ||
| * still sees it. Activation is best-effort: unknown dock ids degrade | ||
| * gracefully. | ||
| */ | ||
| activate: (dockId: string, params?: Record<string, unknown>) => void; | ||
| } | ||
| /** | ||
| * A request to switch the active dock. `params` is an opaque, serializable | ||
| * bag the target dock interprets — the terminals dock reads `params.sessionId` | ||
| * to focus a specific session. | ||
| */ | ||
| interface DevframeDockActivation { | ||
| dockId: string; | ||
| params?: Record<string, unknown>; | ||
| } | ||
| /** | ||
| * Shape of the `devframe:docks:active` shared-state slot — the most recent | ||
| * {@link DevframeDockActivation}, or `null` before any activation. Mirrored | ||
| * so a dock that mounts in response to an activation can still converge on the | ||
| * request instead of missing the live broadcast. | ||
| */ | ||
| interface DevframeDocksActiveState { | ||
| activation: DevframeDockActivation | null; | ||
| } | ||
| type DevframeDockEntryCategory = 'framework' | 'app' | 'ui' | 'data' | 'web' | 'performance' | 'advanced' | 'docs' | 'default' | '~builtin' | (string & {}); | ||
| type DevframeDockEntryIcon = string | { | ||
| light: string; | ||
| dark: string; | ||
| }; | ||
| interface DevframeDockEntryBase { | ||
| id: string; | ||
| title: string; | ||
| icon: DevframeDockEntryIcon; | ||
| /** | ||
| * The default order of the entry in the dock. | ||
| * The higher the number the earlier it appears. | ||
| * @default 0 | ||
| */ | ||
| defaultOrder?: number; | ||
| /** | ||
| * The category of the entry — a field with a dual role that depends on | ||
| * whether {@link groupId} resolves to a registered {@link DevframeViewGroup}: | ||
| * | ||
| * - **Ungrouped (or orphan) entry** — `category` is the entry's OUTER bucket | ||
| * on the dock bar, ordered by {@link import('../constants').DEFAULT_CATEGORIES_ORDER}. | ||
| * - **Grouped entry** (a `groupId` that resolves to a registered group) — | ||
| * the OUTER bucket is instead the group's own `category`, and this field is | ||
| * reinterpreted as the entry's IN-GROUP sub-category, used to sub-divide and | ||
| * sort members inside the group's popover / sub-navigation. | ||
| * | ||
| * Falls back to `'default'` when omitted — both as an outer bucket and, for a | ||
| * grouped member, as its in-group sub-bucket. | ||
| * | ||
| * @default 'default' | ||
| */ | ||
| category?: DevframeDockEntryCategory; | ||
| /** | ||
| * Conditional visibility expression. | ||
| * When set, the dock entry is only visible when the expression evaluates to true. | ||
| * Uses the same syntax as command `when` clauses. | ||
| * | ||
| * Set to `'false'` to unconditionally hide the entry. | ||
| * | ||
| * @example 'clientType == embedded' | ||
| * @see {@link import('devframe/utils/when').evaluateWhen} | ||
| */ | ||
| when?: string; | ||
| /** | ||
| * Render-only conditional visibility expression, same syntax as {@link when}. | ||
| * When it evaluates to `false`, a viewer omits the entry from the rendered | ||
| * dock bar / list, but the entry stays registered and fully reachable — | ||
| * `docks.activate()`/`switchEntry()` by id, RPC lookups, and anything else | ||
| * that walks the raw entry list (e.g. the {@link DevframeViewIframe.subTabs} | ||
| * frame-nav adapter) keep working exactly as if it were visible. | ||
| * | ||
| * Use this instead of {@link when} when an entry must remain part of the | ||
| * model without a dock-bar button of its own — the canonical case is a | ||
| * shared-frame {@link DevframeViewIframe.subTabs anchor}: set | ||
| * `visibility: 'false'` on the anchor so only its synthesized member tabs | ||
| * render, while the anchor itself keeps driving the postMessage nav loop. | ||
| * `when`, by contrast, is the general relevance switch for the entry as a | ||
| * whole; reach for `visibility` only for this render-only carve-out. | ||
| * | ||
| * Set to `'false'` to unconditionally hide the entry's own dock-bar button. | ||
| * | ||
| * @example 'false' | ||
| * @see {@link import('devframe/utils/when').evaluateWhen} | ||
| */ | ||
| visibility?: string; | ||
| /** | ||
| * Badge text to display on the dock icon (e.g., unread count) | ||
| */ | ||
| badge?: string; | ||
| /** | ||
| * Id of the group this entry belongs to. When set, hosts collapse this entry | ||
| * under the matching group's button instead of showing it directly on the | ||
| * dock bar. | ||
| * | ||
| * This is a flat pointer — membership, not containment. The entry stays an | ||
| * independently-registered, top-level entry; only its rendering is grouped | ||
| * downstream. | ||
| * | ||
| * When the referenced group **is** registered, it supplies the entry's OUTER | ||
| * dock-bar category (the group's own {@link category}), and this entry's own | ||
| * {@link category} is reinterpreted as its IN-GROUP sub-category. When the | ||
| * referenced group is **never** registered, the entry renders as a normal | ||
| * top-level entry and falls back to using its own {@link category} as the | ||
| * outer bucket (orphan tolerance). | ||
| * | ||
| * @see {@link DevframeViewGroup} | ||
| */ | ||
| groupId?: string; | ||
| } | ||
| interface ClientScriptEntry { | ||
| /** | ||
| * The filepath or module name to import from | ||
| */ | ||
| importFrom: string; | ||
| /** | ||
| * The name to import the module as | ||
| * | ||
| * @default 'default' | ||
| */ | ||
| importName?: string; | ||
| } | ||
| interface DevframeViewIframe extends DevframeDockEntryBase { | ||
| type: 'iframe'; | ||
| url: string; | ||
| /** | ||
| * The id of the iframe, if multiple tabs is assigned with the same id, the iframe will be shared. | ||
| * | ||
| * When not provided, it would be treated as a unique frame. | ||
| * | ||
| * `frameId` is an axis independent of {@link DevframeDockEntryBase.groupId}: | ||
| * it decides *which* iframe element a dock renders into (and which soft-nav | ||
| * pool it joins), while `groupId` only affects dock-bar grouping. Docks that | ||
| * share a `frameId` may live in one group, several groups, or none. | ||
| */ | ||
| frameId?: string; | ||
| /** | ||
| * Optional client script to import into the iframe | ||
| */ | ||
| clientScript?: ClientScriptEntry; | ||
| /** | ||
| * Soft-navigation target within a shared frame. Set on a **member** dock | ||
| * (one of several docks sharing a {@link frameId}) to describe which internal | ||
| * view the embedded app should show. The hub treats {@link NavTarget.path} as | ||
| * opaque and hands it to the frame's nav shim over `postMessage`; switching to | ||
| * this dock performs client-side navigation instead of reloading the iframe. | ||
| * | ||
| * The anchor dock (the one flagged with {@link subTabs}) leaves this unset. | ||
| */ | ||
| navTarget?: NavTarget; | ||
| /** | ||
| * Marks this iframe as a **shared-frame anchor** whose sub-tabs are discovered | ||
| * at runtime over a host↔iframe `postMessage` protocol. The client host | ||
| * auto-attaches the frame-nav adapter when this iframe mounts: it runs the | ||
| * ready handshake, materializes one client-only member dock per reported tab | ||
| * (grouped/soft-navigated via this anchor's {@link frameId}), and drives the | ||
| * live navigation loop. Absent a shim, the anchor simply renders as a single | ||
| * plain iframe dock. | ||
| * | ||
| * Set {@link DevframeDockEntryBase.visibility} to `'false'` on the anchor to | ||
| * hide its own dock-bar button once tabs are discovered, surfacing only the | ||
| * synthesized member docks while the anchor keeps driving the nav loop. | ||
| */ | ||
| subTabs?: FrameSubTabsConfig; | ||
| /** | ||
| * Enable remote-UI mode: the hub injects a connection descriptor | ||
| * (WS URL + pre-approved auth token) into the iframe URL so a hosted | ||
| * page can connect back via `connectRemoteDevframe()` from | ||
| * `@devframes/hub/client` — without needing to ship a dist with the | ||
| * plugin. | ||
| * | ||
| * Requires dev mode (no effect in build mode — no WS server exists). | ||
| * When enabled, the dock is automatically hidden in build mode unless | ||
| * the author provides an explicit `when` clause. | ||
| */ | ||
| remote?: boolean | RemoteDockOptions; | ||
| } | ||
| /** | ||
| * A structured, soft-navigation target within a shared frame. `path` is opaque | ||
| * to the hub — the embedded app maps it onto its own router. | ||
| * | ||
| * Kept to `path` + `query` so the shape survives shared-state's `Immutable` | ||
| * projection cleanly (a `DevframeViewIframe` must still narrow back from its | ||
| * immutable form). An `unknown`/recursive history-`state` field breaks that | ||
| * round-trip, so richer per-navigation state is intentionally out of scope for | ||
| * now — carry it in `query` or the app's own store. | ||
| */ | ||
| interface NavTarget { | ||
| path: string; | ||
| query?: Record<string, string | readonly string[]>; | ||
| } | ||
| /** | ||
| * Configuration for a {@link DevframeViewIframe.subTabs shared-frame anchor}. | ||
| */ | ||
| interface FrameSubTabsConfig { | ||
| /** Transport for tab discovery + the live nav loop. */ | ||
| protocol: 'postmessage'; | ||
| /** | ||
| * How long (ms) the adapter waits for the shim's `ready` before treating the | ||
| * frame as having no shim (the anchor renders as a single plain iframe dock, | ||
| * and a navigation requested before readiness hard-navigates). | ||
| * | ||
| * @default 3000 | ||
| */ | ||
| handshakeTimeoutMs?: number; | ||
| } | ||
| interface RemoteDockOptions { | ||
| /** | ||
| * How to pass the connection descriptor to the hosted page. | ||
| * | ||
| * - `'fragment'` (default): appended as a URL fragment. | ||
| * Not sent in HTTP requests or Referer headers — safest for auth tokens. | ||
| * - `'query'`: appended as a URL query parameter. Use when your hosting | ||
| * platform rewrites fragments or your SPA router repurposes the fragment | ||
| * for navigation. The token will appear in server access logs and | ||
| * outbound Referer headers. | ||
| * | ||
| * @default 'fragment' | ||
| */ | ||
| transport?: 'fragment' | 'query'; | ||
| /** | ||
| * Reject WS handshakes whose `Origin` header doesn't match the dock URL | ||
| * origin. Turn off when the same hosted app is served from multiple | ||
| * origins (e.g. preview deploys). | ||
| * | ||
| * @default true | ||
| */ | ||
| originLock?: boolean; | ||
| } | ||
| interface RemoteConnectionInfo extends ConnectionMeta { | ||
| backend: 'websocket'; | ||
| websocket: string; | ||
| v: 1; | ||
| authToken: string; | ||
| origin: string; | ||
| } | ||
| type DevframeViewLauncherStatus = 'idle' | 'loading' | 'success' | 'error'; | ||
| interface DevframeViewLauncher extends DevframeDockEntryBase { | ||
| type: 'launcher'; | ||
| launcher: { | ||
| icon?: DevframeDockEntryIcon; | ||
| title: string; | ||
| status?: DevframeViewLauncherStatus; | ||
| error?: string; | ||
| description?: string; | ||
| buttonStart?: string; | ||
| buttonLoading?: string; | ||
| /** | ||
| * Bound command id: the launch button, command palette entry, and any | ||
| * keybinding all resolve to this one handler. A viewer running out of | ||
| * process dispatches it over the `hub:commands:execute` RPC — the | ||
| * serializable path {@link onLaunch} can't cross, since a function is | ||
| * dropped when the entry is projected into the `devframe:docks` shared | ||
| * state. Register the command (with its handler) via `ctx.commands`. | ||
| */ | ||
| command?: string; | ||
| /** | ||
| * Id of the terminal session this launcher tracks (e.g. the one returned | ||
| * by `ctx.terminals.startChildProcess`). A viewer surfaces a first-class | ||
| * "view in terminal" action that calls `hub:docks:activate` with the | ||
| * terminals dock id and `{ sessionId: terminalSessionId }`, jumping the | ||
| * user straight to the running process. | ||
| */ | ||
| terminalSessionId?: string; | ||
| /** | ||
| * Latest single line of progress for inline display beneath the launcher | ||
| * (e.g. the tail of the tracked session's output). Author-set: the owner | ||
| * patches it via `docks.update()` as the process reports progress. | ||
| */ | ||
| digest?: string; | ||
| /** | ||
| * In-process launch handler. Optional: a same-process host can invoke it | ||
| * directly, but it does not survive projection into shared state, so an | ||
| * out-of-process viewer relies on {@link command} instead. Provide one or | ||
| * both. | ||
| */ | ||
| onLaunch?: () => Promise<void>; | ||
| }; | ||
| } | ||
| interface DevframeViewAction extends DevframeDockEntryBase { | ||
| type: 'action'; | ||
| action: ClientScriptEntry; | ||
| } | ||
| interface DevframeViewCustomRender extends DevframeDockEntryBase { | ||
| type: 'custom-render'; | ||
| renderer: ClientScriptEntry; | ||
| } | ||
| /** | ||
| * A view rendered natively by the viewer rather than by a plugin — the | ||
| * settings panel, the terminals feed, the messages feed, etc. A high-level | ||
| * integration registers the built-in views it wants; the viewer recognizes the | ||
| * reserved `id` and renders its own UI for it. | ||
| * | ||
| * Its {@link DevframeDockEntryBase.category} defaults to `'~builtin'` when | ||
| * omitted, so built-in views group together and sort last without every | ||
| * integration repeating it. | ||
| */ | ||
| interface DevframeViewBuiltin extends DevframeDockEntryBase { | ||
| type: '~builtin'; | ||
| id: string; | ||
| } | ||
| /** | ||
| * A dock group: a single dock-bar button that collapses every entry whose | ||
| * {@link DevframeDockEntryBase.groupId} matches this group's `id`. | ||
| * | ||
| * A group carries its own `title`/`icon`/`category`/`defaultOrder`/`when` | ||
| * (inherited from {@link DevframeDockEntryBase}) and has no view payload of its | ||
| * own — hosts render its members in a popover / sub-navigation. It flows | ||
| * through the same `register`/`update`/`values` machinery as every other entry, | ||
| * keyed by `id`. | ||
| * | ||
| * The group's `category` is the OUTER bucket for the group button itself AND | ||
| * for every one of its members — a member's own `category` no longer decides | ||
| * its outer bucket, but is reinterpreted as an in-group sub-category that | ||
| * sub-divides and sorts members inside this group. A group with no `category` | ||
| * buckets itself and its members under `'default'`. | ||
| * | ||
| * Grouping is one level deep: a group entry must not itself set `groupId`. | ||
| */ | ||
| interface DevframeViewGroup extends DevframeDockEntryBase { | ||
| type: 'group'; | ||
| /** | ||
| * Member id auto-opened when the group button is activated. When unset, | ||
| * activating the group only reveals its members (popover-only); no view | ||
| * opens until a member is chosen. | ||
| */ | ||
| defaultChildId?: string; | ||
| /** | ||
| * Per-group override of the in-group sub-category ordering — a map of | ||
| * sub-category id → ordering weight (lower sorts earlier), mirroring the | ||
| * shape of {@link import('../constants').DEFAULT_CATEGORIES_ORDER}. | ||
| * | ||
| * A member's own {@link DevframeDockEntryBase.category} is reinterpreted as | ||
| * its IN-GROUP sub-category, and members are sub-divided and sorted by those | ||
| * sub-categories. By default that sort follows the hub-wide | ||
| * `DEFAULT_CATEGORIES_ORDER`; set this to reorder the sub-categories **inside | ||
| * this group only**, leaving the outer dock-bar ordering (and every other | ||
| * group) untouched. | ||
| * | ||
| * Keys are merged over the defaults, so you only list the sub-categories you | ||
| * want to move; any sub-category absent from the map keeps its default weight | ||
| * (falling back to `0`). | ||
| * | ||
| * @example | ||
| * ```ts | ||
| * // In the "nuxt" group, surface `app` tools before `framework` internals. | ||
| * { type: 'group', id: 'nuxt', categoryOrder: { app: -200 } } | ||
| * ``` | ||
| */ | ||
| categoryOrder?: Record<string, number>; | ||
| /** | ||
| * Optional accent color for the group button. When set, the viewer may use | ||
| * it to style the group button and/or its popover. When unset, the viewer | ||
| * falls back to its own default styling. | ||
| */ | ||
| accentColor?: string; | ||
| } | ||
| /** | ||
| * The **open** registry of dock entry variants, keyed by their `type` | ||
| * discriminator. The hub ships the framework-neutral built-ins; opt-in | ||
| * integrations contribute their own variants through declaration merging — | ||
| * e.g. `@devframes/json-render/hub` adds a `'json-render'` entry. The hub | ||
| * itself stays agnostic: it hard-codes no integration-specific variant. | ||
| * | ||
| * @example | ||
| * ```ts | ||
| * // in an opt-in integration package | ||
| * declare module '@devframes/hub/types' { | ||
| * interface DevframeDockEntryRegistry { | ||
| * 'my-view': MyDockEntry | ||
| * } | ||
| * } | ||
| * ``` | ||
| */ | ||
| interface DevframeDockEntryRegistry { | ||
| 'iframe': DevframeViewIframe; | ||
| 'action': DevframeViewAction; | ||
| 'custom-render': DevframeViewCustomRender; | ||
| 'launcher': DevframeViewLauncher; | ||
| 'group': DevframeViewGroup; | ||
| '~builtin': DevframeViewBuiltin; | ||
| } | ||
| type DevframeDockUserEntry = DevframeDockEntryRegistry[keyof DevframeDockEntryRegistry]; | ||
| type DevframeDockEntry = DevframeDockUserEntry; | ||
| type DevframeDockEntriesGrouped = [category: string, entries: DevframeDockEntry[]][]; | ||
| //#endregion | ||
| //#region src/types/commands.d.ts | ||
| interface DevframeCommandKeybinding { | ||
| /** | ||
| * Keyboard shortcut string. | ||
| * Use "Mod" for platform-aware modifier (Cmd on macOS, Ctrl elsewhere). | ||
| * Examples: "Mod+K", "Mod+Shift+P", "Alt+N" | ||
| */ | ||
| key: string; | ||
| } | ||
| interface DevframeCommandBase { | ||
| /** | ||
| * Unique namespaced ID, e.g. "vite:open-in-editor" | ||
| */ | ||
| id: string; | ||
| title: string; | ||
| description?: string; | ||
| /** | ||
| * Icon for the command. Either an Iconify icon string (e.g. "ph:pencil-duotone") | ||
| * or a theme-specific pair `{ light, dark }` — the same shape as dock icons. | ||
| */ | ||
| icon?: DevframeDockEntryIcon; | ||
| category?: string; | ||
| /** | ||
| * Whether to show in command palette. Default: true | ||
| * | ||
| * - `true` — show the command and flatten its children into search results | ||
| * - `false` — hide the command entirely from the palette | ||
| * - `'without-children'` — show the command but don't flatten children into top-level search (children are still accessible via drill-down) | ||
| */ | ||
| showInPalette?: boolean | 'without-children'; | ||
| /** | ||
| * Optional context expression for conditional visibility. | ||
| * When set, the command is only shown in the palette and only executable | ||
| * when the expression evaluates to true. | ||
| */ | ||
| when?: string; | ||
| /** | ||
| * Default keyboard shortcut(s) for this command | ||
| */ | ||
| keybindings?: DevframeCommandKeybinding[]; | ||
| } | ||
| /** | ||
| * Opt-in agent exposure for a server command — mirrors the `agent` field on | ||
| * `defineRpcFunction`. A command carrying this field (and a `handler`) is | ||
| * projected into `ctx.agent` as a callable tool, reaching MCP clients through | ||
| * the devframe MCP adapter. | ||
| * | ||
| * `when` clauses are evaluated client-side only and are **not** enforced for | ||
| * agent calls — opt in a `when`-gated command only if running it outside its | ||
| * UI context is safe. | ||
| * | ||
| * @experimental The agent-native surface is experimental and may change | ||
| * without a major version bump until it stabilizes. | ||
| */ | ||
| interface DevframeCommandAgentOptions { | ||
| /** | ||
| * Description shown to the agent. Write it as a prompt: state when to call | ||
| * the command, not just what it does. | ||
| */ | ||
| description: string; | ||
| /** Display title (falls back to the command's `title`). */ | ||
| title?: string; | ||
| /** | ||
| * Safety classification — drives MCP hint annotations. | ||
| * @default 'action' | ||
| */ | ||
| safety?: 'read' | 'action' | 'destructive'; | ||
| /** Free-form tags for grouping/filtering. */ | ||
| tags?: readonly string[]; | ||
| /** | ||
| * Positional [Standard Schema](https://standardschema.dev/) validators for | ||
| * the handler's arguments — the same shape RPC definitions carry (valibot, | ||
| * zod, arktype, devframe's built-in `s` builder, …). Each is advertised | ||
| * under `arg0` / `arg1` / … on the tool's JSON-Schema input. Omitted: the | ||
| * tool takes no arguments. | ||
| */ | ||
| args?: readonly StandardSchemaV1[]; | ||
| } | ||
| /** | ||
| * Server command input — what plugins pass to `ctx.commands.register()`. | ||
| */ | ||
| interface DevframeServerCommandInput extends DevframeCommandBase { | ||
| /** | ||
| * Handler for this command. Optional if the command only serves as a group for children. | ||
| */ | ||
| handler?: (...args: any[]) => any | Promise<any>; | ||
| /** | ||
| * Opt this command in to the agent surface (`ctx.agent` → MCP). Requires a | ||
| * `handler`. See {@link DevframeCommandAgentOptions}. | ||
| * | ||
| * @experimental | ||
| */ | ||
| agent?: DevframeCommandAgentOptions; | ||
| /** | ||
| * Static sub-commands. Two levels max (parent → children). | ||
| * Each child must have a globally unique `id`. | ||
| */ | ||
| children?: DevframeServerCommandInput[]; | ||
| } | ||
| /** | ||
| * Serializable server command entry — sent over RPC (no handler). | ||
| */ | ||
| interface DevframeServerCommandEntry extends DevframeCommandBase { | ||
| source: 'server'; | ||
| children?: DevframeServerCommandEntry[]; | ||
| } | ||
| /** | ||
| * Client command — registered in the webcomponent context. | ||
| */ | ||
| interface DevframeClientCommand extends DevframeCommandBase { | ||
| source: 'client'; | ||
| /** | ||
| * Action for this command. Optional if the command only serves as a group for children. | ||
| * Return sub-commands for dynamic nested palette menus (runtime submenus). | ||
| */ | ||
| action?: (...args: any[]) => void | DevframeClientCommand[] | Promise<void | DevframeClientCommand[]>; | ||
| /** | ||
| * Static sub-commands. Two levels max (parent → children). | ||
| */ | ||
| children?: DevframeClientCommand[]; | ||
| } | ||
| /** | ||
| * Union of command entries visible in the palette. | ||
| */ | ||
| type DevframeCommandEntry = DevframeServerCommandEntry | DevframeClientCommand; | ||
| interface DevframeCommandHandle { | ||
| readonly id: string; | ||
| update: (patch: Partial<Omit<DevframeServerCommandInput, 'id'>>) => void; | ||
| unregister: () => void; | ||
| } | ||
| interface DevframeCommandsHostEvents { | ||
| 'command:registered': (command: DevframeServerCommandEntry) => void; | ||
| 'command:unregistered': (id: string) => void; | ||
| } | ||
| interface DevframeCommandsHost { | ||
| readonly commands: Map<string, DevframeServerCommandInput>; | ||
| readonly events: EventEmitter<DevframeCommandsHostEvents>; | ||
| /** | ||
| * Register a command (with optional children). | ||
| */ | ||
| register: (command: DevframeServerCommandInput) => DevframeCommandHandle; | ||
| /** | ||
| * Unregister a command by ID (removes parent and all children). | ||
| */ | ||
| unregister: (id: string) => boolean; | ||
| /** | ||
| * Execute a command by ID. Searches top-level and children. | ||
| * Throws if not found or if command has no handler. | ||
| */ | ||
| execute: (id: string, ...args: any[]) => Promise<unknown>; | ||
| /** | ||
| * Returns serializable list (no handlers), preserving tree structure. | ||
| */ | ||
| list: () => DevframeServerCommandEntry[]; | ||
| } | ||
| interface DevframeCommandShortcutOverrides { | ||
| /** | ||
| * Command ID → keybinding overrides. Empty array = shortcut disabled. | ||
| */ | ||
| [commandId: string]: DevframeCommandKeybinding[]; | ||
| } | ||
| //#endregion | ||
| export { FrameSubTabsConfig as A, DevframeViewAction as C, DevframeViewIframe as D, DevframeViewGroup as E, RemoteConnectionInfo as M, RemoteDockOptions as N, DevframeViewLauncher as O, DevframeDocksHost as S, DevframeViewCustomRender as T, DevframeDockEntryCategory as _, DevframeCommandHandle as a, DevframeDockUserEntry as b, DevframeCommandsHost as c, DevframeServerCommandInput as d, ClientScriptEntry as f, DevframeDockEntryBase as g, DevframeDockEntry as h, DevframeCommandEntry as i, NavTarget as j, DevframeViewLauncherStatus as k, DevframeCommandsHostEvents as l, DevframeDockEntriesGrouped as m, DevframeCommandAgentOptions as n, DevframeCommandKeybinding as o, DevframeDockActivation as p, DevframeCommandBase as r, DevframeCommandShortcutOverrides as s, DevframeClientCommand as t, DevframeServerCommandEntry as u, DevframeDockEntryIcon as v, DevframeViewBuiltin as w, DevframeDocksActiveState as x, DevframeDockEntryRegistry as y }; |
| import { r as defineHubRpcFunction } from "./define-Ceekw2EO.mjs"; | ||
| import { DEFAULT_STATE_USER_SETTINGS } from "./constants.mjs"; | ||
| import { t as buildRemoteConnectionUrl } from "./remote-url-KVXtKP47.mjs"; | ||
| import { createEventEmitter } from "devframe/utils/events"; | ||
| import { createHostContext, createStorage } from "devframe/node"; | ||
| import { debounce } from "perfect-debounce"; | ||
| import { coerceAgentPositionalArgs } from "devframe/internal"; | ||
| import { defineDiagnostics } from "nostics"; | ||
| import { colors } from "devframe/utils/colors"; | ||
| import { ansiFormatter } from "nostics/formatters/ansi"; | ||
| import { getInternalContext, resolveBasePath } from "devframe/node/hub-internals"; | ||
| import { join, resolve } from "pathe"; | ||
| import { nanoid } from "devframe/utils/nanoid"; | ||
| import process from "node:process"; | ||
| //#region src/utils/diagnostics-reporter.ts | ||
| const formatAnsi = ansiFormatter(colors); | ||
| function hubReporter(d, { method = "warn" } = {}) { | ||
| console[method](formatAnsi(d)); | ||
| } | ||
| //#endregion | ||
| //#region src/node/diagnostics.ts | ||
| const diagnostics = defineDiagnostics({ | ||
| docsBase: "https://devfra.me/errors", | ||
| reporters: [hubReporter], | ||
| 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`)." | ||
| }, | ||
| 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("command:registered", 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("command:registered", this.toSerializable(existing)); | ||
| this.agentProvider?.notifyChanged(); | ||
| }, | ||
| unregister: () => this.unregister(command.id) | ||
| }; | ||
| } | ||
| unregister(id) { | ||
| const deleted = this.commands.delete(id); | ||
| if (deleted) { | ||
| this.events.emit("command:unregistered", 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("devframe:user-settings", { 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.prepareRemoteRegistration(view); | ||
| this.views.set(view.id, view); | ||
| this.events.emit("dock:entry:updated", 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("dock:entry:updated", view); | ||
| } | ||
| activate(dockId, params) { | ||
| if (!this.views.has(dockId)) diagnostics.DF8107({ id: dockId }); | ||
| this.events.emit("dock:activate", { | ||
| dockId, | ||
| params | ||
| }); | ||
| } | ||
| 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("message:added", 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("message:updated", 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("message:removed", 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("message:cleared"); | ||
| } | ||
| 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 = "devframe: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("terminal:session:updated", 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("terminal:session:updated", session); | ||
| } | ||
| remove(session) { | ||
| this._boundStreams.get(session.id)?.dispose(); | ||
| this.sessions.delete(session.id); | ||
| this.events.emit("terminal:session:updated", 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("terminal:session:updated", 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("terminal:session:updated", 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. | ||
| */ | ||
| async function installDevframe(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; | ||
| } | ||
| 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")); | ||
| if (d.cli?.distDir) { | ||
| if (ctx.host.mountConnectionMeta) await ctx.host.mountConnectionMeta(base); | ||
| else diagnostics.DF8106({ | ||
| id, | ||
| name: d.name, | ||
| base | ||
| }); | ||
| ctx.views.hostStatic(base, resolve(d.cli.distDir)); | ||
| } | ||
| ctx.docks.register({ | ||
| id, | ||
| title: d.name, | ||
| icon: d.icon, | ||
| ...d.dock, | ||
| ...options.dock, | ||
| type: "iframe", | ||
| url: base | ||
| }); | ||
| await d.setup(ctx); | ||
| } | ||
| //#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:commands:execute", | ||
| 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:messages:add", | ||
| 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:messages:update", | ||
| 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:messages:remove", | ||
| type: "action", | ||
| setup: (context) => ({ async handler(id) { | ||
| await context.messages.remove(id); | ||
| } }) | ||
| }); | ||
| /** `hub:messages:clear` — Remove every message. */ | ||
| const hubMessagesClear = defineHubRpcFunction({ | ||
| name: "hub:messages:clear", | ||
| 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:terminals:write", | ||
| 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:terminals:resize", | ||
| 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:terminals:terminate", | ||
| 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:terminals:restart", | ||
| 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:terminals:remove", | ||
| 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:docks:activate", | ||
| 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("devframe:docks", { initialValue: [] }); | ||
| const refreshDocks = debounce(() => { | ||
| docksSharedState.mutate(() => docks.values()); | ||
| }, debounceMs); | ||
| docks.events.on("dock:entry:updated", refreshDocks); | ||
| docksSharedState.mutate(() => docks.values()); | ||
| const activeDockSharedState = await context.rpc.sharedState.get("devframe:docks:active", { initialValue: { activation: null } }); | ||
| docks.events.on("dock:activate", (activation) => { | ||
| activeDockSharedState.mutate((state) => { | ||
| state.activation = activation; | ||
| }); | ||
| context.rpc.broadcast({ | ||
| method: "devframe:docks:activate", | ||
| args: [activation] | ||
| }); | ||
| }); | ||
| const broadcastTerminals = debounce(() => { | ||
| context.rpc.broadcast({ | ||
| method: "devframe:terminals:updated", | ||
| args: [] | ||
| }); | ||
| docksSharedState.mutate(() => docks.values()); | ||
| }, debounceMs); | ||
| terminals.events.on("terminal:session:updated", broadcastTerminals); | ||
| const broadcastMessages = debounce(() => { | ||
| context.rpc.broadcast({ | ||
| method: "devframe:messages:updated", | ||
| args: [] | ||
| }); | ||
| docksSharedState.mutate(() => docks.values()); | ||
| }, debounceMs); | ||
| messages.events.on("message:added", broadcastMessages); | ||
| messages.events.on("message:updated", broadcastMessages); | ||
| messages.events.on("message:removed", broadcastMessages); | ||
| messages.events.on("message:cleared", broadcastMessages); | ||
| const commandsSharedState = await context.rpc.sharedState.get("devframe:commands", { initialValue: [] }); | ||
| const syncCommands = debounce(() => { | ||
| commandsSharedState.mutate(() => commands.list()); | ||
| }, debounceMs); | ||
| commands.events.on("command:registered", syncCommands); | ||
| commands.events.on("command:unregistered", syncCommands); | ||
| commandsSharedState.mutate(() => commands.list()); | ||
| return context; | ||
| } | ||
| //#endregion | ||
| export { DevframeCommandsHost as _, hubMessagesAdd as a, hubMessagesUpdate as c, hubTerminalsRestart as d, hubTerminalsTerminate as f, DevframeDocksHost as g, DevframeMessagesHost as h, hubDocksActivate as i, hubTerminalsRemove as l, DevframeTerminalsHost as m, builtinHubRpcDeclarations as n, hubMessagesClear as o, hubTerminalsWrite as p, hubCommandsExecute as r, hubMessagesRemove as s, createHubContext as t, hubTerminalsResize as u, diagnostics as v }; |
| import { D as DevframeViewIframe, S as DevframeDocksHost, c as DevframeCommandsHost, p as DevframeDockActivation, v as DevframeDockEntryIcon } from "./commands-CAcTUSlj.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<{ | ||
| 'message:added': (entry: DevframeMessageEntry) => void; | ||
| 'message:updated': (entry: DevframeMessageEntry) => void; | ||
| 'message:removed': (id: string) => void; | ||
| 'message: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<{ | ||
| 'terminal: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 { createDefineWrapperWithContext } from "devframe/rpc"; | ||
| //#region src/define.ts | ||
| const defineHubRpcFunction = createDefineWrapperWithContext(); | ||
| function defineCommand(command) { | ||
| return command; | ||
| } | ||
| function defineDockEntry(entry) { | ||
| return entry; | ||
| } | ||
| //#endregion | ||
| export { defineDockEntry as n, defineHubRpcFunction as r, defineCommand as t }; |
| import "node:path"; | ||
| import "node:fs/promises"; | ||
| import { createServer } from "node:net"; | ||
| import { networkInterfaces } from "node:os"; | ||
| //#region ../../node_modules/.pnpm/get-port-please@3.2.0/node_modules/get-port-please/dist/index.mjs | ||
| const unsafePorts = /* @__PURE__ */ new Set([ | ||
| 1, | ||
| 7, | ||
| 9, | ||
| 11, | ||
| 13, | ||
| 15, | ||
| 17, | ||
| 19, | ||
| 20, | ||
| 21, | ||
| 22, | ||
| 23, | ||
| 25, | ||
| 37, | ||
| 42, | ||
| 43, | ||
| 53, | ||
| 69, | ||
| 77, | ||
| 79, | ||
| 87, | ||
| 95, | ||
| 101, | ||
| 102, | ||
| 103, | ||
| 104, | ||
| 109, | ||
| 110, | ||
| 111, | ||
| 113, | ||
| 115, | ||
| 117, | ||
| 119, | ||
| 123, | ||
| 135, | ||
| 137, | ||
| 139, | ||
| 143, | ||
| 161, | ||
| 179, | ||
| 389, | ||
| 427, | ||
| 465, | ||
| 512, | ||
| 513, | ||
| 514, | ||
| 515, | ||
| 526, | ||
| 530, | ||
| 531, | ||
| 532, | ||
| 540, | ||
| 548, | ||
| 554, | ||
| 556, | ||
| 563, | ||
| 587, | ||
| 601, | ||
| 636, | ||
| 989, | ||
| 990, | ||
| 993, | ||
| 995, | ||
| 1719, | ||
| 1720, | ||
| 1723, | ||
| 2049, | ||
| 3659, | ||
| 4045, | ||
| 5060, | ||
| 5061, | ||
| 6e3, | ||
| 6566, | ||
| 6665, | ||
| 6666, | ||
| 6667, | ||
| 6668, | ||
| 6669, | ||
| 6697, | ||
| 10080 | ||
| ]); | ||
| function isUnsafePort(port) { | ||
| return unsafePorts.has(port); | ||
| } | ||
| function isSafePort(port) { | ||
| return !isUnsafePort(port); | ||
| } | ||
| var GetPortError = class extends Error { | ||
| constructor(message, opts) { | ||
| super(message, opts); | ||
| this.message = message; | ||
| } | ||
| name = "GetPortError"; | ||
| }; | ||
| function _log(verbose, message) { | ||
| if (verbose) console.log(`[get-port] ${message}`); | ||
| } | ||
| function _generateRange(from, to) { | ||
| if (to < from) return []; | ||
| const r = []; | ||
| for (let index = from; index <= to; index++) r.push(index); | ||
| return r; | ||
| } | ||
| function _tryPort(port, host) { | ||
| return new Promise((resolve) => { | ||
| const server = createServer(); | ||
| server.unref(); | ||
| server.on("error", () => { | ||
| resolve(false); | ||
| }); | ||
| server.listen({ | ||
| port, | ||
| host | ||
| }, () => { | ||
| const { port: port2 } = server.address(); | ||
| server.close(() => { | ||
| resolve(isSafePort(port2) && port2); | ||
| }); | ||
| }); | ||
| }); | ||
| } | ||
| function _getLocalHosts(additional) { | ||
| const hosts = new Set(additional); | ||
| for (const _interface of Object.values(networkInterfaces())) for (const config of _interface || []) if (config.address && !config.internal && !config.address.startsWith("fe80::") && !config.address.startsWith("169.254")) hosts.add(config.address); | ||
| return [...hosts]; | ||
| } | ||
| async function _findPort(ports, host) { | ||
| for (const port of ports) { | ||
| const r = await _tryPort(port, host); | ||
| if (r) return r; | ||
| } | ||
| } | ||
| function _fmtOnHost(hostname) { | ||
| return hostname ? `on host ${JSON.stringify(hostname)}` : "on any host"; | ||
| } | ||
| const HOSTNAME_RE = /^(?!-)[\d.:A-Za-z-]{1,63}(?<!-)$/; | ||
| function _validateHostname(hostname, _public, verbose) { | ||
| if (hostname && !HOSTNAME_RE.test(hostname)) { | ||
| const fallbackHost = _public ? "0.0.0.0" : "127.0.0.1"; | ||
| _log(verbose, `Invalid hostname: ${JSON.stringify(hostname)}. Using ${JSON.stringify(fallbackHost)} as fallback.`); | ||
| return fallbackHost; | ||
| } | ||
| return hostname; | ||
| } | ||
| async function getPort(_userOptions = {}) { | ||
| if (typeof _userOptions === "number" || typeof _userOptions === "string") _userOptions = { port: Number.parseInt(_userOptions + "") || 0 }; | ||
| const _port = Number(_userOptions.port ?? process.env.PORT); | ||
| const _userSpecifiedAnyPort = Boolean(_userOptions.port || _userOptions.ports?.length || _userOptions.portRange?.length); | ||
| const options = { | ||
| random: _port === 0, | ||
| ports: [], | ||
| portRange: [], | ||
| alternativePortRange: _userSpecifiedAnyPort ? [] : [3e3, 3100], | ||
| verbose: false, | ||
| ..._userOptions, | ||
| port: _port, | ||
| host: _validateHostname(_userOptions.host ?? process.env.HOST, _userOptions.public, _userOptions.verbose) | ||
| }; | ||
| if (options.random && !_userSpecifiedAnyPort) return getRandomPort(options.host); | ||
| const portsToCheck = [ | ||
| options.port, | ||
| ...options.ports, | ||
| ..._generateRange(...options.portRange) | ||
| ].filter((port) => { | ||
| if (!port) return false; | ||
| if (!isSafePort(port)) { | ||
| _log(options.verbose, `Ignoring unsafe port: ${port}`); | ||
| return false; | ||
| } | ||
| return true; | ||
| }); | ||
| if (portsToCheck.length === 0) portsToCheck.push(3e3); | ||
| let availablePort = await _findPort(portsToCheck, options.host); | ||
| if (!availablePort && options.alternativePortRange.length > 0) { | ||
| availablePort = await _findPort(_generateRange(...options.alternativePortRange), options.host); | ||
| if (portsToCheck.length > 0) { | ||
| let message = `Unable to find an available port (tried ${portsToCheck.join("-")} ${_fmtOnHost(options.host)}).`; | ||
| if (availablePort) message += ` Using alternative port ${availablePort}.`; | ||
| _log(options.verbose, message); | ||
| } | ||
| } | ||
| if (!availablePort && _userOptions.random !== false) { | ||
| availablePort = await getRandomPort(options.host); | ||
| if (availablePort) _log(options.verbose, `Using random port ${availablePort}`); | ||
| } | ||
| if (!availablePort) { | ||
| const triedRanges = [ | ||
| options.port, | ||
| options.portRange.join("-"), | ||
| options.alternativePortRange.join("-") | ||
| ].filter(Boolean).join(", "); | ||
| throw new GetPortError(`Unable to find an available port ${_fmtOnHost(options.host)} (tried ${triedRanges})`); | ||
| } | ||
| return availablePort; | ||
| } | ||
| async function getRandomPort(host) { | ||
| const port = await checkPort(0, host); | ||
| if (port === false) throw new GetPortError(`Unable to find a random port ${_fmtOnHost(host)}`); | ||
| return port; | ||
| } | ||
| async function checkPort(port, host = process.env.HOST, verbose) { | ||
| if (!host) host = _getLocalHosts([void 0, "0.0.0.0"]); | ||
| if (!Array.isArray(host)) return _tryPort(port, host); | ||
| for (const _host of host) { | ||
| const _port = await _tryPort(port, _host); | ||
| if (_port === false) { | ||
| if (port < 1024 && verbose) _log(verbose, `Unable to listen to the privileged port ${port} ${_fmtOnHost(_host)}`); | ||
| return false; | ||
| } | ||
| if (port === 0 && _port !== 0) port = _port; | ||
| } | ||
| return port; | ||
| } | ||
| //#endregion | ||
| export { getPort }; |
| import "./context-Dz0j8Eyg.mjs"; | ||
| import "./commands-CAcTUSlj.mjs"; | ||
| import "./settings-B1qJ47d5.mjs"; | ||
| import { RpcDefinitionsFilter, RpcDefinitionsToFunctions } from "devframe/rpc"; | ||
| import { ConnectionMeta as ConnectionMeta$1, DevframeCapabilities, DevframeDiagnosticsDefinition, DevframeDiagnosticsHost, DevframeDiagnosticsLogger, DevframeHost as DevframeHost$1, DevframeNodeRpcSession, DevframeRpcClientFunctions, DevframeRpcServerFunctions, DevframeRpcSharedStates, DevframeViewHost, EntriesToObject, EventEmitter as EventEmitter$1, EventUnsubscribe, EventsMap, PartialWithoutId, RpcBroadcastOptions, RpcFunctionsHost, RpcSharedStateGetOptions, RpcSharedStateHost, RpcStreamingChannel, RpcStreamingChannelOptions, RpcStreamingHost, Thenable } from "devframe/types"; | ||
| export { RpcStreamingChannel as C, Thenable as E, RpcSharedStateHost as S, RpcStreamingHost as T, RpcBroadcastOptions as _, DevframeDiagnosticsLogger as a, RpcFunctionsHost as b, DevframeRpcClientFunctions as c, DevframeViewHost as d, EntriesToObject as f, PartialWithoutId as g, EventsMap as h, DevframeDiagnosticsHost as i, DevframeRpcServerFunctions as l, EventUnsubscribe as m, DevframeCapabilities as n, DevframeHost$1 as o, EventEmitter$1 as p, DevframeDiagnosticsDefinition as r, DevframeNodeRpcSession as s, ConnectionMeta$1 as t, DevframeRpcSharedStates as u, RpcDefinitionsFilter as v, RpcStreamingChannelOptions as w, RpcSharedStateGetOptions as x, RpcDefinitionsToFunctions as y }; |
| import { i as InstallDevframeOptions, n as DevframeHubContext, t as CreateHubContextOptions } from "../context-Dz0j8Eyg.mjs"; | ||
| import { DevframeInstanceRecord } from "devframe/internal"; | ||
| import { ConnectionMeta, DevframeDefinition, DevframeSseOptions, DevframeStorageScope, DevframeWsOptions, McpRouteOptions } from "devframe/types"; | ||
| import { DevframeAuthHandler } from "devframe/node/auth"; | ||
| import { WsOriginRegistry } from "devframe/rpc/transports/ws-server"; | ||
| import { Buffer } from "node:buffer"; | ||
| import { IncomingMessage, Server, ServerResponse } from "node:http"; | ||
| import { Duplex } from "node:stream"; | ||
| //#region src/node/initiate.d.ts | ||
| /** A `devframes` entry with per-mount dock customization. */ | ||
| interface HubDevframeEntry { | ||
| devframe: DevframeDefinition; | ||
| /** Per-mount overrides for the auto-synthesized iframe dock entry. */ | ||
| dock?: InstallDevframeOptions['dock']; | ||
| } | ||
| type Thenable<T> = T | Promise<T>; | ||
| type Arrayable<T> = T | readonly T[]; | ||
| /** Default mount base for a hub instance — one namespace, one catch-all. */ | ||
| declare const DEVFRAMES_HUB_BASE = "/__devframes/"; | ||
| /** | ||
| * One dock-type → prebuilt renderer-module registration for | ||
| * {@link InitHubOptions.renderers}. The hub serves the module at | ||
| * `<base>__renderers/<type>.mjs` and publishes it in the renderer manifest | ||
| * (the `devframe:dock-renderers` shared-state slot), so any viewer — the | ||
| * reference UI, a community viewer, a hand-rolled host page — lazily imports | ||
| * it the first time a dock of that `type` needs rendering. | ||
| * | ||
| * The module must be a **self-contained browser ES module** (its framework | ||
| * and styles bundled in) whose {@link DockRendererRegistration.importName} | ||
| * export is a ready `DockRenderer`. Renderer packages ship a node helper | ||
| * returning this shape — e.g. `jsonRenderUiRenderer()` from | ||
| * `@devframes/json-render-ui/hub`. | ||
| */ | ||
| interface DockRendererRegistration { | ||
| /** Dock `type` this renderer handles (e.g. `'json-render'`). */ | ||
| type: string; | ||
| /** Absolute path of the prebuilt, self-contained browser ES module. */ | ||
| file: string; | ||
| /** | ||
| * Named export carrying the renderer. | ||
| * | ||
| * @default 'default' | ||
| */ | ||
| importName?: string; | ||
| } | ||
| /** | ||
| * The UI slot of a hub instance — pure data, zero policy. The hub itself is | ||
| * headless: whoever fills this slot decides what a viewer looks like. | ||
| * `@devframes/hub-ui` ships the reference implementation (`createUi()`); | ||
| * Vite DevTools or any community viewer supplies its own object to the same | ||
| * slot and reuses all the infrastructure. | ||
| */ | ||
| interface DevframeHubUi { | ||
| /** | ||
| * A standalone viewer SPA (built with relative asset paths) served at the | ||
| * hub base itself — open `<base>` in a tab and the devtools are there. | ||
| */ | ||
| viewer?: { | ||
| /** Directory of the prebuilt viewer SPA. */ | ||
| distDir: string; | ||
| }; | ||
| /** | ||
| * A prebuilt, self-contained script served at `<base>embedded.js` — the | ||
| * floating-devtools bootstrap a host page loads with one | ||
| * `<script type="module" src="<base>embedded.js">` tag. Visibility policy | ||
| * (always-on, keyboard-summoned, …) belongs entirely to this entry. | ||
| */ | ||
| embedded?: { | ||
| /** File path of the prebuilt single-file module. */ | ||
| entry: string; | ||
| }; | ||
| /** | ||
| * Extra UI-owned files the hub serves at `<base><key>`, each produced lazily | ||
| * from memory. Keys are base-relative paths (e.g. `branding.json`); the | ||
| * content-type is inferred from the key's extension. A generic seam a viewer | ||
| * uses to publish small runtime documents (the reference UI serves its | ||
| * branding this way) without teaching the hub anything about their meaning. | ||
| */ | ||
| assets?: Record<string, () => string | Uint8Array>; | ||
| } | ||
| type DevframesInput = Array<DevframeDefinition | HubDevframeEntry | Thenable<Arrayable<DevframeDefinition | HubDevframeEntry | null | undefined>> | (() => Thenable<Arrayable<DevframeDefinition | HubDevframeEntry | null | undefined>>)>; | ||
| interface InitHubOptions { | ||
| /** | ||
| * Name for the hub instance, used in logs and diagnostics, and mcp server. | ||
| */ | ||
| name?: string; | ||
| /** | ||
| * Version for the hub instance, used in logs and diagnostics, and mcp server. | ||
| */ | ||
| version?: string; | ||
| /** | ||
| * Mount base the hub answers under — required so the mount path is | ||
| * explicit at the call site (pass the exported {@link DEVFRAMES_HUB_BASE} | ||
| * for the conventional `/__devframes/`). Every mounted devframe lives at | ||
| * `<base><id>/`, so the host app needs exactly one catch-all route. The | ||
| * resolved value is echoed back as {@link HubInstance.base} so route and | ||
| * middleware code references it instead of repeating the string. | ||
| */ | ||
| base: string; | ||
| /** | ||
| * Devframes to mount: each runs its `setup()` against the shared hub | ||
| * context (one merged RPC registry, one WebSocket, one auth gate), serves | ||
| * its SPA at `<base><id>/`, and is auto-registered as an iframe dock. | ||
| * Wrap an entry in `{ devframe, dock }` to customize its synthesized dock | ||
| * (category, icon, a `clientScript` to run in the host page, …). | ||
| */ | ||
| devframes?: DevframesInput; | ||
| /** | ||
| * Extra RPC declarations registered at context creation, alongside the | ||
| * hub built-ins — forwarded to `createHubContext`'s | ||
| * `builtinRpcDeclarations`. Declarative mode only (a pre-built `context` | ||
| * already made this choice). | ||
| */ | ||
| rpcDeclarations?: CreateHubContextOptions['builtinRpcDeclarations']; | ||
| /** | ||
| * Bring your own hub context instead of `devframes` — for hosts that | ||
| * assemble `createHubContext` + `ctx.install` themselves (with their own | ||
| * `DevframeHost` serving the frames). The instance then serves only the | ||
| * hub-level endpoints (`__connection.json`, `__index.json`, | ||
| * `__client-imports.js`, the WS transport, MCP, and the `ui` slot); | ||
| * serve each frame's meta yourself from {@link HubInstance.connectionMeta}. | ||
| */ | ||
| context?: DevframeHubContext; | ||
| /** | ||
| * Runs once the context exists and every `devframes` entry is mounted — | ||
| * register docks, commands, terminals, and messages surfaces here. | ||
| */ | ||
| configure?: (ctx: DevframeHubContext) => void | Promise<void>; | ||
| /** See {@link DevframeHubUi} — omitted, the hub stays fully headless. */ | ||
| ui?: DevframeHubUi; | ||
| /** | ||
| * Prebuilt dock-renderer modules to serve and advertise — the composition | ||
| * seam that hands a renderer package (e.g. `@devframes/json-render-ui`) to | ||
| * a prebuilt viewer. Each {@link DockRendererRegistration} is served at | ||
| * `<base>__renderers/<type>.mjs` and published in the renderer manifest; | ||
| * clients import a module lazily the first time a dock of its `type` | ||
| * mounts. Renderers registered directly in client code | ||
| * (`createDevframeClientHost({ renderers })`) take precedence. | ||
| * | ||
| * ```ts | ||
| * import { jsonRenderUiRenderer } from '@devframes/json-render-ui/hub' | ||
| * | ||
| * initHub({ ui: createUi(), renderers: [jsonRenderUiRenderer()] }) | ||
| * ``` | ||
| */ | ||
| renderers?: readonly DockRendererRegistration[]; | ||
| /** | ||
| * Share the host's `node:http` server for the WebSocket RPC endpoint | ||
| * (upgrade bound at `<base>__ws`). Hosts whose handlers never see upgrades | ||
| * (Next.js route handlers, Nitro, Rsbuild) ask for a side-car instead with | ||
| * `ws: { sidecar: true }`; hosts that get their server later wire | ||
| * {@link HubInstance.attach} / {@link HubInstance.handleUpgrade}. | ||
| */ | ||
| server?: Server; | ||
| /** | ||
| * Explicit WebSocket control, same contract as `initDevframe`: the local | ||
| * binding resolves `ws.port` (pinned side-car) > `server` (shared upgrade) | ||
| * > `ws.sidecar` (auto-port side-car) > the host driving upgrades itself, | ||
| * while `url` overrides the advertisement (tunnel pattern) and `route` | ||
| * renames the upgrade segment (default `__ws`). Pass `false` to serve no | ||
| * WebSocket at all — clients connect over SSE instead (`backend: 'sse'`). | ||
| */ | ||
| ws?: DevframeWsOptions | false; | ||
| /** | ||
| * SSE RPC endpoint control, same contract as `initDevframe` — enabled by | ||
| * default at `<base>__sse` as the more portable transport alongside the | ||
| * WebSocket. Pass `false` to disable, or a {@link DevframeSseOptions} to | ||
| * rename the route. | ||
| */ | ||
| sse?: boolean | DevframeSseOptions; | ||
| /** Bind host for a side-car WebSocket server. Default: `localhost`. */ | ||
| host?: string; | ||
| /** | ||
| * The hub's **single Auth**: one gate at the one shared transport covers | ||
| * every mounted frame, the MCP route, and the hub built-ins. Gates by | ||
| * default (devframe's interactive OTP); `false` opts out; a | ||
| * {@link DevframeAuthHandler} installs a custom scheme. | ||
| */ | ||
| auth?: boolean | DevframeAuthHandler; | ||
| /** | ||
| * Expose the **aggregate** MCP endpoint at `<base>__mcp` — one | ||
| * Streamable-HTTP server over the shared context's whole tool registry | ||
| * (ids are already namespaced per plugin). Disabled by default. | ||
| * | ||
| * @experimental | ||
| */ | ||
| mcp?: boolean | McpRouteOptions; | ||
| /** | ||
| * Public origin the host app is reachable at, or a getter. Derived lazily | ||
| * from the first request when omitted. | ||
| */ | ||
| origin?: string | (() => string); | ||
| /** | ||
| * Publish this hub in the global instance registry | ||
| * (`~/.devframe/instances/`) so discovery tooling (`devframe connect`, the | ||
| * inspect plugin's Instances tab) lists it like any standalone devframe. | ||
| * Registration is a dynamic import that fires once the public origin | ||
| * resolves and is torn down on {@link HubInstance.close}. Defaults to off; | ||
| * pass `true` to enable, or an object to override individual record fields | ||
| * (`id`, `name`, `basePath`, …). | ||
| */ | ||
| register?: boolean | Partial<DevframeInstanceRecord>; | ||
| /** Working directory for the hub context. Default: `process.cwd()`. */ | ||
| cwd?: string; | ||
| /** Override where persisted devframe state lives. */ | ||
| getStorageDir?: (scope: DevframeStorageScope) => string; | ||
| /** Extra WS-upgrade origins beyond the loopback default; `false` disables the gate. */ | ||
| allowedOrigins?: readonly string[] | WsOriginRegistry | false; | ||
| /** Destroy off-route upgrades on a shared `server` devframe's adapter owns outright. */ | ||
| destroyUnmatchedUpgrades?: boolean; | ||
| } | ||
| interface HubInstance { | ||
| /** | ||
| * The normalized mount base this hub answers under (leading and trailing | ||
| * slash, e.g. `/__devframes/`). Reference it when wiring the mount — route | ||
| * guards, middleware path checks — instead of repeating the string literal. | ||
| */ | ||
| base: string; | ||
| /** | ||
| * Web-standard request handler for the whole hub — mount it on one | ||
| * catch-all route under {@link HubInstance.base}. | ||
| */ | ||
| handler: (request: Request) => Promise<Response>; | ||
| /** Connect/Express-style middleware over the same surface; `next()`s outside the base. */ | ||
| nodeMiddleware: (req: IncomingMessage, res: ServerResponse, next?: (err?: unknown) => void) => void; | ||
| /** | ||
| * Route a host server's `upgrade` events to the shared RPC socket, | ||
| * returning a detach function — the manual counterpart to the `server` | ||
| * option, for hosts that get their `node:http` server only after the hub | ||
| * exists. Available on the default tier; a configured transport (`server`, | ||
| * `ws.port`, `ws.sidecar`, `ws.url`) already owns the socket and reports | ||
| * `DF0055` / `DF0056` instead. | ||
| */ | ||
| attach: (server: Server) => () => void; | ||
| /** | ||
| * Complete a single `upgrade` event on the shared RPC socket, for hosts | ||
| * that already own an `upgrade` listener. Same availability as | ||
| * {@link HubInstance.attach}. | ||
| */ | ||
| handleUpgrade: (req: IncomingMessage, socket: Duplex, head: Buffer) => void; | ||
| /** Resolves once every frame is mounted and the WebSocket binding is live. */ | ||
| ready: Promise<void>; | ||
| /** The shared hub context, once initialized. */ | ||
| context: Promise<DevframeHubContext>; | ||
| /** The `ConnectionMeta` served at `<base>__connection.json` (and every frame base). */ | ||
| connectionMeta: () => ConnectionMeta; | ||
| /** Tear down: WS transport/side-car, MCP sessions. */ | ||
| close: () => Promise<void>; | ||
| } | ||
| /** | ||
| * Initiate a hub instance — the whole multi-devframe devtools surface | ||
| * behind one framework-agnostic, web-standard handler. Every mounted | ||
| * devframe shares one context (merged RPC registry, shared state, docks / | ||
| * terminals / messages / commands), one WebSocket transport, and one Auth; | ||
| * the instance serves each frame's SPA at `<base><id>/`, the discovery | ||
| * endpoints (`__connection.json`, `__index.json`, `__client-imports.js`), | ||
| * the aggregate MCP route, and whatever the {@link DevframeHubUi} slot | ||
| * provides — the hub itself stays headless. | ||
| * | ||
| * The factory is synchronous and initializes eagerly, and binds no port of | ||
| * its own: the WebSocket follows `server` / `ws` (see | ||
| * {@link InitHubOptions.ws}), or waits for the host to hand upgrades over | ||
| * through {@link HubInstance.attach}. | ||
| */ | ||
| declare function initHub(options: InitHubOptions): HubInstance; | ||
| //#endregion | ||
| export { DEVFRAMES_HUB_BASE, DevframeHubUi, DevframesInput, DockRendererRegistration, HubDevframeEntry, HubInstance, InitHubOptions, initHub }; |
| import { DOCK_RENDERERS_STATE_KEY } from "../constants.mjs"; | ||
| import { t as createHubContext, v as diagnostics } from "../context-DPdojIW8.mjs"; | ||
| 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 { resolve } from "pathe"; | ||
| import process from "node:process"; | ||
| import { existsSync } from "node:fs"; | ||
| import { cleanDoubleSlashes, joinURL, withLeadingSlash, withTrailingSlash, withoutLeadingSlash } from "ufo"; | ||
| import { readFile } from "node:fs/promises"; | ||
| import { mountStaticHandler } from "devframe/utils/serve-static"; | ||
| import { H3 } from "h3"; | ||
| //#region src/node/initiate.ts | ||
| function normalizeDevframeEntry(entry) { | ||
| return "devframe" in entry ? entry : { devframe: entry }; | ||
| } | ||
| /** Default mount base for a hub instance — one namespace, one catch-all. */ | ||
| const DEVFRAMES_HUB_BASE = "/__devframes/"; | ||
| /** Content-type for a UI asset key, inferred from its file extension. */ | ||
| function assetContentType(key) { | ||
| if (key.endsWith(".json")) return "application/json; charset=utf-8"; | ||
| if (key.endsWith(".js") || key.endsWith(".mjs")) return "text/javascript; charset=utf-8"; | ||
| if (key.endsWith(".css")) return "text/css; charset=utf-8"; | ||
| if (key.endsWith(".svg")) return "image/svg+xml"; | ||
| if (key.endsWith(".html")) return "text/html; charset=utf-8"; | ||
| return "application/octet-stream"; | ||
| } | ||
| /** Reserved filenames directly under the hub base — a frame id can't shadow them. */ | ||
| const RESERVED_HUB_PATHS = [ | ||
| DEVFRAME_CONNECTION_META_FILENAME, | ||
| DEVFRAME_DOCK_IMPORTS_FILENAME, | ||
| DEVFRAME_WS_ROUTE, | ||
| DEVFRAME_MCP_ROUTE, | ||
| "__index.json", | ||
| "__renderers", | ||
| "embedded.js" | ||
| ]; | ||
| /** | ||
| * Flatten the `devframes` input: await every thenable, call every factory, | ||
| * spread the arrays, and drop the empty slots — so a host can build the list | ||
| * conditionally (`isDev && loadInspect()`) without filtering it first. | ||
| */ | ||
| function resolveDevframesInput(input) { | ||
| return Promise.all(input.map(async (entry) => { | ||
| const resolved = await (typeof entry === "function" ? entry() : entry); | ||
| return (Array.isArray(resolved) ? resolved : [resolved]).filter((slot) => slot != null).map(normalizeDevframeEntry); | ||
| })).then((arrays) => arrays.flat()); | ||
| } | ||
| function normalizeBase(base) { | ||
| return cleanDoubleSlashes(withTrailingSlash(withLeadingSlash(base))); | ||
| } | ||
| /** | ||
| * Validate the renderer-module registrations fail-fast: route-safe types | ||
| * (each becomes the `<base>__renderers/<type>.mjs` URL segment), one module | ||
| * per type, and an existing bundle file (renderer packages are prebuilt). | ||
| */ | ||
| function resolveRendererRegistrations(registrations) { | ||
| const seen = /* @__PURE__ */ new Set(); | ||
| return registrations.map((registration) => { | ||
| if (!/^[\w.-]+$/.test(registration.type)) throw diagnostics.DF8110({ type: registration.type }); | ||
| if (seen.has(registration.type)) throw diagnostics.DF8108({ type: registration.type }); | ||
| seen.add(registration.type); | ||
| const file = resolve(registration.file); | ||
| if (!existsSync(file)) throw diagnostics.DF8109({ | ||
| type: registration.type, | ||
| file | ||
| }); | ||
| return { | ||
| ...registration, | ||
| file | ||
| }; | ||
| }); | ||
| } | ||
| /** | ||
| * Render the dock client-script import map as an ES module — one dynamic | ||
| * import thunk per dock that carries a client script (`clientScript` on | ||
| * iframe docks, `action`, `renderer`). External viewers import this module | ||
| * from `<base>__client-imports.js` to load per-dock client code into the | ||
| * host page; `importFrom` values must be URL paths the host serves. | ||
| */ | ||
| function renderClientImportsModule(ctx) { | ||
| const entries = []; | ||
| for (const [id, view] of ctx.docks.views) { | ||
| const scripts = []; | ||
| const anyView = view; | ||
| if (anyView.clientScript) scripts.push(anyView.clientScript); | ||
| if (anyView.action) scripts.push(anyView.action); | ||
| if (anyView.renderer) scripts.push(anyView.renderer); | ||
| if (scripts.length === 0) continue; | ||
| const thunks = scripts.map((script) => `() => import(${JSON.stringify(script.importFrom)})`); | ||
| entries.push(` ${JSON.stringify(id)}: [${thunks.join(", ")}],`); | ||
| } | ||
| return `// Generated by @devframes/hub — dock client-script import map.\nexport const clientImports = {\n${entries.join("\n")}\n}\nexport default clientImports\n`; | ||
| } | ||
| /** | ||
| * Initiate a hub instance — the whole multi-devframe devtools surface | ||
| * behind one framework-agnostic, web-standard handler. Every mounted | ||
| * devframe shares one context (merged RPC registry, shared state, docks / | ||
| * terminals / messages / commands), one WebSocket transport, and one Auth; | ||
| * the instance serves each frame's SPA at `<base><id>/`, the discovery | ||
| * endpoints (`__connection.json`, `__index.json`, `__client-imports.js`), | ||
| * the aggregate MCP route, and whatever the {@link DevframeHubUi} slot | ||
| * provides — the hub itself stays headless. | ||
| * | ||
| * The factory is synchronous and initializes eagerly, and binds no port of | ||
| * its own: the WebSocket follows `server` / `ws` (see | ||
| * {@link InitHubOptions.ws}), or waits for the host to hand upgrades over | ||
| * through {@link HubInstance.attach}. | ||
| */ | ||
| function initHub(options) { | ||
| const base = normalizeBase(options.base); | ||
| const baseNoSlash = base.slice(0, -1); | ||
| const app = new H3(); | ||
| const cwd = options.cwd ?? process.cwd(); | ||
| const frames = []; | ||
| const rendererRegistrations = resolveRendererRegistrations(options.renderers ?? []); | ||
| const shell = createInstanceShell({ | ||
| base, | ||
| app, | ||
| host: options.host, | ||
| origin: options.origin, | ||
| auth: options.auth, | ||
| server: options.server, | ||
| ws: options.ws, | ||
| sse: options.sse, | ||
| allowedOrigins: options.allowedOrigins, | ||
| destroyUnmatchedUpgrades: options.destroyUnmatchedUpgrades, | ||
| register: resolveInstanceRegister(options.register, { | ||
| id: options.name ?? "devframes-hub", | ||
| ...options.name !== void 0 ? { name: options.name } : {}, | ||
| rootDir: cwd | ||
| }), | ||
| absoluteWsPath: true, | ||
| resolveSidecarPort: async (sidecarHost) => { | ||
| const { getPort } = await import("../dist-R0T2g9Vw.mjs"); | ||
| return getPort({ | ||
| port: 9777, | ||
| portRange: [9777, 9877], | ||
| host: sidecarHost | ||
| }); | ||
| }, | ||
| onMetaUnavailable: () => { | ||
| throw diagnostics.DF8003(); | ||
| }, | ||
| async init(api) { | ||
| if (options.context && options.devframes?.length) throw diagnostics.DF8002(); | ||
| let ctx; | ||
| if (options.context) ctx = options.context; | ||
| else { | ||
| const host = { | ||
| ...createH3DevframeHost({ | ||
| origin: () => api.origin() ?? "http://localhost", | ||
| appName: "devframes", | ||
| workspaceRoot: cwd, | ||
| mount: (mountBase, dir) => { | ||
| mountStaticHandler(app, mountBase, dir); | ||
| } | ||
| }), | ||
| ...options.getStorageDir ? { getStorageDir: options.getStorageDir } : {}, | ||
| mountConnectionMeta: (frameBase) => { | ||
| app.use(joinURL(frameBase, DEVFRAME_CONNECTION_META_FILENAME), () => api.connectionMeta()); | ||
| } | ||
| }; | ||
| ctx = await createHubContext({ | ||
| cwd, | ||
| workspaceRoot: cwd, | ||
| mode: "dev", | ||
| host, | ||
| ...options.rpcDeclarations ? { builtinRpcDeclarations: options.rpcDeclarations } : {} | ||
| }); | ||
| } | ||
| const devframes = await resolveDevframesInput(options.devframes ?? []); | ||
| for (const { devframe: def, dock } of devframes) { | ||
| if (RESERVED_HUB_PATHS.includes(def.id)) throw diagnostics.DF8000({ id: def.id }); | ||
| if (!/^[\w.-]+$/.test(def.id)) throw diagnostics.DF8004({ id: def.id }); | ||
| const frameBase = withTrailingSlash(joinURL(base, def.id)); | ||
| await ctx.install(def, { | ||
| base: frameBase, | ||
| ...dock ? { dock } : {} | ||
| }); | ||
| frames.push({ | ||
| id: def.id, | ||
| base: frameBase, | ||
| title: def.name | ||
| }); | ||
| } | ||
| await options.configure?.(ctx); | ||
| if (rendererRegistrations.length > 0) { | ||
| const manifest = {}; | ||
| for (const registration of rendererRegistrations) manifest[registration.type] = { | ||
| importFrom: joinURL(base, "__renderers", `${registration.type}.mjs`), | ||
| ...registration.importName ? { importName: registration.importName } : {} | ||
| }; | ||
| (await ctx.rpc.sharedState.get(DOCK_RENDERERS_STATE_KEY, { initialValue: {} })).mutate(() => manifest); | ||
| } | ||
| const mcpConfig = options.mcp === true ? {} : options.mcp; | ||
| if (!mcpConfig) return { context: ctx }; | ||
| const mcpRoute = withoutLeadingSlash(mcpConfig.path ?? DEVFRAME_MCP_ROUTE); | ||
| const { mountMcpHttp } = await import("devframe/adapters/mcp"); | ||
| const mounted = mountMcpHttp(app, ctx, joinURL(base, mcpRoute), { | ||
| serverName: options.name ?? "devframes-hub", | ||
| serverVersion: options.version ?? "0.0.0", | ||
| exposeSharedState: true, | ||
| allowedOrigins: mcpConfig.allowedOrigins | ||
| }); | ||
| return { | ||
| context: ctx, | ||
| mcp: { path: mcpRoute }, | ||
| dispose: mounted.dispose | ||
| }; | ||
| }, | ||
| mount(ctx, meta) { | ||
| app.use(joinURL(base, DEVFRAME_CONNECTION_META_FILENAME), () => meta); | ||
| const indexDocument = () => ({ | ||
| name: options.name, | ||
| version: options.version, | ||
| base, | ||
| frames, | ||
| endpoints: { | ||
| connection: DEVFRAME_CONNECTION_META_FILENAME, | ||
| clientImports: DEVFRAME_DOCK_IMPORTS_FILENAME, | ||
| index: "__index.json", | ||
| websocket: meta.websocket, | ||
| ...meta.mcp ? { mcp: meta.mcp.path } : {}, | ||
| ...options.ui?.embedded ? { embedded: "embedded.js" } : {} | ||
| } | ||
| }); | ||
| app.use(joinURL(base, "__index.json"), () => indexDocument()); | ||
| app.use(joinURL(base, DEVFRAME_DOCK_IMPORTS_FILENAME), (event) => { | ||
| event.res.headers.set("Content-Type", "text/javascript; charset=utf-8"); | ||
| event.res.headers.set("Cache-Control", "no-store"); | ||
| return renderClientImportsModule(ctx); | ||
| }); | ||
| if (options.ui?.embedded) { | ||
| const entry = resolve(options.ui.embedded.entry); | ||
| app.use(joinURL(base, "embedded.js"), async (event) => { | ||
| event.res.headers.set("Content-Type", "text/javascript; charset=utf-8"); | ||
| event.res.headers.set("Cache-Control", "no-store"); | ||
| return await readFile(entry); | ||
| }); | ||
| } | ||
| for (const registration of rendererRegistrations) app.use(joinURL(base, "__renderers", `${registration.type}.mjs`), async (event) => { | ||
| event.res.headers.set("Content-Type", "text/javascript; charset=utf-8"); | ||
| event.res.headers.set("Cache-Control", "no-store"); | ||
| return await readFile(registration.file); | ||
| }); | ||
| for (const [key, produce] of Object.entries(options.ui?.assets ?? {})) app.use(joinURL(base, key), (event) => { | ||
| event.res.headers.set("Content-Type", assetContentType(key)); | ||
| event.res.headers.set("Cache-Control", "no-store"); | ||
| return produce(); | ||
| }); | ||
| if (options.ui?.viewer) mountStaticHandler(app, base, resolve(options.ui.viewer.distDir)); | ||
| else { | ||
| app.use(baseNoSlash, () => indexDocument()); | ||
| app.use(base, () => indexDocument()); | ||
| } | ||
| } | ||
| }); | ||
| return { | ||
| base: shell.base, | ||
| handler: shell.handler, | ||
| nodeMiddleware: shell.nodeMiddleware, | ||
| attach: shell.attach, | ||
| handleUpgrade: shell.handleUpgrade, | ||
| ready: shell.ready, | ||
| context: shell.context, | ||
| connectionMeta: shell.connectionMeta, | ||
| close: shell.close | ||
| }; | ||
| } | ||
| //#endregion | ||
| export { DEVFRAMES_HUB_BASE, initHub }; |
| import { s as DevframeCommandShortcutOverrides } from "./commands-CAcTUSlj.mjs"; | ||
| //#region src/types/settings.d.ts | ||
| interface DevframeDocksUserSettings { | ||
| docksHidden: string[]; | ||
| docksCategoriesHidden: string[]; | ||
| docksPinned: string[]; | ||
| docksCustomOrder: Record<string, number>; | ||
| showIframeAddressBar: boolean; | ||
| closeOnOutsideClick: boolean; | ||
| commandShortcuts: DevframeCommandShortcutOverrides; | ||
| } | ||
| //#endregion | ||
| export { DevframeDocksUserSettings as t }; |
+101
-25
@@ -1,5 +0,5 @@ | ||
| import { N as NavTarget, O as DevframeViewIframe, P as RemoteConnectionInfo, a as DevframeCommandEntry, g as DevframeDockEntry, h as DevframeDockEntriesGrouped, n as DevframeClientCommand, s as DevframeCommandKeybinding, t as DevframeDocksUserSettings, x as DevframeDockUserEntry, y as DevframeDockEntryIcon } from "../settings-Byh48aCd.mjs"; | ||
| import { DEFAULT_CATEGORIES_ORDER } from "../constants.mjs"; | ||
| import { w as DevframeMessagesClient, y as DevframeMessageEntryInput } from "../context-BSgzLnsu.mjs"; | ||
| import "../index-IzywMKfP.mjs"; | ||
| import { E as DevframeMessagesClient, x as DevframeMessageEntryInput } from "../context-Dz0j8Eyg.mjs"; | ||
| import { D as DevframeViewIframe, M as RemoteConnectionInfo, b as DevframeDockUserEntry, f as ClientScriptEntry, h as DevframeDockEntry, i as DevframeCommandEntry, j as NavTarget, m as DevframeDockEntriesGrouped, o as DevframeCommandKeybinding, t as DevframeClientCommand, v as DevframeDockEntryIcon } from "../commands-CAcTUSlj.mjs"; | ||
| import { t as DevframeDocksUserSettings } from "../settings-B1qJ47d5.mjs"; | ||
| import "../index-p4BktGvQ.mjs"; | ||
| import { DevframeClientRpcHost, DevframeConnection, DevframeConnectionStatus, DevframeRpcClient, DevframeRpcClientOptions, DevframeRpcContext, RpcClientEvents, RpcClientEvents as RpcClientEvents$1 } from "devframe/client"; | ||
@@ -14,6 +14,13 @@ import { EventEmitter } from "devframe/types"; | ||
| */ | ||
| interface DockRendererMountOptions { | ||
| interface DockRendererMountOptions<Entry extends DevframeDockEntry = DevframeDockEntry> { | ||
| /** The dock entry being rendered (carries the entry's serializable payload). */ | ||
| entry: DevframeDockEntry; | ||
| /** The DOM element the renderer should mount into. */ | ||
| entry: Entry; | ||
| /** | ||
| * The DOM element the renderer should mount into. It may live inside a | ||
| * **shadow root** (the reference viewer isolates dock content that way), so | ||
| * a renderer must deliver its own styles into `container.getRootNode()` | ||
| * rather than assume a page-level stylesheet. The viewer mirrors a live | ||
| * `dark` class onto this element (the theme contract), and CSS custom | ||
| * properties inherit across the shadow boundary for brand theming. | ||
| */ | ||
| container: HTMLElement; | ||
@@ -30,29 +37,95 @@ /** The assembled client host context (rpc, docks, commands, …). */ | ||
| * A renderer for a dock `type`. The headless hub is renderer-agnostic — a | ||
| * host application registers renderers at boot (e.g. injecting | ||
| * `@devframes/json-render-ui` for the `'json-render'` type). The renderer | ||
| * owns its framework (Vue, React, …); the hub only routes a dock type to it | ||
| * and disposes it on deactivation. | ||
| * host registers renderers at boot (e.g. injecting `@devframes/json-render-ui` | ||
| * for the `'json-render'` type) or serves them as prebuilt modules through the | ||
| * hub's renderer manifest (`initHub({ renderers })`). The renderer owns its | ||
| * framework (Vue, React, …); the hub only routes a dock type to it and | ||
| * disposes it on deactivation. | ||
| * | ||
| * Integration packages narrow `Entry` to export a precisely-typed contract — | ||
| * e.g. `@devframes/json-render/hub` exports | ||
| * `JsonRenderDockRenderer = DockRenderer<DevframeJsonRenderDockEntry>`. | ||
| */ | ||
| type DockRenderer = (options: DockRendererMountOptions) => DockRendererInstance | Promise<DockRendererInstance>; | ||
| type DockRenderer<Entry extends DevframeDockEntry = DevframeDockEntry> = (options: DockRendererMountOptions<Entry>) => DockRendererInstance | Promise<DockRendererInstance>; | ||
| /** | ||
| * The outcome of {@link DockRenderersContext.mount}. A missing renderer is an | ||
| * expected, non-exceptional state — a viewer renders its "no renderer for this | ||
| * dock type" fallback from `missing-renderer`, and its error variant (with a | ||
| * retry affordance) from `load-error`. | ||
| */ | ||
| type DockRendererMountResult = { | ||
| status: 'mounted'; | ||
| dispose: () => void; | ||
| } | { | ||
| status: 'missing-renderer'; | ||
| } | { | ||
| status: 'load-error'; | ||
| error: unknown; | ||
| }; | ||
| /** | ||
| * The dock-renderer registry surfaced on the client host context. A viewer | ||
| * calls {@link DockRenderersContext.mount} to render a dock whose `type` has a | ||
| * registered renderer into a container it owns; the host tracks the instance | ||
| * and disposes it when the entry deactivates. | ||
| * renderer — registered locally at boot, or served as a prebuilt module by the | ||
| * hub's renderer manifest — into a container it owns; the host tracks the | ||
| * instance and disposes it when the entry deactivates. | ||
| */ | ||
| interface DockRenderersContext { | ||
| /** Register a renderer for a dock `type`. Returns an unregister function. */ | ||
| register: (type: string, renderer: DockRenderer) => () => void; | ||
| /** Look up the renderer registered for a dock `type`, if any. */ | ||
| /** | ||
| * Register a renderer for a dock `type`. Returns an unregister function. | ||
| * Accepts a renderer narrowed to any specific entry variant (e.g. a | ||
| * {@link DockRenderer}<DevframeJsonRenderDockEntry> from an integration | ||
| * package) — the type routes only its own entries to it. | ||
| */ | ||
| register: (type: string, renderer: DockRenderer<any>) => () => void; | ||
| /** Look up the locally-registered renderer for a dock `type`, if any. */ | ||
| get: (type: string) => DockRenderer | undefined; | ||
| /** Whether a renderer is registered for a dock `type`. */ | ||
| /** | ||
| * Whether a renderer is available for a dock `type` — registered locally | ||
| * **or** provided by the hub's renderer manifest. A viewer checks this | ||
| * before mounting to render its missing-renderer fallback declaratively. | ||
| */ | ||
| has: (type: string) => boolean; | ||
| /** | ||
| * Mount the entry's registered renderer into `container`. Resolves to a | ||
| * disposer; the same instance is also disposed automatically when the entry | ||
| * deactivates. Warns and resolves to a no-op disposer when no renderer is | ||
| * registered for the entry's type. | ||
| * Mount the entry's renderer into `container` and resolve the | ||
| * {@link DockRendererMountResult}. A local registration wins; otherwise the | ||
| * manifest module for the entry's type is imported (lazily, cached) and | ||
| * registered. The mounted instance is also disposed automatically when the | ||
| * entry deactivates. Resolves `missing-renderer` (with a `console.warn`) | ||
| * when neither source has the type, and `load-error` when the module import | ||
| * or the renderer itself fails — a failed import is not cached, so a retry | ||
| * re-imports. | ||
| */ | ||
| mount: (entry: DevframeDockEntry, container: HTMLElement) => Promise<() => void>; | ||
| mount: (entry: DevframeDockEntry, container: HTMLElement) => Promise<DockRendererMountResult>; | ||
| } | ||
| /** | ||
| * The renderer manifest published by the hub at the | ||
| * `devframe:dock-renderers` shared-state slot — one {@link ClientScriptEntry} | ||
| * per dock `type`, whose `importFrom` is a URL path the hub serves | ||
| * (`<base>__renderers/<type>.mjs`). Mirrors the dock client-script | ||
| * convention: the module's `importName` export (default `'default'`) is a | ||
| * ready {@link DockRenderer}. | ||
| */ | ||
| type DockRendererManifest = Record<string, ClientScriptEntry>; | ||
| /** Options for {@link createDockRenderersContext}. */ | ||
| interface CreateDockRenderersContextOptions { | ||
| /** The assembled client context handed to renderers at mount. */ | ||
| context: () => DevframeClientContext; | ||
| /** Renderers registered locally at boot — these win over manifest modules. */ | ||
| local?: Record<string, DockRenderer<any>>; | ||
| /** The current {@link DockRendererManifest} (live getter). */ | ||
| manifest?: () => DockRendererManifest; | ||
| /** | ||
| * Called with each successful mount's disposer so the caller can tie | ||
| * disposal to its own lifecycle (entry deactivation, host teardown). An | ||
| * optional returned cleanup runs exactly once when the mount is disposed — | ||
| * whichever side (caller or viewer) triggers it first. | ||
| */ | ||
| onMounted?: (dispose: () => void, entry: DevframeDockEntry) => (() => void) | void; | ||
| } | ||
| /** | ||
| * Build the {@link DockRenderersContext} shared by every hub-aware client — | ||
| * `createDevframeClientHost` and viewers that assemble their own context | ||
| * (`@devframes/hub-ui`) both delegate here so local-first resolution, lazy | ||
| * manifest imports, and the typed mount result behave identically everywhere. | ||
| */ | ||
| declare function createDockRenderersContext(options: CreateDockRenderersContextOptions): DockRenderersContext; | ||
| //#endregion | ||
@@ -418,4 +491,7 @@ //#region src/client/docks.d.ts | ||
| * `@devframes/json-render-ui`). The hub ships none by default. | ||
| * | ||
| * Local registrations take precedence over the hub's renderer manifest | ||
| * (`initHub({ renderers })`) — explicit local code beats wire config. | ||
| */ | ||
| renderers?: Record<string, DockRenderer>; | ||
| renderers?: Record<string, DockRenderer<any>>; | ||
| /** | ||
@@ -510,2 +586,2 @@ * Hub-wide override of the top-level dock-bar category ordering — a map of | ||
| //#endregion | ||
| export { CLIENT_CONTEXT_KEY, CommandsContext, ConnectRemoteDevframeOptions, DEFAULT_CATEGORIES_ORDER, DevframeClientContext, DevframeClientHost, DevframeClientHostOptions, type DevframeClientRpcHost, DockClientScriptContext, DockClientType, DockEntryState, DockEntryStateEvents, DockPanelStorage, DockRegistration, DockRenderer, DockRendererInstance, DockRendererMountOptions, DockRenderersContext, DocksConnectionContext, DocksContext, DocksEntriesContext, DocksPanelContext, FRAME_NAV_CHANNEL, FRAME_NAV_VERSION, FrameNavClient, FrameNavClientOptions, FrameNavEnvelope, FrameNavFrameMessage, FrameNavHostMessage, FrameNavHostPayload, FrameNavListenTarget, FrameTab, MessagesClientOptions, type RpcClientEvents, WhenClauseContext, attachFrameNavClient, buildRemoteDevframeUrl, connectRemoteDevframe, createDevframeClientHost, createMessagesClient, getDevframeClientContext, parseRemoteConnection, resolveDockIcon, resolveDockUrl, setDevframeClientContext, stripRemoteConnectionFromUrl }; | ||
| export { CLIENT_CONTEXT_KEY, CommandsContext, ConnectRemoteDevframeOptions, CreateDockRenderersContextOptions, DevframeClientContext, DevframeClientHost, DevframeClientHostOptions, type DevframeClientRpcHost, DockClientScriptContext, DockClientType, DockEntryState, DockEntryStateEvents, DockPanelStorage, DockRegistration, DockRenderer, DockRendererInstance, DockRendererManifest, DockRendererMountOptions, DockRendererMountResult, DockRenderersContext, DocksConnectionContext, DocksContext, DocksEntriesContext, DocksPanelContext, FRAME_NAV_CHANNEL, FRAME_NAV_VERSION, FrameNavClient, FrameNavClientOptions, FrameNavEnvelope, FrameNavFrameMessage, FrameNavHostMessage, FrameNavHostPayload, FrameNavListenTarget, FrameTab, MessagesClientOptions, type RpcClientEvents, WhenClauseContext, attachFrameNavClient, buildRemoteDevframeUrl, connectRemoteDevframe, createDevframeClientHost, createDockRenderersContext, createMessagesClient, getDevframeClientContext, parseRemoteConnection, resolveDockIcon, resolveDockUrl, setDevframeClientContext, stripRemoteConnectionFromUrl }; |
+104
-35
@@ -1,2 +0,2 @@ | ||
| import { DEFAULT_CATEGORIES_ORDER, DEFAULT_STATE_USER_SETTINGS } from "../constants.mjs"; | ||
| import { DEFAULT_CATEGORIES_ORDER, DEFAULT_STATE_USER_SETTINGS, DOCK_RENDERERS_STATE_KEY } from "../constants.mjs"; | ||
| import { n as stripRemoteConnectionFromUrl, t as buildRemoteConnectionUrl } from "../remote-url-KVXtKP47.mjs"; | ||
@@ -327,2 +327,93 @@ import { REMOTE_CONNECTION_KEY } from "devframe/constants"; | ||
| //#endregion | ||
| //#region src/client/renderers.ts | ||
| /** | ||
| * Build the {@link DockRenderersContext} shared by every hub-aware client — | ||
| * `createDevframeClientHost` and viewers that assemble their own context | ||
| * (`@devframes/hub-ui`) both delegate here so local-first resolution, lazy | ||
| * manifest imports, and the typed mount result behave identically everywhere. | ||
| */ | ||
| function createDockRenderersContext(options) { | ||
| const rendererMap = /* @__PURE__ */ new Map(); | ||
| for (const [type, renderer] of Object.entries(options.local ?? {})) rendererMap.set(type, renderer); | ||
| const manifestImports = /* @__PURE__ */ new Map(); | ||
| const manifestEntry = (type) => options.manifest?.()[type]; | ||
| async function importManifestRenderer(type, script) { | ||
| const renderer = (await import( | ||
| /* @vite-ignore */ | ||
| /* webpackIgnore: true */ | ||
| /* turbopackIgnore: true */ | ||
| script.importFrom | ||
| ))[script.importName ?? "default"]; | ||
| return typeof renderer === "function" ? renderer : void 0; | ||
| } | ||
| async function resolveRenderer(type) { | ||
| const local = rendererMap.get(type); | ||
| if (local) return local; | ||
| const script = manifestEntry(type); | ||
| if (!script) return void 0; | ||
| let pending = manifestImports.get(type); | ||
| if (!pending) { | ||
| pending = importManifestRenderer(type, script); | ||
| manifestImports.set(type, pending); | ||
| pending.catch(() => manifestImports.delete(type)); | ||
| } | ||
| const renderer = await pending; | ||
| if (renderer && !rendererMap.has(type)) rendererMap.set(type, renderer); | ||
| return renderer; | ||
| } | ||
| return { | ||
| register(type, renderer) { | ||
| rendererMap.set(type, renderer); | ||
| return () => { | ||
| if (rendererMap.get(type) === renderer) rendererMap.delete(type); | ||
| }; | ||
| }, | ||
| get: (type) => rendererMap.get(type), | ||
| has: (type) => rendererMap.has(type) || manifestEntry(type) !== void 0, | ||
| async mount(entry, container) { | ||
| let renderer; | ||
| try { | ||
| renderer = await resolveRenderer(entry.type); | ||
| } catch (error) { | ||
| console.error(`[@devframes/hub] failed to load the renderer module for dock type "${entry.type}"`, error); | ||
| return { | ||
| status: "load-error", | ||
| error | ||
| }; | ||
| } | ||
| if (!renderer) { | ||
| console.warn(`[@devframes/hub] no renderer registered for dock type "${entry.type}" (entry "${entry.id}")`); | ||
| return { status: "missing-renderer" }; | ||
| } | ||
| let instance; | ||
| try { | ||
| instance = await renderer({ | ||
| entry, | ||
| container, | ||
| context: options.context() | ||
| }); | ||
| } catch (error) { | ||
| console.error(`[@devframes/hub] renderer for dock type "${entry.type}" failed to mount (entry "${entry.id}")`, error); | ||
| return { | ||
| status: "load-error", | ||
| error | ||
| }; | ||
| } | ||
| let disposed = false; | ||
| let cleanup; | ||
| const dispose = () => { | ||
| if (disposed) return; | ||
| disposed = true; | ||
| cleanup?.(); | ||
| instance.dispose?.(); | ||
| }; | ||
| cleanup = options.onMounted?.(dispose, entry); | ||
| return { | ||
| status: "mounted", | ||
| dispose | ||
| }; | ||
| } | ||
| }; | ||
| } | ||
| //#endregion | ||
| //#region src/client/host.ts | ||
@@ -347,6 +438,7 @@ const DOCKS_STATE_KEY = "devframe:docks"; | ||
| let mountedRenderers; | ||
| const [docksState, commandsState, settings] = await Promise.all([ | ||
| const [docksState, commandsState, settings, renderersManifestState] = await Promise.all([ | ||
| rpc.sharedState.get(DOCKS_STATE_KEY, { initialValue: [] }), | ||
| rpc.sharedState.get(COMMANDS_STATE_KEY, { initialValue: [] }), | ||
| rpc.sharedState.get(USER_SETTINGS_STATE_KEY, { initialValue: DEFAULT_STATE_USER_SETTINGS() }) | ||
| rpc.sharedState.get(USER_SETTINGS_STATE_KEY, { initialValue: DEFAULT_STATE_USER_SETTINGS() }), | ||
| rpc.sharedState.get(DOCK_RENDERERS_STATE_KEY, { initialValue: {} }) | ||
| ]); | ||
@@ -584,40 +676,17 @@ let selectedId = null; | ||
| function createRenderersContext() { | ||
| const rendererMap = /* @__PURE__ */ new Map(); | ||
| for (const [type, renderer] of Object.entries(options.renderers ?? {})) rendererMap.set(type, renderer); | ||
| const mountedDisposers = /* @__PURE__ */ new Set(); | ||
| mountedRenderers = mountedDisposers; | ||
| return { | ||
| register(type, renderer) { | ||
| rendererMap.set(type, renderer); | ||
| return createDockRenderersContext({ | ||
| context: () => context, | ||
| ...options.renderers ? { local: options.renderers } : {}, | ||
| manifest: () => renderersManifestState.value(), | ||
| onMounted(dispose, entry) { | ||
| mountedDisposers.add(dispose); | ||
| const offDeactivate = entryToStateMap.get(entry.id)?.events.on("entry:deactivated", dispose); | ||
| return () => { | ||
| if (rendererMap.get(type) === renderer) rendererMap.delete(type); | ||
| }; | ||
| }, | ||
| get: (type) => rendererMap.get(type), | ||
| has: (type) => rendererMap.has(type), | ||
| async mount(entry, container) { | ||
| const renderer = rendererMap.get(entry.type); | ||
| if (!renderer) { | ||
| console.warn(`[@devframes/hub] no renderer registered for dock type "${entry.type}" (entry "${entry.id}")`); | ||
| return () => {}; | ||
| } | ||
| const instance = await renderer({ | ||
| entry, | ||
| container, | ||
| context | ||
| }); | ||
| let disposed = false; | ||
| let offDeactivate; | ||
| const dispose = () => { | ||
| if (disposed) return; | ||
| disposed = true; | ||
| mountedDisposers.delete(dispose); | ||
| offDeactivate?.(); | ||
| instance.dispose?.(); | ||
| }; | ||
| mountedDisposers.add(dispose); | ||
| offDeactivate = entryToStateMap.get(entry.id)?.events.on("entry:deactivated", dispose); | ||
| return dispose; | ||
| } | ||
| }; | ||
| }); | ||
| } | ||
@@ -801,2 +870,2 @@ function clientScriptOf(entry) { | ||
| //#endregion | ||
| export { CLIENT_CONTEXT_KEY, DEFAULT_CATEGORIES_ORDER, FRAME_NAV_CHANNEL, FRAME_NAV_VERSION, attachFrameNavClient, buildRemoteDevframeUrl, connectRemoteDevframe, createDevframeClientHost, createMessagesClient, getDevframeClientContext, parseRemoteConnection, resolveDockIcon, resolveDockUrl, setDevframeClientContext, stripRemoteConnectionFromUrl }; | ||
| export { CLIENT_CONTEXT_KEY, FRAME_NAV_CHANNEL, FRAME_NAV_VERSION, attachFrameNavClient, buildRemoteDevframeUrl, connectRemoteDevframe, createDevframeClientHost, createDockRenderersContext, createMessagesClient, getDevframeClientContext, parseRemoteConnection, resolveDockIcon, resolveDockUrl, setDevframeClientContext, stripRemoteConnectionFromUrl }; |
@@ -1,2 +0,2 @@ | ||
| import { t as DevframeDocksUserSettings } from "./settings-Byh48aCd.mjs"; | ||
| import { t as DevframeDocksUserSettings } from "./settings-B1qJ47d5.mjs"; | ||
| export * from "devframe/constants"; | ||
@@ -15,4 +15,11 @@ //#region src/constants.d.ts | ||
| 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 = "devframe:dock-renderers"; | ||
| declare const DEFAULT_STATE_USER_SETTINGS: () => DevframeDocksUserSettings; | ||
| //#endregion | ||
| export { DEFAULT_CATEGORIES_ORDER, DEFAULT_STATE_USER_SETTINGS }; | ||
| export { DEFAULT_CATEGORIES_ORDER, DEFAULT_STATE_USER_SETTINGS, DOCK_RENDERERS_STATE_KEY }; |
@@ -25,2 +25,9 @@ export * from "devframe/constants"; | ||
| }; | ||
| /** | ||
| * 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 = "devframe:dock-renderers"; | ||
| const DEFAULT_STATE_USER_SETTINGS = () => ({ | ||
@@ -36,2 +43,2 @@ docksHidden: [], | ||
| //#endregion | ||
| export { DEFAULT_CATEGORIES_ORDER, DEFAULT_STATE_USER_SETTINGS }; | ||
| export { DEFAULT_CATEGORIES_ORDER, DEFAULT_STATE_USER_SETTINGS, DOCK_RENDERERS_STATE_KEY }; |
+5
-13
@@ -1,5 +0,5 @@ | ||
| import { A as DevframeViewLauncher, C as DevframeDocksHost, D as DevframeViewGroup, E as DevframeViewCustomRender, F as RemoteDockOptions, I as JsonRenderElement, L as JsonRenderSpec, M as FrameSubTabsConfig, N as NavTarget, O as DevframeViewIframe, P as RemoteConnectionInfo, R as JsonRenderer, S as DevframeDocksActiveState, T as DevframeViewBuiltin, _ as DevframeDockEntryBase, a as DevframeCommandEntry, b as DevframeDockEntryRegistry, c as DevframeCommandShortcutOverrides, d as DevframeServerCommandEntry, f as DevframeServerCommandInput, g as DevframeDockEntry, h as DevframeDockEntriesGrouped, i as DevframeCommandBase, j as DevframeViewLauncherStatus, k as DevframeViewJsonRender, 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 DevframeDockEntryCategory, w as DevframeViewAction, x as DevframeDockUserEntry, y as DevframeDockEntryIcon } from "./settings-Byh48aCd.mjs"; | ||
| import { DEFAULT_CATEGORIES_ORDER } from "./constants.mjs"; | ||
| import { C as DevframeMessageShortcutInput, D as DevframeMessagesListDelta, E as DevframeMessagesLevelShortcuts, S as DevframeMessageLevel, T as DevframeMessagesHost, _ as DevframeMessageEntry, a as DevframeChildProcessOutput, b as DevframeMessageFilePosition, c as DevframePtyExecuteOptions, d as DevframeTerminalSessionBase, f as DevframeTerminalStatus, g as DevframeMessageElementPosition, h as DevframeMessageActivateAction, i as DevframeChildProcessExecuteOptions, l as DevframePtyTerminalSession, m as DevframeMessageAction, n as DevframeHubContext, o as DevframeChildProcessResult, p as DevframeTerminalsHost, s as DevframeChildProcessTerminalSession, t as CreateHubContextOptions, u as DevframeTerminalSession, v as DevframeMessageEntryFrom, w as DevframeMessagesClient, x as DevframeMessageHandle, y as DevframeMessageEntryInput } from "./context-BSgzLnsu.mjs"; | ||
| import { C as RpcStreamingChannel, E as Thenable, S as RpcSharedStateHost, T as RpcStreamingHost, _ as RpcBroadcastOptions, a as DevframeDiagnosticsLogger, b as RpcFunctionsHost, c as DevframeRpcClientFunctions, d as DevframeViewHost, f as EntriesToObject, g as PartialWithoutId, h as EventsMap, i as DevframeDiagnosticsHost, l as DevframeRpcServerFunctions, m as EventUnsubscribe, n as DevframeCapabilities, o as DevframeHost, p as EventEmitter, r as DevframeDiagnosticsDefinition, s as DevframeNodeRpcSession, t as ConnectionMeta, u as DevframeRpcSharedStates, v as RpcDefinitionsFilter, w as RpcStreamingChannelOptions, x as RpcSharedStateGetOptions, y as RpcDefinitionsToFunctions } from "./index-IzywMKfP.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-Dz0j8Eyg.mjs"; | ||
| import { A as FrameSubTabsConfig, C as DevframeViewAction, D as DevframeViewIframe, E as DevframeViewGroup, M as RemoteConnectionInfo, N as RemoteDockOptions, O as DevframeViewLauncher, S as DevframeDocksHost, T as DevframeViewCustomRender, _ as DevframeDockEntryCategory, a as DevframeCommandHandle, b as DevframeDockUserEntry, c as DevframeCommandsHost, d as DevframeServerCommandInput, f as ClientScriptEntry, g as DevframeDockEntryBase, h as DevframeDockEntry, i as DevframeCommandEntry, j as NavTarget, k as DevframeViewLauncherStatus, l as DevframeCommandsHostEvents, m as DevframeDockEntriesGrouped, n as DevframeCommandAgentOptions, o as DevframeCommandKeybinding, p as DevframeDockActivation, r as DevframeCommandBase, s as DevframeCommandShortcutOverrides, t as DevframeClientCommand, u as DevframeServerCommandEntry, v as DevframeDockEntryIcon, w as DevframeViewBuiltin, x as DevframeDocksActiveState, y as DevframeDockEntryRegistry } from "./commands-CAcTUSlj.mjs"; | ||
| import { t as DevframeDocksUserSettings } from "./settings-B1qJ47d5.mjs"; | ||
| import { C as RpcStreamingChannel, E as Thenable, S as RpcSharedStateHost, T as RpcStreamingHost, _ as RpcBroadcastOptions, a as DevframeDiagnosticsLogger, b as RpcFunctionsHost, c as DevframeRpcClientFunctions, d as DevframeViewHost, f as EntriesToObject, g as PartialWithoutId, h as EventsMap, i as DevframeDiagnosticsHost, l as DevframeRpcServerFunctions, m as EventUnsubscribe, n as DevframeCapabilities, o as DevframeHost, p as EventEmitter, r as DevframeDiagnosticsDefinition, s as DevframeNodeRpcSession, t as ConnectionMeta, u as DevframeRpcSharedStates, v as RpcDefinitionsFilter, w as RpcStreamingChannelOptions, x as RpcSharedStateGetOptions, y as RpcDefinitionsToFunctions } from "./index-p4BktGvQ.mjs"; | ||
| import { WhenContext, WhenExpression } from "devframe/utils/when"; | ||
@@ -15,11 +15,3 @@ //#region src/define.d.ts | ||
| }): T; | ||
| /** | ||
| * @deprecated json-render moved out of the hub into the opt-in | ||
| * `@devframes/json-render` integration in 0.7. This identity helper is kept | ||
| * so existing imports keep compiling — pass your spec directly to | ||
| * `createJsonRenderView` (from `@devframes/json-render/node`) instead. | ||
| * Removed in 0.8. | ||
| */ | ||
| declare function defineJsonRenderSpec(spec: JsonRenderSpec): JsonRenderSpec; | ||
| //#endregion | ||
| export { type ClientScriptEntry, type ConnectionMeta, type CreateHubContextOptions, DEFAULT_CATEGORIES_ORDER, type DevframeCapabilities, type DevframeChildProcessExecuteOptions, type DevframeChildProcessOutput, type DevframeChildProcessResult, type DevframeChildProcessTerminalSession, type DevframeClientCommand, type DevframeCommandAgentOptions, type DevframeCommandBase, type DevframeCommandEntry, type DevframeCommandHandle, type DevframeCommandKeybinding, type DevframeCommandShortcutOverrides, type DevframeCommandsHost, type DevframeCommandsHostEvents, type DevframeDiagnosticsDefinition, type DevframeDiagnosticsHost, type DevframeDiagnosticsLogger, type DevframeDockActivation, type DevframeDockEntriesGrouped, type DevframeDockEntry, type DevframeDockEntryBase, type DevframeDockEntryCategory, type DevframeDockEntryIcon, type DevframeDockEntryRegistry, type DevframeDockUserEntry, type DevframeDocksActiveState, type DevframeDocksHost, type DevframeDocksUserSettings, type DevframeHost, type DevframeHubContext, type DevframeMessageAction, type DevframeMessageActivateAction, type DevframeMessageElementPosition, type DevframeMessageEntry, type DevframeMessageEntryFrom, type DevframeMessageEntryInput, type DevframeMessageFilePosition, type DevframeMessageHandle, type DevframeMessageLevel, type DevframeMessageShortcutInput, type DevframeMessagesClient, type DevframeMessagesHost, type DevframeMessagesLevelShortcuts, type DevframeMessagesListDelta, type DevframeNodeRpcSession, type DevframePtyExecuteOptions, type DevframePtyTerminalSession, type DevframeRpcClientFunctions, type DevframeRpcServerFunctions, type DevframeRpcSharedStates, type DevframeServerCommandEntry, type DevframeServerCommandInput, type DevframeTerminalSession, type DevframeTerminalSessionBase, type DevframeTerminalStatus, type DevframeTerminalsHost, type DevframeViewAction, type DevframeViewBuiltin, type DevframeViewCustomRender, type DevframeViewGroup, type DevframeViewHost, type DevframeViewIframe, type DevframeViewJsonRender, type DevframeViewLauncher, type DevframeViewLauncherStatus, type EntriesToObject, type EventEmitter, type EventUnsubscribe, type EventsMap, type FrameSubTabsConfig, type JsonRenderElement, type JsonRenderSpec, type JsonRenderer, type NavTarget, type PartialWithoutId, type RemoteConnectionInfo, type RemoteDockOptions, type RpcBroadcastOptions, type RpcDefinitionsFilter, type RpcDefinitionsToFunctions, type RpcFunctionsHost, type RpcSharedStateGetOptions, type RpcSharedStateHost, type RpcStreamingChannel, type RpcStreamingChannelOptions, type RpcStreamingHost, type Thenable, defineCommand, defineDockEntry, defineHubRpcFunction, defineJsonRenderSpec }; | ||
| export { type ClientScriptEntry, type ConnectionMeta, type CreateHubContextOptions, type DevframeCapabilities, type DevframeChildProcessExecuteOptions, type DevframeChildProcessOutput, type DevframeChildProcessResult, type DevframeChildProcessTerminalSession, type DevframeClientCommand, type DevframeCommandAgentOptions, type DevframeCommandBase, type DevframeCommandEntry, type DevframeCommandHandle, type DevframeCommandKeybinding, type DevframeCommandShortcutOverrides, type DevframeCommandsHost, type DevframeCommandsHostEvents, type DevframeDiagnosticsDefinition, type DevframeDiagnosticsHost, type DevframeDiagnosticsLogger, type DevframeDockActivation, type DevframeDockEntriesGrouped, type DevframeDockEntry, type DevframeDockEntryBase, type DevframeDockEntryCategory, type DevframeDockEntryIcon, type DevframeDockEntryRegistry, type DevframeDockUserEntry, type DevframeDocksActiveState, type DevframeDocksHost, type DevframeDocksUserSettings, type DevframeHost, type DevframeHubContext, type DevframeMessageAction, type DevframeMessageActivateAction, type DevframeMessageCommandAction, type DevframeMessageElementPosition, type DevframeMessageEntry, type DevframeMessageEntryFrom, type DevframeMessageEntryInput, type DevframeMessageFilePosition, type DevframeMessageHandle, type DevframeMessageLevel, type DevframeMessageShortcutInput, type DevframeMessagesClient, type DevframeMessagesHost, type DevframeMessagesLevelShortcuts, type DevframeMessagesListDelta, type DevframeNodeRpcSession, type DevframePtyExecuteOptions, type DevframePtyTerminalSession, type DevframeRpcClientFunctions, type DevframeRpcServerFunctions, type DevframeRpcSharedStates, type DevframeServerCommandEntry, type DevframeServerCommandInput, type DevframeTerminalSession, type DevframeTerminalSessionBase, type DevframeTerminalStatus, type DevframeTerminalsHost, type DevframeViewAction, type DevframeViewBuiltin, type DevframeViewCustomRender, type DevframeViewGroup, type DevframeViewHost, type DevframeViewIframe, type DevframeViewLauncher, type DevframeViewLauncherStatus, type EntriesToObject, type EventEmitter, type EventUnsubscribe, type EventsMap, type FrameSubTabsConfig, type NavTarget, type PartialWithoutId, type RemoteConnectionInfo, type RemoteDockOptions, type RpcBroadcastOptions, type RpcDefinitionsFilter, type RpcDefinitionsToFunctions, type RpcFunctionsHost, type RpcSharedStateGetOptions, type RpcSharedStateHost, type RpcStreamingChannel, type RpcStreamingChannelOptions, type RpcStreamingHost, type Thenable, defineCommand, defineDockEntry, defineHubRpcFunction }; |
+2
-3
@@ -1,3 +0,2 @@ | ||
| import { DEFAULT_CATEGORIES_ORDER } from "./constants.mjs"; | ||
| import { i as defineJsonRenderSpec, n as defineDockEntry, r as defineHubRpcFunction, t as defineCommand } from "./define-Pbx8dsKz.mjs"; | ||
| export { DEFAULT_CATEGORIES_ORDER, defineCommand, defineDockEntry, defineHubRpcFunction, defineJsonRenderSpec }; | ||
| import { n as defineDockEntry, r as defineHubRpcFunction, t as defineCommand } from "./define-Ceekw2EO.mjs"; | ||
| export { defineCommand, defineDockEntry, defineHubRpcFunction }; |
@@ -1,6 +0,5 @@ | ||
| import { C as DevframeDocksHost$1, O as DevframeViewIframe, d as DevframeServerCommandEntry, f as DevframeServerCommandInput, g as DevframeDockEntry, l as DevframeCommandsHost$1, o as DevframeCommandHandle, p as ClientScriptEntry, t as DevframeDocksUserSettings, x as DevframeDockUserEntry } from "../settings-Byh48aCd.mjs"; | ||
| import { DEFAULT_CATEGORIES_ORDER } from "../constants.mjs"; | ||
| import { C as DevframeMessageShortcutInput, D as DevframeMessagesListDelta, T as DevframeMessagesHost$1, _ as DevframeMessageEntry, c as DevframePtyExecuteOptions, d as DevframeTerminalSessionBase, i as DevframeChildProcessExecuteOptions, l as DevframePtyTerminalSession, n as DevframeHubContext, p as DevframeTerminalsHost$1, r as createHubContext, s as DevframeChildProcessTerminalSession, t as CreateHubContextOptions, u as DevframeTerminalSession, x as DevframeMessageHandle, y as DevframeMessageEntryInput } from "../context-BSgzLnsu.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-Dz0j8Eyg.mjs"; | ||
| import { S as DevframeDocksHost$1, a as DevframeCommandHandle, b as DevframeDockUserEntry, c as DevframeCommandsHost$1, d as DevframeServerCommandInput, f as ClientScriptEntry, h as DevframeDockEntry, u as DevframeServerCommandEntry } from "../commands-CAcTUSlj.mjs"; | ||
| import { t as DevframeDocksUserSettings } from "../settings-B1qJ47d5.mjs"; | ||
| import { RpcFunctionDefinitionAny } from "devframe/rpc"; | ||
| import { DevframeDefinition } from "devframe/types"; | ||
| import { SharedState } from "devframe/utils/shared-state"; | ||
@@ -123,30 +122,2 @@ //#region src/node/host-commands.d.ts | ||
| //#endregion | ||
| //#region src/node/mount-devframe.d.ts | ||
| interface MountDevframeOptions { | ||
| /** | ||
| * 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'>>; | ||
| } | ||
| /** | ||
| * Framework-neutral primitive — mounts 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)`. | ||
| * | ||
| * Framework kits wrap this with their own plugin/middleware machinery — | ||
| * e.g. `@vitejs/devtools-kit`'s `createPluginFromDevframe` returns a | ||
| * Vite `Plugin` whose `devtools.setup` ultimately delegates here. | ||
| */ | ||
| declare function mountDevframe(ctx: DevframeHubContext, d: DevframeDefinition, options?: MountDevframeOptions): Promise<void>; | ||
| //#endregion | ||
| //#region src/node/rpc-builtins.d.ts | ||
@@ -405,2 +376,2 @@ /** | ||
| //#endregion | ||
| export { CreateHubContextOptions, DEFAULT_CATEGORIES_ORDER, DevframeCommandsHost, DevframeDocksHost, DevframeHubContext, DevframeMessagesHost, DevframeTerminalsHost, MountDevframeOptions, builtinHubRpcDeclarations, createHubContext, createSimpleClientScript, hubCommandsExecute, hubDocksActivate, hubMessagesAdd, hubMessagesClear, hubMessagesRemove, hubMessagesUpdate, hubTerminalsRemove, hubTerminalsResize, hubTerminalsRestart, hubTerminalsTerminate, hubTerminalsWrite, mountDevframe }; | ||
| export { CreateHubContextOptions, DevframeCommandsHost, DevframeDocksHost, DevframeHubContext, DevframeMessagesHost, DevframeTerminalsHost, type InstallDevframeOptions, builtinHubRpcDeclarations, createHubContext, createSimpleClientScript, hubCommandsExecute, hubDocksActivate, hubMessagesAdd, hubMessagesClear, hubMessagesRemove, hubMessagesUpdate, hubTerminalsRemove, hubTerminalsResize, hubTerminalsRestart, hubTerminalsTerminate, hubTerminalsWrite }; |
@@ -1,4 +0,5 @@ | ||
| import { A as DevframeViewLauncher, C as DevframeDocksHost, D as DevframeViewGroup, E as DevframeViewCustomRender, F as RemoteDockOptions, I as JsonRenderElement, L as JsonRenderSpec, M as FrameSubTabsConfig, N as NavTarget, O as DevframeViewIframe, P as RemoteConnectionInfo, R as JsonRenderer, S as DevframeDocksActiveState, T as DevframeViewBuiltin, _ as DevframeDockEntryBase, a as DevframeCommandEntry, b as DevframeDockEntryRegistry, c as DevframeCommandShortcutOverrides, d as DevframeServerCommandEntry, f as DevframeServerCommandInput, g as DevframeDockEntry, h as DevframeDockEntriesGrouped, i as DevframeCommandBase, j as DevframeViewLauncherStatus, k as DevframeViewJsonRender, 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 DevframeDockEntryCategory, w as DevframeViewAction, x as DevframeDockUserEntry, y as DevframeDockEntryIcon } from "../settings-Byh48aCd.mjs"; | ||
| import { C as DevframeMessageShortcutInput, D as DevframeMessagesListDelta, E as DevframeMessagesLevelShortcuts, S as DevframeMessageLevel, T as DevframeMessagesHost, _ as DevframeMessageEntry, a as DevframeChildProcessOutput, b as DevframeMessageFilePosition, c as DevframePtyExecuteOptions, d as DevframeTerminalSessionBase, f as DevframeTerminalStatus, g as DevframeMessageElementPosition, h as DevframeMessageActivateAction, i as DevframeChildProcessExecuteOptions, l as DevframePtyTerminalSession, m as DevframeMessageAction, n as DevframeHubContext, o as DevframeChildProcessResult, p as DevframeTerminalsHost, s as DevframeChildProcessTerminalSession, t as CreateHubContextOptions, u as DevframeTerminalSession, v as DevframeMessageEntryFrom, w as DevframeMessagesClient, x as DevframeMessageHandle, y as DevframeMessageEntryInput } from "../context-BSgzLnsu.mjs"; | ||
| import { C as RpcStreamingChannel, E as Thenable, S as RpcSharedStateHost, T as RpcStreamingHost, _ as RpcBroadcastOptions, a as DevframeDiagnosticsLogger, b as RpcFunctionsHost, c as DevframeRpcClientFunctions, d as DevframeViewHost, f as EntriesToObject, g as PartialWithoutId, h as EventsMap, i as DevframeDiagnosticsHost, l as DevframeRpcServerFunctions, m as EventUnsubscribe, n as DevframeCapabilities, o as DevframeHost, p as EventEmitter, r as DevframeDiagnosticsDefinition, s as DevframeNodeRpcSession, t as ConnectionMeta, u as DevframeRpcSharedStates, v as RpcDefinitionsFilter, w as RpcStreamingChannelOptions, x as RpcSharedStateGetOptions, y as RpcDefinitionsToFunctions } from "../index-IzywMKfP.mjs"; | ||
| export { ClientScriptEntry, type ConnectionMeta, type CreateHubContextOptions, type DevframeCapabilities, DevframeChildProcessExecuteOptions, DevframeChildProcessOutput, DevframeChildProcessResult, DevframeChildProcessTerminalSession, DevframeClientCommand, DevframeCommandAgentOptions, DevframeCommandBase, DevframeCommandEntry, DevframeCommandHandle, DevframeCommandKeybinding, DevframeCommandShortcutOverrides, DevframeCommandsHost, DevframeCommandsHostEvents, type DevframeDiagnosticsDefinition, type DevframeDiagnosticsHost, type DevframeDiagnosticsLogger, DevframeDockActivation, DevframeDockEntriesGrouped, DevframeDockEntry, DevframeDockEntryBase, DevframeDockEntryCategory, DevframeDockEntryIcon, DevframeDockEntryRegistry, DevframeDockUserEntry, DevframeDocksActiveState, DevframeDocksHost, DevframeDocksUserSettings, type DevframeHost, type DevframeHubContext, DevframeMessageAction, DevframeMessageActivateAction, 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, DevframeViewJsonRender, DevframeViewLauncher, DevframeViewLauncherStatus, type EntriesToObject, type EventEmitter, type EventUnsubscribe, type EventsMap, FrameSubTabsConfig, JsonRenderElement, JsonRenderSpec, JsonRenderer, NavTarget, type PartialWithoutId, RemoteConnectionInfo, RemoteDockOptions, type RpcBroadcastOptions, type RpcDefinitionsFilter, type RpcDefinitionsToFunctions, type RpcFunctionsHost, type RpcSharedStateGetOptions, type RpcSharedStateHost, type RpcStreamingChannel, type RpcStreamingChannelOptions, type RpcStreamingHost, type Thenable }; | ||
| 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-Dz0j8Eyg.mjs"; | ||
| import { A as FrameSubTabsConfig, C as DevframeViewAction, D as DevframeViewIframe, E as DevframeViewGroup, M as RemoteConnectionInfo, N as RemoteDockOptions, O as DevframeViewLauncher, S as DevframeDocksHost, T as DevframeViewCustomRender, _ as DevframeDockEntryCategory, a as DevframeCommandHandle, b as DevframeDockUserEntry, c as DevframeCommandsHost, d as DevframeServerCommandInput, f as ClientScriptEntry, g as DevframeDockEntryBase, h as DevframeDockEntry, i as DevframeCommandEntry, j as NavTarget, k as DevframeViewLauncherStatus, l as DevframeCommandsHostEvents, m as DevframeDockEntriesGrouped, n as DevframeCommandAgentOptions, o as DevframeCommandKeybinding, p as DevframeDockActivation, r as DevframeCommandBase, s as DevframeCommandShortcutOverrides, t as DevframeClientCommand, u as DevframeServerCommandEntry, v as DevframeDockEntryIcon, w as DevframeViewBuiltin, x as DevframeDocksActiveState, y as DevframeDockEntryRegistry } from "../commands-CAcTUSlj.mjs"; | ||
| import { t as DevframeDocksUserSettings } from "../settings-B1qJ47d5.mjs"; | ||
| import { C as RpcStreamingChannel, E as Thenable, S as RpcSharedStateHost, T as RpcStreamingHost, _ as RpcBroadcastOptions, a as DevframeDiagnosticsLogger, b as RpcFunctionsHost, c as DevframeRpcClientFunctions, d as DevframeViewHost, f as EntriesToObject, g as PartialWithoutId, h as EventsMap, i as DevframeDiagnosticsHost, l as DevframeRpcServerFunctions, m as EventUnsubscribe, n as DevframeCapabilities, o as DevframeHost, p as EventEmitter, r as DevframeDiagnosticsDefinition, s as DevframeNodeRpcSession, t as ConnectionMeta, u as DevframeRpcSharedStates, v as RpcDefinitionsFilter, w as RpcStreamingChannelOptions, x as RpcSharedStateGetOptions, y as RpcDefinitionsToFunctions } from "../index-p4BktGvQ.mjs"; | ||
| export { ClientScriptEntry, type ConnectionMeta, type CreateHubContextOptions, type DevframeCapabilities, DevframeChildProcessExecuteOptions, DevframeChildProcessOutput, DevframeChildProcessResult, DevframeChildProcessTerminalSession, DevframeClientCommand, DevframeCommandAgentOptions, DevframeCommandBase, DevframeCommandEntry, DevframeCommandHandle, DevframeCommandKeybinding, DevframeCommandShortcutOverrides, DevframeCommandsHost, DevframeCommandsHostEvents, type DevframeDiagnosticsDefinition, type DevframeDiagnosticsHost, type DevframeDiagnosticsLogger, DevframeDockActivation, 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 EntriesToObject, type EventEmitter, type EventUnsubscribe, type EventsMap, FrameSubTabsConfig, NavTarget, type PartialWithoutId, RemoteConnectionInfo, RemoteDockOptions, type RpcBroadcastOptions, type RpcDefinitionsFilter, type RpcDefinitionsToFunctions, type RpcFunctionsHost, type RpcSharedStateGetOptions, type RpcSharedStateHost, type RpcStreamingChannel, type RpcStreamingChannelOptions, type RpcStreamingHost, type Thenable }; |
+9
-5
| { | ||
| "name": "@devframes/hub", | ||
| "type": "module", | ||
| "version": "0.8.2", | ||
| "description": "Framework-neutral hub layer for devframe — docks, terminals, messages, commands.", | ||
| "version": "0.9.0-beta.1", | ||
| "description": "Hub layer that orchestrates devframe docks, terminals, messages, and commands on any host.", | ||
| "author": "Anthony Fu <anthonyfu117@hotmail.com>", | ||
@@ -27,2 +27,3 @@ "license": "MIT", | ||
| "./constants": "./dist/constants.mjs", | ||
| "./initiate": "./dist/node/initiate.mjs", | ||
| "./node": "./dist/node/index.mjs", | ||
@@ -37,3 +38,3 @@ "./types": "./dist/types/index.mjs", | ||
| "peerDependencies": { | ||
| "devframe": "0.8.2" | ||
| "devframe": "0.9.0-beta.1" | ||
| }, | ||
@@ -43,2 +44,3 @@ "dependencies": { | ||
| "destr": "^2.0.5", | ||
| "h3": "^2.0.1-rc.26", | ||
| "nostics": "^1.2.0", | ||
@@ -48,10 +50,12 @@ "pathe": "^2.0.3", | ||
| "tinyexec": "^1.3.0", | ||
| "ufo": "^1.6.4", | ||
| "zigpty": "^0.2.1" | ||
| }, | ||
| "devDependencies": { | ||
| "@types/node": "^26.1.2", | ||
| "@types/node": "^26.2.0", | ||
| "get-port-please": "^3.2.0", | ||
| "mlly": "^1.8.2", | ||
| "tsdown": "^0.22.14", | ||
| "valibot": "^1.4.2", | ||
| "devframe": "0.8.2" | ||
| "devframe": "0.9.0-beta.1" | ||
| }, | ||
@@ -58,0 +62,0 @@ "scripts": { |
| import { C as DevframeDocksHost, L as JsonRenderSpec, R as JsonRenderer, l as DevframeCommandsHost, m as DevframeDockActivation, y as DevframeDockEntryIcon } from "./settings-Byh48aCd.mjs"; | ||
| import { CreateHostContextOptions } from "devframe/node"; | ||
| import { 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>; | ||
| }; | ||
| } | ||
| type DevframeMessageAction = DevframeMessageActivateAction; | ||
| 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<{ | ||
| 'message:added': (entry: DevframeMessageEntry) => void; | ||
| 'message:updated': (entry: DevframeMessageEntry) => void; | ||
| 'message:removed': (id: string) => void; | ||
| 'message: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<{ | ||
| 'terminal: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/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`) and the deprecated | ||
| * `createJsonRenderer` compatibility factory. | ||
| * | ||
| * 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 itself is not part of the hub: | ||
| * it is an opt-in integration (`@devframes/json-render`) that augments any | ||
| * devframe context and contributes its own dock type — prefer | ||
| * `createJsonRenderView` from `@devframes/json-render/node` over the | ||
| * deprecated factory below. | ||
| */ | ||
| interface DevframeHubContext extends DevframeNodeContext { | ||
| readonly host: DevframeHost; | ||
| docks: DevframeDocksHost; | ||
| terminals: DevframeTerminalsHost; | ||
| messages: DevframeMessagesHost; | ||
| commands: DevframeCommandsHost; | ||
| /** | ||
| * Create a `JsonRenderer` handle for building json-render powered UIs. | ||
| * | ||
| * @deprecated json-render moved out of the hub into the opt-in | ||
| * `@devframes/json-render` integration in 0.7. This factory is kept | ||
| * working (not just type-compatible) for the 0.7 series so existing call | ||
| * sites don't break — use `createJsonRenderView(ctx, { id, spec })` from | ||
| * `@devframes/json-render/node` instead. Will be removed in 0.8. | ||
| */ | ||
| createJsonRenderer: (spec: JsonRenderSpec) => JsonRenderer; | ||
| } | ||
| /** | ||
| * 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 { DevframeMessageShortcutInput as C, DevframeMessagesListDelta as D, DevframeMessagesLevelShortcuts as E, DevframeMessageLevel as S, DevframeMessagesHost as T, DevframeMessageEntry as _, DevframeChildProcessOutput as a, DevframeMessageFilePosition as b, DevframePtyExecuteOptions as c, DevframeTerminalSessionBase as d, DevframeTerminalStatus as f, DevframeMessageElementPosition as g, DevframeMessageActivateAction as h, DevframeChildProcessExecuteOptions as i, DevframePtyTerminalSession as l, DevframeMessageAction as m, DevframeHubContext as n, DevframeChildProcessResult as o, DevframeTerminalsHost as p, createHubContext as r, DevframeChildProcessTerminalSession as s, CreateHubContextOptions as t, DevframeTerminalSession as u, DevframeMessageEntryFrom as v, DevframeMessagesClient as w, DevframeMessageHandle as x, DevframeMessageEntryInput as y }; |
| import { createDefineWrapperWithContext } from "devframe/rpc"; | ||
| //#region src/define.ts | ||
| const defineHubRpcFunction = createDefineWrapperWithContext(); | ||
| function defineCommand(command) { | ||
| return command; | ||
| } | ||
| function defineDockEntry(entry) { | ||
| return entry; | ||
| } | ||
| /** | ||
| * @deprecated json-render moved out of the hub into the opt-in | ||
| * `@devframes/json-render` integration in 0.7. This identity helper is kept | ||
| * so existing imports keep compiling — pass your spec directly to | ||
| * `createJsonRenderView` (from `@devframes/json-render/node`) instead. | ||
| * Removed in 0.8. | ||
| */ | ||
| function defineJsonRenderSpec(spec) { | ||
| return spec; | ||
| } | ||
| //#endregion | ||
| export { defineJsonRenderSpec as i, defineDockEntry as n, defineHubRpcFunction as r, defineCommand as t }; |
| import "./settings-Byh48aCd.mjs"; | ||
| import "./context-BSgzLnsu.mjs"; | ||
| import { RpcDefinitionsFilter, RpcDefinitionsToFunctions } from "devframe/rpc"; | ||
| import { ConnectionMeta as ConnectionMeta$1, DevframeCapabilities, DevframeDiagnosticsDefinition, DevframeDiagnosticsHost, DevframeDiagnosticsLogger, DevframeHost as DevframeHost$1, DevframeNodeRpcSession, DevframeRpcClientFunctions, DevframeRpcServerFunctions, DevframeRpcSharedStates, DevframeViewHost, EntriesToObject, EventEmitter as EventEmitter$1, EventUnsubscribe, EventsMap, PartialWithoutId, RpcBroadcastOptions, RpcFunctionsHost, RpcSharedStateGetOptions, RpcSharedStateHost, RpcStreamingChannel, RpcStreamingChannelOptions, RpcStreamingHost, Thenable } from "devframe/types"; | ||
| export { RpcStreamingChannel as C, Thenable as E, RpcSharedStateHost as S, RpcStreamingHost as T, RpcBroadcastOptions as _, DevframeDiagnosticsLogger as a, RpcFunctionsHost as b, DevframeRpcClientFunctions as c, DevframeViewHost as d, EntriesToObject as f, PartialWithoutId as g, EventsMap as h, DevframeDiagnosticsHost as i, DevframeRpcServerFunctions as l, EventUnsubscribe as m, DevframeCapabilities as n, DevframeHost$1 as o, EventEmitter$1 as p, DevframeDiagnosticsDefinition as r, DevframeNodeRpcSession as s, ConnectionMeta$1 as t, DevframeRpcSharedStates as u, RpcDefinitionsFilter as v, RpcStreamingChannelOptions as w, RpcSharedStateGetOptions as x, RpcDefinitionsToFunctions as y }; |
| import { StandardSchemaV1 } from "@standard-schema/spec"; | ||
| import { ConnectionMeta, EventEmitter } from "devframe/types"; | ||
| //#region src/types/json-render.d.ts | ||
| /** @deprecated Use `DevframeJsonRenderSpec`'s element shape from `@devframes/json-render` instead. Removed in 0.8. */ | ||
| interface JsonRenderElement { | ||
| type: string; | ||
| props?: Record<string, unknown>; | ||
| children?: string[]; | ||
| /** json-render event bindings (e.g. `{ press: { action: "my:action" } }`) */ | ||
| on?: Record<string, unknown>; | ||
| /** json-render visibility condition */ | ||
| visible?: unknown; | ||
| /** json-render repeat binding */ | ||
| repeat?: unknown; | ||
| /** Allow additional json-render element fields */ | ||
| [key: string]: unknown; | ||
| } | ||
| /** @deprecated Use `DevframeJsonRenderSpec` from `@devframes/json-render` instead. Removed in 0.8. */ | ||
| interface JsonRenderSpec { | ||
| root: string; | ||
| elements: Record<string, JsonRenderElement>; | ||
| /** Initial client-side state model for $state/$bindState expressions */ | ||
| state?: Record<string, unknown>; | ||
| } | ||
| /** @deprecated Use `JsonRenderView` from `@devframes/json-render` instead. Removed in 0.8. */ | ||
| interface JsonRenderer { | ||
| /** Replace the entire spec */ | ||
| updateSpec: (spec: JsonRenderSpec) => void | Promise<void>; | ||
| /** Update json-render state values (shallow merge into spec.state) */ | ||
| updateState: (state: Record<string, unknown>) => void | Promise<void>; | ||
| /** Internal: shared state key used by the client to subscribe */ | ||
| readonly _stateKey: string; | ||
| } | ||
| //#endregion | ||
| //#region src/types/docks.d.ts | ||
| interface DevframeDocksHost { | ||
| readonly views: Map<string, DevframeDockUserEntry>; | ||
| readonly events: EventEmitter<{ | ||
| 'dock:entry:updated': (entry: DevframeDockUserEntry) => void; | ||
| 'dock:activate': (activation: DevframeDockActivation) => void; | ||
| }>; | ||
| register: <T extends DevframeDockUserEntry>(entry: T, force?: boolean) => { | ||
| update: (patch: Partial<T>) => void; | ||
| }; | ||
| update: (entry: DevframeDockUserEntry) => void; | ||
| values: () => DevframeDockEntry[]; | ||
| /** | ||
| * Request the active viewer switch its focused dock to `dockId`, optionally | ||
| * carrying `params` for the target dock to interpret (e.g. a terminals | ||
| * session id). | ||
| * | ||
| * Any connected client may drive this via the `hub:docks:activate` RPC — a | ||
| * mounted devframe running in its own iframe can steer the host shell's dock | ||
| * selection, which is otherwise client-local. The request is delivered live | ||
| * to connected clients (broadcast) and mirrored into the | ||
| * `devframe:docks:active` shared state so a dock that mounts in response | ||
| * still sees it. Activation is best-effort: unknown dock ids degrade | ||
| * gracefully. | ||
| */ | ||
| activate: (dockId: string, params?: Record<string, unknown>) => void; | ||
| } | ||
| /** | ||
| * A request to switch the active dock. `params` is an opaque, serializable | ||
| * bag the target dock interprets — the terminals dock reads `params.sessionId` | ||
| * to focus a specific session. | ||
| */ | ||
| interface DevframeDockActivation { | ||
| dockId: string; | ||
| params?: Record<string, unknown>; | ||
| } | ||
| /** | ||
| * Shape of the `devframe:docks:active` shared-state slot — the most recent | ||
| * {@link DevframeDockActivation}, or `null` before any activation. Mirrored | ||
| * so a dock that mounts in response to an activation can still converge on the | ||
| * request instead of missing the live broadcast. | ||
| */ | ||
| interface DevframeDocksActiveState { | ||
| activation: DevframeDockActivation | null; | ||
| } | ||
| type DevframeDockEntryCategory = 'framework' | 'app' | 'ui' | 'data' | 'web' | 'performance' | 'advanced' | 'docs' | 'default' | '~builtin' | (string & {}); | ||
| type DevframeDockEntryIcon = string | { | ||
| light: string; | ||
| dark: string; | ||
| }; | ||
| interface DevframeDockEntryBase { | ||
| id: string; | ||
| title: string; | ||
| icon: DevframeDockEntryIcon; | ||
| /** | ||
| * The default order of the entry in the dock. | ||
| * The higher the number the earlier it appears. | ||
| * @default 0 | ||
| */ | ||
| defaultOrder?: number; | ||
| /** | ||
| * The category of the entry — a field with a dual role that depends on | ||
| * whether {@link groupId} resolves to a registered {@link DevframeViewGroup}: | ||
| * | ||
| * - **Ungrouped (or orphan) entry** — `category` is the entry's OUTER bucket | ||
| * on the dock bar, ordered by {@link import('../constants').DEFAULT_CATEGORIES_ORDER}. | ||
| * - **Grouped entry** (a `groupId` that resolves to a registered group) — | ||
| * the OUTER bucket is instead the group's own `category`, and this field is | ||
| * reinterpreted as the entry's IN-GROUP sub-category, used to sub-divide and | ||
| * sort members inside the group's popover / sub-navigation. | ||
| * | ||
| * Falls back to `'default'` when omitted — both as an outer bucket and, for a | ||
| * grouped member, as its in-group sub-bucket. | ||
| * | ||
| * @default 'default' | ||
| */ | ||
| category?: DevframeDockEntryCategory; | ||
| /** | ||
| * Conditional visibility expression. | ||
| * When set, the dock entry is only visible when the expression evaluates to true. | ||
| * Uses the same syntax as command `when` clauses. | ||
| * | ||
| * Set to `'false'` to unconditionally hide the entry. | ||
| * | ||
| * @example 'clientType == embedded' | ||
| * @see {@link import('devframe/utils/when').evaluateWhen} | ||
| */ | ||
| when?: string; | ||
| /** | ||
| * Render-only conditional visibility expression, same syntax as {@link when}. | ||
| * When it evaluates to `false`, a viewer omits the entry from the rendered | ||
| * dock bar / list, but the entry stays registered and fully reachable — | ||
| * `docks.activate()`/`switchEntry()` by id, RPC lookups, and anything else | ||
| * that walks the raw entry list (e.g. the {@link DevframeViewIframe.subTabs} | ||
| * frame-nav adapter) keep working exactly as if it were visible. | ||
| * | ||
| * Use this instead of {@link when} when an entry must remain part of the | ||
| * model without a dock-bar button of its own — the canonical case is a | ||
| * shared-frame {@link DevframeViewIframe.subTabs anchor}: set | ||
| * `visibility: 'false'` on the anchor so only its synthesized member tabs | ||
| * render, while the anchor itself keeps driving the postMessage nav loop. | ||
| * `when`, by contrast, is the general relevance switch for the entry as a | ||
| * whole; reach for `visibility` only for this render-only carve-out. | ||
| * | ||
| * Set to `'false'` to unconditionally hide the entry's own dock-bar button. | ||
| * | ||
| * @example 'false' | ||
| * @see {@link import('devframe/utils/when').evaluateWhen} | ||
| */ | ||
| visibility?: string; | ||
| /** | ||
| * Badge text to display on the dock icon (e.g., unread count) | ||
| */ | ||
| badge?: string; | ||
| /** | ||
| * Id of the group this entry belongs to. When set, hosts collapse this entry | ||
| * under the matching group's button instead of showing it directly on the | ||
| * dock bar. | ||
| * | ||
| * This is a flat pointer — membership, not containment. The entry stays an | ||
| * independently-registered, top-level entry; only its rendering is grouped | ||
| * downstream. | ||
| * | ||
| * When the referenced group **is** registered, it supplies the entry's OUTER | ||
| * dock-bar category (the group's own {@link category}), and this entry's own | ||
| * {@link category} is reinterpreted as its IN-GROUP sub-category. When the | ||
| * referenced group is **never** registered, the entry renders as a normal | ||
| * top-level entry and falls back to using its own {@link category} as the | ||
| * outer bucket (orphan tolerance). | ||
| * | ||
| * @see {@link DevframeViewGroup} | ||
| */ | ||
| groupId?: string; | ||
| } | ||
| interface ClientScriptEntry { | ||
| /** | ||
| * The filepath or module name to import from | ||
| */ | ||
| importFrom: string; | ||
| /** | ||
| * The name to import the module as | ||
| * | ||
| * @default 'default' | ||
| */ | ||
| importName?: string; | ||
| } | ||
| interface DevframeViewIframe extends DevframeDockEntryBase { | ||
| type: 'iframe'; | ||
| url: string; | ||
| /** | ||
| * The id of the iframe, if multiple tabs is assigned with the same id, the iframe will be shared. | ||
| * | ||
| * When not provided, it would be treated as a unique frame. | ||
| * | ||
| * `frameId` is an axis independent of {@link DevframeDockEntryBase.groupId}: | ||
| * it decides *which* iframe element a dock renders into (and which soft-nav | ||
| * pool it joins), while `groupId` only affects dock-bar grouping. Docks that | ||
| * share a `frameId` may live in one group, several groups, or none. | ||
| */ | ||
| frameId?: string; | ||
| /** | ||
| * Optional client script to import into the iframe | ||
| */ | ||
| clientScript?: ClientScriptEntry; | ||
| /** | ||
| * Soft-navigation target within a shared frame. Set on a **member** dock | ||
| * (one of several docks sharing a {@link frameId}) to describe which internal | ||
| * view the embedded app should show. The hub treats {@link NavTarget.path} as | ||
| * opaque and hands it to the frame's nav shim over `postMessage`; switching to | ||
| * this dock performs client-side navigation instead of reloading the iframe. | ||
| * | ||
| * The anchor dock (the one flagged with {@link subTabs}) leaves this unset. | ||
| */ | ||
| navTarget?: NavTarget; | ||
| /** | ||
| * Marks this iframe as a **shared-frame anchor** whose sub-tabs are discovered | ||
| * at runtime over a host↔iframe `postMessage` protocol. The client host | ||
| * auto-attaches the frame-nav adapter when this iframe mounts: it runs the | ||
| * ready handshake, materializes one client-only member dock per reported tab | ||
| * (grouped/soft-navigated via this anchor's {@link frameId}), and drives the | ||
| * live navigation loop. Absent a shim, the anchor simply renders as a single | ||
| * plain iframe dock. | ||
| * | ||
| * Set {@link DevframeDockEntryBase.visibility} to `'false'` on the anchor to | ||
| * hide its own dock-bar button once tabs are discovered, surfacing only the | ||
| * synthesized member docks while the anchor keeps driving the nav loop. | ||
| */ | ||
| subTabs?: FrameSubTabsConfig; | ||
| /** | ||
| * Enable remote-UI mode: the hub injects a connection descriptor | ||
| * (WS URL + pre-approved auth token) into the iframe URL so a hosted | ||
| * page can connect back via `connectRemoteDevframe()` from | ||
| * `@devframes/hub/client` — without needing to ship a dist with the | ||
| * plugin. | ||
| * | ||
| * Requires dev mode (no effect in build mode — no WS server exists). | ||
| * When enabled, the dock is automatically hidden in build mode unless | ||
| * the author provides an explicit `when` clause. | ||
| */ | ||
| remote?: boolean | RemoteDockOptions; | ||
| } | ||
| /** | ||
| * A structured, soft-navigation target within a shared frame. `path` is opaque | ||
| * to the hub — the embedded app maps it onto its own router. | ||
| * | ||
| * Kept to `path` + `query` so the shape survives shared-state's `Immutable` | ||
| * projection cleanly (a `DevframeViewIframe` must still narrow back from its | ||
| * immutable form). An `unknown`/recursive history-`state` field breaks that | ||
| * round-trip, so richer per-navigation state is intentionally out of scope for | ||
| * now — carry it in `query` or the app's own store. | ||
| */ | ||
| interface NavTarget { | ||
| path: string; | ||
| query?: Record<string, string | readonly string[]>; | ||
| } | ||
| /** | ||
| * Configuration for a {@link DevframeViewIframe.subTabs shared-frame anchor}. | ||
| */ | ||
| interface FrameSubTabsConfig { | ||
| /** Transport for tab discovery + the live nav loop. */ | ||
| protocol: 'postmessage'; | ||
| /** | ||
| * How long (ms) the adapter waits for the shim's `ready` before treating the | ||
| * frame as having no shim (the anchor renders as a single plain iframe dock, | ||
| * and a navigation requested before readiness hard-navigates). | ||
| * | ||
| * @default 3000 | ||
| */ | ||
| handshakeTimeoutMs?: number; | ||
| } | ||
| interface RemoteDockOptions { | ||
| /** | ||
| * How to pass the connection descriptor to the hosted page. | ||
| * | ||
| * - `'fragment'` (default): appended as a URL fragment. | ||
| * Not sent in HTTP requests or Referer headers — safest for auth tokens. | ||
| * - `'query'`: appended as a URL query parameter. Use when your hosting | ||
| * platform rewrites fragments or your SPA router repurposes the fragment | ||
| * for navigation. The token will appear in server access logs and | ||
| * outbound Referer headers. | ||
| * | ||
| * @default 'fragment' | ||
| */ | ||
| transport?: 'fragment' | 'query'; | ||
| /** | ||
| * Reject WS handshakes whose `Origin` header doesn't match the dock URL | ||
| * origin. Turn off when the same hosted app is served from multiple | ||
| * origins (e.g. preview deploys). | ||
| * | ||
| * @default true | ||
| */ | ||
| originLock?: boolean; | ||
| } | ||
| interface RemoteConnectionInfo extends ConnectionMeta { | ||
| backend: 'websocket'; | ||
| websocket: string; | ||
| v: 1; | ||
| authToken: string; | ||
| origin: string; | ||
| } | ||
| type DevframeViewLauncherStatus = 'idle' | 'loading' | 'success' | 'error'; | ||
| interface DevframeViewLauncher extends DevframeDockEntryBase { | ||
| type: 'launcher'; | ||
| launcher: { | ||
| icon?: DevframeDockEntryIcon; | ||
| title: string; | ||
| status?: DevframeViewLauncherStatus; | ||
| error?: string; | ||
| description?: string; | ||
| buttonStart?: string; | ||
| buttonLoading?: string; | ||
| /** | ||
| * Bound command id: the launch button, command palette entry, and any | ||
| * keybinding all resolve to this one handler. A viewer running out of | ||
| * process dispatches it over the `hub:commands:execute` RPC — the | ||
| * serializable path {@link onLaunch} can't cross, since a function is | ||
| * dropped when the entry is projected into the `devframe:docks` shared | ||
| * state. Register the command (with its handler) via `ctx.commands`. | ||
| */ | ||
| command?: string; | ||
| /** | ||
| * Id of the terminal session this launcher tracks (e.g. the one returned | ||
| * by `ctx.terminals.startChildProcess`). A viewer surfaces a first-class | ||
| * "view in terminal" action that calls `hub:docks:activate` with the | ||
| * terminals dock id and `{ sessionId: terminalSessionId }`, jumping the | ||
| * user straight to the running process. | ||
| */ | ||
| terminalSessionId?: string; | ||
| /** | ||
| * Latest single line of progress for inline display beneath the launcher | ||
| * (e.g. the tail of the tracked session's output). Author-set: the owner | ||
| * patches it via `docks.update()` as the process reports progress. | ||
| */ | ||
| digest?: string; | ||
| /** | ||
| * In-process launch handler. Optional: a same-process host can invoke it | ||
| * directly, but it does not survive projection into shared state, so an | ||
| * out-of-process viewer relies on {@link command} instead. Provide one or | ||
| * both. | ||
| */ | ||
| onLaunch?: () => Promise<void>; | ||
| }; | ||
| } | ||
| interface DevframeViewAction extends DevframeDockEntryBase { | ||
| type: 'action'; | ||
| action: ClientScriptEntry; | ||
| } | ||
| interface DevframeViewCustomRender extends DevframeDockEntryBase { | ||
| type: 'custom-render'; | ||
| renderer: ClientScriptEntry; | ||
| } | ||
| /** | ||
| * A view rendered natively by the viewer rather than by a plugin — the | ||
| * settings panel, the terminals feed, the messages feed, etc. A high-level | ||
| * integration registers the built-in views it wants; the viewer recognizes the | ||
| * reserved `id` and renders its own UI for it. | ||
| * | ||
| * Its {@link DevframeDockEntryBase.category} defaults to `'~builtin'` when | ||
| * omitted, so built-in views group together and sort last without every | ||
| * integration repeating it. | ||
| */ | ||
| interface DevframeViewBuiltin extends DevframeDockEntryBase { | ||
| type: '~builtin'; | ||
| id: string; | ||
| } | ||
| /** | ||
| * @deprecated json-render moved out of the hub into the opt-in | ||
| * `@devframes/json-render` integration in 0.7, which contributes its own | ||
| * `'json-render'` entry (carrying a serializable view ref, not a live | ||
| * `JsonRenderer` handle) to {@link DevframeDockEntryRegistry} via declaration | ||
| * merging. This type is kept for compatibility but is no longer a member of | ||
| * {@link DevframeDockUserEntry} — use `@devframes/json-render/hub` instead. | ||
| * Removed in 0.8. | ||
| */ | ||
| interface DevframeViewJsonRender extends DevframeDockEntryBase { | ||
| type: 'json-render'; | ||
| /** JsonRenderer handle created by the deprecated ctx.createJsonRenderer() */ | ||
| ui: JsonRenderer; | ||
| } | ||
| /** | ||
| * A dock group: a single dock-bar button that collapses every entry whose | ||
| * {@link DevframeDockEntryBase.groupId} matches this group's `id`. | ||
| * | ||
| * A group carries its own `title`/`icon`/`category`/`defaultOrder`/`when` | ||
| * (inherited from {@link DevframeDockEntryBase}) and has no view payload of its | ||
| * own — hosts render its members in a popover / sub-navigation. It flows | ||
| * through the same `register`/`update`/`values` machinery as every other entry, | ||
| * keyed by `id`. | ||
| * | ||
| * The group's `category` is the OUTER bucket for the group button itself AND | ||
| * for every one of its members — a member's own `category` no longer decides | ||
| * its outer bucket, but is reinterpreted as an in-group sub-category that | ||
| * sub-divides and sorts members inside this group. A group with no `category` | ||
| * buckets itself and its members under `'default'`. | ||
| * | ||
| * Grouping is one level deep: a group entry must not itself set `groupId`. | ||
| */ | ||
| interface DevframeViewGroup extends DevframeDockEntryBase { | ||
| type: 'group'; | ||
| /** | ||
| * Member id auto-opened when the group button is activated. When unset, | ||
| * activating the group only reveals its members (popover-only); no view | ||
| * opens until a member is chosen. | ||
| */ | ||
| defaultChildId?: string; | ||
| /** | ||
| * Per-group override of the in-group sub-category ordering — a map of | ||
| * sub-category id → ordering weight (lower sorts earlier), mirroring the | ||
| * shape of {@link import('../constants').DEFAULT_CATEGORIES_ORDER}. | ||
| * | ||
| * A member's own {@link DevframeDockEntryBase.category} is reinterpreted as | ||
| * its IN-GROUP sub-category, and members are sub-divided and sorted by those | ||
| * sub-categories. By default that sort follows the hub-wide | ||
| * `DEFAULT_CATEGORIES_ORDER`; set this to reorder the sub-categories **inside | ||
| * this group only**, leaving the outer dock-bar ordering (and every other | ||
| * group) untouched. | ||
| * | ||
| * Keys are merged over the defaults, so you only list the sub-categories you | ||
| * want to move; any sub-category absent from the map keeps its default weight | ||
| * (falling back to `0`). | ||
| * | ||
| * @example | ||
| * ```ts | ||
| * // In the "nuxt" group, surface `app` tools before `framework` internals. | ||
| * { type: 'group', id: 'nuxt', categoryOrder: { app: -200 } } | ||
| * ``` | ||
| */ | ||
| categoryOrder?: Record<string, number>; | ||
| /** | ||
| * Optional accent color for the group button. When set, the viewer may use | ||
| * it to style the group button and/or its popover. When unset, the viewer | ||
| * falls back to its own default styling. | ||
| */ | ||
| accentColor?: string; | ||
| } | ||
| /** | ||
| * The **open** registry of dock entry variants, keyed by their `type` | ||
| * discriminator. The hub ships the framework-neutral built-ins; opt-in | ||
| * integrations contribute their own variants through declaration merging — | ||
| * e.g. `@devframes/json-render/hub` adds a `'json-render'` entry. The hub | ||
| * itself stays agnostic: it hard-codes no integration-specific variant. | ||
| * | ||
| * @example | ||
| * ```ts | ||
| * // in an opt-in integration package | ||
| * declare module '@devframes/hub/types' { | ||
| * interface DevframeDockEntryRegistry { | ||
| * 'my-view': MyDockEntry | ||
| * } | ||
| * } | ||
| * ``` | ||
| */ | ||
| interface DevframeDockEntryRegistry { | ||
| 'iframe': DevframeViewIframe; | ||
| 'action': DevframeViewAction; | ||
| 'custom-render': DevframeViewCustomRender; | ||
| 'launcher': DevframeViewLauncher; | ||
| 'group': DevframeViewGroup; | ||
| '~builtin': DevframeViewBuiltin; | ||
| } | ||
| type DevframeDockUserEntry = DevframeDockEntryRegistry[keyof DevframeDockEntryRegistry]; | ||
| type DevframeDockEntry = DevframeDockUserEntry; | ||
| type DevframeDockEntriesGrouped = [category: string, entries: DevframeDockEntry[]][]; | ||
| //#endregion | ||
| //#region src/types/commands.d.ts | ||
| interface DevframeCommandKeybinding { | ||
| /** | ||
| * Keyboard shortcut string. | ||
| * Use "Mod" for platform-aware modifier (Cmd on macOS, Ctrl elsewhere). | ||
| * Examples: "Mod+K", "Mod+Shift+P", "Alt+N" | ||
| */ | ||
| key: string; | ||
| } | ||
| interface DevframeCommandBase { | ||
| /** | ||
| * Unique namespaced ID, e.g. "vite:open-in-editor" | ||
| */ | ||
| id: string; | ||
| title: string; | ||
| description?: string; | ||
| /** | ||
| * Icon for the command. Either an Iconify icon string (e.g. "ph:pencil-duotone") | ||
| * or a theme-specific pair `{ light, dark }` — the same shape as dock icons. | ||
| */ | ||
| icon?: DevframeDockEntryIcon; | ||
| category?: string; | ||
| /** | ||
| * Whether to show in command palette. Default: true | ||
| * | ||
| * - `true` — show the command and flatten its children into search results | ||
| * - `false` — hide the command entirely from the palette | ||
| * - `'without-children'` — show the command but don't flatten children into top-level search (children are still accessible via drill-down) | ||
| */ | ||
| showInPalette?: boolean | 'without-children'; | ||
| /** | ||
| * Optional context expression for conditional visibility. | ||
| * When set, the command is only shown in the palette and only executable | ||
| * when the expression evaluates to true. | ||
| */ | ||
| when?: string; | ||
| /** | ||
| * Default keyboard shortcut(s) for this command | ||
| */ | ||
| keybindings?: DevframeCommandKeybinding[]; | ||
| } | ||
| /** | ||
| * Opt-in agent exposure for a server command — mirrors the `agent` field on | ||
| * `defineRpcFunction`. A command carrying this field (and a `handler`) is | ||
| * projected into `ctx.agent` as a callable tool, reaching MCP clients through | ||
| * the devframe MCP adapter. | ||
| * | ||
| * `when` clauses are evaluated client-side only and are **not** enforced for | ||
| * agent calls — opt in a `when`-gated command only if running it outside its | ||
| * UI context is safe. | ||
| * | ||
| * @experimental The agent-native surface is experimental and may change | ||
| * without a major version bump until it stabilizes. | ||
| */ | ||
| interface DevframeCommandAgentOptions { | ||
| /** | ||
| * Description shown to the agent. Write it as a prompt: state when to call | ||
| * the command, not just what it does. | ||
| */ | ||
| description: string; | ||
| /** Display title (falls back to the command's `title`). */ | ||
| title?: string; | ||
| /** | ||
| * Safety classification — drives MCP hint annotations. | ||
| * @default 'action' | ||
| */ | ||
| safety?: 'read' | 'action' | 'destructive'; | ||
| /** Free-form tags for grouping/filtering. */ | ||
| tags?: readonly string[]; | ||
| /** | ||
| * Positional [Standard Schema](https://standardschema.dev/) validators for | ||
| * the handler's arguments — the same shape RPC definitions carry (valibot, | ||
| * zod, arktype, devframe's built-in `s` builder, …). Each is advertised | ||
| * under `arg0` / `arg1` / … on the tool's JSON-Schema input. Omitted: the | ||
| * tool takes no arguments. | ||
| */ | ||
| args?: readonly StandardSchemaV1[]; | ||
| } | ||
| /** | ||
| * Server command input — what plugins pass to `ctx.commands.register()`. | ||
| */ | ||
| interface DevframeServerCommandInput extends DevframeCommandBase { | ||
| /** | ||
| * Handler for this command. Optional if the command only serves as a group for children. | ||
| */ | ||
| handler?: (...args: any[]) => any | Promise<any>; | ||
| /** | ||
| * Opt this command in to the agent surface (`ctx.agent` → MCP). Requires a | ||
| * `handler`. See {@link DevframeCommandAgentOptions}. | ||
| * | ||
| * @experimental | ||
| */ | ||
| agent?: DevframeCommandAgentOptions; | ||
| /** | ||
| * Static sub-commands. Two levels max (parent → children). | ||
| * Each child must have a globally unique `id`. | ||
| */ | ||
| children?: DevframeServerCommandInput[]; | ||
| } | ||
| /** | ||
| * Serializable server command entry — sent over RPC (no handler). | ||
| */ | ||
| interface DevframeServerCommandEntry extends DevframeCommandBase { | ||
| source: 'server'; | ||
| children?: DevframeServerCommandEntry[]; | ||
| } | ||
| /** | ||
| * Client command — registered in the webcomponent context. | ||
| */ | ||
| interface DevframeClientCommand extends DevframeCommandBase { | ||
| source: 'client'; | ||
| /** | ||
| * Action for this command. Optional if the command only serves as a group for children. | ||
| * Return sub-commands for dynamic nested palette menus (runtime submenus). | ||
| */ | ||
| action?: (...args: any[]) => void | DevframeClientCommand[] | Promise<void | DevframeClientCommand[]>; | ||
| /** | ||
| * Static sub-commands. Two levels max (parent → children). | ||
| */ | ||
| children?: DevframeClientCommand[]; | ||
| } | ||
| /** | ||
| * Union of command entries visible in the palette. | ||
| */ | ||
| type DevframeCommandEntry = DevframeServerCommandEntry | DevframeClientCommand; | ||
| interface DevframeCommandHandle { | ||
| readonly id: string; | ||
| update: (patch: Partial<Omit<DevframeServerCommandInput, 'id'>>) => void; | ||
| unregister: () => void; | ||
| } | ||
| interface DevframeCommandsHostEvents { | ||
| 'command:registered': (command: DevframeServerCommandEntry) => void; | ||
| 'command:unregistered': (id: string) => void; | ||
| } | ||
| interface DevframeCommandsHost { | ||
| readonly commands: Map<string, DevframeServerCommandInput>; | ||
| readonly events: EventEmitter<DevframeCommandsHostEvents>; | ||
| /** | ||
| * Register a command (with optional children). | ||
| */ | ||
| register: (command: DevframeServerCommandInput) => DevframeCommandHandle; | ||
| /** | ||
| * Unregister a command by ID (removes parent and all children). | ||
| */ | ||
| unregister: (id: string) => boolean; | ||
| /** | ||
| * Execute a command by ID. Searches top-level and children. | ||
| * Throws if not found or if command has no handler. | ||
| */ | ||
| execute: (id: string, ...args: any[]) => Promise<unknown>; | ||
| /** | ||
| * Returns serializable list (no handlers), preserving tree structure. | ||
| */ | ||
| list: () => DevframeServerCommandEntry[]; | ||
| } | ||
| interface DevframeCommandShortcutOverrides { | ||
| /** | ||
| * Command ID → keybinding overrides. Empty array = shortcut disabled. | ||
| */ | ||
| [commandId: string]: DevframeCommandKeybinding[]; | ||
| } | ||
| //#endregion | ||
| //#region src/types/settings.d.ts | ||
| interface DevframeDocksUserSettings { | ||
| docksHidden: string[]; | ||
| docksCategoriesHidden: string[]; | ||
| docksPinned: string[]; | ||
| docksCustomOrder: Record<string, number>; | ||
| showIframeAddressBar: boolean; | ||
| closeOnOutsideClick: boolean; | ||
| commandShortcuts: DevframeCommandShortcutOverrides; | ||
| } | ||
| //#endregion | ||
| export { DevframeViewLauncher as A, DevframeDocksHost as C, DevframeViewGroup as D, DevframeViewCustomRender as E, RemoteDockOptions as F, JsonRenderElement as I, JsonRenderSpec as L, FrameSubTabsConfig as M, NavTarget as N, DevframeViewIframe as O, RemoteConnectionInfo as P, JsonRenderer as R, DevframeDocksActiveState as S, DevframeViewBuiltin as T, DevframeDockEntryBase as _, DevframeCommandEntry as a, DevframeDockEntryRegistry as b, DevframeCommandShortcutOverrides as c, DevframeServerCommandEntry as d, DevframeServerCommandInput as f, DevframeDockEntry as g, DevframeDockEntriesGrouped as h, DevframeCommandBase as i, DevframeViewLauncherStatus as j, DevframeViewJsonRender as k, DevframeCommandsHost as l, DevframeDockActivation as m, DevframeClientCommand as n, DevframeCommandHandle as o, ClientScriptEntry as p, DevframeCommandAgentOptions as r, DevframeCommandKeybinding as s, DevframeDocksUserSettings as t, DevframeCommandsHostEvents as u, DevframeDockEntryCategory as v, DevframeViewAction as w, DevframeDockUserEntry as x, DevframeDockEntryIcon as y }; |
Sorry, the diff of this file is too big to display
Network access
Supply chain riskThis module accesses the network.
Found 2 instances
Environment variable access
Supply chain riskPackage accesses environment variables, which may be a sign of credential stuffing or data theft.
Found 3 instances
Filesystem access
Supply chain riskAccesses the file system, and could potentially read sensitive data.
Found 2 instances
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 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.
402029
9.8%22
29.41%8181
7.59%10
25%6
20%15
87.5%3
200%+ Added
+ Added
+ Added
- Removed